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
.github
.vscode
Project.xmlartassets
build
hmm.json
source
Main.hx
funkin
Highscore.hxInitState.hxPaths.hx
audio
data
graphics
input
modding
play
save
ui
util

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
description: "installs Butler, and uploads to itch.io!"
inputs:
butler-key:
description: "Butler API secret key"
required: true
itch-repo:
description: "Where to upload the game to"
required: true
default: "ninja-muffin24/funkin-secret"
build-dir:
description: "Directory of the game build"
required: true
required: false
target:
description: "Target (html5, win, linux, mac)"
description: "Target (html5, windows, linux, macos)"
required: true
runs:
using: "composite"
steps:
- name: Install butler Windows
if: runner.os == 'Windows'
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default
7z x butler.zip
./butler -v
shell: bash
- name: Install butler Mac
if: runner.os == 'macOS'
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default
unzip butler.zip
./butler -V
shell: bash
- name: Install butler Linux
if: runner.os == 'Linux'
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
unzip butler.zip
chmod +x butler
./butler -V
shell: bash
- name: Upload game to itch.io
env:
BUTLER_API_KEY: ${{inputs.butler-key}}
run: |
./butler login
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME}
shell: bash
# RUNNER_OS = Windows | macOS | Linux
# TARGET_BUILD = windows | macos | linux
# TARGET_ITCH = win | macos | linux
# TARGET_BUTLER_DOWNLOAD = windows-amd64 | darwin-amd64 | linux-amd64
- name: Setup variables
shell: bash
run: |
TARGET_OS=${{ inputs.target }}
RUNNER=${RUNNER_OS@L}
TARGET=${TARGET_OS@L}
case "$TARGET" in
"windows")
TARGET_ITCH=win
;;
*)
TARGET_ITCH=$TARGET
;;
esac
case "$RUNNER" in
"macos")
OS_NODE=darwin
;;
*)
OS_NODE=$RUNNER
;;
esac
BUTLER_PATH=$RUNNER_TEMP/butler
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",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "Linux / Debug",
"target": "linux",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug",
"target": "hl",
@ -130,6 +135,11 @@
"-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)",
"target": "hl",
@ -160,6 +170,11 @@
"target": "windows",
"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)",
"target": "hl",

View file

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

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

View file

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

View file

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

View file

@ -261,6 +261,35 @@ class InitState extends FlxState
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(
{
targetSong: songData,
@ -283,6 +312,10 @@ class InitState extends FlxState
return;
}
// TODO: Rework loading behavior so we don't have to do this.
Paths.setCurrentLevel(levelId);
PlayStatePlaylist.campaignId = levelId;
PlayStatePlaylist.playlistSongIds = currentLevel.getSongs();
PlayStatePlaylist.isStoryMode = true;
PlayStatePlaylist.campaignScore = 0;

View file

@ -9,7 +9,7 @@ import openfl.utils.Assets as OpenFlAssets;
*/
class Paths
{
static var currentLevel:String;
static var currentLevel:Null<String> = null;
public static function setCurrentLevel(name:String):Void
{
@ -113,7 +113,7 @@ class Paths
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

View file

@ -223,11 +223,12 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
// already paused before we lost focus.
if (_lostFocus && !_alreadyPaused)
{
trace('Resuming audio (${this._label}) on focus!');
resume();
}
else
{
trace('Not resuming audio on focus!');
trace('Not resuming audio (${this._label}) on focus!');
}
_lostFocus = false;
}
@ -265,10 +266,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
@:allow(flixel.sound.FlxSoundGroup)
override function updateTransform():Void
{
_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 (_transform != null)
{
_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
@ -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)
{
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);
if (music != null)
{
@ -391,10 +398,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
sound._label = 'unknown';
}
if (autoPlay) sound.play();
sound.volume = volume;
sound.group = FlxG.sound.defaultSoundGroup;
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
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.
*/
public function stop()
public function stop():Void
{
if (members != null)
{
@ -160,7 +160,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
}
}
public override function destroy()
public override function destroy():Void
{
stop();
super.destroy();
@ -178,9 +178,14 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_time():Float
{
if (getFirstAlive() != null) return getFirstAlive().time;
if (getFirstAlive() != null)
{
return getFirstAlive().time;
}
else
{
return 0;
}
}
function set_time(time:Float):Float
@ -195,16 +200,26 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_playing():Bool
{
if (getFirstAlive() != null) return getFirstAlive().playing;
if (getFirstAlive() != null)
{
return getFirstAlive().playing;
}
else
{
return false;
}
}
function get_volume():Float
{
if (getFirstAlive() != null) return getFirstAlive().volume;
if (getFirstAlive() != null)
{
return getFirstAlive().volume;
}
else
{
return 1;
}
}
// 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
{
playerVoices.destroy();
playerVoices = null;
opponentVoices.destroy();
opponentVoices = null;
if (playerVoices != null)
{
playerVoices.destroy();
playerVoices = null;
}
if (opponentVoices != null)
{
opponentVoices.destroy();
opponentVoices = null;
}
super.destroy();
}
}

View file

@ -8,153 +8,180 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import funkin.util.MathUtil;
import funkVis.dsp.SpectralAnalyzer;
import funkVis.audioclip.frontends.LimeAudioClip;
using Lambda;
class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
{
public var vis:VisShit;
// public var vis:VisShit;
var analyzer:SpectralAnalyzer;
var volumes:Array<Float> = [];
public var snd:FlxSound;
public function new(snd:FlxSound)
{
super();
vis = new VisShit(snd);
this.snd = snd;
// vis = new VisShit(snd);
// vis.snd = snd;
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)
{
// pushes initial value
volumes.push(0.0);
var sum = function(num:Float, total:Float) return total += num;
var posX:Float = positionX.slice(0, lol).fold(sum, 0);
var posY:Float = positionY.slice(0, lol).fold(sum, 0);
var viz:FlxSprite = new FlxSprite(50 * lol, 0);
var viz:FlxSprite = new FlxSprite(posX, posY);
viz.frames = visFrms;
add(viz);
var visStr = 'VIZ';
if (lol == 5) visStr = 'viz'; // lol makes it lowercase, accomodates for art that I dont wanna rename!
var visStr = 'viz';
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)
{
// updateViz();
updateFFT(elapsed);
// updateFFT(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)
{
var remappedShit:Int = 0;
animFrame = Math.floor(Math.min(5, animFrame));
animFrame = Math.floor(Math.max(0, animFrame));
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples));
else
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples));
animFrame = Std.int(Math.abs(animFrame - 5)); // shitty dumbass flip, cuz dave got da shit backwards lol!
var fftSamples:Array<Float> = [];
var swagBucks = remappedShit;
for (i in remappedShit...remappedShit + (Std.int((44100 * (1 / 144)))))
{
var left = vis.audioData[swagBucks] / 32767;
var right = vis.audioData[swagBucks + 1] / 32767;
var balanced = (left + right) / 2;
swagBucks += 2;
fftSamples.push(balanced);
}
var freqShit = vis.funnyFFT(fftSamples);
for (i in 0...group.members.length)
{
var getSliceShit = function(s:Int) {
var powShit = FlxMath.remapToRange(s, 0, group.members.length, 0, MathUtil.logBase(10, freqShit[0].length));
return Math.round(Math.pow(10, powShit));
};
// var powShit:Float = getSliceShit(i);
var hzSliced:Int = getSliceShit(i);
var sliceLength:Int = Std.int(freqShit[0].length / group.members.length);
var volSlice = freqShit[0].slice(hzSliced, getSliceShit(i + 1));
var avgVel:Float = 0;
for (slice in volSlice)
{
avgVel += slice;
}
avgVel /= volSlice.length;
avgVel *= 10000000;
volumes[i] += avgVel - (elapsed * (volumes[i] * 50));
var animFrame:Int = Std.int(volumes[i]);
animFrame = Math.floor(Math.min(5, animFrame));
animFrame = Math.floor(Math.max(0, animFrame));
animFrame = Std.int(Math.abs(animFrame - 5)); // shitty dumbass flip, cuz dave got da shit backwards lol!
group.members[i].animation.curAnim.curFrame = animFrame;
if (FlxG.keys.justPressed.U)
{
trace(avgVel);
trace(group.members[i].animation.curAnim.curFrame);
}
}
// group.members[0].animation.curAnim.curFrame =
}
group.members[i].animation.curAnim.curFrame = animFrame;
}
}
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;
}
}
}
}
// function updateFFT(elapsed:Float)
// {
// if (vis.snd != null)
// {
// vis.checkAndSetBuffer();
// if (vis.setBuffer)
// {
// var remappedShit:Int = 0;
// if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples));
// else
// remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples));
// var fftSamples:Array<Float> = [];
// var swagBucks = remappedShit;
// for (i in remappedShit...remappedShit + (Std.int((44100 * (1 / 144)))))
// {
// var left = vis.audioData[swagBucks] / 32767;
// var right = vis.audioData[swagBucks + 1] / 32767;
// var balanced = (left + right) / 2;
// swagBucks += 2;
// fftSamples.push(balanced);
// }
// var freqShit = vis.funnyFFT(fftSamples);
// for (i in 0...group.members.length)
// {
// var getSliceShit = function(s:Int) {
// var powShit = FlxMath.remapToRange(s, 0, group.members.length, 0, MathUtil.logBase(10, freqShit[0].length));
// return Math.round(Math.pow(10, powShit));
// };
// // var powShit:Float = getSliceShit(i);
// var hzSliced:Int = getSliceShit(i);
// var sliceLength:Int = Std.int(freqShit[0].length / group.members.length);
// var volSlice = freqShit[0].slice(hzSliced, getSliceShit(i + 1));
// var avgVel:Float = 0;
// for (slice in volSlice)
// {
// avgVel += slice;
// }
// avgVel /= volSlice.length;
// avgVel *= 10000000;
// volumes[i] += avgVel - (elapsed * (volumes[i] * 50));
// var animFrame:Int = Std.int(volumes[i]);
// animFrame = Math.floor(Math.min(5, animFrame));
// animFrame = Math.floor(Math.max(0, animFrame));
// animFrame = Std.int(Math.abs(animFrame - 5)); // shitty dumbass flip, cuz dave got da shit backwards lol!
// group.members[i].animation.curAnim.curFrame = animFrame;
// if (FlxG.keys.justPressed.U)
// {
// trace(avgVel);
// trace(group.members[i].animation.curAnim.curFrame);
// }
// }
// // group.members[0].animation.curAnim.curFrame =
// }
// }
// }
// public function updateViz()
// {
// if (vis.snd != null)
// {
// var remappedShit:Int = 0;
// vis.checkAndSetBuffer();
// if (vis.setBuffer)
// {
// // var startingSample:Int = Std.int(FlxMath.remapToRange)
// if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples));
// for (i in 0...group.members.length)
// {
// var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, remappedShit, remappedShit + 500));
// var left = vis.audioData[sampleApprox] / 32767;
// var animFrame:Int = Std.int(FlxMath.remapToRange(left, -1, 1, 0, 6));
// group.members[i].animation.curAnim.curFrame = animFrame;
// }
// }
// }
// }
}

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;
import funkin.data.animation.AnimationData;
/**
* A type definition for the data for an album of songs.
* It includes things like what graphics to display in Freeplay.
@ -33,4 +35,11 @@ typedef AlbumData =
* The album title will be displayed below the album art in Freeplay.
*/
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();
}
function loadEntryMetadataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
function loadEntryMetadataFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? 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};
}
function loadMusicDataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
function loadMusicDataFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
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);
}
function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? 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 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);
this.id = id;
bgTexture = pickTexture(width, height);
bgBitmap = FixedBitmapData.fromTexture(bgTexture);
bgFrame = new FlxFrame(new FlxGraphic('', null));

