Merge branch 'develop' into bugfix/editor-tooltips

This commit is contained in:
Hyper_ 2025-04-04 19:52:33 -03:00 committed by GitHub
commit 17a553f75a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
220 changed files with 6344 additions and 3687 deletions
.github
.gitignore
.vscode
CHANGELOG.mdartassets
build
docs
hmm.jsonproject.hxp
source
Prebuild.hx
funkin
InitState.hxPlayerSettings.hxPreferences.hx
api
audio
data
effects
graphics
input
modding
play

View file

@ -6,7 +6,7 @@ body:
- type: markdown
attributes:
value: "# PLEASE READ THE [CONTRIBUTING GUIDE](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md) BEFORE OPENING ISSUES!"
- type: checkboxes
attributes:
label: Issue Checklist
@ -45,11 +45,11 @@ body:
- type: input
attributes:
label: Version
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
placeholder: ex. 0.5.3
validations:
required: true
- type: markdown
attributes:
value: "## Describe your bug."
@ -61,11 +61,11 @@ body:
- type: textarea
attributes:
label: Description (include any images, videos, errors, or crash logs)
description: Provide as much detail as you can. The better others understand your issue, the more they can help you!
description: Provide as much detail as you can. The better others understand your issue, the more they can help you!
placeholder: Describe your issue here...
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce

View file

@ -5,7 +5,7 @@ inputs:
haxe:
description: 'Version of haxe to install'
required: true
default: '4.3.4'
default: '4.3.6'
hxcpp-cache:
description: 'Whether to use a shared hxcpp compile cache'
required: true
@ -30,7 +30,7 @@ runs:
echo "TIMER_HAXE=$(date +%s)" >> "$GITHUB_ENV"
- name: Install Haxe
uses: funkincrew/ci-haxe@v3.1.0
uses: funkincrew/ci-haxe@v3.2.6
with:
haxe-version: ${{ inputs.haxe }}

View file

@ -1,53 +0,0 @@
name: Create and publish Docker image
on:
workflow_dispatch:
push:
paths:
- '**/Dockerfile'
- '.github/workflows/build-docker-image.yml'
jobs:
build-and-push-image:
runs-on: build-set
permissions:
contents: read
packages: write
steps:
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: false
token: ${{ steps.app_token.outputs.token }}
- name: Log into GitHub Container Registry
uses: docker/login-action@v3.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5.3.0
with:
context: ./build
push: true
tags: |
ghcr.io/funkincrew/build-dependencies:latest
ghcr.io/funkincrew/build-dependencies:${{ github.sha }}
labels: |
org.opencontainers.image.description=precooked haxe build-dependencies
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.title=${{ github.repository_owner }}/build-dependencies
org.opencontainers.image.url=https://github.com/${{ github.repository }}
org.opencontainers.image.version=${{ github.sha }}

View file

@ -12,146 +12,238 @@ on:
description: Save the build artifact to Github Actions (sends to itch otherwise)
default: false
push:
paths-ignore:
- '**/Dockerfile'
- '.github/workflows/build-docker-image.yml'
jobs:
gather-changes:
runs-on: build-set
outputs:
trigger-build: ${{ steps.should-trigger.outputs.result }}
steps:
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: false
- uses: dorny/paths-filter@v3
id: filter
with:
base: ${{ github.ref }}
filters: |
docker:
- '.github/workflows/build-game.yml'
- '**/Dockerfile'
- uses: actions/github-script@v7
id: should-trigger
with:
result-encoding: string
script: |
const { payload } = context
const changes = ${{ steps.filter.outputs.changes }}
const manual = payload.commits
.some(c => c.message.toLowerCase().replace(/\W/g, " ").includes("docker rebuild"))
console.log({ payload, changes, manual, commits: payload.commits })
return payload.created
|| payload.forced
|| changes.includes("docker")
|| manual
gather-tags:
runs-on: build-set
outputs:
primary: ${{ steps.build-tags.outputs.primary }}
list: ${{ steps.build-tags.outputs.list }}
steps:
- name: Gather build tags
uses: actions/github-script@v7
id: build-tags
with:
script: |
const base = "ghcr.io/funkincrew/build-dependencies"
const default_branch = "rewrite/master"
const ref_path = context.ref.split("/").slice(2)
const ref = ref_path.join("/")
const ref_tag = ref_path.join("-")
const tags = [ref_tag, context.sha]
if (ref === default_branch) tags.push("latest")
console.log([
`ref: ${ref}`,
`default_branch: ${default_branch}`,
`tags: ${tags.join(", ")}`
].join('\n'))
const tag_list = tags
.map(tag => `${base}:${tag}`)
.join("\n")
core.setOutput("primary", ref_tag)
core.setOutput("list", tag_list)
docker-image:
needs: [gather-changes, gather-tags]
if: needs.gather-changes.outputs.trigger-build == 'true'
runs-on: build-set
permissions:
contents: read
packages: write
steps:
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: false
- name: Log into GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: ./build
push: true
tags: ${{ needs.gather-tags.outputs.list }}
labels: |
org.opencontainers.image.description=precooked haxe build dependencies
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.title=${{ github.repository_owner }}/build-dependencies
org.opencontainers.image.url=https://github.com/${{ github.repository }}
org.opencontainers.image.version=${{ github.sha }}
build-game-on-host:
strategy:
matrix:
include:
- target: windows
- target: macos
- target: windows
runs-on: windows-latest
- target: macos
runs-on: macos
runs-on:
- ${{ matrix.target }}
- ${{ matrix.runs-on }}
defaults:
run:
shell: bash
env:
BUILD_DEFINES: ${{ github.event.inputs.build-defines || '-DGITHUB_BUILD' }}
steps:
- name: Make git happy
run: |
git config --global --replace-all safe.directory $GITHUB_WORKSPACE
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
persist-credentials: false
- name: Setup build environment
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: |
haxelib run lime build windows -v -release ${{ github.event.inputs.build-defines }}
timeout-minutes: 120
- name: Build game
if: ${{ matrix.target != 'windows' }}
run: |
haxelib run lime build ${{ matrix.target }} -v -release --times ${{ github.event.inputs.build-defines }}
timeout-minutes: 120
- name: Save build artifact to Github Actions
if: ${{ github.event.inputs.save-artifact }}
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.target }}
path: export/release/${{matrix.target}}/bin/
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
target: ${{ matrix.target }}
- name: Make git happy
run: |
git config --global --replace-all safe.directory $GITHUB_WORKSPACE
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
persist-credentials: false
- name: Setup build environment
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 (windows)
if: ${{ matrix.target == 'windows' }}
run: |
haxelib run lime build windows -v -release $BUILD_DEFINES
timeout-minutes: 120
- name: Build game (unix)
if: ${{ matrix.target != 'windows' }}
run: |
haxelib run lime build ${{ matrix.target }} -v -release --times $BUILD_DEFINES
timeout-minutes: 120
- name: Save build artifact to Github Actions
if: ${{ github.event.inputs.save-artifact }}
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.target }}
path: export/release/${{matrix.target}}/bin/
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
target: ${{ matrix.target }}
build-game-in-container:
needs: [gather-tags, docker-image]
if: ${{ ! cancelled() }}
runs-on: build-set
container: ghcr.io/funkincrew/build-dependencies:latest
container: ghcr.io/funkincrew/build-dependencies:${{ needs.gather-tags.outputs.primary }}
strategy:
matrix:
include:
- target: linux
- target: html5
- target: linux
- target: html5
defaults:
run:
shell: bash
env:
BUILD_DEFINES: ${{ github.event.inputs.build-defines || '-DGITHUB_BUILD' }}
steps:
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
persist-credentials: false
- name: Config haxelib
run: |
haxelib --never newrepo
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
- name: Restore cached dependencies
id: cache-hmm
uses: actions/cache@v4
with:
path: .haxelib
key: haxe-hmm-${{ runner.os }}-${{ hashFiles('**/hmm.json') }}
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
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
uses: actions/cache@v4
with:
path: /usr/share/hxcpp
key: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}
- name: Build game
run: |
haxelib run lime build ${{ matrix.target }} -v -release --times ${{ github.event.inputs.build-defines }}
timeout-minutes: 120
- name: Save build artifact to Github Actions
if: ${{ github.event.inputs.save-artifact }}
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.target }}
path: export/release/${{matrix.target}}/bin/
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
target: ${{ matrix.target }}
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
persist-credentials: false
- name: Config haxelib
run: |
haxelib --never newrepo
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
- name: Restore cached dependencies
id: cache-hmm
uses: actions/cache@v4
with:
path: .haxelib
key: haxe-hmm-${{ runner.os }}-${{ hashFiles('**/hmm.json') }}
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
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
uses: actions/cache@v4
with:
path: /usr/share/hxcpp
key: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}
- name: Build game
run: |
haxelib run lime build ${{ matrix.target }} -v -release --times $BUILD_DEFINES
timeout-minutes: 120
- name: Save build artifact to Github Actions
if: ${{ github.event.inputs.save-artifact }}
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.target }}
path: export/release/${{matrix.target}}/bin/
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
target: ${{ matrix.target }}

View file

@ -1,22 +0,0 @@
name: "Issue Labeler"
on:
issues:
types:
- opened
- reopened
- edited
jobs:
# 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.

1
.gitignore vendored
View file

@ -8,6 +8,7 @@ RECOVER_*.fla
shitAudio/
.build_time
.swp
NewgroundsCredentials.hx
# Exclude JS stuff
node_modules/

View file

@ -6,7 +6,6 @@
"vshaxe.hxcpp-debugger", // CPP debugging
"openfl.lime-vscode-extension", // Lime integration
"esbenp.prettier-vscode", // JSON formatting
"redhat.vscode-xml", // XML formatting
"ryanluker.vscode-coverage-gutters" // Highlight code coverage
"redhat.vscode-xml" // XML formatting
]
}

65
.vscode/settings.json vendored
View file

@ -70,7 +70,7 @@
"files.eol": "\n",
"haxe.displayPort": "auto",
"haxe.enableCompilationServer": false,
"haxe.enableCompilationServer": true,
"haxe.enableServerView": true,
"haxe.displayServer": {
"arguments": ["-v"]
@ -91,26 +91,6 @@
"haxecheckstyle.codeSimilarityBufferSize": 100,
"lime.targetConfigurations": [
{
"label": "Windows / Debug",
"target": "windows",
"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", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug",
"target": "hl",
"args": ["-debug"]
},
{
"label": "Windows / Debug (Discord)",
"target": "windows",
@ -170,23 +150,13 @@
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Chart Editor)",
"target": "hl",
"args": ["-debug", "-DCHARTING"]
},
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
"args": ["-debug", "-DANIMDEBUG", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Debug hxCodec)",
"label": "Windows / Debug (Debug hxvlc)",
"target": "windows",
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
@ -210,11 +180,6 @@
"target": "windows",
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Release",
"target": "windows",
"args": ["-release"]
},
{
"label": "Windows / Release (GitHub Actions)",
"target": "windows",
@ -225,29 +190,31 @@
"target": "hl",
"args": ["-debug", "-DWAVEFORM"]
},
{
"label": "HTML5 / Debug",
"target": "html5",
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
"args": ["-debug", "-watch", "-DFEATURE_DEBUG_FUNCTIONS"]
},
}
],
"lime.buildTypes": [
{
"label": "macOS / Debug",
"target": "mac",
"label": "Debug",
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "macOS / Release",
"target": "mac",
"label": "Debug (Tracy)",
"args": ["-debug", "-DFEATURE_DEBUG_TRACY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Debug (Straight to Chart Editor)",
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Release",
"args": ["-release"]
},
{
"label": "macOS / Release (GitHub Actions)",
"target": "mac",
"label": "Release (GitHub Actions)",
"args": ["-release", "-DGITHUB_BUILD"]
}
],

File diff suppressed because it is too large Load diff

2
art

@ -1 +1 @@
Subproject commit fbd3e3df77734606d88516770b71b56e6fa04bce
Subproject commit 78dc310219370144719b4eeef9b3b511c5a44532

2
assets

@ -1 +1 @@
Subproject commit c1899ffbefb9a7c98b030c75a33623431d7ea6ba
Subproject commit 0303a03f43b078d5263b541de1ff504fd7cfcde5

View file

@ -1,8 +1,8 @@
FROM ubuntu:mantic
FROM ubuntu:noble
ARG haxe_version=4.3.4
ARG haxe_version=4.3.6
ARG haxelib_version=4.1.0
ARG neko_version=2.3.0
ARG neko_version=2.4.0
# prepare runner
ENV GITHUB_HOME="/github/home"
@ -98,11 +98,12 @@ EOF
# neko
# https://github.com/HaxeFoundation/neko/releases/download/v2-3-0/neko-2.3.0-linux64.tar.gz
RUN <<EOF
neko_url=$(curl https://api.github.com/repos/HaxeFoundation/neko/releases -sfL \
| jq '.[] | select(.name == "'"$neko_version"'")' \
| jq '.assets[] | select(.name | endswith("linux64.tar.gz"))' \
| jq -r '.browser_download_url')
curl -sfL "$neko_url" | tar -xz -C /usr/local
#neko_url=$(curl https://api.github.com/repos/HaxeFoundation/neko/releases -fL \
# | jq '.[] | select(.name == "'"$neko_version"'")' \
# | jq '.assets[] | select(.name | endswith("linux64.tar.gz"))' \
# | jq -r '.browser_download_url')
neko_url="https://geo.thei.rs/funkin/neko-2.4.0-linux64.tar.gz"
curl -fL "$neko_url" | tar -xz -C /usr/local
EOF
RUN <<EOF
@ -117,11 +118,12 @@ ENV PATH="$NEKOPATH:$PATH"
# haxe
# https://github.com/HaxeFoundation/haxe/releases/download/4.0.5/haxe-4.0.5-linux64.tar.gz
RUN <<EOF
haxe_url=$(curl https://api.github.com/repos/HaxeFoundation/haxe/releases -sfL \
| jq '.[] | select(.name == "'"$haxe_version"'")' \
| jq '.assets[] | select(.name | endswith("linux64.tar.gz"))' \
| jq -r '.browser_download_url')
curl -sfL "$haxe_url" | tar -xz -C /usr/local
#haxe_url=$(curl https://api.github.com/repos/HaxeFoundation/haxe/releases -fL \
# | jq '.[] | select(.name == "'"$haxe_version"'")' \
# | jq '.assets[] | select(.name | endswith("linux64.tar.gz"))' \
# | jq -r '.browser_download_url')
haxe_url="https://geo.thei.rs/funkin/haxe-4.3.6-linux64.tar.gz"
curl -fL "$haxe_url" | tar -xz -C /usr/local
EOF
RUN <<EOF

View file

@ -13,7 +13,7 @@
- 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
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:
@ -21,6 +21,8 @@
- Windows 10/11 SDK
- 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/)
- Note: Funkin's fork currently doesn't come with the necessary binaries so you'll have to rebuild Lime. See [Troubleshooting](TROUBLESHOOTING.md#lime-related-issues).
- One of Funkin's dependencies uses libVLC, which requires you to install some packages to be able to compile: `sudo apt install libvlc-dev libvlccore-dev libvlccore9`
- HTML5: Compiles without any extra setup
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`)

10
docs/COMPILING_MAC.md Normal file
View file

@ -0,0 +1,10 @@
# Mac Compiling Guide + Considerations
There's a few extra considerations when compiling FNF for Mac that *we* have to handle when creating a wider release.
- [Creating a Universal Binary](#creating-a-universal-binary)
- Code-signing
- Notarizing
## Creating a Universal Binary
Run the `art/macos-universal.sh` script, which automatically compiles release versions of both arm64 and x86 of Funkin. You can also see there for reference of how it's done.

View file

@ -1,6 +1,50 @@
# Contributing
Welcome to the Contributing Guide!
You can contribute to the Funkin' repository by opening issues or pull requests. This guide will cover best practices for each type of contribution.
You can contribute to the Funkin' repository by opening issues or pull requests.
This guide will cover best practices for each type of contribution.
# Table of Contents
<details open>
<summary><b>Contents</b></summary>
[Part 1: Etiquette](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#part-1-etiquette)
<details>
<summary><a href="https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#part-2-issues">Part 2: Issues</a></summary>
* [Requirements](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#requirements)
* [Rejected Features](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#rejected-features)
* [Issue Types](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#issue-types)
* [Before You Submit...](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#before-you-submit)
</details>
<details>
<summary><a href="https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#part-3-pull-requests">Part 3: Pull Requests</a></summary>
* [Choosing a base branch](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#choosing-a-base-branch)
* [Merge conflicts and rebasing](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#merge-conflicts-and-rebasing)
* [Code PRs](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#code-prs)
* [Documentation PRs](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#documentation-prs)
* [GitHub PRs](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#github-prs)
* [funkin.assets PRs](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#funkinassets-prs)
</details>
[Closing](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#closing)
</details>
# Part 1: Etiquette
- Be respectful to one another. We're here to help each other out!
@ -27,13 +71,21 @@ Here's a list of commonly suggested features and the reasons why they won't be a
| Combo Break + Accuracy Displays | https://github.com/FunkinCrew/Funkin/pull/2681#issuecomment-2156308982 |
| Toggleable Ghost Tapping | https://github.com/FunkinCrew/Funkin/pull/2564#issuecomment-2119701802 |
| Perfectly Centered Strumlines | _same as above^_ |
| MultiKey, 9k, More than 4 keys, etc. | https://github.com/FunkinCrew/Funkin/issues/4243#issuecomment-2692371969 |
| Losing Icons for DD and Parents | https://github.com/FunkinCrew/Funkin/issues/3048#issuecomment-2243491536 |
| Playable GF / Speaker BF / Speaker Pico | https://github.com/FunkinCrew/Funkin/issues/2953#issuecomment-2216985230 |
| Fresh (Chill Mix) as Title Screen Music | https://github.com/FunkinCrew/Funkin/pull/4282#issuecomment-2709334718 |
| Adjusted Difficulty Ratings | https://github.com/FunkinCrew/Funkin/issues/2781#issuecomment-2172053144 |
| Countdown after Unpausing | https://github.com/FunkinCrew/Funkin/issues/2721#issuecomment-2159330106 |
| Difficulty Ratings above 20 | https://github.com/FunkinCrew/Funkin/issues/3075#issuecomment-2368984497 |
| Ability to Reset a Song's Score | https://github.com/FunkinCrew/Funkin/issues/3916#issuecomment-2525408261 |
| Quick Restart Keybind (not R) | https://github.com/FunkinCrew/Funkin/issues/3268#issuecomment-2351095232 |
| Countdown after Unpausing Song | https://github.com/FunkinCrew/Funkin/issues/2721#issuecomment-2159330106 |
| 4:3 Aspect Ratio for Week 6 | https://github.com/FunkinCrew/Funkin/issues/3840#issuecomment-2689158438 |
| "Philly Glow" Effect from Psych Engine | https://github.com/FunkinCrew/Funkin/issues/3788#issuecomment-2688966982 |
| Importing Charts from Psych Engine (and other mod content) | https://github.com/FunkinCrew/Funkin/issues/2586#issuecomment-2125733327 |
| Backwards Compatibility for Modding | https://github.com/FunkinCrew/Funkin/issues/3949#issuecomment-2608391329 |
| Lua Support | https://github.com/FunkinCrew/Funkin/issues/2643#issuecomment-2143718093 |
| Lua Support | https://github.com/FunkinCrew/Funkin/issues/2643#issuecomment-2143718093 |
## Issue Types
Choose the issue template that best suits your needs!
@ -192,7 +244,9 @@ Here are some guidelines for writing comments in your code:
## Documentation PRs
Documentation-based PRs make changes such as **fixing typos** or **adding new information** in documentation files.
This involves modifying one or several of the repositorys `.md` files, found throughout the repository.
Make sure your changes are easy to understand and formatted consistently to maximize clarity and readability.
> [!CAUTION]
@ -229,16 +283,21 @@ Make sure your changes are easy to understand and formatted consistently to maxi
## GitHub PRs
GitHub-related PRs make changes such as **tweaking Issue Templates** or **updating the repositorys workflows**.
This involves modifying one or several of the repositorys `.yml` files, or any other file in the `.github` folder.
Please test these changes on your forks main branch to avoid breaking anything in this repository (e.g. GitHub Actions, issue templates, etc.)!
## funkin.assets PRs
The `assets` submodule has its own repository called [funkin.assets](https://github.com/FunkinCrew/funkin.assets).
If you only modify files in the `assets` folder, open a PR in the `funkin.assets` repository instead of the main repository.
If you simultaneously modify files from both repositories, then open two separate PRs and explain the connection in your PR descriptions.
Be sure to choose `main` as the base branch for `funkin.assets` PRs, as no `develop` branch exists for that repository.
# Closing
Thank you for reading the Contributing Guide.
We look forward to seeing your contributions to the game!

View file

@ -7,6 +7,8 @@ Most of this functionality is only available on debug builds of the game!
- `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-ALT-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace. (Only works in the Main Menu on debug builds).
- `CTRL-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
## **Play State**
@ -29,3 +31,8 @@ Most of this functionality is only available on debug builds of the game!
## **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.
- `CTRL-ALT-SHIFT-M`: ***NO MORE ACCESS***: Re-locks all songs in Freeplay except those unlocked by default. Only available on debug builds.
- `CTRL-ALT-SHIFT-R`: ***GREAT SCORE?***: Give the user a hypothetical overridden score, and see if we can maintain that golden P rank. Only available on debug builds.
- `CTRL-ALT-SHIFT-P`: ***CHARACTER UNLOCK SCREEN***: Forces the Character Select screen to play Pico's unlocking animation. Only available on debug builds.
- `CTRL-ALT-SHIFT-N`: ***CHARACTER NOT SEEN***: Marks all characters as not seen and enables BF's new character unlocked animation in Freeplay. Only available on debug builds.
- `CTRL-ALT-SHIFT-E`: ***DUMP SAVE DATA***: Prompts the user to save their save data as a JSON file, so its contents can be viewed. Only available on debug builds.

View file

@ -29,3 +29,22 @@
- 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**.
## Lime Related Issues
- Segmentation fault and/or crash after `Done mapping time changes: [SongTimeChange(0ms,102bpm)]`
- Caused by using official Lime instead of Funkin's fork. Reinstalling Lime should fix it.
(NOTE: Make sure you do this via `hmm` (e.g `hmm reinstall -f lime`) to guarantee you get Funkin's version of Lime.)
- `Uncaught exception - Could not find lime.ndll.` ... `Advanced users may run "lime rebuild cpp" instead.`
- Usually specific to Linux. Running the commands below should fix it.
```
cd .haxelib/lime/git
git submodule init
git submodule sync
git submodule update
cd ../../..
# Note: The command and packages here might be different depending on your distro.
sudo apt install libgl1-mesa-dev libglu1-mesa-dev g++ g++-multilib gcc-multilib libasound2-dev libx11-dev libxext-dev libxi-dev libxrandr-dev libxinerama-dev libpulse-dev
lime rebuild cpp -64 -release -clean
```
For Windows or MacOS you can download pre-built binaries from [Funkin's Lime](https://github.com/FunkinCrew/lime/tree/dev-funkin/ndll).

View file

@ -4,21 +4,21 @@
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "a1eab7b9bf507b87200a3341719054fe427f3b15",
"ref": "41f35ddb1eb9d10bc742e6f8b5bcc62f9ef8ad84",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
},
{
"name": "flixel",
"type": "git",
"dir": null,
"ref": "ffa691cb2d2d81de35b900a4411e4062ac84ab58",
"ref": "fffb1a74cf08f63dacc2ab09976340563f5b6e6d",
"url": "https://github.com/FunkinCrew/flixel"
},
{
"name": "flixel-addons",
"type": "git",
"dir": null,
"ref": "7424db4e9164ff46f224a7c47de1b732d2542dd7",
"ref": "b9118f47f43a66bc0e5fbfcfd9903f0425e918ee",
"url": "https://github.com/FunkinCrew/flixel-addons"
},
{
@ -32,7 +32,7 @@
"name": "flxanimate",
"type": "git",
"dir": null,
"ref": "0654797e5eb7cd7de0c1b2dbaa1efe5a1e1d9412",
"ref": "713d3de0e566d6cd54cde13711ab3e4f60f59f4b",
"url": "https://github.com/Dot-Stuff/flxanimate"
},
{
@ -44,7 +44,7 @@
"name": "funkin.vis",
"type": "git",
"dir": null,
"ref": "22b1ce089dd924f15cdc4632397ef3504d464e90",
"ref": "1966f8fbbbc509ed90d4b520f3c49c084fc92fd6",
"url": "https://github.com/FunkinCrew/funkVis"
},
{
@ -63,7 +63,7 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "51c23588614397089a5ce182cddea729f0be6fa0",
"ref": "74ba53387eab0c4c2d3825e103fe70df8e46d9b1",
"url": "https://github.com/haxeui/haxeui-core"
},
{
@ -77,22 +77,15 @@
"name": "hscript",
"type": "git",
"dir": null,
"ref": "12785398e2f07082f05034cb580682e5671442a2",
"ref": "27c86f9a761c1d16d4433c4cf252eccb7b2e18de",
"url": "https://github.com/FunkinCrew/hscript"
},
{
"name": "hxCodec",
"type": "git",
"dir": null,
"ref": "61b98a7a353b7f529a8fec84ed9afc919a2dffdd",
"url": "https://github.com/FunkinCrew/hxCodec"
},
{
"name": "hxcpp",
"type": "git",
"dir": null,
"ref": "c6bac3d6c7d683f25104296b2f4c50f8c90b8349",
"url": "https://github.com/cortex-engine/hxcpp"
"ref": "v4.3.75",
"url": "https://github.com/HaxeFoundation/hxcpp"
},
{
"name": "hxcpp-debug-server",
@ -120,6 +113,11 @@
"type": "haxelib",
"version": "1.3.0"
},
{
"name": "hxvlc",
"type": "haxelib",
"version": "2.0.1"
},
{
"name": "json2object",
"type": "git",
@ -145,7 +143,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "fe3368f611a84a19afc03011353945ae4da8fffd",
"ref": "d1322e60f97b5c6e977f9e3e8a04f22b5190e7d9",
"url": "https://github.com/FunkinCrew/lime"
},
{
@ -176,11 +174,18 @@
"ref": "f61be7f7ba796595f45023ca65164a485aba0e7e",
"url": "https://github.com/FunkinCrew/MassiveUnit"
},
{
"name": "newgrounds",
"type": "git",
"dir": null,
"ref": "c8b30832027fa04f48fe6a45fa7903d5b265d56e",
"url": "https://github.com/Geokureli/Newgrounds/"
},
{
"name": "openfl",
"type": "git",
"dir": null,
"ref": "8306425c497766739510ab29e876059c96f77bd2",
"ref": "d061c936b462f040304ec2bd42d9f59d2e59e285",
"url": "https://github.com/FunkinCrew/openfl"
},
{

View file

@ -25,7 +25,7 @@ class Project extends HXProject {
* REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES!
* You only have to change it here, the rest of the game will query this value.
*/
static final VERSION:String = "0.5.3";
static final VERSION:String = "0.6.2";
/**
* The game's name. Used as the default window title.
@ -178,6 +178,33 @@ class Project extends HXProject {
*/
static final FEATURE_NEWGROUNDS:FeatureFlag = "FEATURE_NEWGROUNDS";
/**
* `-DFEATURE_NEWGROUNDS_DEBUG`
* If this flag is enabled, the game will enable Newgrounds.io's debug functions.
* This provides additional information in requests, as well as "faking" medal and leaderboard submissions.
*/
static final FEATURE_NEWGROUNDS_DEBUG:FeatureFlag = "FEATURE_NEWGROUNDS_DEBUG";
/**
* `-DFEATURE_NEWGROUNDS_AUTOLOGIN`
* If this flag is enabled, the game will attempt to automatically login to Newgrounds on startup.
*/
static final FEATURE_NEWGROUNDS_AUTOLOGIN:FeatureFlag = "FEATURE_NEWGROUNDS_AUTOLOGIN";
/**
* `-DFEATURE_NEWGROUNDS_TESTING_MEDALS`
* If this flag is enabled, use the medal IDs from the debug test bench.
* If disabled, use the actual medal IDs from the release project on Newgrounds.
*/
static final FEATURE_NEWGROUNDS_TESTING_MEDALS:FeatureFlag = "FEATURE_NEWGROUNDS_TESTING_MEDALS";
/**
* `-DFEATURE_NEWGROUNDS_EVENTS`
* If this flag is enabled, the game will attempt to send events to Newgrounds when the user does stuff.
* This lets us see cool anonymized stats! It only works if the user is logged in.
*/
static final FEATURE_NEWGROUNDS_EVENTS:FeatureFlag = "FEATURE_NEWGROUNDS_EVENTS";
/**
* `-DFEATURE_FUNKVIS`
* If this flag is enabled, the game will enable the Funkin Visualizer library.
@ -197,7 +224,7 @@ class Project extends HXProject {
/**
* `-DFEATURE_VIDEO_PLAYBACK`
* If this flag is enabled, the game will enable support for video playback.
* This requires the hxCodec library on desktop platforms.
* This requires the hxvlc library on desktop platforms.
*/
static final FEATURE_VIDEO_PLAYBACK:FeatureFlag = "FEATURE_VIDEO_PLAYBACK";
@ -232,6 +259,12 @@ class Project extends HXProject {
*/
static final FEATURE_STAGE_EDITOR:FeatureFlag = "FEATURE_STAGE_EDITOR";
/**
* `-DFEATURE_RESULTS_DEBUG
* If this flag is enabled, a debug menu for Results screen will be accessible from the debug menu.
*/
static final FEATURE_RESULTS_DEBUG:FeatureFlag = "FEATURE_RESULTS_DEBUG";
/**
* `-DFEATURE_POLYMOD_MODS`
* If this flag is enabled, the game will enable the Polymod library's support for atomic mod loading from the `./mods` folder.
@ -330,6 +363,8 @@ class Project extends HXProject {
this.window.hardware = true;
this.window.vsync = false;
// force / allow high DPI
this.window.allowHighDPI = true;
if (isWeb()) {
this.window.resizable = true;
@ -460,7 +495,6 @@ class Project extends HXProject {
// Should be false unless explicitly requested.
GITHUB_BUILD.apply(this, false);
FEATURE_NEWGROUNDS.apply(this, false);
FEATURE_GHOST_TAPPING.apply(this, false);
// Should be true unless explicitly requested.
@ -474,11 +508,18 @@ class Project extends HXProject {
// Should be true on debug builds or if GITHUB_BUILD is enabled.
FEATURE_DEBUG_FUNCTIONS.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this));
FEATURE_RESULTS_DEBUG.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this));
// Got a lot of complains about this being turned off by default on some builds.
// TODO: Look into ways to optimize logging (maybe by using a thread pool?)
FEATURE_LOG_TRACE.apply(this, true);
FEATURE_NEWGROUNDS.apply(this, true);
FEATURE_NEWGROUNDS_DEBUG.apply(this, false);
FEATURE_NEWGROUNDS_TESTING_MEDALS.apply(this, FEATURE_NEWGROUNDS.isEnabled(this) && FEATURE_DEBUG_FUNCTIONS.isEnabled(this));
FEATURE_NEWGROUNDS_AUTOLOGIN.apply(this, FEATURE_NEWGROUNDS.isEnabled(this) && isWeb());
FEATURE_NEWGROUNDS_EVENTS.apply(this, FEATURE_NEWGROUNDS.isEnabled(this));
// Should default to true on workspace builds and false on release builds.
REDIRECT_ASSETS_FOLDER.apply(this, isDebug() && isDesktop());
@ -579,7 +620,7 @@ class Project extends HXProject {
// Ensure all Flixel classes are available at runtime.
// Explicitly ignore packages which require additional dependencies.
addHaxeMacro("include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*' ])");
addHaxeMacro("include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*', 'flixel.addons.tile.FlxRayCastTilemap' ])");
}
/**
@ -593,7 +634,15 @@ class Project extends HXProject {
function configureOutputDir() {
// Set the output directory. Depends on the target platform and build type.
var buildDir = 'export/${isDebug() ? 'debug' : 'release'}/';
var buildDir = 'export/${isDebug() ? 'debug' : 'release'}';
// we use a dedicated 'tracy' folder, since it generally needs a recompile when in use
if (FEATURE_DEBUG_TRACY.isEnabled(this))
buildDir += "-tracy";
// trailing slash might not be needed, works fine on macOS without it, but I haven't tested on Windows!
buildDir += "/";
info('Output directory: $buildDir');
// setenv('BUILD_DIR', buildDir);
@ -650,9 +699,9 @@ class Project extends HXProject {
}
if (isDesktop() && !isHashLink() && FEATURE_VIDEO_PLAYBACK.isEnabled(this)) {
// hxCodec doesn't function on HashLink or non-desktop platforms
// hxvlc doesn't function on HashLink or non-desktop platforms
// It's also unnecessary if video playback is disabled
addHaxelib('hxCodec'); // Video playback
addHaxelib('hxvlc'); // Video playback
}
if (FEATURE_DISCORD_RPC.isEnabled(this)) {
@ -1041,15 +1090,7 @@ class Project extends HXProject {
* Display an info message. This should not interfere with the build process.
*/
public function info(message:String):Void {
// CURSED: We have to disable info() log calls because of a bug.
// https://github.com/haxelime/lime-vscode-extension/issues/88
// Log.info('[INFO] ${message}');
// trace(message);
// Sys.println(message);
// Sys.stdout().writeString(message);
// Sys.stderr().writeString(message);
if(command != "display") { Log.info('[INFO] ${message}'); }
}
}

