diff --git a/.github/actions/setup-haxe/action.yml b/.github/actions/setup-haxe/action.yml new file mode 100644 index 000000000..5a9f7b293 --- /dev/null +++ b/.github/actions/setup-haxe/action.yml @@ -0,0 +1,144 @@ +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 + gh-token: + description: 'GitHub secret for private repos as dependencies' + +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: Prep git for dependency install + uses: gacts/run-and-post-run@v1 + with: + run: git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/ + post: git config --global --unset 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' + + - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} + name: Install dependencies + shell: bash + run: | + haxelib --global run hmm install -q + 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 diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml deleted file mode 100644 index 236d29944..000000000 --- a/.github/actions/setup-haxeshit/action.yml +++ /dev/null @@ -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 diff --git a/.github/actions/upload-itch/action.yml b/.github/actions/upload-itch/action.yml index 2f7d3027d..fb049efc9 100644 --- a/.github/actions/upload-itch/action.yml +++ b/.github/actions/upload-itch/action.yml @@ -1,44 +1,124 @@ name: upload-itch description: "installs Butler, and uploads to itch.io!" + inputs: butler-key: description: "Butler API secret key" required: true + itch-repo: + description: "Where to upload the game to" + required: true + default: "ninja-muffin24/funkin-secret" build-dir: description: "Directory of the game build" - required: true + required: false target: - description: "Target (html5, win, linux, mac)" + description: "Target (html5, windows, linux, macos)" required: true + runs: using: "composite" steps: - - name: Install butler Windows - if: runner.os == 'Windows' - run: | - curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default - 7z x butler.zip - ./butler -v - shell: bash - - name: Install butler Mac - if: runner.os == 'macOS' - run: | - curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default - unzip butler.zip - ./butler -V - shell: bash - - name: Install butler Linux - if: runner.os == 'Linux' - run: | - curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default - unzip butler.zip - chmod +x butler - ./butler -V - shell: bash - - name: Upload game to itch.io - env: - BUTLER_API_KEY: ${{inputs.butler-key}} - run: | - ./butler login - ./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME} - shell: bash + + # RUNNER_OS = Windows | macOS | Linux + # TARGET_BUILD = windows | macos | linux + # TARGET_ITCH = win | macos | linux + # TARGET_BUTLER_DOWNLOAD = windows-amd64 | darwin-amd64 | linux-amd64 + - name: Setup variables + shell: bash + run: | + TARGET_OS=${{ inputs.target }} + RUNNER=${RUNNER_OS@L} + TARGET=${TARGET_OS@L} + case "$TARGET" in + "windows") + TARGET_ITCH=win + ;; + *) + TARGET_ITCH=$TARGET + ;; + esac + case "$RUNNER" in + "macos") + OS_NODE=darwin + ;; + *) + OS_NODE=$RUNNER + ;; + esac + BUTLER_PATH=$RUNNER_TEMP/butler + + echo BUILD_DIR="export/release/$TARGET/bin" >> "$GITHUB_ENV" + echo ITCH_TAG=${{ inputs.itch-repo }}:$TARGET_ITCH-$GITHUB_REF_NAME >> "$GITHUB_ENV" + echo OS_AND_ARCH=$OS_NODE-amd64 >> "$GITHUB_ENV" + echo BUTLER_API_KEY=${{ inputs.butler-key }} >> "$GITHUB_ENV" + echo BUTLER_INSTALL_PATH=$BUTLER_PATH >> "$GITHUB_ENV" + echo TIMER_BUTLER=$(date +%s) >> "$GITHUB_ENV" + echo TARGET_ITCH=$TARGET_ITCH >> "$GITHUB_ENV" + + echo "$BUTLER_PATH" >> "$GITHUB_PATH" + + - name: Get latest butler version + shell: bash + run: | + LATEST=$(curl -sfL https://broth.itch.ovh/butler/$OS_AND_ARCH/LATEST) + echo BUTLER_LATEST=$LATEST >> "$GITHUB_ENV" + + command -v butler \ + && echo BUTLER_CURRENT=$(butler -V 2>&1 | cut -d ',' -f 1) >> "$GITHUB_ENV" \ + || echo BUTLER_CURRENT=none >> "$GITHUB_ENV" + + - name: Try to get butler from cache + id: cache-butler + uses: actions/cache@v4 + with: + path: ${{ env.BUTLER_INSTALL_PATH }} + key: butler-${{ runner.os }}-${{ env.BUTLER_LATEST }} + + - if: steps.cache-butler.outputs.cache-hit == 'true' + name: Make sure butler is executable + shell: bash + run: | + chmod +x $BUTLER_INSTALL_PATH/butler + + - if: steps.cache-butler.outputs.cache-hit != 'true' + name: Install butler + shell: bash + run: | + mkdir -p $BUTLER_INSTALL_PATH + cd $BUTLER_INSTALL_PATH + + curl -L -o butler.zip https://broth.itch.ovh/butler/$OS_AND_ARCH/LATEST/archive/default + unzip butler.zip + chmod +x butler + + - name: Upload game to itch.io + shell: bash + run: | + echo "TIMER_UPLOAD=$(date +%s)" >> "$GITHUB_ENV" + butler login + butler push $BUILD_DIR $ITCH_TAG + echo "TIMER_DONE=$(date +%s)" >> "$GITHUB_ENV" + + - name: Print debug info + shell: bash + run: | + cat << EOF + butler: + version: $( + if [[ "$BUTLER_CURRENT" == "$BUTLER_LATEST" ]] + then + echo $BUTLER_CURRENT + else + echo $BUTLER_CURRENT -> $BUTLER_LATEST + fi + ) + install: + took: $(($TIMER_UPLOAD-$TIMER_BUTLER))s + upload: + tag: $TARGET_ITCH/$GITHUB_REF_NAME + took: $(($TIMER_DONE-$TIMER_UPLOAD))s + EOF + cat << EOF >> "$GITHUB_STEP_SUMMARY" + ### open in launcher: [$TARGET_ITCH/$GITHUB_REF_NAME](https://run.funkin.me/$TARGET_ITCH/$GITHUB_REF_NAME) + EOF diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml new file mode 100644 index 000000000..6fbc9677e --- /dev/null +++ b/.github/workflows/build-docker-image.yml @@ -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 }} diff --git a/.github/workflows/build-game.yml b/.github/workflows/build-game.yml new file mode 100644 index 000000000..07802557c --- /dev/null +++ b/.github/workflows/build-game.yml @@ -0,0 +1,129 @@ +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 + run: | + git config --global --replace-all safe.directory $GITHUB_WORKSPACE + + - name: Get checkout token + uses: actions/create-github-app-token@v1 + id: app_token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PEM }} + owner: ${{ github.repository_owner }} + + - name: Checkout repo + uses: funkincrew/ci-checkout@v6 + with: + submodules: 'recursive' + token: ${{ steps.app_token.outputs.token }} + persist-credentials: false + + - name: Setup build environment + uses: ./.github/actions/setup-haxe + with: + gh-token: ${{ steps.app_token.outputs.token }} + + - name: 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 }} + persist-credentials: false + + - name: Config haxelib + run: | + haxelib --never newrepo + echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV" + + - name: Restore cached dependencies + id: cache-hmm + uses: actions/cache@v4 + with: + path: .haxelib + key: haxe-hmm-${{ runner.os }}-${{ hashFiles('**/hmm.json') }} + + - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} + name: Install dependencies + run: | + git config --global 'url.https://x-access-token:${{ steps.app_token.outputs.token }}@github.com/.insteadOf' https://github.com/ + haxelib --global run hmm install -q + + - 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 }} diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml deleted file mode 100644 index 4d674f025..000000000 --- a/.github/workflows/build-shit.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/cancel-merged-branches.yml b/.github/workflows/cancel-merged-branches.yml index 84e3bedc9..254b21a24 100644 --- a/.github/workflows/cancel-merged-branches.yml +++ b/.github/workflows/cancel-merged-branches.yml @@ -1,35 +1,38 @@ -name: cancel-merged-branches +name: Cancel queued workflows on PR merge + on: pull_request: types: - - closed + - closed jobs: + cancel_stuff: if: github.event.pull_request.merged == true - runs-on: ubuntu-latest + runs-on: build-set permissions: actions: write + steps: - - uses: actions/github-script@v7 - id: cancel-runs - with: - result-encoding: string - retries: 3 - script: | - let branch_workflows = await github.rest.actions.listWorkflowRuns({ + - name: Cancel queued workflows for ${{ github.event.pull_request.head.ref }} + uses: actions/github-script@v7 + with: + result-encoding: string + retries: 3 + script: | + let branch_workflows = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: "build-shit.yml", + status: "queued", + branch: "${{ github.event.pull_request.head.ref }}" + }); + let runs = branch_workflows.data.workflow_runs; + runs.forEach((run) => { + github.rest.actions.cancelWorkflowRun({ owner: context.repo.owner, repo: context.repo.repo, - workflow_id: "build-shit.yml", - status: "queued", - branch: "${{ github.event.pull_request.head.ref }}" + run_id: run.id }); - let runs = branch_workflows.data.workflow_runs; - runs.forEach((run) => { - github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: run.id - }); - }); - console.log(runs); + }); + console.log(runs); diff --git a/.gitignore b/.gitignore index b6a415fe9..84585eee0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ RECOVER_*.fla shitAudio/ .build_time .swp + +# Exclude JS stuff +node_modules/ +package.json +package-lock.json diff --git a/.prettierignore b/.prettierignore index 2e132c06f..ccf886f1a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,3 +9,5 @@ assets/weekend1/images # Don't ignore data files # TODO: These don't work. !assets/preload/data/ + +assets/exclude/data/credits.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 13a1862d2..a8a67245b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -96,6 +96,11 @@ "target": "windows", "args": ["-debug", "-DFORCE_DEBUG_VERSION"] }, + { + "label": "Linux / Debug", + "target": "linux", + "args": ["-debug", "-DFORCE_DEBUG_VERSION"] + }, { "label": "HashLink / Debug", "target": "hl", @@ -130,6 +135,11 @@ "-DFORCE_DEBUG_VERSION" ] }, + { + "label": "Windows / Debug (Straight to Play - 2hot)", + "target": "windows", + "args": ["-debug", "-DSONG=2hot", "-DFORCE_DEBUG_VERSION"] + }, { "label": "HashLink / Debug (Straight to Play - Bopeebo Normal)", "target": "hl", @@ -160,6 +170,11 @@ "target": "windows", "args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"] }, + { + "label": "Windows / Debug (Debug hxCodec)", + "target": "windows", + "args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFORCE_DEBUG_VERSION"] + }, { "label": "HashLink / Debug (Straight to Animation Editor)", "target": "hl", diff --git a/Project.xml b/Project.xml index 4b81fd07b..0a44a268c 100644 --- a/Project.xml +++ b/Project.xml @@ -45,6 +45,7 @@ +
@@ -58,10 +59,13 @@ +
+ + @@ -119,7 +123,9 @@ - + + + @@ -233,6 +239,10 @@ + + + +
diff --git a/assets b/assets index ad6ed62bd..e6c707ede 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ad6ed62bd254456b9584b02ed0a5402118b033a8 +Subproject commit e6c707ededfa1a41c08756a6fc4ec0e2848c3734 diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 000000000..c545d1364 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,186 @@ +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 <> /etc/apt/apt.conf.d/10apt-autoremove +APT::Get::AutomaticRemove "0"; +APT::Get::HideAutoRemove "1"; +EOC + +echo <> /etc/apt/apt.conf.d/80retries +"APT::Acquire::Retries \"10\";" +EOC + +echo <> /etc/apt/apt.conf.d/90assumeyes +"APT::Get::Assume-Yes \"true\";" +EOC +EOF + +# Prepare apt-fast +RUN <> /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 < = null; - static function get_instance():Conductor - { - if (_instance == null) _instance = new Conductor(); - return _instance; - } - /** - * Signal fired when the current Conductor instance advances to a new measure. + * Signal fired when the current static Conductor instance advances to a new measure. */ public static var measureHit(default, null):FlxSignal = new FlxSignal(); + /** + * Signal fired when THIS Conductor instance advances to a new measure. + * TODO: This naming sucks but we can't make a static and instance field with the same name! + */ + public var onMeasureHit(default, null):FlxSignal = new FlxSignal(); + /** * Signal fired when the current Conductor instance advances to a new beat. */ public static var beatHit(default, null):FlxSignal = new FlxSignal(); + /** + * Signal fired when THIS Conductor instance advances to a new beat. + * TODO: This naming sucks but we can't make a static and instance field with the same name! + */ + public var onBeatHit(default, null):FlxSignal = new FlxSignal(); + /** * Signal fired when the current Conductor instance advances to a new step. */ public static var stepHit(default, null):FlxSignal = new FlxSignal(); + /** + * Signal fired when THIS Conductor instance advances to a new step. + * TODO: This naming sucks but we can't make a static and instance field with the same name! + */ + public var onStepHit(default, null):FlxSignal = new FlxSignal(); + /** * The list of time changes in the song. * There should be at least one time change (at the beginning of the song) to define the BPM. @@ -286,6 +298,69 @@ class Conductor return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT); } + /** + * Reset the Conductor, replacing the current instance with a fresh one. + */ + public static function reset():Void + { + set_instance(new Conductor()); + } + + static function dispatchMeasureHit():Void + { + Conductor.measureHit.dispatch(); + } + + static function dispatchBeatHit():Void + { + Conductor.beatHit.dispatch(); + } + + static function dispatchStepHit():Void + { + Conductor.stepHit.dispatch(); + } + + static function setupSingleton(input:Conductor):Void + { + input.onMeasureHit.add(dispatchMeasureHit); + + input.onBeatHit.add(dispatchBeatHit); + + input.onStepHit.add(dispatchStepHit); + } + + static function clearSingleton(input:Conductor):Void + { + input.onMeasureHit.remove(dispatchMeasureHit); + + input.onBeatHit.remove(dispatchBeatHit); + + input.onStepHit.remove(dispatchStepHit); + } + + static function get_instance():Conductor + { + if (Conductor._instance == null) set_instance(new Conductor()); + if (Conductor._instance == null) throw "Could not initialize singleton Conductor!"; + return Conductor._instance; + } + + static function set_instance(instance:Conductor):Conductor + { + // Use _instance in here to avoid recursion + if (Conductor._instance != null) clearSingleton(Conductor._instance); + + Conductor._instance = instance; + + if (Conductor._instance != null) setupSingleton(Conductor._instance); + + return Conductor._instance; + } + + /** + * The constructor. + */ public function new() {} /** @@ -296,6 +371,7 @@ class Conductor * * WARNING: Avoid this for things like setting the BPM of the title screen music, * you should have a metadata file for it instead. + * We should probably deprecate this in the future. */ public function forceBPM(?bpm:Float):Void { @@ -372,24 +448,20 @@ class Conductor this.currentMeasure = Math.floor(currentMeasureTime); } - // Only fire the signal if we are THE Conductor. - if (this == Conductor.instance || forceDispatch) + // FlxSignals are really cool. + if (currentStep != oldStep) { - // FlxSignals are really cool. - if (currentStep != oldStep) - { - Conductor.stepHit.dispatch(); - } + this.onStepHit.dispatch(); + } - if (currentBeat != oldBeat) - { - Conductor.beatHit.dispatch(); - } + if (currentBeat != oldBeat) + { + this.onBeatHit.dispatch(); + } - if (currentMeasure != oldMeasure) - { - Conductor.measureHit.dispatch(); - } + if (currentMeasure != oldMeasure) + { + this.onMeasureHit.dispatch(); } // only update the timestamp if songPosition actually changed @@ -589,22 +661,14 @@ class Conductor * Adds Conductor fields to the Flixel debugger variable display. * @param conductorToUse The conductor to use. Defaults to `Conductor.instance`. */ - public static function watchQuick(?conductorToUse:Conductor):Void + public static function watchQuick(?target:Conductor):Void { - if (conductorToUse == null) conductorToUse = Conductor.instance; + if (target == null) target = Conductor.instance; - FlxG.watch.addQuick("songPosition", conductorToUse.songPosition); - FlxG.watch.addQuick("bpm", conductorToUse.bpm); - FlxG.watch.addQuick("currentMeasureTime", conductorToUse.currentMeasureTime); - FlxG.watch.addQuick("currentBeatTime", conductorToUse.currentBeatTime); - FlxG.watch.addQuick("currentStepTime", conductorToUse.currentStepTime); - } - - /** - * Reset the Conductor, replacing the current instance with a fresh one. - */ - public static function reset():Void - { - _instance = new Conductor(); + FlxG.watch.addQuick('songPosition', target.songPosition); + FlxG.watch.addQuick('bpm', target.bpm); + FlxG.watch.addQuick('currentMeasureTime', target.currentMeasureTime); + FlxG.watch.addQuick('currentBeatTime', target.currentBeatTime); + FlxG.watch.addQuick('currentStepTime', target.currentStepTime); } } diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 59c19d6ce..2377343c3 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -261,6 +261,35 @@ class InitState extends FlxState return; } + // TODO: Rework loading behavior so we don't have to do this. + switch (songId) + { + case 'tutorial' | 'bopeebo' | 'fresh' | 'dadbattle': + Paths.setCurrentLevel('week1'); + PlayStatePlaylist.campaignId = 'week1'; + case 'spookeez' | 'south' | 'monster': + Paths.setCurrentLevel('week2'); + PlayStatePlaylist.campaignId = 'week2'; + case 'pico' | 'philly-nice' | 'blammed': + Paths.setCurrentLevel('week3'); + PlayStatePlaylist.campaignId = 'week3'; + case 'high' | 'satin-panties' | 'milf': + Paths.setCurrentLevel('week4'); + PlayStatePlaylist.campaignId = 'week4'; + case 'cocoa' | 'eggnog' | 'winter-horrorland': + Paths.setCurrentLevel('week5'); + PlayStatePlaylist.campaignId = 'week5'; + case 'senpai' | 'roses' | 'thorns': + Paths.setCurrentLevel('week6'); + PlayStatePlaylist.campaignId = 'week6'; + case 'ugh' | 'guns' | 'stress': + Paths.setCurrentLevel('week7'); + PlayStatePlaylist.campaignId = 'week7'; + case 'darnell' | 'lit-up' | '2hot' | 'blazin': + Paths.setCurrentLevel('weekend1'); + PlayStatePlaylist.campaignId = 'weekend1'; + } + LoadingState.loadPlayState( { targetSong: songData, @@ -283,6 +312,10 @@ class InitState extends FlxState return; } + // TODO: Rework loading behavior so we don't have to do this. + Paths.setCurrentLevel(levelId); + PlayStatePlaylist.campaignId = levelId; + PlayStatePlaylist.playlistSongIds = currentLevel.getSongs(); PlayStatePlaylist.isStoryMode = true; PlayStatePlaylist.campaignScore = 0; diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index fd4ef76fa..54a4b7acf 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -113,7 +113,7 @@ class Paths public static function videos(key:String, ?library:String):String { - return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library); + return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library ?? 'videos'); } public static function voices(song:String, ?suffix:String = ''):String diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 8c1bf3b41..df05cc3ef 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -223,11 +223,12 @@ class FunkinSound extends FlxSound implements ICloneable // already paused before we lost focus. if (_lostFocus && !_alreadyPaused) { + trace('Resuming audio (${this._label}) on focus!'); resume(); } else { - trace('Not resuming audio on focus!'); + trace('Not resuming audio (${this._label}) on focus!'); } _lostFocus = false; } @@ -402,6 +403,12 @@ class FunkinSound extends FlxSound implements ICloneable sound.group = FlxG.sound.defaultSoundGroup; 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 if (onLoad != null && sound._sound != null) onLoad(); diff --git a/source/funkin/audio/visualize/ABotVis.hx b/source/funkin/audio/visualize/ABotVis.hx index 89b004df4..5874b8921 100644 --- a/source/funkin/audio/visualize/ABotVis.hx +++ b/source/funkin/audio/visualize/ABotVis.hx @@ -8,153 +8,180 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.math.FlxMath; import flixel.sound.FlxSound; import funkin.util.MathUtil; +import funkVis.dsp.SpectralAnalyzer; +import funkVis.audioclip.frontends.LimeAudioClip; using Lambda; class ABotVis extends FlxTypedSpriteGroup { - public var vis:VisShit; + // public var vis:VisShit; + var analyzer:SpectralAnalyzer; var volumes:Array = []; + public var snd:FlxSound; + public function new(snd:FlxSound) { super(); - vis = new VisShit(snd); + this.snd = snd; + + // vis = new VisShit(snd); // vis.snd = snd; var visFrms:FlxAtlasFrames = Paths.getSparrowAtlas('aBotViz'); + // these are the differences in X position, from left to right + var positionX:Array = [0, 59, 56, 66, 54, 52, 51]; + var positionY:Array = [0, -8, -3.5, -0.4, 0.5, 4.7, 7]; + for (lol in 1...8) { // pushes initial value volumes.push(0.0); + var sum = function(num:Float, total:Float) return total += num; + var posX:Float = positionX.slice(0, lol).fold(sum, 0); + var posY:Float = positionY.slice(0, lol).fold(sum, 0); - var viz:FlxSprite = new FlxSprite(50 * lol, 0); + var viz:FlxSprite = new FlxSprite(posX, posY); viz.frames = visFrms; add(viz); - var visStr = 'VIZ'; - if (lol == 5) visStr = 'viz'; // lol makes it lowercase, accomodates for art that I dont wanna rename! - + var visStr = 'viz'; viz.animation.addByPrefix('VIZ', visStr + lol, 0); - viz.animation.play('VIZ', false, false, -1); + viz.animation.play('VIZ', false, false, 2); } } + public function initAnalyzer() + { + @:privateAccess + analyzer = new SpectralAnalyzer(7, new LimeAudioClip(cast snd._channel.__source), 0.01, 30); + analyzer.maxDb = -35; + // analyzer.fftN = 2048; + } + + var visTimer:Float = -1; + var visTimeMax:Float = 1 / 30; + override function update(elapsed:Float) { // updateViz(); - updateFFT(elapsed); + // updateFFT(elapsed); + // super.update(elapsed); } - function updateFFT(elapsed:Float) + static inline function min(x:Int, y:Int):Int { - if (vis.snd != null) + return x > y ? y : x; + } + + override function draw() + { + #if web + if (analyzer != null) drawFFT(); + #end + super.draw(); + } + + /** + * TJW funkVis based visualizer! updateFFT() is the old nasty shit that dont worky! + */ + function drawFFT():Void + { + var levels = analyzer.getLevels(false); + + for (i in 0...min(group.members.length, levels.length)) { - vis.checkAndSetBuffer(); + var animFrame:Int = Math.round(levels[i].value * 5); - if (vis.setBuffer) - { - var remappedShit:Int = 0; + animFrame = Math.floor(Math.min(5, animFrame)); + animFrame = Math.floor(Math.max(0, animFrame)); - if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples)); - else - remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples)); + animFrame = Std.int(Math.abs(animFrame - 5)); // shitty dumbass flip, cuz dave got da shit backwards lol! - var fftSamples:Array = []; - - var swagBucks = remappedShit; - - for (i in remappedShit...remappedShit + (Std.int((44100 * (1 / 144))))) - { - var left = vis.audioData[swagBucks] / 32767; - var right = vis.audioData[swagBucks + 1] / 32767; - - var balanced = (left + right) / 2; - - swagBucks += 2; - - fftSamples.push(balanced); - } - - var freqShit = vis.funnyFFT(fftSamples); - - for (i in 0...group.members.length) - { - var getSliceShit = function(s:Int) { - var powShit = FlxMath.remapToRange(s, 0, group.members.length, 0, MathUtil.logBase(10, freqShit[0].length)); - return Math.round(Math.pow(10, powShit)); - }; - - // var powShit:Float = getSliceShit(i); - var hzSliced:Int = getSliceShit(i); - - var sliceLength:Int = Std.int(freqShit[0].length / group.members.length); - - var volSlice = freqShit[0].slice(hzSliced, getSliceShit(i + 1)); - - var avgVel:Float = 0; - - for (slice in volSlice) - { - avgVel += slice; - } - - avgVel /= volSlice.length; - - avgVel *= 10000000; - - volumes[i] += avgVel - (elapsed * (volumes[i] * 50)); - - var animFrame:Int = Std.int(volumes[i]); - - animFrame = Math.floor(Math.min(5, animFrame)); - animFrame = Math.floor(Math.max(0, animFrame)); - - animFrame = Std.int(Math.abs(animFrame - 5)); // shitty dumbass flip, cuz dave got da shit backwards lol! - - group.members[i].animation.curAnim.curFrame = animFrame; - if (FlxG.keys.justPressed.U) - { - trace(avgVel); - trace(group.members[i].animation.curAnim.curFrame); - } - } - - // group.members[0].animation.curAnim.curFrame = - } + group.members[i].animation.curAnim.curFrame = animFrame; } } - public function updateViz() - { - if (vis.snd != null) - { - var remappedShit:Int = 0; - vis.checkAndSetBuffer(); - - if (vis.setBuffer) - { - // var startingSample:Int = Std.int(FlxMath.remapToRange) - - if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples)); - - for (i in 0...group.members.length) - { - var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, remappedShit, remappedShit + 500)); - - var left = vis.audioData[sampleApprox] / 32767; - - var animFrame:Int = Std.int(FlxMath.remapToRange(left, -1, 1, 0, 6)); - - group.members[i].animation.curAnim.curFrame = animFrame; - } - } - } - } + // function updateFFT(elapsed:Float) + // { + // if (vis.snd != null) + // { + // vis.checkAndSetBuffer(); + // if (vis.setBuffer) + // { + // var remappedShit:Int = 0; + // if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples)); + // else + // remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples)); + // var fftSamples:Array = []; + // var swagBucks = remappedShit; + // for (i in remappedShit...remappedShit + (Std.int((44100 * (1 / 144))))) + // { + // var left = vis.audioData[swagBucks] / 32767; + // var right = vis.audioData[swagBucks + 1] / 32767; + // var balanced = (left + right) / 2; + // swagBucks += 2; + // fftSamples.push(balanced); + // } + // var freqShit = vis.funnyFFT(fftSamples); + // for (i in 0...group.members.length) + // { + // var getSliceShit = function(s:Int) { + // var powShit = FlxMath.remapToRange(s, 0, group.members.length, 0, MathUtil.logBase(10, freqShit[0].length)); + // return Math.round(Math.pow(10, powShit)); + // }; + // // var powShit:Float = getSliceShit(i); + // var hzSliced:Int = getSliceShit(i); + // var sliceLength:Int = Std.int(freqShit[0].length / group.members.length); + // var volSlice = freqShit[0].slice(hzSliced, getSliceShit(i + 1)); + // var avgVel:Float = 0; + // for (slice in volSlice) + // { + // avgVel += slice; + // } + // avgVel /= volSlice.length; + // avgVel *= 10000000; + // volumes[i] += avgVel - (elapsed * (volumes[i] * 50)); + // var animFrame:Int = Std.int(volumes[i]); + // animFrame = Math.floor(Math.min(5, animFrame)); + // animFrame = Math.floor(Math.max(0, animFrame)); + // animFrame = Std.int(Math.abs(animFrame - 5)); // shitty dumbass flip, cuz dave got da shit backwards lol! + // group.members[i].animation.curAnim.curFrame = animFrame; + // if (FlxG.keys.justPressed.U) + // { + // trace(avgVel); + // trace(group.members[i].animation.curAnim.curFrame); + // } + // } + // // group.members[0].animation.curAnim.curFrame = + // } + // } + // } + // public function updateViz() + // { + // if (vis.snd != null) + // { + // var remappedShit:Int = 0; + // vis.checkAndSetBuffer(); + // if (vis.setBuffer) + // { + // // var startingSample:Int = Std.int(FlxMath.remapToRange) + // if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples)); + // for (i in 0...group.members.length) + // { + // var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, remappedShit, remappedShit + 500)); + // var left = vis.audioData[sampleApprox] / 32767; + // var animFrame:Int = Std.int(FlxMath.remapToRange(left, -1, 1, 0, 6)); + // group.members[i].animation.curAnim.curFrame = animFrame; + // } + // } + // } + // } } diff --git a/source/funkin/graphics/FunkinCamera.hx b/source/funkin/graphics/FunkinCamera.hx index 90861c263..8c8a1c9a7 100644 --- a/source/funkin/graphics/FunkinCamera.hx +++ b/source/funkin/graphics/FunkinCamera.hx @@ -47,9 +47,13 @@ class FunkinCamera extends FlxCamera public var shouldDraw:Bool = true; - public function new(x:Int = 0, y:Int = 0, width:Int = 0, height:Int = 0, zoom:Float = 0) + // Used to identify the camera during debugging. + final id:String = 'unknown'; + + public function new(id:String = 'unknown', x:Int = 0, y:Int = 0, width:Int = 0, height:Int = 0, zoom:Float = 0) { super(x, y, width, height, zoom); + this.id = id; bgTexture = pickTexture(width, height); bgBitmap = FixedBitmapData.fromTexture(bgTexture); bgFrame = new FlxFrame(new FlxGraphic('', null)); diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx index 5394bce1a..8a77c1c85 100644 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -37,6 +37,11 @@ class FlxAtlasSprite extends FlxAnimate { if (settings == null) settings = SETTINGS; + if (path == null) + { + throw 'Null path specified for FlxAtlasSprite!'; + } + super(x, y, path, settings); if (this.anim.curInstance == null) @@ -137,7 +142,8 @@ class FlxAtlasSprite extends FlxAnimate anim.callback = function(_, frame:Int) { var offset = loop ? 0 : -1; - if (frame == (anim.getFrameLabel(id).duration + offset) + anim.getFrameLabel(id).index) + var frameLabel = anim.getFrameLabel(id); + if (frame == (frameLabel.duration + offset) + frameLabel.index) { if (loop) { diff --git a/source/funkin/graphics/shaders/GaussianBlurShader.hx b/source/funkin/graphics/shaders/GaussianBlurShader.hx index 81167655b..cecfdab80 100644 --- a/source/funkin/graphics/shaders/GaussianBlurShader.hx +++ b/source/funkin/graphics/shaders/GaussianBlurShader.hx @@ -20,6 +20,6 @@ class GaussianBlurShader extends FlxRuntimeShader public function setAmount(value:Float):Void { this.amount = value; - this.setFloat("amount", amount); + this.setFloat("_amount", amount); } } diff --git a/source/funkin/graphics/shaders/Grayscale.hx b/source/funkin/graphics/shaders/Grayscale.hx index 6673ace24..fbd0970e5 100644 --- a/source/funkin/graphics/shaders/Grayscale.hx +++ b/source/funkin/graphics/shaders/Grayscale.hx @@ -17,6 +17,6 @@ class Grayscale extends FlxRuntimeShader public function setAmount(value:Float):Void { amount = value; - this.setFloat("amount", amount); + this.setFloat("_amount", amount); } } diff --git a/source/funkin/graphics/shaders/HSVShader.hx b/source/funkin/graphics/shaders/HSVShader.hx index 9b6ae4cdd..587008ce2 100644 --- a/source/funkin/graphics/shaders/HSVShader.hx +++ b/source/funkin/graphics/shaders/HSVShader.hx @@ -13,6 +13,7 @@ class HSVShader extends FlxRuntimeShader public function new() { super(Assets.getText(Paths.frag('hsv'))); + FlxG.debugger.addTrackerProfile(new TrackerProfile(HSVShader, ['hue', 'saturation', 'value'])); hue = 1; saturation = 1; value = 1; diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx index 0857678d0..14b226d88 100644 --- a/source/funkin/input/Controls.hx +++ b/source/funkin/input/Controls.hx @@ -30,44 +30,44 @@ class Controls extends FlxActionSet * A list of actions that a player would invoke via some input device. * Uses FlxActions to funnel various inputs to a single action. */ - var _ui_up = new FlxActionDigital(Action.UI_UP); - var _ui_left = new FlxActionDigital(Action.UI_LEFT); - var _ui_right = new FlxActionDigital(Action.UI_RIGHT); - var _ui_down = new FlxActionDigital(Action.UI_DOWN); - var _ui_upP = new FlxActionDigital(Action.UI_UP_P); - var _ui_leftP = new FlxActionDigital(Action.UI_LEFT_P); - var _ui_rightP = new FlxActionDigital(Action.UI_RIGHT_P); - var _ui_downP = new FlxActionDigital(Action.UI_DOWN_P); - var _ui_upR = new FlxActionDigital(Action.UI_UP_R); - var _ui_leftR = new FlxActionDigital(Action.UI_LEFT_R); - var _ui_rightR = new FlxActionDigital(Action.UI_RIGHT_R); - var _ui_downR = new FlxActionDigital(Action.UI_DOWN_R); - var _note_up = new FlxActionDigital(Action.NOTE_UP); - var _note_left = new FlxActionDigital(Action.NOTE_LEFT); - var _note_right = new FlxActionDigital(Action.NOTE_RIGHT); - var _note_down = new FlxActionDigital(Action.NOTE_DOWN); - var _note_upP = new FlxActionDigital(Action.NOTE_UP_P); - var _note_leftP = new FlxActionDigital(Action.NOTE_LEFT_P); - var _note_rightP = new FlxActionDigital(Action.NOTE_RIGHT_P); - var _note_downP = new FlxActionDigital(Action.NOTE_DOWN_P); - var _note_upR = new FlxActionDigital(Action.NOTE_UP_R); - var _note_leftR = new FlxActionDigital(Action.NOTE_LEFT_R); - var _note_rightR = new FlxActionDigital(Action.NOTE_RIGHT_R); - var _note_downR = new FlxActionDigital(Action.NOTE_DOWN_R); - var _accept = new FlxActionDigital(Action.ACCEPT); - var _back = new FlxActionDigital(Action.BACK); - var _pause = new FlxActionDigital(Action.PAUSE); - var _reset = new FlxActionDigital(Action.RESET); - var _screenshot = new FlxActionDigital(Action.SCREENSHOT); - var _cutscene_advance = new FlxActionDigital(Action.CUTSCENE_ADVANCE); - var _debug_menu = new FlxActionDigital(Action.DEBUG_MENU); - var _debug_chart = new FlxActionDigital(Action.DEBUG_CHART); - var _debug_stage = new FlxActionDigital(Action.DEBUG_STAGE); - var _volume_up = new FlxActionDigital(Action.VOLUME_UP); - var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN); - var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE); + var _ui_up = new FunkinAction(Action.UI_UP); + var _ui_left = new FunkinAction(Action.UI_LEFT); + var _ui_right = new FunkinAction(Action.UI_RIGHT); + var _ui_down = new FunkinAction(Action.UI_DOWN); + var _ui_upP = new FunkinAction(Action.UI_UP_P); + var _ui_leftP = new FunkinAction(Action.UI_LEFT_P); + var _ui_rightP = new FunkinAction(Action.UI_RIGHT_P); + var _ui_downP = new FunkinAction(Action.UI_DOWN_P); + var _ui_upR = new FunkinAction(Action.UI_UP_R); + var _ui_leftR = new FunkinAction(Action.UI_LEFT_R); + var _ui_rightR = new FunkinAction(Action.UI_RIGHT_R); + var _ui_downR = new FunkinAction(Action.UI_DOWN_R); + var _note_up = new FunkinAction(Action.NOTE_UP); + var _note_left = new FunkinAction(Action.NOTE_LEFT); + var _note_right = new FunkinAction(Action.NOTE_RIGHT); + var _note_down = new FunkinAction(Action.NOTE_DOWN); + var _note_upP = new FunkinAction(Action.NOTE_UP_P); + var _note_leftP = new FunkinAction(Action.NOTE_LEFT_P); + var _note_rightP = new FunkinAction(Action.NOTE_RIGHT_P); + var _note_downP = new FunkinAction(Action.NOTE_DOWN_P); + var _note_upR = new FunkinAction(Action.NOTE_UP_R); + var _note_leftR = new FunkinAction(Action.NOTE_LEFT_R); + var _note_rightR = new FunkinAction(Action.NOTE_RIGHT_R); + var _note_downR = new FunkinAction(Action.NOTE_DOWN_R); + var _accept = new FunkinAction(Action.ACCEPT); + var _back = new FunkinAction(Action.BACK); + var _pause = new FunkinAction(Action.PAUSE); + var _reset = new FunkinAction(Action.RESET); + var _screenshot = new FunkinAction(Action.SCREENSHOT); + var _cutscene_advance = new FunkinAction(Action.CUTSCENE_ADVANCE); + var _debug_menu = new FunkinAction(Action.DEBUG_MENU); + var _debug_chart = new FunkinAction(Action.DEBUG_CHART); + var _debug_stage = new FunkinAction(Action.DEBUG_STAGE); + var _volume_up = new FunkinAction(Action.VOLUME_UP); + var _volume_down = new FunkinAction(Action.VOLUME_DOWN); + var _volume_mute = new FunkinAction(Action.VOLUME_MUTE); - var byName:Map = new Map(); + var byName:Map = new Map(); public var gamepadsAdded:Array = []; public var keyboardScheme = KeyboardScheme.None; @@ -75,122 +75,142 @@ class Controls extends FlxActionSet public var UI_UP(get, never):Bool; inline function get_UI_UP() - return _ui_up.check(); + return _ui_up.checkPressed(); public var UI_LEFT(get, never):Bool; inline function get_UI_LEFT() - return _ui_left.check(); + return _ui_left.checkPressed(); public var UI_RIGHT(get, never):Bool; inline function get_UI_RIGHT() - return _ui_right.check(); + return _ui_right.checkPressed(); public var UI_DOWN(get, never):Bool; inline function get_UI_DOWN() - return _ui_down.check(); + return _ui_down.checkPressed(); public var UI_UP_P(get, never):Bool; inline function get_UI_UP_P() - return _ui_upP.check(); + return _ui_up.checkJustPressed(); public var UI_LEFT_P(get, never):Bool; inline function get_UI_LEFT_P() - return _ui_leftP.check(); + return _ui_left.checkJustPressed(); public var UI_RIGHT_P(get, never):Bool; inline function get_UI_RIGHT_P() - return _ui_rightP.check(); + return _ui_right.checkJustPressed(); public var UI_DOWN_P(get, never):Bool; inline function get_UI_DOWN_P() - return _ui_downP.check(); + return _ui_down.checkJustPressed(); public var UI_UP_R(get, never):Bool; inline function get_UI_UP_R() - return _ui_upR.check(); + return _ui_up.checkJustReleased(); public var UI_LEFT_R(get, never):Bool; inline function get_UI_LEFT_R() - return _ui_leftR.check(); + return _ui_left.checkJustReleased(); public var UI_RIGHT_R(get, never):Bool; inline function get_UI_RIGHT_R() - return _ui_rightR.check(); + return _ui_right.checkJustReleased(); public var UI_DOWN_R(get, never):Bool; inline function get_UI_DOWN_R() - return _ui_downR.check(); + return _ui_down.checkJustReleased(); + + public var UI_UP_GAMEPAD(get, never):Bool; + + inline function get_UI_UP_GAMEPAD() + return _ui_up.checkPressedGamepad(); + + public var UI_LEFT_GAMEPAD(get, never):Bool; + + inline function get_UI_LEFT_GAMEPAD() + return _ui_left.checkPressedGamepad(); + + public var UI_RIGHT_GAMEPAD(get, never):Bool; + + inline function get_UI_RIGHT_GAMEPAD() + return _ui_right.checkPressedGamepad(); + + public var UI_DOWN_GAMEPAD(get, never):Bool; + + inline function get_UI_DOWN_GAMEPAD() + return _ui_down.checkPressedGamepad(); public var NOTE_UP(get, never):Bool; inline function get_NOTE_UP() - return _note_up.check(); + return _note_up.checkPressed(); public var NOTE_LEFT(get, never):Bool; inline function get_NOTE_LEFT() - return _note_left.check(); + return _note_left.checkPressed(); public var NOTE_RIGHT(get, never):Bool; inline function get_NOTE_RIGHT() - return _note_right.check(); + return _note_right.checkPressed(); public var NOTE_DOWN(get, never):Bool; inline function get_NOTE_DOWN() - return _note_down.check(); + return _note_down.checkPressed(); public var NOTE_UP_P(get, never):Bool; inline function get_NOTE_UP_P() - return _note_upP.check(); + return _note_up.checkJustPressed(); public var NOTE_LEFT_P(get, never):Bool; inline function get_NOTE_LEFT_P() - return _note_leftP.check(); + return _note_left.checkJustPressed(); public var NOTE_RIGHT_P(get, never):Bool; inline function get_NOTE_RIGHT_P() - return _note_rightP.check(); + return _note_right.checkJustPressed(); public var NOTE_DOWN_P(get, never):Bool; inline function get_NOTE_DOWN_P() - return _note_downP.check(); + return _note_down.checkJustPressed(); public var NOTE_UP_R(get, never):Bool; inline function get_NOTE_UP_R() - return _note_upR.check(); + return _note_up.checkJustReleased(); public var NOTE_LEFT_R(get, never):Bool; inline function get_NOTE_LEFT_R() - return _note_leftR.check(); + return _note_left.checkJustReleased(); public var NOTE_RIGHT_R(get, never):Bool; inline function get_NOTE_RIGHT_R() - return _note_rightR.check(); + return _note_right.checkJustReleased(); public var NOTE_DOWN_R(get, never):Bool; inline function get_NOTE_DOWN_R() - return _note_downR.check(); + return _note_down.checkJustReleased(); public var ACCEPT(get, never):Bool; @@ -260,26 +280,10 @@ class Controls extends FlxActionSet add(_ui_left); add(_ui_right); add(_ui_down); - add(_ui_upP); - add(_ui_leftP); - add(_ui_rightP); - add(_ui_downP); - add(_ui_upR); - add(_ui_leftR); - add(_ui_rightR); - add(_ui_downR); add(_note_up); add(_note_left); add(_note_right); add(_note_down); - add(_note_upP); - add(_note_leftP); - add(_note_rightP); - add(_note_downP); - add(_note_upR); - add(_note_leftR); - add(_note_rightR); - add(_note_downR); add(_accept); add(_back); add(_pause); @@ -293,8 +297,16 @@ class Controls extends FlxActionSet add(_volume_down); add(_volume_mute); - for (action in digitalActions) - byName[action.name] = action; + for (action in digitalActions) { + if (Std.isOfType(action, FunkinAction)) { + var funkinAction:FunkinAction = cast action; + byName[funkinAction.name] = funkinAction; + if (funkinAction.namePressed != null) + byName[funkinAction.namePressed] = funkinAction; + if (funkinAction.nameReleased != null) + byName[funkinAction.nameReleased] = funkinAction; + } + } if (scheme == null) scheme = None; @@ -307,14 +319,17 @@ class Controls extends FlxActionSet super.update(); } - // inline - public function checkByName(name:Action):Bool + public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool { #if debug if (!byName.exists(name)) throw 'Invalid name: $name'; #end - return byName[name].check(); + var action = byName[name]; + if (gamepadOnly) + return action.checkFiltered(trigger, GAMEPAD); + else + return action.checkFiltered(trigger); } public function getKeysForAction(name:Action):Array { @@ -405,36 +420,36 @@ class Controls extends FlxActionSet { case UI_UP: func(_ui_up, PRESSED); - func(_ui_upP, JUST_PRESSED); - func(_ui_upR, JUST_RELEASED); + func(_ui_up, JUST_PRESSED); + func(_ui_up, JUST_RELEASED); case UI_LEFT: func(_ui_left, PRESSED); - func(_ui_leftP, JUST_PRESSED); - func(_ui_leftR, JUST_RELEASED); + func(_ui_left, JUST_PRESSED); + func(_ui_left, JUST_RELEASED); case UI_RIGHT: func(_ui_right, PRESSED); - func(_ui_rightP, JUST_PRESSED); - func(_ui_rightR, JUST_RELEASED); + func(_ui_right, JUST_PRESSED); + func(_ui_right, JUST_RELEASED); case UI_DOWN: func(_ui_down, PRESSED); - func(_ui_downP, JUST_PRESSED); - func(_ui_downR, JUST_RELEASED); + func(_ui_down, JUST_PRESSED); + func(_ui_down, JUST_RELEASED); case NOTE_UP: func(_note_up, PRESSED); - func(_note_upP, JUST_PRESSED); - func(_note_upR, JUST_RELEASED); + func(_note_up, JUST_PRESSED); + func(_note_up, JUST_RELEASED); case NOTE_LEFT: func(_note_left, PRESSED); - func(_note_leftP, JUST_PRESSED); - func(_note_leftR, JUST_RELEASED); + func(_note_left, JUST_PRESSED); + func(_note_left, JUST_RELEASED); case NOTE_RIGHT: func(_note_right, PRESSED); - func(_note_rightP, JUST_PRESSED); - func(_note_rightR, JUST_RELEASED); + func(_note_right, JUST_PRESSED); + func(_note_right, JUST_RELEASED); case NOTE_DOWN: func(_note_down, PRESSED); - func(_note_downP, JUST_PRESSED); - func(_note_downR, JUST_RELEASED); + func(_note_down, JUST_PRESSED); + func(_note_down, JUST_RELEASED); case ACCEPT: func(_accept, JUST_PRESSED); case BACK: @@ -1042,6 +1057,173 @@ typedef Swipes = ?curTouchPos:FlxPoint }; +/** + * An FlxActionDigital with additional functionality, including: + * - Combining `pressed` and `released` inputs into one action. + * - Filtering by input method (`KEYBOARD`, `MOUSE`, `GAMEPAD`, etc). + */ +class FunkinAction extends FlxActionDigital { + public var namePressed(default, null):Null; + public var nameReleased(default, null):Null; + + var cache:Map = []; + + 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, ?filterDevices:Array):Bool { + if (filterTriggers == null) { + filterTriggers = [PRESSED, JUST_PRESSED]; + } + if (filterDevices == null) { + filterDevices = []; + } + + // Perform checkFiltered for each combination. + for (i in filterTriggers) { + if (filterDevices.length == 0) { + if (checkFiltered(i)) { + return true; + } + } else { + for (j in filterDevices) { + if (checkFiltered(i, j)) { + return true; + } + } + } + } + return false; + } + + /** + * Performs the functionality of `FlxActionDigital.check()`, but with optional filters. + * @param action The action to check for. + * @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`). + * @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`). + */ + public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool { + // The normal + + // Make sure we only update the inputs once per frame. + var key = '${filterTrigger}:${filterDevice}'; + var cacheEntry = cache.get(key); + + if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks) { + return cacheEntry.value; + } + // Use a for loop instead so we can remove inputs while iterating. + + // We don't return early because we need to call check() on ALL inputs. + var result = false; + var len = inputs != null ? inputs.length : 0; + for (i in 0...len) + { + var j = len - i - 1; + var input = inputs[j]; + + // Filter out dead inputs. + if (input.destroyed) + { + inputs.splice(j, 1); + continue; + } + + // Update the input. + input.update(); + + // Check whether the input is the right trigger. + if (filterTrigger != null && input.trigger != filterTrigger) { + continue; + } + + // Check whether the input is the right device. + if (filterDevice != null && input.device != filterDevice) { + continue; + } + + // Check whether the input has triggered. + if (input.check(this)) + { + result = true; + } + } + + // We need to cache this result. + cache.set(key, {timestamp: FlxG.game.ticks, value: result}); + + return result; + } +} + class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital { var touchMap:Map = new Map(); @@ -1229,8 +1411,7 @@ enum Control DEBUG_STAGE; } -enum -abstract Action(String) to String from String +enum abstract Action(String) to String from String { // NOTE var NOTE_UP = "note_up"; diff --git a/source/funkin/input/TurboActionHandler.hx b/source/funkin/input/TurboActionHandler.hx new file mode 100644 index 000000000..9425db8cd --- /dev/null +++ b/source/funkin/input/TurboActionHandler.hx @@ -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); + } +} diff --git a/source/funkin/input/TurboButtonHandler.hx b/source/funkin/input/TurboButtonHandler.hx new file mode 100644 index 000000000..63c2a294b --- /dev/null +++ b/source/funkin/input/TurboButtonHandler.hx @@ -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; + var delay:Float; + var interval:Float; + var targetGamepad:FlxGamepad; + + var allPressedTime:Float = 0; + + function new(inputs:Array, 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, ?delay:Float = DEFAULT_DELAY, + ?interval:Float = DEFAULT_INTERVAL):TurboButtonHandler + { + return new TurboButtonHandler(inputs, delay, interval); + } +} diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 9a54e4053..32be3c8e0 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -240,8 +240,8 @@ class PolymodHandler { return { assetLibraryPaths: [ - 'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'tutorial' => 'tutorial', 'week1' => 'week1', 'week2' => 'week2', - 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1', + 'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'videos' => 'videos', 'tutorial' => 'tutorial', 'week1' => 'week1', + 'week2' => 'week2', 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1', ], coreAssetRedirect: CORE_FOLDER, } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index a715f0a93..787349952 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -245,20 +245,26 @@ class PlayState extends MusicBeatSubState /** * The current camera zoom level without any modifiers applied. */ - public var currentCameraZoom:Float = FlxCamera.defaultZoom * 1.05; + public var currentCameraZoom:Float = FlxCamera.defaultZoom; /** - * currentCameraZoom is increased every beat, and lerped back to this value every frame, creating a smooth 'zoom-in' effect. - * Defaults to 1.05, but may be larger or smaller depending on the current stage. - * Tweened via the `ZoomCamera` song event in direct mode. + * Multiplier for currentCameraZoom for camera bops. + * Lerped back to 1.0x every frame. */ - public var defaultCameraZoom:Float = FlxCamera.defaultZoom * 1.05; + public var cameraBopMultiplier:Float = 1.0; /** - * Camera zoom applied on top of currentCameraZoom. - * Tweened via the `ZoomCamera` song event in additive mode. + * Default camera zoom for the current stage. + * If we aren't in a stage, just use the default zoom (1.05x). */ - public var additiveCameraZoom:Float = 0; + public var stageZoom(get, never):Float; + + function get_stageZoom():Float + { + if (currentStage != null) return currentStage.camZoom; + else + return FlxCamera.defaultZoom * 1.05; + } /** * The current HUD camera zoom level. @@ -268,16 +274,18 @@ class PlayState extends MusicBeatSubState public var defaultHUDCameraZoom:Float = FlxCamera.defaultZoom * 1.0; /** - * Intensity of the gameplay camera zoom. - * @default `1.5%` + * Camera bop intensity multiplier. + * Applied to cameraBopMultiplier on camera bops (usually every beat). + * @default `101.5%` */ - public var cameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY; + public var cameraBopIntensity:Float = Constants.DEFAULT_BOP_INTENSITY; /** * Intensity of the HUD camera zoom. + * Need to make this a multiplier later. Just shoving in 0.015 for now so it doesn't break. * @default `3.0%` */ - public var hudCameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY * 2.0; + public var hudCameraZoomIntensity:Float = 0.015 * 2.0; /** * How many beats (quarter notes) between camera zooms. @@ -490,7 +498,7 @@ class PlayState extends MusicBeatSubState /** * The combo popups. Includes the real-time combo counter and the rating. */ - var comboPopUps:PopUpStuff; + public var comboPopUps:PopUpStuff; /** * PROPERTIES @@ -805,6 +813,7 @@ class PlayState extends MusicBeatSubState super.update(elapsed); + var list = FlxG.sound.list; updateHealthBar(); updateScoreText(); @@ -826,9 +835,12 @@ class PlayState extends MusicBeatSubState inputSpitter = []; // Reset music properly. - FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; - FlxG.sound.music.pitch = playbackRate; - FlxG.sound.music.pause(); + if (FlxG.sound.music != null) + { + FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; + FlxG.sound.music.pitch = playbackRate; + FlxG.sound.music.pause(); + } if (!overrideMusic) { @@ -844,7 +856,7 @@ class PlayState extends MusicBeatSubState vocals.pause(); vocals.time = 0; - FlxG.sound.music.volume = 1; + if (FlxG.sound.music != null) FlxG.sound.music.volume = 1; vocals.volume = 1; vocals.playerVolume = 1; vocals.opponentVolume = 1; @@ -861,8 +873,8 @@ class PlayState extends MusicBeatSubState regenNoteData(); // Reset camera zooming - cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY; - hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * 2.0; + cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY; + hudCameraZoomIntensity = (cameraBopIntensity - 1.0) * 2.0; cameraZoomRate = Constants.DEFAULT_ZOOM_RATE; health = Constants.HEALTH_STARTING; @@ -957,11 +969,12 @@ class PlayState extends MusicBeatSubState if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX; if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN; - // Lerp the camera zoom towards the target level. - if (subState == null) + // Apply camera zoom + multipliers. + if (subState == null && cameraZoomRate > 0.0) // && !isInCutscene) { - currentCameraZoom = FlxMath.lerp(defaultCameraZoom, currentCameraZoom, 0.95); - FlxG.camera.zoom = currentCameraZoom + additiveCameraZoom; + cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x + var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier. + FlxG.camera.zoom = zoomPlusBop; // Actually apply the zoom to the camera. camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95); } @@ -971,6 +984,7 @@ class PlayState extends MusicBeatSubState FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation()); } FlxG.watch.addQuick('health', health); + FlxG.watch.addQuick('cameraBopIntensity', cameraBopIntensity); // TODO: Add a song event for Handle GF dance speed. @@ -1367,15 +1381,15 @@ class PlayState extends MusicBeatSubState // activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING); } - // Only zoom camera if we are zoomed by less than 35%. + // Only bop camera if zoom level is below 135% if (Preferences.zoomCamera - && FlxG.camera.zoom < (1.35 * defaultCameraZoom) + && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) && cameraZoomRate > 0 && Conductor.instance.currentBeat % cameraZoomRate == 0) { - // Zoom camera in (1.5%) - currentCameraZoom += cameraZoomIntensity * defaultCameraZoom; - // Hud zooms double (3%) + // Set zoom multiplier for camera bop. + cameraBopMultiplier = cameraBopIntensity; + // HUD camera zoom still uses old system. To change. (+3%) camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom; } // trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}'); @@ -1457,7 +1471,7 @@ class PlayState extends MusicBeatSubState */ function initCameras():Void { - camGame = new FunkinCamera(); + camGame = new FunkinCamera('playStateCamGame'); camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage. camHUD = new FlxCamera(); camHUD.bgColor.alpha = 0; // Show the game scene behind the camera. @@ -1538,10 +1552,11 @@ class PlayState extends MusicBeatSubState function loadStage(id:String):Void { currentStage = StageRegistry.instance.fetchEntry(id); - currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory. if (currentStage != null) { + currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory. + // Actually create and position the sprites. var event:ScriptEvent = new ScriptEvent(CREATE, false); ScriptEventDispatcher.callEvent(currentStage, event); @@ -1566,12 +1581,11 @@ class PlayState extends MusicBeatSubState { if (PlayState.instance.isMinimalMode) return; // Apply camera zoom level from stage data. - defaultCameraZoom = currentStage?.camZoom ?? 1.0; - currentCameraZoom = defaultCameraZoom; + currentCameraZoom = stageZoom; FlxG.camera.zoom = currentCameraZoom; - // Reset additive zoom. - additiveCameraZoom = 0; + // Reset bop multiplier. + cameraBopMultiplier = 1.0; } /** @@ -1856,6 +1870,8 @@ class PlayState extends MusicBeatSubState isInCutscene = false; camCutscene.visible = false; + + // TODO: Maybe tween in the camera after any cutscenes. camHUD.visible = true; } @@ -1906,8 +1922,6 @@ class PlayState extends MusicBeatSubState */ function startSong():Void { - dispatchEvent(new ScriptEvent(SONG_START)); - startingSong = false; if (!overrideMusic && !isGamePaused && currentChart != null) @@ -1929,7 +1943,7 @@ class PlayState extends MusicBeatSubState // Prevent the volume from being wrong. FlxG.sound.music.volume = 1.0; - FlxG.sound.music.fadeTween?.cancel(); + if (FlxG.sound.music.fadeTween != null) FlxG.sound.music.fadeTween.cancel(); trace('Playing vocals...'); add(vocals); @@ -1948,6 +1962,8 @@ class PlayState extends MusicBeatSubState // FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; handleSkippedNotes(); } + + dispatchEvent(new ScriptEvent(SONG_START)); } /** @@ -2379,13 +2395,6 @@ class PlayState extends MusicBeatSubState // Display the combo meter and add the calculation to the score. popUpScore(note, event.score, event.judgement, event.healthChange); - - if (note.isHoldNote && note.holdNoteSprite != null) - { - playerStrumline.playNoteHoldCover(note.holdNoteSprite); - } - - vocals.playerVolume = 1; } /** @@ -2520,6 +2529,7 @@ class PlayState extends MusicBeatSubState if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible; #end + #if CHART_EDITOR_SUPPORTED // Open the stage editor overlaying the current state. if (controls.DEBUG_STAGE) { @@ -2539,6 +2549,7 @@ class PlayState extends MusicBeatSubState targetSongId: currentSong.id, })); } + #end #if (debug || FORCE_DEBUG_VERSION) // 1: End the song immediately. @@ -2667,6 +2678,13 @@ class PlayState extends MusicBeatSubState } comboPopUps.displayRating(daRating); if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo); + + if (daNote.isHoldNote && daNote.holdNoteSprite != null) + { + playerStrumline.playNoteHoldCover(daNote.holdNoteSprite); + } + + vocals.playerVolume = 1; } /** @@ -2777,7 +2795,7 @@ class PlayState extends MusicBeatSubState // adds current song data into the tallies for the level (story levels) Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel); - if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) + if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) { Save.instance.setSongScore(currentSong.id, currentDifficulty, data); #if newgrounds @@ -3063,18 +3081,18 @@ class PlayState extends MusicBeatSubState title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), scoreData: { - score: songScore, + score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore, tallies: { - sick: Highscore.tallies.sick, - good: Highscore.tallies.good, - bad: Highscore.tallies.bad, - shit: Highscore.tallies.shit, - missed: Highscore.tallies.missed, - combo: Highscore.tallies.combo, - maxCombo: Highscore.tallies.maxCombo, - totalNotesHit: Highscore.tallies.totalNotesHit, - totalNotes: Highscore.tallies.totalNotes, + sick: talliesToUse.sick, + good: talliesToUse.good, + bad: talliesToUse.bad, + shit: talliesToUse.shit, + missed: talliesToUse.missed, + combo: talliesToUse.combo, + maxCombo: talliesToUse.maxCombo, + totalNotesHit: talliesToUse.totalNotesHit, + totalNotes: talliesToUse.totalNotes, }, accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, }, @@ -3116,6 +3134,15 @@ class PlayState extends MusicBeatSubState FlxG.camera.focusOn(cameraFollowPoint.getPosition()); } + /** + * Sets the camera follow point's position and tweens the camera there. + */ + public function tweenCameraToPosition(?x:Float, ?y:Float, ?duration:Float, ?ease:NullFloat>):Void + { + cameraFollowPoint.setPosition(x, y); + tweenCameraToFollowPoint(duration, ease); + } + /** * Disables camera following and tweens the camera to the follow point manually. */ @@ -3157,38 +3184,24 @@ class PlayState extends MusicBeatSubState /** * Tweens the camera zoom to the desired amount. */ - public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?directMode:Bool, ?ease:NullFloat>):Void + public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?direct:Bool, ?ease:NullFloat>):Void { // Cancel the current tween if it's active. cancelCameraZoomTween(); - var targetZoom = zoom * FlxCamera.defaultZoom; + // Direct mode: Set zoom directly. + // Stage mode: Set zoom as a multiplier of the current stage's default zoom. + var targetZoom = zoom * (direct ? FlxCamera.defaultZoom : stageZoom); - if (directMode) // Direct mode: Tween defaultCameraZoom for basic "smooth" zooms. + if (duration == 0) { - if (duration == 0) - { - // Instant zoom. No tween needed. - defaultCameraZoom = targetZoom; - } - else - { - // Zoom tween! Caching it so we can cancel/pause it later if needed. - cameraZoomTween = FlxTween.tween(this, {defaultCameraZoom: targetZoom}, duration, {ease: ease}); - } + // Instant zoom. No tween needed. + currentCameraZoom = targetZoom; } - else // Additive mode: Tween additiveCameraZoom for ease-based zooms. + else { - if (duration == 0) - { - // Instant zoom. No tween needed. - additiveCameraZoom = targetZoom; - } - else - { - // Zoom tween! Caching it so we can cancel/pause it later if needed. - cameraZoomTween = FlxTween.tween(this, {additiveCameraZoom: targetZoom}, duration, {ease: ease}); - } + // Zoom tween! Caching it so we can cancel/pause it later if needed. + cameraZoomTween = FlxTween.tween(this, {currentCameraZoom: targetZoom}, duration, {ease: ease}); } } diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index c05257338..acce8645a 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -64,6 +64,9 @@ class ResultState extends MusicBeatSubState loop: resultsVariation != SHIT }); + // Reset the camera zoom on the results screen. + FlxG.camera.zoom = 1.0; + // TEMP-ish, just used to sorta "cache" the 3000x3000 image! var cacheBullShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem")); add(cacheBullShit); @@ -80,34 +83,34 @@ class ResultState extends MusicBeatSubState bgFlash.visible = false; add(bgFlash); - var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared")); - bfGfExcellent.visible = false; - add(bfGfExcellent); + // var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared")); + // bfGfExcellent.visible = false; + // add(bfGfExcellent); + // + // var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared")); + // bfPerfect.visible = false; + // add(bfPerfect); + // + // var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared")); + // bfSHIT.visible = false; + // add(bfSHIT); + // + // bfGfExcellent.anim.onComplete = () -> { + // bfGfExcellent.anim.curFrame = 28; + // bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! + // }; + // + // bfPerfect.anim.onComplete = () -> { + // bfPerfect.anim.curFrame = 136; + // bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! + // }; + // + // bfSHIT.anim.onComplete = () -> { + // bfSHIT.anim.curFrame = 150; + // bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce! + // }; - var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared")); - bfPerfect.visible = false; - add(bfPerfect); - - var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared")); - bfSHIT.visible = false; - add(bfSHIT); - - bfGfExcellent.anim.onComplete = () -> { - bfGfExcellent.anim.curFrame = 28; - bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! - }; - - bfPerfect.anim.onComplete = () -> { - bfPerfect.anim.curFrame = 136; - bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! - }; - - bfSHIT.anim.onComplete = () -> { - bfSHIT.anim.curFrame = 150; - bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce! - }; - - var gf:FlxSprite = FunkinSprite.createSparrow(500, 300, 'resultScreen/resultGirlfriendGOOD'); + var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD'); gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); gf.visible = false; gf.animation.finishCallback = _ -> { @@ -268,9 +271,9 @@ class ResultState extends MusicBeatSubState switch (resultsVariation) { - case SHIT: - bfSHIT.visible = true; - bfSHIT.playAnimation(""); + // case SHIT: + // bfSHIT.visible = true; + // bfSHIT.playAnimation(""); case NORMAL: boyfriend.animation.play('fall'); @@ -292,9 +295,9 @@ class ResultState extends MusicBeatSubState gf.animation.play('clap', true); gf.visible = true; }); - case PERFECT: - bfPerfect.visible = true; - bfPerfect.playAnimation(""); + // case PERFECT: + // bfPerfect.visible = true; + // bfPerfect.playAnimation(""); // bfGfExcellent.visible = true; // bfGfExcellent.playAnimation(""); diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index f1dadf3e2..ed58b92b5 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -192,6 +192,7 @@ class AnimateAtlasCharacter extends BaseCharacter if (!this.mainSprite.hasAnimation(prefix)) { FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}'); + trace('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}'); continue; } animations.set(anim.name, anim); diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx index 724bf0cb9..b062b22cf 100644 --- a/source/funkin/play/components/PopUpStuff.hx +++ b/source/funkin/play/components/PopUpStuff.hx @@ -10,6 +10,8 @@ import funkin.util.TimerUtil; class PopUpStuff extends FlxTypedGroup { + public var offsets:Array = [0, 0]; + override public function new() { super(); @@ -29,9 +31,9 @@ class PopUpStuff extends FlxTypedGroup rating.scrollFactor.set(0.2, 0.2); rating.zIndex = 1000; - rating.x = FlxG.width * 0.50; + rating.x = (FlxG.width * 0.474) + offsets[0]; // rating.x -= FlxG.camera.scroll.x * 0.2; - rating.y = FlxG.camera.height * 0.4 - 60; + rating.y = (FlxG.camera.height * 0.45 - 60) + offsets[1]; rating.acceleration.y = 550; rating.velocity.y -= FlxG.random.int(140, 175); rating.velocity.x -= FlxG.random.int(0, 10); @@ -40,16 +42,19 @@ class PopUpStuff extends FlxTypedGroup if (PlayState.instance.currentStageId.startsWith('school')) { - rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7)); + rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.65)); rating.antialiasing = false; } else { - rating.setGraphicSize(Std.int(rating.width * 0.7)); + rating.setGraphicSize(Std.int(rating.width * 0.65)); rating.antialiasing = true; } rating.updateHitbox(); + rating.x -= rating.width / 2; + rating.y -= rating.height / 2; + FlxTween.tween(rating, {alpha: 0}, 0.2, { onComplete: function(tween:FlxTween) { @@ -77,8 +82,8 @@ class PopUpStuff extends FlxTypedGroup pixelShitPart2 = '-pixel'; } var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2); - comboSpr.y = FlxG.camera.height * 0.4 + 80; - comboSpr.x = FlxG.width * 0.50; + comboSpr.y = (FlxG.camera.height * 0.44) + offsets[1]; + comboSpr.x = (FlxG.width * 0.507) + offsets[0]; // comboSpr.x -= FlxG.camera.scroll.x * 0.2; comboSpr.acceleration.y = 600; @@ -133,14 +138,14 @@ class PopUpStuff extends FlxTypedGroup } else { - numScore.setGraphicSize(Std.int(numScore.width * 0.5)); + numScore.setGraphicSize(Std.int(numScore.width * 0.45)); numScore.antialiasing = true; } numScore.updateHitbox(); - numScore.x = comboSpr.x - (43 * daLoop); //- 90; - numScore.acceleration.y = FlxG.random.int(200, 300); - numScore.velocity.y -= FlxG.random.int(140, 160); + numScore.x = comboSpr.x - (36 * daLoop) - 65; //- 90; + numScore.acceleration.y = FlxG.random.int(250, 300); + numScore.velocity.y -= FlxG.random.int(130, 150); numScore.velocity.x = FlxG.random.float(-5, 5); add(numScore); diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx index 3da51185f..0939dae38 100644 --- a/source/funkin/play/cutscene/VideoCutscene.hx +++ b/source/funkin/play/cutscene/VideoCutscene.hx @@ -67,8 +67,13 @@ class VideoCutscene if (!openfl.Assets.exists(filePath)) { // Display a popup. - lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video'); - return; + // lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video'); + // return; + + // TODO: After moving videos to their own library, + // this function ALWAYS FAILS on web, but the video still plays. + // I think that's due to a weird quirk with how OpenFL libraries work. + trace('Video file does not exist: ${filePath}'); } var rawFilePath = Paths.stripLibrary(filePath); @@ -311,7 +316,7 @@ class VideoCutscene blackScreen = null; } }); - FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.defaultCameraZoom}, transitionTime, + FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.stageZoom}, transitionTime, { ease: FlxEase.quadInOut, onComplete: function(twn:FlxTween) { diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx index 1e399e2c8..9c217948d 100644 --- a/source/funkin/play/cutscene/dialogue/Conversation.hx +++ b/source/funkin/play/cutscene/dialogue/Conversation.hx @@ -23,6 +23,7 @@ import funkin.modding.IScriptedClass.IDialogueScriptedClass; import funkin.modding.IScriptedClass.IEventHandler; import funkin.play.cutscene.dialogue.DialogueBox; import funkin.util.SortUtil; +import funkin.util.EaseUtil; /** * A high-level handler for dialogue. @@ -179,7 +180,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl if (backdropData.fadeTime > 0.0) { backdrop.alpha = 0.0; - FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: FlxEase.linear}); + FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: EaseUtil.stepped(10)}); } else { @@ -403,6 +404,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl type: ONESHOT, // holy shit like the game no way startDelay: 0, onComplete: (_) -> endOutro(), + ease: EaseUtil.stepped(8) }); FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime); diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index d4ab4f24f..569dcb87a 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -70,80 +70,76 @@ class FocusCameraSongEvent extends SongEvent if (char == null) char = cast data.value; - var useTween:Null = data.getBool('useTween'); - if (useTween == null) useTween = false; var duration:Null = data.getFloat('duration'); if (duration == null) duration = 4.0; var ease:Null = data.getString('ease'); - if (ease == null) ease = 'linear'; + if (ease == null) ease = 'CLASSIC'; + + var currentStage = PlayState.instance.currentStage; + + // Get target position based on char. + var targetX:Float = posX; + var targetY:Float = posY; switch (char) { - case -1: // Position + case -1: // Position ("focus" on origin) trace('Focusing camera on static position.'); - var xTarget:Float = posX; - var yTarget:Float = posY; - PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); - case 0: // Boyfriend - // Focus the camera on the player. - if (PlayState.instance.currentStage.getBoyfriend() == null) + case 0: // Boyfriend (focus on player) + if (currentStage.getBoyfriend() == null) { trace('No BF to focus on.'); return; } trace('Focusing camera on player.'); - var xTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX; - var yTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY; + var bfPoint = currentStage.getBoyfriend().cameraFocusPoint; + targetX += bfPoint.x; + targetY += bfPoint.y; - PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); - case 1: // Dad - // Focus the camera on the dad. - if (PlayState.instance.currentStage.getDad() == null) + case 1: // Dad (focus on opponent) + if (currentStage.getDad() == null) { trace('No dad to focus on.'); return; } - trace('Focusing camera on dad.'); - trace(PlayState.instance.currentStage.getDad()); - var xTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX; - var yTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY; + trace('Focusing camera on opponent.'); + var dadPoint = currentStage.getDad().cameraFocusPoint; + targetX += dadPoint.x; + targetY += dadPoint.y; - PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); - case 2: // Girlfriend - // Focus the camera on the girlfriend. - if (PlayState.instance.currentStage.getGirlfriend() == null) + case 2: // Girlfriend (focus on girlfriend) + if (currentStage.getGirlfriend() == null) { trace('No GF to focus on.'); return; } trace('Focusing camera on girlfriend.'); - var xTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX; - var yTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY; + var gfPoint = currentStage.getGirlfriend().cameraFocusPoint; + targetX += gfPoint.x; + targetY += gfPoint.y; - PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); default: trace('Unknown camera focus: ' + data); } - if (useTween) + // Apply tween based on ease. + switch (ease) { - switch (ease) - { - case 'INSTANT': - PlayState.instance.tweenCameraToFollowPoint(0); - default: - var durSeconds = Conductor.instance.stepLengthMs * duration / 1000; - - var easeFunction:NullFloat> = Reflect.field(FlxEase, ease); - if (easeFunction == null) - { - trace('Invalid ease function: $ease'); - return; - } - - PlayState.instance.tweenCameraToFollowPoint(durSeconds, easeFunction); - } + case 'CLASSIC': // Old-school. No ease. Just set follow point. + PlayState.instance.resetCamera(); + PlayState.instance.cameraFollowPoint.setPosition(targetX, targetY); + case 'INSTANT': // Instant ease. Duration is automatically 0. + PlayState.instance.tweenCameraToPosition(targetX, targetY, 0); + default: + var durSeconds = Conductor.instance.stepLengthMs * duration / 1000; + var easeFunction:NullFloat> = Reflect.field(FlxEase, ease); + if (easeFunction == null) + { + trace('Invalid ease function: $ease'); + return; + } + PlayState.instance.tweenCameraToPosition(targetX, targetY, durSeconds, easeFunction); } } @@ -187,12 +183,6 @@ class FocusCameraSongEvent extends SongEvent type: SongEventFieldType.FLOAT, units: "px" }, - { - name: 'useTween', - title: 'Use Tween', - type: SongEventFieldType.BOOL, - defaultValue: false - }, { name: 'duration', title: 'Duration', @@ -208,7 +198,9 @@ class FocusCameraSongEvent extends SongEvent type: SongEventFieldType.ENUM, keys: [ 'Linear' => 'linear', - 'Instant' => 'INSTANT', + 'Sine In' => 'sineIn', + 'Sine Out' => 'sineOut', + 'Sine In/Out' => 'sineInOut', 'Quad In' => 'quadIn', 'Quad Out' => 'quadOut', 'Quad In/Out' => 'quadInOut', @@ -221,15 +213,17 @@ class FocusCameraSongEvent extends SongEvent 'Quint In' => 'quintIn', 'Quint Out' => 'quintOut', 'Quint In/Out' => 'quintInOut', + 'Expo In' => 'expoIn', + 'Expo Out' => 'expoOut', + 'Expo In/Out' => 'expoInOut', 'Smooth Step In' => 'smoothStepIn', 'Smooth Step Out' => 'smoothStepOut', 'Smooth Step In/Out' => 'smoothStepInOut', - 'Sine In' => 'sineIn', - 'Sine Out' => 'sineOut', - 'Sine In/Out' => 'sineInOut', 'Elastic In' => 'elasticIn', 'Elastic Out' => 'elasticOut', 'Elastic In/Out' => 'elasticInOut', + 'Instant (Ignores duration)' => 'INSTANT', + 'Classic (Ignores duration)' => 'CLASSIC' ] } ]); diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx index a82577a5f..f3efc04e3 100644 --- a/source/funkin/play/event/SetCameraBopSongEvent.hx +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -50,8 +50,8 @@ class SetCameraBopSongEvent extends SongEvent var intensity:Null = data.getFloat('intensity'); if (intensity == null) intensity = 1.0; - PlayState.instance.cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity; - PlayState.instance.hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity * 2.0; + PlayState.instance.cameraBopIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity + 1.0; + PlayState.instance.hudCameraZoomIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity * 2.0; PlayState.instance.cameraZoomRate = rate; trace('Set camera zoom rate to ${PlayState.instance.cameraZoomRate}'); } diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index b913aebe7..748abda19 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -81,7 +81,6 @@ class ZoomCameraSongEvent extends SongEvent PlayState.instance.tweenCameraZoom(zoom, 0, isDirectMode); default: var durSeconds = Conductor.instance.stepLengthMs * duration / 1000; - var easeFunction:NullFloat> = Reflect.field(FlxEase, ease); if (easeFunction == null) { @@ -102,9 +101,9 @@ class ZoomCameraSongEvent extends SongEvent * ``` * { * 'zoom': FLOAT, // Target zoom level. - * 'duration': FLOAT, // Optional duration in steps. - * 'mode': ENUM, // Whether to set additive zoom or direct zoom. - * 'ease': ENUM, // Optional easing function. + * 'duration': FLOAT, // Duration in steps. + * 'mode': ENUM, // Whether zoom is relative to the stage or absolute zoom. + * 'ease': ENUM, // Easing function. * } * @return SongEventSchema */ @@ -130,9 +129,9 @@ class ZoomCameraSongEvent extends SongEvent { name: 'mode', title: 'Mode', - defaultValue: 'direct', + defaultValue: 'stage', type: SongEventFieldType.ENUM, - keys: ['Additive' => 'additive', 'Direct' => 'direct'] + keys: ['Stage zoom' => 'stage', 'Absolute zoom' => 'direct'] }, { name: 'ease', @@ -142,6 +141,9 @@ class ZoomCameraSongEvent extends SongEvent keys: [ 'Linear' => 'linear', 'Instant' => 'INSTANT', + 'Sine In' => 'sineIn', + 'Sine Out' => 'sineOut', + 'Sine In/Out' => 'sineInOut', 'Quad In' => 'quadIn', 'Quad Out' => 'quadOut', 'Quad In/Out' => 'quadInOut', @@ -154,15 +156,15 @@ class ZoomCameraSongEvent extends SongEvent 'Quint In' => 'quintIn', 'Quint Out' => 'quintOut', 'Quint In/Out' => 'quintInOut', + 'Expo In' => 'expoIn', + 'Expo Out' => 'expoOut', + 'Expo In/Out' => 'expoInOut', 'Smooth Step In' => 'smoothStepIn', 'Smooth Step Out' => 'smoothStepOut', 'Smooth Step In/Out' => 'smoothStepInOut', - 'Sine In' => 'sineIn', - 'Sine Out' => 'sineOut', - 'Sine In/Out' => 'sineInOut', 'Elastic In' => 'elasticIn', 'Elastic Out' => 'elasticOut', - 'Elastic In/Out' => 'elasticInOut', + 'Elastic In/Out' => 'elasticInOut' ] } ]); diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index d219dc2f6..e71ae3213 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -609,28 +609,33 @@ class SongDifficulty return cast events; } - public function cacheInst(instrumental = ''):Void + public function getInstPath(instrumental = ''):String { if (characters != null) { if (instrumental != '' && characters.altInstrumentals.contains(instrumental)) { var instId = '-$instrumental'; - FlxG.sound.cache(Paths.inst(this.song.id, instId)); + return Paths.inst(this.song.id, instId); } else { // Fallback to default instrumental. var instId = (characters.instrumental ?? '') != '' ? '-${characters.instrumental}' : ''; - FlxG.sound.cache(Paths.inst(this.song.id, instId)); + return Paths.inst(this.song.id, instId); } } else { - FlxG.sound.cache(Paths.inst(this.song.id)); + return Paths.inst(this.song.id); } } + public function cacheInst(instrumental = ''):Void + { + FlxG.sound.cache(getInstPath(instrumental)); + } + public function playInst(volume:Float = 1.0, looped:Bool = false):Void { var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 89dfe3041..fbec836bb 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -695,7 +695,7 @@ class Save trace("[SAVE] Checking for legacy save data..."); var legacySave:FlxSave = new FlxSave(); legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY); - if (legacySave?.data == null) + if (legacySave.isEmpty()) { trace("[SAVE] No legacy save data found."); return null; diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx index 86afdafd1..2240ec50e 100644 --- a/source/funkin/ui/credits/CreditsDataHandler.hx +++ b/source/funkin/ui/credits/CreditsDataHandler.hx @@ -57,10 +57,6 @@ class CreditsDataHandler {line: 'KawaiSprite'}, {line: 'evilsk8r'}, ] - }, - { - header: 'Kickstarter Backers', - appendBackers: true } ] }; @@ -68,11 +64,11 @@ class CreditsDataHandler public static function fetchBackerEntries():Array { - // 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, // because we don't want to deadname people who haven't logged into the portal yet. // It can be async and paginated for performance! - return ['See the list of backers at $BACKER_PUBLIC_URL.']; + return []; } #if HARDCODED_CREDITS @@ -99,12 +95,19 @@ class CreditsDataHandler static function fetchCreditsData():funkin.data.JsonFile { + #if !macro var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim(); return { fileName: CREDITS_DATA_PATH, contents: rawJson }; + #else + return { + fileName: CREDITS_DATA_PATH, + contents: null + }; + #end } static function parseCreditsData(file:JsonFile):Null diff --git a/source/funkin/ui/credits/CreditsState.hx b/source/funkin/ui/credits/CreditsState.hx index d43e25114..6a15f1263 100644 --- a/source/funkin/ui/credits/CreditsState.hx +++ b/source/funkin/ui/credits/CreditsState.hx @@ -185,7 +185,7 @@ class CreditsState extends MusicBeatState } else if (controls.PAUSE) { - scrollPaused = !scrollPaused; + // scrollPaused = !scrollPaused; } } @@ -196,7 +196,7 @@ class CreditsState extends MusicBeatState function exit():Void { - FlxG.switchState(new funkin.ui.mainmenu.MainMenuState()); + FlxG.switchState(funkin.ui.mainmenu.MainMenuState.new); } public override function destroy():Void diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index dba1a7e55..93907bdda 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -8,6 +8,7 @@ import flixel.FlxSprite; import flixel.FlxSubState; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup; +import flixel.input.gamepad.FlxGamepadInputID; import flixel.input.keyboard.FlxKey; import flixel.input.mouse.FlxMouseEvent; import flixel.math.FlxMath; @@ -40,6 +41,8 @@ import funkin.data.stage.StageData; import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinSprite; import funkin.input.Cursor; +import funkin.input.TurboActionHandler; +import funkin.input.TurboButtonHandler; import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; import funkin.play.character.BaseCharacter.CharacterType; @@ -74,6 +77,7 @@ import funkin.ui.debug.charting.commands.SetItemSelectionCommand; import funkin.ui.debug.charting.components.ChartEditorEventSprite; import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; +import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; import funkin.ui.debug.charting.components.ChartEditorNotePreview; import funkin.ui.debug.charting.components.ChartEditorNoteSprite; import funkin.ui.debug.charting.components.ChartEditorPlaybarHead; @@ -401,8 +405,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); // Offset the selection box start position, if we are dragging. if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; - // Update the note preview viewport box. + + // Update the note preview. setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + refreshNotePreviewPlayheadPosition(); + // Update the measure tick display. if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0; return this.scrollPositionInPixels; @@ -463,6 +470,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Move the playhead sprite to the correct position. gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS; + updatePlayheadGhostHoldNotes(); + refreshNotePreviewPlayheadPosition(); + return this.playheadPositionInPixels; } @@ -769,6 +779,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return currentPlaceNoteData = value; } + /** + * The SongNoteData which is currently being placed, for each column. + * `null` if the user isn't currently placing a note. + * As the user moves down, we will update this note's sustain length, and finalize the note when they release. + */ + var currentLiveInputPlaceNoteData:Array = []; + // Note Movement /** @@ -799,6 +816,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var dragLengthCurrent:Float = 0; + /** + * The current length of the hold note we are placing with the playhead, in steps. + * Play a sound when this value changes. + */ + var playheadDragLengthCurrent:Array = []; + /** * 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); + /** + * 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 */ @@ -1949,10 +2032,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var gridGhostNote:Null = 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 = null; + /** + * A sprite used to indicate the hold note that will be placed on button release. + */ + var gridPlayheadGhostHoldNotes:Array = []; + /** * 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 = 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 = null; + /** * The rectangular sprite used for rendering the selection box. * Uses a 9-slice to stretch the selection box to the correct size without warping. @@ -2091,7 +2185,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState loadPreferences(); - uiCamera = new FunkinCamera(); + uiCamera = new FunkinCamera('chartEditorUI'); FlxG.cameras.reset(uiCamera); buildDefaultSongData(); @@ -2349,7 +2443,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState gridGhostHoldNote = new ChartEditorHoldNoteSprite(this); gridGhostHoldNote.alpha = 0.6; - gridGhostHoldNote.noteData = new SongNoteData(0, 0, 0, ""); + gridGhostHoldNote.noteData = null; gridGhostHoldNote.visible = false; add(gridGhostHoldNote); gridGhostHoldNote.zIndex = 11; @@ -2423,6 +2517,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(notePreviewViewport); notePreviewViewport.zIndex = 30; + notePreviewPlayhead = new FlxSprite().makeGraphic(2, 2, 0xFFFF0000); + notePreviewPlayhead.scrollFactor.set(0, 0); + notePreviewPlayhead.scale.set(notePreview.width / 2, 0.5); // Setting width does nothing. + notePreviewPlayhead.updateHitbox(); + notePreviewPlayhead.x = notePreview.x; + notePreviewPlayhead.y = notePreview.y; + add(notePreviewPlayhead); + notePreviewPlayhead.zIndex = 31; + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); } @@ -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. */ @@ -3015,6 +3125,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ function setupTurboKeyHandlers():Void { + // Keyboard shortcuts add(undoKeyHandler); add(redoKeyHandler); add(upKeyHandler); @@ -3023,6 +3134,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(sKeyHandler); add(pageUpKeyHandler); add(pageDownKeyHandler); + + // Gamepad inputs + add(dpadUpGamepadHandler); + add(dpadDownGamepadHandler); + add(dpadLeftGamepadHandler); + add(dpadRightGamepadHandler); + add(leftStickUpGamepadHandler); + add(leftStickDownGamepadHandler); + add(leftStickLeftGamepadHandler); + add(leftStickRightGamepadHandler); + add(rightStickUpGamepadHandler); + add(rightStickDownGamepadHandler); + add(rightStickLeftGamepadHandler); + add(rightStickRightGamepadHandler); } /** @@ -3709,32 +3834,56 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Up Arrow = Scroll Up if (upKeyHandler.activated && currentLiveInputStyle == None) { - scrollAmount = -GRID_SIZE * 0.25 * 25.0; + scrollAmount = -GRID_SIZE * 4; shouldPause = true; } // Down Arrow = Scroll Down if (downKeyHandler.activated && currentLiveInputStyle == None) { - scrollAmount = GRID_SIZE * 0.25 * 25.0; + scrollAmount = GRID_SIZE * 4; shouldPause = true; } // W = Scroll Up (doesn't work with Ctrl+Scroll) if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL) { - scrollAmount = -GRID_SIZE * 0.25 * 25.0; + scrollAmount = -GRID_SIZE * 4; shouldPause = true; } // S = Scroll Down (doesn't work with Ctrl+Scroll) if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL) { - scrollAmount = GRID_SIZE * 0.25 * 25.0; + scrollAmount = GRID_SIZE * 4; shouldPause = true; } - // PAGE UP = Jump up to nearest measure - if (pageUpKeyHandler.activated) + // GAMEPAD LEFT STICK UP = Scroll Up by 1 note snap + if (leftStickUpGamepadHandler.activated) { + scrollAmount = -GRID_SIZE * noteSnapRatio; + shouldPause = true; + } + // GAMEPAD LEFT STICK DOWN = Scroll Down by 1 note snap + if (leftStickDownGamepadHandler.activated) + { + scrollAmount = GRID_SIZE * noteSnapRatio; + shouldPause = true; + } + + // GAMEPAD RIGHT STICK UP = Scroll Up by 1 note snap (playhead only) + if (rightStickUpGamepadHandler.activated) + { + playheadAmount = -GRID_SIZE * noteSnapRatio; + shouldPause = true; + } + // GAMEPAD RIGHT STICK DOWN = Scroll Down by 1 note snap (playhead only) + if (rightStickDownGamepadHandler.activated) + { + playheadAmount = GRID_SIZE * noteSnapRatio; + shouldPause = true; + } + + var funcJumpUp = (playheadOnly:Bool) -> { var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight; @@ -3744,20 +3893,37 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; } - scrollAmount = targetScrollPosition - playheadPos; + if (playheadOnly) + { + playheadAmount = targetScrollPosition - playheadPos; + } + else + { + scrollAmount = targetScrollPosition - playheadPos; + } + } + + // PAGE UP = Jump up to nearest measure + // GAMEPAD LEFT STICK LEFT = Jump up to nearest measure + if (pageUpKeyHandler.activated || leftStickLeftGamepadHandler.activated) + { + funcJumpUp(false); + shouldPause = true; + } + if (rightStickLeftGamepadHandler.activated) + { + funcJumpUp(true); shouldPause = true; } if (playbarButtonPressed == 'playbarBack') { playbarButtonPressed = ''; - scrollAmount = -GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; + funcJumpUp(false); shouldPause = true; } - // PAGE DOWN = Jump down to nearest measure - if (pageDownKeyHandler.activated) - { + var funcJumpDown = (playheadOnly:Bool) -> { var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight; @@ -3767,26 +3933,46 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; } - scrollAmount = targetScrollPosition - playheadPos; + if (playheadOnly) + { + playheadAmount = targetScrollPosition - playheadPos; + } + else + { + scrollAmount = targetScrollPosition - playheadPos; + } + } + + // PAGE DOWN = Jump down to nearest measure + // GAMEPAD LEFT STICK RIGHT = Jump down to nearest measure + if (pageDownKeyHandler.activated || leftStickRightGamepadHandler.activated) + { + funcJumpDown(false); + shouldPause = true; + } + if (rightStickRightGamepadHandler.activated) + { + funcJumpDown(true); shouldPause = true; } if (playbarButtonPressed == 'playbarForward') { playbarButtonPressed = ''; - scrollAmount = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; + funcJumpDown(false); shouldPause = true; } // SHIFT + Scroll = Scroll Fast - if (FlxG.keys.pressed.SHIFT) + // GAMEPAD LEFT STICK CLICK + Scroll = Scroll Fast + if (FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_STICK_CLICK ?? false)) { scrollAmount *= 2; } // CONTROL + Scroll = Scroll Precise if (FlxG.keys.pressed.CONTROL) { - scrollAmount /= 10; + scrollAmount /= 4; } // Alt + Drag = Scroll but move the playhead the same amount. @@ -4380,9 +4566,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } gridGhostHoldNote.visible = true; - gridGhostHoldNote.noteData = currentPlaceNoteData; - gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection(); - + gridGhostHoldNote.noteData = gridGhostNote.noteData; + gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection(); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true); gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes); @@ -4943,37 +5128,57 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function handlePlayhead():Void { - // Place notes at the playhead. + // Place notes at the playhead with the keyboard. switch (currentLiveInputStyle) { case ChartEditorLiveInputStyle.WASDKeys: if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4); + if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4); if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5); + if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5); if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6); + if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6); if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7); + if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7); if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0); + if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0); if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1); + if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1); if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2); + if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2); if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3); + if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3); case ChartEditorLiveInputStyle.NumberKeys: // Flipped because Dad is on the left but represents data 0-3. if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4); + if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4); if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5); + if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5); if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6); + if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6); if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7); + if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7); if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0); + if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0); if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1); if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2); + if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2); if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3); + if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3); case ChartEditorLiveInputStyle.None: // Do nothing. } + + updatePlayheadGhostHoldNotes(); } function placeNoteAtPlayhead(column:Int):Void { + // SHIFT + press or LEFT_SHOULDER + press to remove notes instead of placing them. + var removeNoteInstead:Bool = FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_SHOULDER ?? false); + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); @@ -4984,14 +5189,136 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio); notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]); - if (notesAtPos.length == 0) + if (notesAtPos.length == 0 && !removeNoteInstead) { + trace('Placing note. ${column}'); var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); + currentLiveInputPlaceNoteData[column] = newNoteData; + } + else if (removeNoteInstead) + { + trace('Removing existing note at position. ${column}'); + performCommand(new RemoveNotesCommand(notesAtPos)); } else { - trace('Already a note there.'); + trace('Already a note there. ${column}'); + } + } + + function updatePlayheadGhostHoldNotes():Void + { + // Ensure all the ghost hold notes exist. + while (gridPlayheadGhostHoldNotes.length < (STRUMLINE_SIZE * 2)) + { + var ghost = new ChartEditorHoldNoteSprite(this); + ghost.alpha = 0.6; + ghost.noteData = null; + ghost.visible = false; + ghost.zIndex = 11; + add(ghost); // Don't add to `renderedHoldNotes` because then it will get killed every frame. + + gridPlayheadGhostHoldNotes.push(ghost); + refresh(); + } + + // Update playhead ghost hold notes. + for (column in 0...gridPlayheadGhostHoldNotes.length) + { + var targetNoteData = currentLiveInputPlaceNoteData[column]; + var ghostHold = gridPlayheadGhostHoldNotes[column]; + + if (targetNoteData == null && ghostHold.noteData != null) + { + // Remove the ghost hold note. + ghostHold.noteData = null; + } + + if (targetNoteData != null && ghostHold.noteData == null) + { + // Readd the new ghost hold note. + ghostHold.noteData = targetNoteData.clone(); + ghostHold.noteDirection = ghostHold.noteData.getDirection(); + ghostHold.visible = true; + ghostHold.alpha = 0.6; + ghostHold.setHeightDirectly(0); + ghostHold.updateHoldNotePosition(renderedHoldNotes); + } + + if (ghostHold.noteData == null) + { + ghostHold.visible = false; + ghostHold.setHeightDirectly(0); + playheadDragLengthCurrent[column] = 0; + continue; + } + + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; + var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; + var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); + var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio; + + var newNoteLength:Float = playheadPosSnappedMs - ghostHold.noteData.time; + trace('newNoteLength: ${newNoteLength}'); + + if (newNoteLength > 0) + { + ghostHold.noteData.length = newNoteLength; + var targetNoteLengthSteps:Float = ghostHold.noteData.getStepLength(true); + var targetNoteLengthStepsInt:Int = Std.int(Math.floor(targetNoteLengthSteps)); + var targetNoteLengthPixels:Float = targetNoteLengthSteps * GRID_SIZE; + + if (playheadDragLengthCurrent[column] != targetNoteLengthStepsInt) + { + stretchySounds = !stretchySounds; + this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI')); + playheadDragLengthCurrent[column] = targetNoteLengthStepsInt; + } + ghostHold.visible = true; + ghostHold.alpha = 0.6; + ghostHold.setHeightDirectly(targetNoteLengthPixels, true); + ghostHold.updateHoldNotePosition(renderedHoldNotes); + trace('lerpLength: ${ghostHold.fullSustainLength}'); + trace('position: ${ghostHold.x}, ${ghostHold.y}'); + } + else + { + ghostHold.visible = false; + ghostHold.setHeightDirectly(0); + playheadDragLengthCurrent[column] = 0; + continue; + } + } + } + + function finishPlaceNoteAtPlayhead(column:Int):Void + { + if (currentLiveInputPlaceNoteData[column] == null) return; + + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; + var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; + var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); + var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio; + + var newNoteLength:Float = playheadPosSnappedMs - currentLiveInputPlaceNoteData[column].time; + trace('finishPlace newNoteLength: ${newNoteLength}'); + + if (newNoteLength < Conductor.instance.stepLengthMs) + { + // Don't extend the note if it's too short. + trace('Not extending note. ${column}'); + currentLiveInputPlaceNoteData[column] = null; + gridPlayheadGhostHoldNotes[column].noteData = null; + } + else + { + // Extend the note to the playhead position. + trace('Extending note. ${column}'); + this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI')); + performCommand(new ExtendNoteLengthCommand(currentLiveInputPlaceNoteData[column], newNoteLength)); + currentLiveInputPlaceNoteData[column] = null; + gridPlayheadGhostHoldNotes[column].noteData = null; } } @@ -5382,7 +5709,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Kill and replace the UI camera so it doesn't get destroyed during the state transition. uiCamera.kill(); FlxG.cameras.remove(uiCamera, false); - FlxG.cameras.reset(new FunkinCamera()); + FlxG.cameras.reset(new FunkinCamera('chartEditorUI2')); this.persistentUpdate = false; this.persistentDraw = false; diff --git a/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx index b4d913607..30f4280d2 100644 --- a/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx @@ -20,6 +20,8 @@ class RemoveEventsCommand implements ChartEditorCommand public function execute(state:ChartEditorState):Void { + if (events.length == 0) return; + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); state.currentEventSelection = []; @@ -34,6 +36,8 @@ class RemoveEventsCommand implements ChartEditorCommand public function undo(state:ChartEditorState):Void { + if (events.length == 0) return; + for (event in events) { state.currentSongChartEventData.push(event); diff --git a/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx index 69317aff4..1cc61f233 100644 --- a/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx @@ -23,6 +23,8 @@ class RemoveItemsCommand implements ChartEditorCommand public function execute(state:ChartEditorState):Void { + if ((notes.length + events.length) == 0) return; + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); @@ -40,6 +42,8 @@ class RemoveItemsCommand implements ChartEditorCommand public function undo(state:ChartEditorState):Void { + if ((notes.length + events.length) == 0) return; + for (note in notes) { state.currentSongChartNoteData.push(note); diff --git a/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx index 4811f831d..18ad6e04d 100644 --- a/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx +++ b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx @@ -20,6 +20,8 @@ class RemoveNotesCommand implements ChartEditorCommand public function execute(state:ChartEditorState):Void { + if (notes.length == 0) return; + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentNoteSelection = []; state.currentEventSelection = []; @@ -35,6 +37,8 @@ class RemoveNotesCommand implements ChartEditorCommand public function undo(state:ChartEditorState):Void { + if (notes.length == 0) return; + for (note in notes) { state.currentSongChartNoteData.push(note); diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx index c7f7747c0..aeb6dd0e4 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx @@ -54,11 +54,16 @@ class ChartEditorHoldNoteSprite extends SustainTrail * Set the height directly, to a value in pixels. * @param h The desired height in pixels. */ - public function setHeightDirectly(h:Float, ?lerp:Bool = false) + public function setHeightDirectly(h:Float, lerp:Bool = false) { - if (lerp != null && lerp) sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25); + if (lerp) + { + sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25); + } else + { sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS); + } fullSustainLength = sustainLength; } diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx index cd403c6f8..98f5a47aa 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx @@ -117,12 +117,6 @@ class ChartEditorNoteSprite extends FlxSprite { noteFrameCollection.pushFrame(frame); } - var frameCollectionNormal2:FlxAtlasFrames = Paths.getSparrowAtlas('NoteHoldNormal'); - - for (frame in frameCollectionNormal2.frames) - { - noteFrameCollection.pushFrame(frame); - } // Pixel notes var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx new file mode 100644 index 000000000..70383d3fd --- /dev/null +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx @@ -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); + } + } +} diff --git a/source/funkin/ui/debug/charting/import.hx b/source/funkin/ui/debug/charting/import.hx index b0569e3bb..2c3d59ef7 100644 --- a/source/funkin/ui/debug/charting/import.hx +++ b/source/funkin/ui/debug/charting/import.hx @@ -5,6 +5,7 @@ package funkin.ui.debug.charting; using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler; using funkin.ui.debug.charting.handlers.ChartEditorContextMenuHandler; using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler; +using funkin.ui.debug.charting.handlers.ChartEditorGamepadHandler; using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler; using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler; using funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler; diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx index 0b04fa4af..35facf131 100644 --- a/source/funkin/ui/freeplay/AlbumRoll.hx +++ b/source/funkin/ui/freeplay/AlbumRoll.hx @@ -1,5 +1,6 @@ package funkin.ui.freeplay; +import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.FlxSprite; import flixel.group.FlxSpriteGroup; import flixel.util.FlxSort; @@ -35,30 +36,47 @@ class AlbumRoll extends FlxSpriteGroup return value; } - var albumArt:FunkinSprite; - var albumTitle:FunkinSprite; - var difficultyStars:DifficultyStars; + var newAlbumArt:FlxAtlasSprite; + // var difficultyStars:DifficultyStars; var _exitMovers:Null; var albumData:Album; + final animNames:Map = [ + "volume1-active" => "ENTRANCE", + "volume2-active" => "ENTRANCE VOL2", + "volume3-active" => "ENTRANCE VOL3", + "volume1-trans" => "VOL1 TRANS", + "volume2-trans" => "VOL2 TRANS", + "volume3-trans" => "VOL3 TRANS", + "volume1-idle" => "VOL1 STILL", + "volume2-idle" => "VOL2 STILL", + "volume3-idle" => "VOL3 STILL", + ]; + public function new() { super(); - albumTitle = new FunkinSprite(947, 491); - albumTitle.visible = true; - albumTitle.zIndex = 200; - add(albumTitle); + newAlbumArt = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum")); + newAlbumArt.visible = false; + newAlbumArt.onAnimationFinish.add(onAlbumFinish); - difficultyStars = new DifficultyStars(140, 39); + add(newAlbumArt); - difficultyStars.stars.visible = true; - albumTitle.visible = false; - // albumArtist.visible = false; + // difficultyStars = new DifficultyStars(140, 39); + // difficultyStars.stars.visible = false; + // add(difficultyStars); + } - // var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite')); + function onAlbumFinish(animName:String):Void + { + // Play the idle animation for the current album. + newAlbumArt.playAnimation(animNames.get('$albumId-idle'), false, false, true); + + // End on the last frame and don't continue until playAnimation is called again. + // newAlbumArt.anim.pause(); } /** @@ -68,13 +86,8 @@ class AlbumRoll extends FlxSpriteGroup { if (albumId == null) { - albumArt.visible = false; - albumTitle.visible = false; - if (titleTimer != null) - { - titleTimer.cancel(); - titleTimer = null; - } + // difficultyStars.stars.visible = false; + return; } albumData = AlbumRegistry.instance.fetchEntry(albumId); @@ -86,41 +99,8 @@ class AlbumRoll extends FlxSpriteGroup return; }; - if (albumArt != null) - { - FlxTween.cancelTweensOf(albumArt); - albumArt.visible = false; - albumArt.destroy(); - remove(albumArt); - } - - // Paths.animateAtlas('freeplay/albumRoll'), - albumArt = FunkinSprite.create(1500, 360, albumData.getAlbumArtAssetKey()); - albumArt.setGraphicSize(262, 262); // Magic number for size IG - albumArt.zIndex = 100; - - // playIntro(); - add(albumArt); - applyExitMovers(); - if (Assets.exists(Paths.image(albumData.getAlbumTitleAssetKey()))) - { - if (albumData.hasAlbumTitleAnimations()) - { - albumTitle.loadSparrow(albumData.getAlbumTitleAssetKey()); - FlxAnimationUtil.addAtlasAnimations(albumTitle, albumData.getAlbumTitleAnimations()); - } - else - { - albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey())); - } - } - else - { - albumTitle.visible = false; - } - refresh(); } @@ -146,33 +126,12 @@ class AlbumRoll extends FlxSpriteGroup if (exitMovers == null) return; - exitMovers.set([albumArt], + exitMovers.set([newAlbumArt], { x: FlxG.width, speed: 0.4, wait: 0 }); - exitMovers.set([albumTitle], - { - x: FlxG.width, - speed: 0.2, - wait: 0.1 - }); - - /* - exitMovers.set([albumArtist], - { - x: FlxG.width * 1.1, - speed: 0.2, - wait: 0.2 - }); - */ - exitMovers.set([difficultyStars], - { - x: FlxG.width * 1.2, - speed: 0.2, - wait: 0.3 - }); } var titleTimer:Null = null; @@ -182,42 +141,31 @@ class AlbumRoll extends FlxSpriteGroup */ public function playIntro():Void { - albumArt.visible = true; - FlxTween.tween(albumArt, {x: 950, y: 320, angle: -340}, 0.5, {ease: FlxEase.elasticOut}); + newAlbumArt.visible = true; + newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false); - albumTitle.visible = false; - - if (titleTimer != null) - { - titleTimer.cancel(); - titleTimer = null; - } - - titleTimer = new FlxTimer().start(0.75, function(_) { - showTitle(); + // difficultyStars.stars.visible = false; + new FlxTimer().start(0.75, function(_) { + // showTitle(); + // showStars(); }); } - public function setDifficultyStars(?difficulty:Int):Void + public function skipIntro():Void { - if (difficulty == null) return; - - difficultyStars.difficulty = difficulty; + newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false); } - public function showTitle():Void - { - albumTitle.visible = true; - albumTitle.animation.play('active'); - albumTitle.animation.finishCallback = (_) -> albumTitle.animation.play('idle'); - } - - /** - * Make the album stars visible. - */ - public function showStars():Void - { - // albumArtist.visible = false; - difficultyStars.stars.visible = false; - } + // public function setDifficultyStars(?difficulty:Int):Void + // { + // if (difficulty == null) return; + // difficultyStars.difficulty = difficulty; + // } + // /** + // * Make the album stars visible. + // */ + // public function showStars():Void + // { + // difficultyStars.stars.visible = false; // true; + // } } diff --git a/source/funkin/ui/freeplay/DifficultyStars.hx b/source/funkin/ui/freeplay/DifficultyStars.hx deleted file mode 100644 index 51526bcbe..000000000 --- a/source/funkin/ui/freeplay/DifficultyStars.hx +++ /dev/null @@ -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; - } -} diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index ea89596db..efd3473e4 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -470,11 +470,7 @@ class FreeplayState extends MusicBeatSubState albumRoll.playIntro(); new FlxTimer().start(0.75, function(_) { - albumRoll.showTitle(); - }); - - new FlxTimer().start(35 / 24, function(_) { - albumRoll.showStars(); + // albumRoll.showTitle(); }); FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut}); @@ -521,7 +517,7 @@ class FreeplayState extends MusicBeatSubState // var swag:Alphabet = new Alphabet(1, 0, 'swag'); - var funnyCam:FunkinCamera = new FunkinCamera(0, 0, FlxG.width, FlxG.height); + var funnyCam:FunkinCamera = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); funnyCam.bgColor = FlxColor.TRANSPARENT; FlxG.cameras.add(funnyCam); @@ -543,41 +539,14 @@ class FreeplayState extends MusicBeatSubState * Given the current filter, rebuild the current song list. * * @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite) - * @param force + * @param force Whether the capsules should "jump" back in or not using their animation * @param onlyIfChanged Only apply the filter if the song list has changed */ public function generateSongList(filterStuff:Null, force:Bool = false, onlyIfChanged:Bool = true):Void { var tempSongs:Array = songs; - if (filterStuff != null) - { - switch (filterStuff.filterType) - { - case REGEXP: - // filterStuff.filterData has a string with the first letter of the sorting range, and the second one - // this creates a filter to return all the songs that start with a letter between those two - var filterRegexp:EReg = new EReg('^[' + filterStuff.filterData + '].*', 'i'); - tempSongs = tempSongs.filter(str -> { - if (str == null) return true; // Random - return filterRegexp.match(str.songName); - }); - case STARTSWITH: - tempSongs = tempSongs.filter(str -> { - if (str == null) return true; // Random - return str.songName.toLowerCase().startsWith(filterStuff.filterData); - }); - case ALL: - // no filter! - case FAVORITE: - tempSongs = tempSongs.filter(str -> { - if (str == null) return true; // Random - return str.isFav; - }); - default: - // return all on default - } - } + if (filterStuff != null) tempSongs = sortSongs(tempSongs, filterStuff); // Filter further by current selected difficulty. if (currentDifficulty != null) @@ -642,14 +611,7 @@ class FreeplayState extends MusicBeatSubState funnyMenu.favIcon.visible = tempSongs[i].isFav; funnyMenu.hsvShader = hsvShader; - if (i < 8) - { - funnyMenu.initJumpIn(Math.min(i, 4), force); - } - else - { - funnyMenu.forcePosition(); - } + funnyMenu.forcePosition(); grpCapsules.add(funnyMenu); } @@ -662,6 +624,48 @@ class FreeplayState extends MusicBeatSubState changeDiff(); } + /** + * Filters an array of songs based on a filter + * @param songsToFilter What data to use when filtering + * @param songFilter The filter to apply + * @return Array + */ + public function sortSongs(songsToFilter:Array, songFilter:SongFilter):Array + { + switch (songFilter.filterType) + { + case REGEXP: + // filterStuff.filterData has a string with the first letter of the sorting range, and the second one + // this creates a filter to return all the songs that start with a letter between those two + + // if filterData looks like "A-C", the regex should look something like this: ^[A-C].* + // to get every song that starts between A and C + var filterRegexp:EReg = new EReg('^[' + songFilter.filterData + '].*', 'i'); + songsToFilter = songsToFilter.filter(str -> { + if (str == null) return true; // Random + return filterRegexp.match(str.songName); + }); + + case STARTSWITH: + // extra note: this is essentially a "search" + + songsToFilter = songsToFilter.filter(str -> { + if (str == null) return true; // Random + return str.songName.toLowerCase().startsWith(songFilter.filterData); + }); + case ALL: + // no filter! + case FAVORITE: + songsToFilter = songsToFilter.filter(str -> { + if (str == null) return true; // Random + return str.isFav; + }); + default: + // return all on default + } + return songsToFilter; + } + var touchY:Float = 0; var touchX:Float = 0; var dxTouch:Float = 0; @@ -1047,15 +1051,12 @@ class FreeplayState extends MusicBeatSubState } } - // Set the difficulty star count on the right. - albumRoll.setDifficultyStars(daSong?.songRating); - // Set the album graphic and play the animation if relevant. var newAlbumId:String = daSong?.albumId; if (albumRoll.albumId != newAlbumId) { albumRoll.albumId = newAlbumId; - albumRoll.playIntro(); + albumRoll.skipIntro(); } } @@ -1169,10 +1170,6 @@ class FreeplayState extends MusicBeatSubState { currentDifficulty = rememberedDifficulty; } - - // Set the difficulty star count on the right. - var daSong:Null = grpCapsules.members[curSelected]?.songData; - albumRoll.setDifficultyStars(daSong?.songRating ?? 0); } function changeSelection(change:Int = 0):Void diff --git a/source/funkin/ui/freeplay/LetterSort.hx b/source/funkin/ui/freeplay/LetterSort.hx index 7e71b6aa1..e813c9198 100644 --- a/source/funkin/ui/freeplay/LetterSort.hx +++ b/source/funkin/ui/freeplay/LetterSort.hx @@ -39,7 +39,6 @@ class LetterSort extends FlxTypedSpriteGroup var letter:FreeplayLetter = new FreeplayLetter(i * 80, 0, i); letter.x += 50; letter.y += 50; - letter.ogY = y; // letter.visible = false; add(letter); @@ -54,7 +53,7 @@ class LetterSort extends FlxTypedSpriteGroup // don't put the last seperator if (i == 4) continue; - var sep:FlxSprite = new FlxSprite((i * 80) + 55, 20).loadGraphic(Paths.image("freeplay/seperator")); + var sep:FlxSprite = new FlxSprite((i * 80) + 60, 20).loadGraphic(Paths.image("freeplay/seperator")); // sep.animation.play("seperator"); sep.color = letter.color.getDarkened(darkness); add(sep); @@ -70,7 +69,7 @@ class LetterSort extends FlxTypedSpriteGroup changeSelection(0); } - override function update(elapsed:Float) + override function update(elapsed:Float):Void { super.update(elapsed); @@ -81,7 +80,27 @@ class LetterSort extends FlxTypedSpriteGroup } } - public function changeSelection(diff:Int = 0) + public function changeSelection(diff:Int = 0):Void + { + doLetterChangeAnims(diff); + + var multiPosOrNeg:Float = diff > 0 ? 1 : -1; + + // if we're moving left (diff < 0), we want control of the right arrow, and vice versa + var arrowToMove:FlxSprite = diff < 0 ? leftArrow : rightArrow; + arrowToMove.offset.x = 3 * multiPosOrNeg; + + new FlxTimer().start(2 / 24, function(_) { + arrowToMove.offset.x = 0; + }); + } + + /** + * Buncho timers and stuff to move the letters and seperators + * Seperated out so we can call it again on letters with songs within them + * @param diff + */ + function doLetterChangeAnims(diff:Int):Void { var ezTimer:Int->FlxSprite->Float->Void = function(frameNum:Int, spr:FlxSprite, offsetNum:Float) { new FlxTimer().start(frameNum / 24, function(_) { @@ -91,148 +110,119 @@ class LetterSort extends FlxTypedSpriteGroup var positions:Array = [-10, -22, 2, 0]; - if (diff < 0) + // if we're moving left, we want to move the positions the same amount, but negative direciton + var multiPosOrNeg:Float = diff > 0 ? 1 : -1; + + for (sep in grpSeperators) { - for (sep in grpSeperators) - { - ezTimer(0, sep, positions[0]); - ezTimer(1, sep, positions[1]); - ezTimer(2, sep, positions[2]); - ezTimer(3, sep, positions[3]); - } - - for (index => letter in letters) - { - letter.offset.x = positions[0]; - - new FlxTimer().start(1 / 24, function(_) { - letter.offset.x = positions[1]; - if (index == 0) letter.visible = false; - }); - - new FlxTimer().start(2 / 24, function(_) { - letter.offset.x = positions[2]; - if (index == 0.) letter.visible = true; - }); - - if (index == 2) - { - ezTimer(3, letter, 0); - // letter.offset.x = 0; - continue; - } - - ezTimer(3, letter, positions[3]); - } - - leftArrow.offset.x = 3; - new FlxTimer().start(2 / 24, function(_) { - leftArrow.offset.x = 0; - }); + ezTimer(0, sep, positions[0] * multiPosOrNeg); + ezTimer(1, sep, positions[1] * multiPosOrNeg); + ezTimer(2, sep, positions[2] * multiPosOrNeg); + ezTimer(3, sep, positions[3] * multiPosOrNeg); } - else if (diff > 0) + + for (index => letter in letters) { - for (sep in grpSeperators) - { - ezTimer(0, sep, -positions[0]); - ezTimer(1, sep, -positions[1]); - ezTimer(2, sep, -positions[2]); - ezTimer(3, sep, -positions[3]); - } - // same timing and functions and shit as the left one... except to the right!! + letter.offset.x = positions[0] * multiPosOrNeg; - for (index => letter in letters) - { - letter.offset.x = -positions[0]; - - new FlxTimer().start(1 / 24, function(_) { - letter.offset.x = -positions[1]; - if (index == 0) letter.visible = false; - }); - - new FlxTimer().start(2 / 24, function(_) { - letter.offset.x = -positions[2]; - if (index == 0) letter.visible = true; - }); - - if (index == 2) - { - ezTimer(3, letter, 0); - // letter.offset.x = 0; - continue; - } - - ezTimer(3, letter, -positions[3]); - } - - rightArrow.offset.x = -3; - new FlxTimer().start(2 / 24, function(_) { - rightArrow.offset.x = 0; + new FlxTimer().start(1 / 24, function(_) { + letter.offset.x = positions[1] * multiPosOrNeg; + if (index == 0) letter.visible = false; }); + + new FlxTimer().start(2 / 24, function(_) { + letter.offset.x = positions[2] * multiPosOrNeg; + if (index == 0.) letter.visible = true; + }); + + if (index == 2) + { + ezTimer(3, letter, 0); + // letter.offset.x = 0; + continue; + } + + ezTimer(3, letter, positions[3] * multiPosOrNeg); } curSelection += diff; - if (curSelection < 0) curSelection = letters[0].arr.length - 1; - if (curSelection >= letters[0].arr.length) curSelection = 0; + if (curSelection < 0) curSelection = letters[0].regexLetters.length - 1; + if (curSelection >= letters[0].regexLetters.length) curSelection = 0; for (letter in letters) letter.changeLetter(diff, curSelection); - if (changeSelectionCallback != null) changeSelectionCallback(letters[2].arr[letters[2].curLetter]); // bullshit and long lol! + if (changeSelectionCallback != null) changeSelectionCallback(letters[2].regexLetters[letters[2].curLetter]); // bullshit and long lol! } } +/** + * The actual FlxAtlasSprite for the letters, with their animation code stuff and regex stuff + */ class FreeplayLetter extends FlxAtlasSprite { - public var arr:Array = []; + /** + * A preformatted array of letter strings, for use when doing regex + * ex: ['A-B', 'C-D', 'E-H', 'I-L' ...] + */ + public var regexLetters:Array = []; + /** + * A preformatted array of the letters, for use when accessing symbol animation info + * ex: ['AB', 'CD', 'EH', 'IL' ...] + */ + public var animLetters:Array = []; + + /** + * The current letter in the regexLetters array this FreeplayLetter is on + */ public var curLetter:Int = 0; - public var ogY:Float = 0; - public function new(x:Float, y:Float, ?letterInd:Int) { super(x, y, Paths.animateAtlas("freeplay/sortedLetters")); - // frames = Paths.getSparrowAtlas("freeplay/letterStuff"); - // this.anim.play("AB"); - // trace(this.anim.symbolDictionary); - var alphabet:String = "AB-CD-EH-I L-MN-OR-s-t-UZ"; - arr = alphabet.split("-"); - arr.insert(0, "ALL"); - arr.insert(0, "fav"); - arr.insert(0, "#"); + // this is used for the regex + // /^[OR].*/gi doesn't work for showing the song Pico, so now it's + // /^[O-R].*/gi ant it works for displaying Pico + // https://regex101.com/r/bWFPfS/1 + // we split by underscores, simply for nice lil convinience + var alphabet:String = 'A-B_C-D_E-H_I-L_M-N_O-R_S_T_U-Z'; + regexLetters = alphabet.split('_'); + regexLetters.insert(0, 'ALL'); + regexLetters.insert(0, 'fav'); + regexLetters.insert(0, '#'); - // trace(arr); - - // for (str in arr) - // { - // animation.addByPrefix(str, str + " "); // string followed by a space! intentional! - // } - - // animation.addByPrefix("arrow", "mini arrow"); - // animation.addByPrefix("seperator", "seperator"); + // the symbols from flash don't have dashes, so we clean this up for use with animations + // (we don't need to re-export, rule of thumb is to accomodate files named in flash from dave + // until we get him programming classes (and since i cant find the .fla file....)) + animLetters = regexLetters.map(animLetter -> animLetter.replace('-', '')); if (letterInd != null) { - this.anim.play(arr[letterInd] + " move"); + this.anim.play(animLetters[letterInd] + " move"); this.anim.pause(); curLetter = letterInd; } } - public function changeLetter(diff:Int = 0, ?curSelection:Int) + /** + * Changes the letter graphic/anim, used in the LetterSort class above + * @param diff -1 or 1, to go left or right in the animation array + * @param curSelection what the current letter selection is, to play the bouncing anim if it matches the current letter + */ + public function changeLetter(diff:Int = 0, ?curSelection:Int):Void { curLetter += diff; - if (curLetter < 0) curLetter = arr.length - 1; - if (curLetter >= arr.length) curLetter = 0; + if (curLetter < 0) curLetter = regexLetters.length - 1; + if (curLetter >= regexLetters.length) curLetter = 0; - var animName:String = arr[curLetter] + " move"; + var animName:String = animLetters[curLetter] + ' move'; - switch (arr[curLetter]) + switch (animLetters[curLetter]) { - case "I L": + case "IL": animName = "IL move"; case "s": animName = "S move"; diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index bfb40c9ac..f6d85e56e 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -46,7 +46,7 @@ class SongMenuItem extends FlxSpriteGroup public var hsvShader(default, set):HSVShader; - var diffRatingSprite:FlxSprite; + // var diffRatingSprite:FlxSprite; public function new(x:Float, y:Float) { @@ -65,13 +65,13 @@ class SongMenuItem extends FlxSpriteGroup var rank:String = FlxG.random.getObject(ranks); ranking = new FlxSprite(capsule.width * 0.84, 30); - ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank)); - ranking.scale.x = ranking.scale.y = realScaled; + // ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank)); + // ranking.scale.x = ranking.scale.y = realScaled; // ranking.alpha = 0.75; - ranking.visible = false; - ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y); - add(ranking); - grpHide.add(ranking); + // ranking.visible = false; + // ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y); + // add(ranking); + // grpHide.add(ranking); switch (rank) { @@ -81,9 +81,9 @@ class SongMenuItem extends FlxSpriteGroup grayscaleShader = new Grayscale(1); - diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00')); + // diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00')); // diffRatingSprite.shader = grayscaleShader; - diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y); + // diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y); // TODO: Readd once ratings are fully implemented // add(diffRatingSprite); // grpHide.add(diffRatingSprite); @@ -118,8 +118,8 @@ class SongMenuItem extends FlxSpriteGroup function updateDifficultyRating(newRating:Int):Void { var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating'; - diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}')); - diffRatingSprite.visible = false; + // diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}')); + // diffRatingSprite.visible = false; } function set_hsvShader(value:HSVShader):HSVShader diff --git a/source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx b/source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx new file mode 100644 index 000000000..9c2901d16 --- /dev/null +++ b/source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx @@ -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); + } +} diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index f38db1ccd..90d79ccc1 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -1,5 +1,6 @@ package funkin.ui.mainmenu; +import funkin.graphics.FunkinSprite; import flixel.addons.transition.FlxTransitionableState; import funkin.ui.debug.DebugMenuSubState; import flixel.FlxObject; @@ -67,7 +68,7 @@ class MainMenuState extends MusicBeatState camFollow = new FlxObject(0, 0, 1, 1); add(camFollow); - magenta = new FlxSprite(Paths.image('menuDesat')); + magenta = new FlxSprite(Paths.image('menuBGMagenta')); magenta.scrollFactor.x = bg.scrollFactor.x; magenta.scrollFactor.y = bg.scrollFactor.y; magenta.setGraphicSize(Std.int(bg.width)); @@ -75,7 +76,6 @@ class MainMenuState extends MusicBeatState magenta.x = bg.x; magenta.y = bg.y; magenta.visible = false; - magenta.color = 0xFFfd719b; // TODO: Why doesn't this line compile I'm going fucking feral @@ -172,8 +172,9 @@ class MainMenuState extends MusicBeatState function resetCamStuff() { - FlxG.cameras.reset(new FunkinCamera()); + FlxG.cameras.reset(new FunkinCamera('mainMenu')); FlxG.camera.follow(camFollow, null, 0.06); + FlxG.camera.snapToTarget(); } function createMenuItem(name:String, atlas:String, callback:Void->Void, fireInstantly:Bool = false):Void @@ -322,10 +323,37 @@ class MainMenuState extends MusicBeatState } // Open the debug menu, defaults to ` / ~ + #if CHART_EDITOR_SUPPORTED if (controls.DEBUG_MENU) { FlxG.state.openSubState(new DebugMenuSubState()); } + #end + + #if (debug || FORCE_DEBUG_VERSION) + if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.W) + { + // Give the user a score of 1 point on Weekend 1 story mode. + // This makes the level count as cleared and displays the songs in Freeplay. + funkin.save.Save.instance.setLevelScore('weekend1', 'easy', + { + score: 1, + tallies: + { + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + }, + accuracy: 0, + }); + } + #end if (FlxG.sound.music.volume < 0.8) { diff --git a/source/funkin/ui/options/ControlsMenu.hx b/source/funkin/ui/options/ControlsMenu.hx index 62ae4b1a9..dd7d5ff38 100644 --- a/source/funkin/ui/options/ControlsMenu.hx +++ b/source/funkin/ui/options/ControlsMenu.hx @@ -48,7 +48,7 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page { super(); - menuCamera = new FunkinCamera(); + menuCamera = new FunkinCamera('controlsMenu'); FlxG.cameras.add(menuCamera, false); menuCamera.bgColor = 0x0; camera = menuCamera; diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx index f3d6d6fe4..81331b266 100644 --- a/source/funkin/ui/options/OptionsState.hx +++ b/source/funkin/ui/options/OptionsState.hx @@ -9,6 +9,7 @@ import flixel.util.FlxSignal; import funkin.audio.FunkinSound; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatState; +import funkin.graphics.shaders.HSVShader; import funkin.util.WindowUtil; import funkin.audio.FunkinSound; import funkin.input.Controls; @@ -19,13 +20,18 @@ class OptionsState extends MusicBeatState var currentName:PageName = Options; var currentPage(get, never):Page; - inline function get_currentPage() + inline function get_currentPage():Page return pages[currentName]; - override function create() + override function create():Void { - var menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat')); - menuBG.color = 0xFFea71fd; + var menuBG = new FlxSprite().loadGraphic(Paths.image('menuBG')); + var hsv = new HSVShader(); + hsv.hue = -0.6; + hsv.saturation = 0.9; + hsv.value = 3.6; + menuBG.shader = hsv; + FlxG.debugger.track(hsv); menuBG.setGraphicSize(Std.int(menuBG.width * 1.1)); menuBG.updateHitbox(); menuBG.screenCenter(); diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx index c23c3f165..783aef0ba 100644 --- a/source/funkin/ui/options/PreferencesMenu.hx +++ b/source/funkin/ui/options/PreferencesMenu.hx @@ -21,7 +21,7 @@ class PreferencesMenu extends Page { super(); - menuCamera = new FunkinCamera(); + menuCamera = new FunkinCamera('prefMenu'); FlxG.cameras.add(menuCamera, false); menuCamera.bgColor = 0x0; camera = menuCamera; diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 5be31fa71..d8bfd59fb 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -344,6 +344,17 @@ class StoryMenuState extends MusicBeatState changeDifficulty(0); } + // TODO: Querying UI_RIGHT_P (justPressed) after UI_RIGHT always returns false. Fix it! + if (controls.UI_RIGHT_P) + { + changeDifficulty(1); + } + + if (controls.UI_LEFT_P) + { + changeDifficulty(-1); + } + if (controls.UI_RIGHT) { rightDifficultyArrow.animation.play('press'); @@ -362,16 +373,6 @@ class StoryMenuState extends MusicBeatState leftDifficultyArrow.animation.play('idle'); } - if (controls.UI_RIGHT_P) - { - changeDifficulty(1); - } - - if (controls.UI_LEFT_P) - { - changeDifficulty(-1); - } - if (FlxG.keys.justPressed.TAB && modeText.visible) { switchMode(!displayingModdedLevels); diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index e76e66003..d122bf7c1 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -188,6 +188,7 @@ class TitleState extends MusicBeatState ngSpr.animation.add('idle', [0, 1], 4); ngSpr.animation.play('idle'); ngSpr.setGraphicSize(Std.int(ngSpr.width * 0.55)); + ngSpr.y += 25; } else { diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index af8798ae2..3b53e1b4a 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -77,7 +77,7 @@ class LoadingState extends MusicBeatSubState var difficulty:String = playParams.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY; var variation:String = playParams.targetVariation ?? Constants.DEFAULT_VARIATION; var targetChart:SongDifficulty = playParams.targetSong?.getDifficulty(difficulty, variation); - var instPath:String = Paths.inst(targetChart.song.id); + var instPath:String = targetChart.getInstPath(playParams.targetInstrumental); var voicesPaths:Array = targetChart.buildVoiceList(); checkLoadSong(instPath); @@ -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. FunkinSprite.preparePurgeCache(); - FunkinSprite.cacheTexture(Paths.image('combo')); FunkinSprite.cacheTexture(Paths.image('healthBar')); FunkinSprite.cacheTexture(Paths.image('menuDesat')); FunkinSprite.cacheTexture(Paths.image('combo')); @@ -333,6 +332,7 @@ class LoadingState extends MusicBeatSubState // Since FlxGraphic tells OpenFL to not cache it, we have to do it manually. if (path.endsWith('spritemap1.png')) { + trace('Preloading FlxAnimate asset: ${path}'); openfl.Assets.getBitmapData(path, true); } } diff --git a/source/funkin/ui/transition/StickerSubState.hx b/source/funkin/ui/transition/StickerSubState.hx index 0b5e16f97..e5abef872 100644 --- a/source/funkin/ui/transition/StickerSubState.hx +++ b/source/funkin/ui/transition/StickerSubState.hx @@ -247,10 +247,6 @@ class StickerSubState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - // TODO: Rework this asset caching stuff - FunkinSprite.preparePurgeCache(); - FunkinSprite.purgeCache(); - // I think this grabs the screen and puts it under the stickers? // Leaving this commented out rather than stripping it out because it's cool... /* @@ -265,7 +261,15 @@ class StickerSubState extends MusicBeatSubState // FlxG.addChildBelowMouse(dipshit); */ - FlxG.switchState(() -> targetState(this)); + FlxG.switchState(() -> { + // TODO: Rework this asset caching stuff + // NOTE: This has to come AFTER the state switch, + // otherwise the game tries to render destroyed sprites! + FunkinSprite.preparePurgeCache(); + FunkinSprite.purgeCache(); + + return targetState(this); + }); } }); }); diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index ebc58889f..568f9ec5a 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -218,14 +218,15 @@ class Constants public static final DEFAULT_VARIATION:String = 'default'; /** - * Standard variations used by the game. + * Standardized variations for charts */ public static final DEFAULT_VARIATION_LIST:Array = ['default', 'erect', 'pico']; /** - * The default intensity for camera zooms. + * The default intensity multiplier for camera bops. + * Prolly needs to be tuned bc it's a multiplier now. */ - public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015; + public static final DEFAULT_BOP_INTENSITY:Float = 1.015; /** * The default rate for camera zooms (in beats per zoom). diff --git a/source/funkin/util/EaseUtil.hx b/source/funkin/util/EaseUtil.hx new file mode 100644 index 000000000..200e74d07 --- /dev/null +++ b/source/funkin/util/EaseUtil.hx @@ -0,0 +1,17 @@ +package funkin.util; + +class EaseUtil +{ + /** + * Returns an ease function that eases via steps. + * Useful for "retro" style fades (week 6!) + * @param steps how many steps to ease over + * @return Float->Float + */ + public static inline function stepped(steps:Int):Float->Float + { + return function(t:Float):Float { + return Math.floor(t * steps) / steps; + } + } +} diff --git a/source/funkin/util/FlxTweenUtil.hx b/source/funkin/util/FlxTweenUtil.hx new file mode 100644 index 000000000..57c8f0cfe --- /dev/null +++ b/source/funkin/util/FlxTweenUtil.hx @@ -0,0 +1,36 @@ +package funkin.util; + +import flixel.addons.plugin.taskManager.FlxTask; +import flixel.tweens.FlxTween; +import flixel.tweens.FlxEase; + +class FlxTweenUtil +{ + public static function pauseTween(tween:FlxTween):Void + { + if (tween != null) + { + tween.active = false; + } + } + + public static function resumeTween(tween:FlxTween):Void + { + if (tween != null) + { + tween.active = true; + } + } + + public static function pauseTweensOf(Object:Dynamic, ?FieldPaths:Array):Void + { + @:privateAccess + FlxTween.globalManager.forEachTweensOf(Object, FieldPaths, pauseTween); + } + + public static function resumeTweensOf(Object:Dynamic, ?FieldPaths:Array):Void + { + @:privateAccess + FlxTween.globalManager.forEachTweensOf(Object, FieldPaths, resumeTween); + } +}