Merge remote-tracking branch 'origin/rewrite/master' into feature/2hot-cutscenes

This commit is contained in:
EliteMasterEric 2024-04-19 18:47:51 -04:00
commit 4fcec49943
92 changed files with 3874 additions and 1124 deletions

135
.github/actions/setup-haxe/action.yml vendored Normal file
View file

@ -0,0 +1,135 @@
name: setup-haxeshit
description: "sets up haxe shit, using HMM!"
inputs:
haxe:
description: 'Version of haxe to install'
required: true
default: '4.3.4'
hxcpp-cache:
description: 'Whether to use a shared hxcpp compile cache'
required: true
default: 'true'
hxcpp-cache-path:
description: 'Path to create hxcpp cache in'
required: true
default: ${{ runner.temp }}/hxcpp_cache
targets:
description: 'Targets we plan to compile to. Installs native dependencies needed.'
required: true
runs:
using: "composite"
steps:
- name: Setup timers
shell: bash
run: |
echo "TIMER_HAXE=$(date +%s)" >> "$GITHUB_ENV"
- name: Install Haxe
uses: funkincrew/ci-haxe@v3.1.0
with:
haxe-version: ${{ inputs.haxe }}
- name: Install native dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
ls -lah /usr/lib/x86_64-linux-gnu/
apt-get update
apt-get install -y \
g++ \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
libasound2-dev
ln -s /usr/lib/x86_64-linux-gnu/libffi.so.8 /usr/lib/x86_64-linux-gnu/libffi.so.6 || true
- name: Install linux-specific dependencies
if: ${{ runner.os == 'Linux' && contains(inputs.targets, 'linux') }}
shell: bash
run: |
apt-get install -y libvlc-dev libvlccore-dev
- name: Config haxelib
shell: bash
run: |
echo "TIMER_HAXELIB=$(date +%s)" >> "$GITHUB_ENV"
haxelib --debug --never install haxelib 4.1.0 --global
haxelib --debug --never deleterepo || true
haxelib --debug --never newrepo
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
haxelib --debug --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
haxelib --debug --global install hmm
echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV"
- name: Restore cached dependencies
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
shell: bash
run: |
haxelib --debug --global run hmm install
echo "TIMER_DONE=$(date +%s)" >> "$GITHUB_ENV"
# by default use a shared hxcpp cache
- if: ${{ inputs.hxcpp-cache == 'true' }}
name: Restore hxcpp cache
uses: actions/cache@v4
with:
path: ${{ inputs.hxcpp-cache-path }}
key: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}
# export env for it to reuse in builds
- if: ${{ inputs.hxcpp-cache == 'true' }}
name: Persist env for hxcpp cache
shell: bash
run: |
echo "HXCPP_COMPILE_CACHE=${{ inputs.hxcpp-cache-path }}" >> "$GITHUB_ENV"
echo 'HXCPP_CACHE_MB="4096"' >> "$GITHUB_ENV"
# if it's explicitly disabled, still cache export/ since that then contains the builds
- if: ${{ inputs.hxcpp-cache != 'true' }}
name: Restore export cache
uses: actions/cache@v4
with:
path: ${{ inputs.hxcpp-cache-path }}
key: haxe-export-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: haxe-export-${{ runner.os }}-${{ github.ref_name }}
- name: Print debug info
shell: bash
run: |
cat << EOF
runner:
kernel: $(uname -a)
haxe:
version: $(haxe -version)
which: $(which haxe)
haxepath: $HAXEPATH
took: $((TIMER_HAXELIB - TIMER_HAXE))s
haxelib:
version: $(haxelib version)
which: $(which haxelib)
local:
config: $(haxelib config)
path: $(haxelib path haxelib || true)
global
config: $(haxelib config --global)
path: $(haxelib path haxelib --global || true)
system
version: $(haxelib --system version)
local:
config: $(haxelib --system config)
global:
config: $(haxelib --system config --global)
took: $((TIMER_DEPS - TIMER_HAXELIB))s
deps:
took: $((TIMER_DONE - TIMER_DEPS))s
hxcpp_cache: |
$(haxelib run hxcpp cache list || true)
EOF

View file