View file

@ -137,7 +137,8 @@ class FlxAtlasSprite extends FlxAnimate
anim.callback = function(_, frame:Int) {
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)
{

View file

@ -20,6 +20,6 @@ class GaussianBlurShader extends FlxRuntimeShader
public function setAmount(value:Float):Void
{
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
{
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
{
this.setFloat('hue', value);
this.setFloat('_hue', value);
this.hue = value;
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.
* Uses FlxActions to funnel various inputs to a single action.
*/
var _ui_up = new FlxActionDigital(Action.UI_UP);
var _ui_left = new FlxActionDigital(Action.UI_LEFT);
var _ui_right = new FlxActionDigital(Action.UI_RIGHT);
var _ui_down = new FlxActionDigital(Action.UI_DOWN);
var _ui_upP = new FlxActionDigital(Action.UI_UP_P);
var _ui_leftP = new FlxActionDigital(Action.UI_LEFT_P);
var _ui_rightP = new FlxActionDigital(Action.UI_RIGHT_P);
var _ui_downP = new FlxActionDigital(Action.UI_DOWN_P);
var _ui_upR = new FlxActionDigital(Action.UI_UP_R);
var _ui_leftR = new FlxActionDigital(Action.UI_LEFT_R);
var _ui_rightR = new FlxActionDigital(Action.UI_RIGHT_R);
var _ui_downR = new FlxActionDigital(Action.UI_DOWN_R);
var _note_up = new FlxActionDigital(Action.NOTE_UP);
var _note_left = new FlxActionDigital(Action.NOTE_LEFT);
var _note_right = new FlxActionDigital(Action.NOTE_RIGHT);
var _note_down = new FlxActionDigital(Action.NOTE_DOWN);
var _note_upP = new FlxActionDigital(Action.NOTE_UP_P);
var _note_leftP = new FlxActionDigital(Action.NOTE_LEFT_P);
var _note_rightP = new FlxActionDigital(Action.NOTE_RIGHT_P);
var _note_downP = new FlxActionDigital(Action.NOTE_DOWN_P);
var _note_upR = new FlxActionDigital(Action.NOTE_UP_R);
var _note_leftR = new FlxActionDigital(Action.NOTE_LEFT_R);
var _note_rightR = new FlxActionDigital(Action.NOTE_RIGHT_R);
var _note_downR = new FlxActionDigital(Action.NOTE_DOWN_R);
var _accept = new FlxActionDigital(Action.ACCEPT);
var _back = new FlxActionDigital(Action.BACK);
var _pause = new FlxActionDigital(Action.PAUSE);
var _reset = new FlxActionDigital(Action.RESET);
var _screenshot = new FlxActionDigital(Action.SCREENSHOT);
var _cutscene_advance = new FlxActionDigital(Action.CUTSCENE_ADVANCE);
var _debug_menu = new FlxActionDigital(Action.DEBUG_MENU);
var _debug_chart = new FlxActionDigital(Action.DEBUG_CHART);
var _debug_stage = new FlxActionDigital(Action.DEBUG_STAGE);
var _volume_up = new FlxActionDigital(Action.VOLUME_UP);
var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN);
var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE);
var _ui_up = new FunkinAction(Action.UI_UP);
var _ui_left = new FunkinAction(Action.UI_LEFT);
var _ui_right = new FunkinAction(Action.UI_RIGHT);
var _ui_down = new FunkinAction(Action.UI_DOWN);
var _ui_upP = new FunkinAction(Action.UI_UP_P);
var _ui_leftP = new FunkinAction(Action.UI_LEFT_P);
var _ui_rightP = new FunkinAction(Action.UI_RIGHT_P);
var _ui_downP = new FunkinAction(Action.UI_DOWN_P);
var _ui_upR = new FunkinAction(Action.UI_UP_R);
var _ui_leftR = new FunkinAction(Action.UI_LEFT_R);
var _ui_rightR = new FunkinAction(Action.UI_RIGHT_R);
var _ui_downR = new FunkinAction(Action.UI_DOWN_R);
var _note_up = new FunkinAction(Action.NOTE_UP);
var _note_left = new FunkinAction(Action.NOTE_LEFT);
var _note_right = new FunkinAction(Action.NOTE_RIGHT);
var _note_down = new FunkinAction(Action.NOTE_DOWN);
var _note_upP = new FunkinAction(Action.NOTE_UP_P);
var _note_leftP = new FunkinAction(Action.NOTE_LEFT_P);
var _note_rightP = new FunkinAction(Action.NOTE_RIGHT_P);
var _note_downP = new FunkinAction(Action.NOTE_DOWN_P);
var _note_upR = new FunkinAction(Action.NOTE_UP_R);
var _note_leftR = new FunkinAction(Action.NOTE_LEFT_R);
var _note_rightR = new FunkinAction(Action.NOTE_RIGHT_R);
var _note_downR = new FunkinAction(Action.NOTE_DOWN_R);
var _accept = new FunkinAction(Action.ACCEPT);
var _back = new FunkinAction(Action.BACK);
var _pause = new FunkinAction(Action.PAUSE);
var _reset = new FunkinAction(Action.RESET);
var _screenshot = new FunkinAction(Action.SCREENSHOT);
var _cutscene_advance = new FunkinAction(Action.CUTSCENE_ADVANCE);
var _debug_menu = new FunkinAction(Action.DEBUG_MENU);
var _debug_chart = new FunkinAction(Action.DEBUG_CHART);
var _debug_stage = new FunkinAction(Action.DEBUG_STAGE);
var _volume_up = new FunkinAction(Action.VOLUME_UP);
var _volume_down = new FunkinAction(Action.VOLUME_DOWN);
var _volume_mute = new FunkinAction(Action.VOLUME_MUTE);
var byName:Map<String, FlxActionDigital> = new Map<String, FlxActionDigital>();
var byName:Map<String, FunkinAction> = new Map<String, FunkinAction>();
public var gamepadsAdded:Array<Int> = [];
public var keyboardScheme = KeyboardScheme.None;
@ -75,122 +75,142 @@ class Controls extends FlxActionSet
public var UI_UP(get, never):Bool;
inline function get_UI_UP()
return _ui_up.check();
return _ui_up.checkPressed();
public var UI_LEFT(get, never):Bool;
inline function get_UI_LEFT()
return _ui_left.check();
return _ui_left.checkPressed();
public var UI_RIGHT(get, never):Bool;
inline function get_UI_RIGHT()
return _ui_right.check();
return _ui_right.checkPressed();
public var UI_DOWN(get, never):Bool;
inline function get_UI_DOWN()
return _ui_down.check();
return _ui_down.checkPressed();
public var UI_UP_P(get, never):Bool;
inline function get_UI_UP_P()
return _ui_upP.check();
return _ui_up.checkJustPressed();
public var UI_LEFT_P(get, never):Bool;
inline function get_UI_LEFT_P()
return _ui_leftP.check();
return _ui_left.checkJustPressed();
public var UI_RIGHT_P(get, never):Bool;
inline function get_UI_RIGHT_P()
return _ui_rightP.check();
return _ui_right.checkJustPressed();
public var UI_DOWN_P(get, never):Bool;
inline function get_UI_DOWN_P()
return _ui_downP.check();
return _ui_down.checkJustPressed();
public var UI_UP_R(get, never):Bool;
inline function get_UI_UP_R()
return _ui_upR.check();
return _ui_up.checkJustReleased();
public var UI_LEFT_R(get, never):Bool;
inline function get_UI_LEFT_R()
return _ui_leftR.check();
return _ui_left.checkJustReleased();
public var UI_RIGHT_R(get, never):Bool;
inline function get_UI_RIGHT_R()
return _ui_rightR.check();
return _ui_right.checkJustReleased();
public var UI_DOWN_R(get, never):Bool;
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;
inline function get_NOTE_UP()
return _note_up.check();
return _note_up.checkPressed();
public var NOTE_LEFT(get, never):Bool;
inline function get_NOTE_LEFT()
return _note_left.check();
return _note_left.checkPressed();
public var NOTE_RIGHT(get, never):Bool;
inline function get_NOTE_RIGHT()
return _note_right.check();
return _note_right.checkPressed();
public var NOTE_DOWN(get, never):Bool;
inline function get_NOTE_DOWN()
return _note_down.check();
return _note_down.checkPressed();
public var NOTE_UP_P(get, never):Bool;
inline function get_NOTE_UP_P()
return _note_upP.check();
return _note_up.checkJustPressed();
public var NOTE_LEFT_P(get, never):Bool;
inline function get_NOTE_LEFT_P()
return _note_leftP.check();
return _note_left.checkJustPressed();
public var NOTE_RIGHT_P(get, never):Bool;
inline function get_NOTE_RIGHT_P()
return _note_rightP.check();
return _note_right.checkJustPressed();
public var NOTE_DOWN_P(get, never):Bool;
inline function get_NOTE_DOWN_P()
return _note_downP.check();
return _note_down.checkJustPressed();
public var NOTE_UP_R(get, never):Bool;
inline function get_NOTE_UP_R()
return _note_upR.check();
return _note_up.checkJustReleased();
public var NOTE_LEFT_R(get, never):Bool;
inline function get_NOTE_LEFT_R()
return _note_leftR.check();
return _note_left.checkJustReleased();
public var NOTE_RIGHT_R(get, never):Bool;
inline function get_NOTE_RIGHT_R()
return _note_rightR.check();
return _note_right.checkJustReleased();
public var NOTE_DOWN_R(get, never):Bool;
inline function get_NOTE_DOWN_R()
return _note_downR.check();
return _note_down.checkJustReleased();
public var ACCEPT(get, never):Bool;
@ -260,26 +280,10 @@ class Controls extends FlxActionSet
add(_ui_left);
add(_ui_right);
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_left);
add(_note_right);
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(_back);
add(_pause);
@ -293,8 +297,16 @@ class Controls extends FlxActionSet
add(_volume_down);
add(_volume_mute);
for (action in digitalActions)
byName[action.name] = action;
for (action in digitalActions) {
if (Std.isOfType(action, FunkinAction)) {
var funkinAction:FunkinAction = cast action;
byName[funkinAction.name] = funkinAction;
if (funkinAction.namePressed != null)
byName[funkinAction.namePressed] = funkinAction;
if (funkinAction.nameReleased != null)
byName[funkinAction.nameReleased] = funkinAction;
}
}
if (scheme == null)
scheme = None;
@ -307,14 +319,17 @@ class Controls extends FlxActionSet
super.update();
}
// inline
public function checkByName(name:Action):Bool
public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool
{
#if debug
if (!byName.exists(name))
throw 'Invalid name: $name';
#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> {
@ -405,36 +420,36 @@ class Controls extends FlxActionSet
{
case UI_UP:
func(_ui_up, PRESSED);
func(_ui_upP, JUST_PRESSED);
func(_ui_upR, JUST_RELEASED);
func(_ui_up, JUST_PRESSED);
func(_ui_up, JUST_RELEASED);
case UI_LEFT:
func(_ui_left, PRESSED);
func(_ui_leftP, JUST_PRESSED);
func(_ui_leftR, JUST_RELEASED);
func(_ui_left, JUST_PRESSED);
func(_ui_left, JUST_RELEASED);
case UI_RIGHT:
func(_ui_right, PRESSED);
func(_ui_rightP, JUST_PRESSED);
func(_ui_rightR, JUST_RELEASED);
func(_ui_right, JUST_PRESSED);
func(_ui_right, JUST_RELEASED);
case UI_DOWN:
func(_ui_down, PRESSED);
func(_ui_downP, JUST_PRESSED);
func(_ui_downR, JUST_RELEASED);
func(_ui_down, JUST_PRESSED);
func(_ui_down, JUST_RELEASED);
case NOTE_UP:
func(_note_up, PRESSED);
func(_note_upP, JUST_PRESSED);
func(_note_upR, JUST_RELEASED);
func(_note_up, JUST_PRESSED);
func(_note_up, JUST_RELEASED);
case NOTE_LEFT:
func(_note_left, PRESSED);
func(_note_leftP, JUST_PRESSED);
func(_note_leftR, JUST_RELEASED);
func(_note_left, JUST_PRESSED);
func(_note_left, JUST_RELEASED);
case NOTE_RIGHT:
func(_note_right, PRESSED);
func(_note_rightP, JUST_PRESSED);
func(_note_rightR, JUST_RELEASED);
func(_note_right, JUST_PRESSED);
func(_note_right, JUST_RELEASED);
case NOTE_DOWN:
func(_note_down, PRESSED);
func(_note_downP, JUST_PRESSED);
func(_note_downR, JUST_RELEASED);
func(_note_down, JUST_PRESSED);
func(_note_down, JUST_RELEASED);
case ACCEPT:
func(_accept, JUST_PRESSED);
case BACK:
@ -1042,6 +1057,173 @@ typedef Swipes =
?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
{
var touchMap:Map<Int, Swipes> = new Map();
@ -1229,8 +1411,7 @@ enum Control
DEBUG_STAGE;
}
enum
abstract Action(String) to String from String
enum abstract Action(String) to String from String
{
// NOTE
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 {
assetLibraryPaths: [
'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'tutorial' => 'tutorial', 'week1' => 'week1', 'week2' => 'week2',
'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1',
'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'videos' => 'videos', 'tutorial' => 'tutorial', 'week1' => 'week1',
'week2' => 'week2', 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1',
],
coreAssetRedirect: CORE_FOLDER,
}

View file

@ -8,7 +8,12 @@ import funkin.modding.IScriptedClass;
*/
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;

View file

@ -3,18 +3,18 @@ package funkin.play;
import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import funkin.audio.FunkinSound;
import flixel.input.touch.FlxTouch;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.PlayState;
import funkin.util.MathUtil;
import funkin.ui.freeplay.FreeplayState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.story.StoryMenuState;
import funkin.util.MathUtil;
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.
*/
@:nullSafety
class GameOverSubState extends MusicBeatSubState
{
/**
* The currently active GameOverSubState.
* 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.
@ -37,7 +38,7 @@ class GameOverSubState extends MusicBeatSubState
* For example, playing a different animation when BF dies in Week 4
* or Pico dies in Weekend 1.
*/
public static var animationSuffix:String = "";
public static var animationSuffix:String = '';
/**
* 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`
* 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.
*/
public static var blueBallSuffix:String = "";
public static var blueBallSuffix:String = '';
static var blueballed:Bool = false;
/**
* The boyfriend character.
*/
var boyfriend:BaseCharacter;
var boyfriend:Null<BaseCharacter> = null;
/**
* The invisible object in the scene which the camera focuses on.
@ -82,7 +85,8 @@ class GameOverSubState extends MusicBeatSubState
var transparent:Bool;
final CAMERA_ZOOM_DURATION:Float = 0.5;
static final CAMERA_ZOOM_DURATION:Float = 0.5;
var targetCameraZoom:Float = 1.0;
public function new(params:GameOverParams)
@ -91,6 +95,8 @@ class GameOverSubState extends MusicBeatSubState
this.isChartingMode = params?.isChartingMode ?? false;
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 = '';
musicSuffix = '';
blueBallSuffix = '';
blueballed = false;
}
override public function create():Void
public override function create():Void
{
if (instance != null)
{
// 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;
@ -121,7 +128,7 @@ class GameOverSubState extends MusicBeatSubState
var playState = PlayState.instance;
// 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,
// but it's normally opaque.
bg.alpha = transparent ? 0.25 : 1.0;
@ -138,21 +145,10 @@ class GameOverSubState extends MusicBeatSubState
boyfriend.isDead = true;
add(boyfriend);
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
//
@ -161,6 +157,27 @@ class GameOverSubState extends MusicBeatSubState
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
{
// Apply camera zoom level from stage data.
@ -175,7 +192,7 @@ class GameOverSubState extends MusicBeatSubState
{
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.
playBlueBalledSFX();
@ -205,10 +222,10 @@ class GameOverSubState extends MusicBeatSubState
// MOBILE ONLY: Restart the level when tapping Boyfriend.
if (FlxG.onMobile)
{
var touch = FlxG.touches.getFirst();
var touch:FlxTouch = FlxG.touches.getFirst();
if (touch != null)
{
if (touch.overlaps(boyfriend))
if (boyfriend == null || touch.overlaps(boyfriend))
{
confirmDeath();
}
@ -228,7 +245,7 @@ class GameOverSubState extends MusicBeatSubState
blueballed = false;
PlayState.instance.deathCounter = 0;
// PlayState.seenCutscene = false; // old thing...
gameOverMusic.stop();
if (gameOverMusic != null) gameOverMusic.stop();
if (isChartingMode)
{
@ -238,11 +255,11 @@ class GameOverSubState extends MusicBeatSubState
}
else if (PlayStatePlaylist.isStoryMode)
{
FlxG.switchState(() -> new StoryMenuState());
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
}
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.
Conductor.instance.update(gameOverMusic.time);
}
else
else if (boyfriend != null)
{
if (PlayState.instance.isMinimalMode)
{
@ -299,7 +316,7 @@ class GameOverSubState extends MusicBeatSubState
isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
if (PlayState.instance.isMinimalMode) {}
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else
{
boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
@ -313,7 +330,7 @@ class GameOverSubState extends MusicBeatSubState
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.instance.needsReset = true;
if (PlayState.instance.isMinimalMode) {}
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else
{
// 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);
@ -345,11 +362,11 @@ class GameOverSubState extends MusicBeatSubState
*/
function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null<String>
{
var basePath = 'gameplay/gameover/gameOver';
if (starting) basePath += 'Start';
else if (ending) basePath += 'End';
var basePath:String = 'gameplay/gameover/gameOver';
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)
{
suffix = suffix.split('-').slice(0, -1).join('-');
@ -362,23 +379,26 @@ class GameOverSubState extends MusicBeatSubState
/**
* 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
{
var musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
var onComplete = null;
var musicPath:Null<String> = resolveMusicPath(musicSuffix, isStarting, isEnding);
var onComplete:() -> Void = () -> {};
if (isStarting)
{
if (musicPath == null)
{
// Looked for starting music and didn't find it. Use middle music instead.
isStarting = false;
musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
}
else
{
onComplete = function() {
isStarting = false;
isStarting = true;
// We need to force to ensure that the non-starting music plays.
startDeathMusic(1.0, true);
};
@ -387,13 +407,16 @@ class GameOverSubState extends MusicBeatSubState
if (musicPath == null)
{
trace('Could not find game over music!');
FlxG.log.warn('[GAMEOVER] Could not find game over music at path ($musicPath)!');
return;
}
else if (gameOverMusic == null || !gameOverMusic.playing || force)
{
if (gameOverMusic != null) gameOverMusic.stop();
gameOverMusic = FunkinSound.load(musicPath);
if (gameOverMusic == null) return;
gameOverMusic.volume = startingVolume;
gameOverMusic.looped = !(isEnding || isStarting);
gameOverMusic.onComplete = onComplete;
@ -406,13 +429,11 @@ class GameOverSubState extends MusicBeatSubState
}
}
static var blueballed:Bool = false;
/**
* Play the sound effect that occurs when
* boyfriend's testicles get utterly annihilated.
*/
public static function playBlueBalledSFX()
public static function playBlueBalledSFX():Void
{
blueballed = true;
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.
* TODO: Make this a module somehow.
*/
function playJeffQuote()
function playJeffQuote():Void
{
var randomCensor:Array<Int> = [];
@ -446,20 +467,27 @@ class GameOverSubState extends MusicBeatSubState
});
}
public override function destroy()
public override function destroy():Void
{
super.destroy();
if (gameOverMusic != null) gameOverMusic.stop();
gameOverMusic = null;
if (gameOverMusic != null)
{
gameOverMusic.stop();
gameOverMusic = null;
}
blueballed = false;
instance = null;
}
public override function toString():String
{
return "GameOverSubState";
return 'GameOverSubState';
}
}
/**
* Parameters used to instantiate a GameOverSubState.
*/
typedef GameOverParams =
{
var isChartingMode:Bool;

View file

@ -12,6 +12,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.audio.FunkinSound;
import funkin.data.song.SongRegistry;
import funkin.ui.freeplay.FreeplayState;
import funkin.graphics.FunkinSprite;
import funkin.play.cutscene.VideoCutscene;
import funkin.play.PlayState;
@ -72,8 +73,8 @@ class PauseSubState extends MusicBeatSubState
*/
static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array<PauseMenuEntry> = [
{text: 'Resume', callback: resume},
{text: 'Restart Cutscene', callback: restartVideoCutscene},
{text: 'Skip Cutscene', callback: skipVideoCutscene},
{text: 'Restart Cutscene', callback: restartVideoCutscene},
{text: 'Exit to Menu', callback: quitToMenu},
];
@ -440,7 +441,7 @@ class PauseSubState extends MusicBeatSubState
var entries:Array<PauseMenuEntry> = [];
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}');
for (difficulty in difficultiesInVariation)
{
@ -567,6 +568,8 @@ class PauseSubState extends MusicBeatSubState
PlayStatePlaylist.campaignDifficulty = difficulty;
PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
FreeplayState.rememberedDifficulty = difficulty;
PlayState.instance.needsReset = true;
state.close();
@ -658,7 +661,7 @@ class PauseSubState extends MusicBeatSubState
}
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.
*/
var comboPopUps:PopUpStuff;
public var comboPopUps:PopUpStuff;
/**
* PROPERTIES
@ -736,6 +736,10 @@ class PlayState extends MusicBeatSubState
#end
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
@ -830,9 +834,12 @@ class PlayState extends MusicBeatSubState
inputSpitter = [];
// Reset music properly.
FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
FlxG.sound.music.pitch = playbackRate;
FlxG.sound.music.pause();
if (FlxG.sound.music != null)
{
FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
FlxG.sound.music.pitch = playbackRate;
FlxG.sound.music.pause();
}
if (!overrideMusic)
{
@ -848,7 +855,7 @@ class PlayState extends MusicBeatSubState
vocals.pause();
vocals.time = 0;
FlxG.sound.music.volume = 1;
if (FlxG.sound.music != null) FlxG.sound.music.volume = 1;
vocals.volume = 1;
vocals.playerVolume = 1;
vocals.opponentVolume = 1;
@ -866,7 +873,7 @@ class PlayState extends MusicBeatSubState
// Reset camera zooming
cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY;
hudCameraZoomIntensity = 0.015 * 2.0;
hudCameraZoomIntensity = (cameraBopIntensity - 1.0) * 2.0;
cameraZoomRate = Constants.DEFAULT_ZOOM_RATE;
health = Constants.HEALTH_STARTING;
@ -962,7 +969,7 @@ class PlayState extends MusicBeatSubState
if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN;
// 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
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('health', health);
FlxG.watch.addQuick('cameraBopIntensity', cameraBopIntensity);
// TODO: Add a song event for Handle GF dance speed.
@ -1462,7 +1470,7 @@ class PlayState extends MusicBeatSubState
*/
function initCameras():Void
{
camGame = new FunkinCamera();
camGame = new FunkinCamera('playStateCamGame');
camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage.
camHUD = new FlxCamera();
camHUD.bgColor.alpha = 0; // Show the game scene behind the camera.
@ -1543,10 +1551,11 @@ class PlayState extends MusicBeatSubState
function loadStage(id:String):Void
{
currentStage = StageRegistry.instance.fetchEntry(id);
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
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.
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentStage, event);
@ -1728,8 +1737,6 @@ class PlayState extends MusicBeatSubState
playerStrumline.fadeInArrows();
opponentStrumline.fadeInArrows();
}
this.refresh();
}
/**
@ -1912,8 +1919,6 @@ class PlayState extends MusicBeatSubState
*/
function startSong():Void
{
dispatchEvent(new ScriptEvent(SONG_START));
startingSong = false;
if (!overrideMusic && !isGamePaused && currentChart != null)
@ -1935,7 +1940,7 @@ class PlayState extends MusicBeatSubState
// Prevent the volume from being wrong.
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...');
add(vocals);
@ -1954,6 +1959,8 @@ class PlayState extends MusicBeatSubState
// FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
handleSkippedNotes();
}
dispatchEvent(new ScriptEvent(SONG_START));
}
/**
@ -2230,8 +2237,8 @@ class PlayState extends MusicBeatSubState
holdNote.handledMiss = true;
// Mute vocals and play miss animation, but don't penalize.
vocals.playerVolume = 0;
if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
// vocals.playerVolume = 0;
// 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.
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)
{
// Break the combo.
Highscore.tallies.combo = comboPopUps.displayCombo(0);
if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
Highscore.tallies.combo = 0;
}
if (playSound)
@ -2576,32 +2577,38 @@ class PlayState extends MusicBeatSubState
*/
function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void
{
vocals.playerVolume = 1;
if (daRating == 'miss')
{
// 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.
comboPopUps.displayRating('miss');
// comboPopUps.displayRating('miss');
return;
}
vocals.playerVolume = 1;
var isComboBreak = false;
switch (daRating)
{
case 'sick':
Highscore.tallies.sick += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
case 'good':
Highscore.tallies.good += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
case 'bad':
Highscore.tallies.bad += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
case 'shit':
Highscore.tallies.shit += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
default:
FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!');
}
health += healthChange;
@ -2609,18 +2616,18 @@ class PlayState extends MusicBeatSubState
if (isComboBreak)
{
// 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
{
Highscore.tallies.combo++;
Highscore.tallies.totalNotesHit++;
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
}
playerStrumline.hitNote(daNote, !isComboBreak);
if (daRating == "sick")
if (daRating == 'sick')
{
playerStrumline.playNoteSplash(daNote.noteData.getDirection());
}
@ -2666,6 +2673,13 @@ class PlayState extends MusicBeatSubState
}
comboPopUps.displayRating(daRating);
if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo);
if (daNote.isHoldNote && daNote.holdNoteSprite != null)
{
playerStrumline.playNoteHoldCover(daNote.holdNoteSprite);
}
vocals.playerVolume = 1;
}
/**
@ -2732,7 +2746,7 @@ class PlayState extends MusicBeatSubState
*/
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;
mayPauseGame = false;
@ -2750,6 +2764,8 @@ class PlayState extends MusicBeatSubState
deathCounter = 0;
var isNewHighscore = false;
if (currentSong != null && currentSong.validScore)
{
// 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)
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);
#if newgrounds
NGio.postScore(score, currentSong.id);
#end
isNewHighscore = true;
}
}
if (PlayStatePlaylist.isStoryMode)
{
isNewHighscore = false;
PlayStatePlaylist.campaignScore += songScore;
// Pop the next song ID from the list.
@ -2793,18 +2812,6 @@ class PlayState extends MusicBeatSubState
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)
{
NGio.unlockMedal(60961);
@ -2834,6 +2841,7 @@ class PlayState extends MusicBeatSubState
#if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end
isNewHighscore = true;
}
}
@ -2845,11 +2853,11 @@ class PlayState extends MusicBeatSubState
{
if (rightGoddamnNow)
{
moveToResultsScreen();
moveToResultsScreen(isNewHighscore);
}
else
{
zoomIntoResultsScreen();
zoomIntoResultsScreen(isNewHighscore);
}
}
}
@ -2862,7 +2870,7 @@ class PlayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
FlxG.sound.music.stop();
if (FlxG.sound.music != null) FlxG.sound.music.stop();
vocals.stop();
// TODO: Softcode this cutscene.
@ -2910,11 +2918,11 @@ class PlayState extends MusicBeatSubState
{
if (rightGoddamnNow)
{
moveToResultsScreen();
moveToResultsScreen(isNewHighscore);
}
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.
*/
function zoomIntoResultsScreen():Void
function zoomIntoResultsScreen(isNewHighscore:Bool):Void
{
trace('WENT TO RESULTS SCREEN!');
@ -3045,7 +3053,7 @@ class PlayState extends MusicBeatSubState
{
ease: FlxEase.expoIn,
onComplete: function(_) {
moveToResultsScreen();
moveToResultsScreen(isNewHighscore);
}
});
});
@ -3054,7 +3062,7 @@ class PlayState extends MusicBeatSubState
/**
* Move to the results screen right goddamn now.
*/
function moveToResultsScreen():Void
function moveToResultsScreen(isNewHighscore:Bool):Void
{
persistentUpdate = false;
vocals.stop();
@ -3066,7 +3074,24 @@ class PlayState extends MusicBeatSubState
{
storyMode: PlayStatePlaylist.isStoryMode,
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;
openSubState(res);
@ -3208,7 +3233,10 @@ class PlayState extends MusicBeatSubState
// Don't go back in time to before the song started.
targetTimeMs = Math.max(0, targetTimeMs);
FlxG.sound.music.time = targetTimeMs;
if (FlxG.sound.music != null)
{
FlxG.sound.music.time = targetTimeMs;
}
handleSkippedNotes();
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.
*/
@:nullSafety
class PlayStatePlaylist
{
/**
* 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.
@ -31,8 +32,9 @@ class PlayStatePlaylist
/**
* 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;
@ -45,7 +47,7 @@ class PlayStatePlaylist
playlistSongIds = [];
campaignScore = 0;
campaignTitle = 'UNKNOWN';
campaignId = 'unknown';
campaignId = null;
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;
import funkin.util.MathUtil;
import funkin.ui.story.StoryMenuState;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxSprite;
@ -10,12 +11,15 @@ import flixel.math.FlxPoint;
import funkin.ui.MusicBeatSubState;
import flixel.math.FlxRect;
import flixel.text.FlxBitmapText;
import funkin.ui.freeplay.FreeplayScore;
import flixel.tweens.FlxEase;
import funkin.ui.freeplay.FreeplayState;
import flixel.tweens.FlxTween;
import funkin.audio.FunkinSound;
import flixel.util.FlxGradient;
import flixel.util.FlxTimer;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.graphics.shaders.LeftMaskShader;
import funkin.play.components.TallyCounter;
@ -42,12 +46,15 @@ class ResultState extends MusicBeatSubState
override function create():Void
{
if (params.tallies.sick == params.tallies.totalNotesHit
&& params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT;
else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50)
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else
resultsVariation = NORMAL;
/*
if (params.scoreData.sick == params.scoreData.totalNotesHit
&& params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT;
else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50)
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else
resultsVariation = NORMAL;
*/
resultsVariation = NORMAL;
FunkinSound.playMusic('results$resultsVariation',
{
@ -73,34 +80,34 @@ class ResultState extends MusicBeatSubState
bgFlash.visible = false;
add(bgFlash);
var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared"));
bfGfExcellent.visible = false;
add(bfGfExcellent);
// var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared"));
// bfGfExcellent.visible = false;
// add(bfGfExcellent);
//
// var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared"));
// bfPerfect.visible = false;
// add(bfPerfect);
//
// var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
// bfSHIT.visible = false;
// add(bfSHIT);
//
// bfGfExcellent.anim.onComplete = () -> {
// bfGfExcellent.anim.curFrame = 28;
// bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
//
// bfPerfect.anim.onComplete = () -> {
// bfPerfect.anim.curFrame = 136;
// bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
//
// bfSHIT.anim.onComplete = () -> {
// bfSHIT.anim.curFrame = 150;
// bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared"));
bfPerfect.visible = false;
add(bfPerfect);
var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
bfSHIT.visible = false;
add(bfSHIT);
bfGfExcellent.anim.onComplete = () -> {
bfGfExcellent.anim.curFrame = 28;
bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
bfPerfect.anim.onComplete = () -> {
bfPerfect.anim.curFrame = 136;
bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
bfSHIT.anim.onComplete = () -> {
bfSHIT.anim.curFrame = 150;
bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
var gf:FlxSprite = FunkinSprite.createSparrow(500, 300, 'resultScreen/resultGirlfriendGOOD');
var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD');
gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
gf.visible = false;
gf.animation.finishCallback = _ -> {
@ -130,12 +137,16 @@ class ResultState extends MusicBeatSubState
var diffSpr:String = switch (PlayState.instance.currentDifficulty)
{
case 'EASY':
case 'easy':
'difEasy';
case 'NORMAL':
case 'normal':
'difNormal';
case 'HARD':
case 'hard':
'difHard';
case 'erect':
'difErect';
case 'nightmare':
'difNightmare';
case _:
'difNormal';
}
@ -178,7 +189,7 @@ class ResultState extends MusicBeatSubState
scorePopin.visible = false;
add(scorePopin);
var highscoreNew:FlxSprite = new FlxSprite(280, 580);
var highscoreNew:FlxSprite = new FlxSprite(310, 570);
highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew");
highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24);
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.
*
*/
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);
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);
hStuf += 2;
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);
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);
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);
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);
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);
var score:ResultScore = new ResultScore(35, 305, 10, params.scoreData.score);
score.visible = false;
add(score);
for (ind => rating in ratingGrp.members)
{
rating.visible = false;
@ -233,18 +248,29 @@ class ResultState extends MusicBeatSubState
ratingsPopin.animation.finishCallback = anim -> {
scorePopin.animation.play("score");
scorePopin.animation.finishCallback = anim -> {
score.visible = true;
score.animateNumbers();
};
scorePopin.visible = true;
highscoreNew.visible = true;
highscoreNew.animation.play("new");
FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
if (params.isNewHighscore)
{
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)
{
case SHIT:
bfSHIT.visible = true;
bfSHIT.playAnimation("");
// case SHIT:
// bfSHIT.visible = true;
// bfSHIT.playAnimation("");
case NORMAL:
boyfriend.animation.play('fall');
@ -266,9 +292,9 @@ class ResultState extends MusicBeatSubState
gf.animation.play('clap', true);
gf.visible = true;
});
case PERFECT:
bfPerfect.visible = true;
bfPerfect.playAnimation("");
// case PERFECT:
// bfPerfect.visible = true;
// bfPerfect.playAnimation("");
// bfGfExcellent.visible = true;
// bfGfExcellent.playAnimation("");
@ -276,8 +302,6 @@ class ResultState extends MusicBeatSubState
}
});
if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
super.create();
}
@ -365,7 +389,7 @@ class ResultState extends MusicBeatSubState
}
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;
/**
* Whether the displayed score is a new highscore
*/
var isNewHighscore:Bool;
/**
* 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))
{
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;
}
animations.set(anim.name, anim);

View file

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

View file

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

View file

@ -67,8 +67,13 @@ class VideoCutscene
if (!openfl.Assets.exists(filePath))
{
// Display a popup.
lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
return;
// lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
// 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);

View file

@ -23,6 +23,7 @@ import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.modding.IScriptedClass.IEventHandler;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.util.SortUtil;
import funkin.util.EaseUtil;
/**
* A high-level handler for dialogue.
@ -179,7 +180,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
if (backdropData.fadeTime > 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
{
@ -403,6 +404,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
type: ONESHOT, // holy shit like the game no way
startDelay: 0,
onComplete: (_) -> endOutro(),
ease: EaseUtil.stepped(8)
});
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');
if (intensity == null) intensity = 1.0;
PlayState.instance.cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY * intensity;
PlayState.instance.hudCameraZoomIntensity = 1.015 * intensity * 2.0;
PlayState.instance.cameraBopIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity + 1.0;
PlayState.instance.hudCameraZoomIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity * 2.0;
PlayState.instance.cameraZoomRate = rate;
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
{
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

View file

@ -295,6 +295,11 @@ class Strumline extends FlxSpriteGroup
{
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 hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS;
var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS;
@ -365,8 +370,6 @@ class Strumline extends FlxSpriteGroup
// Hold note is offscreen, kill it.
holdNote.visible = false;
holdNote.kill(); // Do not destroy! Recycling is faster.
// The cover will see this and clean itself up.
}
else if (holdNote.hitNote && holdNote.sustainLength <= 0)
{
@ -380,10 +383,16 @@ class Strumline extends FlxSpriteGroup
playStatic(holdNote.noteDirection);
}
if (holdNote.cover != null)
if (holdNote.cover != null && isPlayer)
{
holdNote.cover.playEnd();
}
else if (holdNote.cover != null)
{
// *lightning* *zap* *crackle*
holdNote.cover.visible = false;
holdNote.cover.kill();
}
holdNote.visible = false;
holdNote.kill();
@ -405,6 +414,13 @@ class Strumline extends FlxSpriteGroup
{
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)
{
@ -822,7 +838,7 @@ class Strumline extends FlxSpriteGroup
{
// The note sprite pool is full and all note splashes are active.
// We have to create a new note.
result = new SustainTrail(0, 100, noteStyle);
result = new SustainTrail(0, 0, noteStyle);
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 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.
*
* @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 (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)
{
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;
}

View file

@ -9,12 +9,13 @@ import funkin.save.migrator.SaveDataMigrator;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import thx.semver.Version;
import funkin.util.SerializerUtil;
@:nullSafety
class Save
{
// 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";
// 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>
{
if (data.scores?.levels == null)
{
if (data.scores == null)
{
data.scores =
{
songs: [],
levels: []
};
}
else
{
data.scores.levels = [];
}
}
var level = data.scores.levels.get(levelId);
if (level == null)
{
@ -641,6 +658,9 @@ class Save
{
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);
if (FlxG.save.isEmpty())
@ -650,9 +670,9 @@ class Save
if (legacySaveData != null)
{
trace('[SAVE] Found legacy save data, converting...');
var gameSave = SaveDataMigrator.migrate(legacySaveData);
var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData);
@:privateAccess
FlxG.save.mergeData(gameSave.data);
FlxG.save.mergeData(gameSave.data, true);
}
else
{
@ -664,7 +684,7 @@ class Save
trace('[SAVE] Loaded save data.');
@:privateAccess
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...");
var legacySave:FlxSave = new FlxSave();
legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY);
if (legacySave?.data == null)
if (legacySave.isEmpty())
{
trace("[SAVE] No legacy save data found.");
return null;

View file

@ -3,6 +3,7 @@ package funkin.save.migrator;
import funkin.save.Save;
import funkin.save.migrator.RawSaveData_v1_0_0;
import thx.semver.Version;
import funkin.util.StructureUtil;
import funkin.util.VersionUtil;
@:nullSafety
@ -26,7 +27,7 @@ class SaveDataMigrator
if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
{
// Simply import the structured data.
var save:Save = new Save(inputData);
var save:Save = new Save(StructureUtil.deepMerge(Save.getDefault(), inputData));
return save;
}
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.FlxSprite;
import flixel.FlxSubState;
import flixel.graphics.FlxGraphic;
import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.graphics.FunkinCamera;
import flixel.group.FlxSpriteGroup;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
import flixel.input.mouse.FlxMouseEvent;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.sound.FlxSound;
import flixel.system.debug.log.LogStyle;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.text.FlxText;
@ -26,28 +26,23 @@ import flixel.util.FlxSort;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VoicesGroup;
import funkin.audio.waveform.WaveformSprite;
import funkin.data.notestyle.NoteStyleRegistry;
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.SongEventData;
import funkin.data.song.SongData.SongEventData;
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.SongOffsets;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongRegistry;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageData;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite;
import funkin.input.Cursor;
import funkin.input.TurboActionHandler;
import funkin.input.TurboButtonHandler;
import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent;
import funkin.play.character.BaseCharacter.CharacterType;
@ -56,13 +51,12 @@ import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.components.HealthIcon;
import funkin.play.notes.NoteSprite;
import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.save.Save;
import funkin.ui.debug.charting.commands.AddEventsCommand;
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.CopyItemsCommand;
import funkin.ui.debug.charting.commands.CutItemsCommand;
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.ChartEditorHoldNoteSprite;
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.ChartEditorNoteSprite;
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.HaxeUIState;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.transition.LoadingState;
import funkin.util.Constants;
import funkin.util.FileUtil;
import funkin.util.logging.CrashHandler;
@ -119,7 +115,6 @@ import haxe.ui.containers.Grid;
import haxe.ui.containers.HBox;
import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.ScrollView;
@ -130,7 +125,6 @@ import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.focus.FocusManager;
import haxe.ui.Toolkit;
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);
// Offset the selection box start position, if we are dragging.
if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff;
// Update the note preview viewport box.
// Update the note preview.
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
refreshNotePreviewPlayheadPosition();
// Update the measure tick display.
if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0;
return this.scrollPositionInPixels;
@ -473,6 +470,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Move the playhead sprite to the correct position.
gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS;
updatePlayheadGhostHoldNotes();
refreshNotePreviewPlayheadPosition();
return this.playheadPositionInPixels;
}
@ -779,6 +779,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
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
/**
@ -809,6 +816,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
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.
*/
@ -1081,6 +1094,66 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
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
*/
@ -1959,10 +2032,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
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;
/**
* 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.
*/
@ -1980,6 +2058,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
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.
* 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();
uiCamera = new FunkinCamera();
uiCamera = new FunkinCamera('chartEditorUI');
FlxG.cameras.reset(uiCamera);
buildDefaultSongData();
@ -2359,7 +2443,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
gridGhostHoldNote = new ChartEditorHoldNoteSprite(this);
gridGhostHoldNote.alpha = 0.6;
gridGhostHoldNote.noteData = new SongNoteData(0, 0, 0, "");
gridGhostHoldNote.noteData = null;
gridGhostHoldNote.visible = false;
add(gridGhostHoldNote);
gridGhostHoldNote.zIndex = 11;
@ -2433,6 +2517,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(notePreviewViewport);
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());
}
@ -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.
*/
@ -3025,6 +3125,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
function setupTurboKeyHandlers():Void
{
// Keyboard shortcuts
add(undoKeyHandler);
add(redoKeyHandler);
add(upKeyHandler);
@ -3033,6 +3134,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(sKeyHandler);
add(pageUpKeyHandler);
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
if (upKeyHandler.activated && currentLiveInputStyle == None)
{
scrollAmount = -GRID_SIZE * 0.25 * 25.0;
scrollAmount = -GRID_SIZE * 4;
shouldPause = true;
}
// Down Arrow = Scroll Down
if (downKeyHandler.activated && currentLiveInputStyle == None)
{
scrollAmount = GRID_SIZE * 0.25 * 25.0;
scrollAmount = GRID_SIZE * 4;
shouldPause = true;
}
// W = Scroll Up (doesn't work with Ctrl+Scroll)
if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = -GRID_SIZE * 0.25 * 25.0;
scrollAmount = -GRID_SIZE * 4;
shouldPause = true;
}
// S = Scroll Down (doesn't work with Ctrl+Scroll)
if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = GRID_SIZE * 0.25 * 25.0;
scrollAmount = GRID_SIZE * 4;
shouldPause = true;
}
// PAGE UP = Jump up to nearest measure
if (pageUpKeyHandler.activated)
// GAMEPAD LEFT STICK UP = Scroll Up by 1 note snap
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 playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
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;
}
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;
}
if (playbarButtonPressed == 'playbarBack')
{
playbarButtonPressed = '';
scrollAmount = -GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
funcJumpUp(false);
shouldPause = true;
}
// PAGE DOWN = Jump down to nearest measure
if (pageDownKeyHandler.activated)
{
var funcJumpDown = (playheadOnly:Bool) -> {
var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
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;
}
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;
}
if (playbarButtonPressed == 'playbarForward')
{
playbarButtonPressed = '';
scrollAmount = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
funcJumpDown(false);
shouldPause = true;
}
// 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;
}
// CONTROL + Scroll = Scroll Precise
if (FlxG.keys.pressed.CONTROL)
{
scrollAmount /= 10;
scrollAmount /= 4;
}
// 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.noteData = currentPlaceNoteData;
gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
gridGhostHoldNote.noteData = gridGhostNote.noteData;
gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
@ -4953,37 +5128,57 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handlePlayhead():Void
{
// Place notes at the playhead.
// Place notes at the playhead with the keyboard.
switch (currentLiveInputStyle)
{
case ChartEditorLiveInputStyle.WASDKeys:
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.NumberKeys:
// Flipped because Dad is on the left but represents data 0-3.
if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
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.SEVEN) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.None:
// Do nothing.
}
updatePlayheadGhostHoldNotes();
}
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 playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
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);
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);
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
{
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)
{
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;
}
// TODO: Rework asset system so we can remove this.
// TODO: Rework asset system so we can remove this jank.
switch (currentSongStage)
{
case 'mainStage':
Paths.setCurrentLevel('week1');
PlayStatePlaylist.campaignId = 'week1';
case 'spookyMansion':
Paths.setCurrentLevel('week2');
PlayStatePlaylist.campaignId = 'week2';
case 'phillyTrain':
Paths.setCurrentLevel('week3');
PlayStatePlaylist.campaignId = 'week3';
case 'limoRide':
Paths.setCurrentLevel('week4');
PlayStatePlaylist.campaignId = 'week4';
case 'mallXmas' | 'mallEvil':
Paths.setCurrentLevel('week5');
PlayStatePlaylist.campaignId = 'week5';
case 'school' | 'schoolEvil':
Paths.setCurrentLevel('week6');
PlayStatePlaylist.campaignId = 'week6';
case 'tankmanBattlefield':
Paths.setCurrentLevel('week7');
PlayStatePlaylist.campaignId = 'week7';
case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2':
Paths.setCurrentLevel('weekend1');
PlayStatePlaylist.campaignId = 'weekend1';
}
Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
subStateClosed.add(reviveUICamera);
subStateClosed.add(resetConductorAfterTest);
@ -5369,7 +5687,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false;
var targetState = new PlayState(
var targetStateParams =
{
targetSong: targetSong,
targetDifficulty: selectedDifficulty,
@ -5380,24 +5698,26 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
startTimestamp: startTimestamp,
playbackRate: playbackRate,
overrideMusic: true,
});
};
// Override music.
if (audioInstTrack != null)
{
FlxG.sound.music = audioInstTrack;
}
targetState.vocals = audioVocalTrackGroup;
// Kill and replace the UI camera so it doesn't get destroyed during the state transition.
uiCamera.kill();
FlxG.cameras.remove(uiCamera, false);
FlxG.cameras.reset(new FunkinCamera());
FlxG.cameras.reset(new FunkinCamera('chartEditorUI2'));
this.persistentUpdate = false;
this.persistentDraw = false;
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
{
if (events.length == 0) return;
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentEventSelection = [];
@ -34,6 +36,8 @@ class RemoveEventsCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void
{
if (events.length == 0) return;
for (event in events)
{
state.currentSongChartEventData.push(event);

View file

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

View file

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

View file

@ -54,11 +54,16 @@ class ChartEditorHoldNoteSprite extends SustainTrail
* Set the height directly, to a value 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
{
sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
}
fullSustainLength = sustainLength;
}

View file

@ -117,12 +117,6 @@ class ChartEditorNoteSprite extends FlxSprite
{
noteFrameCollection.pushFrame(frame);
}
var frameCollectionNormal2:FlxAtlasFrames = Paths.getSparrowAtlas('NoteHoldNormal');
for (frame in frameCollectionNormal2.frames)
{
noteFrameCollection.pushFrame(frame);
}
// Pixel notes
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);
}
for (difficultyId in song.listDifficulties(variation))
for (difficultyId in song.listDifficulties(variation, true, true))
{
var diff:Null<SongDifficulty> = song.getDifficulty(difficultyId, variation);
if (diff == null) continue;

View file

@ -308,16 +308,6 @@ class ChartEditorToolboxHandler
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);
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.ChartEditorContextMenuHandler;
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.ChartEditorNotificationHandler;
using funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package funkin.ui.freeplay;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.util.FlxSort;
@ -7,6 +8,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;
import flixel.tweens.FlxEase;
import funkin.data.freeplay.AlbumRegistry;
import funkin.util.assets.FlxAnimationUtil;
import funkin.graphics.FunkinSprite;
import funkin.util.SortUtil;
import openfl.utils.Assets;
@ -21,9 +23,9 @@ class AlbumRoll extends FlxSpriteGroup
* The ID of the album to display.
* 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)
{
@ -34,30 +36,47 @@ class AlbumRoll extends FlxSpriteGroup
return value;
}
var albumArt:FunkinSprite;
var albumTitle:FunkinSprite;
var difficultyStars:DifficultyStars;
var newAlbumArt:FlxAtlasSprite;
// var difficultyStars:DifficultyStars;
var _exitMovers:Null<FreeplayState.ExitMoverData>;
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()
{
super();
albumTitle = new FunkinSprite(947, 491);
albumTitle.visible = true;
albumTitle.zIndex = 200;
add(albumTitle);
newAlbumArt = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum"));
newAlbumArt.visible = false;
newAlbumArt.onAnimationFinish.add(onAlbumFinish);
difficultyStars = new DifficultyStars(140, 39);
add(newAlbumArt);
difficultyStars.stars.visible = true;
albumTitle.visible = false;
// albumArtist.visible = false;
// difficultyStars = new DifficultyStars(140, 39);
// difficultyStars.stars.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
{
if (albumId == null)
{
// difficultyStars.stars.visible = false;
return;
}
albumData = AlbumRegistry.instance.fetchEntry(albumId);
if (albumData == null)
@ -74,33 +99,8 @@ class AlbumRoll extends FlxSpriteGroup
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();
if (Assets.exists(Paths.image(albumData.getAlbumTitleAssetKey())))
{
albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey()));
}
else
{
albumTitle.visible = false;
}
refresh();
}
@ -126,67 +126,46 @@ class AlbumRoll extends FlxSpriteGroup
if (exitMovers == null) return;
exitMovers.set([albumArt],
exitMovers.set([newAlbumArt],
{
x: FlxG.width,
speed: 0.4,
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.
*/
public function playIntro():Void
{
albumArt.visible = true;
FlxTween.tween(albumArt, {x: 950, y: 320, angle: -340}, 0.5, {ease: FlxEase.elasticOut});
newAlbumArt.visible = true;
newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false);
albumTitle.visible = false;
// difficultyStars.stars.visible = false;
new FlxTimer().start(0.75, function(_) {
showTitle();
// showTitle();
// showStars();
});
}
public function setDifficultyStars(?difficulty:Int):Void
public function skipIntro():Void
{
if (difficulty == null) return;
difficultyStars.difficulty = difficulty;
newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false);
}
public function showTitle():Void
{
albumTitle.visible = true;
}
/**
* Make the album stars visible.
*/
public function showStars():Void
{
// albumArtist.visible = false;
difficultyStars.stars.visible = false;
}
// public function setDifficultyStars(?difficulty:Int):Void
// {
// if (difficulty == null) return;
// difficultyStars.difficulty = difficulty;
// }
// /**
// * Make the album stars visible.
// */
// public function showStars():Void
// {
// difficultyStars.stars.visible = false; // true;
// }
}

