Merge branch 'rewrite/master' into bugfix/draw-crash

This commit is contained in:
Cameron Taylor 2024-04-19 14:03:58 -04:00 committed by GitHub
commit 035db7f00a
41 changed files with 1900 additions and 564 deletions

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

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

View file

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

View file

@ -1,44 +1,124 @@
name: upload-itch name: upload-itch
description: "installs Butler, and uploads to itch.io!" description: "installs Butler, and uploads to itch.io!"
inputs: inputs:
butler-key: butler-key:
description: "Butler API secret key" description: "Butler API secret key"
required: true required: true
itch-repo:
description: "Where to upload the game to"
required: true
default: "ninja-muffin24/funkin-secret"
build-dir: build-dir:
description: "Directory of the game build" description: "Directory of the game build"
required: true required: false
target: target:
description: "Target (html5, win, linux, mac)" description: "Target (html5, windows, linux, macos)"
required: true required: true
runs: runs:
using: "composite" using: "composite"
steps: steps:
- name: Install butler Windows
if: runner.os == 'Windows' # RUNNER_OS = Windows | macOS | Linux
run: | # TARGET_BUILD = windows | macos | linux
curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default # TARGET_ITCH = win | macos | linux
7z x butler.zip # TARGET_BUTLER_DOWNLOAD = windows-amd64 | darwin-amd64 | linux-amd64
./butler -v - name: Setup variables
shell: bash shell: bash
- name: Install butler Mac
if: runner.os == 'macOS'
run: | run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default TARGET_OS=${{ inputs.target }}
unzip butler.zip RUNNER=${RUNNER_OS@L}
./butler -V 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 shell: bash
- name: Install butler Linux
if: runner.os == 'Linux'
run: | run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default 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 unzip butler.zip
chmod +x butler chmod +x butler
./butler -V
shell: bash
- name: Upload game to itch.io - 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 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,136 +0,0 @@
name: build-upload
on:
workflow_dispatch:
push:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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

@ -1,18 +1,21 @@
name: cancel-merged-branches name: Cancel queued workflows on PR merge
on: on:
pull_request: pull_request:
types: types:
- closed - closed
jobs: jobs:
cancel_stuff: cancel_stuff:
if: github.event.pull_request.merged == true if: github.event.pull_request.merged == true
runs-on: ubuntu-latest runs-on: build-set
permissions: permissions:
actions: write actions: write
steps: steps:
- uses: actions/github-script@v7 - name: Cancel queued workflows for ${{ github.event.pull_request.head.ref }}
id: cancel-runs uses: actions/github-script@v7
with: with:
result-encoding: string result-encoding: string
retries: 3 retries: 3

10
.vscode/settings.json vendored
View file

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

View file

@ -45,6 +45,7 @@
<library name="week6" preload="true" /> <library name="week6" preload="true" />
<library name="week7" preload="true" /> <library name="week7" preload="true" />
<library name="weekend1" preload="true" /> <library name="weekend1" preload="true" />
<library name="videos" preload="true" />
</section> </section>
<section if="NO_PRELOAD_ALL"> <section if="NO_PRELOAD_ALL">
<library name="songs" preload="false" /> <library name="songs" preload="false" />
@ -58,10 +59,13 @@
<library name="week6" preload="false" /> <library name="week6" preload="false" />
<library name="week7" preload="false" /> <library name="week7" preload="false" />
<library name="weekend1" preload="false" /> <library name="weekend1" preload="false" />
<library name="videos" preload="false" />
</section> </section>
<library name="art" preload="false" /> <library name="art" preload="false" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" /> <assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" /> <assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" />
<!-- Videos go in their own library because web never needs to preload them, they can just be streamed. -->
<assets path="assets/videos" library="videos" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" /> <assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" /> <assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" /> <assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" />

2
assets

@ -1 +1 @@
Subproject commit 1a7a0b6cc60dc8131f1651caa7abef0c1944a10c Subproject commit 069c9bf45f197ebe0b38483d11bb30c15bbb5eca

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

@ -104,7 +104,7 @@
"name": "lime", "name": "lime",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "1359fe6ad52e91175dc636a516d460bd54ea22ed", "ref": "43ebebdd8119936b99f23407057025c7849c5f5b",
"url": "https://github.com/FunkinCrew/lime" "url": "https://github.com/FunkinCrew/lime"
}, },
{ {

View file

@ -128,6 +128,8 @@ class Main extends Sprite
Toolkit.init(); Toolkit.init();
Toolkit.theme = 'dark'; // don't be cringe Toolkit.theme = 'dark'; // don't be cringe
Toolkit.autoScale = false; Toolkit.autoScale = false;
// Don't focus on UI elements when they first appear.
haxe.ui.focus.FocusManager.instance.autoFocus = false;
funkin.input.Cursor.registerHaxeUICursors(); funkin.input.Cursor.registerHaxeUICursors();
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200; haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
} }

View file

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

View file

@ -113,7 +113,7 @@ class Paths
public static function videos(key:String, ?library:String):String public static function videos(key:String, ?library:String):String
{ {
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library); return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library ?? 'videos');
} }
public static function voices(song:String, ?suffix:String = ''):String public static function voices(song:String, ?suffix:String = ''):String