@ -1,55 +0,0 @@
name: setup-haxeshit
description: "sets up haxe shit, using HMM!"
runs:
using: "composite"
steps:
- name: Install Haxe lol
uses: funkincrew/ci-haxe@v3.1.0
with:
haxe-version: 4.3.3
- name: Config haxelib
run: |
haxelib --never install haxelib 4.1.0 --global
haxelib --never deleterepo || true
haxelib --never newrepo
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
haxelib --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
shell: bash
- name: Gather debug info
run: |
cat << EOF >> "$GITHUB_STEP_SUMMARY"
## haxe
- version: \`$(haxe -version)\`
- exe: \`$(which haxe)\`
## haxelib
- version: \`$(haxelib version)\`
- exe: \`$(which haxelib)\`
- path: \`$HAXEPATH\`
### local
- config: \`$(haxelib config)\`
- path: \`$(haxelib path haxelib || true)\`
### global
- config: \`$(haxelib config --global)\`
- path: \`$(haxelib path haxelib --global || true)\`
### system
- version: \`$(haxelib --system version)\`
- local: \`$(haxelib --system config)\`
- global: \`$(haxelib --system config --global)\`
EOF
shell: bash
- name: Install hmm
# hmm only supports global installs
run: |
haxelib --global install hmm
shell: bash
- name: Restore cached dependencies
id: cache-hmm
uses: actions/cache@v4
with:
path: .haxelib
key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }}
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
name: hmm install
run: |
haxelib --global run hmm install
shell: bash

View file

@ -1,44 +1,124 @@
name: upload-itch name: upload-itch
description: "installs Butler, and uploads to itch.io!" description: "installs Butler, and uploads to itch.io!"
inputs: inputs:
butler-key: butler-key:
description: "Butler API secret key" description: "Butler API secret key"
required: true required: true
itch-repo:
description: "Where to upload the game to"
required: true
default: "ninja-muffin24/funkin-secret"
build-dir: build-dir:
description: "Directory of the game build" description: "Directory of the game build"
required: true required: false
target: target:
description: "Target (html5, win, linux, mac)" description: "Target (html5, windows, linux, macos)"
required: true required: true
runs: runs:
using: "composite" using: "composite"
steps: steps:
- name: Install butler Windows
if: runner.os == 'Windows' # RUNNER_OS = Windows | macOS | Linux
run: | # TARGET_BUILD = windows | macos | linux
curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default # TARGET_ITCH = win | macos | linux
7z x butler.zip # TARGET_BUTLER_DOWNLOAD = windows-amd64 | darwin-amd64 | linux-amd64
./butler -v - name: Setup variables
shell: bash shell: bash
- name: Install butler Mac run: |
if: runner.os == 'macOS' TARGET_OS=${{ inputs.target }}
run: | RUNNER=${RUNNER_OS@L}
curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default TARGET=${TARGET_OS@L}
unzip butler.zip case "$TARGET" in
./butler -V "windows")
shell: bash TARGET_ITCH=win
- name: Install butler Linux ;;
if: runner.os == 'Linux' *)
run: | TARGET_ITCH=$TARGET
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default ;;
unzip butler.zip esac
chmod +x butler case "$RUNNER" in
./butler -V "macos")
shell: bash OS_NODE=darwin
- name: Upload game to itch.io ;;
env: *)
BUTLER_API_KEY: ${{inputs.butler-key}} OS_NODE=$RUNNER
run: | ;;
./butler login esac
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME} BUTLER_PATH=$RUNNER_TEMP/butler
shell: bash
echo BUILD_DIR="export/release/$TARGET/bin" >> "$GITHUB_ENV"
echo ITCH_TAG=${{ inputs.itch-repo }}:$TARGET_ITCH-$GITHUB_REF_NAME >> "$GITHUB_ENV"
echo OS_AND_ARCH=$OS_NODE-amd64 >> "$GITHUB_ENV"
echo BUTLER_API_KEY=${{ inputs.butler-key }} >> "$GITHUB_ENV"
echo BUTLER_INSTALL_PATH=$BUTLER_PATH >> "$GITHUB_ENV"
echo TIMER_BUTLER=$(date +%s) >> "$GITHUB_ENV"
echo TARGET_ITCH=$TARGET_ITCH >> "$GITHUB_ENV"
echo "$BUTLER_PATH" >> "$GITHUB_PATH"
- name: Get latest butler version
shell: bash
run: |
LATEST=$(curl -sfL https://broth.itch.ovh/butler/$OS_AND_ARCH/LATEST)
echo BUTLER_LATEST=$LATEST >> "$GITHUB_ENV"
command -v butler \
&& echo BUTLER_CURRENT=$(butler -V 2>&1 | cut -d ',' -f 1) >> "$GITHUB_ENV" \
|| echo BUTLER_CURRENT=none >> "$GITHUB_ENV"
- name: Try to get butler from cache
id: cache-butler
uses: actions/cache@v4
with:
path: ${{ env.BUTLER_INSTALL_PATH }}
key: butler-${{ runner.os }}-${{ env.BUTLER_LATEST }}
- if: steps.cache-butler.outputs.cache-hit == 'true'
name: Make sure butler is executable
shell: bash
run: |
chmod +x $BUTLER_INSTALL_PATH/butler
- if: steps.cache-butler.outputs.cache-hit != 'true'
name: Install butler
shell: bash
run: |
mkdir -p $BUTLER_INSTALL_PATH
cd $BUTLER_INSTALL_PATH
curl -L -o butler.zip https://broth.itch.ovh/butler/$OS_AND_ARCH/LATEST/archive/default
unzip butler.zip
chmod +x butler
- name: Upload game to itch.io
shell: bash
run: |
echo "TIMER_UPLOAD=$(date +%s)" >> "$GITHUB_ENV"
butler login
butler push $BUILD_DIR $ITCH_TAG
echo "TIMER_DONE=$(date +%s)" >> "$GITHUB_ENV"
- name: Print debug info
shell: bash
run: |
cat << EOF
butler:
version: $(
if [[ "$BUTLER_CURRENT" == "$BUTLER_LATEST" ]]
then
echo $BUTLER_CURRENT
else
echo $BUTLER_CURRENT -> $BUTLER_LATEST
fi
)
install:
took: $(($TIMER_UPLOAD-$TIMER_BUTLER))s
upload:
tag: $TARGET_ITCH/$GITHUB_REF_NAME
took: $(($TIMER_DONE-$TIMER_UPLOAD))s
EOF
cat << EOF >> "$GITHUB_STEP_SUMMARY"
### open in launcher: [$TARGET_ITCH/$GITHUB_REF_NAME](https://run.funkin.me/$TARGET_ITCH/$GITHUB_REF_NAME)
EOF

View file

@ -0,0 +1,53 @@
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 }}

125
.github/workflows/build-game.yml vendored Normal file
View file

@ -0,0 +1,125 @@
name: Build and Upload nightly game builds
on:
workflow_dispatch:
push:
paths-ignore:
- '**/Dockerfile'
- '.github/workflows/build-docker-image.yml'
jobs:
build-game-on-host:
strategy:
matrix:
include:
- target: windows
- target: macos
runs-on:
- ${{ matrix.target }}
defaults:
run:
shell: bash
steps:
- name: Make git happy
if: ${{ matrix.target == 'macos' }}
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- name: Setup build environment
uses: ./.github/actions/setup-haxe
- name: Build game
if: ${{ matrix.target == 'windows' }}
run: |
haxelib run lime build windows -v -release -DGITHUB_BUILD
timeout-minutes: 120
- name: Build game
if: ${{ matrix.target != 'windows' }}
run: |
haxelib run lime build ${{ matrix.target }} -v -release --times -DGITHUB_BUILD
timeout-minutes: 120
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
target: ${{ matrix.target }}
build-game-in-container:
runs-on: build-set
container: ghcr.io/funkincrew/build-dependencies:latest
strategy:
matrix:
include:
- target: linux
- target: html5
defaults:
run:
shell: bash
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 }}
- 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: |
haxelib --global run hmm install
- 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 -DGITHUB_BUILD
timeout-minutes: 120
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
target: ${{ matrix.target }}

View file

@ -1,132 +0,0 @@
name: build-upload
on:
workflow_dispatch:
push:
jobs:
create-nightly-html5:
runs-on: [self-hosted, linux]
container: ubuntu:23.10
steps:
- name: Install tools missing in container
run: |
apt update
apt install -y sudo git curl unzip
- name: Fix git config on posix runner
# this can't be {{ github.workspace }} because that's not docker-aware
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- name: Install Haxe, dependencies
uses: ./.github/actions/setup-haxeshit
- name: Install native dependencies
run: |
apt install -y \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
libasound2-dev
- name: Build game
run: |
haxelib run lime build html5 -release --times -DGITHUB_BUILD
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/html5/bin
target: html5
create-nightly-win:
runs-on: [self-hosted, windows]
defaults:
run:
shell: bash
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 }}
- name: Install Haxe, dependencies
uses: ./.github/actions/setup-haxeshit
- name: Setup build cache
run: |
mkdir -p ${{ runner.temp }}/hxcpp_cache
- name: Restore build cache
id: cache-build-win
uses: actions/cache@v4
with:
path: |
export
${{ runner.temp }}/hxcpp_cache
key: ${{ runner.os }}-build-win-${{ github.ref_name }}
- name: Build game
run: |
haxelib run lime build windows -v -release -DGITHUB_BUILD
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY }}
build-dir: export/release/windows/bin
target: win
create-nightly-mac:
runs-on: [self-hosted, macos]
steps:
- name: Fix git config on posix runner
# this can't be {{ github.workspace }} because that's not docker-aware
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- name: Install Haxe, dependencies
uses: ./.github/actions/setup-haxeshit
- name: Setup build cache
run: |
mkdir -p ${{ runner.temp }}/hxcpp_cache
- name: Restore build cache
id: cache-build-win
uses: actions/cache@v4
with:
path: |
export
${{ runner.temp }}/hxcpp_cache
key: ${{ runner.os }}-build-mac-${{ github.ref_name }}
- name: Build game
run: |
haxelib run lime build macos -release --times -DGITHUB_BUILD
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache"
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/macos/bin
target: macos

View file

@ -0,0 +1,38 @@
name: Cancel queued workflows on PR merge
on:
pull_request:
types:
- closed
jobs:
cancel_stuff:
if: github.event.pull_request.merged == true
runs-on: build-set
permissions:
actions: write
steps:
- name: Cancel queued workflows for ${{ github.event.pull_request.head.ref }}
uses: actions/github-script@v7
with:
result-encoding: string
retries: 3
script: |
let branch_workflows = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: "build-shit.yml",
status: "queued",
branch: "${{ github.event.pull_request.head.ref }}"
});
let runs = branch_workflows.data.workflow_runs;
runs.forEach((run) => {
github.rest.actions.cancelWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run.id
});
});
console.log(runs);

15
.vscode/settings.json vendored
View file

@ -96,6 +96,11 @@
"target": "windows", "target": "windows",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DFORCE_DEBUG_VERSION"]
}, },
{
"label": "Linux / Debug",
"target": "linux",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{ {
"label": "HashLink / Debug", "label": "HashLink / Debug",
"target": "hl", "target": "hl",
@ -130,6 +135,11 @@
"-DFORCE_DEBUG_VERSION" "-DFORCE_DEBUG_VERSION"
] ]
}, },
{
"label": "Windows / Debug (Straight to Play - 2hot)",
"target": "windows",
"args": ["-debug", "-DSONG=2hot", "-DFORCE_DEBUG_VERSION"]
},
{ {
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)", "label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
"target": "hl", "target": "hl",
@ -160,6 +170,11 @@
"target": "windows", "target": "windows",
"args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"]
}, },
{
"label": "Windows / Debug (Debug hxCodec)",
"target": "windows",
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFORCE_DEBUG_VERSION"]
},
{ {
"label": "HashLink / Debug (Straight to Animation Editor)", "label": "HashLink / Debug (Straight to Animation Editor)",
"target": "hl", "target": "hl",

View file

@ -45,6 +45,7 @@
<library name="week6" preload="true" /> <library name="week6" preload="true" />
<library name="week7" preload="true" /> <library name="week7" preload="true" />
<library name="weekend1" preload="true" /> <library name="weekend1" preload="true" />
<library name="videos" preload="true" />
</section> </section>
<section if="NO_PRELOAD_ALL"> <section if="NO_PRELOAD_ALL">
<library name="songs" preload="false" /> <library name="songs" preload="false" />
@ -58,10 +59,13 @@
<library name="week6" preload="false" /> <library name="week6" preload="false" />
<library name="week7" preload="false" /> <library name="week7" preload="false" />
<library name="weekend1" preload="false" /> <library name="weekend1" preload="false" />
<library name="videos" preload="false" />
</section> </section>
<library name="art" preload="false" /> <library name="art" preload="false" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" /> <assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" /> <assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" />
<!-- Videos go in their own library because web never needs to preload them, they can just be streamed. -->
<assets path="assets/videos" library="videos" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" /> <assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" /> <assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" /> <assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" />
@ -119,7 +123,9 @@
<haxelib name="flixel-text-input" /> <!-- Improved text field rendering for HaxeUI --> <haxelib name="flixel-text-input" /> <!-- Improved text field rendering for HaxeUI -->
<haxelib name="polymod" /> <!-- Modding framework --> <haxelib name="polymod" /> <!-- Modding framework -->
<haxelib name="flxanimate" /> <!-- Texture atlas rendering --> <haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
<haxelib name="hxCodec" if="desktop" /> <!-- Video playback --> <haxelib name="hxCodec" if="desktop" unless="hl" /> <!-- Video playback -->
<haxelib name="funkVis"/>
<haxelib name="json2object" /> <!-- JSON parsing --> <haxelib name="json2object" /> <!-- JSON parsing -->
<haxelib name="thx.semver" /> <!-- Version string handling --> <haxelib name="thx.semver" /> <!-- Version string handling -->
@ -183,6 +189,7 @@
<haxedef name="haxeui_focus_out_on_click" /> <haxedef name="haxeui_focus_out_on_click" />
<!-- Required to use haxe.ui.backend.flixel.UIState with build macros. --> <!-- Required to use haxe.ui.backend.flixel.UIState with build macros. -->
<haxedef name="haxeui_dont_impose_base_class" /> <haxedef name="haxeui_dont_impose_base_class" />
<haxedef name="HARDCODED_CREDITS" />
<!-- Skip the Intro --> <!-- Skip the Intro -->
<section if="debug"> <section if="debug">

2
art

@ -1 +1 @@
Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34 Subproject commit 00463685fa570f0c853d08e250b46ef80f30bc48

2
assets

@ -1 +1 @@
Subproject commit 9c35ee01c5305ba04cde618bc224535e56c051fb Subproject commit c25a4119d6c37fa5ff49533111bd797e6fe2d7b1

185
build/Dockerfile Normal file
View file

@ -0,0 +1,185 @@
FROM ubuntu:mantic
ARG haxe_version=4.3.4
ARG haxelib_version=4.1.0
ARG neko_version=2.3.0
# prepare runner
ENV GITHUB_HOME="/github/home"
RUN <<EOF
mkdir -p "$GITHUB_HOME"
mkdir -p /opt
mkdir -p /usr/share/hxcpp
mkdir -p /usr/local/bin
chmod -R 777 /opt
chmod -R 777 /usr/share
chmod -R 777 /usr/local/bin
EOF
# Prepare Ubuntu
# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/configure-environment.sh
# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/configure-system.sh
RUN <<EOF
echo 'vm.max_map_count=262144' | tee -a /etc/sysctl.conf
echo 'fs.inotify.max_user_watches=655360' | tee -a /etc/sysctl.conf
echo 'fs.inotify.max_user_instances=1280' | tee -a /etc/sysctl.conf
EOF
ENV DEBIAN_FRONTEND="noninteractive"
# Prepare APT
RUN <<EOF
cat <<EOC >> /etc/apt/apt.conf.d/10apt-autoremove
APT::Get::AutomaticRemove "0";
APT::Get::HideAutoRemove "1";
EOC
echo <<EOC >> /etc/apt/apt.conf.d/80retries
"APT::Acquire::Retries \"10\";"
EOC
echo <<EOC >> /etc/apt/apt.conf.d/90assumeyes
"APT::Get::Assume-Yes \"true\";"
EOC
EOF
# Prepare apt-fast
RUN <<EOF
apt-get update
apt-get install -y --no-install-recommends software-properties-common
add-apt-repository -y ppa:apt-fast/stable
apt-get -y install apt-fast
echo debconf apt-fast/maxdownloads string 8 | debconf-set-selections
echo debconf apt-fast/dlflag boolean true | debconf-set-selections
echo debconf apt-fast/aptmanager string apt-get | debconf-set-selections
EOF
# Base packages
# https://github.com/actions/runner-images/blob/main/images/ubuntu/toolsets/toolset-2204.json#L114
RUN <<EOF
apt-fast install -y --no-install-recommends \
ca-certificates \
bzip2 curl g++ gcc make jq tar unzip wget \
sudo git openssh-client
EOF
# Prepare git
RUN <<EOF
cat <<EOC >> /etc/gitconfig
[safe]
directory = *
EOC
ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /etc/ssh/ssh_known_hosts
ssh-keyscan -t rsa,ecdsa,ed25519 ravy.dev >> /etc/ssh/ssh_known_hosts
EOF
# Haxe native dependencies
RUN <<EOF
apt-fast install -y --no-install-recommends \
libc6-dev libffi-dev \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
libasound2-dev \
libvlc-dev libvlccore-dev
EOF
# Janky libffi.6 fix
RUN <<EOF
ln -s \
/usr/lib/x86_64-linux-gnu/libffi.so.8 \
/usr/lib/x86_64-linux-gnu/libffi.so.6 \
|| true
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
EOF
RUN <<EOF
neko_path="$(find /usr/local -maxdepth 1 -type d -name 'neko*')"
ln -s "$neko_path" /usr/local/neko
EOF
ENV NEKOPATH="/usr/local/neko"
ENV LD_LIBRARY_PATH="$NEKOPATH:$LD_LIBRARY_PATH"
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
EOF
RUN <<EOF
haxe_path="$(find /usr/local -maxdepth 1 -type d -name 'haxe*')"
ln -s "$haxe_path" /usr/local/haxe
EOF
ENV HAXEPATH="/usr/local/haxe"
ENV HAXE_STD_PATH="$HAXEPATH/std"
ENV PATH="$HAXEPATH:$PATH"
# haxelib
RUN <<EOF
HOME=/etc haxelib setup "$HAXEPATH/lib"
haxelib --global --never install haxelib $haxelib_version
haxelib --global --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
haxelib --global --never install hmm
EOF
# hxcpp
ENV HXCPP_COMPILE_CACHE="/usr/share/hxcpp"
ENV HXCPP_CACHE_MB="4096"
# Clean up
# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/cleanup.sh
RUN <<EOF
rm -r /var/cache/apt/apt-fast
apt-get clean
if [ -d /var/lib/apt/lists ]; then
rm -rf /var/lib/apt/lists/*
fi
if [ -d /tmp ]; then
rm -rf /tmp/*
fi
if [ -d /root/.cache ]; then
rm -rf /root/.cache
fi
if command -v journalctl; then
journalctl --rotate
journalctl --vacuum-time=1s
fi
if [ -d /var/log ]; then
find /var/log -type f -regex ".*\.gz$" -delete
find /var/log -type f -regex ".*\.[0-9]$" -delete
find /var/log/ -type f -exec cp /dev/null {} \;
fi
if [ -f /usr/local/bin/invoke_tests ]; then
rm -rf /usr/local/bin/invoke_tests
fi
EOF
# Print debug info
RUN <<EOF
echo "/root"
ls -la /root
cat /root/.haxelib && echo
id
env
EOF

View file

@ -4,7 +4,7 @@
"name": "discord_rpc", "name": "discord_rpc",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "2d83fa863ef0c1eace5f1cf67c3ac315d1a3a8a5", "ref": "2d83fa8",
"url": "https://github.com/Aidan63/linc_discord-rpc" "url": "https://github.com/Aidan63/linc_discord-rpc"
}, },
{ {
@ -18,7 +18,7 @@
"name": "flixel-addons", "name": "flixel-addons",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "a523c3b56622f0640933944171efed46929e360e", "ref": "a523c3b",
"url": "https://github.com/FunkinCrew/flixel-addons" "url": "https://github.com/FunkinCrew/flixel-addons"
}, },
{ {
@ -30,14 +30,14 @@
"name": "flixel-ui", "name": "flixel-ui",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15", "ref": "719b4f1",
"url": "https://github.com/HaxeFlixel/flixel-ui" "url": "https://github.com/HaxeFlixel/flixel-ui"
}, },
{ {
"name": "flxanimate", "name": "flxanimate",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "9bacdd6ea39f5e3a33b0f5dfb7bc583fe76060d4", "ref": "9bacdd6",
"url": "https://github.com/FunkinCrew/flxanimate" "url": "https://github.com/FunkinCrew/flxanimate"
}, },
{ {
@ -45,6 +45,13 @@
"type": "haxelib", "type": "haxelib",
"version": "3.5.0" "version": "3.5.0"
}, },
{
"name": "funkVis",
"type": "git",
"dir": null,
"ref": "backend-rework",
"url": "https://github.com/FunkinCrew/funkVis"
},
{ {
"name": "hamcrest", "name": "hamcrest",
"type": "haxelib", "type": "haxelib",
@ -54,14 +61,14 @@
"name": "haxeui-core", "name": "haxeui-core",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "0212d8fdfcafeb5f0d5a41e1ddba8ff21d0e183b", "ref": "0212d8fd",
"url": "https://github.com/haxeui/haxeui-core" "url": "https://github.com/haxeui/haxeui-core"
}, },
{ {
"name": "haxeui-flixel", "name": "haxeui-flixel",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "63a906a6148958dbfde8c7b48d90b0693767fd95", "ref": "63a906a",
"url": "https://github.com/haxeui/haxeui-flixel" "url": "https://github.com/haxeui/haxeui-flixel"
}, },
{ {
@ -73,7 +80,7 @@
"name": "hxCodec", "name": "hxCodec",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "387e1665d6feb5762358134f168e6ebfe46acec8", "ref": "387e166",
"url": "https://github.com/FunkinCrew/hxCodec" "url": "https://github.com/FunkinCrew/hxCodec"
}, },
{ {
@ -85,7 +92,7 @@
"name": "hxcpp-debug-server", "name": "hxcpp-debug-server",
"type": "git", "type": "git",
"dir": "hxcpp-debug-server", "dir": "hxcpp-debug-server",
"ref": "147294123f983e35f50a966741474438069a7a8f", "ref": "1472941",
"url": "https://github.com/FunkinCrew/hxcpp-debugger" "url": "https://github.com/FunkinCrew/hxcpp-debugger"
}, },
{ {
@ -97,49 +104,49 @@
"name": "json2object", "name": "json2object",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa", "ref": "a8c26f1",
"url": "https://github.com/FunkinCrew/json2object" "url": "https://github.com/FunkinCrew/json2object"
}, },
{ {
"name": "lime", "name": "lime",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "1359fe6ad52e91175dc636a516d460bd54ea22ed", "ref": "43ebebdd8119936b99f23407057025c7849c5f5b",
"url": "https://github.com/FunkinCrew/lime" "url": "https://github.com/FunkinCrew/lime"
}, },
{ {
"name": "mconsole", "name": "mconsole",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "master", "ref": "06c0499",
"url": "https://github.com/massive-oss/mconsole" "url": "https://github.com/massive-oss/mconsole"
}, },
{ {
"name": "mcover", "name": "mcover",
"type": "git", "type": "git",
"dir": "src", "dir": "src",
"ref": "master", "ref": "c3c47cd",
"url": "https://github.com/massive-oss/mcover" "url": "https://github.com/massive-oss/mcover"
}, },
{ {
"name": "mockatoo", "name": "mockatoo",
"type": "git", "type": "git",
"dir": "src", "dir": "src",
"ref": "master", "ref": "13d77a0",
"url": "https://github.com/FunkinCrew/mockatoo" "url": "https://github.com/FunkinCrew/mockatoo"
}, },
{ {
"name": "munit", "name": "munit",
"type": "git", "type": "git",
"dir": "src", "dir": "src",
"ref": "master", "ref": "f61be7f",
"url": "https://github.com/FunkinCrew/MassiveUnit" "url": "https://github.com/FunkinCrew/MassiveUnit"
}, },
{ {
"name": "openfl", "name": "openfl",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "f229d76361c7e31025a048fe7909847f75bb5d5e", "ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134",
"url": "https://github.com/FunkinCrew/openfl" "url": "https://github.com/FunkinCrew/openfl"
}, },
{ {

View file

@ -113,6 +113,8 @@ class Main extends Sprite
addChild(game); addChild(game);
addChild(fpsCounter);
#if hxcpp_debug_server #if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.'); trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#else #else
@ -128,6 +130,8 @@ class Main extends Sprite
Toolkit.init(); Toolkit.init();
Toolkit.theme = 'dark'; // don't be cringe Toolkit.theme = 'dark'; // don't be cringe
Toolkit.autoScale = false; Toolkit.autoScale = false;
// Don't focus on UI elements when they first appear.
haxe.ui.focus.FocusManager.instance.autoFocus = false;
funkin.input.Cursor.registerHaxeUICursors(); funkin.input.Cursor.registerHaxeUICursors();
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200; haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
} }

View file

@ -59,6 +59,7 @@ abstract Tallies(RawTallies)
totalNotes: 0, totalNotes: 0,
totalNotesHit: 0, totalNotesHit: 0,
maxCombo: 0, maxCombo: 0,
score: 0,
isNewHighscore: false isNewHighscore: false
} }
} }
@ -81,6 +82,9 @@ typedef RawTallies =
var good:Int; var good:Int;
var sick:Int; var sick:Int;
var maxCombo:Int; var maxCombo:Int;
var score:Int;
var isNewHighscore:Bool; var isNewHighscore:Bool;
/** /**

View file

@ -261,6 +261,35 @@ class InitState extends FlxState
return; return;
} }
// TODO: Rework loading behavior so we don't have to do this.
switch (songId)
{
case 'tutorial' | 'bopeebo' | 'fresh' | 'dadbattle':
Paths.setCurrentLevel('week1');
PlayStatePlaylist.campaignId = 'week1';
case 'spookeez' | 'south' | 'monster':
Paths.setCurrentLevel('week2');
PlayStatePlaylist.campaignId = 'week2';
case 'pico' | 'philly-nice' | 'blammed':
Paths.setCurrentLevel('week3');
PlayStatePlaylist.campaignId = 'week3';
case 'high' | 'satin-panties' | 'milf':
Paths.setCurrentLevel('week4');
PlayStatePlaylist.campaignId = 'week4';
case 'cocoa' | 'eggnog' | 'winter-horrorland':
Paths.setCurrentLevel('week5');
PlayStatePlaylist.campaignId = 'week5';
case 'senpai' | 'roses' | 'thorns':
Paths.setCurrentLevel('week6');
PlayStatePlaylist.campaignId = 'week6';
case 'ugh' | 'guns' | 'stress':
Paths.setCurrentLevel('week7');
PlayStatePlaylist.campaignId = 'week7';
case 'darnell' | 'lit-up' | '2hot' | 'blazin':
Paths.setCurrentLevel('weekend1');
PlayStatePlaylist.campaignId = 'weekend1';
}
LoadingState.loadPlayState( LoadingState.loadPlayState(
{ {
targetSong: songData, targetSong: songData,
@ -283,6 +312,10 @@ class InitState extends FlxState
return; return;
} }
// TODO: Rework loading behavior so we don't have to do this.
Paths.setCurrentLevel(levelId);
PlayStatePlaylist.campaignId = levelId;
PlayStatePlaylist.playlistSongIds = currentLevel.getSongs(); PlayStatePlaylist.playlistSongIds = currentLevel.getSongs();
PlayStatePlaylist.isStoryMode = true; PlayStatePlaylist.isStoryMode = true;
PlayStatePlaylist.campaignScore = 0; PlayStatePlaylist.campaignScore = 0;

View file

@ -9,7 +9,7 @@ import openfl.utils.Assets as OpenFlAssets;
*/ */
class Paths class Paths
{ {
static var currentLevel:String; static var currentLevel:Null<String> = null;
public static function setCurrentLevel(name:String):Void public static function setCurrentLevel(name:String):Void
{ {
@ -113,7 +113,7 @@ class Paths
public static function videos(key:String, ?library:String):String public static function videos(key:String, ?library:String):String
{ {
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library); return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library ?? 'videos');
} }
public static function voices(song:String, ?suffix:String = ''):String public static function voices(song:String, ?suffix:String = ''):String

View file

@ -223,11 +223,12 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
// already paused before we lost focus. // already paused before we lost focus.
if (_lostFocus && !_alreadyPaused) if (_lostFocus && !_alreadyPaused)
{ {
trace('Resuming audio (${this._label}) on focus!');
resume(); resume();
} }
else else
{ {
trace('Not resuming audio on focus!'); trace('Not resuming audio (${this._label}) on focus!');
} }
_lostFocus = false; _lostFocus = false;
} }
@ -265,10 +266,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
@:allow(flixel.sound.FlxSoundGroup) @:allow(flixel.sound.FlxSoundGroup)
override function updateTransform():Void override function updateTransform():Void
{ {
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end if (_transform != null)
(group != null ? group.volume : 1) * _volume * _volumeAdjust; {
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
}
if (_channel != null) _channel.soundTransform = _transform; if (_channel != null)
{
_channel.soundTransform = _transform;
}
} }
public function clone():FunkinSound public function clone():FunkinSound
@ -315,6 +322,13 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
} }
} }
if (FlxG.sound.music != null)
{
FlxG.sound.music.fadeTween?.cancel();
FlxG.sound.music.stop();
FlxG.sound.music.kill();
}
if (params?.mapTimeChanges ?? true) if (params?.mapTimeChanges ?? true)
{ {
var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key); var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key);
@ -329,13 +343,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
} }
} }
if (FlxG.sound.music != null)
{
FlxG.sound.music.fadeTween?.cancel();
FlxG.sound.music.stop();
FlxG.sound.music.kill();
}
var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true); var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
if (music != null) if (music != null)
{ {
@ -391,10 +398,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
sound._label = 'unknown'; sound._label = 'unknown';
} }
if (autoPlay) sound.play();
sound.volume = volume; sound.volume = volume;
sound.group = FlxG.sound.defaultSoundGroup; sound.group = FlxG.sound.defaultSoundGroup;
sound.persist = true; sound.persist = true;
if (autoPlay) sound.play();
// Make sure to add the sound to the list.
// If it's already in, it won't get re-added.
// If it's not in the list (it gets removed by FunkinSound.playMusic()),
// it will get re-added (then if this was called by playMusic(), removed again)
FlxG.sound.list.add(sound);
// Call onLoad() because the sound already loaded // Call onLoad() because the sound already loaded
if (onLoad != null && sound._sound != null) onLoad(); if (onLoad != null && sound._sound != null) onLoad();

View file

@ -150,7 +150,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
/** /**
* Stop all the sounds in the group. * Stop all the sounds in the group.
*/ */
public function stop() public function stop():Void
{ {
if (members != null) if (members != null)
{ {
@ -160,7 +160,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
} }
} }
public override function destroy() public override function destroy():Void
{ {
stop(); stop();
super.destroy(); super.destroy();
@ -178,9 +178,14 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_time():Float function get_time():Float
{ {
if (getFirstAlive() != null) return getFirstAlive().time; if (getFirstAlive() != null)
{
return getFirstAlive().time;
}
else else
{
return 0; return 0;
}
} }
function set_time(time:Float):Float function set_time(time:Float):Float
@ -195,16 +200,26 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_playing():Bool function get_playing():Bool
{ {
if (getFirstAlive() != null) return getFirstAlive().playing; if (getFirstAlive() != null)
{
return getFirstAlive().playing;
}
else else
{
return false; return false;
}
} }
function get_volume():Float function get_volume():Float
{ {
if (getFirstAlive() != null) return getFirstAlive().volume; if (getFirstAlive() != null)
{
return getFirstAlive().volume;
}
else else
{
return 1; return 1;
}
} }
// in PlayState, adjust the code so that it only mutes the player1 vocal tracks? // in PlayState, adjust the code so that it only mutes the player1 vocal tracks?

View file

@ -159,10 +159,18 @@ class VoicesGroup extends SoundGroup
public override function destroy():Void public override function destroy():Void
{ {
playerVoices.destroy(); if (playerVoices != null)
playerVoices = null; {
opponentVoices.destroy(); playerVoices.destroy();
opponentVoices = null; playerVoices = null;
}
if (opponentVoices != null)
{
opponentVoices.destroy();
opponentVoices = null;
}
super.destroy(); super.destroy();
} }
} }

View file

@ -8,153 +8,180 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.sound.FlxSound; import flixel.sound.FlxSound;
import funkin.util.MathUtil; import funkin.util.MathUtil;
import funkVis.dsp.SpectralAnalyzer;
import funkVis.audioclip.frontends.LimeAudioClip;
using Lambda; using Lambda;
class ABotVis extends FlxTypedSpriteGroup<FlxSprite> class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
{ {
public var vis:VisShit; // public var vis:VisShit;
var analyzer:SpectralAnalyzer;
var volumes:Array<Float> = []; var volumes:Array<Float> = [];
public var snd:FlxSound;
public function new(snd:FlxSound) public function new(snd:FlxSound)
{ {
super(); super();
vis = new VisShit(snd); this.snd = snd;
// vis = new VisShit(snd);
// vis.snd = snd; // vis.snd = snd;
var visFrms:FlxAtlasFrames = Paths.getSparrowAtlas('aBotViz'); var visFrms:FlxAtlasFrames = Paths.getSparrowAtlas('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];
for (lol in 1...8) for (lol in 1...8)
{ {
// pushes initial value // pushes initial value
volumes.push(0.0); 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(50 * lol, 0); var viz:FlxSprite = new FlxSprite(posX, posY);
viz.frames = visFrms; viz.frames = visFrms;
add(viz); add(viz);
var visStr = 'VIZ'; var visStr = 'viz';
if (lol == 5) visStr = 'viz'; // lol makes it lowercase, accomodates for art that I dont wanna rename!
viz.animation.addByPrefix('VIZ', visStr + lol, 0); viz.animation.addByPrefix('VIZ', visStr + lol, 0);
viz.animation.play('VIZ', false, false, -1); viz.animation.play('VIZ', false, false, 2);
} }
} }
public function initAnalyzer()
{
@:privateAccess
analyzer = new SpectralAnalyzer(7, new LimeAudioClip(cast snd._channel.__source), 0.01, 30);
analyzer.maxDb = -35;
// analyzer.fftN = 2048;
}
var visTimer:Float = -1;
var visTimeMax:Float = 1 / 30;
override function update(elapsed:Float) override function update(elapsed:Float)
{ {
// updateViz(); // updateViz();
updateFFT(elapsed); // updateFFT(elapsed);
//
super.update(elapsed); super.update(elapsed);
} }
function updateFFT(elapsed:Float) static inline function min(x:Int, y:Int):Int
{ {
if (vis.snd != null) return x > y ? y : x;
}
override function draw()
{
#if web
if (analyzer != null) drawFFT();
#end
super.draw();
}
/**
* TJW funkVis based visualizer! updateFFT() is the old nasty shit that dont worky!
*/
function drawFFT():Void
{
var levels = analyzer.getLevels(false);
for (i in 0...min(group.members.length, levels.length))
{ {
vis.checkAndSetBuffer(); var animFrame:Int = Math.round(levels[i].value * 5);
if (vis.setBuffer) animFrame = Math.floor(Math.min(5, animFrame));
{ animFrame = Math.floor(Math.max(0, animFrame));
var remappedShit:Int = 0;
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples)); animFrame = Std.int(Math.abs(animFrame - 5)); // shitty dumbass flip, cuz dave got da shit backwards lol!
else
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples));
var fftSamples:Array<Float> = []; group.members[i].animation.curAnim.curFrame = animFrame;
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() // function updateFFT(elapsed:Float)
{ // {
if (vis.snd != null) // if (vis.snd != null)
{ // {
var remappedShit:Int = 0; // vis.checkAndSetBuffer();
vis.checkAndSetBuffer(); // if (vis.setBuffer)
// {
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));
// var startingSample:Int = Std.int(FlxMath.remapToRange) // else
// remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples));
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples)); // var fftSamples:Array<Float> = [];
// var swagBucks = remappedShit;
for (i in 0...group.members.length) // for (i in remappedShit...remappedShit + (Std.int((44100 * (1 / 144)))))
{ // {
var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, remappedShit, remappedShit + 500)); // var left = vis.audioData[swagBucks] / 32767;
// var right = vis.audioData[swagBucks + 1] / 32767;
var left = vis.audioData[sampleApprox] / 32767; // var balanced = (left + right) / 2;
// swagBucks += 2;
var animFrame:Int = Std.int(FlxMath.remapToRange(left, -1, 1, 0, 6)); // fftSamples.push(balanced);
// }
group.members[i].animation.curAnim.curFrame = animFrame; // 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;
// }
// }
// }
// }
} }

View file

@ -325,12 +325,3 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
} }
} }
} }
/**
* A pair of a file name and its contents.
*/
typedef JsonFile =
{
fileName:String,
contents:String
};

View file

@ -0,0 +1,10 @@
package funkin.data;
/**
* A pair of a file name and its contents.
*/
typedef JsonFile =
{
fileName:String,
contents:String
};

View file

@ -1,5 +1,7 @@
package funkin.data.freeplay; package funkin.data.freeplay;
import funkin.data.animation.AnimationData;
/** /**
* A type definition for the data for an album of songs. * A type definition for the data for an album of songs.
* It includes things like what graphics to display in Freeplay. * It includes things like what graphics to display in Freeplay.
@ -33,4 +35,11 @@ typedef AlbumData =
* The album title will be displayed below the album art in Freeplay. * The album title will be displayed below the album art in Freeplay.
*/ */
public var albumTitleAsset:String; public var albumTitleAsset:String;
/**
* An optional array of animations for the album title.
*/
@:optional
@:default([])
public var albumTitleAnimations:Array<AnimationData>;
} }

View file

@ -427,7 +427,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return ScriptedSong.listScriptClasses(); return ScriptedSong.listScriptClasses();
} }
function loadEntryMetadataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> function loadEntryMetadataFile(id:String, ?variation:String):Null<JsonFile>
{ {
variation = variation == null ? Constants.DEFAULT_VARIATION : variation; variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
@ -442,7 +442,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return {fileName: entryFilePath, contents: rawJson}; return {fileName: entryFilePath, contents: rawJson};
} }
function loadMusicDataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> function loadMusicDataFile(id:String, ?variation:String):Null<JsonFile>
{ {
variation = variation == null ? Constants.DEFAULT_VARIATION : variation; variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json'); var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json');
@ -460,7 +460,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return openfl.Assets.exists(entryFilePath); return openfl.Assets.exists(entryFilePath);
} }
function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile>
{ {
variation = variation == null ? Constants.DEFAULT_VARIATION : variation; variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');

View file

@ -47,9 +47,13 @@ class FunkinCamera extends FlxCamera
public var shouldDraw:Bool = true; public var shouldDraw:Bool = true;
public function new(x:Int = 0, y:Int = 0, width:Int = 0, height:Int = 0, zoom:Float = 0) // Used to identify the camera during debugging.
final id:String = 'unknown';
public function new(id:String = 'unknown', x:Int = 0, y:Int = 0, width:Int = 0, height:Int = 0, zoom:Float = 0)
{ {
super(x, y, width, height, zoom); super(x, y, width, height, zoom);
this.id = id;
bgTexture = pickTexture(width, height); bgTexture = pickTexture(width, height);
bgBitmap = FixedBitmapData.fromTexture(bgTexture); bgBitmap = FixedBitmapData.fromTexture(bgTexture);
bgFrame = new FlxFrame(new FlxGraphic('', null)); bgFrame = new FlxFrame(new FlxGraphic('', null));

View file

@ -137,7 +137,8 @@ class FlxAtlasSprite extends FlxAnimate
anim.callback = function(_, frame:Int) { anim.callback = function(_, frame:Int) {
var offset = loop ? 0 : -1; var offset = loop ? 0 : -1;
if (frame == (anim.getFrameLabel(id).duration + offset) + anim.getFrameLabel(id).index) var frameLabel = anim.getFrameLabel(id);
if (frame == (frameLabel.duration + offset) + frameLabel.index)
{ {
if (loop) if (loop)
{ {

View file

@ -20,6 +20,6 @@ class GaussianBlurShader extends FlxRuntimeShader
public function setAmount(value:Float):Void public function setAmount(value:Float):Void
{ {
this.amount = value; this.amount = value;
this.setFloat("amount", amount); this.setFloat("_amount", amount);
} }
} }

View file

@ -17,6 +17,6 @@ class Grayscale extends FlxRuntimeShader
public function setAmount(value:Float):Void public function setAmount(value:Float):Void
{ {
amount = value; amount = value;
this.setFloat("amount", amount); this.setFloat("_amount", amount);
} }
} }

View file

@ -20,7 +20,7 @@ class HSVShader extends FlxRuntimeShader
function set_hue(value:Float):Float function set_hue(value:Float):Float
{ {
this.setFloat('hue', value); this.setFloat('_hue', value);
this.hue = value; this.hue = value;
return this.hue; return this.hue;

View file

@ -30,44 +30,44 @@ class Controls extends FlxActionSet
* A list of actions that a player would invoke via some input device. * A list of actions that a player would invoke via some input device.
* Uses FlxActions to funnel various inputs to a single action. * Uses FlxActions to funnel various inputs to a single action.
*/ */
var _ui_up = new FlxActionDigital(Action.UI_UP); var _ui_up = new FunkinAction(Action.UI_UP);
var _ui_left = new FlxActionDigital(Action.UI_LEFT); var _ui_left = new FunkinAction(Action.UI_LEFT);
var _ui_right = new FlxActionDigital(Action.UI_RIGHT); var _ui_right = new FunkinAction(Action.UI_RIGHT);
var _ui_down = new FlxActionDigital(Action.UI_DOWN); var _ui_down = new FunkinAction(Action.UI_DOWN);
var _ui_upP = new FlxActionDigital(Action.UI_UP_P); var _ui_upP = new FunkinAction(Action.UI_UP_P);
var _ui_leftP = new FlxActionDigital(Action.UI_LEFT_P); var _ui_leftP = new FunkinAction(Action.UI_LEFT_P);
var _ui_rightP = new FlxActionDigital(Action.UI_RIGHT_P); var _ui_rightP = new FunkinAction(Action.UI_RIGHT_P);
var _ui_downP = new FlxActionDigital(Action.UI_DOWN_P); var _ui_downP = new FunkinAction(Action.UI_DOWN_P);
var _ui_upR = new FlxActionDigital(Action.UI_UP_R); var _ui_upR = new FunkinAction(Action.UI_UP_R);
var _ui_leftR = new FlxActionDigital(Action.UI_LEFT_R); var _ui_leftR = new FunkinAction(Action.UI_LEFT_R);
var _ui_rightR = new FlxActionDigital(Action.UI_RIGHT_R); var _ui_rightR = new FunkinAction(Action.UI_RIGHT_R);
var _ui_downR = new FlxActionDigital(Action.UI_DOWN_R); var _ui_downR = new FunkinAction(Action.UI_DOWN_R);
var _note_up = new FlxActionDigital(Action.NOTE_UP); var _note_up = new FunkinAction(Action.NOTE_UP);
var _note_left = new FlxActionDigital(Action.NOTE_LEFT); var _note_left = new FunkinAction(Action.NOTE_LEFT);
var _note_right = new FlxActionDigital(Action.NOTE_RIGHT); var _note_right = new FunkinAction(Action.NOTE_RIGHT);
var _note_down = new FlxActionDigital(Action.NOTE_DOWN); var _note_down = new FunkinAction(Action.NOTE_DOWN);
var _note_upP = new FlxActionDigital(Action.NOTE_UP_P); var _note_upP = new FunkinAction(Action.NOTE_UP_P);
var _note_leftP = new FlxActionDigital(Action.NOTE_LEFT_P); var _note_leftP = new FunkinAction(Action.NOTE_LEFT_P);
var _note_rightP = new FlxActionDigital(Action.NOTE_RIGHT_P); var _note_rightP = new FunkinAction(Action.NOTE_RIGHT_P);
var _note_downP = new FlxActionDigital(Action.NOTE_DOWN_P); var _note_downP = new FunkinAction(Action.NOTE_DOWN_P);
var _note_upR = new FlxActionDigital(Action.NOTE_UP_R); var _note_upR = new FunkinAction(Action.NOTE_UP_R);
var _note_leftR = new FlxActionDigital(Action.NOTE_LEFT_R); var _note_leftR = new FunkinAction(Action.NOTE_LEFT_R);
var _note_rightR = new FlxActionDigital(Action.NOTE_RIGHT_R); var _note_rightR = new FunkinAction(Action.NOTE_RIGHT_R);
var _note_downR = new FlxActionDigital(Action.NOTE_DOWN_R); var _note_downR = new FunkinAction(Action.NOTE_DOWN_R);
var _accept = new FlxActionDigital(Action.ACCEPT); var _accept = new FunkinAction(Action.ACCEPT);
var _back = new FlxActionDigital(Action.BACK); var _back = new FunkinAction(Action.BACK);
var _pause = new FlxActionDigital(Action.PAUSE); var _pause = new FunkinAction(Action.PAUSE);
var _reset = new FlxActionDigital(Action.RESET); var _reset = new FunkinAction(Action.RESET);
var _screenshot = new FlxActionDigital(Action.SCREENSHOT); var _screenshot = new FunkinAction(Action.SCREENSHOT);
var _cutscene_advance = new FlxActionDigital(Action.CUTSCENE_ADVANCE); var _cutscene_advance = new FunkinAction(Action.CUTSCENE_ADVANCE);
var _debug_menu = new FlxActionDigital(Action.DEBUG_MENU); var _debug_menu = new FunkinAction(Action.DEBUG_MENU);
var _debug_chart = new FlxActionDigital(Action.DEBUG_CHART); var _debug_chart = new FunkinAction(Action.DEBUG_CHART);
var _debug_stage = new FlxActionDigital(Action.DEBUG_STAGE); var _debug_stage = new FunkinAction(Action.DEBUG_STAGE);
var _volume_up = new FlxActionDigital(Action.VOLUME_UP); var _volume_up = new FunkinAction(Action.VOLUME_UP);
var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN); var _volume_down = new FunkinAction(Action.VOLUME_DOWN);
var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE); var _volume_mute = new FunkinAction(Action.VOLUME_MUTE);
var byName:Map<String, FlxActionDigital> = new Map<String, FlxActionDigital>(); var byName:Map<String, FunkinAction> = new Map<String, FunkinAction>();
public var gamepadsAdded:Array<Int> = []; public var gamepadsAdded:Array<Int> = [];
public var keyboardScheme = KeyboardScheme.None; public var keyboardScheme = KeyboardScheme.None;
@ -75,122 +75,142 @@ class Controls extends FlxActionSet
public var UI_UP(get, never):Bool; public var UI_UP(get, never):Bool;
inline function get_UI_UP() inline function get_UI_UP()
return _ui_up.check(); return _ui_up.checkPressed();
public var UI_LEFT(get, never):Bool; public var UI_LEFT(get, never):Bool;
inline function get_UI_LEFT() inline function get_UI_LEFT()
return _ui_left.check(); return _ui_left.checkPressed();
public var UI_RIGHT(get, never):Bool; public var UI_RIGHT(get, never):Bool;
inline function get_UI_RIGHT() inline function get_UI_RIGHT()
return _ui_right.check(); return _ui_right.checkPressed();
public var UI_DOWN(get, never):Bool; public var UI_DOWN(get, never):Bool;
inline function get_UI_DOWN() inline function get_UI_DOWN()
return _ui_down.check(); return _ui_down.checkPressed();
public var UI_UP_P(get, never):Bool; public var UI_UP_P(get, never):Bool;
inline function get_UI_UP_P() inline function get_UI_UP_P()
return _ui_upP.check(); return _ui_up.checkJustPressed();
public var UI_LEFT_P(get, never):Bool; public var UI_LEFT_P(get, never):Bool;
inline function get_UI_LEFT_P() inline function get_UI_LEFT_P()
return _ui_leftP.check(); return _ui_left.checkJustPressed();
public var UI_RIGHT_P(get, never):Bool; public var UI_RIGHT_P(get, never):Bool;
inline function get_UI_RIGHT_P() inline function get_UI_RIGHT_P()
return _ui_rightP.check(); return _ui_right.checkJustPressed();
public var UI_DOWN_P(get, never):Bool; public var UI_DOWN_P(get, never):Bool;
inline function get_UI_DOWN_P() inline function get_UI_DOWN_P()
return _ui_downP.check(); return _ui_down.checkJustPressed();
public var UI_UP_R(get, never):Bool; public var UI_UP_R(get, never):Bool;
inline function get_UI_UP_R() inline function get_UI_UP_R()
return _ui_upR.check(); return _ui_up.checkJustReleased();
public var UI_LEFT_R(get, never):Bool; public var UI_LEFT_R(get, never):Bool;
inline function get_UI_LEFT_R() inline function get_UI_LEFT_R()
return _ui_leftR.check(); return _ui_left.checkJustReleased();
public var UI_RIGHT_R(get, never):Bool; public var UI_RIGHT_R(get, never):Bool;
inline function get_UI_RIGHT_R() inline function get_UI_RIGHT_R()
return _ui_rightR.check(); return _ui_right.checkJustReleased();
public var UI_DOWN_R(get, never):Bool; public var UI_DOWN_R(get, never):Bool;
inline function get_UI_DOWN_R() inline function get_UI_DOWN_R()
return _ui_downR.check(); return _ui_down.checkJustReleased();
public var UI_UP_GAMEPAD(get, never):Bool;
inline function get_UI_UP_GAMEPAD()
return _ui_up.checkPressedGamepad();
public var UI_LEFT_GAMEPAD(get, never):Bool;
inline function get_UI_LEFT_GAMEPAD()
return _ui_left.checkPressedGamepad();
public var UI_RIGHT_GAMEPAD(get, never):Bool;
inline function get_UI_RIGHT_GAMEPAD()
return _ui_right.checkPressedGamepad();
public var UI_DOWN_GAMEPAD(get, never):Bool;
inline function get_UI_DOWN_GAMEPAD()
return _ui_down.checkPressedGamepad();
public var NOTE_UP(get, never):Bool; public var NOTE_UP(get, never):Bool;
inline function get_NOTE_UP() inline function get_NOTE_UP()
return _note_up.check(); return _note_up.checkPressed();
public var NOTE_LEFT(get, never):Bool; public var NOTE_LEFT(get, never):Bool;
inline function get_NOTE_LEFT() inline function get_NOTE_LEFT()
return _note_left.check(); return _note_left.checkPressed();
public var NOTE_RIGHT(get, never):Bool; public var NOTE_RIGHT(get, never):Bool;
inline function get_NOTE_RIGHT() inline function get_NOTE_RIGHT()
return _note_right.check(); return _note_right.checkPressed();
public var NOTE_DOWN(get, never):Bool; public var NOTE_DOWN(get, never):Bool;
inline function get_NOTE_DOWN() inline function get_NOTE_DOWN()
return _note_down.check(); return _note_down.checkPressed();
public var NOTE_UP_P(get, never):Bool; public var NOTE_UP_P(get, never):Bool;
inline function get_NOTE_UP_P() inline function get_NOTE_UP_P()
return _note_upP.check(); return _note_up.checkJustPressed();
public var NOTE_LEFT_P(get, never):Bool; public var NOTE_LEFT_P(get, never):Bool;
inline function get_NOTE_LEFT_P() inline function get_NOTE_LEFT_P()
return _note_leftP.check(); return _note_left.checkJustPressed();
public var NOTE_RIGHT_P(get, never):Bool; public var NOTE_RIGHT_P(get, never):Bool;
inline function get_NOTE_RIGHT_P() inline function get_NOTE_RIGHT_P()
return _note_rightP.check(); return _note_right.checkJustPressed();
public var NOTE_DOWN_P(get, never):Bool; public var NOTE_DOWN_P(get, never):Bool;
inline function get_NOTE_DOWN_P() inline function get_NOTE_DOWN_P()
return _note_downP.check(); return _note_down.checkJustPressed();
public var NOTE_UP_R(get, never):Bool; public var NOTE_UP_R(get, never):Bool;
inline function get_NOTE_UP_R() inline function get_NOTE_UP_R()
return _note_upR.check(); return _note_up.checkJustReleased();
public var NOTE_LEFT_R(get, never):Bool; public var NOTE_LEFT_R(get, never):Bool;
inline function get_NOTE_LEFT_R() inline function get_NOTE_LEFT_R()
return _note_leftR.check(); return _note_left.checkJustReleased();
public var NOTE_RIGHT_R(get, never):Bool; public var NOTE_RIGHT_R(get, never):Bool;
inline function get_NOTE_RIGHT_R() inline function get_NOTE_RIGHT_R()
return _note_rightR.check(); return _note_right.checkJustReleased();
public var NOTE_DOWN_R(get, never):Bool; public var NOTE_DOWN_R(get, never):Bool;
inline function get_NOTE_DOWN_R() inline function get_NOTE_DOWN_R()
return _note_downR.check(); return _note_down.checkJustReleased();
public var ACCEPT(get, never):Bool; public var ACCEPT(get, never):Bool;
@ -260,26 +280,10 @@ class Controls extends FlxActionSet
add(_ui_left); add(_ui_left);
add(_ui_right); add(_ui_right);
add(_ui_down); add(_ui_down);
add(_ui_upP);
add(_ui_leftP);
add(_ui_rightP);
add(_ui_downP);
add(_ui_upR);
add(_ui_leftR);
add(_ui_rightR);
add(_ui_downR);
add(_note_up); add(_note_up);
add(_note_left); add(_note_left);
add(_note_right); add(_note_right);
add(_note_down); add(_note_down);
add(_note_upP);
add(_note_leftP);
add(_note_rightP);
add(_note_downP);
add(_note_upR);
add(_note_leftR);
add(_note_rightR);
add(_note_downR);
add(_accept); add(_accept);
add(_back); add(_back);
add(_pause); add(_pause);
@ -293,8 +297,16 @@ class Controls extends FlxActionSet
add(_volume_down); add(_volume_down);
add(_volume_mute); add(_volume_mute);
for (action in digitalActions) for (action in digitalActions) {
byName[action.name] = action; if (Std.isOfType(action, FunkinAction)) {
var funkinAction:FunkinAction = cast action;
byName[funkinAction.name] = funkinAction;
if (funkinAction.namePressed != null)
byName[funkinAction.namePressed] = funkinAction;
if (funkinAction.nameReleased != null)
byName[funkinAction.nameReleased] = funkinAction;
}
}
if (scheme == null) if (scheme == null)
scheme = None; scheme = None;
@ -307,14 +319,17 @@ class Controls extends FlxActionSet
super.update(); super.update();
} }
// inline public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool
public function checkByName(name:Action):Bool
{ {
#if debug #if debug
if (!byName.exists(name)) if (!byName.exists(name))
throw 'Invalid name: $name'; throw 'Invalid name: $name';
#end #end
return byName[name].check(); var action = byName[name];
if (gamepadOnly)
return action.checkFiltered(trigger, GAMEPAD);
else
return action.checkFiltered(trigger);
} }
public function getKeysForAction(name:Action):Array<FlxKey> { public function getKeysForAction(name:Action):Array<FlxKey> {
@ -405,36 +420,36 @@ class Controls extends FlxActionSet
{ {
case UI_UP: case UI_UP:
func(_ui_up, PRESSED); func(_ui_up, PRESSED);
func(_ui_upP, JUST_PRESSED); func(_ui_up, JUST_PRESSED);
func(_ui_upR, JUST_RELEASED); func(_ui_up, JUST_RELEASED);
case UI_LEFT: case UI_LEFT:
func(_ui_left, PRESSED); func(_ui_left, PRESSED);
func(_ui_leftP, JUST_PRESSED); func(_ui_left, JUST_PRESSED);
func(_ui_leftR, JUST_RELEASED); func(_ui_left, JUST_RELEASED);
case UI_RIGHT: case UI_RIGHT:
func(_ui_right, PRESSED); func(_ui_right, PRESSED);
func(_ui_rightP, JUST_PRESSED); func(_ui_right, JUST_PRESSED);
func(_ui_rightR, JUST_RELEASED); func(_ui_right, JUST_RELEASED);
case UI_DOWN: case UI_DOWN:
func(_ui_down, PRESSED); func(_ui_down, PRESSED);
func(_ui_downP, JUST_PRESSED); func(_ui_down, JUST_PRESSED);
func(_ui_downR, JUST_RELEASED); func(_ui_down, JUST_RELEASED);
case NOTE_UP: case NOTE_UP:
func(_note_up, PRESSED); func(_note_up, PRESSED);
func(_note_upP, JUST_PRESSED); func(_note_up, JUST_PRESSED);
func(_note_upR, JUST_RELEASED); func(_note_up, JUST_RELEASED);
case NOTE_LEFT: case NOTE_LEFT:
func(_note_left, PRESSED); func(_note_left, PRESSED);
func(_note_leftP, JUST_PRESSED); func(_note_left, JUST_PRESSED);
func(_note_leftR, JUST_RELEASED); func(_note_left, JUST_RELEASED);
case NOTE_RIGHT: case NOTE_RIGHT:
func(_note_right, PRESSED); func(_note_right, PRESSED);
func(_note_rightP, JUST_PRESSED); func(_note_right, JUST_PRESSED);
func(_note_rightR, JUST_RELEASED); func(_note_right, JUST_RELEASED);
case NOTE_DOWN: case NOTE_DOWN:
func(_note_down, PRESSED); func(_note_down, PRESSED);
func(_note_downP, JUST_PRESSED); func(_note_down, JUST_PRESSED);
func(_note_downR, JUST_RELEASED); func(_note_down, JUST_RELEASED);
case ACCEPT: case ACCEPT:
func(_accept, JUST_PRESSED); func(_accept, JUST_PRESSED);
case BACK: case BACK:
@ -1042,6 +1057,173 @@ typedef Swipes =
?curTouchPos:FlxPoint ?curTouchPos:FlxPoint
}; };
/**
* An FlxActionDigital with additional functionality, including:
* - Combining `pressed` and `released` inputs into one action.
* - Filtering by input method (`KEYBOARD`, `MOUSE`, `GAMEPAD`, etc).
*/
class FunkinAction extends FlxActionDigital {
public var namePressed(default, null):Null<String>;
public var nameReleased(default, null):Null<String>;
var cache:Map<String, {timestamp:Int, value:Bool}> = [];
public function new(?name:String = "", ?namePressed:String, ?nameReleased:String)
{
super(name);
this.namePressed = namePressed;
this.nameReleased = nameReleased;
}
/**
* Input checks default to whether the input was just pressed, on any input device.
*/
public override function check():Bool {
return checkFiltered(JUST_PRESSED);
}
/**
* Check whether the input is currently being held.
*/
public function checkPressed():Bool {
return checkFiltered(PRESSED);
}
/**
* Check whether the input is currently being held, and was not held last frame.
*/
public function checkJustPressed():Bool {
return checkFiltered(JUST_PRESSED);
}
/**
* Check whether the input is not currently being held.
*/
public function checkReleased():Bool {
return checkFiltered(RELEASED);
}
/**
* Check whether the input is not currently being held, and was held last frame.
*/
public function checkJustReleased():Bool {
return checkFiltered(JUST_RELEASED);
}
/**
* Check whether the input is currently being held by a gamepad device.
*/
public function checkPressedGamepad():Bool {
return checkFiltered(PRESSED, GAMEPAD);
}
/**
* Check whether the input is currently being held by a gamepad device, and was not held last frame.
*/
public function checkJustPressedGamepad():Bool {
return checkFiltered(JUST_PRESSED, GAMEPAD);
}
/**
* Check whether the input is not currently being held by a gamepad device.
*/
public function checkReleasedGamepad():Bool {
return checkFiltered(RELEASED, GAMEPAD);
}
/**
* Check whether the input is not currently being held by a gamepad device, and was held last frame.
*/
public function checkJustReleasedGamepad():Bool {
return checkFiltered(JUST_RELEASED, GAMEPAD);
}
public function checkMultiFiltered(?filterTriggers:Array<FlxInputState>, ?filterDevices:Array<FlxInputDevice>):Bool {
if (filterTriggers == null) {
filterTriggers = [PRESSED, JUST_PRESSED];
}
if (filterDevices == null) {
filterDevices = [];
}
// Perform checkFiltered for each combination.
for (i in filterTriggers) {
if (filterDevices.length == 0) {
if (checkFiltered(i)) {
return true;
}
} else {
for (j in filterDevices) {
if (checkFiltered(i, j)) {
return true;
}
}
}
}
return false;
}
/**
* Performs the functionality of `FlxActionDigital.check()`, but with optional filters.
* @param action The action to check for.
* @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`).
* @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`).
*/
public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool {
// The normal
// Make sure we only update the inputs once per frame.
var key = '${filterTrigger}:${filterDevice}';
var cacheEntry = cache.get(key);
if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks) {
return cacheEntry.value;
}
// Use a for loop instead so we can remove inputs while iterating.
// We don't return early because we need to call check() on ALL inputs.
var result = false;
var len = inputs != null ? inputs.length : 0;
for (i in 0...len)
{
var j = len - i - 1;
var input = inputs[j];
// Filter out dead inputs.
if (input.destroyed)
{
inputs.splice(j, 1);
continue;
}
// Update the input.
input.update();
// Check whether the input is the right trigger.
if (filterTrigger != null && input.trigger != filterTrigger) {
continue;
}
// Check whether the input is the right device.
if (filterDevice != null && input.device != filterDevice) {
continue;
}
// Check whether the input has triggered.
if (input.check(this))
{
result = true;
}
}
// We need to cache this result.
cache.set(key, {timestamp: FlxG.game.ticks, value: result});
return result;
}
}
class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital
{ {
var touchMap:Map<Int, Swipes> = new Map(); var touchMap:Map<Int, Swipes> = new Map();
@ -1229,8 +1411,7 @@ enum Control
DEBUG_STAGE; DEBUG_STAGE;
} }
enum enum abstract Action(String) to String from String
abstract Action(String) to String from String
{ {
// NOTE // NOTE
var NOTE_UP = "note_up"; var NOTE_UP = "note_up";

View file

@ -0,0 +1,111 @@
package funkin.input;
import flixel.input.keyboard.FlxKey;
import flixel.FlxBasic;
import funkin.input.Controls;
import funkin.input.Controls.Action;
/**
* Handles repeating behavior when holding down a control action.
*
* When the `action` is pressed, `activated` will be true for the first frame,
* then wait `delay` seconds before becoming true for one frame every `interval` seconds.
*
* Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly.
*/
class TurboActionHandler extends FlxBasic
{
/**
* Default delay before repeating.
*/
static inline final DEFAULT_DELAY:Float = 0.4;
/**
* Default interval between repeats.
*/
static inline final DEFAULT_INTERVAL:Float = 0.1;
/**
* Whether the action for this handler is pressed.
*/
public var pressed(get, never):Bool;
/**
* Whether the action for this handler is pressed,
* and the handler is ready to repeat.
*/
public var activated(default, null):Bool = false;
/**
* The Funkin Controls handler.
*/
var controls(get, never):Controls;
function get_controls():Controls
{
return PlayerSettings.player1.controls;
}
var action:Action;
var delay:Float;
var interval:Float;
var gamepadOnly:Bool;
var pressedTime:Float = 0;
function new(action:Action, delay:Float = DEFAULT_DELAY, interval:Float = DEFAULT_INTERVAL, gamepadOnly:Bool = false)
{
super();
this.action = action;
this.delay = delay;
this.interval = interval;
this.gamepadOnly = gamepadOnly;
}
function get_pressed():Bool
{
return controls.check(action, PRESSED, gamepadOnly);
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (pressed)
{
if (pressedTime == 0)
{
activated = true;
}
else if (pressedTime >= (delay + interval))
{
activated = true;
pressedTime -= interval;
}
else
{
activated = false;
}
pressedTime += elapsed;
}
else
{
pressedTime = 0;
activated = false;
}
}
/**
* Builds a TurboActionHandler that monitors from a single key.
* @param inputKey The key to monitor.
* @param delay How long to wait before repeating.
* @param repeatDelay How long to wait between repeats.
* @return A TurboActionHandler
*/
public static overload inline extern function build(action:Action, ?delay:Float = DEFAULT_DELAY, ?interval:Float = DEFAULT_INTERVAL,
?gamepadOnly:Bool = false):TurboActionHandler
{
return new TurboActionHandler(action, delay, interval);
}
}

View file

@ -0,0 +1,127 @@
package funkin.input;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.gamepad.FlxGamepad;
import flixel.FlxBasic;
/**
* Handles repeating behavior when holding down a gamepad button or button combination.
*
* When the `inputs` are pressed, `activated` will be true for the first frame,
* then wait `delay` seconds before becoming true for one frame every `interval` seconds.
*
* Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly.
*/
class TurboButtonHandler extends FlxBasic
{
/**
* Default delay before repeating.
*/
static inline final DEFAULT_DELAY:Float = 0.4;
/**
* Default interval between repeats.
*/
static inline final DEFAULT_INTERVAL:Float = 0.1;
/**
* Whether all of the keys for this handler are pressed.
*/
public var allPressed(get, never):Bool;
/**
* Whether all of the keys for this handler are activated,
* and the handler is ready to repeat.
*/
public var activated(default, null):Bool = false;
var inputs:Array<FlxGamepadInputID>;
var delay:Float;
var interval:Float;
var targetGamepad:FlxGamepad;
var allPressedTime:Float = 0;
function new(inputs:Array<FlxGamepadInputID>, delay:Float = DEFAULT_DELAY, interval:Float = DEFAULT_INTERVAL, ?targetGamepad:FlxGamepad)
{
super();
this.inputs = inputs;
this.delay = delay;
this.interval = interval;
this.targetGamepad = targetGamepad ?? FlxG.gamepads.firstActive;
}
function get_allPressed():Bool
{
if (targetGamepad == null) return false;
if (inputs == null || inputs.length == 0) return false;
if (inputs.length == 1) return targetGamepad.anyPressed(inputs);
// Check if ANY keys are unpressed
for (input in inputs)
{
if (!targetGamepad.anyPressed([input])) return false;
}
return true;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
// Try to find a gamepad if we don't have one
if (targetGamepad == null)
{
targetGamepad = FlxG.gamepads.firstActive;
}
if (allPressed)
{
if (allPressedTime == 0)
{
activated = true;
}
else if (allPressedTime >= (delay + interval))
{
activated = true;
allPressedTime -= interval;
}
else
{
activated = false;
}
allPressedTime += elapsed;
}
else
{
allPressedTime = 0;
activated = false;
}
}
/**
* Builds a TurboButtonHandler that monitors from a single input.
* @param input The input to monitor.
* @param delay How long to wait before repeating.
* @param repeatDelay How long to wait between repeats.
* @return A TurboKeyHandler
*/
public static overload inline extern function build(input:FlxGamepadInputID, ?delay:Float = DEFAULT_DELAY,
?interval:Float = DEFAULT_INTERVAL):TurboButtonHandler
{
return new TurboButtonHandler([input], delay, interval);
}
/**
* Builds a TurboKeyHandler that monitors a key combination.
* @param inputs The combination of inputs to monitor.
* @param delay How long to wait before repeating.
* @param repeatDelay How long to wait between repeats.
* @return A TurboKeyHandler
*/
public static overload inline extern function build(inputs:Array<FlxGamepadInputID>, ?delay:Float = DEFAULT_DELAY,
?interval:Float = DEFAULT_INTERVAL):TurboButtonHandler
{
return new TurboButtonHandler(inputs, delay, interval);
}
}

View file

@ -240,8 +240,8 @@ class PolymodHandler
{ {
return { return {
assetLibraryPaths: [ assetLibraryPaths: [
'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'tutorial' => 'tutorial', 'week1' => 'week1', 'week2' => 'week2', 'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'videos' => 'videos', 'tutorial' => 'tutorial', 'week1' => 'week1',
'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1', 'week2' => 'week2', 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1',
], ],
coreAssetRedirect: CORE_FOLDER, coreAssetRedirect: CORE_FOLDER,
} }

View file

@ -8,7 +8,12 @@ import funkin.modding.IScriptedClass;
*/ */
class ScriptEventDispatcher class ScriptEventDispatcher
{ {
public static function callEvent(target:IScriptedClass, event:ScriptEvent):Void /**
* Invoke the given event hook on the given scripted class.
* @param target The target class to call script hooks on.
* @param event The event, which determines the script hook to call and provides parameters for it.
*/
public static function callEvent(target:Null<IScriptedClass>, event:ScriptEvent):Void
{ {
if (target == null || event == null) return; if (target == null || event == null) return;

View file

@ -3,18 +3,18 @@ package funkin.play;
import flixel.FlxG; import flixel.FlxG;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite; import flixel.FlxSprite;
import funkin.audio.FunkinSound; import flixel.input.touch.FlxTouch;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher; import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter; import funkin.play.character.BaseCharacter;
import funkin.play.PlayState;
import funkin.util.MathUtil;
import funkin.ui.freeplay.FreeplayState; import funkin.ui.freeplay.FreeplayState;
import funkin.ui.MusicBeatSubState; import funkin.ui.MusicBeatSubState;
import funkin.ui.story.StoryMenuState; import funkin.ui.story.StoryMenuState;
import funkin.util.MathUtil;
import openfl.utils.Assets; import openfl.utils.Assets;
/** /**
@ -23,13 +23,14 @@ import openfl.utils.Assets;
* *
* The newest implementation uses a substate, which prevents having to reload the song and stage each reset. * The newest implementation uses a substate, which prevents having to reload the song and stage each reset.
*/ */
@:nullSafety
class GameOverSubState extends MusicBeatSubState class GameOverSubState extends MusicBeatSubState
{ {
/** /**
* The currently active GameOverSubState. * The currently active GameOverSubState.
* There should be only one GameOverSubState in existance at a time, we can use a singleton. * There should be only one GameOverSubState in existance at a time, we can use a singleton.
*/ */
public static var instance:GameOverSubState = null; public static var instance:Null<GameOverSubState> = null;
/** /**
* Which alternate animation on the character to use. * Which alternate animation on the character to use.
@ -37,7 +38,7 @@ class GameOverSubState extends MusicBeatSubState
* For example, playing a different animation when BF dies in Week 4 * For example, playing a different animation when BF dies in Week 4
* or Pico dies in Weekend 1. * or Pico dies in Weekend 1.
*/ */
public static var animationSuffix:String = ""; public static var animationSuffix:String = '';
/** /**
* Which alternate game over music to use. * Which alternate game over music to use.
@ -45,17 +46,19 @@ class GameOverSubState extends MusicBeatSubState
* For example, the bf-pixel script sets this to `-pixel` * For example, the bf-pixel script sets this to `-pixel`
* and the pico-playable script sets this to `Pico`. * and the pico-playable script sets this to `Pico`.
*/ */
public static var musicSuffix:String = ""; public static var musicSuffix:String = '';
/** /**
* Which alternate "blue ball" sound effect to use. * Which alternate "blue ball" sound effect to use.
*/ */
public static var blueBallSuffix:String = ""; public static var blueBallSuffix:String = '';
static var blueballed:Bool = false;
/** /**
* The boyfriend character. * The boyfriend character.
*/ */
var boyfriend:BaseCharacter; var boyfriend:Null<BaseCharacter> = null;
/** /**
* The invisible object in the scene which the camera focuses on. * The invisible object in the scene which the camera focuses on.
@ -82,7 +85,8 @@ class GameOverSubState extends MusicBeatSubState
var transparent:Bool; var transparent:Bool;
final CAMERA_ZOOM_DURATION:Float = 0.5; static final CAMERA_ZOOM_DURATION:Float = 0.5;
var targetCameraZoom:Float = 1.0; var targetCameraZoom:Float = 1.0;
public function new(params:GameOverParams) public function new(params:GameOverParams)
@ -91,6 +95,8 @@ class GameOverSubState extends MusicBeatSubState
this.isChartingMode = params?.isChartingMode ?? false; this.isChartingMode = params?.isChartingMode ?? false;
transparent = params.transparent; transparent = params.transparent;
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
} }
/** /**
@ -101,14 +107,15 @@ class GameOverSubState extends MusicBeatSubState
animationSuffix = ''; animationSuffix = '';
musicSuffix = ''; musicSuffix = '';
blueBallSuffix = ''; blueBallSuffix = '';
blueballed = false;
} }
override public function create():Void public override function create():Void
{ {
if (instance != null) if (instance != null)
{ {
// TODO: Do something in this case? IDK. // TODO: Do something in this case? IDK.
trace('WARNING: GameOverSubState instance already exists. This should not happen.'); FlxG.log.warn('WARNING: GameOverSubState instance already exists. This should not happen.');
} }
instance = this; instance = this;
@ -121,7 +128,7 @@ class GameOverSubState extends MusicBeatSubState
var playState = PlayState.instance; var playState = PlayState.instance;
// Add a black background to the screen. // Add a black background to the screen.
var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
// We make this transparent so that we can see the stage underneath during debugging, // We make this transparent so that we can see the stage underneath during debugging,
// but it's normally opaque. // but it's normally opaque.
bg.alpha = transparent ? 0.25 : 1.0; bg.alpha = transparent ? 0.25 : 1.0;
@ -138,21 +145,10 @@ class GameOverSubState extends MusicBeatSubState
boyfriend.isDead = true; boyfriend.isDead = true;
add(boyfriend); add(boyfriend);
boyfriend.resetCharacter(); boyfriend.resetCharacter();
// Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint);
FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom();
} }
setCameraTarget();
// //
// Set up the audio // Set up the audio
// //
@ -161,6 +157,27 @@ class GameOverSubState extends MusicBeatSubState
Conductor.instance.update(0); Conductor.instance.update(0);
} }
@:nullSafety(Off)
function setCameraTarget():Void
{
// Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint);
FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE / 2);
targetCameraZoom = (PlayState?.instance?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom();
}
/**
* Forcibly reset the camera zoom level to that of the current stage.
* This prevents camera zoom events from adversely affecting the game over state.
*/
public function resetCameraZoom():Void public function resetCameraZoom():Void
{ {
// Apply camera zoom level from stage data. // Apply camera zoom level from stage data.
@ -175,7 +192,7 @@ class GameOverSubState extends MusicBeatSubState
{ {
hasStartedAnimation = true; hasStartedAnimation = true;
if (PlayState.instance.isMinimalMode) if (boyfriend == null || PlayState.instance.isMinimalMode)
{ {
// Play the "blue balled" sound. May play a variant if one has been assigned. // Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX(); playBlueBalledSFX();
@ -205,10 +222,10 @@ class GameOverSubState extends MusicBeatSubState
// MOBILE ONLY: Restart the level when tapping Boyfriend. // MOBILE ONLY: Restart the level when tapping Boyfriend.
if (FlxG.onMobile) if (FlxG.onMobile)
{ {
var touch = FlxG.touches.getFirst(); var touch:FlxTouch = FlxG.touches.getFirst();
if (touch != null) if (touch != null)
{ {
if (touch.overlaps(boyfriend)) if (boyfriend == null || touch.overlaps(boyfriend))
{ {
confirmDeath(); confirmDeath();
} }
@ -228,7 +245,7 @@ class GameOverSubState extends MusicBeatSubState
blueballed = false; blueballed = false;
PlayState.instance.deathCounter = 0; PlayState.instance.deathCounter = 0;
// PlayState.seenCutscene = false; // old thing... // PlayState.seenCutscene = false; // old thing...
gameOverMusic.stop(); if (gameOverMusic != null) gameOverMusic.stop();
if (isChartingMode) if (isChartingMode)
{ {
@ -238,11 +255,11 @@ class GameOverSubState extends MusicBeatSubState
} }
else if (PlayStatePlaylist.isStoryMode) else if (PlayStatePlaylist.isStoryMode)
{ {
FlxG.switchState(() -> new StoryMenuState()); openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
} }
else else
{ {
FlxG.switchState(() -> new FreeplayState()); openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(sticker)));
} }
} }
@ -252,7 +269,7 @@ class GameOverSubState extends MusicBeatSubState
// This enables the stepHit and beatHit events. // This enables the stepHit and beatHit events.
Conductor.instance.update(gameOverMusic.time); Conductor.instance.update(gameOverMusic.time);
} }
else else if (boyfriend != null)
{ {
if (PlayState.instance.isMinimalMode) if (PlayState.instance.isMinimalMode)
{ {
@ -299,7 +316,7 @@ class GameOverSubState extends MusicBeatSubState
isEnding = true; isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior. startDeathMusic(1.0, true); // isEnding changes this function's behavior.
if (PlayState.instance.isMinimalMode) {} if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else else
{ {
boyfriend.playAnimation('deathConfirm' + animationSuffix, true); boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
@ -313,7 +330,7 @@ class GameOverSubState extends MusicBeatSubState
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true); FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.instance.needsReset = true; PlayState.instance.needsReset = true;
if (PlayState.instance.isMinimalMode) {} if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else else
{ {
// Readd Boyfriend to the stage. // Readd Boyfriend to the stage.
@ -332,7 +349,7 @@ class GameOverSubState extends MusicBeatSubState
} }
} }
public override function dispatchEvent(event:ScriptEvent) public override function dispatchEvent(event:ScriptEvent):Void
{ {
super.dispatchEvent(event); super.dispatchEvent(event);
@ -345,11 +362,11 @@ class GameOverSubState extends MusicBeatSubState
*/ */
function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null<String> function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null<String>
{ {
var basePath = 'gameplay/gameover/gameOver'; var basePath:String = 'gameplay/gameover/gameOver';
if (starting) basePath += 'Start'; if (ending) basePath += 'End';
else if (ending) basePath += 'End'; else if (starting) basePath += 'Start';
var musicPath = Paths.music(basePath + suffix); var musicPath:String = Paths.music(basePath + suffix);
while (!Assets.exists(musicPath) && suffix.length > 0) while (!Assets.exists(musicPath) && suffix.length > 0)
{ {
suffix = suffix.split('-').slice(0, -1).join('-'); suffix = suffix.split('-').slice(0, -1).join('-');
@ -362,23 +379,26 @@ class GameOverSubState extends MusicBeatSubState
/** /**
* Starts the death music at the appropriate volume. * Starts the death music at the appropriate volume.
* @param startingVolume * @param startingVolume The initial volume for the music.
* @param force Whether or not to force the music to restart.
*/ */
public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void
{ {
var musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding); var musicPath:Null<String> = resolveMusicPath(musicSuffix, isStarting, isEnding);
var onComplete = null; var onComplete:() -> Void = () -> {};
if (isStarting) if (isStarting)
{ {
if (musicPath == null) if (musicPath == null)
{ {
// Looked for starting music and didn't find it. Use middle music instead.
isStarting = false; isStarting = false;
musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding); musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
} }
else else
{ {
onComplete = function() { onComplete = function() {
isStarting = false; isStarting = true;
// We need to force to ensure that the non-starting music plays. // We need to force to ensure that the non-starting music plays.
startDeathMusic(1.0, true); startDeathMusic(1.0, true);
}; };
@ -387,13 +407,16 @@ class GameOverSubState extends MusicBeatSubState
if (musicPath == null) if (musicPath == null)
{ {
trace('Could not find game over music!'); FlxG.log.warn('[GAMEOVER] Could not find game over music at path ($musicPath)!');
return; return;
} }
else if (gameOverMusic == null || !gameOverMusic.playing || force) else if (gameOverMusic == null || !gameOverMusic.playing || force)
{ {
if (gameOverMusic != null) gameOverMusic.stop(); if (gameOverMusic != null) gameOverMusic.stop();
gameOverMusic = FunkinSound.load(musicPath); gameOverMusic = FunkinSound.load(musicPath);
if (gameOverMusic == null) return;
gameOverMusic.volume = startingVolume; gameOverMusic.volume = startingVolume;
gameOverMusic.looped = !(isEnding || isStarting); gameOverMusic.looped = !(isEnding || isStarting);
gameOverMusic.onComplete = onComplete; gameOverMusic.onComplete = onComplete;
@ -406,13 +429,11 @@ class GameOverSubState extends MusicBeatSubState
} }
} }
static var blueballed:Bool = false;
/** /**
* Play the sound effect that occurs when * Play the sound effect that occurs when
* boyfriend's testicles get utterly annihilated. * boyfriend's testicles get utterly annihilated.
*/ */
public static function playBlueBalledSFX() public static function playBlueBalledSFX():Void
{ {
blueballed = true; blueballed = true;
if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix))) if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)))
@ -431,7 +452,7 @@ class GameOverSubState extends MusicBeatSubState
* Week 7-specific hardcoded behavior, to play a custom death quote. * Week 7-specific hardcoded behavior, to play a custom death quote.
* TODO: Make this a module somehow. * TODO: Make this a module somehow.
*/ */
function playJeffQuote() function playJeffQuote():Void
{ {
var randomCensor:Array<Int> = []; var randomCensor:Array<Int> = [];
@ -446,20 +467,27 @@ class GameOverSubState extends MusicBeatSubState
}); });
} }
public override function destroy() public override function destroy():Void
{ {
super.destroy(); super.destroy();
if (gameOverMusic != null) gameOverMusic.stop(); if (gameOverMusic != null)
gameOverMusic = null; {
gameOverMusic.stop();
gameOverMusic = null;
}
blueballed = false;
instance = null; instance = null;
} }
public override function toString():String public override function toString():String
{ {
return "GameOverSubState"; return 'GameOverSubState';
} }
} }
/**
* Parameters used to instantiate a GameOverSubState.
*/
typedef GameOverParams = typedef GameOverParams =
{ {
var isChartingMode:Bool; var isChartingMode:Bool;

View file

@ -12,6 +12,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.audio.FunkinSound; import funkin.audio.FunkinSound;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.ui.freeplay.FreeplayState;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.play.cutscene.VideoCutscene; import funkin.play.cutscene.VideoCutscene;
import funkin.play.PlayState; import funkin.play.PlayState;
@ -72,8 +73,8 @@ class PauseSubState extends MusicBeatSubState
*/ */
static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array<PauseMenuEntry> = [ static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array<PauseMenuEntry> = [
{text: 'Resume', callback: resume}, {text: 'Resume', callback: resume},
{text: 'Restart Cutscene', callback: restartVideoCutscene},
{text: 'Skip Cutscene', callback: skipVideoCutscene}, {text: 'Skip Cutscene', callback: skipVideoCutscene},
{text: 'Restart Cutscene', callback: restartVideoCutscene},
{text: 'Exit to Menu', callback: quitToMenu}, {text: 'Exit to Menu', callback: quitToMenu},
]; ];
@ -440,7 +441,7 @@ class PauseSubState extends MusicBeatSubState
var entries:Array<PauseMenuEntry> = []; var entries:Array<PauseMenuEntry> = [];
if (PlayState.instance.currentChart != null) if (PlayState.instance.currentChart != null)
{ {
var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation); var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation, true);
trace('DIFFICULTIES: ${difficultiesInVariation}'); trace('DIFFICULTIES: ${difficultiesInVariation}');
for (difficulty in difficultiesInVariation) for (difficulty in difficultiesInVariation)
{ {
@ -567,6 +568,8 @@ class PauseSubState extends MusicBeatSubState
PlayStatePlaylist.campaignDifficulty = difficulty; PlayStatePlaylist.campaignDifficulty = difficulty;
PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty; PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
FreeplayState.rememberedDifficulty = difficulty;
PlayState.instance.needsReset = true; PlayState.instance.needsReset = true;
state.close(); state.close();
@ -658,7 +661,7 @@ class PauseSubState extends MusicBeatSubState
} }
else else
{ {
state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker))); state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker)));
} }
} }

View file

@ -498,7 +498,7 @@ class PlayState extends MusicBeatSubState
/** /**
* The combo popups. Includes the real-time combo counter and the rating. * The combo popups. Includes the real-time combo counter and the rating.
*/ */
var comboPopUps:PopUpStuff; public var comboPopUps:PopUpStuff;
/** /**
* PROPERTIES * PROPERTIES
@ -736,6 +736,10 @@ class PlayState extends MusicBeatSubState
#end #end
initialized = true; initialized = true;
// This step ensures z-indexes are applied properly,
// and it's important to call it last so all elements get affected.
refresh();
} }
public override function draw():Void public override function draw():Void
@ -830,9 +834,12 @@ class PlayState extends MusicBeatSubState
inputSpitter = []; inputSpitter = [];
// Reset music properly. // Reset music properly.
FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; if (FlxG.sound.music != null)
FlxG.sound.music.pitch = playbackRate; {
FlxG.sound.music.pause(); FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
FlxG.sound.music.pitch = playbackRate;
FlxG.sound.music.pause();
}
if (!overrideMusic) if (!overrideMusic)
{ {
@ -848,7 +855,7 @@ class PlayState extends MusicBeatSubState
vocals.pause(); vocals.pause();
vocals.time = 0; vocals.time = 0;
FlxG.sound.music.volume = 1; if (FlxG.sound.music != null) FlxG.sound.music.volume = 1;
vocals.volume = 1; vocals.volume = 1;
vocals.playerVolume = 1; vocals.playerVolume = 1;
vocals.opponentVolume = 1; vocals.opponentVolume = 1;
@ -866,7 +873,7 @@ class PlayState extends MusicBeatSubState
// Reset camera zooming // Reset camera zooming
cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY; cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY;
hudCameraZoomIntensity = 0.015 * 2.0; hudCameraZoomIntensity = (cameraBopIntensity - 1.0) * 2.0;
cameraZoomRate = Constants.DEFAULT_ZOOM_RATE; cameraZoomRate = Constants.DEFAULT_ZOOM_RATE;
health = Constants.HEALTH_STARTING; health = Constants.HEALTH_STARTING;
@ -962,7 +969,7 @@ class PlayState extends MusicBeatSubState
if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN; if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN;
// Apply camera zoom + multipliers. // Apply camera zoom + multipliers.
if (subState == null) if (subState == null && cameraZoomRate > 0.0 && !isInCutscene)
{ {
cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier. var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier.
@ -976,6 +983,7 @@ class PlayState extends MusicBeatSubState
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation()); FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
} }
FlxG.watch.addQuick('health', health); FlxG.watch.addQuick('health', health);
FlxG.watch.addQuick('cameraBopIntensity', cameraBopIntensity);
// TODO: Add a song event for Handle GF dance speed. // TODO: Add a song event for Handle GF dance speed.
@ -1462,7 +1470,7 @@ class PlayState extends MusicBeatSubState
*/ */
function initCameras():Void function initCameras():Void
{ {
camGame = new FunkinCamera(); camGame = new FunkinCamera('playStateCamGame');
camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage. camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage.
camHUD = new FlxCamera(); camHUD = new FlxCamera();
camHUD.bgColor.alpha = 0; // Show the game scene behind the camera. camHUD.bgColor.alpha = 0; // Show the game scene behind the camera.
@ -1543,10 +1551,11 @@ class PlayState extends MusicBeatSubState
function loadStage(id:String):Void function loadStage(id:String):Void
{ {
currentStage = StageRegistry.instance.fetchEntry(id); currentStage = StageRegistry.instance.fetchEntry(id);
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
if (currentStage != null) if (currentStage != null)
{ {
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
// Actually create and position the sprites. // Actually create and position the sprites.
var event:ScriptEvent = new ScriptEvent(CREATE, false); var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentStage, event); ScriptEventDispatcher.callEvent(currentStage, event);
@ -1728,8 +1737,6 @@ class PlayState extends MusicBeatSubState
playerStrumline.fadeInArrows(); playerStrumline.fadeInArrows();
opponentStrumline.fadeInArrows(); opponentStrumline.fadeInArrows();
} }
this.refresh();
} }
/** /**
@ -1912,8 +1919,6 @@ class PlayState extends MusicBeatSubState
*/ */
function startSong():Void function startSong():Void
{ {
dispatchEvent(new ScriptEvent(SONG_START));
startingSong = false; startingSong = false;
if (!overrideMusic && !isGamePaused && currentChart != null) if (!overrideMusic && !isGamePaused && currentChart != null)
@ -1935,7 +1940,7 @@ class PlayState extends MusicBeatSubState
// Prevent the volume from being wrong. // Prevent the volume from being wrong.
FlxG.sound.music.volume = 1.0; FlxG.sound.music.volume = 1.0;
FlxG.sound.music.fadeTween?.cancel(); if (FlxG.sound.music.fadeTween != null) FlxG.sound.music.fadeTween.cancel();
trace('Playing vocals...'); trace('Playing vocals...');
add(vocals); add(vocals);
@ -1954,6 +1959,8 @@ class PlayState extends MusicBeatSubState
// FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; // FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
handleSkippedNotes(); handleSkippedNotes();
} }
dispatchEvent(new ScriptEvent(SONG_START));
} }
/** /**
@ -2230,8 +2237,8 @@ class PlayState extends MusicBeatSubState
holdNote.handledMiss = true; holdNote.handledMiss = true;
// Mute vocals and play miss animation, but don't penalize. // Mute vocals and play miss animation, but don't penalize.
vocals.playerVolume = 0; // vocals.playerVolume = 0;
if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true); // if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
} }
} }
} }
@ -2385,13 +2392,6 @@ class PlayState extends MusicBeatSubState
// Display the combo meter and add the calculation to the score. // Display the combo meter and add the calculation to the score.
popUpScore(note, event.score, event.judgement, event.healthChange); popUpScore(note, event.score, event.judgement, event.healthChange);
if (note.isHoldNote && note.holdNoteSprite != null)
{
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
}
vocals.playerVolume = 1;
} }
/** /**
@ -2449,7 +2449,8 @@ class PlayState extends MusicBeatSubState
if (Highscore.tallies.combo != 0) if (Highscore.tallies.combo != 0)
{ {
// Break the combo. // Break the combo.
Highscore.tallies.combo = comboPopUps.displayCombo(0); if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
Highscore.tallies.combo = 0;
} }
if (playSound) if (playSound)
@ -2576,32 +2577,38 @@ class PlayState extends MusicBeatSubState
*/ */
function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void
{ {
vocals.playerVolume = 1;
if (daRating == 'miss') if (daRating == 'miss')
{ {
// If daRating is 'miss', that means we made a mistake and should not continue. // If daRating is 'miss', that means we made a mistake and should not continue.
trace('[WARNING] popUpScore judged a note as a miss!'); FlxG.log.warn('popUpScore judged a note as a miss!');
// TODO: Remove this. // TODO: Remove this.
comboPopUps.displayRating('miss'); // comboPopUps.displayRating('miss');
return; return;
} }
vocals.playerVolume = 1;
var isComboBreak = false; var isComboBreak = false;
switch (daRating) switch (daRating)
{ {
case 'sick': case 'sick':
Highscore.tallies.sick += 1; Highscore.tallies.sick += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK; isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
case 'good': case 'good':
Highscore.tallies.good += 1; Highscore.tallies.good += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK; isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
case 'bad': case 'bad':
Highscore.tallies.bad += 1; Highscore.tallies.bad += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
case 'shit': case 'shit':
Highscore.tallies.shit += 1; Highscore.tallies.shit += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
default:
FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!');
} }
health += healthChange; health += healthChange;
@ -2609,18 +2616,18 @@ class PlayState extends MusicBeatSubState
if (isComboBreak) if (isComboBreak)
{ {
// Break the combo, but don't increment tallies.misses. // Break the combo, but don't increment tallies.misses.
Highscore.tallies.combo = comboPopUps.displayCombo(0); if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
Highscore.tallies.combo = 0;
} }
else else
{ {
Highscore.tallies.combo++; Highscore.tallies.combo++;
Highscore.tallies.totalNotesHit++;
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
} }
playerStrumline.hitNote(daNote, !isComboBreak); playerStrumline.hitNote(daNote, !isComboBreak);
if (daRating == "sick") if (daRating == 'sick')
{ {
playerStrumline.playNoteSplash(daNote.noteData.getDirection()); playerStrumline.playNoteSplash(daNote.noteData.getDirection());
} }
@ -2666,6 +2673,13 @@ class PlayState extends MusicBeatSubState
} }
comboPopUps.displayRating(daRating); comboPopUps.displayRating(daRating);
if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo); if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo);
if (daNote.isHoldNote && daNote.holdNoteSprite != null)
{
playerStrumline.playNoteHoldCover(daNote.holdNoteSprite);
}
vocals.playerVolume = 1;
} }
/** /**
@ -2732,7 +2746,7 @@ class PlayState extends MusicBeatSubState
*/ */
public function endSong(rightGoddamnNow:Bool = false):Void public function endSong(rightGoddamnNow:Bool = false):Void
{ {
FlxG.sound.music.volume = 0; if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
vocals.volume = 0; vocals.volume = 0;
mayPauseGame = false; mayPauseGame = false;
@ -2750,6 +2764,8 @@ class PlayState extends MusicBeatSubState
deathCounter = 0; deathCounter = 0;
var isNewHighscore = false;
if (currentSong != null && currentSong.validScore) if (currentSong != null && currentSong.validScore)
{ {
// crackhead double thingie, sets whether was new highscore, AND saves the song! // crackhead double thingie, sets whether was new highscore, AND saves the song!
@ -2774,17 +2790,20 @@ class PlayState extends MusicBeatSubState
// adds current song data into the tallies for the level (story levels) // adds current song data into the tallies for the level (story levels)
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel); Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data))
{ {
Save.instance.setSongScore(currentSong.id, currentDifficulty, data); Save.instance.setSongScore(currentSong.id, currentDifficulty, data);
#if newgrounds #if newgrounds
NGio.postScore(score, currentSong.id); NGio.postScore(score, currentSong.id);
#end #end
isNewHighscore = true;
} }
} }
if (PlayStatePlaylist.isStoryMode) if (PlayStatePlaylist.isStoryMode)
{ {
isNewHighscore = false;
PlayStatePlaylist.campaignScore += songScore; PlayStatePlaylist.campaignScore += songScore;
// Pop the next song ID from the list. // Pop the next song ID from the list.
@ -2793,18 +2812,6 @@ class PlayState extends MusicBeatSubState
if (targetSongId == null) if (targetSongId == null)
{ {
FunkinSound.playMusic('freakyMenu',
{
overrideExisting: true,
restartTrack: false
});
// transIn = FlxTransitionableState.defaultTransIn;
// transOut = FlxTransitionableState.defaultTransOut;
// TODO: Rework week unlock logic.
// StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true;
if (currentSong.validScore) if (currentSong.validScore)
{ {
NGio.unlockMedal(60961); NGio.unlockMedal(60961);
@ -2834,6 +2841,7 @@ class PlayState extends MusicBeatSubState
#if newgrounds #if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}'); NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end #end
isNewHighscore = true;
} }
} }
@ -2845,11 +2853,11 @@ class PlayState extends MusicBeatSubState
{ {
if (rightGoddamnNow) if (rightGoddamnNow)
{ {
moveToResultsScreen(); moveToResultsScreen(isNewHighscore);
} }
else else
{ {
zoomIntoResultsScreen(); zoomIntoResultsScreen(isNewHighscore);
} }
} }
} }
@ -2862,7 +2870,7 @@ class PlayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true; FlxTransitionableState.skipNextTransOut = true;
FlxG.sound.music.stop(); if (FlxG.sound.music != null) FlxG.sound.music.stop();
vocals.stop(); vocals.stop();
// TODO: Softcode this cutscene. // TODO: Softcode this cutscene.
@ -2910,11 +2918,11 @@ class PlayState extends MusicBeatSubState
{ {
if (rightGoddamnNow) if (rightGoddamnNow)
{ {
moveToResultsScreen(); moveToResultsScreen(isNewHighscore);
} }
else else
{ {
zoomIntoResultsScreen(); zoomIntoResultsScreen(isNewHighscore);
} }
} }
} }
@ -2988,7 +2996,7 @@ class PlayState extends MusicBeatSubState
/** /**
* Play the camera zoom animation and then move to the results screen once it's done. * Play the camera zoom animation and then move to the results screen once it's done.
*/ */
function zoomIntoResultsScreen():Void function zoomIntoResultsScreen(isNewHighscore:Bool):Void
{ {
trace('WENT TO RESULTS SCREEN!'); trace('WENT TO RESULTS SCREEN!');
@ -3045,7 +3053,7 @@ class PlayState extends MusicBeatSubState
{ {
ease: FlxEase.expoIn, ease: FlxEase.expoIn,
onComplete: function(_) { onComplete: function(_) {
moveToResultsScreen(); moveToResultsScreen(isNewHighscore);
} }
}); });
}); });
@ -3054,7 +3062,7 @@ class PlayState extends MusicBeatSubState
/** /**
* Move to the results screen right goddamn now. * Move to the results screen right goddamn now.
*/ */
function moveToResultsScreen():Void function moveToResultsScreen(isNewHighscore:Bool):Void
{ {
persistentUpdate = false; persistentUpdate = false;
vocals.stop(); vocals.stop();
@ -3066,7 +3074,24 @@ class PlayState extends MusicBeatSubState
{ {
storyMode: PlayStatePlaylist.isStoryMode, storyMode: PlayStatePlaylist.isStoryMode,
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
tallies: talliesToUse, scoreData:
{
score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
tallies:
{
sick: talliesToUse.sick,
good: talliesToUse.good,
bad: talliesToUse.bad,
shit: talliesToUse.shit,
missed: talliesToUse.missed,
combo: talliesToUse.combo,
maxCombo: talliesToUse.maxCombo,
totalNotesHit: talliesToUse.totalNotesHit,
totalNotes: talliesToUse.totalNotes,
},
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
},
isNewHighscore: isNewHighscore
}); });
res.camera = camHUD; res.camera = camHUD;
openSubState(res); openSubState(res);
@ -3208,7 +3233,10 @@ class PlayState extends MusicBeatSubState
// Don't go back in time to before the song started. // Don't go back in time to before the song started.
targetTimeMs = Math.max(0, targetTimeMs); targetTimeMs = Math.max(0, targetTimeMs);
FlxG.sound.music.time = targetTimeMs; if (FlxG.sound.music != null)
{
FlxG.sound.music.time = targetTimeMs;
}
handleSkippedNotes(); handleSkippedNotes();
SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition); SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition);

View file

@ -5,12 +5,13 @@ package funkin.play;
* *
* TODO: Add getters/setters for all these properties to validate them. * TODO: Add getters/setters for all these properties to validate them.
*/ */
@:nullSafety
class PlayStatePlaylist class PlayStatePlaylist
{ {
/** /**
* Whether the game is currently in Story Mode. If false, we are in Free Play Mode. * Whether the game is currently in Story Mode. If false, we are in Free Play Mode.
*/ */
public static var isStoryMode(default, default):Bool = false; public static var isStoryMode:Bool = false;
/** /**
* The loist of upcoming songs to be played. * The loist of upcoming songs to be played.
@ -31,8 +32,9 @@ class PlayStatePlaylist
/** /**
* The internal ID of the current playlist, for example `week4` or `weekend-1`. * The internal ID of the current playlist, for example `week4` or `weekend-1`.
* @default `null`, used when no playlist is loaded
*/ */
public static var campaignId:String = 'unknown'; public static var campaignId:Null<String> = null;
public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY; public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY;
@ -45,7 +47,7 @@ class PlayStatePlaylist
playlistSongIds = []; playlistSongIds = [];
campaignScore = 0; campaignScore = 0;
campaignTitle = 'UNKNOWN'; campaignTitle = 'UNKNOWN';
campaignId = 'unknown'; campaignId = null;
campaignDifficulty = Constants.DEFAULT_DIFFICULTY; campaignDifficulty = Constants.DEFAULT_DIFFICULTY;
} }
} }

View file

@ -0,0 +1,140 @@
package funkin.play;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
{
public var scoreShit(default, set):Int = 0;
function set_scoreShit(val):Int
{
if (group == null || group.members == null) return val;
var loopNum:Int = group.members.length - 1;
var dumbNumb = Std.parseInt(Std.string(val));
var prevNum:ScoreNum;
while (dumbNumb > 0)
{
group.members[loopNum].digit = dumbNumb % 10;
// var funnyNum = group.members[loopNum];
// prevNum = group.members[loopNum + 1];
// if (prevNum != null)
// {
// funnyNum.x = prevNum.x - (funnyNum.width * 0.7);
// }
// funnyNum.y = (funnyNum.baseY - (funnyNum.height / 2)) + 73;
// funnyNum.x = (funnyNum.baseX - (funnyNum.width / 2)) + 450; // this plus value is hand picked lol!
dumbNumb = Math.floor(dumbNumb / 10);
loopNum--;
}
while (loopNum > 0)
{
group.members[loopNum].digit = 10;
loopNum--;
}
return val;
}
public function animateNumbers():Void
{
for (i in group.members)
{
i.playAnim();
}
}
public function new(x:Float, y:Float, digitCount:Int, scoreShit:Int = 100)
{
super(x, y);
for (i in 0...digitCount)
{
add(new ScoreNum(x + (65 * i), y));
}
this.scoreShit = scoreShit;
}
public function updateScore(scoreNew:Int)
{
scoreShit = scoreNew;
}
}
class ScoreNum extends FlxSprite
{
public var digit(default, set):Int = 10;
function set_digit(val):Int
{
if (val >= 0 && animation.curAnim != null && animation.curAnim.name != numToString[val])
{
animation.play(numToString[val], true, false, 0);
updateHitbox();
switch (val)
{
case 1:
// offset.x -= 15;
case 5:
// set offsets
// offset.x += 0;
// offset.y += 10;
case 7:
// offset.y += 6;
case 4:
// offset.y += 5;
case 9:
// offset.y += 5;
default:
centerOffsets(false);
}
}
return digit = val;
}
public function playAnim():Void
{
animation.play(numToString[digit], true, false, 0);
}
public var baseY:Float = 0;
public var baseX:Float = 0;
var numToString:Array<String> = [
"ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "DISABLED"
];
public function new(x:Float, y:Float)
{
super(x, y);
baseY = y;
baseX = x;
frames = Paths.getSparrowAtlas('resultScreen/score-digital-numbers');
for (i in 0...10)
{
var stringNum:String = numToString[i];
animation.addByPrefix(stringNum, '$stringNum DIGITAL', 24, false);
}
animation.addByPrefix('DISABLED', 'DISABLED', 24, false);
this.digit = 10;
animation.play(numToString[digit], true);
updateHitbox();
}
}

View file

@ -1,5 +1,6 @@
package funkin.play; package funkin.play;
import funkin.util.MathUtil;
import funkin.ui.story.StoryMenuState; import funkin.ui.story.StoryMenuState;
import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxSprite; import flixel.FlxSprite;
@ -10,12 +11,15 @@ import flixel.math.FlxPoint;
import funkin.ui.MusicBeatSubState; import funkin.ui.MusicBeatSubState;
import flixel.math.FlxRect; import flixel.math.FlxRect;
import flixel.text.FlxBitmapText; import flixel.text.FlxBitmapText;
import funkin.ui.freeplay.FreeplayScore;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import funkin.ui.freeplay.FreeplayState; import funkin.ui.freeplay.FreeplayState;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import funkin.audio.FunkinSound; import funkin.audio.FunkinSound;
import flixel.util.FlxGradient; import flixel.util.FlxGradient;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.graphics.shaders.LeftMaskShader; import funkin.graphics.shaders.LeftMaskShader;
import funkin.play.components.TallyCounter; import funkin.play.components.TallyCounter;
@ -42,12 +46,15 @@ class ResultState extends MusicBeatSubState
override function create():Void override function create():Void
{ {
if (params.tallies.sick == params.tallies.totalNotesHit /*
&& params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT; if (params.scoreData.sick == params.scoreData.totalNotesHit
else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50) && params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT;
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending! else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50)
else resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
resultsVariation = NORMAL; else
resultsVariation = NORMAL;
*/
resultsVariation = NORMAL;
FunkinSound.playMusic('results$resultsVariation', FunkinSound.playMusic('results$resultsVariation',
{ {
@ -73,34 +80,34 @@ class ResultState extends MusicBeatSubState
bgFlash.visible = false; bgFlash.visible = false;
add(bgFlash); add(bgFlash);
var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared")); // var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared"));
bfGfExcellent.visible = false; // bfGfExcellent.visible = false;
add(bfGfExcellent); // add(bfGfExcellent);
//
// var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared"));
// bfPerfect.visible = false;
// add(bfPerfect);
//
// var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
// bfSHIT.visible = false;
// add(bfSHIT);
//
// bfGfExcellent.anim.onComplete = () -> {
// bfGfExcellent.anim.curFrame = 28;
// bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
//
// bfPerfect.anim.onComplete = () -> {
// bfPerfect.anim.curFrame = 136;
// bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
//
// bfSHIT.anim.onComplete = () -> {
// bfSHIT.anim.curFrame = 150;
// bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared")); var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD');
bfPerfect.visible = false;
add(bfPerfect);
var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
bfSHIT.visible = false;
add(bfSHIT);
bfGfExcellent.anim.onComplete = () -> {
bfGfExcellent.anim.curFrame = 28;
bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
bfPerfect.anim.onComplete = () -> {
bfPerfect.anim.curFrame = 136;
bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
bfSHIT.anim.onComplete = () -> {
bfSHIT.anim.curFrame = 150;
bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
var gf:FlxSprite = FunkinSprite.createSparrow(500, 300, 'resultScreen/resultGirlfriendGOOD');
gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
gf.visible = false; gf.visible = false;
gf.animation.finishCallback = _ -> { gf.animation.finishCallback = _ -> {
@ -130,12 +137,16 @@ class ResultState extends MusicBeatSubState
var diffSpr:String = switch (PlayState.instance.currentDifficulty) var diffSpr:String = switch (PlayState.instance.currentDifficulty)
{ {
case 'EASY': case 'easy':
'difEasy'; 'difEasy';
case 'NORMAL': case 'normal':
'difNormal'; 'difNormal';
case 'HARD': case 'hard':
'difHard'; 'difHard';
case 'erect':
'difErect';
case 'nightmare':
'difNightmare';
case _: case _:
'difNormal'; 'difNormal';
} }
@ -178,7 +189,7 @@ class ResultState extends MusicBeatSubState
scorePopin.visible = false; scorePopin.visible = false;
add(scorePopin); add(scorePopin);
var highscoreNew:FlxSprite = new FlxSprite(280, 580); var highscoreNew:FlxSprite = new FlxSprite(310, 570);
highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew"); highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew");
highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24); highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24);
highscoreNew.visible = false; highscoreNew.visible = false;
@ -195,29 +206,33 @@ class ResultState extends MusicBeatSubState
* NOTE: We display how many notes were HIT, not how many notes there were in total. * NOTE: We display how many notes were HIT, not how many notes there were in total.
* *
*/ */
var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit); var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.scoreData.tallies.totalNotesHit);
ratingGrp.add(totalHit); ratingGrp.add(totalHit);
var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.tallies.maxCombo); var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.scoreData.tallies.maxCombo);
ratingGrp.add(maxCombo); ratingGrp.add(maxCombo);
hStuf += 2; hStuf += 2;
var extraYOffset:Float = 5; var extraYOffset:Float = 5;
var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.tallies.sick, 0xFF89E59E); var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E);
ratingGrp.add(tallySick); ratingGrp.add(tallySick);
var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5); var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.scoreData.tallies.good, 0xFF89C9E5);
ratingGrp.add(tallyGood); ratingGrp.add(tallyGood);
var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xFFE6CF8A); var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.scoreData.tallies.bad, 0xFFE6CF8A);
ratingGrp.add(tallyBad); ratingGrp.add(tallyBad);
var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A); var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.scoreData.tallies.shit, 0xFFE68C8A);
ratingGrp.add(tallyShit); ratingGrp.add(tallyShit);
var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.tallies.missed, 0xFFC68AE6); var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6);
ratingGrp.add(tallyMissed); ratingGrp.add(tallyMissed);
var score:ResultScore = new ResultScore(35, 305, 10, params.scoreData.score);
score.visible = false;
add(score);
for (ind => rating in ratingGrp.members) for (ind => rating in ratingGrp.members)
{ {
rating.visible = false; rating.visible = false;
@ -233,18 +248,29 @@ class ResultState extends MusicBeatSubState
ratingsPopin.animation.finishCallback = anim -> { ratingsPopin.animation.finishCallback = anim -> {
scorePopin.animation.play("score"); scorePopin.animation.play("score");
scorePopin.animation.finishCallback = anim -> {
score.visible = true;
score.animateNumbers();
};
scorePopin.visible = true; scorePopin.visible = true;
highscoreNew.visible = true; if (params.isNewHighscore)
highscoreNew.animation.play("new"); {
FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); highscoreNew.visible = true;
highscoreNew.animation.play("new");
FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
}
else
{
highscoreNew.visible = false;
}
}; };
switch (resultsVariation) switch (resultsVariation)
{ {
case SHIT: // case SHIT:
bfSHIT.visible = true; // bfSHIT.visible = true;
bfSHIT.playAnimation(""); // bfSHIT.playAnimation("");
case NORMAL: case NORMAL:
boyfriend.animation.play('fall'); boyfriend.animation.play('fall');
@ -266,9 +292,9 @@ class ResultState extends MusicBeatSubState
gf.animation.play('clap', true); gf.animation.play('clap', true);
gf.visible = true; gf.visible = true;
}); });
case PERFECT: // case PERFECT:
bfPerfect.visible = true; // bfPerfect.visible = true;
bfPerfect.playAnimation(""); // bfPerfect.playAnimation("");
// bfGfExcellent.visible = true; // bfGfExcellent.visible = true;
// bfGfExcellent.playAnimation(""); // bfGfExcellent.playAnimation("");
@ -276,8 +302,6 @@ class ResultState extends MusicBeatSubState
} }
}); });
if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
super.create(); super.create();
} }
@ -365,7 +389,7 @@ class ResultState extends MusicBeatSubState
} }
else else
{ {
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new FreeplayState(null, sticker))); openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker)));
} }
} }
@ -393,8 +417,13 @@ typedef ResultsStateParams =
*/ */
var title:String; var title:String;
/**
* Whether the displayed score is a new highscore
*/
var isNewHighscore:Bool;
/** /**
* The score, accuracy, and judgements. * The score, accuracy, and judgements.
*/ */
var tallies:Highscore.Tallies; var scoreData:SaveScoreData;
}; };

View file

@ -192,6 +192,7 @@ class AnimateAtlasCharacter extends BaseCharacter
if (!this.mainSprite.hasAnimation(prefix)) if (!this.mainSprite.hasAnimation(prefix))
{ {
FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}'); FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}');
trace('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}');
continue; continue;
} }
animations.set(anim.name, anim); animations.set(anim.name, anim);

View file

@ -10,6 +10,8 @@ import funkin.util.TimerUtil;
class PopUpStuff extends FlxTypedGroup<FlxSprite> class PopUpStuff extends FlxTypedGroup<FlxSprite>
{ {
public var offsets:Array<Int> = [0, 0];
override public function new() override public function new()
{ {
super(); super();
@ -29,9 +31,9 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
rating.scrollFactor.set(0.2, 0.2); rating.scrollFactor.set(0.2, 0.2);
rating.zIndex = 1000; rating.zIndex = 1000;
rating.x = FlxG.width * 0.50; rating.x = (FlxG.width * 0.474) + offsets[0];
// rating.x -= FlxG.camera.scroll.x * 0.2; // rating.x -= FlxG.camera.scroll.x * 0.2;
rating.y = FlxG.camera.height * 0.4 - 60; rating.y = (FlxG.camera.height * 0.45 - 60) + offsets[1];
rating.acceleration.y = 550; rating.acceleration.y = 550;
rating.velocity.y -= FlxG.random.int(140, 175); rating.velocity.y -= FlxG.random.int(140, 175);
rating.velocity.x -= FlxG.random.int(0, 10); rating.velocity.x -= FlxG.random.int(0, 10);
@ -40,16 +42,19 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
if (PlayState.instance.currentStageId.startsWith('school')) if (PlayState.instance.currentStageId.startsWith('school'))
{ {
rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7)); rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.65));
rating.antialiasing = false; rating.antialiasing = false;
} }
else else
{ {
rating.setGraphicSize(Std.int(rating.width * 0.7)); rating.setGraphicSize(Std.int(rating.width * 0.65));
rating.antialiasing = true; rating.antialiasing = true;
} }
rating.updateHitbox(); rating.updateHitbox();
rating.x -= rating.width / 2;
rating.y -= rating.height / 2;
FlxTween.tween(rating, {alpha: 0}, 0.2, FlxTween.tween(rating, {alpha: 0}, 0.2,
{ {
onComplete: function(tween:FlxTween) { onComplete: function(tween:FlxTween) {
@ -77,15 +82,15 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
pixelShitPart2 = '-pixel'; pixelShitPart2 = '-pixel';
} }
var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2); var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2);
comboSpr.y = FlxG.camera.height * 0.4 + 80; comboSpr.y = (FlxG.camera.height * 0.44) + offsets[1];
comboSpr.x = FlxG.width * 0.50; comboSpr.x = (FlxG.width * 0.507) + offsets[0];
// comboSpr.x -= FlxG.camera.scroll.x * 0.2; // comboSpr.x -= FlxG.camera.scroll.x * 0.2;
comboSpr.acceleration.y = 600; comboSpr.acceleration.y = 600;
comboSpr.velocity.y -= 150; comboSpr.velocity.y -= 150;
comboSpr.velocity.x += FlxG.random.int(1, 10); comboSpr.velocity.x += FlxG.random.int(1, 10);
add(comboSpr); // add(comboSpr);
if (PlayState.instance.currentStageId.startsWith('school')) if (PlayState.instance.currentStageId.startsWith('school'))
{ {
@ -133,14 +138,14 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
} }
else else
{ {
numScore.setGraphicSize(Std.int(numScore.width * 0.5)); numScore.setGraphicSize(Std.int(numScore.width * 0.45));
numScore.antialiasing = true; numScore.antialiasing = true;
} }
numScore.updateHitbox(); numScore.updateHitbox();
numScore.x = comboSpr.x - (43 * daLoop); //- 90; numScore.x = comboSpr.x - (36 * daLoop) - 65; //- 90;
numScore.acceleration.y = FlxG.random.int(200, 300); numScore.acceleration.y = FlxG.random.int(250, 300);
numScore.velocity.y -= FlxG.random.int(140, 160); numScore.velocity.y -= FlxG.random.int(130, 150);
numScore.velocity.x = FlxG.random.float(-5, 5); numScore.velocity.x = FlxG.random.float(-5, 5);
add(numScore); add(numScore);

View file

@ -6,6 +6,8 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.text.FlxText.FlxTextAlign;
import funkin.util.MathUtil;
/** /**
* Numerical counters used next to each judgement in the Results screen. * Numerical counters used next to each judgement in the Results screen.
@ -13,18 +15,23 @@ import flixel.tweens.FlxTween;
class TallyCounter extends FlxTypedSpriteGroup<FlxSprite> class TallyCounter extends FlxTypedSpriteGroup<FlxSprite>
{ {
public var curNumber:Float = 0; public var curNumber:Float = 0;
public var neededNumber:Int = 0; public var neededNumber:Int = 0;
public var flavour:Int = 0xFFFFFFFF; public var flavour:Int = 0xFFFFFFFF;
public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF) public var align:FlxTextAlign = FlxTextAlign.LEFT;
public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF, align:FlxTextAlign = FlxTextAlign.LEFT)
{ {
super(x, y); super(x, y);
this.align = align;
this.flavour = flavour; this.flavour = flavour;
this.neededNumber = neededNumber; this.neededNumber = neededNumber;
drawNumbers();
if (curNumber == neededNumber) drawNumbers();
} }
var tmr:Float = 0; var tmr:Float = 0;
@ -41,6 +48,8 @@ class TallyCounter extends FlxTypedSpriteGroup<FlxSprite>
var seperatedScore:Array<Int> = []; var seperatedScore:Array<Int> = [];
var tempCombo:Int = Math.round(curNumber); var tempCombo:Int = Math.round(curNumber);
var fullNumberDigits:Int = Std.int(Math.max(1, Math.ceil(MathUtil.logBase(10, neededNumber))));
while (tempCombo != 0) while (tempCombo != 0)
{ {
seperatedScore.push(tempCombo % 10); seperatedScore.push(tempCombo % 10);
@ -55,7 +64,13 @@ class TallyCounter extends FlxTypedSpriteGroup<FlxSprite>
{ {
if (ind >= members.length) if (ind >= members.length)
{ {
var numb:TallyNumber = new TallyNumber(ind * 43, 0, num); var xPos = ind * (43 * this.scale.x);
if (this.align == FlxTextAlign.RIGHT)
{
xPos -= (fullNumberDigits * (43 * this.scale.x));
}
var numb:TallyNumber = new TallyNumber(xPos, 0, num);
numb.scale.set(this.scale.x, this.scale.y);
add(numb); add(numb);
numb.color = flavour; numb.color = flavour;
} }

View file

@ -67,8 +67,13 @@ class VideoCutscene
if (!openfl.Assets.exists(filePath)) if (!openfl.Assets.exists(filePath))
{ {
// Display a popup. // Display a popup.
lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video'); // lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
return; // return;
// TODO: After moving videos to their own library,
// this function ALWAYS FAILS on web, but the video still plays.
// I think that's due to a weird quirk with how OpenFL libraries work.
trace('Video file does not exist: ${filePath}');
} }
var rawFilePath = Paths.stripLibrary(filePath); var rawFilePath = Paths.stripLibrary(filePath);

View file

@ -23,6 +23,7 @@ import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.modding.IScriptedClass.IEventHandler; import funkin.modding.IScriptedClass.IEventHandler;
import funkin.play.cutscene.dialogue.DialogueBox; import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import funkin.util.EaseUtil;
/** /**
* A high-level handler for dialogue. * A high-level handler for dialogue.
@ -179,7 +180,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
if (backdropData.fadeTime > 0.0) if (backdropData.fadeTime > 0.0)
{ {
backdrop.alpha = 0.0; backdrop.alpha = 0.0;
FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: FlxEase.linear}); FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: EaseUtil.stepped(10)});
} }
else else
{ {
@ -403,6 +404,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
type: ONESHOT, // holy shit like the game no way type: ONESHOT, // holy shit like the game no way
startDelay: 0, startDelay: 0,
onComplete: (_) -> endOutro(), onComplete: (_) -> endOutro(),
ease: EaseUtil.stepped(8)
}); });
FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime); FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);

View file

@ -50,8 +50,8 @@ class SetCameraBopSongEvent extends SongEvent
var intensity:Null<Float> = data.getFloat('intensity'); var intensity:Null<Float> = data.getFloat('intensity');
if (intensity == null) intensity = 1.0; if (intensity == null) intensity = 1.0;
PlayState.instance.cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY * intensity; PlayState.instance.cameraBopIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity + 1.0;
PlayState.instance.hudCameraZoomIntensity = 1.015 * intensity * 2.0; PlayState.instance.hudCameraZoomIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity * 2.0;
PlayState.instance.cameraZoomRate = rate; PlayState.instance.cameraZoomRate = rate;
trace('Set camera zoom rate to ${PlayState.instance.cameraZoomRate}'); trace('Set camera zoom rate to ${PlayState.instance.cameraZoomRate}');
} }

View file

@ -77,13 +77,6 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite>
public override function update(elapsed):Void public override function update(elapsed):Void
{ {
super.update(elapsed); super.update(elapsed);
if ((!holdNote.alive || holdNote.missedNote) && !glow.animation.curAnim.name.startsWith('holdCoverEnd'))
{
// If alive is false, the hold note was held to completion.
// If missedNote is true, the hold note was "dropped".
playEnd();
}
} }
public function playStart():Void public function playStart():Void

View file

@ -295,6 +295,11 @@ class Strumline extends FlxSpriteGroup
{ {
if (noteData.length == 0) return; if (noteData.length == 0) return;
// Ensure note data gets reset if the song happens to loop.
// NOTE: I had to remove this line because it was causing notes visible during the countdown to be placed multiple times.
// I don't remember what bug I was trying to fix by adding this.
// if (conductorInUse.currentStep == 0) nextNoteIndex = 0;
var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0; var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0;
var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS; var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS;
var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS; var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS;
@ -365,8 +370,6 @@ class Strumline extends FlxSpriteGroup
// Hold note is offscreen, kill it. // Hold note is offscreen, kill it.
holdNote.visible = false; holdNote.visible = false;
holdNote.kill(); // Do not destroy! Recycling is faster. holdNote.kill(); // Do not destroy! Recycling is faster.
// The cover will see this and clean itself up.
} }
else if (holdNote.hitNote && holdNote.sustainLength <= 0) else if (holdNote.hitNote && holdNote.sustainLength <= 0)
{ {
@ -380,10 +383,16 @@ class Strumline extends FlxSpriteGroup
playStatic(holdNote.noteDirection); playStatic(holdNote.noteDirection);
} }
if (holdNote.cover != null) if (holdNote.cover != null && isPlayer)
{ {
holdNote.cover.playEnd(); holdNote.cover.playEnd();
} }
else if (holdNote.cover != null)
{
// *lightning* *zap* *crackle*
holdNote.cover.visible = false;
holdNote.cover.kill();
}
holdNote.visible = false; holdNote.visible = false;
holdNote.kill(); holdNote.kill();
@ -405,6 +414,13 @@ class Strumline extends FlxSpriteGroup
{ {
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2; holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2;
} }
// Clean up the cover.
if (holdNote.cover != null)
{
holdNote.cover.visible = false;
holdNote.cover.kill();
}
} }
else if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote) else if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote)
{ {
@ -822,7 +838,7 @@ class Strumline extends FlxSpriteGroup
{ {
// The note sprite pool is full and all note splashes are active. // The note sprite pool is full and all note splashes are active.
// We have to create a new note. // We have to create a new note.
result = new SustainTrail(0, 100, noteStyle); result = new SustainTrail(0, 0, noteStyle);
this.holdNotes.add(result); this.holdNotes.add(result);
} }

View file

@ -404,11 +404,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
* *
* @param variationId Optionally filter by a single variation. * @param variationId Optionally filter by a single variation.
* @param variationIds Optionally filter by multiple variations. * @param variationIds Optionally filter by multiple variations.
* @param showLocked Include charts which are not unlocked
* @param showHidden Include charts which are not accessible to the player. * @param showHidden Include charts which are not accessible to the player.
* *
* @return The list of difficulties. * @return The list of difficulties.
*/ */
public function listDifficulties(?variationId:String, ?variationIds:Array<String>, showHidden:Bool = false):Array<String> public function listDifficulties(?variationId:String, ?variationIds:Array<String>, showLocked:Bool = false, showHidden:Bool = false):Array<String>
{ {
if (variationIds == null) variationIds = []; if (variationIds == null) variationIds = [];
if (variationId != null) variationIds.push(variationId); if (variationId != null) variationIds.push(variationId);

View file

@ -223,7 +223,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
if (propSprite.frames == null || propSprite.frames.numFrames == 0) if (propSprite.frames == null || propSprite.frames.numFrames == 0)
{ {
trace(' ERROR: Could not build texture for prop.'); @:privateAccess
trace(' ERROR: Could not build texture for prop. Check the asset path (${Paths.currentLevel ?? 'default'}, ${dataProp.assetPath}).');
continue; continue;
} }

View file

@ -9,12 +9,13 @@ import funkin.save.migrator.SaveDataMigrator;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle; import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import thx.semver.Version; import thx.semver.Version;
import funkin.util.SerializerUtil;
@:nullSafety @:nullSafety
class Save class Save
{ {
// Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null. // Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2"; public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.3";
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
// We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
@ -391,6 +392,22 @@ class Save
*/ */
public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null<SaveScoreData> public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null<SaveScoreData>
{ {
if (data.scores?.levels == null)
{
if (data.scores == null)
{
data.scores =
{
songs: [],
levels: []
};
}
else
{
data.scores.levels = [];
}
}
var level = data.scores.levels.get(levelId); var level = data.scores.levels.get(levelId);
if (level == null) if (level == null)
{ {
@ -641,6 +658,9 @@ class Save
{ {
trace("[SAVE] Loading save from slot " + slot + "..."); trace("[SAVE] Loading save from slot " + slot + "...");
// Prevent crashes if the save data is corrupted.
SerializerUtil.initSerializer();
FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH); FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH);
if (FlxG.save.isEmpty()) if (FlxG.save.isEmpty())
@ -650,9 +670,9 @@ class Save
if (legacySaveData != null) if (legacySaveData != null)
{ {
trace('[SAVE] Found legacy save data, converting...'); trace('[SAVE] Found legacy save data, converting...');
var gameSave = SaveDataMigrator.migrate(legacySaveData); var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData);
@:privateAccess @:privateAccess
FlxG.save.mergeData(gameSave.data); FlxG.save.mergeData(gameSave.data, true);
} }
else else
{ {
@ -664,7 +684,7 @@ class Save
trace('[SAVE] Loaded save data.'); trace('[SAVE] Loaded save data.');
@:privateAccess @:privateAccess
var gameSave = SaveDataMigrator.migrate(FlxG.save.data); var gameSave = SaveDataMigrator.migrate(FlxG.save.data);
FlxG.save.mergeData(gameSave.data); FlxG.save.mergeData(gameSave.data, true);
} }
} }
@ -673,7 +693,7 @@ class Save
trace("[SAVE] Checking for legacy save data..."); trace("[SAVE] Checking for legacy save data...");
var legacySave:FlxSave = new FlxSave(); var legacySave:FlxSave = new FlxSave();
legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY); legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY);
if (legacySave?.data == null) if (legacySave.isEmpty())
{ {
trace("[SAVE] No legacy save data found."); trace("[SAVE] No legacy save data found.");
return null; return null;

View file

@ -3,6 +3,7 @@ package funkin.save.migrator;
import funkin.save.Save; import funkin.save.Save;
import funkin.save.migrator.RawSaveData_v1_0_0; import funkin.save.migrator.RawSaveData_v1_0_0;
import thx.semver.Version; import thx.semver.Version;
import funkin.util.StructureUtil;
import funkin.util.VersionUtil; import funkin.util.VersionUtil;
@:nullSafety @:nullSafety
@ -26,7 +27,7 @@ class SaveDataMigrator
if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE)) if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
{ {
// Simply import the structured data. // Simply import the structured data.
var save:Save = new Save(inputData); var save:Save = new Save(StructureUtil.deepMerge(Save.getDefault(), inputData));
return save; return save;
} }
else else

View file

@ -0,0 +1,34 @@
package funkin.ui.credits;
/**
* The members of the Funkin' Crew, organized by their roles.
*/
typedef CreditsData =
{
var entries:Array<CreditsDataRole>;
}
/**
* The members of a specific role on the Funkin' Crew.
*/
typedef CreditsDataRole =
{
@:optional
var header:String;
@:optional
@:default([])
var body:Array<CreditsDataMember>;
@:optional
@:default(false)
var appendBackers:Bool;
}
/**
* A member of a specific person on the Funkin' Crew.
*/
typedef CreditsDataMember =
{
var line:String;
}

View file

@ -0,0 +1,142 @@
package funkin.ui.credits;
import funkin.data.JsonFile;
using StringTools;
@:nullSafety
class CreditsDataHandler
{
public static final BACKER_PUBLIC_URL:String = 'https://funkin.me/backers';
#if HARDCODED_CREDITS
static final CREDITS_DATA_PATH:String = "assets/exclude/data/credits.json";
#else
static final CREDITS_DATA_PATH:String = "assets/data/credits.json";
#end
public static function debugPrint(data:Null<CreditsData>):Void
{
if (data == null)
{
trace('CreditsData(NULL)');
return;
}
if (data.entries == null || data.entries.length == 0)
{
trace('CreditsData(EMPTY)');
return;
}
var entryCount = data.entries.length;
var lineCount = 0;
for (entry in data.entries)
{
lineCount += entry?.body?.length ?? 0;
}
trace('CreditsData($entryCount entries containing $lineCount lines)');
}
/**
* If for some reason the full credits won't load,
* use this hardcoded data for the original Funkin' Crew.
*
* @return `CreditsData`
*/
public static inline function getFallback():CreditsData
{
return {
entries: [
{
header: 'Founders',
body: [
{line: 'ninjamuffin99'},
{line: 'PhantomArcade'},
{line: 'KawaiSprite'},
{line: 'evilsk8r'},
]
}
]
};
}
public static function fetchBackerEntries():Array<String>
{
// TODO: Implement a web request.
// We can't just grab the current Kickstarter data and include it in builds,
// because we don't want to deadname people who haven't logged into the portal yet.
// It can be async and paginated for performance!
return [];
}
#if HARDCODED_CREDITS
/**
* The data for the credits.
* Hardcoded into game via a macro at compile time.
*/
public static final CREDITS_DATA:Null<CreditsData> = #if macro null #else CreditsDataMacro.loadCreditsData() #end;
#else
/**
* The data for the credits.
* Loaded dynamically from the game folder when needed.
* Nullable because data may fail to parse.
*/
public static var CREDITS_DATA(get, default):Null<CreditsData> = null;
static function get_CREDITS_DATA():Null<CreditsData>
{
if (CREDITS_DATA == null) CREDITS_DATA = parseCreditsData(fetchCreditsData());
return CREDITS_DATA;
}
static function fetchCreditsData():funkin.data.JsonFile
{
#if !macro
var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim();
return {
fileName: CREDITS_DATA_PATH,
contents: rawJson
};
#else
return {
fileName: CREDITS_DATA_PATH,
contents: null
};
#end
}
static function parseCreditsData(file:JsonFile):Null<CreditsData>
{
#if !macro
if (file.contents == null) return null;
var parser = new json2object.JsonParser<CreditsData>();
parser.ignoreUnknownVariables = false;
trace('[CREDITS] Parsing credits data from ${CREDITS_DATA_PATH}');
parser.fromJson(file.contents, file.fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, file.fileName);
return null;
}
return parser.value;
#else
return null;
#end
}
static function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
{
trace('[CREDITS] Failed to parse credits data: ${id}');
for (error in errors)
funkin.data.DataError.printError(error);
}
#end
}

View file

@ -0,0 +1,67 @@
package funkin.ui.credits;
#if macro
import haxe.macro.Context;
#end
@:access(funkin.ui.credits.CreditsDataHandler)
class CreditsDataMacro
{
public static macro function loadCreditsData():haxe.macro.Expr.ExprOf<CreditsData>
{
#if !display
trace('Hardcoding credits data...');
var json = CreditsDataMacro.fetchJSON();
if (json == null)
{
Context.info('[WARN] Could not fetch JSON data for credits.', Context.currentPos());
return macro $v{CreditsDataHandler.getFallback()};
}
var creditsData = CreditsDataMacro.parseJSON(json);
if (creditsData == null)
{
Context.info('[WARN] Could not parse JSON data for credits.', Context.currentPos());
return macro $v{CreditsDataHandler.getFallback()};
}
CreditsDataHandler.debugPrint(creditsData);
return macro $v{creditsData};
// return macro $v{null};
#else
// `#if display` is used for code completion. In this case we return
// a minimal value to keep code completion fast.
return macro $v{CreditsDataHandler.getFallback()};
#end
}
#if macro
static function fetchJSON():Null<String>
{
return sys.io.File.getContent(CreditsDataHandler.CREDITS_DATA_PATH);
}
/**
* Parse the JSON data for the credits.
*
* @param json The string data to parse.
* @return The parsed data.
*/
static function parseJSON(json:String):Null<CreditsData>
{
try
{
// TODO: Use something with better validation but that still works at macro time.
return haxe.Json.parse(json);
}
catch (e)
{
trace('[ERROR] Failed to parse JSON data for credits.');
trace(e);
return null;
}
}
#end
}