View file

@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite
var gotSpooked:Bool = false;
static final SPOOK_PERIOD:Float = 10.0;
static final TV_PERIOD:Float = 10.0;
static final SPOOK_PERIOD:Float = 120.0;
static final TV_PERIOD:Float = 180.0;
// Time since dad last SPOOKED you.
var timeSinceSpook:Float = 0;
@ -43,7 +43,14 @@ class DJBoyfriend extends FlxAtlasSprite
switch (name)
{
case "Boyfriend DJ watchin tv OG":
if (number == 85) runTvLogic();
if (number == 80)
{
FunkinSound.playOnce(Paths.sound('remote_click'));
}
if (number == 85)
{
runTvLogic();
}
default:
}
};
@ -219,19 +226,17 @@ class DJBoyfriend extends FlxAtlasSprite
if (cartoonSnd == null)
{
// 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() {
// });
loadCartoon();
FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() {
loadCartoon();
});
}
else
{
// plays it smidge after the click
// new FlxTimer().start(0.1, function(_) {
// // FunkinSound.playOnce(Paths.sound('channel_switch'));
// });
cartoonSnd.destroy();
loadCartoon();
FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() {
cartoonSnd.destroy();
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;
}
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);
for (i in 0...7)
for (i in 0...digitCount)
{
add(new ScoreNum(x + (45 * i), y, 0));
}