View file

@ -9,10 +9,23 @@ class Prebuild
{
static inline final BUILD_TIME_FILE:String = '.build_time';
static final NG_CREDS_PATH:String = './source/funkin/api/newgrounds/NewgroundsCredentials.hx';
static final NG_CREDS_TEMPLATE:String = "package funkin.api.newgrounds;
class NewgroundsCredentials
{
public static final APP_ID:String = #if API_NG_APP_ID haxe.macro.Compiler.getDefine(\"API_NG_APP_ID\") #else 'INSERT APP ID HERE' #end;
public static final ENCRYPTION_KEY:String = #if API_NG_ENC_KEY haxe.macro.Compiler.getDefine(\"API_NG_ENC_KEY\") #else 'INSERT ENCRYPTION KEY HERE' #end;
}";
static function main():Void
{
saveBuildTime();
trace('Building...');
saveBuildTime();
buildCredsFile();
}
static function saveBuildTime():Void
@ -22,4 +35,22 @@ class Prebuild
fo.writeDouble(now);
fo.close();
}
static function buildCredsFile():Void
{
#if sys
if (sys.FileSystem.exists(NG_CREDS_PATH))
{
trace('NewgroundsCredentials.hx already exists, skipping.');
}
else
{
trace('Creating NewgroundsCredentials.hx...');
var fileContents:String = NG_CREDS_TEMPLATE;
sys.io.File.saveContent(NG_CREDS_PATH, fileContents);
}
#end
}
}

View file

@ -38,6 +38,9 @@ import openfl.display.BitmapData;
#if FEATURE_DISCORD_RPC
import funkin.api.discord.DiscordClient;
#end
#if FEATURE_NEWGROUNDS
import funkin.api.newgrounds.NewgroundsClient;
#end
/**
* A core class which performs initialization of the game.
@ -82,6 +85,10 @@ class InitState extends FlxState
// Disable the thing on Windows where it tries to send a bug report to Microsoft because why do they care?
WindowUtil.disableCrashHandler();
#if FEATURE_DEBUG_TRACY
funkin.util.WindowUtil.initTracy();
#end
// This ain't a pixel art game! (most of the time)
FlxSprite.defaultAntialiasing = true;
@ -117,8 +124,8 @@ class InitState extends FlxState
//
// NEWGROUNDS API SETUP
//
#if newgrounds
NGio.init();
#if FEATURE_NEWGROUNDS
NewgroundsClient.instance.init();
#end
//
@ -151,6 +158,7 @@ class InitState extends FlxState
#if FEATURE_SCREENSHOTS
funkin.util.plugins.ScreenshotPlugin.initialize();
#end
funkin.util.plugins.NewgroundsMedalPlugin.initialize();
funkin.util.plugins.EvacuateDebugPlugin.initialize();
funkin.util.plugins.ForceCrashPlugin.initialize();
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
@ -232,7 +240,7 @@ class InitState extends FlxState
storyMode: true,
title: "Cum Song Erect by Kawai Sprite",
songId: "cum",
characterId: "pico-playable",
characterId: "pico",
difficultyId: "nightmare",
isNewHighscore: true,
scoreData:
@ -248,9 +256,10 @@ class InitState extends FlxState
combo: 69,
maxCombo: 69,
totalNotesHit: 140,
totalNotes: 190
totalNotes: 240
}
// 2400 total notes = 7% = LOSS
// 275 total notes = 69% = NICE
// 240 total notes = 79% = GOOD
// 230 total notes = 82% = GREAT
// 210 total notes = 91% = EXCELLENT

View file

@ -2,11 +2,9 @@ package funkin;
import funkin.save.Save;
import funkin.input.Controls;
import flixel.FlxCamera;
import funkin.input.PreciseInputManager;
import flixel.input.actions.FlxActionInput;
import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxSignal;
import flixel.util.FlxSignal.FlxTypedSignal;
/**
* A core class which represents the current player(s) and their controls and other configuration.

View file

@ -218,6 +218,121 @@ class Preferences
public static var lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
#end
/**
* If >0, the game will display a semi-opaque background under the notes.
* `0` for no background, `100` for solid black if you're freaky like that
* @default `0`
*/
public static var strumlineBackgroundOpacity(get, set):Int;
static function get_strumlineBackgroundOpacity():Int
{
return (Save?.instance?.options?.strumlineBackgroundOpacity ?? 0);
}
static function set_strumlineBackgroundOpacity(value:Int):Int
{
var save:Save = Save.instance;
save.options.strumlineBackgroundOpacity = value;
save.flush();
return value;
}
/**
* If enabled, the game will hide the mouse when taking a screenshot.
* @default `true`
*/
public static var shouldHideMouse(get, set):Bool;
static function get_shouldHideMouse():Bool
{
return Save?.instance?.options?.screenshot?.shouldHideMouse ?? true;
}
static function set_shouldHideMouse(value:Bool):Bool
{
var save:Save = Save.instance;
save.options.screenshot.shouldHideMouse = value;
save.flush();
return value;
}
/**
* If enabled, the game will show a preview after taking a screenshot.
* @default `true`
*/
public static var fancyPreview(get, set):Bool;
static function get_fancyPreview():Bool
{
return Save?.instance?.options?.screenshot?.fancyPreview ?? true;
}
static function set_fancyPreview(value:Bool):Bool
{
var save:Save = Save.instance;
save.options.screenshot.fancyPreview = value;
save.flush();
return value;
}
/**
* If enabled, the game will show the preview only after a screenshot is saved.
* @default `true`
*/
public static var previewOnSave(get, set):Bool;
static function get_previewOnSave():Bool
{
return Save?.instance?.options?.screenshot?.previewOnSave ?? true;
}
static function set_previewOnSave(value:Bool):Bool
{
var save:Save = Save.instance;
save.options.screenshot.previewOnSave = value;
save.flush();
return value;
}
/**
* The game will save any screenshots taken to this format.
* @default `PNG`
*/
public static var saveFormat(get, set):Any;
static function get_saveFormat():Any
{
return Save?.instance?.options?.screenshot?.saveFormat ?? 'PNG';
}
static function set_saveFormat(value):Any
{
var save:Save = Save.instance;
save.options.screenshot.saveFormat = value;
save.flush();
return value;
}
/**
* The game will save JPEG screenshots with this quality percentage.
* @default `80`
*/
public static var jpegQuality(get, set):Int;
static function get_jpegQuality():Int
{
return Save?.instance?.options?.screenshot?.jpegQuality ?? 80;
}
static function set_jpegQuality(value:Int):Int
{
var save:Save = Save.instance;
save.options.screenshot.jpegQuality = value;
save.flush();
return value;
}
/**
* Loads the user's preferences from the save data and apply them.
*/

View file

@ -2,7 +2,10 @@ package funkin.api.discord;
#if FEATURE_DISCORD_RPC
import hxdiscord_rpc.Discord;
import hxdiscord_rpc.Types;
import hxdiscord_rpc.Types.DiscordButton;
import hxdiscord_rpc.Types.DiscordEventHandlers;
import hxdiscord_rpc.Types.DiscordRichPresence;
import hxdiscord_rpc.Types.DiscordUser;
import sys.thread.Thread;
class DiscordClient

View file

@ -0,0 +1,106 @@
package funkin.api.newgrounds;
import io.newgrounds.Call.CallOutcome;
import io.newgrounds.NG;
import io.newgrounds.objects.events.Outcome;
import io.newgrounds.objects.events.Result;
/**
* Use Newgrounds to perform basic telemetry. Ignore if not logged in to Newgrounds.
*/
@:nullSafety
class Events
{
// Only allow letters, numbers, spaces, dashes, and underscores.
static final EVENT_NAME_REGEX:EReg = ~/[^a-zA-Z0-9 -_]/g;
public static function logEvent(eventName:String):Void
{
#if (FEATURE_NEWGROUNDS && FEATURE_NEWGROUNDS_EVENTS)
if (NewgroundsClient.instance.isLoggedIn())
{
var eventHandler = NG.core.calls.event;
if (eventHandler != null)
{
var sanitizedEventName = EVENT_NAME_REGEX.replace(eventName, '');
var outcomeHandler = onEventLogged.bind(sanitizedEventName, _);
eventHandler.logEvent(sanitizedEventName).addOutcomeHandler(outcomeHandler).send();
}
}
#end
}
static function onEventLogged(eventName:String, outcome:CallOutcome<LogEventData>)
{
switch (outcome)
{
case SUCCESS(data):
trace('[NEWGROUNDS] Logged event: ${data.eventName}');
case FAIL(outcome):
switch (outcome)
{
case HTTP(error):
trace('[NEWGROUNDS] HTTP error while logging event: ${error}');
case RESPONSE(error):
trace('[NEWGROUNDS] Response error (${error.code}) while logging event: ${error.message}');
case RESULT(error):
switch (error.code)
{
case 103: // Invalid custom event name
trace('[NEWGROUNDS] Invalid custom event name: ${eventName}');
default:
trace('[NEWGROUNDS] Result error (${error.code}) while logging event: ${error.message}');
}
}
}
}
public static inline function logStartGame():Void
{
logEvent('start-game');
}
public static inline function logStartSong(songId:String, variation:String):Void
{
logEvent('start-song_${songId}-${variation}');
}
public static inline function logFailSong(songId:String, variation:String):Void
{
logEvent('blueballs_${songId}-${variation}');
}
public static inline function logCompleteSong(songId:String, variation:String):Void
{
logEvent('complete-song_${songId}-${variation}');
}
public static inline function logStartLevel(levelId:String):Void
{
logEvent('start-level_${levelId}');
}
public static inline function logCompleteLevel(levelId:String):Void
{
logEvent('complete-level_${levelId}');
}
public static inline function logEarnRank(rankName:String):Void
{
logEvent('earn-rank_${rankName}');
}
public static inline function logWatchCartoon():Void
{
logEvent('watch-cartoon');
}
// Note there is already a loadReferral call for the merch link
// and that gets logged as an event!
public static inline function logOpenCredits():Void
{
logEvent('open-credits');
}
}

View file

@ -0,0 +1,395 @@
package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS
import io.newgrounds.Call.CallError;
import io.newgrounds.objects.ScoreBoard as LeaderboardData;
import io.newgrounds.objects.events.Outcome;
import io.newgrounds.utils.ScoreBoardList;
class Leaderboards
{
public static function listLeaderboardData():Map<Leaderboard, LeaderboardData>
{
var leaderboardList:Null<ScoreBoardList> = NewgroundsClient.instance.leaderboards;
if (leaderboardList == null)
{
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
return [];
}
else
{
var result:Map<Leaderboard, LeaderboardData> = [];
for (leaderboardId in leaderboardList.keys())
{
var leaderboardData = leaderboardList.get(leaderboardId);
if (leaderboardData == null) continue;
// A little hacky, but it works.
result.set(cast leaderboardId, leaderboardData);
}
return result;
}
}
/**
* Submit a score to Newgrounds.
* @param leaderboard The leaderboard to submit to.
* @param score The score to submit.
* @param tag An optional tag to attach to the score.
*/
public static function submitScore(leaderboard:Leaderboard, score:Int, ?tag:String):Void
{
// Silently reject submissions for unknown leaderboards.
if (leaderboard == Leaderboard.Unknown) return;
if (NewgroundsClient.instance.isLoggedIn())
{
var leaderboardList = NewgroundsClient.instance.leaderboards;
if (leaderboardList == null) return;
var leaderboardData:Null<LeaderboardData> = leaderboardList.get(leaderboard.getId());
if (leaderboardData != null)
{
leaderboardData.postScore(score, function(outcome:Outcome<CallError>):Void {
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Submitted score!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to submit score!');
trace(error);
}
});
}
}
}
/**
* Submit a score for a Story Level to Newgrounds.
*/
public static function submitLevelScore(levelId:String, difficultyId:String, score:Int):Void
{
var tag = '${difficultyId}';
Leaderboards.submitScore(Leaderboard.getLeaderboardByLevel(levelId), score, tag);
}
/**
* Submit a score for a song to Newgrounds.
*/
public static function submitSongScore(songId:String, difficultyId:String, score:Int):Void
{
var tag = '${difficultyId}';
Leaderboards.submitScore(Leaderboard.getLeaderboardBySong(songId, difficultyId), score, tag);
}
}
#end
enum abstract Leaderboard(Int)
{
/**
* Represents an undefined or invalid leaderboard.
*/
var Unknown = -1;
//
// STORY LEVELS
//
var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14239 #else 9615 #end;
var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14240 #else 9616 #end;
var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14242 #else 9767 #end;
var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14241 #else 9866 #end;
var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14243 #else 9956 #end;
var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14244 #else 9957 #end;
var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14245 #else 14682 #end;
var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14237 #else 14683 #end;
//
// SONGS
//
// Tutorial
var Tutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14249 #else 14684 #end;
// Week 1
var Bopeebo = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14246 #else 9603 #end;
var BopeeboErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14685 #end;
var BopeeboPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14686 #end;
var Fresh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14247 #else 9602 #end;
var FreshErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14687 #end;
var FreshPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14688 #end;
var DadBattle = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14248 #else 9605 #end;
var DadBattleErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14689 #end;
var DadBattlePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14690 #end;
// Week 2
var Spookeez = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9604 #end;
var SpookeezErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14691 #end;
var SpookeezPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14692 #end;
var South = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9606 #end;
var SouthErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14693 #end;
var SouthPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14694 #end;
var Monster = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14703 #end;
// Week 3
var Pico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9766 #end;
var PicoErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14695 #end;
var PicoPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14696 #end;
var PhillyNice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9769 #end;
var PhillyNiceErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14697 #end;
var PhillyNicePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14698 #end;
var Blammed = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9768 #end;
var BlammedErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14704 #end;
var BlammedPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14705 #end;
// Week 4
var SatinPanties = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9869 #end;
var SatinPantiesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14701 #end;
var High = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9867 #end;
var HighErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14699 #end;
var MILF = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9868 #end;
// Week 5
var Cocoa = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14706 #end;
var CocoaErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14707 #end;
var CocoaPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14708 #end;
var Eggnog = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14709 #end;
var EggnogErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14711 #end;
var EggnogPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14710 #end;
var WinterHorrorland = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14712 #end;
// Week 6
var Senpai = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9958 #end;
var SenpaiErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14713 #end;
var SenpaiPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14716 #end;
var Roses = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9959 #end;
var RosesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14714 #end;
var RosesPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14717 #end;
var Thorns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9960 #end;
var ThornsErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14715 #end;
// Week 7
var Ugh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14718 #end;
var UghErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14722 #end;
var UghPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14721 #end;
var Guns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14719 #end;
var GunsPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14723 #end;
var Stress = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14720 #end;
var StressPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14724 #end;
// Weekend 1
var Darnell = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14725 #end;
var DarnellErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14727 #end;
var DarnellBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14726 #end;
var LitUp = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14728 #end;
var LitUpBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14729 #end;
var TwoHot = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14730 #end; // Variable names can't start with a number!
var Blazin = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14731 #end;
public function getId():Int
{
return this;
}
/**
* Get the leaderboard for a given level and difficulty.
* @param levelId The ID for the story level.
* @param difficulty The current difficulty.
* @return The Leaderboard ID for the given level and difficulty.
*/
public static function getLeaderboardByLevel(levelId:String):Leaderboard
{
switch (levelId)
{
case "week1":
return StoryWeek1;
case "week2":
return StoryWeek2;
case "week3":
return StoryWeek3;
case "week4":
return StoryWeek4;
case "week5":
return StoryWeek5;
case "week6":
return StoryWeek6;
case "week7":
return StoryWeek7;
case "weekend1":
return StoryWeekend1;
default:
return Unknown;
}
}
/**
* Get the leaderboard for a given level and difficulty.
* @param levelId The ID for the story level.
* @param difficulty The current difficulty, suffixed with the variation, like `easy-pico` or `nightmare`.
* @return The Leaderboard ID for the given level and difficulty.
*/
public static function getLeaderboardBySong(songId:String, difficulty:String):Leaderboard
{
var variation = Constants.DEFAULT_VARIATION;
var difficultyParts = difficulty.split('-');
if (difficultyParts.length >= 2)
{
variation = difficultyParts[difficultyParts.length - 1];
}
else if (Constants.DEFAULT_DIFFICULTY_LIST_ERECT.contains(difficulty))
{
variation = "erect";
}
switch (variation)
{
case "pico":
switch (songId)
{
case "bopeebo":
return BopeeboPicoMix;
case "fresh":
return FreshPicoMix;
case "dadbattle":
return DadBattlePicoMix;
case "spookeez":
return SpookeezPicoMix;
case "south":
return SouthPicoMix;
case "pico":
return PicoPicoMix;
case "philly-nice":
return PhillyNicePicoMix;
case "blammed":
return BlammedPicoMix;
case "cocoa":
return CocoaPicoMix;
case "eggnog":
return EggnogPicoMix;
case "senpai":
return SenpaiPicoMix;
case "roses":
return RosesPicoMix;
case "ugh":
return UghPicoMix;
case "guns":
return GunsPicoMix;
case "stress":
return StressPicoMix;
default:
return Unknown;
}
case "bf":
switch (songId)
{
case "darnell":
return DarnellBFMix;
case "litup":
return LitUpBFMix;
default:
return Unknown;
}
case "erect":
switch (songId)
{
case "bopeebo":
return BopeeboErect;
case "fresh":
return FreshErect;
case "dadbattle":
return DadBattleErect;
case "spookeez":
return SpookeezErect;
case "south":
return SouthErect;
case "pico":
return PicoErect;
case "philly-nice":
return PhillyNiceErect;
case "blammed":
return BlammedErect;
case "satin-panties":
return SatinPantiesErect;
case "high":
return HighErect;
case "cocoa":
return CocoaErect;
case "eggnog":
return EggnogErect;
case "senpai":
return SenpaiErect;
case "roses":
return RosesErect;
case "thorns":
return ThornsErect;
case "ugh":
return UghErect;
case "darnell":
return DarnellErect;
default:
return Unknown;
}
case "default":
switch (songId)
{
case "tutorial":
return Tutorial;
case "bopeebo":
return Bopeebo;
case "fresh":
return Fresh;
case "dadbattle":
return DadBattle;
case "spookeez":
return Spookeez;
case "south":
return South;
case "monster":
return Monster;
case "pico":
return Pico;
case "philly-nice":
return PhillyNice;
case "blammed":
return Blammed;
case "satin-panties":
return SatinPanties;
case "high":
return High;
case "milf":
return MILF;
case "cocoa":
return Cocoa;
case "eggnog":
return Eggnog;
case "winter-horrorland":
return WinterHorrorland;
case "senpai":
return Senpai;
case "roses":
return Roses;
case "thorns":
return Thorns;
case "ugh":
return Ugh;
case "guns":
return Guns;
case "stress":
return Stress;
case "darnell":
return Darnell;
case "litup":
return LitUp;
case "2hot":
return TwoHot;
case "blazin":
return Blazin;
default:
return Unknown;
}
default:
return Unknown;
}
}
}