View file

@ -0,0 +1,213 @@
package funkin.ui.credits;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.audio.FunkinSound;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
/**
* The state used to display the credits scroll.
* AAA studios often fail to credit properly, and we're better than them!
*/
class CreditsState extends MusicBeatState
{
/**
* The height the credits should start at.
* Make this an instanced variable so it gets set by the constructor.
*/
final STARTING_HEIGHT = FlxG.height;
/**
* The padding on each side of the screen.
*/
static final SCREEN_PAD = 24;
/**
* The width of the screen the credits should maximally fill up.
* Make this an instanced variable so it gets set by the constructor.
*/
final FULL_WIDTH = FlxG.width - (SCREEN_PAD * 2);
/**
* The font to use to display the text.
* To use a font from the `assets` folder, use `Paths.font(...)`.
* Choose something that will render Unicode properly.
*/
static final CREDITS_FONT = 'Arial';
/**
* The size of the font.
*/
static final CREDITS_FONT_SIZE = 48;
static final CREDITS_HEADER_FONT_SIZE = 72;
/**
* The color of the text itself.
*/
static final CREDITS_FONT_COLOR = FlxColor.WHITE;
/**
* The color of the text's outline.
*/
static final CREDITS_FONT_STROKE_COLOR = FlxColor.BLACK;
/**
* The speed the credits scroll at, in pixels per second.
*/
static final CREDITS_SCROLL_BASE_SPEED = 25.0;
/**
* The speed the credits scroll at while the button is held, in pixels per second.
*/
static final CREDITS_SCROLL_FAST_SPEED = CREDITS_SCROLL_BASE_SPEED * 4.0;
/**
* The actual sprites and text used to display the credits.
*/
var creditsGroup:FlxSpriteGroup;
var scrollPaused:Bool = false;
public function new()
{
super();
}
public override function create():Void
{
super.create();
// Background
var bg = new FlxSprite(Paths.image('menuDesat'));
bg.scrollFactor.x = 0;
bg.scrollFactor.y = 0;
bg.setGraphicSize(Std.int(FlxG.width));
bg.updateHitbox();
bg.x = 0;
bg.y = 0;
bg.visible = true;
bg.color = 0xFFB57EDC; // Lavender
add(bg);
// TODO: Once we need to display Kickstarter backers,
// make this use a recycled pool so we don't kill peformance.
creditsGroup = new FlxSpriteGroup();
creditsGroup.x = SCREEN_PAD;
creditsGroup.y = STARTING_HEIGHT;
buildCreditsGroup();
add(creditsGroup);
// Music
FunkinSound.playMusic('freeplayRandom',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: true,
loop: true
});
FlxG.sound.music.fadeIn(2, 0, 0.8);
}
function buildCreditsGroup():Void
{
var y = 0;
for (entry in CreditsDataHandler.CREDITS_DATA.entries)
{
if (entry.header != null)
{
creditsGroup.add(buildCreditsLine(entry.header, y, true, CreditsSide.Center));
y += CREDITS_HEADER_FONT_SIZE;
}
for (line in entry?.body ?? [])
{
creditsGroup.add(buildCreditsLine(line.line, y, false, CreditsSide.Center));
y += CREDITS_FONT_SIZE;
}
if (entry.appendBackers)
{
var backers = CreditsDataHandler.fetchBackerEntries();
for (backer in backers)
{
creditsGroup.add(buildCreditsLine(backer, y, false, CreditsSide.Center));
y += CREDITS_FONT_SIZE;
}
}
// Padding between each role.
y += CREDITS_FONT_SIZE * 2;
}
}
function buildCreditsLine(text:String, yPos:Float, header:Bool, side:CreditsSide = CreditsSide.Center):FlxText
{
// CreditsSide.Center: Full screen width
// CreditsSide.Left: Left half of screen
// CreditsSide.Right: Right half of screen
var xPos = (side == CreditsSide.Right) ? (FULL_WIDTH / 2) : 0;
var width = (side == CreditsSide.Center) ? FULL_WIDTH : (FULL_WIDTH / 2);
var size = header ? CREDITS_HEADER_FONT_SIZE : CREDITS_FONT_SIZE;
var creditsLine:FlxText = new FlxText(xPos, yPos, width, text);
creditsLine.setFormat(CREDITS_FONT, size, CREDITS_FONT_COLOR, FlxTextAlign.CENTER, FlxTextBorderStyle.OUTLINE, CREDITS_FONT_STROKE_COLOR, true);
return creditsLine;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (!scrollPaused)
{
// TODO: Replace with whatever the special note button is.
if (controls.ACCEPT || FlxG.keys.pressed.SPACE)
{
// Move the whole group.
creditsGroup.y -= CREDITS_SCROLL_FAST_SPEED * elapsed;
}
else
{
// Move the whole group.
creditsGroup.y -= CREDITS_SCROLL_BASE_SPEED * elapsed;
}
}
if (controls.BACK || hasEnded())
{
exit();
}
else if (controls.PAUSE)
{
scrollPaused = !scrollPaused;
}
}
function hasEnded():Bool
{
return creditsGroup.y < -creditsGroup.height;
}
function exit():Void
{
FlxG.switchState(new funkin.ui.mainmenu.MainMenuState());
}
public override function destroy():Void
{
super.destroy();
}
}
enum CreditsSide
{
Left;
Center;
Right;
}