View file

@ -133,8 +133,8 @@ class FreeplayState extends MusicBeatSubState
var stickerSubState:StickerSubState;
static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
static var rememberedSongId:Null<String> = null;
public static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
public static var rememberedSongId:Null<String> = 'tutorial';
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
{
@ -145,7 +145,7 @@ class FreeplayState extends MusicBeatSubState
stickerSubState = stickers;
}
super();
super(FlxColor.TRANSPARENT);
}
override function create():Void
@ -195,7 +195,7 @@ class FreeplayState extends MusicBeatSubState
var song:Song = SongRegistry.instance.fetchEntry(songId);
// 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;
songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
@ -380,7 +380,7 @@ class FreeplayState extends MusicBeatSubState
}
albumRoll = new AlbumRoll();
albumRoll.albumId = 'volume1';
albumRoll.albumId = null;
add(albumRoll);
albumRoll.applyExitMovers(exitMovers);
@ -425,7 +425,7 @@ class FreeplayState extends MusicBeatSubState
tmr.time = FlxG.random.float(20, 60);
}, 0);
fp = new FreeplayScore(460, 60, 100);
fp = new FreeplayScore(460, 60, 7, 100);
fp.visible = false;
add(fp);
@ -470,11 +470,7 @@ class FreeplayState extends MusicBeatSubState
albumRoll.playIntro();
new FlxTimer().start(0.75, function(_) {
albumRoll.showTitle();
});
new FlxTimer().start(35 / 24, function(_) {
albumRoll.showStars();
// albumRoll.showTitle();
});
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 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;
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.
*
* @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite)
* @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;
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 randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem);
@ -616,14 +638,7 @@ class FreeplayState extends MusicBeatSubState
funnyMenu.favIcon.visible = tempSongs[i].isFav;
funnyMenu.hsvShader = hsvShader;
if (i < 8)
{
funnyMenu.initJumpIn(Math.min(i, 4), force);
}
else
{
funnyMenu.forcePosition();
}
funnyMenu.forcePosition();
grpCapsules.add(funnyMenu);
}
@ -658,11 +673,12 @@ class FreeplayState extends MusicBeatSubState
if (FlxG.keys.justPressed.F)
{
if (songs[curSelected] != null)
var targetSong = grpCapsules.members[curSelected]?.songData;
if (targetSong != null)
{
var realShit:Int = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav;
if (songs[curSelected].isFav)
targetSong.isFav = !targetSong.isFav;
if (targetSong.isFav)
{
FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
{
@ -854,11 +870,13 @@ class FreeplayState extends MusicBeatSubState
{
dj.resetAFKTimer();
changeDiff(-1);
generateSongList(currentFilter, true);
}
if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL)
{
dj.resetAFKTimer();
changeDiff(1);
generateSongList(currentFilter, true);
}
if (controls.BACK && !typing.hasFocus)
@ -877,6 +895,8 @@ class FreeplayState extends MusicBeatSubState
for (spr in grpSpr)
{
if (spr == null) continue;
var funnyMoveShit:MoveData = moveData;
if (moveData.x == null) funnyMoveShit.x = spr.x;
@ -899,7 +919,7 @@ class FreeplayState extends MusicBeatSubState
if (Type.getClass(FlxG.state) == MainMenuState)
{
FlxG.state.persistentUpdate = true;
FlxG.state.persistentUpdate = false;
FlxG.state.persistentDraw = true;
}
@ -908,6 +928,11 @@ class FreeplayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransOut = true;
if (Type.getClass(FlxG.state) == MainMenuState)
{
FunkinSound.playMusic('freakyMenu',
{
overrideExisting: true,
restartTrack: false
});
close();
}
else
@ -926,7 +951,7 @@ class FreeplayState extends MusicBeatSubState
public override function destroy():Void
{
super.destroy();
var daSong:Null<FreeplaySongData> = songs[curSelected];
var daSong:Null<FreeplaySongData> = currentFilteredSongs[curSelected];
if (daSong != null)
{
clearDaCache(daSong.songName);
@ -948,10 +973,10 @@ class FreeplayState extends MusicBeatSubState
currentDifficulty = diffIdsCurrent[currentDifficultyIndex];
var daSong:Null<FreeplaySongData> = songs[curSelected];
var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].songData;
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;
intendedCompletion = songScore?.accuracy ?? 0.0;
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.
var newAlbumId:String = daSong?.albumId ?? Constants.DEFAULT_ALBUM_ID;
var newAlbumId:String = daSong?.albumId;
if (albumRoll.albumId != newAlbumId)
{
albumRoll.albumId = newAlbumId;
albumRoll.playIntro();
albumRoll.skipIntro();
}
}
@ -1103,6 +1125,12 @@ class FreeplayState extends MusicBeatSubState
targetVariation: targetVariation,
practiceMode: 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.
// startTimestamp: 0.0,
// playbackRate: 0.5,
@ -1115,20 +1143,18 @@ class FreeplayState extends MusicBeatSubState
{
if (rememberedSongId != null)
{
curSelected = songs.findIndex(function(song) {
curSelected = currentFilteredSongs.findIndex(function(song) {
if (song == null) return false;
return song.songId == rememberedSongId;
});
if (curSelected == -1) curSelected = 0;
}
if (rememberedDifficulty != null)
{
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
@ -1156,8 +1182,10 @@ class FreeplayState extends MusicBeatSubState
{
intendedScore = 0;
intendedCompletion = 0.0;
diffIdsCurrent = diffIdsTotal;
rememberedSongId = null;
rememberedDifficulty = null;
albumRoll.albumId = null;
}
for (index => capsule in grpCapsules.members)
@ -1195,12 +1223,33 @@ class FreeplayState extends MusicBeatSubState
});
if (didReplace)
{
FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: false
});
FlxG.sound.music.fadeIn(2, 0, 0.8);
}
}
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 songCharacter(default, null):String = '';
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 displayedVariations(default, null):Array<String> = [Constants.DEFAULT_VARIATION];
@ -1333,7 +1382,7 @@ class FreeplaySongData
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;
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations);
@ -1341,7 +1390,15 @@ class FreeplaySongData
this.songName = songDifficulty.songName;
this.songCharacter = songDifficulty.characters.opponent;
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;
var diffRatingSprite:FlxSprite;
// var diffRatingSprite:FlxSprite;
public function new(x:Float, y:Float)
{
@ -65,13 +65,13 @@ class SongMenuItem extends FlxSpriteGroup
var rank:String = FlxG.random.getObject(ranks);
ranking = new FlxSprite(capsule.width * 0.84, 30);
ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
ranking.scale.x = ranking.scale.y = realScaled;
// ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
// ranking.scale.x = ranking.scale.y = realScaled;
// ranking.alpha = 0.75;
ranking.visible = false;
ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y);
add(ranking);
grpHide.add(ranking);
// ranking.visible = false;
// ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y);
// add(ranking);
// grpHide.add(ranking);
switch (rank)
{
@ -81,12 +81,12 @@ class SongMenuItem extends FlxSpriteGroup
grayscaleShader = new Grayscale(1);
diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00'));
diffRatingSprite.shader = grayscaleShader;
// diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00'));
// diffRatingSprite.shader = grayscaleShader;
// diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y);
// TODO: Readd once ratings are fully implemented
// 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));
add(songText);
@ -118,8 +118,8 @@ class SongMenuItem extends FlxSpriteGroup
function updateDifficultyRating(newRating:Int):Void
{
var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
diffRatingSprite.visible = false;
// diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
// diffRatingSprite.visible = false;
}
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;
import funkin.graphics.FunkinSprite;
import flixel.addons.transition.FlxTransitionableState;
import funkin.ui.debug.DebugMenuSubState;
import flixel.FlxObject;
@ -51,12 +52,10 @@ class MainMenuState extends MusicBeatState
transIn = FlxTransitionableState.defaultTransIn;
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'));
bg.scrollFactor.x = 0;
@ -69,7 +68,7 @@ class MainMenuState extends MusicBeatState
camFollow = new FlxObject(0, 0, 1, 1);
add(camFollow);
magenta = new FlxSprite(Paths.image('menuDesat'));
magenta = new FlxSprite(Paths.image('menuBGMagenta'));
magenta.scrollFactor.x = bg.scrollFactor.x;
magenta.scrollFactor.y = bg.scrollFactor.y;
magenta.setGraphicSize(Std.int(bg.width));
@ -77,7 +76,6 @@ class MainMenuState extends MusicBeatState
magenta.x = bg.x;
magenta.y = bg.y;
magenta.visible = false;
magenta.color = 0xFFfd719b;
// TODO: Why doesn't this line compile I'm going fucking feral
@ -109,14 +107,21 @@ class MainMenuState extends MusicBeatState
});
#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;
createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker);
createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker);
#end
createMenuItem('options', 'mainmenu/options', function() {
startExitState(() -> new funkin.ui.options.OptionsState());
});
createMenuItem('credits', 'mainmenu/credits', function() {
startExitState(() -> new funkin.ui.credits.CreditsState());
});
// Reset position of menu items.
var spacing = 160;
var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2;
@ -125,6 +130,9 @@ class MainMenuState extends MusicBeatState
var menuItem = menuItems.members[i];
menuItem.x = FlxG.width / 2;
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();
@ -164,8 +172,9 @@ class MainMenuState extends MusicBeatState
function resetCamStuff()
{
FlxG.cameras.reset(new FunkinCamera());
FlxG.cameras.reset(new FunkinCamera('mainMenu'));
FlxG.camera.follow(camFollow, null, 0.06);
FlxG.camera.snapToTarget();
}
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);
}
function selectMerch()
{
WindowUtil.openURL(Constants.URL_MERCH);
}
#end
#if newgrounds
@ -311,8 +325,6 @@ class MainMenuState extends MusicBeatState
// Open the debug menu, defaults to ` / ~
if (controls.DEBUG_MENU)
{
this.persistentUpdate = false;
this.persistentDraw = false;
FlxG.state.openSubState(new DebugMenuSubState());
}