View file

@ -223,11 +223,12 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
// already paused before we lost focus. // already paused before we lost focus.
if (_lostFocus && !_alreadyPaused) if (_lostFocus && !_alreadyPaused)
{ {
trace('Resuming audio (${this._label}) on focus!');
resume(); resume();
} }
else else
{ {
trace('Not resuming audio on focus!'); trace('Not resuming audio (${this._label}) on focus!');
} }
_lostFocus = false; _lostFocus = false;
} }
@ -402,6 +403,12 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
sound.group = FlxG.sound.defaultSoundGroup; sound.group = FlxG.sound.defaultSoundGroup;
sound.persist = true; sound.persist = true;
// Make sure to add the sound to the list.
// If it's already in, it won't get re-added.
// If it's not in the list (it gets removed by FunkinSound.playMusic()),
// it will get re-added (then if this was called by playMusic(), removed again)
FlxG.sound.list.add(sound);
// Call onLoad() because the sound already loaded // Call onLoad() because the sound already loaded
if (onLoad != null && sound._sound != null) onLoad(); if (onLoad != null && sound._sound != null) onLoad();

View file

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

View file

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

View file

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

View file

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

View file

@ -834,9 +834,12 @@ class PlayState extends MusicBeatSubState
inputSpitter = []; inputSpitter = [];
// Reset music properly. // Reset music properly.
if (FlxG.sound.music != null)
{
FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
FlxG.sound.music.pitch = playbackRate; FlxG.sound.music.pitch = playbackRate;
FlxG.sound.music.pause(); FlxG.sound.music.pause();
}
if (!overrideMusic) if (!overrideMusic)
{ {
@ -852,7 +855,7 @@ class PlayState extends MusicBeatSubState
vocals.pause(); vocals.pause();
vocals.time = 0; vocals.time = 0;
FlxG.sound.music.volume = 1; if (FlxG.sound.music != null) FlxG.sound.music.volume = 1;
vocals.volume = 1; vocals.volume = 1;
vocals.playerVolume = 1; vocals.playerVolume = 1;
vocals.opponentVolume = 1; vocals.opponentVolume = 1;
@ -1548,10 +1551,11 @@ class PlayState extends MusicBeatSubState
function loadStage(id:String):Void function loadStage(id:String):Void
{ {
currentStage = StageRegistry.instance.fetchEntry(id); currentStage = StageRegistry.instance.fetchEntry(id);
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
if (currentStage != null) if (currentStage != null)
{ {
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
// Actually create and position the sprites. // Actually create and position the sprites.
var event:ScriptEvent = new ScriptEvent(CREATE, false); var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentStage, event); ScriptEventDispatcher.callEvent(currentStage, event);
@ -2388,13 +2392,6 @@ class PlayState extends MusicBeatSubState
// Display the combo meter and add the calculation to the score. // Display the combo meter and add the calculation to the score.
popUpScore(note, event.score, event.judgement, event.healthChange); popUpScore(note, event.score, event.judgement, event.healthChange);
if (note.isHoldNote && note.holdNoteSprite != null)
{
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
}
vocals.playerVolume = 1;
} }
/** /**
@ -2676,6 +2673,13 @@ class PlayState extends MusicBeatSubState
} }
comboPopUps.displayRating(daRating); comboPopUps.displayRating(daRating);
if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo); if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo);
if (daNote.isHoldNote && daNote.holdNoteSprite != null)
{
playerStrumline.playNoteHoldCover(daNote.holdNoteSprite);
}
vocals.playerVolume = 1;
} }
/** /**
@ -2786,7 +2790,7 @@ class PlayState extends MusicBeatSubState
// adds current song data into the tallies for the level (story levels) // adds current song data into the tallies for the level (story levels)
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel); Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data))
{ {
Save.instance.setSongScore(currentSong.id, currentDifficulty, data); Save.instance.setSongScore(currentSong.id, currentDifficulty, data);
#if newgrounds #if newgrounds
@ -3072,18 +3076,18 @@ class PlayState extends MusicBeatSubState
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
scoreData: scoreData:
{ {
score: songScore, score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
tallies: tallies:
{ {
sick: Highscore.tallies.sick, sick: talliesToUse.sick,
good: Highscore.tallies.good, good: talliesToUse.good,
bad: Highscore.tallies.bad, bad: talliesToUse.bad,
shit: Highscore.tallies.shit, shit: talliesToUse.shit,
missed: Highscore.tallies.missed, missed: talliesToUse.missed,
combo: Highscore.tallies.combo, combo: talliesToUse.combo,
maxCombo: Highscore.tallies.maxCombo, maxCombo: talliesToUse.maxCombo,
totalNotesHit: Highscore.tallies.totalNotesHit, totalNotesHit: talliesToUse.totalNotesHit,
totalNotes: Highscore.tallies.totalNotes, totalNotes: talliesToUse.totalNotes,
}, },
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
}, },

View file

@ -80,34 +80,34 @@ class ResultState extends MusicBeatSubState
bgFlash.visible = false; bgFlash.visible = false;
add(bgFlash); add(bgFlash);
var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared")); // var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared"));
bfGfExcellent.visible = false; // bfGfExcellent.visible = false;
add(bfGfExcellent); // add(bfGfExcellent);
//
// var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared"));
// bfPerfect.visible = false;
// add(bfPerfect);
//
// var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
// bfSHIT.visible = false;
// add(bfSHIT);
//
// bfGfExcellent.anim.onComplete = () -> {
// bfGfExcellent.anim.curFrame = 28;
// bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
//
// bfPerfect.anim.onComplete = () -> {
// bfPerfect.anim.curFrame = 136;
// bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
//
// bfSHIT.anim.onComplete = () -> {
// bfSHIT.anim.curFrame = 150;
// bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared")); var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD');
bfPerfect.visible = false;
add(bfPerfect);
var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
bfSHIT.visible = false;
add(bfSHIT);
bfGfExcellent.anim.onComplete = () -> {
bfGfExcellent.anim.curFrame = 28;
bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
bfPerfect.anim.onComplete = () -> {
bfPerfect.anim.curFrame = 136;
bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
bfSHIT.anim.onComplete = () -> {
bfSHIT.anim.curFrame = 150;
bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
var gf:FlxSprite = FunkinSprite.createSparrow(500, 300, 'resultScreen/resultGirlfriendGOOD');
gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
gf.visible = false; gf.visible = false;
gf.animation.finishCallback = _ -> { gf.animation.finishCallback = _ -> {
@ -268,9 +268,9 @@ class ResultState extends MusicBeatSubState
switch (resultsVariation) switch (resultsVariation)
{ {
case SHIT: // case SHIT:
bfSHIT.visible = true; // bfSHIT.visible = true;
bfSHIT.playAnimation(""); // bfSHIT.playAnimation("");
case NORMAL: case NORMAL:
boyfriend.animation.play('fall'); boyfriend.animation.play('fall');
@ -292,9 +292,9 @@ class ResultState extends MusicBeatSubState
gf.animation.play('clap', true); gf.animation.play('clap', true);
gf.visible = true; gf.visible = true;
}); });
case PERFECT: // case PERFECT:
bfPerfect.visible = true; // bfPerfect.visible = true;
bfPerfect.playAnimation(""); // bfPerfect.playAnimation("");
// bfGfExcellent.visible = true; // bfGfExcellent.visible = true;
// bfGfExcellent.playAnimation(""); // bfGfExcellent.playAnimation("");

View file

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

View file

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

View file

@ -693,7 +693,7 @@ class Save
trace("[SAVE] Checking for legacy save data..."); trace("[SAVE] Checking for legacy save data...");
var legacySave:FlxSave = new FlxSave(); var legacySave:FlxSave = new FlxSave();
legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY); legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY);
if (legacySave?.data == null) if (legacySave.isEmpty())
{ {
trace("[SAVE] No legacy save data found."); trace("[SAVE] No legacy save data found.");
return null; return null;

View file

@ -57,10 +57,6 @@ class CreditsDataHandler
{line: 'KawaiSprite'}, {line: 'KawaiSprite'},
{line: 'evilsk8r'}, {line: 'evilsk8r'},
] ]
},
{
header: 'Kickstarter Backers',
appendBackers: true
} }
] ]
}; };
@ -68,11 +64,11 @@ class CreditsDataHandler
public static function fetchBackerEntries():Array<String> public static function fetchBackerEntries():Array<String>
{ {
// TODO: Replace this with a web request. // TODO: Implement a web request.
// We can't just grab the current Kickstarter data and include it in builds, // 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. // because we don't want to deadname people who haven't logged into the portal yet.
// It can be async and paginated for performance! // It can be async and paginated for performance!
return ['See the list of backers at $BACKER_PUBLIC_URL.']; return [];
} }
#if HARDCODED_CREDITS #if HARDCODED_CREDITS
@ -99,12 +95,19 @@ class CreditsDataHandler
static function fetchCreditsData():funkin.data.JsonFile static function fetchCreditsData():funkin.data.JsonFile
{ {
#if !macro
var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim(); var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim();
return { return {
fileName: CREDITS_DATA_PATH, fileName: CREDITS_DATA_PATH,
contents: rawJson contents: rawJson
}; };
#else
return {
fileName: CREDITS_DATA_PATH,
contents: null
};
#end
} }
static function parseCreditsData(file:JsonFile):Null<CreditsData> static function parseCreditsData(file:JsonFile):Null<CreditsData>

View file

@ -8,6 +8,7 @@ import flixel.FlxSprite;
import flixel.FlxSubState; import flixel.FlxSubState;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey; import flixel.input.keyboard.FlxKey;
import flixel.input.mouse.FlxMouseEvent; import flixel.input.mouse.FlxMouseEvent;
import flixel.math.FlxMath; import flixel.math.FlxMath;
@ -40,6 +41,8 @@ import funkin.data.stage.StageData;
import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.input.Cursor; import funkin.input.Cursor;
import funkin.input.TurboActionHandler;
import funkin.input.TurboButtonHandler;
import funkin.input.TurboKeyHandler; import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.BaseCharacter.CharacterType;
@ -74,6 +77,7 @@ import funkin.ui.debug.charting.commands.SetItemSelectionCommand;
import funkin.ui.debug.charting.components.ChartEditorEventSprite; import funkin.ui.debug.charting.components.ChartEditorEventSprite;
import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
import funkin.ui.debug.charting.components.ChartEditorNotePreview; import funkin.ui.debug.charting.components.ChartEditorNotePreview;
import funkin.ui.debug.charting.components.ChartEditorNoteSprite; import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
import funkin.ui.debug.charting.components.ChartEditorPlaybarHead; import funkin.ui.debug.charting.components.ChartEditorPlaybarHead;
@ -401,8 +405,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0);
// Offset the selection box start position, if we are dragging. // Offset the selection box start position, if we are dragging.
if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff;
// Update the note preview viewport box.
// Update the note preview.
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
refreshNotePreviewPlayheadPosition();
// Update the measure tick display. // Update the measure tick display.
if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0; if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0;
return this.scrollPositionInPixels; return this.scrollPositionInPixels;
@ -463,6 +470,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Move the playhead sprite to the correct position. // Move the playhead sprite to the correct position.
gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS; gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS;
updatePlayheadGhostHoldNotes();
refreshNotePreviewPlayheadPosition();
return this.playheadPositionInPixels; return this.playheadPositionInPixels;
} }
@ -769,6 +779,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return currentPlaceNoteData = value; return currentPlaceNoteData = value;
} }
/**
* The SongNoteData which is currently being placed, for each column.
* `null` if the user isn't currently placing a note.
* As the user moves down, we will update this note's sustain length, and finalize the note when they release.
*/
var currentLiveInputPlaceNoteData:Array<SongNoteData> = [];
// Note Movement // Note Movement
/** /**
@ -799,6 +816,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var dragLengthCurrent:Float = 0; var dragLengthCurrent:Float = 0;
/**
* The current length of the hold note we are placing with the playhead, in steps.
* Play a sound when this value changes.
*/
var playheadDragLengthCurrent:Array<Float> = [];
/** /**
* Flip-flop to alternate between two stretching sounds. * Flip-flop to alternate between two stretching sounds.
*/ */
@ -1071,6 +1094,66 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var pageDownKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEDOWN); var pageDownKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEDOWN);
/**
* Variable used to track how long the user has been holding up on the dpad.
*/
var dpadUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_UP);
/**
* Variable used to track how long the user has been holding down on the dpad.
*/
var dpadDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_DOWN);
/**
* Variable used to track how long the user has been holding left on the dpad.
*/
var dpadLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_LEFT);
/**
* Variable used to track how long the user has been holding right on the dpad.
*/
var dpadRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_RIGHT);
/**
* Variable used to track how long the user has been holding up on the left stick.
*/
var leftStickUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_UP);
/**
* Variable used to track how long the user has been holding down on the left stick.
*/
var leftStickDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_DOWN);
/**
* Variable used to track how long the user has been holding left on the left stick.
*/
var leftStickLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_LEFT);
/**
* Variable used to track how long the user has been holding right on the left stick.
*/
var leftStickRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_RIGHT);
/**
* Variable used to track how long the user has been holding up on the right stick.
*/
var rightStickUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_UP);
/**
* Variable used to track how long the user has been holding down on the right stick.
*/
var rightStickDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_DOWN);
/**
* Variable used to track how long the user has been holding left on the right stick.
*/
var rightStickLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_LEFT);
/**
* Variable used to track how long the user has been holding right on the right stick.
*/
var rightStickRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_RIGHT);
/** /**
* AUDIO AND SOUND DATA * AUDIO AND SOUND DATA
*/ */
@ -1949,10 +2032,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var gridGhostNote:Null<ChartEditorNoteSprite> = null; var gridGhostNote:Null<ChartEditorNoteSprite> = null;
/** /**
* A sprite used to indicate the note that will be placed on click. * A sprite used to indicate the hold note that will be placed on click.
*/ */
var gridGhostHoldNote:Null<ChartEditorHoldNoteSprite> = null; var gridGhostHoldNote:Null<ChartEditorHoldNoteSprite> = null;
/**
* A sprite used to indicate the hold note that will be placed on button release.
*/
var gridPlayheadGhostHoldNotes:Array<ChartEditorHoldNoteSprite> = [];
/** /**
* A sprite used to indicate the event that will be placed on click. * A sprite used to indicate the event that will be placed on click.
*/ */
@ -1970,6 +2058,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var notePreviewViewport:Null<FlxSliceSprite> = null; var notePreviewViewport:Null<FlxSliceSprite> = null;
/**
* The thin sprite used for representing the playhead on the note preview.
* We move this up and down to represent the current position.
*/
var notePreviewPlayhead:Null<FlxSprite> = null;
/** /**
* The rectangular sprite used for rendering the selection box. * The rectangular sprite used for rendering the selection box.
* Uses a 9-slice to stretch the selection box to the correct size without warping. * Uses a 9-slice to stretch the selection box to the correct size without warping.
@ -2349,7 +2443,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
gridGhostHoldNote = new ChartEditorHoldNoteSprite(this); gridGhostHoldNote = new ChartEditorHoldNoteSprite(this);
gridGhostHoldNote.alpha = 0.6; gridGhostHoldNote.alpha = 0.6;
gridGhostHoldNote.noteData = new SongNoteData(0, 0, 0, ""); gridGhostHoldNote.noteData = null;
gridGhostHoldNote.visible = false; gridGhostHoldNote.visible = false;
add(gridGhostHoldNote); add(gridGhostHoldNote);
gridGhostHoldNote.zIndex = 11; gridGhostHoldNote.zIndex = 11;
@ -2423,6 +2517,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(notePreviewViewport); add(notePreviewViewport);
notePreviewViewport.zIndex = 30; notePreviewViewport.zIndex = 30;
notePreviewPlayhead = new FlxSprite().makeGraphic(2, 2, 0xFFFF0000);
notePreviewPlayhead.scrollFactor.set(0, 0);
notePreviewPlayhead.scale.set(notePreview.width / 2, 0.5); // Setting width does nothing.
notePreviewPlayhead.updateHitbox();
notePreviewPlayhead.x = notePreview.x;
notePreviewPlayhead.y = notePreview.y;
add(notePreviewPlayhead);
notePreviewPlayhead.zIndex = 31;
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
} }
@ -2519,6 +2622,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
} }
function refreshNotePreviewPlayheadPosition():Void
{
if (notePreviewPlayhead == null) return;
notePreviewPlayhead.y = notePreview.y + (notePreview.height * ((scrollPositionInPixels + playheadPositionInPixels) / songLengthInPixels));
}
/** /**
* Builds the group that will hold all the notes. * Builds the group that will hold all the notes.
*/ */
@ -3015,6 +3125,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
function setupTurboKeyHandlers():Void function setupTurboKeyHandlers():Void
{ {
// Keyboard shortcuts
add(undoKeyHandler); add(undoKeyHandler);
add(redoKeyHandler); add(redoKeyHandler);
add(upKeyHandler); add(upKeyHandler);
@ -3023,6 +3134,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(sKeyHandler); add(sKeyHandler);
add(pageUpKeyHandler); add(pageUpKeyHandler);
add(pageDownKeyHandler); add(pageDownKeyHandler);
// Gamepad inputs
add(dpadUpGamepadHandler);
add(dpadDownGamepadHandler);
add(dpadLeftGamepadHandler);
add(dpadRightGamepadHandler);
add(leftStickUpGamepadHandler);
add(leftStickDownGamepadHandler);
add(leftStickLeftGamepadHandler);
add(leftStickRightGamepadHandler);
add(rightStickUpGamepadHandler);
add(rightStickDownGamepadHandler);
add(rightStickLeftGamepadHandler);
add(rightStickRightGamepadHandler);
} }
/** /**
@ -3709,32 +3834,56 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Up Arrow = Scroll Up // Up Arrow = Scroll Up
if (upKeyHandler.activated && currentLiveInputStyle == None) if (upKeyHandler.activated && currentLiveInputStyle == None)
{ {
scrollAmount = -GRID_SIZE * 0.25 * 25.0; scrollAmount = -GRID_SIZE * 4;
shouldPause = true; shouldPause = true;
} }
// Down Arrow = Scroll Down // Down Arrow = Scroll Down
if (downKeyHandler.activated && currentLiveInputStyle == None) if (downKeyHandler.activated && currentLiveInputStyle == None)
{ {
scrollAmount = GRID_SIZE * 0.25 * 25.0; scrollAmount = GRID_SIZE * 4;
shouldPause = true; shouldPause = true;
} }
// W = Scroll Up (doesn't work with Ctrl+Scroll) // W = Scroll Up (doesn't work with Ctrl+Scroll)
if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL) if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{ {
scrollAmount = -GRID_SIZE * 0.25 * 25.0; scrollAmount = -GRID_SIZE * 4;
shouldPause = true; shouldPause = true;
} }
// S = Scroll Down (doesn't work with Ctrl+Scroll) // S = Scroll Down (doesn't work with Ctrl+Scroll)
if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL) if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{ {
scrollAmount = GRID_SIZE * 0.25 * 25.0; scrollAmount = GRID_SIZE * 4;
shouldPause = true; shouldPause = true;
} }
// PAGE UP = Jump up to nearest measure // GAMEPAD LEFT STICK UP = Scroll Up by 1 note snap
if (pageUpKeyHandler.activated) if (leftStickUpGamepadHandler.activated)
{ {
scrollAmount = -GRID_SIZE * noteSnapRatio;
shouldPause = true;
}
// GAMEPAD LEFT STICK DOWN = Scroll Down by 1 note snap
if (leftStickDownGamepadHandler.activated)
{
scrollAmount = GRID_SIZE * noteSnapRatio;
shouldPause = true;
}
// GAMEPAD RIGHT STICK UP = Scroll Up by 1 note snap (playhead only)
if (rightStickUpGamepadHandler.activated)
{
playheadAmount = -GRID_SIZE * noteSnapRatio;
shouldPause = true;
}
// GAMEPAD RIGHT STICK DOWN = Scroll Down by 1 note snap (playhead only)
if (rightStickDownGamepadHandler.activated)
{
playheadAmount = GRID_SIZE * noteSnapRatio;
shouldPause = true;
}
var funcJumpUp = (playheadOnly:Bool) -> {
var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight; var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight;
@ -3744,20 +3893,37 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure;
} }
scrollAmount = targetScrollPosition - playheadPos;
if (playheadOnly)
{
playheadAmount = targetScrollPosition - playheadPos;
}
else
{
scrollAmount = targetScrollPosition - playheadPos;
}
}
// PAGE UP = Jump up to nearest measure
// GAMEPAD LEFT STICK LEFT = Jump up to nearest measure
if (pageUpKeyHandler.activated || leftStickLeftGamepadHandler.activated)
{
funcJumpUp(false);
shouldPause = true;
}
if (rightStickLeftGamepadHandler.activated)
{
funcJumpUp(true);
shouldPause = true; shouldPause = true;
} }
if (playbarButtonPressed == 'playbarBack') if (playbarButtonPressed == 'playbarBack')
{ {
playbarButtonPressed = ''; playbarButtonPressed = '';
scrollAmount = -GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; funcJumpUp(false);
shouldPause = true; shouldPause = true;
} }
// PAGE DOWN = Jump down to nearest measure var funcJumpDown = (playheadOnly:Bool) -> {
if (pageDownKeyHandler.activated)
{
var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight; var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight;
@ -3767,26 +3933,46 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure;
} }
scrollAmount = targetScrollPosition - playheadPos;
if (playheadOnly)
{
playheadAmount = targetScrollPosition - playheadPos;
}
else
{
scrollAmount = targetScrollPosition - playheadPos;
}
}
// PAGE DOWN = Jump down to nearest measure
// GAMEPAD LEFT STICK RIGHT = Jump down to nearest measure
if (pageDownKeyHandler.activated || leftStickRightGamepadHandler.activated)
{
funcJumpDown(false);
shouldPause = true;
}
if (rightStickRightGamepadHandler.activated)
{
funcJumpDown(true);
shouldPause = true; shouldPause = true;
} }
if (playbarButtonPressed == 'playbarForward') if (playbarButtonPressed == 'playbarForward')
{ {
playbarButtonPressed = ''; playbarButtonPressed = '';
scrollAmount = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; funcJumpDown(false);
shouldPause = true; shouldPause = true;
} }
// SHIFT + Scroll = Scroll Fast // SHIFT + Scroll = Scroll Fast
if (FlxG.keys.pressed.SHIFT) // GAMEPAD LEFT STICK CLICK + Scroll = Scroll Fast
if (FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_STICK_CLICK ?? false))
{ {
scrollAmount *= 2; scrollAmount *= 2;
} }
// CONTROL + Scroll = Scroll Precise // CONTROL + Scroll = Scroll Precise
if (FlxG.keys.pressed.CONTROL) if (FlxG.keys.pressed.CONTROL)
{ {
scrollAmount /= 10; scrollAmount /= 4;
} }
// Alt + Drag = Scroll but move the playhead the same amount. // Alt + Drag = Scroll but move the playhead the same amount.
@ -4380,9 +4566,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
gridGhostHoldNote.visible = true; gridGhostHoldNote.visible = true;
gridGhostHoldNote.noteData = currentPlaceNoteData; gridGhostHoldNote.noteData = gridGhostNote.noteData;
gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection(); gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes); gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
@ -4943,37 +5128,57 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handlePlayhead():Void function handlePlayhead():Void
{ {
// Place notes at the playhead. // Place notes at the playhead with the keyboard.
switch (currentLiveInputStyle) switch (currentLiveInputStyle)
{ {
case ChartEditorLiveInputStyle.WASDKeys: case ChartEditorLiveInputStyle.WASDKeys:
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4); if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5); if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6); if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7); if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0); if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1); if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2); if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3); if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.NumberKeys: case ChartEditorLiveInputStyle.NumberKeys:
// Flipped because Dad is on the left but represents data 0-3. // Flipped because Dad is on the left but represents data 0-3.
if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4); if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5); if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6); if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7); if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0); if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1); if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2); if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3); if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.None: case ChartEditorLiveInputStyle.None:
// Do nothing. // Do nothing.
} }
updatePlayheadGhostHoldNotes();
} }
function placeNoteAtPlayhead(column:Int):Void function placeNoteAtPlayhead(column:Int):Void
{ {
// SHIFT + press or LEFT_SHOULDER + press to remove notes instead of placing them.
var removeNoteInstead:Bool = FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_SHOULDER ?? false);
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep));
@ -4984,14 +5189,136 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio); playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio);
notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]); notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]);
if (notesAtPos.length == 0) if (notesAtPos.length == 0 && !removeNoteInstead)
{ {
trace('Placing note. ${column}');
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace); var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
currentLiveInputPlaceNoteData[column] = newNoteData;
}
else if (removeNoteInstead)
{
trace('Removing existing note at position. ${column}');
performCommand(new RemoveNotesCommand(notesAtPos));
} }
else else
{ {
trace('Already a note there.'); trace('Already a note there. ${column}');
}
}
function updatePlayheadGhostHoldNotes():Void
{
// Ensure all the ghost hold notes exist.
while (gridPlayheadGhostHoldNotes.length < (STRUMLINE_SIZE * 2))
{
var ghost = new ChartEditorHoldNoteSprite(this);
ghost.alpha = 0.6;
ghost.noteData = null;
ghost.visible = false;
ghost.zIndex = 11;
add(ghost); // Don't add to `renderedHoldNotes` because then it will get killed every frame.
gridPlayheadGhostHoldNotes.push(ghost);
refresh();
}
// Update playhead ghost hold notes.
for (column in 0...gridPlayheadGhostHoldNotes.length)
{
var targetNoteData = currentLiveInputPlaceNoteData[column];
var ghostHold = gridPlayheadGhostHoldNotes[column];
if (targetNoteData == null && ghostHold.noteData != null)
{
// Remove the ghost hold note.
ghostHold.noteData = null;
}
if (targetNoteData != null && ghostHold.noteData == null)
{
// Readd the new ghost hold note.
ghostHold.noteData = targetNoteData.clone();
ghostHold.noteDirection = ghostHold.noteData.getDirection();
ghostHold.visible = true;
ghostHold.alpha = 0.6;
ghostHold.setHeightDirectly(0);
ghostHold.updateHoldNotePosition(renderedHoldNotes);
}
if (ghostHold.noteData == null)
{
ghostHold.visible = false;
ghostHold.setHeightDirectly(0);
playheadDragLengthCurrent[column] = 0;
continue;
}
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep));
var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio;
var newNoteLength:Float = playheadPosSnappedMs - ghostHold.noteData.time;
trace('newNoteLength: ${newNoteLength}');
if (newNoteLength > 0)
{
ghostHold.noteData.length = newNoteLength;
var targetNoteLengthSteps:Float = ghostHold.noteData.getStepLength(true);
var targetNoteLengthStepsInt:Int = Std.int(Math.floor(targetNoteLengthSteps));
var targetNoteLengthPixels:Float = targetNoteLengthSteps * GRID_SIZE;
if (playheadDragLengthCurrent[column] != targetNoteLengthStepsInt)
{
stretchySounds = !stretchySounds;
this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
playheadDragLengthCurrent[column] = targetNoteLengthStepsInt;
}
ghostHold.visible = true;
ghostHold.alpha = 0.6;
ghostHold.setHeightDirectly(targetNoteLengthPixels, true);
ghostHold.updateHoldNotePosition(renderedHoldNotes);
trace('lerpLength: ${ghostHold.fullSustainLength}');
trace('position: ${ghostHold.x}, ${ghostHold.y}');
}
else
{
ghostHold.visible = false;
ghostHold.setHeightDirectly(0);
playheadDragLengthCurrent[column] = 0;
continue;
}
}
}
function finishPlaceNoteAtPlayhead(column:Int):Void
{
if (currentLiveInputPlaceNoteData[column] == null) return;
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep));
var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio;
var newNoteLength:Float = playheadPosSnappedMs - currentLiveInputPlaceNoteData[column].time;
trace('finishPlace newNoteLength: ${newNoteLength}');
if (newNoteLength < Conductor.instance.stepLengthMs)
{
// Don't extend the note if it's too short.
trace('Not extending note. ${column}');
currentLiveInputPlaceNoteData[column] = null;
gridPlayheadGhostHoldNotes[column].noteData = null;
}
else
{
// Extend the note to the playhead position.
trace('Extending note. ${column}');
this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
performCommand(new ExtendNoteLengthCommand(currentLiveInputPlaceNoteData[column], newNoteLength));
currentLiveInputPlaceNoteData[column] = null;
gridPlayheadGhostHoldNotes[column].noteData = null;
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -37,8 +37,8 @@ class AlbumRoll extends FlxSpriteGroup
} }
var newAlbumArt:FlxAtlasSprite; var newAlbumArt:FlxAtlasSprite;
var difficultyStars:DifficultyStars;
// var difficultyStars:DifficultyStars;
var _exitMovers:Null<FreeplayState.ExitMoverData>; var _exitMovers:Null<FreeplayState.ExitMoverData>;
var albumData:Album; var albumData:Album;
@ -65,9 +65,9 @@ class AlbumRoll extends FlxSpriteGroup
add(newAlbumArt); add(newAlbumArt);
difficultyStars = new DifficultyStars(140, 39); // difficultyStars = new DifficultyStars(140, 39);
difficultyStars.stars.visible = false; // difficultyStars.stars.visible = false;
add(difficultyStars); // add(difficultyStars);
} }
function onAlbumFinish(animName:String):Void function onAlbumFinish(animName:String):Void
@ -86,7 +86,7 @@ class AlbumRoll extends FlxSpriteGroup
{ {
if (albumId == null) if (albumId == null)
{ {
difficultyStars.stars.visible = false; // difficultyStars.stars.visible = false;
return; return;
} }
@ -132,13 +132,6 @@ class AlbumRoll extends FlxSpriteGroup
speed: 0.4, speed: 0.4,
wait: 0 wait: 0
}); });
exitMovers.set([difficultyStars],
{
x: FlxG.width * 1.2,
speed: 0.2,
wait: 0.3
});
} }
var titleTimer:Null<FlxTimer> = null; var titleTimer:Null<FlxTimer> = null;
@ -151,10 +144,10 @@ class AlbumRoll extends FlxSpriteGroup
newAlbumArt.visible = true; newAlbumArt.visible = true;
newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false); newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false);
difficultyStars.stars.visible = false; // difficultyStars.stars.visible = false;
new FlxTimer().start(0.75, function(_) { new FlxTimer().start(0.75, function(_) {
// showTitle(); // showTitle();
showStars(); // showStars();
}); });
} }
@ -163,18 +156,16 @@ class AlbumRoll extends FlxSpriteGroup
newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false); newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false);
} }
public function setDifficultyStars(?difficulty:Int):Void // public function setDifficultyStars(?difficulty:Int):Void
{ // {
if (difficulty == null) return; // if (difficulty == null) return;
// difficultyStars.difficulty = difficulty;
difficultyStars.difficulty = difficulty; // }
} // /**
// * Make the album stars visible.
/** // */
* Make the album stars visible. // public function showStars():Void
*/ // {
public function showStars():Void // difficultyStars.stars.visible = false; // true;
{ // }
difficultyStars.stars.visible = false; // true;
}
} }

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