View file

@ -6,15 +6,15 @@ import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxCamera; import flixel.FlxCamera;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.FlxSubState; import flixel.FlxSubState;
import flixel.graphics.FlxGraphic;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.graphics.FunkinCamera;
import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey; import flixel.input.keyboard.FlxKey;
import flixel.input.mouse.FlxMouseEvent; import flixel.input.mouse.FlxMouseEvent;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import flixel.math.FlxRect; import flixel.math.FlxRect;
import flixel.sound.FlxSound;
import flixel.system.debug.log.LogStyle; import flixel.system.debug.log.LogStyle;
import flixel.system.FlxAssets.FlxSoundAsset; import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.text.FlxText; import flixel.text.FlxText;
@ -26,28 +26,23 @@ import flixel.util.FlxSort;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.audio.FunkinSound; import funkin.audio.FunkinSound;
import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VoicesGroup; import funkin.audio.VoicesGroup;
import funkin.audio.waveform.WaveformSprite; import funkin.audio.waveform.WaveformSprite;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongMetadata; import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongOffsets; import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongDataUtils; import funkin.data.song.SongDataUtils;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongRegistry;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.data.stage.StageData; import funkin.data.stage.StageData;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.input.Cursor; import funkin.input.Cursor;
import funkin.input.TurboActionHandler;
import funkin.input.TurboButtonHandler;
import funkin.input.TurboKeyHandler; import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.BaseCharacter.CharacterType;
@ -56,13 +51,12 @@ import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.components.HealthIcon; import funkin.play.components.HealthIcon;
import funkin.play.notes.NoteSprite; import funkin.play.notes.NoteSprite;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song; import funkin.play.song.Song;
import funkin.save.Save; import funkin.save.Save;
import funkin.ui.debug.charting.commands.AddEventsCommand; import funkin.ui.debug.charting.commands.AddEventsCommand;
import funkin.ui.debug.charting.commands.AddNotesCommand; import funkin.ui.debug.charting.commands.AddNotesCommand;
import funkin.ui.debug.charting.commands.ChartEditorCommand; import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.commands.CopyItemsCommand; import funkin.ui.debug.charting.commands.CopyItemsCommand;
import funkin.ui.debug.charting.commands.CutItemsCommand; import funkin.ui.debug.charting.commands.CutItemsCommand;
import funkin.ui.debug.charting.commands.DeselectAllItemsCommand; import funkin.ui.debug.charting.commands.DeselectAllItemsCommand;
@ -83,6 +77,7 @@ import funkin.ui.debug.charting.commands.SetItemSelectionCommand;
import funkin.ui.debug.charting.components.ChartEditorEventSprite; import funkin.ui.debug.charting.components.ChartEditorEventSprite;
import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
import funkin.ui.debug.charting.components.ChartEditorNotePreview; import funkin.ui.debug.charting.components.ChartEditorNotePreview;
import funkin.ui.debug.charting.components.ChartEditorNoteSprite; import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
import funkin.ui.debug.charting.components.ChartEditorPlaybarHead; import funkin.ui.debug.charting.components.ChartEditorPlaybarHead;
@ -95,6 +90,7 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox;
import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.HaxeUIState; import funkin.ui.haxeui.HaxeUIState;
import funkin.ui.mainmenu.MainMenuState; import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.transition.LoadingState;
import funkin.util.Constants; import funkin.util.Constants;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import funkin.util.logging.CrashHandler; import funkin.util.logging.CrashHandler;
@ -119,7 +115,6 @@ import haxe.ui.containers.Grid;
import haxe.ui.containers.HBox; import haxe.ui.containers.HBox;
import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuBar; import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuCheckBox; import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.containers.menus.MenuItem; import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.ScrollView; import haxe.ui.containers.ScrollView;
@ -130,7 +125,6 @@ import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent; import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent; import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent; import haxe.ui.events.UIEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.focus.FocusManager; import haxe.ui.focus.FocusManager;
import haxe.ui.Toolkit; import haxe.ui.Toolkit;
import openfl.display.BitmapData; import openfl.display.BitmapData;
@ -411,8 +405,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0);
// Offset the selection box start position, if we are dragging. // Offset the selection box start position, if we are dragging.
if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff;
// Update the note preview viewport box.
// Update the note preview.
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
refreshNotePreviewPlayheadPosition();
// Update the measure tick display. // Update the measure tick display.
if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0; if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0;
return this.scrollPositionInPixels; return this.scrollPositionInPixels;
@ -473,6 +470,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Move the playhead sprite to the correct position. // Move the playhead sprite to the correct position.
gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS; gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS;
updatePlayheadGhostHoldNotes();
refreshNotePreviewPlayheadPosition();
return this.playheadPositionInPixels; return this.playheadPositionInPixels;
} }
@ -779,6 +779,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return currentPlaceNoteData = value; return currentPlaceNoteData = value;
} }
/**
* The SongNoteData which is currently being placed, for each column.
* `null` if the user isn't currently placing a note.
* As the user moves down, we will update this note's sustain length, and finalize the note when they release.
*/
var currentLiveInputPlaceNoteData:Array<SongNoteData> = [];
// Note Movement // Note Movement
/** /**
@ -809,6 +816,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var dragLengthCurrent:Float = 0; var dragLengthCurrent:Float = 0;
/**
* The current length of the hold note we are placing with the playhead, in steps.
* Play a sound when this value changes.
*/
var playheadDragLengthCurrent:Array<Float> = [];
/** /**
* Flip-flop to alternate between two stretching sounds. * Flip-flop to alternate between two stretching sounds.
*/ */
@ -1081,6 +1094,66 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var pageDownKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEDOWN); var pageDownKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEDOWN);
/**
* Variable used to track how long the user has been holding up on the dpad.
*/
var dpadUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_UP);
/**
* Variable used to track how long the user has been holding down on the dpad.
*/
var dpadDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_DOWN);
/**
* Variable used to track how long the user has been holding left on the dpad.
*/
var dpadLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_LEFT);
/**
* Variable used to track how long the user has been holding right on the dpad.
*/
var dpadRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_RIGHT);
/**
* Variable used to track how long the user has been holding up on the left stick.
*/
var leftStickUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_UP);
/**
* Variable used to track how long the user has been holding down on the left stick.
*/
var leftStickDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_DOWN);
/**
* Variable used to track how long the user has been holding left on the left stick.
*/
var leftStickLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_LEFT);
/**
* Variable used to track how long the user has been holding right on the left stick.
*/
var leftStickRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_RIGHT);
/**
* Variable used to track how long the user has been holding up on the right stick.
*/
var rightStickUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_UP);
/**
* Variable used to track how long the user has been holding down on the right stick.
*/
var rightStickDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_DOWN);
/**
* Variable used to track how long the user has been holding left on the right stick.
*/
var rightStickLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_LEFT);
/**
* Variable used to track how long the user has been holding right on the right stick.
*/
var rightStickRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_RIGHT);
/** /**
* AUDIO AND SOUND DATA * AUDIO AND SOUND DATA
*/ */
@ -1959,10 +2032,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var gridGhostNote:Null<ChartEditorNoteSprite> = null; var gridGhostNote:Null<ChartEditorNoteSprite> = null;
/** /**
* A sprite used to indicate the note that will be placed on click. * A sprite used to indicate the hold note that will be placed on click.
*/ */
var gridGhostHoldNote:Null<ChartEditorHoldNoteSprite> = null; var gridGhostHoldNote:Null<ChartEditorHoldNoteSprite> = null;
/**
* A sprite used to indicate the hold note that will be placed on button release.
*/
var gridPlayheadGhostHoldNotes:Array<ChartEditorHoldNoteSprite> = [];
/** /**
* A sprite used to indicate the event that will be placed on click. * A sprite used to indicate the event that will be placed on click.
*/ */
@ -1980,6 +2058,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var notePreviewViewport:Null<FlxSliceSprite> = null; var notePreviewViewport:Null<FlxSliceSprite> = null;
/**
* The thin sprite used for representing the playhead on the note preview.
* We move this up and down to represent the current position.
*/
var notePreviewPlayhead:Null<FlxSprite> = null;
/** /**
* The rectangular sprite used for rendering the selection box. * The rectangular sprite used for rendering the selection box.
* Uses a 9-slice to stretch the selection box to the correct size without warping. * Uses a 9-slice to stretch the selection box to the correct size without warping.
@ -2101,7 +2185,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
loadPreferences(); loadPreferences();
uiCamera = new FunkinCamera(); uiCamera = new FunkinCamera('chartEditorUI');
FlxG.cameras.reset(uiCamera); FlxG.cameras.reset(uiCamera);
buildDefaultSongData(); buildDefaultSongData();
@ -2359,7 +2443,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
gridGhostHoldNote = new ChartEditorHoldNoteSprite(this); gridGhostHoldNote = new ChartEditorHoldNoteSprite(this);
gridGhostHoldNote.alpha = 0.6; gridGhostHoldNote.alpha = 0.6;
gridGhostHoldNote.noteData = new SongNoteData(0, 0, 0, ""); gridGhostHoldNote.noteData = null;
gridGhostHoldNote.visible = false; gridGhostHoldNote.visible = false;
add(gridGhostHoldNote); add(gridGhostHoldNote);
gridGhostHoldNote.zIndex = 11; gridGhostHoldNote.zIndex = 11;
@ -2433,6 +2517,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(notePreviewViewport); add(notePreviewViewport);
notePreviewViewport.zIndex = 30; notePreviewViewport.zIndex = 30;
notePreviewPlayhead = new FlxSprite().makeGraphic(2, 2, 0xFFFF0000);
notePreviewPlayhead.scrollFactor.set(0, 0);
notePreviewPlayhead.scale.set(notePreview.width / 2, 0.5); // Setting width does nothing.
notePreviewPlayhead.updateHitbox();
notePreviewPlayhead.x = notePreview.x;
notePreviewPlayhead.y = notePreview.y;
add(notePreviewPlayhead);
notePreviewPlayhead.zIndex = 31;
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
} }
@ -2529,6 +2622,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
} }
function refreshNotePreviewPlayheadPosition():Void
{
if (notePreviewPlayhead == null) return;
notePreviewPlayhead.y = notePreview.y + (notePreview.height * ((scrollPositionInPixels + playheadPositionInPixels) / songLengthInPixels));
}
/** /**
* Builds the group that will hold all the notes. * Builds the group that will hold all the notes.
*/ */
@ -3025,6 +3125,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
function setupTurboKeyHandlers():Void function setupTurboKeyHandlers():Void
{ {
// Keyboard shortcuts
add(undoKeyHandler); add(undoKeyHandler);
add(redoKeyHandler); add(redoKeyHandler);
add(upKeyHandler); add(upKeyHandler);
@ -3033,6 +3134,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(sKeyHandler); add(sKeyHandler);
add(pageUpKeyHandler); add(pageUpKeyHandler);
add(pageDownKeyHandler); add(pageDownKeyHandler);
// Gamepad inputs
add(dpadUpGamepadHandler);
add(dpadDownGamepadHandler);
add(dpadLeftGamepadHandler);
add(dpadRightGamepadHandler);
add(leftStickUpGamepadHandler);
add(leftStickDownGamepadHandler);
add(leftStickLeftGamepadHandler);
add(leftStickRightGamepadHandler);
add(rightStickUpGamepadHandler);
add(rightStickDownGamepadHandler);
add(rightStickLeftGamepadHandler);
add(rightStickRightGamepadHandler);
} }
/** /**
@ -3719,32 +3834,56 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Up Arrow = Scroll Up // Up Arrow = Scroll Up
if (upKeyHandler.activated && currentLiveInputStyle == None) if (upKeyHandler.activated && currentLiveInputStyle == None)
{ {
scrollAmount = -GRID_SIZE * 0.25 * 25.0; scrollAmount = -GRID_SIZE * 4;
shouldPause = true; shouldPause = true;
} }
// Down Arrow = Scroll Down // Down Arrow = Scroll Down
if (downKeyHandler.activated && currentLiveInputStyle == None) if (downKeyHandler.activated && currentLiveInputStyle == None)
{ {
scrollAmount = GRID_SIZE * 0.25 * 25.0; scrollAmount = GRID_SIZE * 4;
shouldPause = true; shouldPause = true;
} }
// W = Scroll Up (doesn't work with Ctrl+Scroll) // W = Scroll Up (doesn't work with Ctrl+Scroll)
if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL) if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{ {
scrollAmount = -GRID_SIZE * 0.25 * 25.0; scrollAmount = -GRID_SIZE * 4;
shouldPause = true; shouldPause = true;
} }
// S = Scroll Down (doesn't work with Ctrl+Scroll) // S = Scroll Down (doesn't work with Ctrl+Scroll)
if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL) if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{ {
scrollAmount = GRID_SIZE * 0.25 * 25.0; scrollAmount = GRID_SIZE * 4;
shouldPause = true; shouldPause = true;
} }
// PAGE UP = Jump up to nearest measure // GAMEPAD LEFT STICK UP = Scroll Up by 1 note snap
if (pageUpKeyHandler.activated) if (leftStickUpGamepadHandler.activated)
{ {
scrollAmount = -GRID_SIZE * noteSnapRatio;
shouldPause = true;
}
// GAMEPAD LEFT STICK DOWN = Scroll Down by 1 note snap
if (leftStickDownGamepadHandler.activated)
{
scrollAmount = GRID_SIZE * noteSnapRatio;
shouldPause = true;
}
// GAMEPAD RIGHT STICK UP = Scroll Up by 1 note snap (playhead only)
if (rightStickUpGamepadHandler.activated)
{
playheadAmount = -GRID_SIZE * noteSnapRatio;
shouldPause = true;
}
// GAMEPAD RIGHT STICK DOWN = Scroll Down by 1 note snap (playhead only)
if (rightStickDownGamepadHandler.activated)
{
playheadAmount = GRID_SIZE * noteSnapRatio;
shouldPause = true;
}
var funcJumpUp = (playheadOnly:Bool) -> {
var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight; var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight;
@ -3754,20 +3893,37 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure;
} }
scrollAmount = targetScrollPosition - playheadPos;
if (playheadOnly)
{
playheadAmount = targetScrollPosition - playheadPos;
}
else
{
scrollAmount = targetScrollPosition - playheadPos;
}
}
// PAGE UP = Jump up to nearest measure
// GAMEPAD LEFT STICK LEFT = Jump up to nearest measure
if (pageUpKeyHandler.activated || leftStickLeftGamepadHandler.activated)
{
funcJumpUp(false);
shouldPause = true;
}
if (rightStickLeftGamepadHandler.activated)
{
funcJumpUp(true);
shouldPause = true; shouldPause = true;
} }
if (playbarButtonPressed == 'playbarBack') if (playbarButtonPressed == 'playbarBack')
{ {
playbarButtonPressed = ''; playbarButtonPressed = '';
scrollAmount = -GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; funcJumpUp(false);
shouldPause = true; shouldPause = true;
} }
// PAGE DOWN = Jump down to nearest measure var funcJumpDown = (playheadOnly:Bool) -> {
if (pageDownKeyHandler.activated)
{
var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight; var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight;
@ -3777,26 +3933,46 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure;
} }
scrollAmount = targetScrollPosition - playheadPos;
if (playheadOnly)
{
playheadAmount = targetScrollPosition - playheadPos;
}
else
{
scrollAmount = targetScrollPosition - playheadPos;
}
}
// PAGE DOWN = Jump down to nearest measure
// GAMEPAD LEFT STICK RIGHT = Jump down to nearest measure
if (pageDownKeyHandler.activated || leftStickRightGamepadHandler.activated)
{
funcJumpDown(false);
shouldPause = true;
}
if (rightStickRightGamepadHandler.activated)
{
funcJumpDown(true);
shouldPause = true; shouldPause = true;
} }
if (playbarButtonPressed == 'playbarForward') if (playbarButtonPressed == 'playbarForward')
{ {
playbarButtonPressed = ''; playbarButtonPressed = '';
scrollAmount = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; funcJumpDown(false);
shouldPause = true; shouldPause = true;
} }
// SHIFT + Scroll = Scroll Fast // SHIFT + Scroll = Scroll Fast
if (FlxG.keys.pressed.SHIFT) // GAMEPAD LEFT STICK CLICK + Scroll = Scroll Fast
if (FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_STICK_CLICK ?? false))
{ {
scrollAmount *= 2; scrollAmount *= 2;
} }
// CONTROL + Scroll = Scroll Precise // CONTROL + Scroll = Scroll Precise
if (FlxG.keys.pressed.CONTROL) if (FlxG.keys.pressed.CONTROL)
{ {
scrollAmount /= 10; scrollAmount /= 4;
} }
// Alt + Drag = Scroll but move the playhead the same amount. // Alt + Drag = Scroll but move the playhead the same amount.
@ -4390,9 +4566,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
gridGhostHoldNote.visible = true; gridGhostHoldNote.visible = true;
gridGhostHoldNote.noteData = currentPlaceNoteData; gridGhostHoldNote.noteData = gridGhostNote.noteData;
gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection(); gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes); gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
@ -4953,37 +5128,57 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handlePlayhead():Void function handlePlayhead():Void
{ {
// Place notes at the playhead. // Place notes at the playhead with the keyboard.
switch (currentLiveInputStyle) switch (currentLiveInputStyle)
{ {
case ChartEditorLiveInputStyle.WASDKeys: case ChartEditorLiveInputStyle.WASDKeys:
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4); if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5); if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6); if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7); if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0); if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1); if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2); if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3); if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.NumberKeys: case ChartEditorLiveInputStyle.NumberKeys:
// Flipped because Dad is on the left but represents data 0-3. // Flipped because Dad is on the left but represents data 0-3.
if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4); if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5); if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6); if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7); if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0); if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1); if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2); if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3); if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.None: case ChartEditorLiveInputStyle.None:
// Do nothing. // Do nothing.
} }
updatePlayheadGhostHoldNotes();
} }
function placeNoteAtPlayhead(column:Int):Void function placeNoteAtPlayhead(column:Int):Void
{ {
// SHIFT + press or LEFT_SHOULDER + press to remove notes instead of placing them.
var removeNoteInstead:Bool = FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_SHOULDER ?? false);
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep));
@ -4994,14 +5189,136 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio); playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio);
notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]); notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]);
if (notesAtPos.length == 0) if (notesAtPos.length == 0 && !removeNoteInstead)
{ {
trace('Placing note. ${column}');
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace); var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
currentLiveInputPlaceNoteData[column] = newNoteData;
}
else if (removeNoteInstead)
{
trace('Removing existing note at position. ${column}');
performCommand(new RemoveNotesCommand(notesAtPos));
} }
else else
{ {
trace('Already a note there.'); trace('Already a note there. ${column}');
}
}
function updatePlayheadGhostHoldNotes():Void
{
// Ensure all the ghost hold notes exist.
while (gridPlayheadGhostHoldNotes.length < (STRUMLINE_SIZE * 2))
{
var ghost = new ChartEditorHoldNoteSprite(this);
ghost.alpha = 0.6;
ghost.noteData = null;
ghost.visible = false;
ghost.zIndex = 11;
add(ghost); // Don't add to `renderedHoldNotes` because then it will get killed every frame.
gridPlayheadGhostHoldNotes.push(ghost);
refresh();
}
// Update playhead ghost hold notes.
for (column in 0...gridPlayheadGhostHoldNotes.length)
{
var targetNoteData = currentLiveInputPlaceNoteData[column];
var ghostHold = gridPlayheadGhostHoldNotes[column];
if (targetNoteData == null && ghostHold.noteData != null)
{
// Remove the ghost hold note.
ghostHold.noteData = null;
}
if (targetNoteData != null && ghostHold.noteData == null)
{
// Readd the new ghost hold note.
ghostHold.noteData = targetNoteData.clone();
ghostHold.noteDirection = ghostHold.noteData.getDirection();
ghostHold.visible = true;
ghostHold.alpha = 0.6;
ghostHold.setHeightDirectly(0);
ghostHold.updateHoldNotePosition(renderedHoldNotes);
}
if (ghostHold.noteData == null)
{
ghostHold.visible = false;
ghostHold.setHeightDirectly(0);
playheadDragLengthCurrent[column] = 0;
continue;
}
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep));
var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio;
var newNoteLength:Float = playheadPosSnappedMs - ghostHold.noteData.time;
trace('newNoteLength: ${newNoteLength}');
if (newNoteLength > 0)
{
ghostHold.noteData.length = newNoteLength;
var targetNoteLengthSteps:Float = ghostHold.noteData.getStepLength(true);
var targetNoteLengthStepsInt:Int = Std.int(Math.floor(targetNoteLengthSteps));
var targetNoteLengthPixels:Float = targetNoteLengthSteps * GRID_SIZE;
if (playheadDragLengthCurrent[column] != targetNoteLengthStepsInt)
{
stretchySounds = !stretchySounds;
this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
playheadDragLengthCurrent[column] = targetNoteLengthStepsInt;
}
ghostHold.visible = true;
ghostHold.alpha = 0.6;
ghostHold.setHeightDirectly(targetNoteLengthPixels, true);
ghostHold.updateHoldNotePosition(renderedHoldNotes);
trace('lerpLength: ${ghostHold.fullSustainLength}');
trace('position: ${ghostHold.x}, ${ghostHold.y}');
}
else
{
ghostHold.visible = false;
ghostHold.setHeightDirectly(0);
playheadDragLengthCurrent[column] = 0;
continue;
}
}
}
function finishPlaceNoteAtPlayhead(column:Int):Void
{
if (currentLiveInputPlaceNoteData[column] == null) return;
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep));
var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio;
var newNoteLength:Float = playheadPosSnappedMs - currentLiveInputPlaceNoteData[column].time;
trace('finishPlace newNoteLength: ${newNoteLength}');
if (newNoteLength < Conductor.instance.stepLengthMs)
{
// Don't extend the note if it's too short.
trace('Not extending note. ${column}');
currentLiveInputPlaceNoteData[column] = null;
gridPlayheadGhostHoldNotes[column].noteData = null;
}
else
{
// Extend the note to the playhead position.
trace('Extending note. ${column}');
this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
performCommand(new ExtendNoteLengthCommand(currentLiveInputPlaceNoteData[column], newNoteLength));
currentLiveInputPlaceNoteData[column] = null;
gridPlayheadGhostHoldNotes[column].noteData = null;
} }
} }
@ -5338,30 +5655,31 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
catch (e) catch (e)
{ {
this.error("Could Not Playtest", 'Got an error trying to playtest the song.\n${e}'); this.error('Could Not Playtest', 'Got an error trying to playtest the song.\n${e}');
return; return;
} }
// TODO: Rework asset system so we can remove this. // TODO: Rework asset system so we can remove this jank.
switch (currentSongStage) switch (currentSongStage)
{ {
case 'mainStage': case 'mainStage':
Paths.setCurrentLevel('week1'); PlayStatePlaylist.campaignId = 'week1';
case 'spookyMansion': case 'spookyMansion':
Paths.setCurrentLevel('week2'); PlayStatePlaylist.campaignId = 'week2';
case 'phillyTrain': case 'phillyTrain':
Paths.setCurrentLevel('week3'); PlayStatePlaylist.campaignId = 'week3';
case 'limoRide': case 'limoRide':
Paths.setCurrentLevel('week4'); PlayStatePlaylist.campaignId = 'week4';
case 'mallXmas' | 'mallEvil': case 'mallXmas' | 'mallEvil':
Paths.setCurrentLevel('week5'); PlayStatePlaylist.campaignId = 'week5';
case 'school' | 'schoolEvil': case 'school' | 'schoolEvil':
Paths.setCurrentLevel('week6'); PlayStatePlaylist.campaignId = 'week6';
case 'tankmanBattlefield': case 'tankmanBattlefield':
Paths.setCurrentLevel('week7'); PlayStatePlaylist.campaignId = 'week7';
case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2': case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2':
Paths.setCurrentLevel('weekend1'); PlayStatePlaylist.campaignId = 'weekend1';
} }
Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
subStateClosed.add(reviveUICamera); subStateClosed.add(reviveUICamera);
subStateClosed.add(resetConductorAfterTest); subStateClosed.add(resetConductorAfterTest);
@ -5369,7 +5687,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
FlxTransitionableState.skipNextTransIn = false; FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false; FlxTransitionableState.skipNextTransOut = false;
var targetState = new PlayState( var targetStateParams =
{ {
targetSong: targetSong, targetSong: targetSong,
targetDifficulty: selectedDifficulty, targetDifficulty: selectedDifficulty,
@ -5380,24 +5698,26 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
startTimestamp: startTimestamp, startTimestamp: startTimestamp,
playbackRate: playbackRate, playbackRate: playbackRate,
overrideMusic: true, overrideMusic: true,
}); };
// Override music. // Override music.
if (audioInstTrack != null) if (audioInstTrack != null)
{ {
FlxG.sound.music = audioInstTrack; FlxG.sound.music = audioInstTrack;
} }
targetState.vocals = audioVocalTrackGroup;
// Kill and replace the UI camera so it doesn't get destroyed during the state transition. // Kill and replace the UI camera so it doesn't get destroyed during the state transition.
uiCamera.kill(); uiCamera.kill();
FlxG.cameras.remove(uiCamera, false); FlxG.cameras.remove(uiCamera, false);
FlxG.cameras.reset(new FunkinCamera()); FlxG.cameras.reset(new FunkinCamera('chartEditorUI2'));
this.persistentUpdate = false; this.persistentUpdate = false;
this.persistentDraw = false; this.persistentDraw = false;
stopWelcomeMusic(); stopWelcomeMusic();
openSubState(targetState);
LoadingState.loadPlayState(targetStateParams, false, true, function(targetState) {
targetState.vocals = audioVocalTrackGroup;
});
} }
/** /**

View file

@ -20,6 +20,8 @@ class RemoveEventsCommand implements ChartEditorCommand
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
if (events.length == 0) return;
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentEventSelection = []; state.currentEventSelection = [];
@ -34,6 +36,8 @@ class RemoveEventsCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
if (events.length == 0) return;
for (event in events) for (event in events)
{ {
state.currentSongChartEventData.push(event); state.currentSongChartEventData.push(event);

View file

@ -23,6 +23,8 @@ class RemoveItemsCommand implements ChartEditorCommand
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
if ((notes.length + events.length) == 0) return;
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
@ -40,6 +42,8 @@ class RemoveItemsCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
if ((notes.length + events.length) == 0) return;
for (note in notes) for (note in notes)
{ {
state.currentSongChartNoteData.push(note); state.currentSongChartNoteData.push(note);

View file

@ -20,6 +20,8 @@ class RemoveNotesCommand implements ChartEditorCommand
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
if (notes.length == 0) return;
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentNoteSelection = []; state.currentNoteSelection = [];
state.currentEventSelection = []; state.currentEventSelection = [];
@ -35,6 +37,8 @@ class RemoveNotesCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
if (notes.length == 0) return;
for (note in notes) for (note in notes)
{ {
state.currentSongChartNoteData.push(note); state.currentSongChartNoteData.push(note);

View file

@ -54,11 +54,16 @@ class ChartEditorHoldNoteSprite extends SustainTrail
* Set the height directly, to a value in pixels. * Set the height directly, to a value in pixels.
* @param h The desired height in pixels. * @param h The desired height in pixels.
*/ */
public function setHeightDirectly(h:Float, ?lerp:Bool = false) public function setHeightDirectly(h:Float, lerp:Bool = false)
{ {
if (lerp != null && lerp) sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25); if (lerp)
{
sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25);
}
else else
{
sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS); sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
}
fullSustainLength = sustainLength; fullSustainLength = sustainLength;
} }