View file

@ -0,0 +1,346 @@
package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS
import io.newgrounds.objects.Medal as MedalData;
import funkin.util.plugins.NewgroundsMedalPlugin;
import flixel.graphics.FlxGraphic;
import openfl.display.BitmapData;
import io.newgrounds.utils.MedalList;
import haxe.Json;
class Medals
{
public static var medalJSON:Array<MedalJSON> = [];
public static function listMedalData():Map<Medal, MedalData>
{
var medalList = NewgroundsClient.instance.medals;
if (medalList == null)
{
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
return [];
}
else
{
// TODO: Why do I have to do this, @:nullSafety is fucked up
var result:Map<Medal, MedalData> = [];
for (medalId in medalList.keys())
{
var medalData = medalList.get(medalId);
if (medalData == null) continue;
// A little hacky, but it works.
result.set(cast medalId, medalData);
}
return result;
}
}
public static function award(medal:Medal):Void
{
if (NewgroundsClient.instance.isLoggedIn())
{
var medalList = NewgroundsClient.instance.medals;
if (medalList == null) return;
var medalData:Null<MedalData> = medalList.get(medal.getId());
@:privateAccess
if (medalData == null || medalData._data == null)
{
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
return;
}
else if (!medalData.unlocked)
{
trace('[NEWGROUNDS] Awarding medal (${medal}).');
medalData.sendUnlock();
// Play the medal unlock animation, but only if the user has not already unlocked it.
#if html5
// Web builds support parsing the bitmap data from the URL directly.
BitmapData.loadFromFile("https:" + medalData.icon).onComplete(function(bmp:BitmapData) {
var medalGraphic = FlxGraphic.fromBitmapData(bmp);
medalGraphic.persist = true;
NewgroundsMedalPlugin.play(medalData.value, medalData.name, medalGraphic);
});
#else
if (medalJSON == null) loadMedalJSON();
// We have to use a medal image from the game files. We use a Base64 encoded image that NG spits out.
// TODO: Wait, don't they give us the medal icon?
var localMedalData:Null<MedalJSON> = medalJSON.filter(function(jsonMedal) {
#if FEATURE_NEWGROUNDS_TESTING_MEDALS
return medal == jsonMedal.idTest;
#else
return medal == jsonMedal.id;
#end
})[0];
if (localMedalData == null) throw "You forgot to encode a Base64 image for medal: " + medal;
var str:String = localMedalData.icon;
// Lime/OpenFL parses it without the included prefix stuff, so we remove it.
str = str.replace("data:image/png;base64,", "").trim();
var bitmapData = BitmapData.fromBase64(str, "image/png");
var medalGraphic:Null<FlxGraphic> = null;
if (str != null)
{
medalGraphic = FlxGraphic.fromBitmapData(bitmapData);
medalGraphic.persist = true;
}
NewgroundsMedalPlugin.play(medalData.value, medalData.name, medalGraphic);
#end
}
else
{
trace('[NEWGROUNDS] User already has medal (${medal}).');
}
}
else
{
trace('[NEWGROUNDS] Attempted to award medal (${medal}), but not logged into Newgrounds.');
}
}
public static function loadMedalJSON():Void
{
var jsonPath = Paths.json('medals');
var jsonString = Assets.getText(jsonPath);
var parser = new json2object.JsonParser<Array<MedalJSON>>();
parser.ignoreUnknownVariables = false;
trace('[NEWGROUNDS] Parsing local medal data...');
parser.fromJson(jsonString, jsonPath);
if (parser.errors.length > 0)
{
trace('[NEWGROUNDS] Failed to parse local medal data!');
for (error in parser.errors)
funkin.data.DataError.printError(error);
medalJSON = [];
}
else
{
medalJSON = parser.value;
}
}
public static function awardStoryLevel(id:String):Void
{
switch (id)
{
case 'tutorial':
Medals.award(Medal.StoryTutorial);
case 'week1':
Medals.award(Medal.StoryWeek1);
case 'week2':
Medals.award(Medal.StoryWeek2);
case 'week3':
Medals.award(Medal.StoryWeek3);
case 'week4':
Medals.award(Medal.StoryWeek4);
case 'week5':
Medals.award(Medal.StoryWeek5);
case 'week6':
Medals.award(Medal.StoryWeek6);
case 'week7':
Medals.award(Medal.StoryWeek7);
case 'weekend1':
Medals.award(Medal.StoryWeekend1);
default:
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
}
}
}
#end
/**
* Represents NG Medal data in a JSON format.
*/
typedef MedalJSON =
{
/**
* Medal ID to use for release builds
*/
var id:Int;
/**
* Medal ID to use for testing builds
*/
var idTest:Int;
/**
* The English name for the medal
*/
var name:String;
/**
* The English name for the medal
*/
var icon:String;
}
enum abstract Medal(Int) from Int to Int
{
/**
* Represents an undefined or invalid medal.
*/
var Unknown = -1;
/**
* I Said Funkin'!
* Start the game for the first time.
*/
var StartGame = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80894 #else 60960 #end;
/**
* That's How You Do It!
* Beat Tutoria l in Story Mode (on any difficulty).
*/
var StoryTutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80906 #else 83647 #end;
/**
* More Like Daddy Queerest
* Beat Week 1 in Story Mode (on any difficulty).
*/
var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80899 #else 60961 #end;
/**
* IT IS THE SPOOKY MONTH
* Beat Week 2 in Story Mode (on any difficulty).
*/
var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80900 #else 83648 #end;
/**
* Pico Funny
* Beat Week 3 in Story Mode (on any difficulty).
*/
var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80901 #else 83649 #end;
/**
* Mommy Must Murder
* Beat Week 4 in Story Mode (on any difficulty).
*/
var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80902 #else 83650 #end;
/**
* Yule Tide Joy
* Beat Week 5 in Story Mode (on any difficulty).
*/
var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80903 #else 83651 #end;
/**
* A Visual Novelty
* Beat Week 6 in Story Mode (on any difficulty).
*/
var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80904 #else 83652 #end;
/**
* I <3 JohnnyUtah
* Beat Week 7 in Story Mode (on any difficulty).
*/
var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80905 #else 83653 #end;
/**
* Yo, Really Think So?
* Beat Weekend 1 in Story Mode (on any difficulty).
*/
var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80907 #else 83654 #end;
/**
* Stay Funky
* Press TAB in Freeplay and unlock your first character.
*/
var CharSelect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83633 #else 83655 #end;
/**
* A Challenger Appears
* Beat any Pico remix in Freeplay (on any difficulty).
*/
var FreeplayPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80910 #else 83656 #end;
/**
* De-Stressing
* Beat Stress (Pico Mix) in Freeplay (on Normal difficulty or higher).
*/
var FreeplayStressPico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83629 #else 83657 #end;
/**
* L
* Earn a Loss rating on any song (on any difficulty).
*/
var LossRating = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80915 #else 83658 #end;
/**
* Getting Freaky
* Earn a Perfect rating on any song on Hard difficulty or higher.
* NOTE: Should also be awarded for a Gold Perfect because otherwise that would be annoying.
*/
var PerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80908 #else 83659 #end;
/**
* You Should Drink More Water
* Earn a Golden Perfect rating on any song on Hard difficulty or higher.
*/
var GoldPerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80909 #else 83660 #end;
/**
* Harder Than Hard
* Beat any Erect remix in Freeplay on Erect or Nightmare difficulty.
*/
var ErectDifficulty = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80911 #else 83661 #end;
/**
* The Rap God
* Earn a Gold Perfect rating on any song on Nightmare difficulty.
*/
var GoldPerfectRatingNightmare = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80912 #else 83662 #end;
/**
* Just like the game!
* Get freaky on a Friday.
* NOTE: You must beat at least one song on any difficulty.
*/
var FridayNight = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80913 #else 61034 #end;
/**
* Nice
* Earn a rating of EXACTLY 69% (good luck).
*/
var Nice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80914 #else 83646 #end;
public function getId():Int
{
return this;
}
public static function getMedalByStoryLevel(levelId:String):Medal
{
switch (levelId)
{
case "week1":
return StoryWeek1;
case "week2":
return StoryWeek2;
case "week3":
return StoryWeek3;
case "week4":
return StoryWeek4;
case "week5":
return StoryWeek5;
case "week6":
return StoryWeek6;
case "week7":
return StoryWeek7;
case "weekend1":
return StoryWeekend1;
default:
return Unknown;
}
}
}

View file

@ -1,71 +0,0 @@
package funkin.api.newgrounds;
#if newgrounds
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;
#end
/**
* Contains any script functions which should be BLOCKED from use by modded scripts.
*/
class NGUnsafe
{
static public function logEvent(event:String)
{
#if newgrounds
NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event);
#else
#if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib');
#end
#end
}
static public function unlockMedal(id:Int)
{
#if newgrounds
if (isLoggedIn)
{
var medal = NG.core.medals.get(id);
if (!medal.unlocked) medal.sendUnlock();
}
#else
#if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib');
#end
#end
}
static public function postScore(score:Int = 0, song:String)
{
#if newgrounds
if (isLoggedIn)
{
for (id in NG.core.scoreBoards.keys())
{
var board = NG.core.scoreBoards.get(id);
if (song == board.name)
{
board.postScore(score, "Uhh meow?");
}
// trace('loaded scoreboard id:$id, name:${board.name}');
}
}
#else
#if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end
#end
}
}

View file