@ -469,6 +469,10 @@ class FreeplayState extends MusicBeatSubState
albumRoll.playIntro(); albumRoll.playIntro();
new FlxTimer().start(0.75, function(_) {
// albumRoll.showTitle();
});
FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut}); FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
var diffSelLeft:DifficultySelector = new DifficultySelector(20, grpDifficulties.y - 10, false, controls); var diffSelLeft:DifficultySelector = new DifficultySelector(20, grpDifficulties.y - 10, false, controls);
@ -634,14 +638,7 @@ class FreeplayState extends MusicBeatSubState
funnyMenu.favIcon.visible = tempSongs[i].isFav; funnyMenu.favIcon.visible = tempSongs[i].isFav;
funnyMenu.hsvShader = hsvShader; funnyMenu.hsvShader = hsvShader;
if (i < 8)
{
funnyMenu.initJumpIn(Math.min(i, 4), force);
}
else
{
funnyMenu.forcePosition(); funnyMenu.forcePosition();
}
grpCapsules.add(funnyMenu); grpCapsules.add(funnyMenu);
} }
@ -1039,9 +1036,6 @@ class FreeplayState extends MusicBeatSubState
} }
} }
// Set the difficulty star count on the right.
albumRoll.setDifficultyStars(daSong?.songRating);
// Set the album graphic and play the animation if relevant. // Set the album graphic and play the animation if relevant.
var newAlbumId:String = daSong?.albumId; var newAlbumId:String = daSong?.albumId;
if (albumRoll.albumId != newAlbumId) if (albumRoll.albumId != newAlbumId)
@ -1161,10 +1155,6 @@ class FreeplayState extends MusicBeatSubState
{ {
currentDifficulty = rememberedDifficulty; currentDifficulty = rememberedDifficulty;
} }
// Set the difficulty star count on the right.
var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected]?.songData;
albumRoll.setDifficultyStars(daSong?.songRating ?? 0);
} }
function changeSelection(change:Int = 0):Void function changeSelection(change:Int = 0):Void