View file

@ -117,12 +117,6 @@ class ChartEditorNoteSprite extends FlxSprite
{ {
noteFrameCollection.pushFrame(frame); noteFrameCollection.pushFrame(frame);
} }
var frameCollectionNormal2:FlxAtlasFrames = Paths.getSparrowAtlas('NoteHoldNormal');
for (frame in frameCollectionNormal2.frames)
{
noteFrameCollection.pushFrame(frame);
}
// Pixel notes // Pixel notes
var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null); var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null);

View file

@ -0,0 +1,193 @@
package funkin.ui.debug.charting.handlers;
import haxe.ui.focus.FocusManager;
import flixel.input.gamepad.FlxGamepad;
import haxe.ui.actions.ActionManager;
import haxe.ui.actions.IActionInputSource;
import haxe.ui.actions.ActionType;
/**
* Yes, we're that crazy. Gamepad support for the chart editor.
*/
// @:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorGamepadHandler
{
public static function handleGamepadControls(chartEditorState:ChartEditorState)
{
if (FlxG.gamepads.firstActive != null) handleGamepad(chartEditorState, FlxG.gamepads.firstActive);
}
/**
* Handle context-generic binds for the gamepad.
* @param chartEditorState The chart editor state.
* @param gamepad The gamepad to handle.
*/
static function handleGamepad(chartEditorState:ChartEditorState, gamepad:FlxGamepad):Void
{
if (chartEditorState.isHaxeUIFocused)
{
ChartEditorGamepadActionInputSource.instance.handleGamepad(gamepad);
}
else
{
handleGamepadLiveInputs(chartEditorState, gamepad);
if (gamepad.justPressed.RIGHT_SHOULDER)
{
trace('Gamepad: Right shoulder pressed, toggling audio playback.');
chartEditorState.toggleAudioPlayback();
}
if (gamepad.justPressed.START)
{
var minimal = gamepad.pressed.LEFT_SHOULDER;
chartEditorState.hideAllToolboxes();
trace('Gamepad: Start pressed, opening playtest (minimal: ${minimal})');
chartEditorState.testSongInPlayState(minimal);
}
if (gamepad.justPressed.BACK && !gamepad.pressed.LEFT_SHOULDER)
{
trace('Gamepad: Back pressed, focusing on HaxeUI menu.');
// FocusManager.instance.focus = chartEditorState.menubarMenuFile;
}
else if (gamepad.justPressed.BACK && gamepad.pressed.LEFT_SHOULDER)
{
trace('Gamepad: Back pressed, unfocusing on HaxeUI menu.');
FocusManager.instance.focus = null;
}
}
if (gamepad.justPressed.GUIDE)
{
trace('Gamepad: Guide pressed, quitting chart editor.');
chartEditorState.quitChartEditor();
}
}
static function handleGamepadLiveInputs(chartEditorState:ChartEditorState, gamepad:FlxGamepad):Void
{
// Place notes at the playhead with the gamepad.
// Disable when we are interacting with HaxeUI.
if (!(chartEditorState.isHaxeUIFocused || chartEditorState.isHaxeUIDialogOpen))
{
if (gamepad.justPressed.DPAD_LEFT) chartEditorState.placeNoteAtPlayhead(4);
if (gamepad.justReleased.DPAD_LEFT) chartEditorState.finishPlaceNoteAtPlayhead(4);
if (gamepad.justPressed.DPAD_DOWN) chartEditorState.placeNoteAtPlayhead(5);
if (gamepad.justReleased.DPAD_DOWN) chartEditorState.finishPlaceNoteAtPlayhead(5);
if (gamepad.justPressed.DPAD_UP) chartEditorState.placeNoteAtPlayhead(6);
if (gamepad.justReleased.DPAD_UP) chartEditorState.finishPlaceNoteAtPlayhead(6);
if (gamepad.justPressed.DPAD_RIGHT) chartEditorState.placeNoteAtPlayhead(7);
if (gamepad.justReleased.DPAD_RIGHT) chartEditorState.finishPlaceNoteAtPlayhead(7);
if (gamepad.justPressed.X) chartEditorState.placeNoteAtPlayhead(0);
if (gamepad.justReleased.X) chartEditorState.finishPlaceNoteAtPlayhead(0);
if (gamepad.justPressed.A) chartEditorState.placeNoteAtPlayhead(1);
if (gamepad.justReleased.A) chartEditorState.finishPlaceNoteAtPlayhead(1);
if (gamepad.justPressed.Y) chartEditorState.placeNoteAtPlayhead(2);
if (gamepad.justReleased.Y) chartEditorState.finishPlaceNoteAtPlayhead(2);
if (gamepad.justPressed.B) chartEditorState.placeNoteAtPlayhead(3);
if (gamepad.justReleased.B) chartEditorState.finishPlaceNoteAtPlayhead(3);
}
}
}
class ChartEditorGamepadActionInputSource implements IActionInputSource
{
public static var instance:ChartEditorGamepadActionInputSource = new ChartEditorGamepadActionInputSource();
public function new() {}
public function start():Void {}
/**
* Handle HaxeUI-specific binds for the gamepad.
* Only called when the HaxeUI menu is focused.
* @param chartEditorState The chart editor state.
* @param gamepad The gamepad to handle.
*/
public function handleGamepad(gamepad:FlxGamepad):Void
{
if (gamepad.justPressed.DPAD_LEFT)
{
trace('Gamepad: DPAD_LEFT pressed, moving left.');
ActionManager.instance.actionStart(ActionType.LEFT, this);
}
else if (gamepad.justReleased.DPAD_LEFT)
{
ActionManager.instance.actionEnd(ActionType.LEFT, this);
}
if (gamepad.justPressed.DPAD_RIGHT)
{
trace('Gamepad: DPAD_RIGHT pressed, moving right.');
ActionManager.instance.actionStart(ActionType.RIGHT, this);
}
else if (gamepad.justReleased.DPAD_RIGHT)
{
ActionManager.instance.actionEnd(ActionType.RIGHT, this);
}
if (gamepad.justPressed.DPAD_UP)
{
trace('Gamepad: DPAD_UP pressed, moving up.');
ActionManager.instance.actionStart(ActionType.UP, this);
}
else if (gamepad.justReleased.DPAD_UP)
{
ActionManager.instance.actionEnd(ActionType.UP, this);
}
if (gamepad.justPressed.DPAD_DOWN)
{
trace('Gamepad: DPAD_DOWN pressed, moving down.');
ActionManager.instance.actionStart(ActionType.DOWN, this);
}
else if (gamepad.justReleased.DPAD_DOWN)
{
ActionManager.instance.actionEnd(ActionType.DOWN, this);
}
if (gamepad.justPressed.A)
{
trace('Gamepad: A pressed, confirmingg.');
ActionManager.instance.actionStart(ActionType.CONFIRM, this);
}
else if (gamepad.justReleased.A)
{
ActionManager.instance.actionEnd(ActionType.CONFIRM, this);
}
if (gamepad.justPressed.B)
{
trace('Gamepad: B pressed, cancelling.');
ActionManager.instance.actionStart(ActionType.CANCEL, this);
}
else if (gamepad.justReleased.B)
{
ActionManager.instance.actionEnd(ActionType.CANCEL, this);
}
if (gamepad.justPressed.LEFT_TRIGGER)
{
trace('Gamepad: LEFT_TRIGGER pressed, moving to previous item.');
ActionManager.instance.actionStart(ActionType.PREVIOUS, this);
}
else if (gamepad.justReleased.LEFT_TRIGGER)
{
ActionManager.instance.actionEnd(ActionType.PREVIOUS, this);
}
if (gamepad.justPressed.RIGHT_TRIGGER)
{
trace('Gamepad: RIGHT_TRIGGER pressed, moving to next item.');
ActionManager.instance.actionStart(ActionType.NEXT, this);
}
else if (gamepad.justReleased.RIGHT_TRIGGER)
{
ActionManager.instance.actionEnd(ActionType.NEXT, this);
}
}
}