View file

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

View file

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

View file

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

View file

@ -169,7 +169,7 @@ class Level implements IRegistryEntry<LevelData>
if (firstSong != null)
{
// 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);
}

View file

@ -17,7 +17,7 @@ import funkin.ui.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
{
@ -29,10 +29,12 @@ class AttractState extends MusicBeatState
}
#if html5
trace('Playing web video ${ATTRACT_VIDEO_PATH}');
playVideoHTML5(ATTRACT_VIDEO_PATH);
#end
#if hxCodec
trace('Playing native video ${ATTRACT_VIDEO_PATH}');
playVideoNative(ATTRACT_VIDEO_PATH);
#end
}

View file

@ -220,7 +220,7 @@ class TitleState extends MusicBeatState
function playMenuMusic():Void
{
var shouldFadeIn = (FlxG.sound.music == null);
var shouldFadeIn:Bool = (FlxG.sound.music == null);
// Load music. Includes logic to handle BPM changes.
FunkinSound.playMusic('freakyMenu',
{
@ -229,7 +229,7 @@ class TitleState extends MusicBeatState
restartTrack: true
});
// 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>>
@ -290,18 +290,6 @@ class TitleState extends MusicBeatState
// do controls.PAUSE | controls.ACCEPT instead?
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;
if (gamepad != null)

View file

@ -22,10 +22,12 @@ import openfl.filters.ShaderFilter;
import openfl.utils.Assets;
import flixel.util.typeLimit.NextState;
class LoadingState extends MusicBeatState
class LoadingState extends MusicBeatSubState
{
inline static var MIN_TIME = 1.0;
var asSubState:Bool = false;
var target:NextState;
var playParams:Null<PlayStateParams>;
var stopMusic:Bool = false;
@ -178,7 +180,16 @@ class LoadingState extends MusicBeatState
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
@ -190,17 +201,41 @@ class LoadingState extends MusicBeatState
* Starts the transition to a new `PlayState` to start a new song.
* First switches to the `LoadingState` if assets need to be loaded.
* @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.
*/
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);
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
// Switch to loading state while we load assets (default on HTML5 target).
var loadStateCtor:NextState = () -> new LoadingState(playStateCtor, shouldStopMusic, params);
FlxG.switchState(loadStateCtor);
var loadStateCtor = function() {
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
// All assets preloaded, switch directly to play state (defualt on other targets).
if (shouldStopMusic && FlxG.sound.music != null)
@ -210,14 +245,42 @@ class LoadingState extends MusicBeatState
}
// 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);
}
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.
FunkinSprite.preparePurgeCache();
FunkinSprite.cacheTexture(Paths.image('combo'));
FunkinSprite.cacheTexture(Paths.image('healthBar'));
FunkinSprite.cacheTexture(Paths.image('menuDesat'));
FunkinSprite.cacheTexture(Paths.image('combo'));
@ -247,7 +310,10 @@ class LoadingState extends MusicBeatState
// 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.
// 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);
trace('Got ${assets.length} assets: ${assets}');
@ -278,20 +344,6 @@ class LoadingState extends MusicBeatState
// FunkinSprite.cacheAllSongTextures(stage)
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