View file

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

View file

@ -1,5 +1,6 @@
package funkin.ui.mainmenu; package funkin.ui.mainmenu;
import funkin.graphics.FunkinSprite;
import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionableState;
import funkin.ui.debug.DebugMenuSubState; import funkin.ui.debug.DebugMenuSubState;
import flixel.FlxObject; import flixel.FlxObject;
@ -67,7 +68,7 @@ class MainMenuState extends MusicBeatState
camFollow = new FlxObject(0, 0, 1, 1); camFollow = new FlxObject(0, 0, 1, 1);
add(camFollow); add(camFollow);
magenta = new FlxSprite(Paths.image('menuDesat')); magenta = new FlxSprite(Paths.image('menuBGMagenta'));
magenta.scrollFactor.x = bg.scrollFactor.x; magenta.scrollFactor.x = bg.scrollFactor.x;
magenta.scrollFactor.y = bg.scrollFactor.y; magenta.scrollFactor.y = bg.scrollFactor.y;
magenta.setGraphicSize(Std.int(bg.width)); magenta.setGraphicSize(Std.int(bg.width));
@ -75,7 +76,6 @@ class MainMenuState extends MusicBeatState
magenta.x = bg.x; magenta.x = bg.x;
magenta.y = bg.y; magenta.y = bg.y;
magenta.visible = false; magenta.visible = false;
magenta.color = 0xFFfd719b;
// TODO: Why doesn't this line compile I'm going fucking feral // TODO: Why doesn't this line compile I'm going fucking feral

View file

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

View file

@ -281,7 +281,6 @@ class LoadingState extends MusicBeatSubState
{ {
// TODO: This section is a hack! Redo this later when we have a proper asset caching system. // TODO: This section is a hack! Redo this later when we have a proper asset caching system.
FunkinSprite.preparePurgeCache(); FunkinSprite.preparePurgeCache();
FunkinSprite.cacheTexture(Paths.image('combo'));
FunkinSprite.cacheTexture(Paths.image('healthBar')); FunkinSprite.cacheTexture(Paths.image('healthBar'));
FunkinSprite.cacheTexture(Paths.image('menuDesat')); FunkinSprite.cacheTexture(Paths.image('menuDesat'));
FunkinSprite.cacheTexture(Paths.image('combo')); FunkinSprite.cacheTexture(Paths.image('combo'));