View file

@ -73,7 +73,7 @@ class ChartEditorImportExportHandler
state.loadInstFromAsset(Paths.inst(songId, '-$variation'), variation); state.loadInstFromAsset(Paths.inst(songId, '-$variation'), variation);
} }
for (difficultyId in song.listDifficulties(variation)) for (difficultyId in song.listDifficulties(variation, true, true))
{ {
var diff:Null<SongDifficulty> = song.getDifficulty(difficultyId, variation); var diff:Null<SongDifficulty> = song.getDifficulty(difficultyId, variation);
if (diff == null) continue; if (diff == null) continue;

View file

@ -308,16 +308,6 @@ class ChartEditorToolboxHandler
state.playtestBotPlayMode = checkboxBotPlay.selected; state.playtestBotPlayMode = checkboxBotPlay.selected;
}; };
var checkboxDebugger:Null<CheckBox> = toolbox.findComponent('playtestDebuggerCheckbox', CheckBox);
if (checkboxDebugger == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestDebuggerCheckbox component.';
state.enabledDebuggerPopup = checkboxDebugger.selected;
checkboxDebugger.onClick = _ -> {
state.enabledDebuggerPopup = checkboxDebugger.selected;
};
var checkboxSongScripts:Null<CheckBox> = toolbox.findComponent('playtestSongScriptsCheckbox', CheckBox); var checkboxSongScripts:Null<CheckBox> = toolbox.findComponent('playtestSongScriptsCheckbox', CheckBox);
if (checkboxSongScripts == null) if (checkboxSongScripts == null)

View file

@ -5,6 +5,7 @@ package funkin.ui.debug.charting;
using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler; using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler;
using funkin.ui.debug.charting.handlers.ChartEditorContextMenuHandler; using funkin.ui.debug.charting.handlers.ChartEditorContextMenuHandler;
using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler; using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler;
using funkin.ui.debug.charting.handlers.ChartEditorGamepadHandler;
using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler; using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler;
using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler; using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler;
using funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler; using funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;

View file

@ -171,10 +171,7 @@ class LatencyState extends MusicBeatSubState
trace(FlxG.sound.music._channel.position); trace(FlxG.sound.music._channel.position);
*/ */
#if FLX_DEBUG // localConductor.update(swagSong.time, false);
funnyStatsGraph.update(FlxG.sound.music.time % 500);
realStats.update(swagSong.getTimeWithDiff() % 500);
#end
if (FlxG.keys.justPressed.S) if (FlxG.keys.justPressed.S)
{ {

View file

@ -1,6 +1,7 @@
package funkin.ui.freeplay; package funkin.ui.freeplay;
import funkin.data.freeplay.AlbumData; import funkin.data.freeplay.AlbumData;
import funkin.data.animation.AnimationData;
import funkin.data.freeplay.AlbumRegistry; import funkin.data.freeplay.AlbumRegistry;
import funkin.data.IRegistryEntry; import funkin.data.IRegistryEntry;
import flixel.graphics.FlxGraphic; import flixel.graphics.FlxGraphic;
@ -75,6 +76,16 @@ class Album implements IRegistryEntry<AlbumData>
return _data.albumTitleAsset; return _data.albumTitleAsset;
} }
public function hasAlbumTitleAnimations()
{
return _data.albumTitleAnimations.length > 0;
}
public function getAlbumTitleAnimations():Array<AnimationData>
{
return _data.albumTitleAnimations;
}
public function toString():String public function toString():String
{ {
return 'Album($id)'; return 'Album($id)';

View file

@ -1,5 +1,6 @@
package funkin.ui.freeplay; package funkin.ui.freeplay;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup;
import flixel.util.FlxSort; import flixel.util.FlxSort;
@ -7,6 +8,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import funkin.data.freeplay.AlbumRegistry; import funkin.data.freeplay.AlbumRegistry;
import funkin.util.assets.FlxAnimationUtil;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import openfl.utils.Assets; import openfl.utils.Assets;
@ -21,9 +23,9 @@ class AlbumRoll extends FlxSpriteGroup
* The ID of the album to display. * The ID of the album to display.
* Modify this value to automatically update the album art and title. * Modify this value to automatically update the album art and title.
*/ */
public var albumId(default, set):String; public var albumId(default, set):Null<String>;
function set_albumId(value:String):String function set_albumId(value:Null<String>):Null<String>
{ {
if (this.albumId != value) if (this.albumId != value)
{ {
@ -34,30 +36,47 @@ class AlbumRoll extends FlxSpriteGroup
return value; return value;
} }
var albumArt:FunkinSprite; var newAlbumArt:FlxAtlasSprite;
var albumTitle:FunkinSprite;
var difficultyStars:DifficultyStars;
// var difficultyStars:DifficultyStars;
var _exitMovers:Null<FreeplayState.ExitMoverData>; var _exitMovers:Null<FreeplayState.ExitMoverData>;
var albumData:Album; var albumData:Album;
final animNames:Map<String, String> = [
"volume1-active" => "ENTRANCE",
"volume2-active" => "ENTRANCE VOL2",
"volume3-active" => "ENTRANCE VOL3",
"volume1-trans" => "VOL1 TRANS",
"volume2-trans" => "VOL2 TRANS",
"volume3-trans" => "VOL3 TRANS",
"volume1-idle" => "VOL1 STILL",
"volume2-idle" => "VOL2 STILL",
"volume3-idle" => "VOL3 STILL",
];
public function new() public function new()
{ {
super(); super();
albumTitle = new FunkinSprite(947, 491); newAlbumArt = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum"));
albumTitle.visible = true; newAlbumArt.visible = false;
albumTitle.zIndex = 200; newAlbumArt.onAnimationFinish.add(onAlbumFinish);
add(albumTitle);
difficultyStars = new DifficultyStars(140, 39); add(newAlbumArt);
difficultyStars.stars.visible = true; // difficultyStars = new DifficultyStars(140, 39);
albumTitle.visible = false; // difficultyStars.stars.visible = false;
// albumArtist.visible = false; // add(difficultyStars);
}
// var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite')); function onAlbumFinish(animName:String):Void
{
// Play the idle animation for the current album.
newAlbumArt.playAnimation(animNames.get('$albumId-idle'), false, false, true);
// End on the last frame and don't continue until playAnimation is called again.
// newAlbumArt.anim.pause();
} }
/** /**
@ -65,6 +84,12 @@ class AlbumRoll extends FlxSpriteGroup
*/ */
function updateAlbum():Void function updateAlbum():Void
{ {
if (albumId == null)
{
// difficultyStars.stars.visible = false;
return;
}
albumData = AlbumRegistry.instance.fetchEntry(albumId); albumData = AlbumRegistry.instance.fetchEntry(albumId);
if (albumData == null) if (albumData == null)
@ -74,33 +99,8 @@ class AlbumRoll extends FlxSpriteGroup
return; return;
}; };
if (albumArt != null)
{
FlxTween.cancelTweensOf(albumArt);
albumArt.visible = false;
albumArt.destroy();
remove(albumArt);
}
// Paths.animateAtlas('freeplay/albumRoll'),
albumArt = FunkinSprite.create(1500, 360, albumData.getAlbumArtAssetKey());
albumArt.setGraphicSize(262, 262); // Magic number for size IG
albumArt.zIndex = 100;
// playIntro();
add(albumArt);
applyExitMovers(); applyExitMovers();
if (Assets.exists(Paths.image(albumData.getAlbumTitleAssetKey())))
{
albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey()));
}
else
{
albumTitle.visible = false;
}
refresh(); refresh();
} }
@ -126,67 +126,46 @@ class AlbumRoll extends FlxSpriteGroup
if (exitMovers == null) return; if (exitMovers == null) return;
exitMovers.set([albumArt], exitMovers.set([newAlbumArt],
{ {
x: FlxG.width, x: FlxG.width,
speed: 0.4, speed: 0.4,
wait: 0 wait: 0
}); });
exitMovers.set([albumTitle],
{
x: FlxG.width,
speed: 0.2,
wait: 0.1
});
/*
exitMovers.set([albumArtist],
{
x: FlxG.width * 1.1,
speed: 0.2,
wait: 0.2
});
*/
exitMovers.set([difficultyStars],
{
x: FlxG.width * 1.2,
speed: 0.2,
wait: 0.3
});
} }
var titleTimer:Null<FlxTimer> = null;
/** /**
* Play the intro animation on the album art. * Play the intro animation on the album art.
*/ */
public function playIntro():Void public function playIntro():Void
{ {
albumArt.visible = true; newAlbumArt.visible = true;
FlxTween.tween(albumArt, {x: 950, y: 320, angle: -340}, 0.5, {ease: FlxEase.elasticOut}); newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false);
albumTitle.visible = false; // difficultyStars.stars.visible = false;
new FlxTimer().start(0.75, function(_) { new FlxTimer().start(0.75, function(_) {
showTitle(); // showTitle();
// showStars();
}); });
} }
public function setDifficultyStars(?difficulty:Int):Void public function skipIntro():Void
{ {
if (difficulty == null) return; newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false);
difficultyStars.difficulty = difficulty;
} }
public function showTitle():Void // public function setDifficultyStars(?difficulty:Int):Void
{ // {
albumTitle.visible = true; // if (difficulty == null) return;
} // difficultyStars.difficulty = difficulty;
// }
/** // /**
* Make the album stars visible. // * Make the album stars visible.
*/ // */
public function showStars():Void // public function showStars():Void
{ // {
// albumArtist.visible = false; // difficultyStars.stars.visible = false; // true;
difficultyStars.stars.visible = false; // }
}
} }