@ -1,243 +0,0 @@
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;
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;
#end
/**
* Contains any script functions which should be ALLOWD for use by modded scripts.
*/
class NGUtil
{
#if newgrounds
/**
* True, if the saved sessionId was used in the initial login, and failed to connect.
* Used in MainMenuState to show a popup to establish a new connection
*/
public static var savedSessionFailed(default, null):Bool = false;
public static var scoreboardsLoaded:Bool = false;
public static var isLoggedIn(get, never):Bool;
inline static function get_isLoggedIn()
{
return NG.core != null && NG.core.loggedIn;
}
public static var scoreboardArray:Array<Score> = [];
public static var ngDataLoaded(default, null):FlxSignal = new FlxSignal();
public static var ngScoresLoaded(default, null):FlxSignal = new FlxSignal();
public static var GAME_VER:String = "";
static public function checkVersion(callback:String->Void)
{
trace('checking NG.io version');
GAME_VER = "v" + Application.current.meta.get('version');
NG.core.calls.app.getCurrentVersion(GAME_VER).addDataHandler(function(response) {
GAME_VER = response.result.data.currentVersion;
trace('CURRENT NG VERSION: ' + GAME_VER);
callback(GAME_VER);
}).send();
}
static public function init()
{
var api = APIStuff.API;
if (api == null || api.length == 0)
{
trace("Missing Newgrounds API key, aborting connection");
return;
}
trace("connecting to newgrounds");
#if NG_FORCE_EXPIRED_SESSION
var sessionId:String = "fake_session_id";
function onSessionFail(error:Error)
{
trace("Forcing an expired saved session. " + "To disable, comment out NG_FORCE_EXPIRED_SESSION in Project.xml");
savedSessionFailed = true;
}
#else
var sessionId:String = NGLite.getSessionId();
if (sessionId != null) trace("found web session id");
#if (debug)
if (sessionId == null && APIStuff.SESSION != null)
{
trace("using debug session id");
sessionId = APIStuff.SESSION;
}
#end
var onSessionFail:Error->Void = null;
if (sessionId == null && Save.instance.ngSessionId != null)
{
trace("using stored session id");
sessionId = Save.instance.ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
NG.create(api, sessionId, #if NG_DEBUG true #else false #end, onSessionFail);
#if NG_VERBOSE
NG.core.verbose = true;
#end
// Set the encryption cipher/format to RC4/Base64. AES128 and Hex are not implemented yet
NG.core.initEncryption(APIStuff.EncKey); // Found in you NG project view
if (NG.core.attemptingLogin)
{
/* a session_id was found in the loadervars, this means the user is playing on newgrounds.com
* and we should login shortly. lets wait for that to happen
*/
trace("attempting login");
NG.core.onLogin.add(onNGLogin);
}
// GK: taking out auto login, adding a login button to the main menu
// else
// {
// /* They are NOT playing on newgrounds.com, no session id was found. We must start one manually, if we want to.
// * Note: This will cause a new browser window to pop up where they can log in to newgrounds
// */
// NG.core.requestLogin(onNGLogin);
// }
}
/**
* Attempts to log in to newgrounds by requesting a new session ID, only call if no session ID was found automatically
* @param popupLauncher The function to call to open the login url, must be inside
* a user input event or the popup blocker will block it.
* @param onComplete A callback with the result of the connection.
*/
static public function login(?popupLauncher:(Void->Void)->Void, onComplete:ConnectionResult->Void)
{
trace("Logging in manually");
var onPending:Void->Void = null;
if (popupLauncher != null)
{
onPending = function() popupLauncher(NG.core.openPassportUrl);
}
var onSuccess:Void->Void = onNGLogin;
var onFail:Error->Void = null;
var onCancel:Void->Void = null;
if (onComplete != null)
{
onSuccess = function() {
onNGLogin();
onComplete(Success);
}
onFail = function(e) onComplete(Fail(e.message));
onCancel = function() onComplete(Cancelled);
}
NG.core.requestLogin(onSuccess, onPending, onFail, onCancel);
}
inline static public function cancelLogin():Void
{
NG.core.cancelLoginRequest();
}
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
Save.instance.ngSessionId = NG.core.sessionId;
Save.instance.flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
// Load Scoreboards hten call onNGBoardsFetch()
NG.core.requestScoreBoards(onNGBoardsFetch);
ngDataLoaded.dispatch();
}
static public function logout()
{
NG.core.logOut();
Save.instance.ngSessionId = null;
Save.instance.flush();
}
// --- MEDALS
static function onNGMedalFetch():Void
{
/*
// Reading medal info
for (id in NG.core.medals.keys())
{
var medal = NG.core.medals.get(id);
trace('loaded medal id:$id, name:${medal.name}, description:${medal.description}');
}
// Unlocking medals
var unlockingMedal = NG.core.medals.get(54352);// medal ids are listed in your NG project viewer
if (!unlockingMedal.unlocked)
unlockingMedal.sendUnlock();
*/
}
// --- SCOREBOARDS
static function onNGBoardsFetch():Void
{
/*
// Reading medal info
for (id in NG.core.scoreBoards.keys())
{
var board = NG.core.scoreBoards.get(id);
trace('loaded scoreboard id:$id, name:${board.name}');
}
*/
// var board = NG.core.scoreBoards.get(8004);// ID found in NG project view
// Posting a score thats OVER 9000!
// board.postScore(FlxG.random.int(0, 1000));
// --- To view the scores you first need to select the range of scores you want to see ---
// add an update listener so we know when we get the new scores
// board.onUpdate.add(onNGScoresFetch);
trace("shoulda got score by NOW!");
// board.requestScores(20);// get the best 10 scores ever logged
// more info on scores --- http://www.newgrounds.io/help/components/#scoreboard-getscores
}
static function onNGScoresFetch():Void
{
scoreboardsLoaded = true;
ngScoresLoaded.dispatch();
/*
for (score in NG.core.scoreBoards.get(8737).scores)
{
trace('score loaded user:${score.user.name}, score:${score.formatted_value}');
}
*/
// var board = NG.core.scoreBoards.get(8004);// ID found in NG project view
// board.postScore(HighScore.score);
// NGUtil.scoreboardArray = NG.core.scoreBoards.get(8004).scores;
}
#end
}

View file

@ -1,298 +0,0 @@
package funkin.api.newgrounds;
#if newgrounds
import flixel.util.FlxSignal;
import io.newgrounds.NG;
import io.newgrounds.NGLite;
import io.newgrounds.objects.Error;
import io.newgrounds.objects.Score;
import lime.app.Application;
#end
/**
* MADE BY GEOKURELI THE LEGENED GOD HERO MVP
*/
class NGio
{
#if newgrounds
/**
* True, if the saved sessionId was used in the initial login, and failed to connect.
* Used in MainMenuState to show a popup to establish a new connection
*/
public static var savedSessionFailed(default, null):Bool = false;
public static var scoreboardsLoaded:Bool = false;
public static var isLoggedIn(get, never):Bool;
inline static function get_isLoggedIn()
{
return NG.core != null && NG.core.loggedIn;
}
public static var scoreboardArray:Array<Score> = [];
public static var ngDataLoaded(default, null):FlxSignal = new FlxSignal();
public static var ngScoresLoaded(default, null):FlxSignal = new FlxSignal();
public static var GAME_VER:String = "";
static public function checkVersion(callback:String->Void)
{
trace('checking NG.io version');
GAME_VER = "v" + Application.current.meta.get('version');
NG.core.calls.app.getCurrentVersion(GAME_VER).addDataHandler(function(response) {
GAME_VER = response.result.data.currentVersion;
trace('CURRENT NG VERSION: ' + GAME_VER);
callback(GAME_VER);
}).send();
}
static public function init()
{
var api = APIStuff.API;
if (api == null || api.length == 0)
{
trace("Missing Newgrounds API key, aborting connection");
return;
}
trace("connecting to newgrounds");
#if NG_FORCE_EXPIRED_SESSION
var sessionId:String = "fake_session_id";
function onSessionFail(error:Error)
{
trace("Forcing an expired saved session. " + "To disable, comment out NG_FORCE_EXPIRED_SESSION in Project.xml");
savedSessionFailed = true;
}
#else
var sessionId:String = NGLite.getSessionId();
if (sessionId != null) trace("found web session id");
#if (debug)
if (sessionId == null && APIStuff.SESSION != null)
{
trace("using debug session id");
sessionId = APIStuff.SESSION;
}
#end
var onSessionFail:Error->Void = null;
if (sessionId == null && Save.instance.ngSessionId != null)
{
trace("using stored session id");
sessionId = Save.instance.ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
NG.create(api, sessionId, #if NG_DEBUG true #else false #end, onSessionFail);
#if NG_VERBOSE
NG.core.verbose = true;
#end
// Set the encryption cipher/format to RC4/Base64. AES128 and Hex are not implemented yet
NG.core.initEncryption(APIStuff.EncKey); // Found in you NG project view
if (NG.core.attemptingLogin)
{
/* a session_id was found in the loadervars, this means the user is playing on newgrounds.com
* and we should login shortly. lets wait for that to happen
*/
trace("attempting login");
NG.core.onLogin.add(onNGLogin);
}
// GK: taking out auto login, adding a login button to the main menu
// else
// {
// /* They are NOT playing on newgrounds.com, no session id was found. We must start one manually, if we want to.
// * Note: This will cause a new browser window to pop up where they can log in to newgrounds
// */
// NG.core.requestLogin(onNGLogin);
// }
}
/**
* Attempts to log in to newgrounds by requesting a new session ID, only call if no session ID was found automatically
* @param popupLauncher The function to call to open the login url, must be inside
* a user input event or the popup blocker will block it.
* @param onComplete A callback with the result of the connection.
*/
static public function login(?popupLauncher:(Void->Void)->Void, onComplete:ConnectionResult->Void)
{
trace("Logging in manually");
var onPending:Void->Void = null;
if (popupLauncher != null)
{
onPending = function() popupLauncher(NG.core.openPassportUrl);
}
var onSuccess:Void->Void = onNGLogin;
var onFail:Error->Void = null;
var onCancel:Void->Void = null;
if (onComplete != null)
{
onSuccess = function() {
onNGLogin();
onComplete(Success);
}
onFail = function(e) onComplete(Fail(e.message));
onCancel = function() onComplete(Cancelled);
}
NG.core.requestLogin(onSuccess, onPending, onFail, onCancel);
}
inline static public function cancelLogin():Void
{
NG.core.cancelLoginRequest();
}
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
Save.instance.ngSessionId = NG.core.sessionId;
Save.instance.flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
// Load Scoreboards hten call onNGBoardsFetch()
NG.core.requestScoreBoards(onNGBoardsFetch);
ngDataLoaded.dispatch();
}
static public function logout()
{
NG.core.logOut();
Save.instance.ngSessionId = null;
Save.instance.flush();
}
// --- MEDALS
static function onNGMedalFetch():Void
{
/*
// Reading medal info
for (id in NG.core.medals.keys())
{
var medal = NG.core.medals.get(id);
trace('loaded medal id:$id, name:${medal.name}, description:${medal.description}');
}
// Unlocking medals
var unlockingMedal = NG.core.medals.get(54352);// medal ids are listed in your NG project viewer
if (!unlockingMedal.unlocked)
unlockingMedal.sendUnlock();
*/
}
// --- SCOREBOARDS
static function onNGBoardsFetch():Void
{
/*
// Reading medal info
for (id in NG.core.scoreBoards.keys())
{
var board = NG.core.scoreBoards.get(id);
trace('loaded scoreboard id:$id, name:${board.name}');
}
*/
// var board = NG.core.scoreBoards.get(8004);// ID found in NG project view
// Posting a score thats OVER 9000!
// board.postScore(FlxG.random.int(0, 1000));
// --- To view the scores you first need to select the range of scores you want to see ---
// add an update listener so we know when we get the new scores
// board.onUpdate.add(onNGScoresFetch);
trace("shoulda got score by NOW!");
// board.requestScores(20);// get the best 10 scores ever logged
// more info on scores --- http://www.newgrounds.io/help/components/#scoreboard-getscores
}
static function onNGScoresFetch():Void
{
scoreboardsLoaded = true;
ngScoresLoaded.dispatch();
/*
for (score in NG.core.scoreBoards.get(8737).scores)
{
trace('score loaded user:${score.user.name}, score:${score.formatted_value}');
}
*/
// var board = NG.core.scoreBoards.get(8004);// ID found in NG project view
// board.postScore(HighScore.score);
// NGio.scoreboardArray = NG.core.scoreBoards.get(8004).scores;
}
#end
static public function logEvent(event:String)
{
#if newgrounds
NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event);
#else
#if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib');
#end
#end
}
static public function unlockMedal(id:Int)
{
#if newgrounds
if (isLoggedIn)
{
var medal = NG.core.medals.get(id);
if (!medal.unlocked) medal.sendUnlock();
}
#else
#if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib');
#end
#end
}
static public function postScore(score:Int = 0, song:String)
{
#if newgrounds
if (isLoggedIn)
{
for (id in NG.core.scoreBoards.keys())
{
var board = NG.core.scoreBoards.get(id);
if (song == board.name)
{
board.postScore(score, "Uhh meow?");
}
// trace('loaded scoreboard id:$id, name:${board.name}');
}
}
#else
#if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end
#end
}
}
enum ConnectionResult
{
/** Log in successful */
Success;
/** Could not login */
Fail(msg:String);
/** User cancelled the login */
Cancelled;
}

View file

@ -0,0 +1,331 @@
package funkin.api.newgrounds;
import funkin.save.Save;
import funkin.api.newgrounds.Medals.Medal;
#if FEATURE_NEWGROUNDS
import io.newgrounds.Call.CallError;
import io.newgrounds.NG;
import io.newgrounds.NGLite;
import io.newgrounds.NGLite.LoginOutcome;
import io.newgrounds.NGLite.LoginFail;
import io.newgrounds.objects.events.Outcome;
import io.newgrounds.utils.MedalList;
import io.newgrounds.utils.ScoreBoardList;
import io.newgrounds.objects.User;
@:nullSafety
class NewgroundsClient
{
public static var instance(get, never):NewgroundsClient;
static var _instance:Null<NewgroundsClient> = null;
static function get_instance():NewgroundsClient
{
if (NewgroundsClient._instance == null) _instance = new NewgroundsClient();
if (NewgroundsClient._instance == null) throw "Could not initialize singleton NewgroundsClient!";
return NewgroundsClient._instance;
}
public var user(get, never):Null<User>;
public var medals(get, never):Null<MedalList>;
public var leaderboards(get, never):Null<ScoreBoardList>;
private function new()
{
trace('[NEWGROUNDS] Initializing client...');
#if FEATURE_NEWGROUNDS_DEBUG
trace('[NEWGROUNDS] App ID: ${NewgroundsCredentials.APP_ID}');
trace('[NEWGROUNDS] Encryption Key: ${NewgroundsCredentials.ENCRYPTION_KEY}');
#end
if (!hasValidCredentials())
{
FlxG.log.warn("Tried to initialize Newgrounds client, but credentials are invalid!");
return;
}
var debug = #if FEATURE_NEWGROUNDS_DEBUG true #else false #end;
NG.create(NewgroundsCredentials.APP_ID, getSessionId(), debug, onLoginResolved);
NG.core.setupEncryption(NewgroundsCredentials.ENCRYPTION_KEY);
}
public function init()
{
if (NG.core == null) return;
trace('[NEWGROUNDS] Setting up connection...');
#if FEATURE_NEWGROUNDS_DEBUG
NG.core.verbose = true;
#end
NG.core.onLogin.add(onLoginSuccessful);
if (NG.core.attemptingLogin)
{
// Session ID was valid and we should be logged in soon.
trace('[NEWGROUNDS] Waiting for existing login!');
}
else
{
#if FEATURE_NEWGROUNDS_AUTOLOGIN
// Attempt an automatic login.
trace('[NEWGROUNDS] Attempting new login immediately!');
this.autoLogin();
#else
trace('[NEWGROUNDS] Not logged in, you have to login manually!');
#end
}
}
/**
* Attempt to log into Newgrounds and create a session ID.
* @param onSuccess An optional callback for when the login is successful.
* @param onError An optional callback for when the login fails.
*/
public function login(?onSuccess:Void->Void, ?onError:Void->Void):Void
{
if (NG.core == null)
{
FlxG.log.warn("No Newgrounds client initialized! Are your credentials invalid?");
return;
}
if (onSuccess != null && onError != null)
{
NG.core.requestLogin(onLoginResolvedWithCallbacks.bind(_, onSuccess, onError));
}
else
{
NG.core.requestLogin(onLoginResolved);
}
}
public function autoLogin(?onSuccess:Void->Void, ?onError:Void->Void):Void
{
if (NG.core == null)
{
FlxG.log.warn("No Newgrounds client initialized! Are your credentials invalid?");
return;
}
var dummyPassport:String->Void = function(_) {
// just a dummy passport, so we don't create a popup
// otherwise `NG.core.requestLogin()` will automatically attempt to open a tab at the beginning of the game
// users should go to the Options Menu to login to NG
// we cancel the request, so we can call it later
NG.core.cancelLoginRequest();
};
if (onSuccess != null && onError != null)
{
NG.core.requestLogin(onLoginResolvedWithCallbacks.bind(_, onSuccess, onError), dummyPassport);
}
else
{
NG.core.requestLogin(onLoginResolved, dummyPassport);
}
}
/**
* Log out of Newgrounds and invalidate the current session.
* @param onSuccess An optional callback for when the logout is successful.
*/
public function logout(?onSuccess:Void->Void, ?onError:Void->Void):Void
{
if (NG.core != null)
{
if (onSuccess != null && onError != null)
{
NG.core.logOut(onLogoutResolvedWithCallbacks.bind(_, onSuccess, onError));
}
else
{
NG.core.logOut(onLogoutResolved);
}
}
Save.instance.ngSessionId = null;
}
public function isLoggedIn():Bool
{
#if FEATURE_NEWGROUNDS
return NG.core != null && NG.core.loggedIn;
#else
return false;
#end
}
/**
* @returns `false` if either the app ID or the encryption key is invalid.
*/
static function hasValidCredentials():Bool
{
return !(NewgroundsCredentials.APP_ID == null
|| NewgroundsCredentials.APP_ID == ""
|| NewgroundsCredentials.APP_ID.contains(" ")
|| NewgroundsCredentials.ENCRYPTION_KEY == null
|| NewgroundsCredentials.ENCRYPTION_KEY == ""
|| NewgroundsCredentials.ENCRYPTION_KEY.contains(" "));
}
function onLoginResolved(outcome:LoginOutcome):Void
{
switch (outcome)
{
case SUCCESS:
onLoginSuccessful();
case FAIL(result):
onLoginFailed(result);
}
}
function onLoginResolvedWithCallbacks(outcome:LoginOutcome, onSuccess:Void->Void, onError:Void->Void):Void
{
onLoginResolved(outcome);
switch (outcome)
{
case SUCCESS:
onSuccess();
case FAIL(result):
onError();
}
}
function onLogoutResolved(outcome:Outcome<CallError>):Void
{
switch (outcome)
{
case SUCCESS:
onLogoutSuccessful();
case FAIL(result):
onLogoutFailed(result);
}
}
function onLogoutResolvedWithCallbacks(outcome:Outcome<CallError>, onSuccess:Void->Void, onError:Void->Void):Void
{
onLogoutResolved(outcome);
switch (outcome)
{
case SUCCESS:
onSuccess();
case FAIL(result):
onError();
}
}
function onLoginSuccessful():Void
{
if (NG.core == null) return;
trace('[NEWGROUNDS] Login successful!');
// Persist the session ID.
Save.instance.ngSessionId = NG.core.sessionId;
trace('[NEWGROUNDS] Submitting medal request...');
NG.core.requestMedals(onFetchedMedals);
trace('[NEWGROUNDS] Submitting leaderboard request...');
NG.core.scoreBoards.loadList(onFetchedLeaderboards);
}
function onLoginFailed(result:LoginFail):Void
{
switch (result)
{
case CANCELLED(type):
switch (type)
{
case PASSPORT:
trace('[NEWGROUNDS] Login cancelled by passport website.');
case MANUAL:
trace('[NEWGROUNDS] Login cancelled by application.');
default:
trace('[NEWGROUNDS] Login cancelled by unknown source.');
}
case ERROR(error):
switch (error)
{
case HTTP(error):
trace('[NEWGROUNDS] Login failed due to HTTP error: ${error}');
case RESPONSE(error):
trace('[NEWGROUNDS] Login failed due to response error: ${error.message} (${error.code})');
case RESULT(error):
trace('[NEWGROUNDS] Login failed due to result error: ${error.message} (${error.code})');
default:
trace('[NEWGROUNDS] Login failed due to unknown error: ${error}');
}
default:
trace('[NEWGROUNDS] Login failed due to unknown reason.');
}
}
function onLogoutSuccessful():Void
{
trace('[NEWGROUNDS] Logout successful!');
}
function onLogoutFailed(result:CallError):Void
{
switch (result)
{
case HTTP(error):
trace('[NEWGROUNDS] Logout failed due to HTTP error: ${error}');
case RESPONSE(error):
trace('[NEWGROUNDS] Logout failed due to response error: ${error.message} (${error.code})');
case RESULT(error):
trace('[NEWGROUNDS] Logout failed due to result error: ${error.message} (${error.code})');
default:
trace('[NEWGROUNDS] Logout failed due to unknown error: ${result}');
}
}
function onFetchedMedals(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched medals!');
}
function onFetchedLeaderboards(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched leaderboards!');
// trace(funkin.api.newgrounds.Leaderboards.listLeaderboardData());
}
function get_user():Null<User>
{
if (NG.core == null || !this.isLoggedIn()) return null;
return NG.core.user;
}
function get_medals():Null<MedalList>
{
if (NG.core == null || !this.isLoggedIn()) return null;
return NG.core.medals;
}
function get_leaderboards():Null<ScoreBoardList>
{
if (NG.core == null || !this.isLoggedIn()) return null;
return NG.core.scoreBoards;
}
static function getSessionId():Null<String>
{
#if js
// We can fetch the session ID from the URL.
var result:Null<String> = NGLite.getSessionId();
if (result != null) return result;
#end
// We have to fetch the session ID from the save file.
return Save.instance.ngSessionId;
}
}
#end

View file

@ -1,101 +0,0 @@
package funkin.api.newgrounds;
#if newgrounds
import funkin.NGio;
import funkin.ui.Prompt;
class NgPrompt extends Prompt
{
public function new(text:String, style:ButtonStyle = Yes_No)
{
super(text, style);
}
static public function showLogin()
{
return showLoginPrompt(true);
}
static public function showSavedSessionFailed()
{
return showLoginPrompt(false);
}
static function showLoginPrompt(fromUi:Bool)
{
var prompt = new NgPrompt("Talking to server...", None);
prompt.openCallback = NGUtil.login.bind(function popupLauncher(openPassportUrl)
{
var choiceMsg = fromUi ? #if web "Log in to Newgrounds?" #else null #end // User-input needed to allow popups
: "Your session has expired.\n Please login again.";
if (choiceMsg != null)
{
prompt.setText(choiceMsg);
prompt.setButtons(Yes_No);
#if web
prompt.buttons.getItem("yes").fireInstantly = true;
#end
prompt.onYes = function() {
prompt.setText("Connecting..." #if web + "\n(check your popup blocker)" #end);
prompt.setButtons(None);
openPassportUrl();
};
prompt.onNo = function() {
prompt.close();
prompt = null;
NGio.cancelLogin();
};
}
else
{
prompt.setText("Connecting...");
openPassportUrl();
}
}, function onLoginComplete(result:ConnectionResult)
{
switch (result)
{
case Success:
{
prompt.setText("Login Successful");
prompt.setButtons(Ok);
prompt.onYes = prompt.close;
}
case Fail(msg):
{
trace("Login Error:" + msg);
prompt.setText("Login failed");
prompt.setButtons(Ok);
prompt.onYes = prompt.close;
}
case Cancelled:
{
if (prompt != null)
{
prompt.setText("Login cancelled by user");
prompt.setButtons(Ok);
prompt.onYes = prompt.close;
}
else
trace("Login cancelled via prompt");
}
}
});
return prompt;
}
static public function showLogout()
{
var user = io.newgrounds.NG.core.user.name;
var prompt = new NgPrompt('Log out of $user?', Yes_No);
prompt.onYes = function() {
NGio.logout();
prompt.close();
};
prompt.onNo = prompt.close;
return prompt;
}
}
#end

View file

@ -1,9 +0,0 @@
# funkin.api.newgrounds
This package contains two main classes:
- `NGUtil` contains utility functions for interacting with the Newgrounds API.
- This includes any functions which scripts should be able to use,
such as retrieving achievement status.
- `NGUnsafe` contains sensitive utility functions for interacting with the Newgrounds API.
- This includes any functions which scripts should not be able to use,
such as writing high scores or posting achievements.

View file

@ -1,9 +1,6 @@
package funkin.audio;
import flash.media.Sound;
#if flash11
import flash.utils.ByteArray;
#end
import flixel.sound.FlxSound;
import flixel.system.FlxAssets.FlxSoundAsset;
import openfl.Assets;

View file

@ -13,8 +13,6 @@ 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;

View file

@ -1,7 +1,6 @@
package funkin.audio;
import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.audio.FunkinSound;
import flixel.tweens.FlxTween;
/**

View file

@ -1,60 +1,76 @@
package funkin.audio.visualize;
import funkin.graphics.FunkinSprite;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.sound.FlxSound;
import funkin.vis.dsp.SpectralAnalyzer;
import funkin.vis.audioclip.frontends.LimeAudioClip;
using Lambda;
@:nullSafety
class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
{
// public var vis:VisShit;
var analyzer:SpectralAnalyzer;
var analyzer:Null<SpectralAnalyzer> = null;
var volumes:Array<Float> = [];
public var snd:FlxSound;
public var snd:Null<FlxSound> = null;
public function new(snd:FlxSound)
static final BAR_COUNT:Int = 7;
// TODO: Make the sprites easier to soft code.
public function new(snd:FlxSound, pixel:Bool)
{
super();
this.snd = snd;
// vis = new VisShit(snd);
// vis.snd = snd;
var visCount = pixel ? (BAR_COUNT + 1) : (BAR_COUNT + 1);
var visScale = pixel ? 6 : 1;
var visFrms:FlxAtlasFrames = Paths.getSparrowAtlas('aBotViz');
var visFrms:FlxAtlasFrames = Paths.getSparrowAtlas(pixel ? 'characters/abotPixel/aBotVizPixel' : 'characters/abot/aBotViz');
// these are the differences in X position, from left to right
var positionX:Array<Float> = [0, 59, 56, 66, 54, 52, 51];
var positionY:Array<Float> = [0, -8, -3.5, -0.4, 0.5, 4.7, 7];
var positionX:Array<Float> = pixel ? [0, 7 * 6, 8 * 6, 9 * 6, 10 * 6, 6 * 6, 7 * 6] : [0, 59, 56, 66, 54, 52, 51];
var positionY:Array<Float> = pixel ? [0, -2 * 6, -1 * 6, 0, 0, 1 * 6, 2 * 6] : [0, -8, -3.5, -0.4, 0.5, 4.7, 7];
for (lol in 1...8)
for (index in 1...visCount)
{
// pushes initial value
volumes.push(0.0);
var sum = function(num:Float, total:Float) return total += num;
var posX:Float = positionX.slice(0, lol).fold(sum, 0);
var posY:Float = positionY.slice(0, lol).fold(sum, 0);
var viz:FlxSprite = new FlxSprite(posX, posY);
// Sum the offsets up to the current index
var sum = function(num:Float, total:Float) return total += num;
var posX:Float = positionX.slice(0, index).fold(sum, 0);
var posY:Float = positionY.slice(0, index).fold(sum, 0);
var viz:FunkinSprite = new FunkinSprite(posX, posY);
viz.frames = visFrms;
viz.antialiasing = pixel ? false : true;
viz.scale.set(visScale, visScale);
add(viz);
var visStr = 'viz';
viz.animation.addByPrefix('VIZ', visStr + lol, 0);
viz.animation.play('VIZ', false, false, 6);
viz.animation.addByPrefix('VIZ', '$visStr${index}0', 0);
viz.animation.play('VIZ', false, false, 1);
}
}
public function initAnalyzer()
public function initAnalyzer():Void
{
if (snd == null) return;
@:privateAccess
analyzer = new SpectralAnalyzer(snd._channel.__audioSource, 7, 0.1, 40);
analyzer = new SpectralAnalyzer(snd._channel.__audioSource, BAR_COUNT, 0.1, 40);
// A-Bot tuning...
analyzer.minDb = -65;
analyzer.maxDb = -25;
analyzer.maxFreq = 22000;
// we use a very low minFreq since some songs use low low subbass like a boss
analyzer.minFreq = 10;
#if desktop
// On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
@ -66,16 +82,17 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
// analyzer.fftN = 2048;
}
public function dumpSound():Void
{
snd = null;
analyzer = null;
}
var visTimer:Float = -1;
var visTimeMax:Float = 1 / 30;
override function update(elapsed:Float)
{
// updateViz();
// updateFFT(elapsed);
//
super.update(elapsed);
}
@ -86,8 +103,8 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
override function draw()
{
if (analyzer != null) drawFFT();
super.draw();
drawFFT();
}
/**
@ -95,17 +112,17 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
*/
function drawFFT():Void
{
var levels = analyzer.getLevels();
var levels = (analyzer != null) ? analyzer.getLevels() : getDefaultLevels();
for (i in 0...min(group.members.length, levels.length))
{
var animFrame:Int = Math.round(levels[i].value * 5);
var animFrame:Int = Math.round(levels[i].value * 6);
#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
// don't display if we're at 0 volume from the level
group.members[i].visible = animFrame > 0;
// decrement our animFrame, so we can get a value from 0-5 for animation frames
animFrame -= 1;
animFrame = Math.floor(Math.min(5, animFrame));
animFrame = Math.floor(Math.max(0, animFrame));
@ -116,79 +133,19 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
}
}
// function updateFFT(elapsed:Float)
// {
// if (vis.snd != null)
// {
// vis.checkAndSetBuffer();
// if (vis.setBuffer)
// {
// var remappedShit:Int = 0;
// if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples));
// else
// remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples));
// var fftSamples:Array<Float> = [];
// var swagBucks = remappedShit;
// for (i in remappedShit...remappedShit + (Std.int((44100 * (1 / 144)))))
// {
// var left = vis.audioData[swagBucks] / 32767;
// var right = vis.audioData[swagBucks + 1] / 32767;
// var balanced = (left + right) / 2;
// swagBucks += 2;
// fftSamples.push(balanced);
// }
// var freqShit = vis.funnyFFT(fftSamples);
// for (i in 0...group.members.length)
// {
// var getSliceShit = function(s:Int) {
// var powShit = FlxMath.remapToRange(s, 0, group.members.length, 0, MathUtil.logBase(10, freqShit[0].length));
// return Math.round(Math.pow(10, powShit));
// };
// // var powShit:Float = getSliceShit(i);
// var hzSliced:Int = getSliceShit(i);
// var sliceLength:Int = Std.int(freqShit[0].length / group.members.length);
// var volSlice = freqShit[0].slice(hzSliced, getSliceShit(i + 1));
// var avgVel:Float = 0;
// for (slice in volSlice)
// {
// avgVel += slice;
// }
// avgVel /= volSlice.length;
// avgVel *= 10000000;
// volumes[i] += avgVel - (elapsed * (volumes[i] * 50));
// var animFrame:Int = Std.int(volumes[i]);
// animFrame = Math.floor(Math.min(5, animFrame));
// animFrame = Math.floor(Math.max(0, animFrame));
// animFrame = Std.int(Math.abs(animFrame - 5)); // shitty dumbass flip, cuz dave got da shit backwards lol!
// group.members[i].animation.curAnim.curFrame = animFrame;
// if (FlxG.keys.justPressed.U)
// {
// trace(avgVel);
// trace(group.members[i].animation.curAnim.curFrame);
// }
// }
// // group.members[0].animation.curAnim.curFrame =
// }
// }
// }
// public function updateViz()
// {
// if (vis.snd != null)
// {
// var remappedShit:Int = 0;
// vis.checkAndSetBuffer();
// if (vis.setBuffer)
// {
// // var startingSample:Int = Std.int(FlxMath.remapToRange)
// if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples));
// for (i in 0...group.members.length)
// {
// var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, remappedShit, remappedShit + 500));
// var left = vis.audioData[sampleApprox] / 32767;
// var animFrame:Int = Std.int(FlxMath.remapToRange(left, -1, 1, 0, 6));
// group.members[i].animation.curAnim.curFrame = animFrame;
// }
// }
// }
// }
/**
* Explicitly define the default levels to draw when the analyzer is not available.
* @return Array<Bar>
*/
static function getDefaultLevels():Array<Bar>
{
var result:Array<Bar> = [];
for (i in 0...BAR_COUNT)
{
result.push({value: 0, peak: 0.0});
}
return result;
}
}

View file

@ -4,7 +4,7 @@ import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.sound.FlxSound;
import flixel.util.FlxColor;
import funkin.audio.visualize.VisShit;
import funkin.audio.visualize.VisShit.CurAudioInfo;
import funkin.graphics.rendering.MeshRender;
import lime.utils.Int16Array;

View file

@ -17,6 +17,16 @@ class AnimationDataUtil
};
}
/**
* @param data
* @param name (adds index to name)
* @return Array<AnimationData>
*/
public static function toNamedArray(data:Array<UnnamedAnimationData>, ?name:String = ""):Array<AnimationData>
{
return data.mapi(function(animItem, ind) return toNamed(animItem, '$name$ind'));
}
public static function toUnnamed(data:AnimationData):UnnamedAnimationData
{
return {
@ -30,6 +40,11 @@ class AnimationDataUtil
frameIndices: data.frameIndices
};
}
public static function toUnnamedArray(data:Array<AnimationData>):Array<UnnamedAnimationData>
{
return data.map(toUnnamed);
}
}
/**

View file

@ -13,7 +13,7 @@ class DialogueBoxRegistry extends BaseRegistry<DialogueBox, DialogueBoxData>
*/
public static final DIALOGUEBOX_DATA_VERSION:thx.semver.Version = "1.1.0";
public static final DIALOGUEBOX_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
public static final DIALOGUEBOX_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.2.0";
public static var instance(get, never):DialogueBoxRegistry;
static var _instance:Null<DialogueBoxRegistry> = null;

View file

@ -1,7 +1,6 @@
package funkin.data.dialogue.speaker;
import funkin.play.cutscene.dialogue.Speaker;
import funkin.data.dialogue.speaker.SpeakerData;
import funkin.play.cutscene.dialogue.ScriptedSpeaker;
class SpeakerRegistry extends BaseRegistry<Speaker, SpeakerData>

View file

@ -1,7 +1,6 @@
package funkin.data.event;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.song.SongData.SongEventData;
import funkin.util.macro.ClassMacro;
import funkin.play.event.ScriptedSongEvent;

View file

@ -1,11 +1,5 @@
package funkin.data.event;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.song.SongData.SongEventData;
import funkin.util.macro.ClassMacro;
import funkin.play.event.ScriptedSongEvent;
@:forward(name, title, type, keys, min, max, step, units, defaultValue, iterator)
abstract SongEventSchema(SongEventSchemaRaw)
{

View file

@ -331,6 +331,10 @@ typedef PlayerResultsAnimationData =
@:default([0, 0])
var offsets:Array<Float>;
@:optional
@:default("both")
var filter:String;
@:optional
@:default(500)
var zIndex:Int;
@ -347,6 +351,10 @@ typedef PlayerResultsAnimationData =
@:default('')
var startFrameLabel:Null<String>;
@:optional
@:default('')
var sound:Null<String>;
@:optional
@:default(true)
var looped:Bool;

View file

@ -5,6 +5,8 @@ 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).
<!-- TODO: Include NoteSplash and NoteStyle changes made to accomodate week 6 note splash/hold stuff -->
## [1.1.0]
### Added
- Added several new `assets`:

View file

@ -187,6 +187,10 @@ typedef NoteStyleAssetData<T> =
@:optional
var isPixel:Bool;
@:default(1.0)
@:optional
var alpha:Float;
/**
* If true, animations will be played on the graphic.
* @default `false` to save performance.
@ -251,6 +255,30 @@ typedef NoteStyleData_NoteSplash =
@:optional
@:default(true)
var enabled:Bool;
@:optional
@:default(24)
var framerateDefault:Int;
@:optional
@:default(2)
var framerateVariance:Int;
@:optional
@:default("normal")
var blendMode:String;
@:optional
var leftSplashes:Array<UnnamedAnimationData>;
@:optional
var downSplashes:Array<UnnamedAnimationData>;
@:optional
var upSplashes:Array<UnnamedAnimationData>;
@:optional
var rightSplashes:Array<UnnamedAnimationData>;
};
typedef NoteStyleData_HoldNoteCover =
@ -262,4 +290,33 @@ typedef NoteStyleData_HoldNoteCover =
@:optional
@:default(true)
var enabled:Bool;
@:optional
var left:NoteStyleData_HoldNoteCoverDirectionData;
@:optional
var down:NoteStyleData_HoldNoteCoverDirectionData;
@:optional
var up:NoteStyleData_HoldNoteCoverDirectionData;
@:optional
var right:NoteStyleData_HoldNoteCoverDirectionData;
};
typedef NoteStyleData_HoldNoteCoverDirectionData =
{
/**
* Optionally specify an asset path to use for this specific animation.
* @:default The assetPath of the main holdNoteCover asset
*/
@:optional
var assetPath:String;
@:optional
var start:UnnamedAnimationData;
@:optional
var hold:UnnamedAnimationData;
@:optional
var end:UnnamedAnimationData;
}

View file