View file

@ -247,10 +247,6 @@ class StickerSubState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = 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?
// 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.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;
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 flash.display.Bitmap;
import flash.display.BitmapData;
@ -46,7 +51,7 @@ class FunkinPreloader extends FlxBasePreloader
*/
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.
@ -108,13 +113,22 @@ class FunkinPreloader extends FlxBasePreloader
#if TOUCH_HERE_TO_PLAY
var touchHereToPlay:Bitmap;
#end
var progressBarPieces:Array<Sprite>;
var progressBar:Bitmap;
var progressLeftText: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()
{
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.
trace('Initializing custom preloader...');
@ -146,7 +160,7 @@ class FunkinPreloader extends FlxBasePreloader
bmp.x = (this._width - bmp.width) / 2;
bmp.y = (this._height - bmp.height) / 2;
});
addChild(logo);
// addChild(logo);
#if TOUCH_HERE_TO_PLAY
touchHereToPlay = createBitmap(TouchHereToPlayImage, function(bmp:Bitmap) {
@ -160,16 +174,48 @@ class FunkinPreloader extends FlxBasePreloader
addChild(touchHereToPlay);
#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.
progressBar = new Bitmap(new BitmapData(1, BAR_HEIGHT, true, Constants.COLOR_PRELOADER_BAR));
progressBar.x = BAR_PADDING;
progressBar.y = this._height - BAR_PADDING - BAR_HEIGHT;
addChild(progressBar);
// progressBar = new Bitmap(new BitmapData(1, BAR_HEIGHT, true, Constants.COLOR_PRELOADER_BAR));
// progressBar.x = BAR_PADDING;
// progressBar.y = this._height - BAR_PADDING - BAR_HEIGHT;
// addChild(progressBar);
// Create the progress message.
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;
progressLeftText.defaultTextFormat = progressLeftTextFormat;
@ -177,13 +223,14 @@ class FunkinPreloader extends FlxBasePreloader
progressLeftText.width = this._width - BAR_PADDING * 2;
progressLeftText.text = 'Downloading assets...';
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);
// Create the progress %.
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;
progressRightText.defaultTextFormat = progressRightTextFormat;
@ -193,6 +240,60 @@ class FunkinPreloader extends FlxBasePreloader
progressRightText.x = BAR_PADDING;
progressRightText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4;
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;
@ -200,6 +301,8 @@ class FunkinPreloader extends FlxBasePreloader
override function update(percent:Float):Void
{
var elapsed:Float = (Date.now().getTime() - this._startTime) / 1000.0;
vfdShader.update(elapsed * 100);
// trace('Time since last frame: ' + (lastElapsed - elapsed));
downloadingAssetsPercent = percent;
@ -748,12 +851,19 @@ class FunkinPreloader extends FlxBasePreloader
else
{
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
var maxWidth = this._width - BAR_PADDING * 2;
var barWidth = maxWidth * percent;
progressBar.width = barWidth;
// progressBar.width = barWidth;
// Cycle ellipsis count to show loading
var ellipsisCount:Int = Std.int(elapsed / ELLIPSIS_TIME) % 3 + 1;
@ -766,29 +876,29 @@ class FunkinPreloader extends FlxBasePreloader
{
// case FunkinPreloaderState.NotStarted:
default:
updateProgressLeftText('Loading (0/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Loading \n0/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.DownloadingAssets:
updateProgressLeftText('Downloading assets (1/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Downloading assets \n1/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.PreloadingPlayAssets:
updateProgressLeftText('Preloading assets (2/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Preloading assets \n2/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.InitializingScripts:
updateProgressLeftText('Initializing scripts (3/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Initializing scripts \n3/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.CachingGraphics:
updateProgressLeftText('Caching graphics (4/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Caching graphics \n4/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.CachingAudio:
updateProgressLeftText('Caching audio (5/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Caching audio \n5/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.CachingData:
updateProgressLeftText('Caching data (6/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Caching data \n6/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingSpritesheets:
updateProgressLeftText('Parsing spritesheets (7/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Parsing spritesheets \n7/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingStages:
updateProgressLeftText('Parsing stages (8/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Parsing stages \n8/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingCharacters:
updateProgressLeftText('Parsing characters (9/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Parsing characters \n9/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingSongs:
updateProgressLeftText('Parsing songs (10/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Parsing songs \n10/$TOTAL_STEPS $ellipsis');
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
case FunkinPreloaderState.TouchHereToPlay:
updateProgressLeftText(null);
@ -815,10 +925,21 @@ class FunkinPreloader extends FlxBasePreloader
else if (progressLeftText.text != text)
{
// 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;
progressLeftText.defaultTextFormat = progressLeftTextFormat;
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;
// Fade out progress bar too.
progressBar.alpha = logo.alpha;
// progressBar.alpha = logo.alpha;
progressLeftText.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;
}
@ -901,8 +1030,8 @@ class FunkinPreloader extends FlxBasePreloader
{
// Ensure the graphics are properly destroyed and GC'd.
removeChild(logo);
removeChild(progressBar);
logo = progressBar = null;
// removeChild(progressBar);
logo = null;
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.
* 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
*/
public static final COLOR_PRELOADER_BAR:FlxColor = 0xFF00FF00;
public static final COLOR_PRELOADER_BAR:FlxColor = 0xFFA4FF11;
/**
* Color for the preloader site lock background
@ -181,6 +186,12 @@ class Constants
*/
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.
*/
@ -347,7 +358,7 @@ class Constants
* The progress bare is automatically rescaled to match.
*/
#if debug
public static final PRELOADER_MIN_STAGE_TIME:Float = 1.0;
public static final PRELOADER_MIN_STAGE_TIME:Float = 0.0;
#else
public static final PRELOADER_MIN_STAGE_TIME:Float = 0.1;
#end
@ -515,4 +526,10 @@ class Constants
* The vertical offset of the strumline from the top edge of the screen.
*/
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.
*/
@ -90,3 +115,26 @@ class SerializerUtil
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();
}
/**
* 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.
*/