View file

@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite
var gotSpooked:Bool = false; var gotSpooked:Bool = false;
static final SPOOK_PERIOD:Float = 10.0; static final SPOOK_PERIOD:Float = 120.0;
static final TV_PERIOD:Float = 10.0; static final TV_PERIOD:Float = 180.0;
// Time since dad last SPOOKED you. // Time since dad last SPOOKED you.
var timeSinceSpook:Float = 0; var timeSinceSpook:Float = 0;
@ -43,7 +43,14 @@ class DJBoyfriend extends FlxAtlasSprite
switch (name) switch (name)
{ {
case "Boyfriend DJ watchin tv OG": case "Boyfriend DJ watchin tv OG":
if (number == 85) runTvLogic(); if (number == 80)
{
FunkinSound.playOnce(Paths.sound('remote_click'));
}
if (number == 85)
{
runTvLogic();
}
default: default:
} }
}; };
@ -219,19 +226,17 @@ class DJBoyfriend extends FlxAtlasSprite
if (cartoonSnd == null) if (cartoonSnd == null)
{ {
// tv is OFF, but getting turned on // tv is OFF, but getting turned on
// Eric got FUCKING TROLLED there is no `tv_on` or `channel_switch` sound! FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() {
// FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { loadCartoon();
// }); });
loadCartoon();
} }
else else
{ {
// plays it smidge after the click // plays it smidge after the click
// new FlxTimer().start(0.1, function(_) { FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() {
// // FunkinSound.playOnce(Paths.sound('channel_switch')); cartoonSnd.destroy();
// }); loadCartoon();
cartoonSnd.destroy(); });
loadCartoon();
} }
// loadCartoon(); // loadCartoon();

View file

@ -1,106 +0,0 @@
package funkin.ui.freeplay;
import flixel.group.FlxSpriteGroup;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.graphics.shaders.HSVShader;
class DifficultyStars extends FlxSpriteGroup
{
/**
* Internal handler var for difficulty... ranges from 0... to 15
* 0 is 1 star... 15 is 0 stars!
*/
var curDifficulty(default, set):Int = 0;
/**
* Range between 0 and 15
*/
public var difficulty(default, set):Int = 1;
public var stars:FlxAtlasSprite;
var flames:FreeplayFlames;
var hsvShader:HSVShader;
public function new(x:Float, y:Float)
{
super(x, y);
hsvShader = new HSVShader();
flames = new FreeplayFlames(0, 0);
add(flames);
stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars"));
stars.anim.play("diff stars");
add(stars);
stars.shader = hsvShader;
for (memb in flames.members)
memb.shader = hsvShader;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// "loops" the current animation
// for clarity, the animation file looks like
// frame : stars
// 0-99: 1 star
// 100-199: 2 stars
// ......
// 1300-1499: 15 stars
// 1500 : 0 stars
if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100)
{
stars.anim.play("diff stars", true, false, curDifficulty * 100);
}
}
function set_difficulty(value:Int):Int
{
difficulty = value;
if (difficulty <= 0)
{
difficulty = 0;
curDifficulty = 15;
}
else if (difficulty <= 15)
{
difficulty = value;
curDifficulty = difficulty - 1;
}
else
{
difficulty = 15;
curDifficulty = difficulty - 1;
}
if (difficulty > 10) flames.flameCount = difficulty - 10;
else
flames.flameCount = 0;
return difficulty;
}
function set_curDifficulty(value:Int):Int
{
curDifficulty = value;
if (curDifficulty == 15)
{
stars.anim.play("diff stars", true, false, 1500);
stars.anim.pause();
}
else
{
stars.anim.curFrame = Std.int(curDifficulty * 100);
stars.anim.play("diff stars", true, false, curDifficulty * 100);
}
return curDifficulty;
}
}

View file

@ -42,11 +42,11 @@ class FreeplayScore extends FlxTypedSpriteGroup<ScoreNum>
return val; return val;
} }
public function new(x:Float, y:Float, scoreShit:Int = 100) public function new(x:Float, y:Float, digitCount:Int, scoreShit:Int = 100)
{ {
super(x, y); super(x, y);
for (i in 0...7) for (i in 0...digitCount)
{ {
add(new ScoreNum(x + (45 * i), y, 0)); add(new ScoreNum(x + (45 * i), y, 0));
} }

View file

@ -133,8 +133,8 @@ class FreeplayState extends MusicBeatSubState
var stickerSubState:StickerSubState; var stickerSubState:StickerSubState;
static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY; public static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
static var rememberedSongId:Null<String> = null; public static var rememberedSongId:Null<String> = 'tutorial';
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
{ {
@ -145,7 +145,7 @@ class FreeplayState extends MusicBeatSubState
stickerSubState = stickers; stickerSubState = stickers;
} }
super(); super(FlxColor.TRANSPARENT);
} }
override function create():Void override function create():Void
@ -195,7 +195,7 @@ class FreeplayState extends MusicBeatSubState
var song:Song = SongRegistry.instance.fetchEntry(songId); var song:Song = SongRegistry.instance.fetchEntry(songId);
// Only display songs which actually have available charts for the current character. // Only display songs which actually have available charts for the current character.
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations); var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
if (availableDifficultiesForSong.length == 0) continue; if (availableDifficultiesForSong.length == 0) continue;
songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations)); songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
@ -380,7 +380,7 @@ class FreeplayState extends MusicBeatSubState
} }
albumRoll = new AlbumRoll(); albumRoll = new AlbumRoll();
albumRoll.albumId = 'volume1'; albumRoll.albumId = null;
add(albumRoll); add(albumRoll);
albumRoll.applyExitMovers(exitMovers); albumRoll.applyExitMovers(exitMovers);
@ -425,7 +425,7 @@ class FreeplayState extends MusicBeatSubState
tmr.time = FlxG.random.float(20, 60); tmr.time = FlxG.random.float(20, 60);
}, 0); }, 0);
fp = new FreeplayScore(460, 60, 100); fp = new FreeplayScore(460, 60, 7, 100);
fp.visible = false; fp.visible = false;
add(fp); add(fp);
@ -470,11 +470,7 @@ class FreeplayState extends MusicBeatSubState
albumRoll.playIntro(); albumRoll.playIntro();
new FlxTimer().start(0.75, function(_) { new FlxTimer().start(0.75, function(_) {
albumRoll.showTitle(); // albumRoll.showTitle();
});
new FlxTimer().start(35 / 24, function(_) {
albumRoll.showStars();
}); });
FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut}); FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
@ -521,7 +517,7 @@ class FreeplayState extends MusicBeatSubState
// var swag:Alphabet = new Alphabet(1, 0, 'swag'); // var swag:Alphabet = new Alphabet(1, 0, 'swag');
var funnyCam:FunkinCamera = new FunkinCamera(0, 0, FlxG.width, FlxG.height); var funnyCam:FunkinCamera = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height);
funnyCam.bgColor = FlxColor.TRANSPARENT; funnyCam.bgColor = FlxColor.TRANSPARENT;
FlxG.cameras.add(funnyCam); FlxG.cameras.add(funnyCam);
@ -536,21 +532,18 @@ class FreeplayState extends MusicBeatSubState
}); });
} }
var currentFilter:SongFilter = null;
var currentFilteredSongs:Array<FreeplaySongData> = [];
/** /**
* Given the current filter, rebuild the current song list. * Given the current filter, rebuild the current song list.
* *
* @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite) * @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite)
* @param force * @param force
* @param onlyIfChanged Only apply the filter if the song list has changed
*/ */
public function generateSongList(?filterStuff:SongFilter, force:Bool = false):Void public function generateSongList(filterStuff:Null<SongFilter>, force:Bool = false, onlyIfChanged:Bool = true):Void
{ {
curSelected = 1;
for (cap in grpCapsules.members)
{
cap.kill();
}
var tempSongs:Array<FreeplaySongData> = songs; var tempSongs:Array<FreeplaySongData> = songs;
if (filterStuff != null) if (filterStuff != null)
@ -582,6 +575,35 @@ class FreeplayState extends MusicBeatSubState
} }
} }
// Filter further by current selected difficulty.
if (currentDifficulty != null)
{
tempSongs = tempSongs.filter(song -> {
if (song == null) return true; // Random
return song.songDifficulties.contains(currentDifficulty);
});
}
if (onlyIfChanged)
{
// == performs equality by reference
if (tempSongs.isEqualUnordered(currentFilteredSongs)) return;
}
// Only now do we know that the filter is actually changing.
rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId ?? rememberedSongId;
for (cap in grpCapsules.members)
{
cap.kill();
}
currentFilter = filterStuff;
currentFilteredSongs = tempSongs;
curSelected = 0;
var hsvShader:HSVShader = new HSVShader(); var hsvShader:HSVShader = new HSVShader();
var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem);
@ -616,14 +638,7 @@ class FreeplayState extends MusicBeatSubState
funnyMenu.favIcon.visible = tempSongs[i].isFav; funnyMenu.favIcon.visible = tempSongs[i].isFav;
funnyMenu.hsvShader = hsvShader; funnyMenu.hsvShader = hsvShader;
if (i < 8) funnyMenu.forcePosition();
{
funnyMenu.initJumpIn(Math.min(i, 4), force);
}
else
{
funnyMenu.forcePosition();
}
grpCapsules.add(funnyMenu); grpCapsules.add(funnyMenu);
} }
@ -658,11 +673,12 @@ class FreeplayState extends MusicBeatSubState
if (FlxG.keys.justPressed.F) if (FlxG.keys.justPressed.F)
{ {
if (songs[curSelected] != null) var targetSong = grpCapsules.members[curSelected]?.songData;
if (targetSong != null)
{ {
var realShit:Int = curSelected; var realShit:Int = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav; targetSong.isFav = !targetSong.isFav;
if (songs[curSelected].isFav) if (targetSong.isFav)
{ {
FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
{ {
@ -854,11 +870,13 @@ class FreeplayState extends MusicBeatSubState
{ {
dj.resetAFKTimer(); dj.resetAFKTimer();
changeDiff(-1); changeDiff(-1);
generateSongList(currentFilter, true);
} }
if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL)
{ {
dj.resetAFKTimer(); dj.resetAFKTimer();
changeDiff(1); changeDiff(1);
generateSongList(currentFilter, true);
} }
if (controls.BACK && !typing.hasFocus) if (controls.BACK && !typing.hasFocus)
@ -877,6 +895,8 @@ class FreeplayState extends MusicBeatSubState
for (spr in grpSpr) for (spr in grpSpr)
{ {
if (spr == null) continue;
var funnyMoveShit:MoveData = moveData; var funnyMoveShit:MoveData = moveData;
if (moveData.x == null) funnyMoveShit.x = spr.x; if (moveData.x == null) funnyMoveShit.x = spr.x;
@ -899,7 +919,7 @@ class FreeplayState extends MusicBeatSubState
if (Type.getClass(FlxG.state) == MainMenuState) if (Type.getClass(FlxG.state) == MainMenuState)
{ {
FlxG.state.persistentUpdate = true; FlxG.state.persistentUpdate = false;
FlxG.state.persistentDraw = true; FlxG.state.persistentDraw = true;
} }
@ -908,6 +928,11 @@ class FreeplayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransOut = true; FlxTransitionableState.skipNextTransOut = true;
if (Type.getClass(FlxG.state) == MainMenuState) if (Type.getClass(FlxG.state) == MainMenuState)
{ {
FunkinSound.playMusic('freakyMenu',
{
overrideExisting: true,
restartTrack: false
});
close(); close();
} }
else else
@ -926,7 +951,7 @@ class FreeplayState extends MusicBeatSubState
public override function destroy():Void public override function destroy():Void
{ {
super.destroy(); super.destroy();
var daSong:Null<FreeplaySongData> = songs[curSelected]; var daSong:Null<FreeplaySongData> = currentFilteredSongs[curSelected];
if (daSong != null) if (daSong != null)
{ {
clearDaCache(daSong.songName); clearDaCache(daSong.songName);
@ -948,10 +973,10 @@ class FreeplayState extends MusicBeatSubState
currentDifficulty = diffIdsCurrent[currentDifficultyIndex]; currentDifficulty = diffIdsCurrent[currentDifficultyIndex];
var daSong:Null<FreeplaySongData> = songs[curSelected]; var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].songData;
if (daSong != null) if (daSong != null)
{ {
var songScore:SaveScoreData = Save.instance.getSongScore(songs[curSelected].songId, currentDifficulty); var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty);
intendedScore = songScore?.score ?? 0; intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0; intendedCompletion = songScore?.accuracy ?? 0.0;
rememberedDifficulty = currentDifficulty; rememberedDifficulty = currentDifficulty;
@ -1011,15 +1036,12 @@ class FreeplayState extends MusicBeatSubState
} }
} }
// Set the difficulty star count on the right.
albumRoll.setDifficultyStars(daSong?.songRating);
// Set the album graphic and play the animation if relevant. // Set the album graphic and play the animation if relevant.
var newAlbumId:String = daSong?.albumId ?? Constants.DEFAULT_ALBUM_ID; var newAlbumId:String = daSong?.albumId;
if (albumRoll.albumId != newAlbumId) if (albumRoll.albumId != newAlbumId)
{ {
albumRoll.albumId = newAlbumId; albumRoll.albumId = newAlbumId;
albumRoll.playIntro(); albumRoll.skipIntro();
} }
} }
@ -1103,6 +1125,12 @@ class FreeplayState extends MusicBeatSubState
targetVariation: targetVariation, targetVariation: targetVariation,
practiceMode: false, practiceMode: false,
minimalMode: false, minimalMode: false,
#if (debug || FORCE_DEBUG_VERSION)
botPlayMode: FlxG.keys.pressed.SHIFT,
#else
botPlayMode: false,
#end
// TODO: Make these an option! It's currently only accessible via chart editor. // TODO: Make these an option! It's currently only accessible via chart editor.
// startTimestamp: 0.0, // startTimestamp: 0.0,
// playbackRate: 0.5, // playbackRate: 0.5,
@ -1115,20 +1143,18 @@ class FreeplayState extends MusicBeatSubState
{ {
if (rememberedSongId != null) if (rememberedSongId != null)
{ {
curSelected = songs.findIndex(function(song) { curSelected = currentFilteredSongs.findIndex(function(song) {
if (song == null) return false; if (song == null) return false;
return song.songId == rememberedSongId; return song.songId == rememberedSongId;
}); });
if (curSelected == -1) curSelected = 0;
} }
if (rememberedDifficulty != null) if (rememberedDifficulty != null)
{ {
currentDifficulty = rememberedDifficulty; currentDifficulty = rememberedDifficulty;
} }
// Set the difficulty star count on the right.
var daSong:Null<FreeplaySongData> = songs[curSelected];
albumRoll.setDifficultyStars(daSong?.songRating ?? 0);
} }
function changeSelection(change:Int = 0):Void function changeSelection(change:Int = 0):Void
@ -1156,8 +1182,10 @@ class FreeplayState extends MusicBeatSubState
{ {
intendedScore = 0; intendedScore = 0;
intendedCompletion = 0.0; intendedCompletion = 0.0;
diffIdsCurrent = diffIdsTotal;
rememberedSongId = null; rememberedSongId = null;
rememberedDifficulty = null; rememberedDifficulty = null;
albumRoll.albumId = null;
} }
for (index => capsule in grpCapsules.members) for (index => capsule in grpCapsules.members)
@ -1195,12 +1223,33 @@ class FreeplayState extends MusicBeatSubState
}); });
if (didReplace) if (didReplace)
{ {
FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: false
});
FlxG.sound.music.fadeIn(2, 0, 0.8); FlxG.sound.music.fadeIn(2, 0, 0.8);
} }
} }
grpCapsules.members[curSelected].selected = true; grpCapsules.members[curSelected].selected = true;
} }
} }
/**
* Build an instance of `FreeplayState` that is above the `MainMenuState`.
* @return The MainMenuState with the FreeplayState as a substate.
*/
public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
{
var result = new MainMenuState();
result.persistentUpdate = false;
result.persistentDraw = true;
result.openSubState(new FreeplayState(params, stickers));
return result;
}
} }
/** /**
@ -1307,7 +1356,7 @@ class FreeplaySongData
public var songName(default, null):String = ''; public var songName(default, null):String = '';
public var songCharacter(default, null):String = ''; public var songCharacter(default, null):String = '';
public var songRating(default, null):Int = 0; public var songRating(default, null):Int = 0;
public var albumId(default, null):String = ''; public var albumId(default, null):Null<String> = null;
public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
public var displayedVariations(default, null):Array<String> = [Constants.DEFAULT_VARIATION]; public var displayedVariations(default, null):Array<String> = [Constants.DEFAULT_VARIATION];
@ -1333,7 +1382,7 @@ class FreeplaySongData
function updateValues(variations:Array<String>):Void function updateValues(variations:Array<String>):Void
{ {
this.songDifficulties = song.listDifficulties(variations); this.songDifficulties = song.listDifficulties(variations, false, false);
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY; if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations); var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations);
@ -1341,7 +1390,15 @@ class FreeplaySongData
this.songName = songDifficulty.songName; this.songName = songDifficulty.songName;
this.songCharacter = songDifficulty.characters.opponent; this.songCharacter = songDifficulty.characters.opponent;
this.songRating = songDifficulty.difficultyRating; this.songRating = songDifficulty.difficultyRating;
this.albumId = songDifficulty.album; if (songDifficulty.album == null)
{
FlxG.log.warn('No album for: ${songDifficulty.songName}');
this.albumId = Constants.DEFAULT_ALBUM_ID;
}
else
{
this.albumId = songDifficulty.album;
}
} }
} }

View file

@ -46,7 +46,7 @@ class SongMenuItem extends FlxSpriteGroup
public var hsvShader(default, set):HSVShader; public var hsvShader(default, set):HSVShader;
var diffRatingSprite:FlxSprite; // var diffRatingSprite:FlxSprite;
public function new(x:Float, y:Float) public function new(x:Float, y:Float)
{ {
@ -65,13 +65,13 @@ class SongMenuItem extends FlxSpriteGroup
var rank:String = FlxG.random.getObject(ranks); var rank:String = FlxG.random.getObject(ranks);
ranking = new FlxSprite(capsule.width * 0.84, 30); ranking = new FlxSprite(capsule.width * 0.84, 30);
ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank)); // ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
ranking.scale.x = ranking.scale.y = realScaled; // ranking.scale.x = ranking.scale.y = realScaled;
// ranking.alpha = 0.75; // ranking.alpha = 0.75;
ranking.visible = false; // ranking.visible = false;
ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y); // ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y);
add(ranking); // add(ranking);
grpHide.add(ranking); // grpHide.add(ranking);
switch (rank) switch (rank)
{ {
@ -81,12 +81,12 @@ class SongMenuItem extends FlxSpriteGroup
grayscaleShader = new Grayscale(1); grayscaleShader = new Grayscale(1);
diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00')); // diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00'));
diffRatingSprite.shader = grayscaleShader; // diffRatingSprite.shader = grayscaleShader;
// diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y);
// TODO: Readd once ratings are fully implemented // TODO: Readd once ratings are fully implemented
// add(diffRatingSprite); // add(diffRatingSprite);
diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y); // grpHide.add(diffRatingSprite);
grpHide.add(diffRatingSprite);
songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled)); songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled));
add(songText); add(songText);
@ -118,8 +118,8 @@ class SongMenuItem extends FlxSpriteGroup
function updateDifficultyRating(newRating:Int):Void function updateDifficultyRating(newRating:Int):Void
{ {
var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating'; var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}')); // diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
diffRatingSprite.visible = false; // diffRatingSprite.visible = false;
} }
function set_hsvShader(value:HSVShader):HSVShader function set_hsvShader(value:HSVShader):HSVShader

View file

@ -0,0 +1,53 @@
package funkin.ui.haxeui;
import flixel.FlxBasic;
import flixel.input.gamepad.FlxGamepad;
import haxe.ui.actions.IActionInputSource;
/**
* Receives button presses from the Flixel gamepad and emits HaxeUI events.
*/
class FlxGamepadActionInputSource extends FlxBasic
{
public static var instance(get, null):FlxGamepadActionInputSource;
static function get_instance():FlxGamepadActionInputSource
{
if (instance == null) instance = new FlxGamepadActionInputSource();
return instance;
}
public function new()
{
super();
}
public function start():Void
{
FlxG.plugins.addPlugin(this);
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (FlxG.gamepads.firstActive != null)
{
updateGamepad(elapsed, FlxG.gamepads.firstActive);
}
}
function updateGamepad(elapsed:Float, gamepad:FlxGamepad):Void
{
if (gamepad.justPressed.BACK)
{
//
}
}
public override function destroy():Void
{
super.destroy();
FlxG.plugins.remove(this);
}
}

View file

@ -1,5 +1,6 @@
package funkin.ui.mainmenu; package funkin.ui.mainmenu;
import funkin.graphics.FunkinSprite;
import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionableState;
import funkin.ui.debug.DebugMenuSubState; import funkin.ui.debug.DebugMenuSubState;
import flixel.FlxObject; import flixel.FlxObject;
@ -51,12 +52,10 @@ class MainMenuState extends MusicBeatState
transIn = FlxTransitionableState.defaultTransIn; transIn = FlxTransitionableState.defaultTransIn;
transOut = FlxTransitionableState.defaultTransOut; transOut = FlxTransitionableState.defaultTransOut;
if (!(FlxG?.sound?.music?.playing ?? false)) playMenuMusic();
{
playMenuMusic();
}
persistentUpdate = persistentDraw = true; persistentUpdate = false;
persistentDraw = true;
var bg:FlxSprite = new FlxSprite(Paths.image('menuBG')); var bg:FlxSprite = new FlxSprite(Paths.image('menuBG'));
bg.scrollFactor.x = 0; bg.scrollFactor.x = 0;
@ -69,7 +68,7 @@ class MainMenuState extends MusicBeatState
camFollow = new FlxObject(0, 0, 1, 1); camFollow = new FlxObject(0, 0, 1, 1);
add(camFollow); add(camFollow);
magenta = new FlxSprite(Paths.image('menuDesat')); magenta = new FlxSprite(Paths.image('menuBGMagenta'));
magenta.scrollFactor.x = bg.scrollFactor.x; magenta.scrollFactor.x = bg.scrollFactor.x;
magenta.scrollFactor.y = bg.scrollFactor.y; magenta.scrollFactor.y = bg.scrollFactor.y;
magenta.setGraphicSize(Std.int(bg.width)); magenta.setGraphicSize(Std.int(bg.width));
@ -77,7 +76,6 @@ class MainMenuState extends MusicBeatState
magenta.x = bg.x; magenta.x = bg.x;
magenta.y = bg.y; magenta.y = bg.y;
magenta.visible = false; magenta.visible = false;
magenta.color = 0xFFfd719b;
// TODO: Why doesn't this line compile I'm going fucking feral // TODO: Why doesn't this line compile I'm going fucking feral
@ -109,14 +107,21 @@ class MainMenuState extends MusicBeatState
}); });
#if CAN_OPEN_LINKS #if CAN_OPEN_LINKS
// In order to prevent popup blockers from triggering,
// we need to open the link as an immediate result of a keypress event,
// so we can't wait for the flicker animation to complete.
var hasPopupBlocker = #if web true #else false #end; var hasPopupBlocker = #if web true #else false #end;
createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker); createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker);
#end #end
createMenuItem('options', 'mainmenu/options', function() { createMenuItem('options', 'mainmenu/options', function() {
startExitState(() -> new funkin.ui.options.OptionsState()); startExitState(() -> new funkin.ui.options.OptionsState());
}); });
createMenuItem('credits', 'mainmenu/credits', function() {
startExitState(() -> new funkin.ui.credits.CreditsState());
});
// Reset position of menu items. // Reset position of menu items.
var spacing = 160; var spacing = 160;
var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2; var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2;
@ -125,6 +130,9 @@ class MainMenuState extends MusicBeatState
var menuItem = menuItems.members[i]; var menuItem = menuItems.members[i];
menuItem.x = FlxG.width / 2; menuItem.x = FlxG.width / 2;
menuItem.y = top + spacing * i; menuItem.y = top + spacing * i;
menuItem.scrollFactor.x = 0.0;
// This one affects how much the menu items move when you scroll between them.
menuItem.scrollFactor.y = 0.4;
} }
resetCamStuff(); resetCamStuff();
@ -164,8 +172,9 @@ class MainMenuState extends MusicBeatState
function resetCamStuff() function resetCamStuff()
{ {
FlxG.cameras.reset(new FunkinCamera()); FlxG.cameras.reset(new FunkinCamera('mainMenu'));
FlxG.camera.follow(camFollow, null, 0.06); FlxG.camera.follow(camFollow, null, 0.06);
FlxG.camera.snapToTarget();
} }
function createMenuItem(name:String, atlas:String, callback:Void->Void, fireInstantly:Bool = false):Void function createMenuItem(name:String, atlas:String, callback:Void->Void, fireInstantly:Bool = false):Void
@ -212,6 +221,11 @@ class MainMenuState extends MusicBeatState
{ {
WindowUtil.openURL(Constants.URL_ITCH); WindowUtil.openURL(Constants.URL_ITCH);
} }
function selectMerch()
{
WindowUtil.openURL(Constants.URL_MERCH);
}
#end #end
#if newgrounds #if newgrounds
@ -311,8 +325,6 @@ class MainMenuState extends MusicBeatState
// Open the debug menu, defaults to ` / ~ // Open the debug menu, defaults to ` / ~
if (controls.DEBUG_MENU) if (controls.DEBUG_MENU)
{ {
this.persistentUpdate = false;
this.persistentDraw = false;
FlxG.state.openSubState(new DebugMenuSubState()); FlxG.state.openSubState(new DebugMenuSubState());
} }

View file

@ -48,7 +48,7 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page
{ {
super(); super();
menuCamera = new FunkinCamera(); menuCamera = new FunkinCamera('controlsMenu');
FlxG.cameras.add(menuCamera, false); FlxG.cameras.add(menuCamera, false);
menuCamera.bgColor = 0x0; menuCamera.bgColor = 0x0;
camera = menuCamera; camera = menuCamera;

View file

@ -23,8 +23,7 @@ class OptionsState extends MusicBeatState
override function create() override function create()
{ {
var menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat')); var menuBG = new FlxSprite().loadGraphic(Paths.image('menuBGBlue'));
menuBG.color = 0xFFea71fd;
menuBG.setGraphicSize(Std.int(menuBG.width * 1.1)); menuBG.setGraphicSize(Std.int(menuBG.width * 1.1));
menuBG.updateHitbox(); menuBG.updateHitbox();
menuBG.screenCenter(); menuBG.screenCenter();

View file

@ -21,7 +21,7 @@ class PreferencesMenu extends Page
{ {
super(); super();
menuCamera = new FunkinCamera(); menuCamera = new FunkinCamera('prefMenu');
FlxG.cameras.add(menuCamera, false); FlxG.cameras.add(menuCamera, false);
menuCamera.bgColor = 0x0; menuCamera.bgColor = 0x0;
camera = menuCamera; camera = menuCamera;

View file

@ -169,7 +169,7 @@ class Level implements IRegistryEntry<LevelData>
if (firstSong != null) if (firstSong != null)
{ {
// Don't display alternate characters in Story Mode. Only show `default` and `erect` variations. // Don't display alternate characters in Story Mode. Only show `default` and `erect` variations.
for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect'])) for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect'], false, false))
{ {
difficulties.push(difficulty); difficulties.push(difficulty);
} }

View file

@ -17,7 +17,7 @@ import funkin.ui.MusicBeatState;
*/ */
class AttractState extends MusicBeatState class AttractState extends MusicBeatState
{ {
static final ATTRACT_VIDEO_PATH:String = Paths.videos('kickstarterTrailer'); static final ATTRACT_VIDEO_PATH:String = Paths.stripLibrary(Paths.videos('kickstarterTrailer', 'shared'));
public override function create():Void public override function create():Void
{ {
@ -29,10 +29,12 @@ class AttractState extends MusicBeatState
} }
#if html5 #if html5
trace('Playing web video ${ATTRACT_VIDEO_PATH}');
playVideoHTML5(ATTRACT_VIDEO_PATH); playVideoHTML5(ATTRACT_VIDEO_PATH);
#end #end
#if hxCodec #if hxCodec
trace('Playing native video ${ATTRACT_VIDEO_PATH}');
playVideoNative(ATTRACT_VIDEO_PATH); playVideoNative(ATTRACT_VIDEO_PATH);
#end #end
} }

View file

@ -220,7 +220,7 @@ class TitleState extends MusicBeatState
function playMenuMusic():Void function playMenuMusic():Void
{ {
var shouldFadeIn = (FlxG.sound.music == null); var shouldFadeIn:Bool = (FlxG.sound.music == null);
// Load music. Includes logic to handle BPM changes. // Load music. Includes logic to handle BPM changes.
FunkinSound.playMusic('freakyMenu', FunkinSound.playMusic('freakyMenu',
{ {
@ -229,7 +229,7 @@ class TitleState extends MusicBeatState
restartTrack: true restartTrack: true
}); });
// Fade from 0.0 to 0.7 over 4 seconds // Fade from 0.0 to 0.7 over 4 seconds
if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 0.7); if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
} }
function getIntroTextShit():Array<Array<String>> function getIntroTextShit():Array<Array<String>>
@ -290,18 +290,6 @@ class TitleState extends MusicBeatState
// do controls.PAUSE | controls.ACCEPT instead? // do controls.PAUSE | controls.ACCEPT instead?
var pressedEnter:Bool = FlxG.keys.justPressed.ENTER; var pressedEnter:Bool = FlxG.keys.justPressed.ENTER;
if (FlxG.onMobile)
{
for (touch in FlxG.touches.list)
{
if (touch.justPressed)
{
FlxG.switchState(() -> new FreeplayState());
pressedEnter = true;
}
}
}
var gamepad:FlxGamepad = FlxG.gamepads.lastActive; var gamepad:FlxGamepad = FlxG.gamepads.lastActive;
if (gamepad != null) if (gamepad != null)

View file

@ -22,10 +22,12 @@ import openfl.filters.ShaderFilter;
import openfl.utils.Assets; import openfl.utils.Assets;
import flixel.util.typeLimit.NextState; import flixel.util.typeLimit.NextState;
class LoadingState extends MusicBeatState class LoadingState extends MusicBeatSubState
{ {
inline static var MIN_TIME = 1.0; inline static var MIN_TIME = 1.0;
var asSubState:Bool = false;
var target:NextState; var target:NextState;
var playParams:Null<PlayStateParams>; var playParams:Null<PlayStateParams>;
var stopMusic:Bool = false; var stopMusic:Bool = false;
@ -178,7 +180,16 @@ class LoadingState extends MusicBeatState
FlxG.sound.music = null; FlxG.sound.music = null;
} }
FlxG.switchState(target); if (asSubState)
{
this.close();
// We will assume the target is a valid substate.
FlxG.state.openSubState(cast target);
}
else
{
FlxG.switchState(target);
}
} }
static function getSongPath():String static function getSongPath():String
@ -190,17 +201,41 @@ class LoadingState extends MusicBeatState
* Starts the transition to a new `PlayState` to start a new song. * Starts the transition to a new `PlayState` to start a new song.
* First switches to the `LoadingState` if assets need to be loaded. * First switches to the `LoadingState` if assets need to be loaded.
* @param params The parameters for the next `PlayState`. * @param params The parameters for the next `PlayState`.
* @param asSubState Whether to open as a substate rather than switching to the `PlayState`.
* @param shouldStopMusic Whether to stop the current music while loading. * @param shouldStopMusic Whether to stop the current music while loading.
*/ */
public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false):Void public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false, asSubState = false, ?onConstruct:PlayState->Void):Void
{ {
Paths.setCurrentLevel(PlayStatePlaylist.campaignId); Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
var playStateCtor:NextState = () -> new PlayState(params); var playStateCtor:() -> PlayState = function() {
return new PlayState(params);
};
if (onConstruct != null)
{
playStateCtor = function() {
var result = new PlayState(params);
onConstruct(result);
return result;
};
}
#if NO_PRELOAD_ALL #if NO_PRELOAD_ALL
// Switch to loading state while we load assets (default on HTML5 target). // Switch to loading state while we load assets (default on HTML5 target).
var loadStateCtor:NextState = () -> new LoadingState(playStateCtor, shouldStopMusic, params); var loadStateCtor = function() {
FlxG.switchState(loadStateCtor); var result = new LoadingState(playStateCtor, shouldStopMusic, params);
@:privateAccess
result.asSubState = asSubState;
return result;
}
if (asSubState)
{
FlxG.state.openSubState(cast loadStateCtor());
}
else
{
FlxG.switchState(loadStateCtor);
}
#else #else
// All assets preloaded, switch directly to play state (defualt on other targets). // All assets preloaded, switch directly to play state (defualt on other targets).
if (shouldStopMusic && FlxG.sound.music != null) if (shouldStopMusic && FlxG.sound.music != null)
@ -210,14 +245,42 @@ class LoadingState extends MusicBeatState
} }
// Load and cache the song's charts. // Load and cache the song's charts.
if (params?.targetSong != null) // Don't do this if we already provided the music and charts.
if (params?.targetSong != null && !params.overrideMusic)
{ {
params.targetSong.cacheCharts(true); params.targetSong.cacheCharts(true);
} }
var shouldPreloadLevelAssets:Bool = !(params?.minimalMode ?? false);
if (shouldPreloadLevelAssets) preloadLevelAssets();
if (asSubState)
{
FlxG.state.openSubState(cast playStateCtor());
}
else
{
FlxG.switchState(playStateCtor);
}
#end
}
#if NO_PRELOAD_ALL
static function isSoundLoaded(path:String):Bool
{
return Assets.cache.hasSound(path);
}
static function isLibraryLoaded(library:String):Bool
{
return Assets.getLibrary(library) != null;
}
#else
static function preloadLevelAssets():Void
{
// TODO: This section is a hack! Redo this later when we have a proper asset caching system. // TODO: This section is a hack! Redo this later when we have a proper asset caching system.
FunkinSprite.preparePurgeCache(); FunkinSprite.preparePurgeCache();
FunkinSprite.cacheTexture(Paths.image('combo'));
FunkinSprite.cacheTexture(Paths.image('healthBar')); FunkinSprite.cacheTexture(Paths.image('healthBar'));
FunkinSprite.cacheTexture(Paths.image('menuDesat')); FunkinSprite.cacheTexture(Paths.image('menuDesat'));
FunkinSprite.cacheTexture(Paths.image('combo')); FunkinSprite.cacheTexture(Paths.image('combo'));
@ -247,7 +310,10 @@ class LoadingState extends MusicBeatState
// List all image assets in the level's library. // List all image assets in the level's library.
// This is crude and I want to remove it when we have a proper asset caching system. // This is crude and I want to remove it when we have a proper asset caching system.
// TODO: Get rid of this junk! // TODO: Get rid of this junk!
var library = openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId); var library = PlayStatePlaylist.campaignId != null ? openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId) : null;
if (library == null) return; // We don't need to do anymore precaching.
var assets = library.list(lime.utils.AssetType.IMAGE); var assets = library.list(lime.utils.AssetType.IMAGE);
trace('Got ${assets.length} assets: ${assets}'); trace('Got ${assets.length} assets: ${assets}');
@ -278,20 +344,6 @@ class LoadingState extends MusicBeatState
// FunkinSprite.cacheAllSongTextures(stage) // FunkinSprite.cacheAllSongTextures(stage)
FunkinSprite.purgeCache(); FunkinSprite.purgeCache();
FlxG.switchState(playStateCtor);
#end
}
#if NO_PRELOAD_ALL
static function isSoundLoaded(path:String):Bool
{
return Assets.cache.hasSound(path);
}
static function isLibraryLoaded(library:String):Bool
{
return Assets.getLibrary(library) != null;
} }
#end #end