@ -13,7 +13,7 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
*/
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.1.x";
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.2.0";
public static var instance(get, never):NoteStyleRegistry;
static var _instance:Null<NoteStyleRegistry> = null;

View file

@ -1,5 +1,6 @@
package funkin.data.song;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.data.song.SongData;
import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0;
import funkin.data.song.migrator.SongData_v2_1_0.SongMetadata_v2_1_0;
@ -171,7 +172,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
variation = variation ?? Constants.DEFAULT_VARIATION;
// If a version rule is not specified, do not check against it.
if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE))
@ -192,12 +193,13 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
}
}
public function parseEntryMetadataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongMetadata>
public function parseEntryMetadataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version,
?variation:String):Null<SongMetadata>
{
// If a version rule is not specified, do not check against it.
if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE))
{
return parseEntryMetadataRaw(contents, fileName);
return parseEntryMetadataRaw(contents, fileName, variation);
}
else if (VersionUtil.validateVersion(version, "2.1.x"))
{
@ -546,4 +548,39 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return listBaseGameSongIds().indexOf(id) == -1;
});
}
/**
* A list of all difficulties for a specific character.
*/
public function listAllDifficulties(characterId:String):Array<String>
{
var allDifficulties:Array<String> = Constants.DEFAULT_DIFFICULTY_LIST.copy();
var character = PlayerRegistry.instance.fetchEntry(characterId);
if (character == null)
{
trace(' [WARN] Could not locate character $characterId');
return allDifficulties;
}
allDifficulties = [];
for (songId in listEntryIds())
{
var song = fetchEntry(songId);
if (song == null) continue;
for (diff in song.listDifficulties(null, song.getVariationsByCharacter(character)))
{
if (!allDifficulties.contains(diff)) allDifficulties.push(diff);
}
}
if (allDifficulties.length == 0)
{
trace(' [WARN] No difficulties found. Returning default difficulty list.');
allDifficulties = Constants.DEFAULT_DIFFICULTY_LIST.copy();
}
return allDifficulties;
}
}

View file

@ -173,10 +173,10 @@ typedef StageDataProp =
* [1, 1] means the prop moves 1:1 with the camera.
* [0.5, 0.5] means the prop moves half as much as the camera.
* [0, 0] means the prop is not moved.
* @default [0, 0]
* @default [1, 1]
*/
@:optional
@:default([0, 0])
@:default([1, 1])
var scroll:Array<Float>;
/**

View file

@ -1,6 +1,5 @@
package funkin.data.stage;
import funkin.data.stage.StageData;
import funkin.play.stage.Stage;
import funkin.play.stage.ScriptedStage;
@ -13,7 +12,7 @@ class StageRegistry extends BaseRegistry<Stage, StageData>
*/
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.3";
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <=1.0.3";
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0";
public static var instance(get, never):StageRegistry;
static var _instance:Null<StageRegistry> = null;

View file

@ -5,7 +5,7 @@ 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]
## [1.0.1]
- Added `visible` attribute.
## [1.0.0]

View file

@ -2,7 +2,6 @@ package funkin.data.story.level;
import funkin.util.SortUtil;
import funkin.ui.story.Level;
import funkin.data.story.level.LevelData;
import funkin.ui.story.ScriptedLevel;
class LevelRegistry extends BaseRegistry<Level, LevelData>
@ -14,7 +13,7 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
*/
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.1";
public static final LEVEL_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final LEVEL_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0";
public static var instance(get, never):LevelRegistry;
static var _instance:Null<LevelRegistry> = null;

View file

@ -3,7 +3,8 @@ package funkin.effects;
import flixel.addons.effects.FlxTrail;
import funkin.play.stage.Bopper;
import flixel.FlxSprite;
import flixel.system.FlxAssets;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.math.FlxPoint;
/**
* An offshoot of FlxTrail, but accomodates the way Funkin
@ -14,27 +15,41 @@ 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.
* @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)
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);
super(target, graphic, length, delay, alpha, diff);
}
override public function update(elapsed:Float):Void
/**
* An offset applied to the target position whenever a new frame is saved.
*/
public final frameOffset:FlxPoint = FlxPoint.get();
override function destroy():Void
{
super.destroy();
frameOffset.put();
}
override function addTrailFrame():Void
{
super.addTrailFrame();
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);
}
@:privateAccess
frameOffset.set((targ.animOffsets[0] - targ.globalOffsets[0]) * targ.scale.x, (targ.animOffsets[1] - targ.globalOffsets[1]) * targ.scale.y);
super.update(elapsed);
_recentPositions[0]?.subtract(frameOffset.x, frameOffset.y);
}
}
}

View file

@ -7,23 +7,17 @@ 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;

View file

@ -9,7 +9,7 @@ import funkin.graphics.framebuffer.FixedBitmapData;
import openfl.display.BitmapData;
import flixel.math.FlxRect;
import flixel.math.FlxPoint;
import flixel.graphics.frames.FlxFrame;
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
import flixel.FlxCamera;
/**
@ -97,14 +97,58 @@ class FunkinSprite extends FlxSprite
return this;
}
public function loadTextureAsync(key:String, fade:Bool = false):Void
{
var fadeTween:Null<FlxTween> = null;
if (fade)
{
fadeTween = FlxTween.tween(this, {alpha: 0}, 0.25);
}
trace('[ASYNC] Start loading image (${key})');
graphic.persist = true;
openfl.Assets.loadBitmapData(key)
.onComplete(function(bitmapData:openfl.display.BitmapData) {
trace('[ASYNC] Finished loading image');
var cache:Bool = false;
loadBitmapData(bitmapData, cache);
if (fadeTween != null)
{
fadeTween.cancel();
FlxTween.tween(this, {alpha: 1.0}, 0.25);
}
})
.onError(function(error:Dynamic) {
trace('[ASYNC] Failed to load image: ${error}');
if (fadeTween != null)
{
fadeTween.cancel();
this.alpha = 1.0;
}
})
.onProgress(function(progress:Int, total:Int) {
trace('[ASYNC] Loading image progress: ${progress}/${total}');
});
}
/**
* Apply an OpenFL `BitmapData` to this sprite.
* @param input The OpenFL `BitmapData` to apply
* @return This sprite, for chaining
*/
public function loadBitmapData(input:BitmapData):FunkinSprite
public function loadBitmapData(input:BitmapData, cache:Bool = true):FunkinSprite
{
loadGraphic(input);
if (cache)
{
loadGraphic(input);
}
else
{
var graphic:FlxGraphic = FlxGraphic.fromBitmapData(input, false, null, false);
this.graphic = graphic;
this.frames = this.graphic.imageFrame;
}
return this;
}

View file

@ -3,10 +3,8 @@ package funkin.graphics.adobeanimate;
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 flixel.math.FlxPoint;
import flxanimate.animate.FlxKeyFrame;
@ -37,6 +35,11 @@ class FlxAtlasSprite extends FlxAnimate
*/
public var onAnimationComplete:FlxTypedSignal<String->Void> = new FlxTypedSignal();
/**
* Signal dispatched when a looping animation finishes playing.
*/
public var onAnimationLoop:FlxTypedSignal<String->Void> = new FlxTypedSignal();
var currentAnimation:String;
var canPlayOtherAnims:Bool = true;
@ -63,8 +66,6 @@ class FlxAtlasSprite extends FlxAnimate
throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
}
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('');
@ -198,33 +199,44 @@ class FlxAtlasSprite extends FlxAnimate
goToFrameLabel(id);
fr = anim.getFrameLabel(id);
anim.curFrame += startFrame;
// Resume animation if it's paused.
anim.resume();
}
}
override public function update(elapsed:Float)
override public function update(elapsed:Float):Void
{
super.update(elapsed);
}
/**
* Returns true if the animation has finished playing.
* Never true if animation is configured to loop.
* @return Whether the animation has finished playing.
*/
public function isAnimationFinished():Bool
{
return this.anim.finished;
return isLoopComplete();
}
/**
* Returns true if the animation has reached the last frame.
* Can be true even if animation is configured to loop.
* @return Whether the animation has reached the last frame.
*/
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));
if (fr != null)
{
var curFrame = anim.curFrame;
var startFrame = fr.index;
var endFrame = (fr.index + fr.duration);
return (anim.reversed) ? (curFrame < startFrame) : (curFrame >= endFrame);
}
return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1));
}
@ -252,7 +264,7 @@ class FlxAtlasSprite extends FlxAnimate
this.anim.goToFrameLabel(label);
}
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String> = null)
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Array<String>
{
var labels = this.anim.getFrameLabels(layer);
var array = [];
@ -295,16 +307,18 @@ class FlxAtlasSprite extends FlxAnimate
if (isLoopComplete())
{
anim.pause();
_onAnimationComplete();
if (looping)
{
anim.curFrame = (fr != null) ? fr.index : 0;
anim.resume();
_onAnimationLoop();
}
else if (fr != null && anim.curFrame != anim.length - 1)
{
anim.curFrame--;
cleanupAnimation(currentAnimation ?? "");
_onAnimationComplete();
}
}
}
@ -322,11 +336,32 @@ class FlxAtlasSprite extends FlxAnimate
}
}
function _onAnimationLoop():Void
{
if (currentAnimation != null)
{
onAnimationLoop.dispatch(currentAnimation);
}
else
{
onAnimationLoop.dispatch('');
}
}
var prevFrames:Map<Int, FlxFrame> = [];
public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void
{
if (graphic == null || !Assets.exists(graphic))
var cond = false;
if (graphic == null) cond = true;
else
{
if ((graphic is String)) cond = !Assets.exists(graphic)
else
cond = false;
}
if (cond)
{
var prevFrame:Null<FlxFrame> = prevFrames.get(index);
if (prevFrame == null) return;

View file

@ -1,7 +1,6 @@
package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
class BlueFade extends FlxShader

View file

@ -0,0 +1,227 @@
package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxColor;
import flixel.FlxSprite;
import flixel.math.FlxAngle;
import flixel.graphics.frames.FlxFrame;
import openfl.display.BitmapData;
/*
A screenspace version of the DropShadowShader.. currently the only way to use this effect with
FlxAnimate :(
*/
class DropShadowScreenspace extends DropShadowShader
{
/*
The current zoom of the camera. Needed to figure out how much to multiply the drop shadow size.
*/
public var curZoom(default, set):Float;
function set_curZoom(val:Float):Float
{
curZoom = val;
zoom.value = [val];
return val;
}
@:glFragmentSource('
#pragma header
// This shader aims to mostly recreate how Adobe Animate/Flash handles drop shadows, but its main use here is for rim lighting.
// this shader also includes a recreation of the Animate/Flash "Adjust Color" filter,
// which was kindly provided and written by Rozebud https://github.com/ThatRozebudDude ( thank u rozebud :) )
// Adapted from Andrey-Postelzhuks shader found here: https://forum.unity.com/threads/hue-saturation-brightness-contrast-shader.260649/
// Hue rotation stuff is from here: https://www.w3.org/TR/filter-effects/#feColorMatrixElement
// equals (frame.left, frame.top, frame.right, frame.bottom)
uniform vec4 uFrameBounds;
uniform float ang;
uniform float dist;
uniform float str;
uniform float thr;
// need to account for rotated frames... oops
uniform float angOffset;
uniform sampler2D altMask;
uniform bool useMask;
uniform float thr2;
uniform vec3 dropColor;
uniform float hue;
uniform float saturation;
uniform float brightness;
uniform float contrast;
uniform float zoom;
uniform float AA_STAGES;
const vec3 grayscaleValues = vec3(0.3098039215686275, 0.607843137254902, 0.0823529411764706);
const float e = 2.718281828459045;
vec3 applyHueRotate(vec3 aColor, float aHue){
float angle = radians(aHue);
mat3 m1 = mat3(0.213, 0.213, 0.213, 0.715, 0.715, 0.715, 0.072, 0.072, 0.072);
mat3 m2 = mat3(0.787, -0.213, -0.213, -0.715, 0.285, -0.715, -0.072, -0.072, 0.928);
mat3 m3 = mat3(-0.213, 0.143, -0.787, -0.715, 0.140, 0.715, 0.928, -0.283, 0.072);
mat3 m = m1 + cos(angle) * m2 + sin(angle) * m3;
return m * aColor;
}
vec3 applySaturation(vec3 aColor, float value){
if(value > 0.0){ value = value * 3.0; }
value = (1.0 + (value / 100.0));
vec3 grayscale = vec3(dot(aColor, grayscaleValues));
return clamp(mix(grayscale, aColor, value), 0.0, 1.0);
}
vec3 applyContrast(vec3 aColor, float value){
value = (1.0 + (value / 100.0));
if(value > 1.0){
value = (((0.00852259 * pow(e, 4.76454 * (value - 1.0))) * 1.01) - 0.0086078159) * 10.0; //Just roll with it...
value += 1.0;
}
return clamp((aColor - 0.25) * value + 0.25, 0.0, 1.0);
}
vec3 applyHSBCEffect(vec3 color){
//Brightness
color = color + ((brightness) / 255.0);
//Hue
color = applyHueRotate(color, hue);
//Contrast
color = applyContrast(color, contrast);
//Saturation
color = applySaturation(color, saturation);
return color;
}
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);
}
float intensityPass(vec2 fragCoord, float curThreshold, bool useMask) {
vec4 col = texture2D(bitmap, fragCoord);
float maskIntensity = 0.0;
if(useMask == true){
maskIntensity = mix(0.0, 1.0, texture2D(altMask, fragCoord).b);
}
if(col.a == 0.0){
return 0.0;
}
float intensity = dot(col.rgb, vec3(0.3098, 0.6078, 0.0823));
intensity = maskIntensity > 0.0 ? float(intensity > thr2) : float(intensity > thr);
return intensity;
}
// essentially just stole this from the AngleMask shader but repurposed it to smooth
// the threshold because without any sort of smoothing it produces horrible edges
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
// In GLSL 100, we need to use constant loop bounds
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop
// The actual number of iterations will be controlled by a condition inside
const int MAX_AA = 8; // This should be large enough for most uses
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
float color = intensityPass(fragCoord, curThreshold, useMask);
for (int i = 0; i < MAX_AA * MAX_AA; i++) {
// Calculate x and y from i
int x = i / MAX_AA;
int y = i - (MAX_AA * int(i/MAX_AA)); // poor mans modulus
// Skip iterations beyond our desired AA_STAGES
if (float(x) >= AA_STAGES || float(y) >= AA_STAGES) {
continue;
}
vec2 offset = AA_JITTER * (2.0 * hash22(vec2(float(x), float(y))) - 1.0) / openfl_TextureSize.xy;
color += intensityPass(fragCoord + offset, curThreshold, useMask);
}
return color / AA_TOTAL_PASSES;
}
vec3 createDropShadow(vec3 col, float curThreshold, bool useMask) {
// essentially a mask so that areas under the threshold dont show the rimlight (mainly the outlines)
float intensity = antialias(openfl_TextureCoordv, curThreshold, useMask);
// the distance the dropshadow moves needs to be correctly scaled based on the texture size
vec2 imageRatio = vec2(1.0/openfl_TextureSize.x, 1.0/openfl_TextureSize.y);
// check the pixel in the direction and distance specified
vec2 checkedPixel = vec2(openfl_TextureCoordv.x + ((dist*zoom) * cos(ang + angOffset) * imageRatio.x), openfl_TextureCoordv.y - ((dist*zoom) * sin(ang + angOffset) * imageRatio.y));
// multiplier for the intensity of the drop shadow
float dropShadowAmount = 0.0;
dropShadowAmount = texture2D(bitmap, checkedPixel).a;
// add the dropshadow color based on the amount, strength, and intensity
col.rgb += dropColor.rgb * ((1.0 - (dropShadowAmount * str))*intensity);
return col;
}
void main()
{
vec4 col = texture2D(bitmap, openfl_TextureCoordv);
vec3 unpremultipliedColor = col.a > 0.0 ? col.rgb / col.a : col.rgb;
vec3 outColor = applyHSBCEffect(unpremultipliedColor);
outColor = createDropShadow(outColor, thr, useMask);
gl_FragColor = vec4(outColor.rgb * col.a, col.a);
}
')
override public function new()
{
super();
angle = 90;
strength = 1;
distance = 15;
threshold = 0.1;
baseHue = 0;
baseSaturation = 0;
baseBrightness = 0;
baseContrast = 0;
useAltMask = false;
color = 0xFFDFEF3C;
antialiasAmt = 2;
curZoom = 1;
angOffset.value = [0];
}
}

View file

@ -0,0 +1,451 @@
package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxColor;
import flixel.FlxSprite;
import flixel.math.FlxAngle;
import flixel.graphics.frames.FlxFrame;
import openfl.display.BitmapData;
/*
A shader that aims to *mostly recreate how Adobe Animate/Flash handles drop shadows, but its main use here is for rim lighting.
Has options for color, angle, distance, and a threshold to not cast the shadow on parts like outlines.
Can also be supplied a secondary mask which can then have an alternate threshold, for when sprites have too many conflicting colors
for the drop shadow to look right (e.g. the tankmen on GF's speakers).
Also has an Adjust Color shader in here so they can work together when needed.
*/
class DropShadowShader extends FlxShader
{
/*
The color of the drop shadow.
*/
public var color(default, set):FlxColor;
/*
The angle of the drop shadow.
for reference, depending on the angle, the affected side will be:
0 = RIGHT
90 = UP
180 = LEFT
270 = DOWN
*/
public var angle(default, set):Float;
/*
The distance or size of the drop shadow, in pixels,
relative to the texture itself... NOT the camera.
*/
public var distance(default, set):Float;
/*
The strength of the drop shadow.
Effectively just an alpha multiplier.
*/
public var strength(default, set):Float;
/*
The brightness threshold for the drop shadow.
Anything below this number will NOT be affected by the drop shadow shader.
A value of 0 effectively means theres no threshold, and vice versa.
*/
public var threshold(default, set):Float;
/*
The amount of antialias samples per-pixel,
used to smooth out any hard edges the brightness thresholding creates.
Defaults to 2, and 0 will remove any smoothing.
*/
public var antialiasAmt(default, set):Float;
/*
Whether the shader should try and use the alternate mask.
False by default.
*/
public var useAltMask(default, set):Bool;
/*
The image for the alternate mask.
At the moment, it uses the blue channel to specify what is or isnt going to use the alternate threshold.
(its kinda sloppy rn i need to make it work a little nicer)
TODO: maybe have a sort of "threshold intensity texture" as well? where higher/lower values indicate threshold strength..
*/
public var altMaskImage(default, set):BitmapData;
/*
An alternate brightness threshold for the drop shadow.
Anything below this number will NOT be affected by the drop shadow shader,
but ONLY when the pixel is within the mask.
*/
public var maskThreshold(default, set):Float;
/*
The FlxSprite that the shader should get the frame data from.
Needed to keep the drop shadow shader in the correct bounds and rotation.
*/
public var attachedSprite(default, set):FlxSprite;
/*
The hue component of the Adjust Color part of the shader.
*/
public var baseHue(default, set):Float;
/*
The saturation component of the Adjust Color part of the shader.
*/
public var baseSaturation(default, set):Float;
/*
The brightness component of the Adjust Color part of the shader.
*/
public var baseBrightness(default, set):Float;
/*
The contrast component of the Adjust Color part of the shader.
*/
public var baseContrast(default, set):Float;
/*
Sets all 4 adjust color values.
*/
public function setAdjustColor(b:Float, h:Float, c:Float, s:Float)
{
baseBrightness = b;
baseHue = h;
baseContrast = c;
baseSaturation = s;
}
function set_baseHue(val:Float):Float
{
baseHue = val;
hue.value = [val];
return val;
}
function set_baseSaturation(val:Float):Float
{
baseSaturation = val;
saturation.value = [val];
return val;
}
function set_baseBrightness(val:Float):Float
{
baseBrightness = val;
brightness.value = [val];
return val;
}
function set_baseContrast(val:Float):Float
{
baseContrast = val;
contrast.value = [val];
return val;
}
function set_threshold(val:Float):Float
{
threshold = val;
thr.value = [val];
return val;
}
function set_antialiasAmt(val:Float):Float
{
antialiasAmt = val;
AA_STAGES.value = [val];
return val;
}
function set_color(col:FlxColor):FlxColor
{
color = col;
dropColor.value = [color.red / 255, color.green / 255, color.blue / 255];
return color;
}
function set_angle(val:Float):Float
{
angle = val;
ang.value = [angle * FlxAngle.TO_RAD];
return angle;
}
function set_distance(val:Float):Float
{
distance = val;
dist.value = [val];
return val;
}
function set_strength(val:Float):Float
{
strength = val;
str.value = [val];
return val;
}
function set_attachedSprite(spr:FlxSprite):FlxSprite
{
attachedSprite = spr;
updateFrameInfo(attachedSprite.frame);
return spr;
}
/*
Loads an image for the mask.
While you *could* directly set the value of the mask, this function works for both HTML5 and desktop targets.
*/
public function loadAltMask(path:String)
{
#if html5
BitmapData.loadFromFile(path).onComplete(function(bmp:BitmapData) {
altMaskImage = bmp;
});
#else
altMaskImage = BitmapData.fromFile(path);
#end
}
/*
Should be called on the animation.callback of the attached sprite.
TODO: figure out why the reference to the attachedSprite breaks on web??
*/
public function onAttachedFrame(name, frameNum, frameIndex)
{
if (attachedSprite != null) updateFrameInfo(attachedSprite.frame);
}
/*
Updates the frame bounds and angle offset of the sprite for the shader.
*/
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];
// if a frame is rotated the shader will look completely wrong lol
angOffset.value = [frame.angle * FlxAngle.TO_RAD];
}
function set_altMaskImage(_bitmapData:BitmapData):BitmapData
{
altMask.input = _bitmapData;
return _bitmapData;
}
function set_maskThreshold(val:Float):Float
{
maskThreshold = val;
thr2.value = [val];
return val;
}
function set_useAltMask(val:Bool):Bool
{
useAltMask = val;
useMask.value = [val];
return val;
}
@:glFragmentSource('
#pragma header
// This shader aims to mostly recreate how Adobe Animate/Flash handles drop shadows, but its main use here is for rim lighting.
// this shader also includes a recreation of the Animate/Flash "Adjust Color" filter,
// which was kindly provided and written by Rozebud https://github.com/ThatRozebudDude ( thank u rozebud :) )
// Adapted from Andrey-Postelzhuks shader found here: https://forum.unity.com/threads/hue-saturation-brightness-contrast-shader.260649/
// Hue rotation stuff is from here: https://www.w3.org/TR/filter-effects/#feColorMatrixElement
// equals (frame.left, frame.top, frame.right, frame.bottom)
uniform vec4 uFrameBounds;
uniform float ang;
uniform float dist;
uniform float str;
uniform float thr;
// need to account for rotated frames... oops
uniform float angOffset;
uniform sampler2D altMask;
uniform bool useMask;
uniform float thr2;
uniform vec3 dropColor;
uniform float hue;
uniform float saturation;
uniform float brightness;
uniform float contrast;
uniform float AA_STAGES;
const vec3 grayscaleValues = vec3(0.3098039215686275, 0.607843137254902, 0.0823529411764706);
const float e = 2.718281828459045;
vec3 applyHueRotate(vec3 aColor, float aHue){
float angle = radians(aHue);
mat3 m1 = mat3(0.213, 0.213, 0.213, 0.715, 0.715, 0.715, 0.072, 0.072, 0.072);
mat3 m2 = mat3(0.787, -0.213, -0.213, -0.715, 0.285, -0.715, -0.072, -0.072, 0.928);
mat3 m3 = mat3(-0.213, 0.143, -0.787, -0.715, 0.140, 0.715, 0.928, -0.283, 0.072);
mat3 m = m1 + cos(angle) * m2 + sin(angle) * m3;
return m * aColor;
}
vec3 applySaturation(vec3 aColor, float value){
if(value > 0.0){ value = value * 3.0; }
value = (1.0 + (value / 100.0));
vec3 grayscale = vec3(dot(aColor, grayscaleValues));
return clamp(mix(grayscale, aColor, value), 0.0, 1.0);
}
vec3 applyContrast(vec3 aColor, float value){
value = (1.0 + (value / 100.0));
if(value > 1.0){
value = (((0.00852259 * pow(e, 4.76454 * (value - 1.0))) * 1.01) - 0.0086078159) * 10.0; //Just roll with it...
value += 1.0;
}
return clamp((aColor - 0.25) * value + 0.25, 0.0, 1.0);
}
vec3 applyHSBCEffect(vec3 color){
//Brightness
color = color + ((brightness) / 255.0);
//Hue
color = applyHueRotate(color, hue);
//Contrast
color = applyContrast(color, contrast);
//Saturation
color = applySaturation(color, saturation);
return color;
}
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);
}
float intensityPass(vec2 fragCoord, float curThreshold, bool useMask) {
vec4 col = texture2D(bitmap, fragCoord);
float maskIntensity = 0.0;
if(useMask == true){
maskIntensity = mix(0.0, 1.0, texture2D(altMask, fragCoord).b);
}
if(col.a == 0.0){
return 0.0;
}
float intensity = dot(col.rgb, vec3(0.3098, 0.6078, 0.0823));
intensity = maskIntensity > 0.0 ? float(intensity > thr2) : float(intensity > thr);
return intensity;
}
// essentially just stole this from the AngleMask shader but repurposed it to smooth
// the threshold because without any sort of smoothing it produces horrible edges
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
// In GLSL 100, we need to use constant loop bounds
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop
// The actual number of iterations will be controlled by a condition inside
const int MAX_AA = 8; // This should be large enough for most uses
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
float color = intensityPass(fragCoord, curThreshold, useMask);
for (int i = 0; i < MAX_AA * MAX_AA; i++) {
// Calculate x and y from i
int x = i / MAX_AA;
int y = i - (MAX_AA * int(i/MAX_AA)); // poor mans modulus
// Skip iterations beyond our desired AA_STAGES
if (float(x) >= AA_STAGES || float(y) >= AA_STAGES) {
continue;
}
vec2 offset = AA_JITTER * (2.0 * hash22(vec2(float(x), float(y))) - 1.0) / openfl_TextureSize.xy;
color += intensityPass(fragCoord + offset, curThreshold, useMask);
}
return color / AA_TOTAL_PASSES;
}
vec3 createDropShadow(vec3 col, float curThreshold, bool useMask) {
// essentially a mask so that areas under the threshold dont show the rimlight (mainly the outlines)
float intensity = antialias(openfl_TextureCoordv, curThreshold, useMask);
// the distance the dropshadow moves needs to be correctly scaled based on the texture size
vec2 imageRatio = vec2(1.0/openfl_TextureSize.x, 1.0/openfl_TextureSize.y);
// check the pixel in the direction and distance specified
vec2 checkedPixel = vec2(openfl_TextureCoordv.x + (dist * cos(ang + angOffset) * imageRatio.x), openfl_TextureCoordv.y - (dist * sin(ang + angOffset) * imageRatio.y));
// multiplier for the intensity of the drop shadow
float dropShadowAmount = 0.0;
if(checkedPixel.x > uFrameBounds.x && checkedPixel.y > uFrameBounds.y && checkedPixel.x < uFrameBounds.z && checkedPixel.y < uFrameBounds.w){
dropShadowAmount = texture2D(bitmap, checkedPixel).a;
}
// add the dropshadow color based on the amount, strength, and intensity
col.rgb += dropColor.rgb * ((1.0 - (dropShadowAmount * str))*intensity);
return col;
}
void main()
{
vec4 col = texture2D(bitmap, openfl_TextureCoordv);
vec3 unpremultipliedColor = col.a > 0.0 ? col.rgb / col.a : col.rgb;
vec3 outColor = applyHSBCEffect(unpremultipliedColor);
outColor = createDropShadow(outColor, thr, useMask);
gl_FragColor = vec4(outColor.rgb * col.a, col.a);
}
')
public function new()
{
super();
angle = 0;
strength = 1;
distance = 15;
threshold = 0.1;
baseHue = 0;
baseSaturation = 0;
baseBrightness = 0;
baseContrast = 0;
antialiasAmt = 2;
useAltMask = false;
angOffset.value = [0];
}
}

View file

@ -8,13 +8,13 @@ class HSVShader extends FlxRuntimeShader
public var saturation(default, set):Float;
public var value(default, set):Float;
public function new()
public function new(h:Float = 1, s:Float = 1, v:Float = 1)
{
super(Assets.getText(Paths.frag('hsv')));
FlxG.debugger.addTrackerProfile(new TrackerProfile(HSVShader, ['hue', 'saturation', 'value']));
hue = 1;
saturation = 1;
value = 1;
hue = h;
saturation = s;
value = v;
}
function set_hue(value:Float):Float

View file

@ -1,6 +1,5 @@
package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
import openfl.display.BitmapData;
import openfl.display.ShaderParameter;
import openfl.display.ShaderParameterType;

View file

@ -1,7 +1,6 @@
package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxColor;
import openfl.display.BitmapData;
class TextureSwap extends FlxShader
@ -9,6 +8,17 @@ class TextureSwap extends FlxShader
public var swappedImage(default, set):BitmapData;
public var amount(default, set):Float;
public function loadSwapImage(path:String)
{
#if html5
BitmapData.loadFromFile(path).onComplete(function(bmp:BitmapData) {
swappedImage = bmp;
});
#else
swappedImage = BitmapData.fromFile(path);
#end
}
function set_swappedImage(_bitmapData:BitmapData):BitmapData
{
image.input = _bitmapData;

View file

@ -1,6 +1,5 @@
package funkin.graphics.shaders;
import flixel.math.FlxPoint;
import flixel.system.FlxAssets.FlxShader;
class TitleOutline extends FlxShader

View file

@ -1,9 +1,7 @@
package funkin.graphics.video;
import flixel.util.FlxColor;
import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.audio.FunkinSound;
import openfl.display3D.textures.TextureBase;
import openfl.events.NetStatusEvent;
import openfl.media.SoundTransform;
import openfl.media.Video;
@ -12,8 +10,8 @@ import openfl.net.NetStream;
/**
* Plays a video via a NetStream. Only works on HTML5.
* This does NOT replace hxCodec, nor does hxCodec replace this.
* hxCodec only works on desktop and does not work on HTML5!
* This does NOT replace hxvlc, nor does hxvlc replace this.
* hxvlc only works on desktop and does not work on HTML5!
*/
class FlxVideo extends FunkinSprite
{

View file

@ -1,34 +1,17 @@
package funkin.graphics.video;
#if hxCodec
import hxcodec.flixel.FlxVideoSprite;
#if hxvlc
import hxvlc.flixel.FlxVideoSprite;
/**
* Not to be confused with FlxVideo, this is a hxcodec based video class
* Not to be confused with FlxVideo, this is a hxvlc 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

View file

@ -2,23 +2,17 @@ package funkin.input;
import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxDirectionFlags;
import flixel.FlxObject;
import flixel.input.FlxInput;
import flixel.input.FlxInput.FlxInputState;
import flixel.input.actions.FlxAction;
import flixel.input.actions.FlxActionInput;
import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogClickAndDragMouseMotion;
import flixel.input.actions.FlxActionInputDigital;
import flixel.input.actions.FlxActionManager;
import flixel.input.actions.FlxActionSet;
import flixel.input.android.FlxAndroidKey;
import flixel.input.gamepad.FlxGamepadButton;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
import flixel.input.mouse.FlxMouseButton.FlxMouseButtonID;
import flixel.math.FlxAngle;
import flixel.math.FlxPoint;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import lime.ui.Haptic;
/**
@ -423,6 +417,7 @@ class Controls extends FlxActionSet
public function getDialogueName(action:FlxActionDigital, ?ignoreSurrounding:Bool = false):String
{
if (action.inputs.length == 0) return 'N/A';
var input = action.inputs[0];
if (ignoreSurrounding == false)
{
@ -731,7 +726,7 @@ class Controls extends FlxActionSet
forEachBound(control, function(action, state) addKeys(action, keys, state));
}
public function bindSwipe(control:Control, swipeDir:Int = FlxDirectionFlags.UP, ?swpLength:Float = 90)
public function bindSwipe(control:Control, swipeDir:FlxDirectionFlags = FlxDirectionFlags.UP, ?swpLength:Float = 90)
{
forEachBound(control, function(action, press) action.add(new FlxActionInputDigitalMobileSwipeGameplay(swipeDir, press, swpLength)));
}
@ -1483,9 +1478,9 @@ class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital
var activateLength:Float = 90;
var hapticPressure:Int = 100;
public function new(swipeDir:Int = FlxDirectionFlags.ANY, Trigger:FlxInputState, ?swipeLength:Float = 90)
public function new(swipeDir:FlxDirectionFlags = FlxDirectionFlags.ANY, Trigger:FlxInputState, ?swipeLength:Float = 90)
{
super(OTHER, swipeDir, Trigger);
super(OTHER, swipeDir.toInt(), Trigger);
activateLength = swipeLength;
}
@ -1567,16 +1562,21 @@ class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital
case JUST_PRESSED:
if (swp.touchLength >= activateLength)
{
switch (inputID)
if (inputID == FlxDirectionFlags.UP.toInt())
{
case FlxDirectionFlags.UP:
if (degAngle >= 45 && degAngle <= 90 + 45) return properTouch(swp);
case FlxDirectionFlags.DOWN:
if (-degAngle >= 45 && -degAngle <= 90 + 45) return properTouch(swp);
case FlxDirectionFlags.LEFT:
if (degAngle <= 45 && -degAngle <= 45) return properTouch(swp);
case FlxDirectionFlags.RIGHT:
if (degAngle >= 90 + 45 && degAngle <= -90 + -45) return properTouch(swp);
if (degAngle >= 45 && degAngle <= 90 + 45) return properTouch(swp);
}
else if (inputID == FlxDirectionFlags.DOWN.toInt())
{
if (-degAngle >= 45 && -degAngle <= 90 + 45) return properTouch(swp);
}
else if (inputID == FlxDirectionFlags.LEFT.toInt())
{
if (degAngle <= 45 && -degAngle <= 45) return properTouch(swp);
}
else if (inputID == FlxDirectionFlags.RIGHT.toInt())
{
if (degAngle >= 90 + 45 && degAngle <= -90 + -45) return properTouch(swp);
}
}
default:

View file

@ -151,7 +151,7 @@ class PolymodHandler
frameworkParams: buildFrameworkParams(),
// List of filenames to ignore in mods. Use the default list to ignore the metadata file, etc.
ignoredFiles: Polymod.getDefaultIgnoreList(),
ignoredFiles: buildIgnoreList(),
// Parsing rules for various data formats.
parseRules: buildParseRules(),
@ -257,12 +257,12 @@ class PolymodHandler
Polymod.blacklistImport('Sys');
// `Reflect`
// Reflect.callMethod() can access blacklisted packages
Polymod.blacklistImport('Reflect');
// Reflect.callMethod() can access blacklisted packages, but some functions are whitelisted
Polymod.addImportAlias('Reflect', funkin.util.ReflectUtil);
// `Type`
// Type.createInstance(Type.resolveClass()) can access blacklisted packages
Polymod.blacklistImport('Type');
// Type.createInstance(Type.resolveClass()) can access blacklisted packages, but some functions are whitelisted
Polymod.addImportAlias('Type', funkin.util.ReflectUtil);
// `cpp.Lib`
// Lib.load() can load malicious DLLs
@ -296,6 +296,15 @@ class PolymodHandler
// Can load native processes on the host operating system.
Polymod.blacklistImport('openfl.desktop.NativeProcess');
// `funkin.api.*`
// Contains functions which may allow for cheating and such.
for (cls in ClassMacro.listClassesInPackage('funkin.api'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// `polymod.*`
// Contains functions which may allow for un-blacklisting other modules.
for (cls in ClassMacro.listClassesInPackage('polymod'))
@ -305,6 +314,24 @@ class PolymodHandler
Polymod.blacklistImport(className);
}
// `funkin.api.newgrounds.*`
// Contains functions which allow for cheating medals and leaderboards.
for (cls in ClassMacro.listClassesInPackage('funkin.api.newgrounds'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// `io.newgrounds.*`
// Contains functions which allow for cheating medals and leaderboards.
for (cls in ClassMacro.listClassesInPackage('io.newgrounds'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// `sys.*`
// Access to system utilities such as the file system.
for (cls in ClassMacro.listClassesInPackage('sys'))
@ -315,6 +342,21 @@ class PolymodHandler
}
}
/**
* Build a list of file paths that will be ignored in mods.
*/
static function buildIgnoreList():Array<String>
{
var result = Polymod.getDefaultIgnoreList();
result.push('.git');
result.push('.gitignore');
result.push('.gitattributes');
result.push('README.md');
return result;
}
static function buildParseRules():polymod.format.ParseRules
{
var output:polymod.format.ParseRules = polymod.format.ParseRules.getDefault();

View file

@ -8,7 +8,6 @@ import funkin.play.notes.NoteSprite;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.Countdown.CountdownStep;
import funkin.play.notes.NoteDirection;
import openfl.events.EventType;
import openfl.events.KeyboardEvent;
/**

View file

@ -2,10 +2,6 @@ package funkin.play;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.FlxSprite;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer;

View file

@ -1,5 +1,7 @@
package funkin.play;
import flixel.FlxState;
import funkin.data.freeplay.player.PlayerRegistry;
import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
@ -145,10 +147,13 @@ class GameOverSubState extends MusicBeatSubState
else
{
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
boyfriend.canPlayOtherAnims = true;
boyfriend.isDead = true;
add(boyfriend);
boyfriend.resetCharacter();
if (boyfriend != null)
{
boyfriend.canPlayOtherAnims = true;
boyfriend.isDead = true;
add(boyfriend);
boyfriend.resetCharacter();
}
}
setCameraTarget();
@ -269,6 +274,14 @@ class GameOverSubState extends MusicBeatSubState
// PlayState.seenCutscene = false; // old thing...
if (gameOverMusic != null) gameOverMusic.stop();
// Stop death quotes immediately.
hasPlayedDeathQuote = true;
if (deathQuoteSound != null)
{
deathQuoteSound.stop();
deathQuoteSound = null;
}
if (isChartingMode)
{
this.close();
@ -276,13 +289,26 @@ class GameOverSubState extends MusicBeatSubState
PlayState.instance.close(); // This only works because PlayState is a substate!
return;
}
else if (PlayStatePlaylist.isStoryMode)
{
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
}
else
{
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(sticker)));
var targetState:funkin.ui.transition.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) ->
new StoryMenuState(sticker) : (sticker) -> FreeplayState.build(sticker);
if (PlayStatePlaylist.isStoryMode)
{
PlayStatePlaylist.reset();
}
var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player);
var stickerSet = (playerCharacterId == "pico") ? "stickers-set-2" : "stickers-set-1";
var stickerPack = switch (PlayState.instance.currentChart.song.id)
{
case "tutorial": "tutorial";
case "darnell" | "lit-up" | "2hot": "weekend";
default: "all";
};
openSubState(new funkin.ui.transition.StickerSubState({targetState: targetState, stickerSet: stickerSet, stickerPack: stickerPack}));
}
}
@ -301,26 +327,23 @@ class GameOverSubState extends MusicBeatSubState
else
{
// Music hasn't started yet.
switch (PlayStatePlaylist.campaignId)
if (boyfriend.getDeathQuote() != null)
{
// TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded.
// This will simplify the class and make it easier for mods to add death quotes.
case 'week7':
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
{
playingJeffQuote = true;
playJeffQuote();
// Start music at lower volume
startDeathMusic(0.2, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
default:
// Start music at normal volume once the initial death animation finishes.
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
{
startDeathMusic(1.0, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !hasPlayedDeathQuote)
{
hasPlayedDeathQuote = true;
playDeathQuote();
}
}
else
{
// Start music at normal volume once the initial death animation finishes.
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
{
startDeathMusic(1.0, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
}
}
}
@ -329,6 +352,33 @@ class GameOverSubState extends MusicBeatSubState
super.update(elapsed);
}
var deathQuoteSound:Null<FunkinSound> = null;
function playDeathQuote():Void
{
if (boyfriend == null) return;
var deathQuote = boyfriend.getDeathQuote();
if (deathQuote == null) return;
if (deathQuoteSound != null)
{
deathQuoteSound.stop();
deathQuoteSound = null;
}
// Start music at lower volume
startDeathMusic(0.2, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
deathQuoteSound = FunkinSound.playOnce(deathQuote, function() {
// Once the quote ends, fade in the game over music.
if (!isEnding && gameOverMusic != null)
{
gameOverMusic.fadeIn(4, 0.2, 1);
}
});
}
/**
* Do behavior which occurs when you confirm and move to restart the level.
*/
@ -339,6 +389,14 @@ class GameOverSubState extends MusicBeatSubState
isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
// Stop death quotes immediately.
hasPlayedDeathQuote = true;
if (deathQuoteSound != null)
{
deathQuoteSound.stop();
deathQuoteSound = null;
}
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else
{
@ -487,26 +545,7 @@ class GameOverSubState extends MusicBeatSubState
}
}
var playingJeffQuote:Bool = false;
/**
* Week 7-specific hardcoded behavior, to play a custom death quote.
* TODO: Make this a module somehow.
*/
function playJeffQuote():Void
{
var randomCensor:Array<Int> = [];
if (!Preferences.naughtyness) randomCensor = [1, 3, 8, 13, 17, 21];
FunkinSound.playOnce(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), function() {
// Once the quote ends, fade in the game over music.
if (!isEnding && gameOverMusic != null)
{
gameOverMusic.fadeIn(4, 0.2, 1);
}
});
}
var hasPlayedDeathQuote:Bool = false;
public override function destroy():Void
{

View file

@ -1,8 +1,7 @@
package funkin.play;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxAtlasFrames;
import funkin.play.PlayState;
import funkin.play.PlayState.PlayStateParams;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import flixel.addons.transition.FlxTransitionableState;

View file

@ -1,27 +1,23 @@
package funkin.play;
import flixel.FlxState;
import funkin.ui.story.StoryMenuState;
import funkin.data.freeplay.player.PlayerRegistry;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.data.song.SongRegistry;
import funkin.ui.freeplay.FreeplayState;
import funkin.graphics.FunkinSprite;
import funkin.play.cutscene.VideoCutscene;
import funkin.play.PlayState;
import funkin.ui.AtlasText;
import funkin.ui.debug.latency.LatencyState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.transition.StickerSubState;
/**
* Parameters for initializing the PauseSubState.
@ -126,7 +122,10 @@ class PauseSubState extends MusicBeatSubState
* Disallow input until transitions are complete!
* This prevents the pause menu from immediately closing when opened, among other things.
*/
public var allowInput:Bool = false;
public var allowInput:Bool = true;
// If this is true, it means we are frame 1 of our substate.
var justOpened:Bool = true;
/**
* The entries currently displayed in the pause menu.
@ -395,10 +394,6 @@ class PauseSubState extends MusicBeatSubState
FlxTween.tween(child, {alpha: 1, y: child.y + 5}, 1.8, {ease: FlxEase.quartOut, startDelay: delay});
delay += 0.1;
}
new FlxTimer().start(0.2, (_) -> {
allowInput = true;
});
}
// ===============
@ -421,15 +416,18 @@ class PauseSubState extends MusicBeatSubState
changeSelection(1);
}
if (controls.ACCEPT)
if (controls.ACCEPT && !justOpened)
{
currentMenuEntries[currentEntry].callback(this);
}
else if (controls.PAUSE)
else if (controls.PAUSE && !justOpened)
{
resume(this);
}
// we only want justOpened to be true for 1 single frame, when we first get into the pause menu substate
justOpened = false;
#if FEATURE_DEBUG_FUNCTIONS
// to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H)
@ -438,6 +436,7 @@ class PauseSubState extends MusicBeatSubState
metadata.visible = visible;
menuEntryText.visible = visible;
background.visible = visible;
this.bgColor = visible ? 0x99000000 : 0x00000000; // 60% or fully transparent black
}
#end
@ -738,15 +737,25 @@ class PauseSubState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
var targetState:funkin.ui.transition.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) -> new StoryMenuState(sticker) : (sticker) ->
FreeplayState.build(sticker);
// Do this AFTER because this resets the value of isStoryMode!
if (PlayStatePlaylist.isStoryMode)
{
PlayStatePlaylist.reset();
state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.story.StoryMenuState(sticker)));
}
else
var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player);
var stickerSet = (playerCharacterId == "pico") ? "stickers-set-2" : "stickers-set-1";
var stickerPack = switch (PlayState.instance.currentChart.song.id)
{
state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker)));
}
case "tutorial": "tutorial";
case "darnell" | "lit-up" | "2hot": "weekend";
default: "all";
};
state.openSubState(new funkin.ui.transition.StickerSubState({targetState: targetState, stickerSet: stickerSet, stickerPack: stickerPack}));
}
/**

View file

@ -13,7 +13,6 @@ import flixel.ui.FlxBar;
import flixel.util.FlxColor;
import flixel.util.FlxStringUtil;
import flixel.util.FlxTimer;
import funkin.api.newgrounds.NGio;
import funkin.audio.FunkinSound;
import funkin.audio.VoicesGroup;
import funkin.data.dialogue.conversation.ConversationRegistry;
@ -29,6 +28,7 @@ import funkin.graphics.FunkinSprite;
import funkin.Highscore.Tallies;
import funkin.input.PreciseInputManager;
import funkin.modding.events.ScriptEvent;
import funkin.api.newgrounds.Events;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
@ -58,6 +58,10 @@ import haxe.Int64;
#if FEATURE_DISCORD_RPC
import funkin.api.discord.DiscordClient;
#end
#if FEATURE_NEWGROUNDS
import funkin.api.newgrounds.Medals;
import funkin.api.newgrounds.Leaderboards;
#end
/**
* Parameters used to initialize the PlayState.
@ -328,7 +332,8 @@ class PlayState extends MusicBeatSubState
public var isInCutscene:Bool = false;
/**
* Whether the inputs should be disabled for whatever reason... used for the stage edit lol!
* Whether the inputs should be disabled for whatever reason...
* Used after the song ends, and in the Stage Editor.
*/
public var disableKeys:Bool = false;
@ -358,13 +363,13 @@ class PlayState extends MusicBeatSubState
/**
* Key press inputs which have been received but not yet processed.
* These are encoded with an OS timestamp, so they
* These are encoded with an OS timestamp, so we can account for input latency.
**/
var inputPressQueue:Array<PreciseInputEvent> = [];
/**
* Key release inputs which have been received but not yet processed.
* These are encoded with an OS timestamp, so they
* These are encoded with an OS timestamp, so we can account for input latency.
**/
var inputReleaseQueue:Array<PreciseInputEvent> = [];
@ -802,8 +807,6 @@ class PlayState extends MusicBeatSubState
public override function update(elapsed:Float):Void
{
// TOTAL: 9.42% CPU Time when profiled in VS 2019.
if (criticalFailure) return;
super.update(elapsed);
@ -888,6 +891,10 @@ class PlayState extends MusicBeatSubState
Highscore.tallies.combo = 0;
Countdown.performCountdown();
// Reset the health icons.
currentStage.getBoyfriend().initHealthIcon(false);
currentStage.getDad().initHealthIcon(true);
needsReset = false;
}
@ -917,6 +924,13 @@ class PlayState extends MusicBeatSubState
}
Conductor.instance.update(Conductor.instance.songPosition + elapsed * 1000, false); // Normal conductor update.
// If, after updating the conductor, the instrumental has finished, end the song immediately.
// This helps prevent a major bug where the level suddenly loops back to the start or middle.
if (Conductor.instance.songPosition >= (FlxG.sound.music.endTime ?? FlxG.sound.music.length))
{
if (mayPauseGame) endSong(skipEndingTransition);
}
}
var androidPause:Bool = false;
@ -1033,6 +1047,9 @@ class PlayState extends MusicBeatSubState
if (FlxG.sound.music != null) FlxG.sound.music.pause();
deathCounter += 1;
#if FEATURE_NEWGROUNDS
Events.logFailSong(currentSong.id, currentVariation);
#end
dispatchEvent(new ScriptEvent(GAME_OVER));
@ -1114,8 +1131,8 @@ class PlayState extends MusicBeatSubState
if (!isMinimalMode)
{
iconP1.updatePosition();
iconP2.updatePosition();
if (iconP1 != null) iconP1.updatePosition();
if (iconP1 != null) iconP2.updatePosition();
}
// Transition to the game over substate.
@ -1194,7 +1211,7 @@ class PlayState extends MusicBeatSubState
{
// If there is a substate which requires the game to continue,
// then make this a condition.
var shouldPause = (Std.isOfType(subState, PauseSubState) || Std.isOfType(subState, GameOverSubState));
var shouldPause:Bool = (Std.isOfType(subState, PauseSubState) || Std.isOfType(subState, GameOverSubState));
if (shouldPause)
{
@ -1440,7 +1457,7 @@ class PlayState extends MusicBeatSubState
var playerVoicesError:Float = 0;
var opponentVoicesError:Float = 0;
if (vocals != null)
if (vocals != null && vocals.playing)
{
@:privateAccess // todo: maybe make the groups public :thinking:
{
@ -2059,7 +2076,7 @@ class PlayState extends MusicBeatSubState
}
FlxG.sound.music.onComplete = function() {
endSong(skipEndingTransition);
if (mayPauseGame) endSong(skipEndingTransition);
};
// A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
@ -2100,6 +2117,10 @@ class PlayState extends MusicBeatSubState
}
dispatchEvent(new ScriptEvent(SONG_START));
#if FEATURE_NEWGROUNDS
Events.logStartSong(currentSong.id, currentVariation);
#end
}
/**
@ -2906,6 +2927,9 @@ class PlayState extends MusicBeatSubState
vocals.volume = 0;
mayPauseGame = false;
// Prevent ghost misses while the song is ending.
disableKeys = true;
// Check if any events want to prevent the song from ending.
var event = new ScriptEvent(SONG_END, true);
dispatchEvent(event);
@ -2951,8 +2975,16 @@ class PlayState extends MusicBeatSubState
// adds current song data into the tallies for the level (story levels)
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
#if FEATURE_NEWGROUNDS
Leaderboards.submitSongScore(currentSong.id, suffixedDifficulty, data.score);
#end
if (!isPracticeMode && !isBotPlayMode)
{
#if FEATURE_NEWGROUNDS
Events.logCompleteSong(currentSong.id, currentVariation);
#end
isNewHighscore = Save.instance.isSongHighScore(currentSong.id, suffixedDifficulty, data);
// If no high score is present, save both score and rank.
@ -2960,15 +2992,48 @@ class PlayState extends MusicBeatSubState
// If neither are higher, nothing will change.
Save.instance.applySongRank(currentSong.id, suffixedDifficulty, data);
if (isNewHighscore)
{
#if newgrounds
NGio.postScore(score, currentSong.id);
#end
}
if (isNewHighscore) {}
}
}
#if FEATURE_NEWGROUNDS
// Only award medals if we are LEGIT.
if (!isPracticeMode && !isBotPlayMode && !isChartingMode && currentSong.validScore)
{
// Award a medal for beating at least one song on any difficulty on a Friday.
if (Date.now().getDay() == 5) Medals.award(FridayNight);
// Determine the score rank for this song we just finished.
var scoreRank:ScoringRank = Scoring.calculateRank(
{
score: songScore,
tallies:
{
sick: Highscore.tallies.sick,
good: Highscore.tallies.good,
bad: Highscore.tallies.bad,
shit: Highscore.tallies.shit,
missed: Highscore.tallies.missed,
combo: Highscore.tallies.combo,
maxCombo: Highscore.tallies.maxCombo,
totalNotesHit: Highscore.tallies.totalNotesHit,
totalNotes: Highscore.tallies.totalNotes,
}
});
// Award various medals based on variation, difficulty, song ID, and scoring rank.
if (scoreRank == ScoringRank.SHIT) Medals.award(LossRating);
if (scoreRank >= ScoringRank.PERFECT && currentDifficulty == 'hard') Medals.award(PerfectRatingHard);
if (scoreRank == ScoringRank.PERFECT_GOLD && currentDifficulty == 'hard') Medals.award(GoldPerfectRatingHard);
if (Constants.DEFAULT_DIFFICULTY_LIST_ERECT.contains(currentDifficulty)) Medals.award(ErectDifficulty);
if (scoreRank == ScoringRank.PERFECT_GOLD && currentDifficulty == 'nightmare') Medals.award(GoldPerfectRatingNightmare);
if (currentVariation == 'pico' && !PlayStatePlaylist.isStoryMode) Medals.award(FreeplayPicoMix);
if (currentVariation == 'pico' && currentSong.id == 'stress') Medals.award(FreeplayStressPico);
Events.logEarnRank(scoreRank.toString());
}
#end
if (PlayStatePlaylist.isStoryMode)
{
isNewHighscore = false;
@ -2983,8 +3048,6 @@ class PlayState extends MusicBeatSubState
{
if (currentSong.validScore)
{
NGio.unlockMedal(60961);
var data =
{
score: PlayStatePlaylist.campaignScore,
@ -3003,12 +3066,19 @@ class PlayState extends MusicBeatSubState
},
};
#if FEATURE_NEWGROUNDS
// Award a medal for beating a Story level.
Medals.awardStoryLevel(PlayStatePlaylist.campaignId);
// Submit the score for the Story level to Newgrounds.
Leaderboards.submitLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, PlayStatePlaylist.campaignScore);
Events.logCompleteLevel(PlayStatePlaylist.campaignId);
#end
if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
{
Save.instance.setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
#if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end
isNewHighscore = true;
}
}
@ -3283,7 +3353,9 @@ class PlayState extends MusicBeatSubState
totalNotes: talliesToUse.totalNotes,
},
},
isNewHighscore: isNewHighscore
isNewHighscore: isNewHighscore,
isPracticeMode: isPracticeMode,
isBotPlayMode: isBotPlayMode,
});
this.persistentDraw = false;
openSubState(res);

View file

@ -1,10 +1,8 @@
package funkin.play;
import funkin.util.MathUtil;
import funkin.ui.story.StoryMenuState;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.FlxSubState;
import funkin.graphics.FunkinSprite;
import flixel.effects.FlxFlicker;
@ -14,27 +12,26 @@ import flixel.math.FlxPoint;
import funkin.ui.MusicBeatSubState;
import flixel.math.FlxRect;
import flixel.text.FlxBitmapText;
import funkin.ui.freeplay.FreeplayScore;
import flixel.text.FlxText;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.data.freeplay.player.PlayerData;
import funkin.data.freeplay.player.PlayerData.PlayerResultsAnimationData;
import funkin.ui.freeplay.charselect.PlayableCharacter;
import flixel.util.FlxColor;
import flixel.tweens.FlxEase;
import funkin.graphics.FunkinCamera;
import funkin.input.Controls;
import funkin.ui.freeplay.FreeplayState;
import flixel.tweens.FlxTween;
import flixel.addons.display.FlxBackdrop;
import funkin.audio.FunkinSound;
import flixel.util.FlxGradient;
import flixel.util.FlxTimer;
import funkin.save.Save;
import funkin.play.scoring.Scoring;
import funkin.save.Save.SaveScoreData;
import funkin.graphics.shaders.LeftMaskShader;
import funkin.play.components.TallyCounter;
import funkin.play.components.ClearPercentCounter;
#if FEATURE_NEWGROUNDS
import funkin.api.newgrounds.Medals;
#end
/**
* The state for the results screen after a song or week is finished.
@ -65,7 +62,9 @@ class ResultState extends MusicBeatSubState
{
sprite:FlxAtlasSprite,
delay:Float,
forceLoop:Bool
forceLoop:Bool,
startFrameLabel:String,
sound:String
}> = [];
var characterSparrowAnimations:Array<
{
@ -182,6 +181,11 @@ class ResultState extends MusicBeatSubState
{
if (animData == null) continue;
if (animData.filter != "both")
{
if (Preferences.naughtyness && animData.filter != "naughty" || !Preferences.naughtyness && animData.filter != "safe") continue;
}
var animPath:String = Paths.stripLibrary(animData.assetPath);
var animLibrary:String = Paths.getLibrary(animData.assetPath);
var offsets = animData.offsets ?? [0, 0];
@ -197,7 +201,6 @@ class ResultState extends MusicBeatSubState
{
// Animation is not looped.
animation.onAnimationComplete.add((_name:String) -> {
trace("AHAHAH 2");
if (animation != null)
{
animation.anim.pause();
@ -207,7 +210,6 @@ class ResultState extends MusicBeatSubState
else if (animData.loopFrameLabel != null)
{
animation.onAnimationComplete.add((_name:String) -> {
trace("AHAHAH 2");
if (animation != null)
{
animation.playAnimation(animData.loopFrameLabel ?? '', true, false, true); // unpauses this anim, since it's on PlayOnce!
@ -225,7 +227,6 @@ class ResultState extends MusicBeatSubState
}
});
}
// Hide until ready to play.
animation.visible = false;
// Queue to play.
@ -233,7 +234,9 @@ class ResultState extends MusicBeatSubState
{
sprite: animation,
delay: animData.delay ?? 0.0,
forceLoop: (animData.loopFrame ?? -1) == 0
forceLoop: (animData.loopFrame ?? -1) == 0,
startFrameLabel: (animData.startFrameLabel ?? ""),
sound: (animData.sound ?? "")
});
// Add to the scene.
add(animation);
@ -366,6 +369,12 @@ class ResultState extends MusicBeatSubState
var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.scoreData.tallies.maxCombo);
ratingGrp.add(maxCombo);
if (params.scoreData.tallies.totalNotesHit >= 1000)
{
totalHit.x -= 30;
maxCombo.x -= 30;
}
hStuf += 2;
var extraYOffset:Float = 7;
@ -492,6 +501,12 @@ class ResultState extends MusicBeatSubState
// Just to be sure that the lerp didn't mess things up.
clearPercentCounter.curNumber = clearPercentTarget;
#if FEATURE_NEWGROUNDS
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false);
// This is the easiest spot to do the medal calculation lol.
if (isScoreValid && clearPercentTarget == 69) Medals.award(Nice);
#end
clearPercentCounter.flash(true);
new FlxTimer().start(0.4, _ -> {
clearPercentCounter.flash(false);
@ -591,7 +606,14 @@ class ResultState extends MusicBeatSubState
new FlxTimer().start(atlas.delay, _ -> {
if (atlas.sprite == null) return;
atlas.sprite.visible = true;
atlas.sprite.playAnimation('');
atlas.sprite.playAnimation(atlas.startFrameLabel);
if (atlas.sound != "")
{
var sndPath:String = Paths.stripLibrary(atlas.sound);
var sndLibrary:String = Paths.getLibrary(atlas.sound);
FunkinSound.playOnce(Paths.sound(sndPath, sndLibrary), 1.0);
}
});
}
@ -676,33 +698,6 @@ class ResultState extends MusicBeatSubState
override function update(elapsed:Float):Void
{
// if(FlxG.keys.justPressed.R){
// FlxG.switchState(() -> new funkin.play.ResultState(
// {
// storyMode: false,
// title: "Cum Song Erect by Kawai Sprite",
// songId: "cum",
// difficultyId: "nightmare",
// isNewHighscore: true,
// scoreData:
// {
// score: 1_234_567,
// tallies:
// {
// sick: 200,
// good: 0,
// bad: 0,
// shit: 0,
// missed: 0,
// combo: 0,
// maxCombo: 69,
// totalNotesHit: 200,
// totalNotes: 200 // 0,
// }
// },
// }));
// }
// maskShaderSongName.swagSprX = songName.x;
maskShaderDifficulty.swagSprX = difficulty.x;
@ -721,15 +716,10 @@ class ResultState extends MusicBeatSubState
}
}
if (FlxG.keys.justPressed.RIGHT) speedOfTween.x += 0.1;
if (FlxG.keys.justPressed.LEFT)
{
speedOfTween.x -= 0.1;
}
if (controls.PAUSE || controls.ACCEPT)
{
if (_parentState is funkin.ui.debug.results.ResultsDebugSubState)
close(); // IF we are a substate, we will close ourselves. This is used from ResultsDebugSubState
if (introMusicAudio != null)
{
@:nullSafety(Off)
@ -776,6 +766,14 @@ class ResultState extends MusicBeatSubState
var shouldTween = false;
var shouldUseSubstate = false;
var stickerSet = (playerCharacterId == "pico") ? "stickers-set-2" : "stickers-set-1";
var stickerPack = switch (PlayState.instance?.currentChart?.song?.id)
{
case "tutorial": "tutorial";
case "darnell" | "lit-up" | "2hot": "weekend";
default: "all";
};
if (params.storyMode)
{
if (PlayerRegistry.instance.hasNewCharacter())
@ -797,12 +795,21 @@ class ResultState extends MusicBeatSubState
// No new characters.
shouldTween = false;
shouldUseSubstate = true;
targetState = new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker));
targetState = new funkin.ui.transition.StickerSubState(
{
targetState: (sticker) -> new StoryMenuState(sticker),
stickerSet: stickerSet,
stickerPack: stickerPack
});
}
}
else
{
if (rank > Scoring.calculateRank(params?.prevScoreData))
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false);
var isPersonalBest = rank > Scoring.calculateRank(params?.prevScoreData);
if (isScoreValid && isPersonalBest)
{
trace('THE RANK IS Higher.....');
@ -826,7 +833,12 @@ class ResultState extends MusicBeatSubState
{
shouldTween = false;
shouldUseSubstate = true;
targetState = new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker));
targetState = new funkin.ui.transition.StickerSubState(
{
targetState: (sticker) -> FreeplayState.build(null, sticker),
stickerSet: stickerSet,
stickerPack: stickerPack
});
}
}
@ -893,6 +905,16 @@ typedef ResultsStateParams =
*/
var ?isNewHighscore:Bool;
/**
* Whether the displayed score is from a song played with Practice Mode enabled.
*/
var ?isPracticeMode:Bool;
/**
* Whether the displayed score is from a song played with Bot Play Mode enabled.
*/
var ?isBotPlayMode:Bool;
/**
* The difficulty ID of the song/week we just played.
* @default Normal

View file

@ -16,6 +16,7 @@ import flixel.util.FlxDestroyUtil;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.modding.events.ScriptEvent;
import funkin.play.character.CharacterData.CharacterRenderType;
import flixel.util.FlxDirectionFlags;
import openfl.display.BitmapData;
import openfl.display.BlendMode;
@ -74,10 +75,14 @@ class AnimateAtlasCharacter extends BaseCharacter
override function onCreate(event:ScriptEvent):Void
{
trace('Creating Animate Atlas character: ' + this.characterId);
// Display a custom scope for debugging purposes.
#if FEATURE_DEBUG_TRACY
cpp.vm.tracy.TracyProfiler.zoneScoped('AnimateAtlasCharacter.create(${this.characterId})');
#end
try
{
trace('Loading assets for Animate Atlas character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0"));
var atlasSprite:FlxAtlasSprite = loadAtlasSprite();
setSprite(atlasSprite);
@ -93,12 +98,10 @@ class AnimateAtlasCharacter extends BaseCharacter
public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void
{
if ((!canPlayOtherAnims && !ignoreOther)) return;
var correctName = correctAnimationName(name);
if (correctName == null)
{
trace('Could not find Atlas animation: ' + name);
trace('$characterName Could not find Atlas animation: ' + name);
return;
}
@ -145,9 +148,16 @@ class AnimateAtlasCharacter extends BaseCharacter
{
super.onAnimationFinished(prefix);
if (!getCurrentAnimation().endsWith(Constants.ANIMATION_HOLD_SUFFIX)
&& hasAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX))
{
playAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX);
}
if (getAnimationData() != null && getAnimationData().looped)
{
playAnimation(currentAnimName, true, false);
if (StringTools.endsWith(prefix, "-hold")) trace(prefix);
playAnimation(prefix, true, false);
}
else
{
@ -380,7 +390,7 @@ class AnimateAtlasCharacter extends BaseCharacter
inline function directAlphaTransform(sprite:FlxSprite, alpha:Float):Void
sprite.alpha = alpha; // direct set
inline function facingTransform(sprite:FlxSprite, facing:Int):Void
inline function facingTransform(sprite:FlxSprite, facing:FlxDirectionFlags):Void
sprite.facing = facing;
inline function flipXTransform(sprite:FlxSprite, flipX:Bool):Void
@ -449,6 +459,57 @@ class AnimateAtlasCharacter extends BaseCharacter
}
}
var resS:FlxPoint = new FlxPoint();
/**
* Reset the character so it can be used at the start of the level.
* Call this when restarting the level.
*/
override public function resetCharacter(resetCamera:Bool = true):Void
{
trace("RESETTING ATLAS " + characterName);
// Reset the animation offsets. This will modify x and y to be the absolute position of the character.
// this.animOffsets = [0, 0];
// Now we can set the x and y to be their original values without having to account for animOffsets.
this.resetPosition();
mainSprite.setPosition(originalPosition.x, originalPosition.y);
// Then reapply animOffsets...
// applyAnimationOffsets(getCurrentAnimation());
// Make sure we are playing the idle animation
// ...then update the hitbox so that this.width and this.height are correct.
mainSprite.scale.set(1, 1);
mainSprite.alpha = 0.0001;
mainSprite.width = 0;
mainSprite.height = 0;
this.dance(true); // Force to avoid the old animation playing with the wrong offset at the start of the song.
mainSprite.draw(); // refresh frame
if (resS.x == 0)
{
resS.x = mainSprite.width; // clunky bizz
resS.y = mainSprite.height;
}
mainSprite.alpha = alpha;
mainSprite.width = resS.x;
mainSprite.height = resS.y;
frameWidth = 0;
frameHeight = 0;
scaleCallback(scale);
this.updateHitbox();
// Reset the camera focus point while we're at it.
if (resetCamera) this.resetCameraFocusPoint();
}
inline function offsetCallback(offset:FlxPoint):Void
transformChildren(offsetTransform, offset);
@ -528,7 +589,7 @@ class AnimateAtlasCharacter extends BaseCharacter
return alpha = value;
}
override function set_facing(value:Int):Int
override function set_facing(value:FlxDirectionFlags):FlxDirectionFlags
{
if (exists && facing != value) transformChildren(facingTransform, value);
return facing = value;

View file

@ -149,6 +149,7 @@ class BaseCharacter extends Bopper
public function new(id:String, renderType:CharacterRenderType)
{
super(CharacterDataParser.DEFAULT_DANCEEVERY);
this.characterId = id;
ignoreExclusionPref = ["sing"];
@ -643,6 +644,11 @@ class BaseCharacter extends Bopper
{
super.playAnimation(name, restart, ignoreOther, reversed);
}
public function getDeathQuote():Null<String>
{
return null;
}
}
/**

View file

@ -296,7 +296,7 @@ class CharacterDataParser
charPath += "monsterpixel";
case "mom" | "mom-car":
charPath += "mommypixel";
case "pico-blazin" | "pico-playable" | "pico-speaker":
case "pico-blazin" | "pico-playable" | "pico-speaker" | "pico-pixel" | "pico-holding-nene":
charPath += "picopixel";
case "gf-christmas" | "gf-car" | "gf-pixel" | "gf-tankmen" | "gf-dark":
charPath += "gfpixel";
@ -308,7 +308,7 @@ class CharacterDataParser
charPath += "senpaipixel";
case "spooky-dark":
charPath += "spookypixel";
case "tankman-atlas":
case "tankman-atlas" | "tankman-bloody":
charPath += "tankmanpixel";
case "pico-christmas" | "pico-dark":
charPath += "picopixel";

View file

@ -26,7 +26,10 @@ class MultiSparrowCharacter extends BaseCharacter
override function onCreate(event:ScriptEvent):Void
{
trace('Creating Multi-Sparrow character: ' + this.characterId);
// Display a custom scope for debugging purposes.
#if FEATURE_DEBUG_TRACY
cpp.vm.tracy.TracyProfiler.zoneScoped('MultiSparrowCharacter.create(${this.characterId})');
#end
buildSprites();
super.onCreate(event);
@ -41,8 +44,8 @@ class MultiSparrowCharacter extends BaseCharacter
{
this.isPixel = true;
this.antialiasing = false;
pixelPerfectRender = true;
pixelPerfectPosition = true;
// pixelPerfectRender = true;
// pixelPerfectPosition = true;
}
else
{
@ -53,6 +56,8 @@ class MultiSparrowCharacter extends BaseCharacter
function buildSpritesheet():Void
{
trace('Loading assets for Multi-Sparrow character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0"));
var assetList = [];
for (anim in _data.animations)
{
@ -123,10 +128,6 @@ class MultiSparrowCharacter extends BaseCharacter
public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void
{
// Make sure we ignore other animations if we're currently playing a forced one,
// unless we're forcing a new animation.
if (!this.canPlayOtherAnims && !ignoreOther) return;
super.playAnimation(name, restart, ignoreOther, reverse);
}
}

View file

@ -18,7 +18,10 @@ class PackerCharacter extends BaseCharacter
override function onCreate(event:ScriptEvent):Void
{
trace('Creating Packer character: ' + this.characterId);
// Display a custom scope for debugging purposes.
#if FEATURE_DEBUG_TRACY
cpp.vm.tracy.TracyProfiler.zoneScoped('PackerCharacter.create(${this.characterId})');
#end
loadSpritesheet();
loadAnimations();
@ -28,7 +31,7 @@ class PackerCharacter extends BaseCharacter
function loadSpritesheet():Void
{
trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
trace('Loading assets for Packer character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0"));
var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath);
if (tex == null)
@ -43,8 +46,8 @@ class PackerCharacter extends BaseCharacter
{
this.isPixel = true;
this.antialiasing = false;
pixelPerfectRender = true;
pixelPerfectPosition = true;
// pixelPerfectRender = true;
// pixelPerfectPosition = true;
}
else
{

View file

@ -21,7 +21,10 @@ class SparrowCharacter extends BaseCharacter
override function onCreate(event:ScriptEvent):Void
{
trace('Creating Sparrow character: ' + this.characterId);
// Display a custom scope for debugging purposes.
#if FEATURE_DEBUG_TRACY
cpp.vm.tracy.TracyProfiler.zoneScoped('SparrowCharacter.create(${this.characterId})');
#end
loadSpritesheet();
loadAnimations();
@ -31,7 +34,7 @@ class SparrowCharacter extends BaseCharacter
function loadSpritesheet()
{
trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
trace('Loading assets for Sparrow character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0"));
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
if (tex == null)
@ -46,8 +49,8 @@ class SparrowCharacter extends BaseCharacter
{
this.isPixel = true;
this.antialiasing = false;
pixelPerfectRender = true;
pixelPerfectPosition = true;
// pixelPerfectRender = true;
// pixelPerfectPosition = true;
}
else
{

View file

@ -3,13 +3,7 @@ package funkin.play.components;
import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.PureColor;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.text.FlxText.FlxTextAlign;
import funkin.util.MathUtil;
import flixel.util.FlxColor;
/**

View file

@ -1,14 +1,9 @@
package funkin.play.components;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.tweens.FlxTween;
import flixel.util.FlxDirection;
import funkin.graphics.FunkinSprite;
import funkin.play.PlayState;
import funkin.util.TimerUtil;
import funkin.util.EaseUtil;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
@:nullSafety

View file

@ -1,11 +1,7 @@
package funkin.play.components;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.text.FlxText.FlxTextAlign;
import funkin.util.MathUtil;

View file

@ -6,11 +6,10 @@ import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxSignal;
import flixel.util.FlxTimer;
#if html5
import funkin.graphics.video.FlxVideo;
#end
#if hxCodec
#if hxvlc
import funkin.graphics.video.FunkinVideoSprite;
#end
@ -25,7 +24,7 @@ class VideoCutscene
#if html5
static var vid:FlxVideo;
#end
#if hxCodec
#if hxvlc
static var vid:FunkinVideoSprite;
#end
@ -92,8 +91,8 @@ class VideoCutscene
#if html5
playVideoHTML5(rawFilePath);
#elseif hxCodec
playVideoNative(rawFilePath);
#elseif hxvlc
playVideoNative(filePath);
#else
throw "No video support for this platform!";
#end
@ -101,7 +100,7 @@ class VideoCutscene
public static function isPlaying():Bool
{
#if (html5 || hxCodec)
#if (html5 || hxvlc)
return vid != null;
#else
return false;
@ -134,7 +133,7 @@ class VideoCutscene
}
#end
#if hxCodec
#if hxvlc
static function playVideoNative(filePath:String):Void
{
// Video displays OVER the FlxState.
@ -144,17 +143,18 @@ class VideoCutscene
{
vid.zIndex = 0;
vid.bitmap.onEndReached.add(finishVideo.bind(0.5));
vid.autoPause = FlxG.autoPause;
vid.cameras = [PlayState.instance.camCutscene];
PlayState.instance.add(vid);
PlayState.instance.refresh();
vid.play(filePath, false);
if (vid.load(filePath)) vid.play();
// Resize videos bigger or smaller than the screen.
vid.bitmap.onTextureSetup.add(() -> {
vid.bitmap.onFormatSetup.add(function():Void {
if (vid == null) return;
vid.setGraphicSize(FlxG.width, FlxG.height);
vid.updateHitbox();
vid.x = 0;
@ -181,7 +181,7 @@ class VideoCutscene
}
#end
#if hxCodec
#if hxvlc
if (vid != null)
{
// Seek to the start of the video.
@ -207,7 +207,7 @@ class VideoCutscene
}
#end
#if hxCodec
#if hxvlc
if (vid != null)
{
vid.pause();
@ -226,7 +226,7 @@ class VideoCutscene
}
#end
#if hxCodec
#if hxvlc
if (vid != null)
{
vid.visible = false;
@ -245,7 +245,7 @@ class VideoCutscene
}
#end
#if hxCodec
#if hxvlc
if (vid != null)
{
vid.visible = true;
@ -264,7 +264,7 @@ class VideoCutscene
}
#end
#if hxCodec
#if hxvlc
if (vid != null)
{
vid.resume();
@ -291,7 +291,7 @@ class VideoCutscene
}
#end
#if hxCodec
#if hxvlc
if (vid != null)
{
vid.stop();
@ -299,7 +299,7 @@ class VideoCutscene
}
#end
#if (html5 || hxCodec)
#if (html5 || hxvlc)
vid.destroy();
vid = null;
#end

View file

@ -1,7 +1,5 @@
package funkin.play.cutscene.dialogue;
import flixel.addons.display.FlxPieDial;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
@ -11,9 +9,7 @@ import funkin.audio.FunkinSound;
import funkin.data.dialogue.conversation.ConversationData;
import funkin.data.dialogue.conversation.ConversationData.DialogueEntryData;
import funkin.data.dialogue.conversation.ConversationRegistry;
import funkin.data.dialogue.dialoguebox.DialogueBoxData;
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
import funkin.data.dialogue.speaker.SpeakerData;
import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.IRegistryEntry;
import funkin.graphics.FunkinSprite;
@ -21,7 +17,6 @@ import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.modding.IScriptedClass.IEventHandler;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.util.SortUtil;
import funkin.util.EaseUtil;
@ -30,6 +25,7 @@ import funkin.util.EaseUtil;
*
* This shit is great for modders but it's pretty elaborate for how much it'll actually be used, lolol. -Eric
*/
@:nullSafety
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<ConversationData>
{
/**
@ -44,8 +40,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
/**
* Conversation data as parsed from the JSON file.
* `null` if the data could not be parsed or loaded.
*/
public final _data:ConversationData;
public final _data:Null<ConversationData>;
/**
* The current entry in the dialogue.
@ -56,7 +53,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
function get_currentDialogueEntryCount():Int
{
return _data.dialogue.length;
return _data?.dialogue?.length ?? 0;
}
/**
@ -68,12 +65,12 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
function get_currentDialogueLineCount():Int
{
return currentDialogueEntryData.text.length;
return currentDialogueEntryData?.text?.length ?? 0;
}
var currentDialogueEntryData(get, never):DialogueEntryData;
var currentDialogueEntryData(get, never):Null<DialogueEntryData>;
function get_currentDialogueEntryData():DialogueEntryData
function get_currentDialogueEntryData():Null<DialogueEntryData>
{
if (_data == null || _data.dialogue == null) return null;
if (currentDialogueEntry < 0 || currentDialogueEntry >= _data.dialogue.length) return null;
@ -85,22 +82,23 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
function get_currentDialogueLineString():String
{
return currentDialogueEntryData?.text[currentDialogueLine];
// TODO: Replace "" with some placeholder text?
return currentDialogueEntryData?.text[currentDialogueLine] ?? "";
}
/**
* AUDIO
*/
var music:FunkinSound;
var music:Null<FunkinSound>;
/**
* GRAPHICS
*/
var backdrop:FunkinSprite;
var backdrop:Null<FunkinSprite>;
var currentSpeaker:Speaker;
var currentSpeaker:Null<Speaker>;
var currentDialogueBox:DialogueBox;
var currentDialogueBox:Null<DialogueBox>;
public function new(id:String)
{
@ -128,17 +126,23 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
function setupMusic():Void
{
if (_data.music == null) return;
if (_data == null) return;
if (_data.music == null || (_data.music.asset ?? "") == "") return;
music = FunkinSound.load(Paths.music(_data.music.asset), 0.0, true, true, true);
var fadeTime:Float = _data.music.fadeTime ?? 0.0;
if (_data.music.fadeTime > 0.0)
if (fadeTime > 0.0)
{
FlxTween.tween(music, {volume: 1.0}, _data.music.fadeTime, {ease: FlxEase.linear});
FlxTween.tween(music, {volume: 1.0}, fadeTime, {ease: FlxEase.linear});
}
else
{
music.volume = 1.0;
if (music != null)
{
music.volume = 1.0;
}
}
}
@ -160,6 +164,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
function setupBackdrop():Void
{
if (_data == null) return;
if (backdrop != null)
{
backdrop.destroy();
@ -175,12 +181,13 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
switch (_data.backdrop)
{
case SOLID(backdropData):
var targetColor:FlxColor = FlxColor.fromString(backdropData.color);
var targetColor:Null<FlxColor> = FlxColor.fromString(backdropData.color);
backdrop.makeSolidColor(Std.int(FlxG.width), Std.int(FlxG.height), targetColor);
if (backdropData.fadeTime > 0.0)
var fadeTime = backdropData.fadeTime ?? 0.0;
if (fadeTime > 0.0)
{
backdrop.alpha = 0.0;
FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: EaseUtil.stepped(10)});
FlxTween.tween(backdrop, {alpha: 1.0}, fadeTime, {ease: EaseUtil.stepped(10)});
}
else
{
@ -204,7 +211,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
function showCurrentSpeaker():Void
{
var nextSpeakerId:String = currentDialogueEntryData.speaker;
var nextSpeakerId:String = currentDialogueEntryData?.speaker ?? "";
// Skip the next steps if the current speaker is already displayed.
if ((currentSpeaker != null && currentSpeaker.alive) && nextSpeakerId == currentSpeaker.id) return;
@ -216,7 +223,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
currentSpeaker = null;
}
var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
var nextSpeaker:Null<Speaker> = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
if (nextSpeaker == null)
{
@ -242,11 +249,11 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
function playSpeakerAnimation():Void
{
var nextSpeakerAnimation:String = currentDialogueEntryData.speakerAnimation;
var nextSpeakerAnimation:Null<String> = currentDialogueEntryData?.speakerAnimation;
if (nextSpeakerAnimation == null) return;
currentSpeaker.playAnimation(nextSpeakerAnimation);
if (currentSpeaker != null) currentSpeaker.playAnimation(nextSpeakerAnimation);
}
public function refresh():Void
@ -256,7 +263,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
function showCurrentDialogueBox():Void
{
var nextDialogueBoxId:String = currentDialogueEntryData?.box;
var nextDialogueBoxId:String = currentDialogueEntryData?.box ?? "";
// Skip the next steps if the current dialogue box is already displayed.
if ((currentDialogueBox != null && currentDialogueBox.alive) && nextDialogueBoxId == currentDialogueBox.id) return;
@ -268,7 +275,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
currentDialogueBox = null;
}
var nextDialogueBox:DialogueBox = DialogueBoxRegistry.instance.fetchEntry(nextDialogueBoxId);
var nextDialogueBox:Null<DialogueBox> = DialogueBoxRegistry.instance.fetchEntry(nextDialogueBoxId);
if (nextDialogueBox == null)
{
@ -290,11 +297,11 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
function playDialogueBoxAnimation():Void
{
var nextDialogueBoxAnimation:String = currentDialogueEntryData?.boxAnimation;
var nextDialogueBoxAnimation:Null<String> = currentDialogueEntryData?.boxAnimation;
if (nextDialogueBoxAnimation == null) return;
currentDialogueBox.playAnimation(nextDialogueBoxAnimation);
if (currentDialogueBox != null) currentDialogueBox.playAnimation(nextDialogueBoxAnimation);
}
function onTypingComplete():Void
@ -363,20 +370,32 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
}
outroTween = null;
if (this.music != null) this.music.stop();
this.music = null;
if (this.music != null)
{
this.music.stop();
this.music = null;
}
if (currentSpeaker != null) currentSpeaker.kill();
remove(currentSpeaker);
currentSpeaker = null;
if (currentSpeaker != null)
{
currentSpeaker.kill();
remove(currentSpeaker);
currentSpeaker = null;
}
if (currentDialogueBox != null) currentDialogueBox.kill();
remove(currentDialogueBox);
currentDialogueBox = null;
if (currentDialogueBox != null)
{
currentDialogueBox.kill();
remove(currentDialogueBox);
currentDialogueBox = null;
}
if (backdrop != null) backdrop.destroy();
remove(backdrop);
backdrop = null;
if (backdrop != null)
{
backdrop.destroy();
remove(backdrop);
backdrop = null;
}
startConversation();
}
@ -392,7 +411,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
dispatchEvent(new DialogueScriptEvent(DIALOGUE_SKIP, this, true));
}
var outroTween:FlxTween;
var outroTween:Null<FlxTween> = null;
public function startOutro():Void
{
@ -407,7 +426,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
ease: EaseUtil.stepped(8)
});
FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);
if (this.music != null) FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);
case NONE(_):
// Immediately clean up.
endOutro();
@ -417,7 +436,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
}
}
public var completeCallback:() -> Void;
public var completeCallback:Null<Void->Void> = null;
public function endOutro():Void
{
@ -477,7 +496,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
{
// Continue the dialog with more lines.
state = Speaking;
currentDialogueBox.appendText(currentDialogueLineString);
if (currentDialogueBox != null) currentDialogueBox.appendText(currentDialogueLineString);
}
}
@ -489,7 +508,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
propagateEvent(event);
if (event.eventCanceled) return;
currentDialogueBox.skip();
if (currentDialogueBox != null) currentDialogueBox.skip();
}
/**
@ -564,17 +583,26 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
if (this.music != null) this.music.stop();
this.music = null;
if (currentSpeaker != null) currentSpeaker.kill();
remove(currentSpeaker);
currentSpeaker = null;
if (currentSpeaker != null)
{
currentSpeaker.kill();
remove(currentSpeaker);
currentSpeaker = null;
}
if (currentDialogueBox != null) currentDialogueBox.kill();
remove(currentDialogueBox);
currentDialogueBox = null;
if (currentDialogueBox != null)
{
currentDialogueBox.kill();
remove(currentDialogueBox);
currentDialogueBox = null;
}
if (backdrop != null) backdrop.destroy();
remove(backdrop);
backdrop = null;
if (backdrop != null)
{
backdrop.destroy();
remove(backdrop);
backdrop = null;
}
this.clear();

View file

@ -2,10 +2,8 @@ package funkin.play.event;
import flixel.tweens.FlxEase;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventSchema.SongEventFieldType;

View file

@ -3,10 +3,8 @@ package funkin.play.event;
import flixel.FlxSprite;
import funkin.play.character.BaseCharacter;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventSchema.SongEventFieldType;

View file

@ -1,6 +1,5 @@
package funkin.play.event;
import funkin.play.song.Song;
import polymod.hscript.HScriptedClass;
/**

View file

@ -1,13 +1,9 @@
package funkin.play.event;
import flixel.tweens.FlxTween;
import flixel.FlxCamera;
import flixel.tweens.FlxEase;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventSchema.SongEventFieldType;

View file

@ -1,13 +1,8 @@
package funkin.play.event;
import flixel.tweens.FlxTween;
import flixel.FlxCamera;
import flixel.tweens.FlxEase;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventSchema.SongEventFieldType;

View file

@ -0,0 +1,3 @@
package funkin.play.event;
// TODO: Add a song event which switches characters.

View file

@ -0,0 +1,121 @@
package funkin.play.event;
import funkin.data.event.SongEventSchema;
import funkin.play.character.CharacterData.HealthIconData;
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
/**
* This class represents a handler for scroll speed events.
*
* Example: Set the health icon of Boyfriend to "bf-pixel":
* ```
* {
* 'e': 'SetHealthIcon',
* "v": {
* "char": 0,
* "id": "bf-pixel",
*
* // Optional params:
* "scale": 1.0,
* "flipX": false,
* "isPixel": false,
* "offsetX": 0.0,
* "offsetY": 0.0
* }
* }
* ```
*/
class SetHealthIconSongEvent extends SongEvent
{
public function new()
{
super('SetHealthIcon');
}
public override function handleEvent(data:SongEventData):Void
{
// Does nothing if there is no PlayState.
if (PlayState.instance == null) return;
// Works even if we are minimal mode.
// if (PlayState.instance.isMinimalMode) return;
var offsets:Array<Float> = [data.value.offsetX ?? 0.0, data.value.offsetY ?? 0.0];
var healthIconData:HealthIconData =
{
id: data.value.id ?? "bf",
scale: data.value.scale ?? 1.0,
flipX: data.value.flipX ?? false,
isPixel: data.value.isPixel ?? false,
offsets: offsets,
};
switch (data?.value?.char ?? 0)
{
case 0:
trace('Applying Player health icon via song event: ${healthIconData.id}');
PlayState.instance.iconP1.configure(healthIconData);
case 1:
trace('Applying Opponent health icon via song event: ${healthIconData.id}');
PlayState.instance.iconP2.configure(healthIconData);
default:
trace('[WARN] Unknown character index: ' + data.value.char);
}
}
public override function getTitle():String
{
return 'Set Health Icon';
}
public override function getEventSchema():SongEventSchema
{
return new SongEventSchema([
{
name: 'char',
title: 'Character',
defaultValue: 0,
type: SongEventFieldType.ENUM,
keys: ['Player' => 0, 'Opponent' => 1],
},
{
name: 'id',
title: 'Health Icon ID',
defaultValue: 'bf',
type: SongEventFieldType.STRING,
},
{
name: 'scale',
title: 'Scale',
defaultValue: 1.0,
type: SongEventFieldType.FLOAT,
},
{
name: 'flipX',
title: 'Flip X?',
defaultValue: false,
type: SongEventFieldType.BOOL,
},
{
name: 'isPixel',
title: 'Is Pixel?',
defaultValue: false,
type: SongEventFieldType.BOOL,
},
{
name: 'offsetX',
title: 'X Offset',
defaultValue: 0,
type: SongEventFieldType.FLOAT,
},
{
name: 'offsetY',
title: 'Y Offset',
defaultValue: 0,
type: SongEventFieldType.FLOAT,
}
]);
}
}