View file

@ -247,10 +247,6 @@ class StickerSubState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true; FlxTransitionableState.skipNextTransOut = true;
// TODO: Rework this asset caching stuff
FunkinSprite.preparePurgeCache();
FunkinSprite.purgeCache();
// I think this grabs the screen and puts it under the stickers? // I think this grabs the screen and puts it under the stickers?
// Leaving this commented out rather than stripping it out because it's cool... // Leaving this commented out rather than stripping it out because it's cool...
/* /*
@ -265,7 +261,15 @@ class StickerSubState extends MusicBeatSubState
// FlxG.addChildBelowMouse(dipshit); // FlxG.addChildBelowMouse(dipshit);
*/ */
FlxG.switchState(() -> targetState(this)); FlxG.switchState(() -> {
// TODO: Rework this asset caching stuff
// NOTE: This has to come AFTER the state switch,
// otherwise the game tries to render destroyed sprites!
FunkinSprite.preparePurgeCache();
FunkinSprite.purgeCache();
return targetState(this);
});
} }
}); });
}); });

View file

@ -1,5 +1,10 @@
package funkin.ui.transition.preload; package funkin.ui.transition.preload;
import openfl.filters.GlowFilter;
import openfl.display.SpreadMethod;
import openfl.display.GradientType;
import openfl.geom.Matrix;
import openfl.filters.BlurFilter;
import openfl.events.MouseEvent; import openfl.events.MouseEvent;
import flash.display.Bitmap; import flash.display.Bitmap;
import flash.display.BitmapData; import flash.display.BitmapData;
@ -46,7 +51,7 @@ class FunkinPreloader extends FlxBasePreloader
*/ */
static final BAR_PADDING:Float = 20; static final BAR_PADDING:Float = 20;
static final BAR_HEIGHT:Int = 20; static final BAR_HEIGHT:Int = 12;
/** /**
* Logo takes this long (in seconds) to fade in. * Logo takes this long (in seconds) to fade in.
@ -108,13 +113,22 @@ class FunkinPreloader extends FlxBasePreloader
#if TOUCH_HERE_TO_PLAY #if TOUCH_HERE_TO_PLAY
var touchHereToPlay:Bitmap; var touchHereToPlay:Bitmap;
#end #end
var progressBarPieces:Array<Sprite>;
var progressBar:Bitmap; var progressBar:Bitmap;
var progressLeftText:TextField; var progressLeftText:TextField;
var progressRightText:TextField; var progressRightText:TextField;
var dspText:TextField;
var enhancedText:TextField;
var stereoText:TextField;
var vfdShader:VFDOverlay;
var box:Sprite;
var progressLines:Sprite;
public function new() public function new()
{ {
super(Constants.PRELOADER_MIN_STAGE_TIME, Constants.SITE_LOCK); super(Constants.PRELOADER_MIN_STAGE_TIME);
// We can't even call trace() yet, until Flixel loads. // We can't even call trace() yet, until Flixel loads.
trace('Initializing custom preloader...'); trace('Initializing custom preloader...');
@ -146,7 +160,7 @@ class FunkinPreloader extends FlxBasePreloader
bmp.x = (this._width - bmp.width) / 2; bmp.x = (this._width - bmp.width) / 2;
bmp.y = (this._height - bmp.height) / 2; bmp.y = (this._height - bmp.height) / 2;
}); });
addChild(logo); // addChild(logo);
#if TOUCH_HERE_TO_PLAY #if TOUCH_HERE_TO_PLAY
touchHereToPlay = createBitmap(TouchHereToPlayImage, function(bmp:Bitmap) { touchHereToPlay = createBitmap(TouchHereToPlayImage, function(bmp:Bitmap) {
@ -160,16 +174,48 @@ class FunkinPreloader extends FlxBasePreloader
addChild(touchHereToPlay); addChild(touchHereToPlay);
#end #end
var amountOfPieces:Int = 16;
progressBarPieces = [];
var maxBarWidth = this._width - BAR_PADDING * 2;
var pieceWidth = maxBarWidth / amountOfPieces;
var pieceGap:Int = 8;
progressLines = new openfl.display.Sprite();
progressLines.graphics.lineStyle(2, Constants.COLOR_PRELOADER_BAR);
progressLines.graphics.drawRect(-2, 480, this._width + 4, 30);
addChild(progressLines);
var progressBarPiece = new Sprite();
progressBarPiece.graphics.beginFill(Constants.COLOR_PRELOADER_BAR);
progressBarPiece.graphics.drawRoundRect(0, 0, pieceWidth - pieceGap, BAR_HEIGHT, 4, 4);
progressBarPiece.graphics.endFill();
for (i in 0...amountOfPieces)
{
var piece = new Sprite();
piece.graphics.beginFill(Constants.COLOR_PRELOADER_BAR);
piece.graphics.drawRoundRect(0, 0, pieceWidth - pieceGap, BAR_HEIGHT, 4, 4);
piece.graphics.endFill();
piece.x = i * (piece.width + pieceGap);
piece.y = this._height - BAR_PADDING - BAR_HEIGHT - 200;
addChild(piece);
progressBarPieces.push(piece);
}
// Create the progress bar. // Create the progress bar.
progressBar = new Bitmap(new BitmapData(1, BAR_HEIGHT, true, Constants.COLOR_PRELOADER_BAR)); // progressBar = new Bitmap(new BitmapData(1, BAR_HEIGHT, true, Constants.COLOR_PRELOADER_BAR));
progressBar.x = BAR_PADDING; // progressBar.x = BAR_PADDING;
progressBar.y = this._height - BAR_PADDING - BAR_HEIGHT; // progressBar.y = this._height - BAR_PADDING - BAR_HEIGHT;
addChild(progressBar); // addChild(progressBar);
// Create the progress message. // Create the progress message.
progressLeftText = new TextField(); progressLeftText = new TextField();
dspText = new TextField();
enhancedText = new TextField();
stereoText = new TextField();
var progressLeftTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true); var progressLeftTextFormat = new TextFormat("DS-Digital", 32, Constants.COLOR_PRELOADER_BAR, true);
progressLeftTextFormat.align = TextFormatAlign.LEFT; progressLeftTextFormat.align = TextFormatAlign.LEFT;
progressLeftText.defaultTextFormat = progressLeftTextFormat; progressLeftText.defaultTextFormat = progressLeftTextFormat;
@ -177,13 +223,14 @@ class FunkinPreloader extends FlxBasePreloader
progressLeftText.width = this._width - BAR_PADDING * 2; progressLeftText.width = this._width - BAR_PADDING * 2;
progressLeftText.text = 'Downloading assets...'; progressLeftText.text = 'Downloading assets...';
progressLeftText.x = BAR_PADDING; progressLeftText.x = BAR_PADDING;
progressLeftText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4; progressLeftText.y = this._height - BAR_PADDING - BAR_HEIGHT - 290;
// progressLeftText.shader = new VFDOverlay();
addChild(progressLeftText); addChild(progressLeftText);
// Create the progress %. // Create the progress %.
progressRightText = new TextField(); progressRightText = new TextField();
var progressRightTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true); var progressRightTextFormat = new TextFormat("DS-Digital", 16, Constants.COLOR_PRELOADER_BAR, true);
progressRightTextFormat.align = TextFormatAlign.RIGHT; progressRightTextFormat.align = TextFormatAlign.RIGHT;
progressRightText.defaultTextFormat = progressRightTextFormat; progressRightText.defaultTextFormat = progressRightTextFormat;
@ -193,6 +240,60 @@ class FunkinPreloader extends FlxBasePreloader
progressRightText.x = BAR_PADDING; progressRightText.x = BAR_PADDING;
progressRightText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4; progressRightText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4;
addChild(progressRightText); addChild(progressRightText);
box = new Sprite();
box.graphics.beginFill(Constants.COLOR_PRELOADER_BAR, 1);
box.graphics.drawRoundRect(0, 0, 64, 20, 5, 5);
box.graphics.drawRoundRect(70, 0, 58, 20, 5, 5);
box.graphics.endFill();
box.graphics.beginFill(Constants.COLOR_PRELOADER_BAR, 0.1);
box.graphics.drawRoundRect(0, 0, 128, 20, 5, 5);
box.graphics.endFill();
box.x = 880;
box.y = 440;
addChild(box);
dspText.selectable = false;
dspText.textColor = 0x000000;
dspText.width = this._width;
dspText.height = 20;
dspText.text = 'DSP';
dspText.x = 10;
dspText.y = -5;
box.addChild(dspText);
enhancedText.selectable = false;
enhancedText.textColor = Constants.COLOR_PRELOADER_BAR;
enhancedText.width = this._width;
enhancedText.height = 100;
enhancedText.text = 'ENHANCED';
enhancedText.x = -100;
enhancedText.y = 0;
box.addChild(enhancedText);
stereoText.selectable = false;
stereoText.textColor = Constants.COLOR_PRELOADER_BAR;
stereoText.width = this._width;
stereoText.height = 100;
stereoText.text = 'STEREO';
stereoText.x = 0;
stereoText.y = -40;
box.addChild(stereoText);
// var dummyMatrix:openfl.geom.Matrix = new Matrix();
// dummyMatrix.createGradientBox(this._width, this._height * 0.1, 90 * Math.PI / 180);
// var gradient:Sprite = new Sprite();
// gradient.graphics.beginGradientFill(GradientType.LINEAR, [0xFFFFFF, 0x000000], [1, 1], [0, 255], dummyMatrix, SpreadMethod.REFLECT);
// gradient.graphics.drawRect(0, 0, this._width, this._height);
// gradient.graphics.endFill();
// addChild(gradient);
var vfdBitmap:Bitmap = new Bitmap(new BitmapData(this._width, this._height, true, 0xFFFFFFFF));
addChild(vfdBitmap);
vfdShader = new VFDOverlay();
vfdBitmap.shader = vfdShader;
} }
var lastElapsed:Float = 0.0; var lastElapsed:Float = 0.0;
@ -200,6 +301,8 @@ class FunkinPreloader extends FlxBasePreloader
override function update(percent:Float):Void override function update(percent:Float):Void
{ {
var elapsed:Float = (Date.now().getTime() - this._startTime) / 1000.0; var elapsed:Float = (Date.now().getTime() - this._startTime) / 1000.0;
vfdShader.update(elapsed * 100);
// trace('Time since last frame: ' + (lastElapsed - elapsed)); // trace('Time since last frame: ' + (lastElapsed - elapsed));
downloadingAssetsPercent = percent; downloadingAssetsPercent = percent;
@ -748,12 +851,19 @@ class FunkinPreloader extends FlxBasePreloader
else else
{ {
renderLogoFadeIn(elapsed); renderLogoFadeIn(elapsed);
// Render progress bar
var maxWidth = this._width - BAR_PADDING * 2;
var barWidth = maxWidth * percent;
var piecesToRender:Int = Std.int(percent * progressBarPieces.length);
for (i => piece in progressBarPieces)
{
piece.alpha = i <= piecesToRender ? 0.9 : 0.1;
}
} }
// Render progress bar // progressBar.width = barWidth;
var maxWidth = this._width - BAR_PADDING * 2;
var barWidth = maxWidth * percent;
progressBar.width = barWidth;
// Cycle ellipsis count to show loading // Cycle ellipsis count to show loading
var ellipsisCount:Int = Std.int(elapsed / ELLIPSIS_TIME) % 3 + 1; var ellipsisCount:Int = Std.int(elapsed / ELLIPSIS_TIME) % 3 + 1;
@ -766,29 +876,29 @@ class FunkinPreloader extends FlxBasePreloader
{ {
// case FunkinPreloaderState.NotStarted: // case FunkinPreloaderState.NotStarted:
default: default:
updateProgressLeftText('Loading (0/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Loading \n0/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.DownloadingAssets: case FunkinPreloaderState.DownloadingAssets:
updateProgressLeftText('Downloading assets (1/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Downloading assets \n1/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.PreloadingPlayAssets: case FunkinPreloaderState.PreloadingPlayAssets:
updateProgressLeftText('Preloading assets (2/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Preloading assets \n2/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.InitializingScripts: case FunkinPreloaderState.InitializingScripts:
updateProgressLeftText('Initializing scripts (3/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Initializing scripts \n3/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.CachingGraphics: case FunkinPreloaderState.CachingGraphics:
updateProgressLeftText('Caching graphics (4/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Caching graphics \n4/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.CachingAudio: case FunkinPreloaderState.CachingAudio:
updateProgressLeftText('Caching audio (5/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Caching audio \n5/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.CachingData: case FunkinPreloaderState.CachingData:
updateProgressLeftText('Caching data (6/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Caching data \n6/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingSpritesheets: case FunkinPreloaderState.ParsingSpritesheets:
updateProgressLeftText('Parsing spritesheets (7/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Parsing spritesheets \n7/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingStages: case FunkinPreloaderState.ParsingStages:
updateProgressLeftText('Parsing stages (8/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Parsing stages \n8/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingCharacters: case FunkinPreloaderState.ParsingCharacters:
updateProgressLeftText('Parsing characters (9/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Parsing characters \n9/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingSongs: case FunkinPreloaderState.ParsingSongs:
updateProgressLeftText('Parsing songs (10/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Parsing songs \n10/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.Complete: case FunkinPreloaderState.Complete:
updateProgressLeftText('Finishing up ($TOTAL_STEPS/$TOTAL_STEPS)$ellipsis'); updateProgressLeftText('Finishing up \n$TOTAL_STEPS/$TOTAL_STEPS $ellipsis');
#if TOUCH_HERE_TO_PLAY #if TOUCH_HERE_TO_PLAY
case FunkinPreloaderState.TouchHereToPlay: case FunkinPreloaderState.TouchHereToPlay:
updateProgressLeftText(null); updateProgressLeftText(null);
@ -815,10 +925,21 @@ class FunkinPreloader extends FlxBasePreloader
else if (progressLeftText.text != text) else if (progressLeftText.text != text)
{ {
// We have to keep updating the text format, because the font can take a frame or two to load. // We have to keep updating the text format, because the font can take a frame or two to load.
var progressLeftTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true); var progressLeftTextFormat = new TextFormat("DS-Digital", 32, Constants.COLOR_PRELOADER_BAR, true);
progressLeftTextFormat.align = TextFormatAlign.LEFT; progressLeftTextFormat.align = TextFormatAlign.LEFT;
progressLeftText.defaultTextFormat = progressLeftTextFormat; progressLeftText.defaultTextFormat = progressLeftTextFormat;
progressLeftText.text = text; progressLeftText.text = text;
dspText.defaultTextFormat = new TextFormat("Quantico", 20, 0x000000, false);
dspText.text = 'DSP\t\t\t\t\tFNF'; // fukin dum....
dspText.textColor = 0x000000;
enhancedText.defaultTextFormat = new TextFormat("Inconsolata Black", 16, Constants.COLOR_PRELOADER_BAR, false);
enhancedText.text = 'ENHANCED';
enhancedText.textColor = Constants.COLOR_PRELOADER_BAR;
stereoText.defaultTextFormat = new TextFormat("Inconsolata Bold", 36, Constants.COLOR_PRELOADER_BAR, false);
stereoText.text = 'NATURAL STEREO';
} }
} }
} }
@ -845,9 +966,17 @@ class FunkinPreloader extends FlxBasePreloader
logo.y = (this._height - logo.height) / 2; logo.y = (this._height - logo.height) / 2;
// Fade out progress bar too. // Fade out progress bar too.
progressBar.alpha = logo.alpha; // progressBar.alpha = logo.alpha;
progressLeftText.alpha = logo.alpha; progressLeftText.alpha = logo.alpha;
progressRightText.alpha = logo.alpha; progressRightText.alpha = logo.alpha;
box.alpha = logo.alpha;
dspText.alpha = logo.alpha;
enhancedText.alpha = logo.alpha;
stereoText.alpha = logo.alpha;
progressLines.alpha = logo.alpha;
for (piece in progressBarPieces)
piece.alpha = logo.alpha;
return elapsedFinished; return elapsedFinished;
} }
@ -901,8 +1030,8 @@ class FunkinPreloader extends FlxBasePreloader
{ {
// Ensure the graphics are properly destroyed and GC'd. // Ensure the graphics are properly destroyed and GC'd.
removeChild(logo); removeChild(logo);
removeChild(progressBar); // removeChild(progressBar);
logo = progressBar = null; logo = null;
super.destroy(); super.destroy();
} }

View file

@ -0,0 +1,70 @@
package funkin.ui.transition.preload;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import openfl.display.GraphicsShader;
class VFDOverlay extends GraphicsShader
{
public var elapsedTime(default, set):Float = 0;
function set_elapsedTime(value:Float):Float
{
u_time.value = [value];
return value;
}
@:glFragmentSource('#pragma header
const vec2 s = vec2(1, 1.7320508);
uniform float u_time;
float rand(float co) { return fract(sin(co*(91.3458)) * 47453.5453); }
float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); }
void main(void) {
vec4 col = texture2D (bitmap, openfl_TextureCoordv);
vec2 game_res = vec2(1280.0, 720.0);
const float tileAmount = 10.;
vec2 uv = (2. * openfl_TextureCoordv.xy * -1.);
uv *= 50.;
vec4 hexCenter = floor(vec4(uv, uv - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
vec4 offset = vec4(uv - hexCenter.xy * s, uv - (hexCenter.zw + 0.5) * s) + 0.0;
vec4 hexInfo = dot(offset.xy, offset.xy) < dot(offset.zw, offset.zw) ? vec4(offset.xy, hexCenter.xy) : vec4(offset.zw, hexCenter.zw);
// Distance to the nearest edge of a hexagon
vec2 p = abs(hexInfo.xy) ;
float edgeDist = max(dot(p, normalize(vec2(1.0, sqrt(3.0)))), p.x);
float edgeWidth = 0.05 * tileAmount; // Adjust edge width based on tile amount
float edgeSharpness = 0.011 * tileAmount;
float outline = smoothstep(edgeWidth - edgeSharpness, edgeWidth, edgeDist);
float color_mix = mix(0.0, 0.3, outline); // Mix black outline with white fill
float flicker = (sin(u_time) * 0.05) + 1.0;
float sinshit = smoothstep(-3.0, 1.0, sin(uv.y * 3.));
col = vec4(vec3(0.0), color_mix);
col = mix(col, vec4(0., 0., 0., sinshit), 0.5 * flicker);
float specs = rand(uv.xy);
vec4 noise = vec4(0., 0., 0., specs);
col = mix(col, noise, 0.1);
gl_FragColor = col;
}
')
public function new()
{
super();
this.elapsedTime = 0;
}
public function update(elapsed:Float):Void
{
this.elapsedTime += elapsed;
}
}

View file

@ -60,6 +60,11 @@ class Constants
*/ */
// ============================== // ==============================
/**
* Link to buy merch for the game.
*/
public static final URL_MERCH:String = 'https://needlejuicerecords.com/pages/friday-night-funkin';
/** /**
* Preloader sitelock. * Preloader sitelock.
* Matching is done by `FlxStringUtil.getDomain`, so any URL on the domain will work. * Matching is done by `FlxStringUtil.getDomain`, so any URL on the domain will work.
@ -135,7 +140,7 @@ class Constants
/** /**
* Color for the preloader progress bar * Color for the preloader progress bar
*/ */
public static final COLOR_PRELOADER_BAR:FlxColor = 0xFF00FF00; public static final COLOR_PRELOADER_BAR:FlxColor = 0xFFA4FF11;
/** /**
* Color for the preloader site lock background * Color for the preloader site lock background
@ -181,6 +186,12 @@ class Constants
*/ */
public static final DEFAULT_DIFFICULTY_LIST:Array<String> = ['easy', 'normal', 'hard']; public static final DEFAULT_DIFFICULTY_LIST:Array<String> = ['easy', 'normal', 'hard'];
/**
* List of all difficulties used by the base game.
* Includes Erect and Nightmare.
*/
public static final DEFAULT_DIFFICULTY_LIST_FULL:Array<String> = ['easy', 'normal', 'hard', 'erect', 'nightmare'];
/** /**
* Default player character for charts. * Default player character for charts.
*/ */
@ -347,7 +358,7 @@ class Constants
* The progress bare is automatically rescaled to match. * The progress bare is automatically rescaled to match.
*/ */
#if debug #if debug
public static final PRELOADER_MIN_STAGE_TIME:Float = 1.0; public static final PRELOADER_MIN_STAGE_TIME:Float = 0.0;
#else #else
public static final PRELOADER_MIN_STAGE_TIME:Float = 0.1; public static final PRELOADER_MIN_STAGE_TIME:Float = 0.1;
#end #end
@ -515,4 +526,10 @@ class Constants
* The vertical offset of the strumline from the top edge of the screen. * The vertical offset of the strumline from the top edge of the screen.
*/ */
public static final STRUMLINE_Y_OFFSET:Float = 24; public static final STRUMLINE_Y_OFFSET:Float = 24;
/**
* The rate at which the camera lerps to its target.
* 0.04 = 4% of distance per frame.
*/
public static final DEFAULT_CAMERA_FOLLOW_RATE:Float = 0.04;
} }

View file

@ -0,0 +1,17 @@
package funkin.util;
class EaseUtil
{
/**
* Returns an ease function that eases via steps.
* Useful for "retro" style fades (week 6!)
* @param steps how many steps to ease over
* @return Float->Float
*/
public static inline function stepped(steps:Int):Float->Float
{
return function(t:Float):Float {
return Math.floor(t * steps) / steps;
}
}
}

View file

@ -63,6 +63,31 @@ class SerializerUtil
} }
} }
public static function initSerializer():Void
{
haxe.Unserializer.DEFAULT_RESOLVER = new FunkinTypeResolver();
}
/**
* Serialize a Haxe object using the built-in Serializer.
* @param input The object to serialize
* @return The serialized object as a string
*/
public static function fromHaxeObject(input:Dynamic):String
{
return haxe.Serializer.run(input);
}
/**
* Convert a serialized Haxe object back into a Haxe object.
* @param input The serialized object as a string
* @return The deserialized object
*/
public static function toHaxeObject(input:String):Dynamic
{
return haxe.Unserializer.run(input);
}
/** /**
* Customize how certain types are serialized when converting to JSON. * Customize how certain types are serialized when converting to JSON.
*/ */
@ -90,3 +115,26 @@ class SerializerUtil
return result; return result;
} }
} }
class FunkinTypeResolver
{
public function new()
{
// Blank constructor.
}
public function resolveClass(name:String):Class<Dynamic>
{
if (name == 'Dynamic')
{
FlxG.log.warn('Found invalid class type in save data, indicates partial save corruption.');
return null;
}
return Type.resolveClass(name);
};
public function resolveEnum(name:String):Enum<Dynamic>
{
return Type.resolveEnum(name);
};
}

View file

@ -0,0 +1,136 @@
package funkin.util;
import funkin.util.tools.MapTools;
import haxe.DynamicAccess;
/**
* Utilities for working with anonymous structures.
*/
class StructureUtil
{
/**
* Merge two structures, with the second overwriting the first.
* Performs a SHALLOW clone, where child structures are not merged.
* @param a The base structure.
* @param b The new structure.
* @return The merged structure.
*/
public static function merge(a:Dynamic, b:Dynamic):Dynamic
{
var result:DynamicAccess<Dynamic> = Reflect.copy(a);
for (field in Reflect.fields(b))
{
result.set(field, Reflect.field(b, field));
}
return result;
}
public static function toMap(a:Dynamic):haxe.ds.Map<String, Dynamic>
{
var result:haxe.ds.Map<String, Dynamic> = [];
for (field in Reflect.fields(a))
{
result.set(field, Reflect.field(a, field));
}
return result;
}
public static function isMap(a:Dynamic):Bool
{
return Std.isOfType(a, haxe.Constraints.IMap);
}
public static function isObject(a:Dynamic):Bool
{
switch (Type.typeof(a))
{
case TObject:
return true;
default:
return false;
}
}
public static function isPrimitive(a:Dynamic):Bool
{
switch (Type.typeof(a))
{
case TInt | TFloat | TBool:
return true;
case TClass(c):
return false;
case TEnum(e):
return false;
case TObject:
return false;
case TFunction:
return false;
case TNull:
return true;
case TUnknown:
return false;
default:
return false;
}
}
/**
* Merge two structures, with the second overwriting the first.
* Performs a DEEP clone, where child structures are also merged recursively.
* @param a The base structure.
* @param b The new structure.
* @return The merged structure.
*/
public static function deepMerge(a:Dynamic, b:Dynamic):Dynamic
{
if (a == null) return b;
if (b == null) return null;
if (isPrimitive(a) && isPrimitive(b)) return b;
if (isMap(b))
{
if (isMap(a))
{
return MapTools.merge(a, b);
}
else
{
return StructureUtil.toMap(a).merge(b);
}
}
if (!Reflect.isObject(a) || !Reflect.isObject(b)) return b;
if (Std.isOfType(b, haxe.ds.StringMap))
{
if (Std.isOfType(a, haxe.ds.StringMap))
{
return MapTools.merge(a, b);
}
else
{
return StructureUtil.toMap(a).merge(b);
}
}
var result:DynamicAccess<Dynamic> = Reflect.copy(a);
for (field in Reflect.fields(b))
{
if (Reflect.isObject(b))
{
// Note that isObject also returns true for class instances,
// but we just assume that's not a problem here.
result.set(field, deepMerge(Reflect.field(result, field), Reflect.field(b, field)));
}
else
{
// If we're here, b[field] is a primitive.
result.set(field, Reflect.field(b, field));
}
}
return result;
}
}

View file

@ -33,6 +33,24 @@ class MapTools
return map.copy(); return map.copy();
} }
/**
* Create a new map which is a combination of the two given maps.
* @param a The base map.
* @param b The other map. The values from this take precedence.
* @return The combined map.
*/
public static function merge<K, T>(a:Map<K, T>, b:Map<K, T>):Map<K, T>
{
var result = a.copy();
for (pair in b.keyValueIterator())
{
result.set(pair.key, pair.value);
}
return result;
}
/** /**
* Create a new array with clones of all elements of the given array, to prevent modifying the original. * Create a new array with clones of all elements of the given array, to prevent modifying the original.
*/ */