View file

@ -0,0 +1,3 @@
package funkin.play.event;
// TODO: Add a song event which switches stages.

View file

@ -1,13 +1,9 @@
package funkin.play.event;
import flixel.tweens.FlxTween;
import flixel.FlxCamera;
import flixel.tweens.FlxEase;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventSchema.SongEventFieldType;

View file

@ -1,72 +1,40 @@
package funkin.play.notes;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import funkin.play.notes.NoteDirection;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.util.assets.FlxAnimationUtil;
import flixel.FlxG;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
import funkin.play.notes.notestyle.NoteStyle;
class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite>
{
static final FRAMERATE_DEFAULT:Int = 24;
static var glowFrames:FlxFramesCollection;
public var holdNote:SustainTrail;
var glow:FlxSprite;
public var glow:FlxSprite;
var sparks:FlxSprite;
public function new()
public function new(noteStyle:NoteStyle)
{
super(0, 0);
setup();
}
public static function preloadFrames():Void
{
glowFrames = null;
for (direction in Strumline.DIRECTIONS)
{
var directionName = direction.colorName.toTitleCase();
var atlas:FlxFramesCollection = Paths.getSparrowAtlas('holdCover${directionName}');
atlas.parent.persist = true;
if (glowFrames != null)
{
glowFrames = FlxAnimationUtil.combineFramesCollections(glowFrames, atlas);
}
else
{
glowFrames = atlas;
}
}
setupHoldNoteCover(noteStyle);
}
/**
* Add ALL the animations to this sprite. We will recycle and reuse the FlxSprite multiple times.
*/
function setup():Void
function setupHoldNoteCover(noteStyle:NoteStyle):Void
{
glow = new FlxSprite();
add(glow);
if (glowFrames == null) preloadFrames();
glow.frames = glowFrames;
for (direction in Strumline.DIRECTIONS)
{
var directionName = direction.colorName.toTitleCase();
// TODO: null check here like how NoteSplash does
noteStyle.buildHoldCoverSprite(this);
glow.animation.addByPrefix('holdCoverStart$directionName', 'holdCoverStart${directionName}0', FRAMERATE_DEFAULT, false, false, false);
glow.animation.addByPrefix('holdCover$directionName', 'holdCover${directionName}0', FRAMERATE_DEFAULT, true, false, false);
glow.animation.addByPrefix('holdCoverEnd$directionName', 'holdCoverEnd${directionName}0', FRAMERATE_DEFAULT, false, false, false);
}
glow.animation.finishCallback = this.onAnimationFinished;
glow.animation.onFinish.add(this.onAnimationFinished);
if (glow.animation.getAnimationList().length < 3 * 4)
{

View file

@ -1,53 +1,32 @@
package funkin.play.notes;
import funkin.play.notes.NoteDirection;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.FlxG;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
class NoteSplash extends FlxSprite
{
static final ALPHA:Float = 0.6;
static final FRAMERATE_DEFAULT:Int = 24;
static final FRAMERATE_VARIANCE:Int = 2;
public var splashFramerate:Int = 24;
public var splashFramerateVariance:Int = 2;
static var frameCollection:FlxFramesCollection;
public static function preloadFrames():Void
{
frameCollection = Paths.getSparrowAtlas('noteSplashes');
frameCollection.parent.persist = true;
}
public function new()
public function new(noteStyle:NoteStyle)
{
super(0, 0);
setup();
setupSplashGraphic(noteStyle);
this.alpha = ALPHA;
this.animation.finishCallback = this.onAnimationFinished;
this.animation.onFinish.add(this.onAnimationFinished);
}
/**
* Add ALL the animations to this sprite. We will recycle and reuse the FlxSprite multiple times.
*/
function setup():Void
function setupSplashGraphic(noteStyle:NoteStyle):Void
{
if (frameCollection?.parent?.isDestroyed ?? false) frameCollection = null;
if (frameCollection == null) preloadFrames();
this.frames = frameCollection;
this.animation.addByPrefix('splash1Left', 'note impact 1 purple0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash1Down', 'note impact 1 blue0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash1Up', 'note impact 1 green0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash1Right', 'note impact 1 red0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Left', 'note impact 2 purple0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Down', 'note impact 2 blue0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Up', 'note impact 2 green0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Right', 'note impact 2 red0', FRAMERATE_DEFAULT, false, false, false);
if (frames == null) noteStyle.buildSplashSprite(this);
if (this.animation.getAnimationList().length < 8)
{
@ -62,24 +41,21 @@ class NoteSplash extends FlxSprite
public function play(direction:NoteDirection, variant:Int = null):Void
{
if (variant == null) variant = FlxG.random.int(1, 2);
switch (direction)
if (variant == null)
{
case NoteDirection.LEFT:
this.playAnimation('splash${variant}Left');
case NoteDirection.DOWN:
this.playAnimation('splash${variant}Down');
case NoteDirection.UP:
this.playAnimation('splash${variant}Up');
case NoteDirection.RIGHT:
this.playAnimation('splash${variant}Right');
var animationAmount:Int = this.animation.getAnimationList().filter(function(anim) return anim.name.startsWith('splash${direction.nameUpper}')).length
- 1;
variant = FlxG.random.int(0, animationAmount);
}
// splashUP0, splashUP1, splashRIGHT0, etc.
// the animations are processed via `NoteStyle.fetchSplashAnimationData()` in this format
this.playAnimation('splash${direction.nameUpper}${variant}');
if (animation.curAnim == null) return;
// Vary the speed of the animation a bit.
animation.curAnim.frameRate = FRAMERATE_DEFAULT + FlxG.random.int(-FRAMERATE_VARIANCE, FRAMERATE_VARIANCE);
animation.curAnim.frameRate = splashFramerate + FlxG.random.int(-splashFramerateVariance, splashFramerateVariance);
// Center the animation on the note splash.
offset.set(width * 0.3, height * 0.3);

View file

@ -3,8 +3,6 @@ package funkin.play.notes;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.NoteParamData;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.HSVShader;

View file

@ -8,14 +8,9 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxSort;
import funkin.play.notes.NoteHoldCover;
import funkin.play.notes.NoteSplash;
import funkin.play.notes.NoteSprite;
import funkin.play.notes.SustainTrail;
import funkin.graphics.FunkinSprite;
import funkin.data.song.SongData.SongNoteData;
import funkin.ui.options.PreferencesMenu;
import funkin.util.SortUtil;
import funkin.modding.events.ScriptEvent;
import funkin.play.notes.notekind.NoteKindManager;
/**
@ -110,6 +105,8 @@ class Strumline extends FlxSpriteGroup
public var onNoteIncoming:FlxTypedSignal<NoteSprite->Void>;
var background:FunkinSprite;
var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>;
var noteSplashes:FlxTypedSpriteGroup<NoteSplash>;
var noteHoldCovers:FlxTypedSpriteGroup<NoteHoldCover>;
@ -133,6 +130,8 @@ class Strumline extends FlxSpriteGroup
var heldKeys:Array<Bool> = [];
static final BACKGROUND_PAD:Int = 16;
public function new(noteStyle:NoteStyle, isPlayer:Bool)
{
super();
@ -169,6 +168,13 @@ class Strumline extends FlxSpriteGroup
this.noteSplashes.zIndex = 50;
this.add(this.noteSplashes);
this.background = new FunkinSprite(0, 0).makeSolidColor(Std.int(this.width + BACKGROUND_PAD * 2), FlxG.height, 0xFF000000);
// Convert the percent to a number between 0 and 1.
this.background.alpha = Preferences.strumlineBackgroundOpacity / 100.0;
this.background.scrollFactor.set(0, 0);
this.background.x = -BACKGROUND_PAD;
this.add(this.background);
this.refresh();
this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>();
@ -193,6 +199,25 @@ class Strumline extends FlxSpriteGroup
this.active = true;
}
override function set_y(value:Float):Float
{
super.set_y(value);
// Keep the background on the screen.
if (this.background != null) this.background.y = 0;
return value;
}
override function set_alpha(value:Float):Float
{
super.set_alpha(value);
this.background.alpha = Preferences.strumlineBackgroundOpacity / 100.0 * alpha;
return value;
}
public function refresh():Void
{
sort(SortUtil.byZIndex, FlxSort.ASCENDING);
@ -754,9 +779,11 @@ class Strumline extends FlxSpriteGroup
splash.x = this.x;
splash.x += getXPos(direction);
splash.x += INITIAL_OFFSET;
splash.x += noteStyle.getSplashOffsets()[0] * splash.scale.x;
splash.y = this.y;
splash.y -= INITIAL_OFFSET;
splash.y += 0;
splash.y += noteStyle.getSplashOffsets()[1] * splash.scale.y;
}
}
@ -779,12 +806,14 @@ class Strumline extends FlxSpriteGroup
cover.x += getXPos(holdNote.noteDirection);
cover.x += STRUMLINE_SIZE / 2;
cover.x -= cover.width / 2;
cover.x += -12; // Manual tweaking because fuck.
cover.x += noteStyle.getHoldCoverOffsets()[0] * cover.scale.x;
cover.x += -12; // hardcoded adjustment, because we are evil.
cover.y = this.y;
cover.y += INITIAL_OFFSET;
cover.y += STRUMLINE_SIZE / 2;
cover.y += -96; // Manual tweaking because fuck.
cover.y += noteStyle.getHoldCoverOffsets()[1] * cover.scale.y;
cover.y += -96; // hardcoded adjustment, because we are evil.
}
}
@ -817,7 +846,10 @@ class Strumline extends FlxSpriteGroup
if (holdNoteSprite != null)
{
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id);
if (noteKindStyle == null) noteKindStyle = NoteKindManager.getNoteStyle(note.kind, null);
if (noteKindStyle == null) noteKindStyle = this.noteStyle;
holdNoteSprite.setupHoldNoteGraphic(noteKindStyle);
holdNoteSprite.parentStrumline = this;
@ -852,7 +884,7 @@ class Strumline extends FlxSpriteGroup
if (noteSplashes.length < noteSplashes.maxSize)
{
// Create a new note splash.
result = new NoteSplash();
result = new NoteSplash(noteStyle);
this.noteSplashes.add(result);
}
else
@ -886,7 +918,7 @@ class Strumline extends FlxSpriteGroup
if (noteHoldCovers.length < noteHoldCovers.maxSize)
{
// Create a new note hold cover.
result = new NoteHoldCover();
result = new NoteHoldCover(noteStyle);
this.noteHoldCovers.add(result);
}
else
@ -1010,4 +1042,42 @@ class Strumline extends FlxSpriteGroup
{
return FlxSort.byValues(order, a?.strumTime, b?.strumTime);
}
override function findMinYHelper()
{
var value = Math.POSITIVE_INFINITY;
for (member in group.members)
{
if (member == null) continue;
// SKIP THE BACKGROUND
if (member == this.background) continue;
var minY:Float;
if (member.flixelType == SPRITEGROUP) minY = (cast member : FlxSpriteGroup).findMinY();
else
minY = member.y;
if (minY < value) value = minY;
}
return value;
}
override function findMaxYHelper()
{
var value = Math.NEGATIVE_INFINITY;
for (member in group.members)
{
if (member == null) continue;
// SKIP THE BACKGROUND
if (member == this.background) continue;
var maxY:Float;
if (member.flixelType == SPRITEGROUP) maxY = (cast member : FlxSpriteGroup).findMaxY();
else
maxY = member.y + member.height;
if (maxY > value) value = maxY;
}
return value;
}
}

View file

@ -1,14 +1,11 @@
package funkin.play.notes;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.NoteDirection;
import funkin.data.song.SongData.SongNoteData;
import flixel.util.FlxDirectionFlags;
import flixel.FlxSprite;
import flixel.graphics.FlxGraphic;
import flixel.graphics.tile.FlxDrawTrianglesItem;
import flixel.graphics.tile.FlxDrawTrianglesItem.DrawData;
import flixel.math.FlxMath;
import funkin.ui.options.PreferencesMenu;
/**
* This is based heavily on the `FlxStrip` class. It uses `drawTriangles()` to clip a sustain note

View file

@ -2,7 +2,6 @@ package funkin.play.notes.notekind;
import funkin.modding.IScriptedClass.INoteScriptedClass;
import funkin.modding.events.ScriptEvent;
import flixel.math.FlxMath;
/**
* Class for note scripts

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