diff --git a/.github/actions/setup-haxe/action.yml b/.github/actions/setup-haxe/action.yml new file mode 100644 index 000000000..54db9bf79 --- /dev/null +++ b/.github/actions/setup-haxe/action.yml @@ -0,0 +1,135 @@ +name: setup-haxeshit +description: "sets up haxe shit, using HMM!" + +inputs: + haxe: + description: 'Version of haxe to install' + required: true + default: '4.3.4' + hxcpp-cache: + description: 'Whether to use a shared hxcpp compile cache' + required: true + default: 'true' + hxcpp-cache-path: + description: 'Path to create hxcpp cache in' + required: true + default: ${{ runner.temp }}/hxcpp_cache + targets: + description: 'Targets we plan to compile to. Installs native dependencies needed.' + required: true + +runs: + using: "composite" + steps: + + - name: Setup timers + shell: bash + run: | + echo "TIMER_HAXE=$(date +%s)" >> "$GITHUB_ENV" + + - name: Install Haxe + uses: funkincrew/ci-haxe@v3.1.0 + with: + haxe-version: ${{ inputs.haxe }} + + - name: Install native dependencies + if: ${{ runner.os == 'Linux' }} + shell: bash + run: | + ls -lah /usr/lib/x86_64-linux-gnu/ + apt-get update + apt-get install -y \ + g++ \ + libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \ + libgl-dev libgl1-mesa-dev \ + libasound2-dev + ln -s /usr/lib/x86_64-linux-gnu/libffi.so.8 /usr/lib/x86_64-linux-gnu/libffi.so.6 || true + - name: Install linux-specific dependencies + if: ${{ runner.os == 'Linux' && contains(inputs.targets, 'linux') }} + shell: bash + run: | + apt-get install -y libvlc-dev libvlccore-dev + + - name: Config haxelib + shell: bash + run: | + echo "TIMER_HAXELIB=$(date +%s)" >> "$GITHUB_ENV" + haxelib --debug --never install haxelib 4.1.0 --global + haxelib --debug --never deleterepo || true + haxelib --debug --never newrepo + echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV" + haxelib --debug --never git haxelib https://github.com/HaxeFoundation/haxelib.git master + haxelib --debug --global install hmm + echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV" + + - name: Restore cached dependencies + id: cache-hmm + uses: actions/cache@v4 + with: + path: .haxelib + key: haxe-hmm-${{ runner.os }}-${{ hashFiles('**/hmm.json') }} + + - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} + name: Install dependencies + shell: bash + run: | + haxelib --debug --global run hmm install + echo "TIMER_DONE=$(date +%s)" >> "$GITHUB_ENV" + + # by default use a shared hxcpp cache + - if: ${{ inputs.hxcpp-cache == 'true' }} + name: Restore hxcpp cache + uses: actions/cache@v4 + with: + path: ${{ inputs.hxcpp-cache-path }} + key: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }} + # export env for it to reuse in builds + - if: ${{ inputs.hxcpp-cache == 'true' }} + name: Persist env for hxcpp cache + shell: bash + run: | + echo "HXCPP_COMPILE_CACHE=${{ inputs.hxcpp-cache-path }}" >> "$GITHUB_ENV" + echo 'HXCPP_CACHE_MB="4096"' >> "$GITHUB_ENV" + + # if it's explicitly disabled, still cache export/ since that then contains the builds + - if: ${{ inputs.hxcpp-cache != 'true' }} + name: Restore export cache + uses: actions/cache@v4 + with: + path: ${{ inputs.hxcpp-cache-path }} + key: haxe-export-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: haxe-export-${{ runner.os }}-${{ github.ref_name }} + + - name: Print debug info + shell: bash + run: | + cat << EOF + runner: + kernel: $(uname -a) + haxe: + version: $(haxe -version) + which: $(which haxe) + haxepath: $HAXEPATH + took: $((TIMER_HAXELIB - TIMER_HAXE))s + haxelib: + version: $(haxelib version) + which: $(which haxelib) + local: + config: $(haxelib config) + path: $(haxelib path haxelib || true) + global + config: $(haxelib config --global) + path: $(haxelib path haxelib --global || true) + system + version: $(haxelib --system version) + local: + config: $(haxelib --system config) + global: + config: $(haxelib --system config --global) + took: $((TIMER_DEPS - TIMER_HAXELIB))s + deps: + took: $((TIMER_DONE - TIMER_DEPS))s + hxcpp_cache: | + $(haxelib run hxcpp cache list || true) + EOF 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..15c9e5582 --- /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..3bfea20f2 --- /dev/null +++ b/.github/workflows/build-game.yml @@ -0,0 +1,125 @@ +name: Build and Upload nightly game builds + +on: + workflow_dispatch: + push: + paths-ignore: + - '**/Dockerfile' + - '.github/workflows/build-docker-image.yml' + +jobs: + + build-game-on-host: + strategy: + matrix: + include: + - target: windows + - target: macos + runs-on: + - ${{ matrix.target }} + defaults: + run: + shell: bash + + steps: + - name: Make git happy + if: ${{ matrix.target == 'macos' }} + run: | + git config --global --add safe.directory $GITHUB_WORKSPACE + + - name: Get checkout token + uses: actions/create-github-app-token@v1 + id: app_token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PEM }} + owner: ${{ github.repository_owner }} + + - name: Checkout repo + uses: funkincrew/ci-checkout@v6 + with: + submodules: 'recursive' + token: ${{ steps.app_token.outputs.token }} + + - name: Setup build environment + uses: ./.github/actions/setup-haxe + + - name: Build game + if: ${{ matrix.target == 'windows' }} + run: | + haxelib run lime build windows -v -release -DGITHUB_BUILD + timeout-minutes: 120 + - name: Build game + if: ${{ matrix.target != 'windows' }} + run: | + haxelib run lime build ${{ matrix.target }} -v -release --times -DGITHUB_BUILD + timeout-minutes: 120 + + - name: Upload build artifacts + uses: ./.github/actions/upload-itch + with: + butler-key: ${{ secrets.BUTLER_API_KEY}} + target: ${{ matrix.target }} + + build-game-in-container: + runs-on: build-set + container: ghcr.io/funkincrew/build-dependencies:latest + strategy: + matrix: + include: + - target: linux + - target: html5 + defaults: + run: + shell: bash + + steps: + - name: Get checkout token + uses: actions/create-github-app-token@v1 + id: app_token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PEM }} + owner: ${{ github.repository_owner }} + + - name: Checkout repo + uses: funkincrew/ci-checkout@v6 + with: + submodules: 'recursive' + token: ${{ steps.app_token.outputs.token }} + + - name: Config haxelib + run: | + haxelib --never newrepo + echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV" + + - name: Restore cached dependencies + id: cache-hmm + uses: actions/cache@v4 + with: + path: .haxelib + key: haxe-hmm-${{ runner.os }}-${{ hashFiles('**/hmm.json') }} + + - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} + name: Install dependencies + run: | + haxelib --global run hmm install + + - if: ${{ matrix.target != 'html5' }} + name: Restore hxcpp cache + uses: actions/cache@v4 + with: + path: /usr/share/hxcpp + key: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }} + + - name: Build game + run: | + haxelib run lime build ${{ matrix.target }} -v -release --times -DGITHUB_BUILD + timeout-minutes: 120 + + - name: Upload build artifacts + uses: ./.github/actions/upload-itch + with: + butler-key: ${{ secrets.BUTLER_API_KEY}} + target: ${{ matrix.target }} diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml deleted file mode 100644 index e217d1f18..000000000 --- a/.github/workflows/build-shit.yml +++ /dev/null @@ -1,132 +0,0 @@ -name: build-upload -on: - workflow_dispatch: - push: - -jobs: - create-nightly-html5: - runs-on: [self-hosted, linux] - container: ubuntu:23.10 - steps: - - name: Install tools missing in container - run: | - apt update - apt install -y sudo git curl unzip - - name: Fix git config on posix runner - # this can't be {{ github.workspace }} because that's not docker-aware - run: | - git config --global --add safe.directory $GITHUB_WORKSPACE - - name: Get checkout token - uses: actions/create-github-app-token@v1 - id: app_token - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PEM }} - owner: ${{ github.repository_owner }} - - name: Checkout repo - uses: funkincrew/ci-checkout@v6 - with: - submodules: 'recursive' - token: ${{ steps.app_token.outputs.token }} - - name: Install Haxe, dependencies - uses: ./.github/actions/setup-haxeshit - - name: Install native dependencies - run: | - apt install -y \ - libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \ - libgl-dev libgl1-mesa-dev \ - libasound2-dev - - name: Build game - run: | - haxelib run lime build html5 -release --times -DGITHUB_BUILD - - name: Upload build artifacts - uses: ./.github/actions/upload-itch - with: - butler-key: ${{ secrets.BUTLER_API_KEY}} - build-dir: export/release/html5/bin - target: html5 - create-nightly-win: - runs-on: [self-hosted, windows] - defaults: - run: - shell: bash - steps: - - name: Get checkout token - uses: actions/create-github-app-token@v1 - id: app_token - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PEM }} - owner: ${{ github.repository_owner }} - - name: Checkout repo - uses: funkincrew/ci-checkout@v6 - with: - submodules: 'recursive' - token: ${{ steps.app_token.outputs.token }} - - name: Install Haxe, dependencies - uses: ./.github/actions/setup-haxeshit - - name: Setup build cache - run: | - mkdir -p ${{ runner.temp }}/hxcpp_cache - - name: Restore build cache - id: cache-build-win - uses: actions/cache@v4 - with: - path: | - export - ${{ runner.temp }}/hxcpp_cache - key: ${{ runner.os }}-build-win-${{ github.ref_name }} - - name: Build game - run: | - haxelib run lime build windows -v -release -DGITHUB_BUILD - env: - HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache" - - name: Upload build artifacts - uses: ./.github/actions/upload-itch - with: - butler-key: ${{ secrets.BUTLER_API_KEY }} - build-dir: export/release/windows/bin - target: win - create-nightly-mac: - runs-on: [self-hosted, macos] - steps: - - name: Fix git config on posix runner - # this can't be {{ github.workspace }} because that's not docker-aware - run: | - git config --global --add safe.directory $GITHUB_WORKSPACE - - name: Get checkout token - uses: actions/create-github-app-token@v1 - id: app_token - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PEM }} - owner: ${{ github.repository_owner }} - - name: Checkout repo - uses: funkincrew/ci-checkout@v6 - with: - submodules: 'recursive' - token: ${{ steps.app_token.outputs.token }} - - name: Install Haxe, dependencies - uses: ./.github/actions/setup-haxeshit - - name: Setup build cache - run: | - mkdir -p ${{ runner.temp }}/hxcpp_cache - - name: Restore build cache - id: cache-build-win - uses: actions/cache@v4 - with: - path: | - export - ${{ runner.temp }}/hxcpp_cache - key: ${{ runner.os }}-build-mac-${{ github.ref_name }} - - name: Build game - run: | - haxelib run lime build macos -release --times -DGITHUB_BUILD - env: - HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache" - - name: Upload build artifacts - uses: ./.github/actions/upload-itch - with: - butler-key: ${{ secrets.BUTLER_API_KEY}} - build-dir: export/release/macos/bin - target: macos diff --git a/.github/workflows/cancel-merged-branches.yml b/.github/workflows/cancel-merged-branches.yml new file mode 100644 index 000000000..f66f9647b --- /dev/null +++ b/.github/workflows/cancel-merged-branches.yml @@ -0,0 +1,38 @@ +name: Cancel queued workflows on PR merge + +on: + pull_request: + types: + - closed + +jobs: + + cancel_stuff: + if: github.event.pull_request.merged == true + runs-on: build-set + permissions: + actions: write + + steps: + - name: Cancel queued workflows for ${{ github.event.pull_request.head.ref }} + uses: actions/github-script@v7 + with: + result-encoding: string + retries: 3 + script: | + let branch_workflows = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: "build-shit.yml", + status: "queued", + branch: "${{ github.event.pull_request.head.ref }}" + }); + let runs = branch_workflows.data.workflow_runs; + runs.forEach((run) => { + github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id + }); + }); + console.log(runs); diff --git a/.vscode/settings.json b/.vscode/settings.json index 13a1862d2..c28bebeab 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", diff --git a/Project.xml b/Project.xml index c0da3c89a..db338d32a 100644 --- a/Project.xml +++ b/Project.xml @@ -45,6 +45,7 @@ <library name="week6" preload="true" /> <library name="week7" preload="true" /> <library name="weekend1" preload="true" /> + <library name="videos" preload="true" /> </section> <section if="NO_PRELOAD_ALL"> <library name="songs" preload="false" /> @@ -58,10 +59,13 @@ <library name="week6" preload="false" /> <library name="week7" preload="false" /> <library name="weekend1" preload="false" /> + <library name="videos" preload="false" /> </section> <library name="art" preload="false" /> <assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" /> <assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" /> + <!-- Videos go in their own library because web never needs to preload them, they can just be streamed. --> + <assets path="assets/videos" library="videos" /> <assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" /> <assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" /> <assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" /> @@ -119,7 +123,7 @@ <haxelib name="flixel-text-input" /> <!-- Improved text field rendering for HaxeUI --> <haxelib name="polymod" /> <!-- Modding framework --> <haxelib name="flxanimate" /> <!-- Texture atlas rendering --> - <haxelib name="hxCodec" if="desktop" /> <!-- Video playback --> + <haxelib name="hxCodec" if="desktop" unless="hl" /> <!-- Video playback --> <haxelib name="json2object" /> <!-- JSON parsing --> <haxelib name="thx.semver" /> <!-- Version string handling --> @@ -183,6 +187,7 @@ <haxedef name="haxeui_focus_out_on_click" /> <!-- Required to use haxe.ui.backend.flixel.UIState with build macros. --> <haxedef name="haxeui_dont_impose_base_class" /> + <haxedef name="HARDCODED_CREDITS" /> <!-- Skip the Intro --> <section if="debug"> diff --git a/assets b/assets index 485243fdd..837a8639b 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 485243fdd44acbc4db6a97ec7bf10a8b18350be9 +Subproject commit 837a8639bd7abe4aa8786dc3790e8d4576f04f28 diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 000000000..88b44f7a6 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,185 @@ +FROM ubuntu:mantic + +ARG haxe_version=4.3.4 +ARG haxelib_version=4.1.0 +ARG neko_version=2.3.0 + +# prepare runner +ENV GITHUB_HOME="/github/home" + +RUN <<EOF +mkdir -p "$GITHUB_HOME" +mkdir -p /opt +mkdir -p /usr/share/hxcpp +mkdir -p /usr/local/bin +chmod -R 777 /opt +chmod -R 777 /usr/share +chmod -R 777 /usr/local/bin +EOF + + +# Prepare Ubuntu +# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/configure-environment.sh +# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/configure-system.sh +RUN <<EOF +echo 'vm.max_map_count=262144' | tee -a /etc/sysctl.conf +echo 'fs.inotify.max_user_watches=655360' | tee -a /etc/sysctl.conf +echo 'fs.inotify.max_user_instances=1280' | tee -a /etc/sysctl.conf +EOF + +ENV DEBIAN_FRONTEND="noninteractive" + +# Prepare APT +RUN <<EOF +cat <<EOC >> /etc/apt/apt.conf.d/10apt-autoremove +APT::Get::AutomaticRemove "0"; +APT::Get::HideAutoRemove "1"; +EOC + +echo <<EOC >> /etc/apt/apt.conf.d/80retries +"APT::Acquire::Retries \"10\";" +EOC + +echo <<EOC >> /etc/apt/apt.conf.d/90assumeyes +"APT::Get::Assume-Yes \"true\";" +EOC +EOF + +# Prepare apt-fast +RUN <<EOF +apt-get update +apt-get install -y --no-install-recommends software-properties-common +add-apt-repository -y ppa:apt-fast/stable +apt-get -y install apt-fast +echo debconf apt-fast/maxdownloads string 8 | debconf-set-selections +echo debconf apt-fast/dlflag boolean true | debconf-set-selections +echo debconf apt-fast/aptmanager string apt-get | debconf-set-selections +EOF + +# Base packages +# https://github.com/actions/runner-images/blob/main/images/ubuntu/toolsets/toolset-2204.json#L114 +RUN <<EOF +apt-fast install -y --no-install-recommends \ + ca-certificates \ + bzip2 curl g++ gcc make jq tar unzip wget \ + sudo git openssh-client +EOF + +# Prepare git +RUN <<EOF +cat <<EOC >> /etc/gitconfig +[safe] + directory = * +EOC + +ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /etc/ssh/ssh_known_hosts +ssh-keyscan -t rsa,ecdsa,ed25519 ravy.dev >> /etc/ssh/ssh_known_hosts +EOF + +# Haxe native dependencies +RUN <<EOF +apt-fast install -y --no-install-recommends \ + libc6-dev libffi-dev \ + libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \ + libgl-dev libgl1-mesa-dev \ + libasound2-dev \ + libvlc-dev libvlccore-dev +EOF + +# Janky libffi.6 fix +RUN <<EOF +ln -s \ + /usr/lib/x86_64-linux-gnu/libffi.so.8 \ + /usr/lib/x86_64-linux-gnu/libffi.so.6 \ + || true +EOF + +# neko +# https://github.com/HaxeFoundation/neko/releases/download/v2-3-0/neko-2.3.0-linux64.tar.gz +RUN <<EOF +neko_url=$(curl https://api.github.com/repos/HaxeFoundation/neko/releases -sfL \ + | jq '.[] | select(.name == "'"$neko_version"'")' \ + | jq '.assets[] | select(.name | endswith("linux64.tar.gz"))' \ + | jq -r '.browser_download_url') +curl -sfL "$neko_url" | tar -xz -C /usr/local +EOF + +RUN <<EOF +neko_path="$(find /usr/local -maxdepth 1 -type d -name 'neko*')" +ln -s "$neko_path" /usr/local/neko +EOF + +ENV NEKOPATH="/usr/local/neko" +ENV LD_LIBRARY_PATH="$NEKOPATH:$LD_LIBRARY_PATH" +ENV PATH="$NEKOPATH:$PATH" + +# haxe +# https://github.com/HaxeFoundation/haxe/releases/download/4.0.5/haxe-4.0.5-linux64.tar.gz +RUN <<EOF +haxe_url=$(curl https://api.github.com/repos/HaxeFoundation/haxe/releases -sfL \ + | jq '.[] | select(.name == "'"$haxe_version"'")' \ + | jq '.assets[] | select(.name | endswith("linux64.tar.gz"))' \ + | jq -r '.browser_download_url') +curl -sfL "$haxe_url" | tar -xz -C /usr/local +EOF + +RUN <<EOF +haxe_path="$(find /usr/local -maxdepth 1 -type d -name 'haxe*')" +ln -s "$haxe_path" /usr/local/haxe +EOF + +ENV HAXEPATH="/usr/local/haxe" +ENV HAXE_STD_PATH="$HAXEPATH/std" +ENV PATH="$HAXEPATH:$PATH" + +# haxelib +RUN <<EOF +HOME=/etc haxelib setup "$HAXEPATH/lib" +haxelib --global --never install haxelib $haxelib_version +haxelib --global --never git haxelib https://github.com/HaxeFoundation/haxelib.git master +haxelib --global --never install hmm +EOF + +# hxcpp +ENV HXCPP_COMPILE_CACHE="/usr/share/hxcpp" +ENV HXCPP_CACHE_MB="4096" + +# Clean up +# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/cleanup.sh +RUN <<EOF +rm -r /var/cache/apt/apt-fast +apt-get clean +if [ -d /var/lib/apt/lists ]; then + rm -rf /var/lib/apt/lists/* +fi + +if [ -d /tmp ]; then + rm -rf /tmp/* +fi +if [ -d /root/.cache ]; then + rm -rf /root/.cache +fi + +if command -v journalctl; then + journalctl --rotate + journalctl --vacuum-time=1s +fi +if [ -d /var/log ]; then + find /var/log -type f -regex ".*\.gz$" -delete + find /var/log -type f -regex ".*\.[0-9]$" -delete + find /var/log/ -type f -exec cp /dev/null {} \; +fi +if [ -f /usr/local/bin/invoke_tests ]; then + rm -rf /usr/local/bin/invoke_tests +fi +EOF + +# Print debug info +RUN <<EOF +echo "/root" +ls -la /root +cat /root/.haxelib && echo + +id +env +EOF diff --git a/hmm.json b/hmm.json index 0dfe88ded..8c07023c7 100644 --- a/hmm.json +++ b/hmm.json @@ -104,7 +104,7 @@ "name": "lime", "type": "git", "dir": null, - "ref": "1359fe6ad52e91175dc636a516d460bd54ea22ed", + "ref": "43ebebdd8119936b99f23407057025c7849c5f5b", "url": "https://github.com/FunkinCrew/lime" }, { @@ -139,7 +139,7 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "f229d76361c7e31025a048fe7909847f75bb5d5e", + "ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134", "url": "https://github.com/FunkinCrew/openfl" }, { diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx index 996e2367e..94f41cea4 100644 --- a/source/funkin/Highscore.hx +++ b/source/funkin/Highscore.hx @@ -59,6 +59,7 @@ abstract Tallies(RawTallies) totalNotes: 0, totalNotesHit: 0, maxCombo: 0, + score: 0, isNewHighscore: false } } @@ -81,6 +82,9 @@ typedef RawTallies = var good:Int; var sick:Int; var maxCombo:Int; + + var score:Int; + var isNewHighscore:Bool; /** diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 6ea77ec18..9b842bc13 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 d2c4833f2..54a4b7acf 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -9,7 +9,7 @@ import openfl.utils.Assets as OpenFlAssets; */ class Paths { - static var currentLevel:String; + static var currentLevel:Null<String> = null; public static function setCurrentLevel(name:String):Void { @@ -113,7 +113,7 @@ class Paths public static function videos(key:String, ?library:String):String { - return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library); + return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library ?? 'videos'); } public static function voices(song:String, ?suffix:String = ''):String diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 0df290feb..df05cc3ef 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -223,11 +223,12 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound> // already paused before we lost focus. if (_lostFocus && !_alreadyPaused) { + trace('Resuming audio (${this._label}) on focus!'); resume(); } else { - trace('Not resuming audio on focus!'); + trace('Not resuming audio (${this._label}) on focus!'); } _lostFocus = false; } @@ -265,10 +266,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound> @:allow(flixel.sound.FlxSoundGroup) override function updateTransform():Void { - _transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end - (group != null ? group.volume : 1) * _volume * _volumeAdjust; + if (_transform != null) + { + _transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end + (group != null ? group.volume : 1) * _volume * _volumeAdjust; + } - if (_channel != null) _channel.soundTransform = _transform; + if (_channel != null) + { + _channel.soundTransform = _transform; + } } public function clone():FunkinSound @@ -315,6 +322,13 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound> } } + if (FlxG.sound.music != null) + { + FlxG.sound.music.fadeTween?.cancel(); + FlxG.sound.music.stop(); + FlxG.sound.music.kill(); + } + if (params?.mapTimeChanges ?? true) { var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key); @@ -329,13 +343,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound> } } - if (FlxG.sound.music != null) - { - FlxG.sound.music.fadeTween?.cancel(); - FlxG.sound.music.stop(); - FlxG.sound.music.kill(); - } - var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true); if (music != null) { @@ -391,10 +398,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound> sound._label = 'unknown'; } + if (autoPlay) sound.play(); sound.volume = volume; sound.group = FlxG.sound.defaultSoundGroup; sound.persist = true; - if (autoPlay) sound.play(); + + // Make sure to add the sound to the list. + // If it's already in, it won't get re-added. + // If it's not in the list (it gets removed by FunkinSound.playMusic()), + // it will get re-added (then if this was called by playMusic(), removed again) + FlxG.sound.list.add(sound); // Call onLoad() because the sound already loaded if (onLoad != null && sound._sound != null) onLoad(); @@ -402,10 +415,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound> return sound; } + @:nullSafety(Off) public override function destroy():Void { // trace('[FunkinSound] Destroying sound "${this._label}"'); super.destroy(); + if (fadeTween != null) + { + fadeTween.cancel(); + fadeTween = null; + } FlxTween.cancelTweensOf(this); this._label = 'unknown'; } diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx index 020d5f5bb..5fc2abe0e 100644 --- a/source/funkin/audio/SoundGroup.hx +++ b/source/funkin/audio/SoundGroup.hx @@ -150,7 +150,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound> /** * Stop all the sounds in the group. */ - public function stop() + public function stop():Void { if (members != null) { @@ -160,7 +160,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound> } } - public override function destroy() + public override function destroy():Void { stop(); super.destroy(); @@ -178,9 +178,14 @@ class SoundGroup extends FlxTypedGroup<FunkinSound> function get_time():Float { - if (getFirstAlive() != null) return getFirstAlive().time; + if (getFirstAlive() != null) + { + return getFirstAlive().time; + } else + { return 0; + } } function set_time(time:Float):Float @@ -195,16 +200,26 @@ class SoundGroup extends FlxTypedGroup<FunkinSound> function get_playing():Bool { - if (getFirstAlive() != null) return getFirstAlive().playing; + if (getFirstAlive() != null) + { + return getFirstAlive().playing; + } else + { return false; + } } function get_volume():Float { - if (getFirstAlive() != null) return getFirstAlive().volume; + if (getFirstAlive() != null) + { + return getFirstAlive().volume; + } else + { return 1; + } } // in PlayState, adjust the code so that it only mutes the player1 vocal tracks? diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index 91054cfb0..5037ee1d0 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -159,10 +159,18 @@ class VoicesGroup extends SoundGroup public override function destroy():Void { - playerVoices.destroy(); - playerVoices = null; - opponentVoices.destroy(); - opponentVoices = null; + if (playerVoices != null) + { + playerVoices.destroy(); + playerVoices = null; + } + + if (opponentVoices != null) + { + opponentVoices.destroy(); + opponentVoices = null; + } + super.destroy(); } } diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 7419d9425..118516bec 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -325,12 +325,3 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo } } } - -/** - * A pair of a file name and its contents. - */ -typedef JsonFile = -{ - fileName:String, - contents:String -}; diff --git a/source/funkin/data/JsonFile.hx b/source/funkin/data/JsonFile.hx new file mode 100644 index 000000000..421ffc22f --- /dev/null +++ b/source/funkin/data/JsonFile.hx @@ -0,0 +1,10 @@ +package funkin.data; + +/** + * A pair of a file name and its contents. + */ +typedef JsonFile = +{ + fileName:String, + contents:String +}; diff --git a/source/funkin/data/freeplay/AlbumData.hx b/source/funkin/data/freeplay/AlbumData.hx index 265a01fce..ca851376d 100644 --- a/source/funkin/data/freeplay/AlbumData.hx +++ b/source/funkin/data/freeplay/AlbumData.hx @@ -1,5 +1,7 @@ package funkin.data.freeplay; +import funkin.data.animation.AnimationData; + /** * A type definition for the data for an album of songs. * It includes things like what graphics to display in Freeplay. @@ -33,4 +35,11 @@ typedef AlbumData = * The album title will be displayed below the album art in Freeplay. */ public var albumTitleAsset:String; + + /** + * An optional array of animations for the album title. + */ + @:optional + @:default([]) + public var albumTitleAnimations:Array<AnimationData>; } diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 4fdf5d0df..277dcd9e1 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -427,7 +427,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> return ScriptedSong.listScriptClasses(); } - function loadEntryMetadataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> + function loadEntryMetadataFile(id:String, ?variation:String):Null<JsonFile> { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); @@ -442,7 +442,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> return {fileName: entryFilePath, contents: rawJson}; } - function loadMusicDataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> + function loadMusicDataFile(id:String, ?variation:String):Null<JsonFile> { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json'); @@ -460,7 +460,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> return openfl.Assets.exists(entryFilePath); } - function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> + function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile> { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx index 5394bce1a..5ab2df837 100644 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -137,7 +137,8 @@ class FlxAtlasSprite extends FlxAnimate anim.callback = function(_, frame:Int) { var offset = loop ? 0 : -1; - if (frame == (anim.getFrameLabel(id).duration + offset) + anim.getFrameLabel(id).index) + var frameLabel = anim.getFrameLabel(id); + if (frame == (frameLabel.duration + offset) + frameLabel.index) { if (loop) { 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 733bbca7f..2dfdac2c9 100644 --- a/source/funkin/graphics/shaders/HSVShader.hx +++ b/source/funkin/graphics/shaders/HSVShader.hx @@ -20,7 +20,7 @@ class HSVShader extends FlxRuntimeShader function set_hue(value:Float):Float { - this.setFloat('hue', value); + this.setFloat('_hue', value); this.hue = value; return this.hue; diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 78f660d3f..62860ee0f 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/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index fd58d0fad..c262c311d 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -8,7 +8,12 @@ import funkin.modding.IScriptedClass; */ class ScriptEventDispatcher { - public static function callEvent(target:IScriptedClass, event:ScriptEvent):Void + /** + * Invoke the given event hook on the given scripted class. + * @param target The target class to call script hooks on. + * @param event The event, which determines the script hook to call and provides parameters for it. + */ + public static function callEvent(target:Null<IScriptedClass>, event:ScriptEvent):Void { if (target == null || event == null) return; diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index a1796e912..e7b128385 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -3,18 +3,18 @@ package funkin.play; import flixel.FlxG; import flixel.FlxObject; import flixel.FlxSprite; -import funkin.audio.FunkinSound; +import flixel.input.touch.FlxTouch; import flixel.util.FlxColor; import flixel.util.FlxTimer; +import funkin.audio.FunkinSound; import funkin.graphics.FunkinSprite; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.BaseCharacter; -import funkin.play.PlayState; -import funkin.util.MathUtil; import funkin.ui.freeplay.FreeplayState; import funkin.ui.MusicBeatSubState; import funkin.ui.story.StoryMenuState; +import funkin.util.MathUtil; import openfl.utils.Assets; /** @@ -23,13 +23,14 @@ import openfl.utils.Assets; * * The newest implementation uses a substate, which prevents having to reload the song and stage each reset. */ +@:nullSafety class GameOverSubState extends MusicBeatSubState { /** * The currently active GameOverSubState. * There should be only one GameOverSubState in existance at a time, we can use a singleton. */ - public static var instance:GameOverSubState = null; + public static var instance:Null<GameOverSubState> = null; /** * Which alternate animation on the character to use. @@ -37,7 +38,7 @@ class GameOverSubState extends MusicBeatSubState * For example, playing a different animation when BF dies in Week 4 * or Pico dies in Weekend 1. */ - public static var animationSuffix:String = ""; + public static var animationSuffix:String = ''; /** * Which alternate game over music to use. @@ -45,17 +46,19 @@ class GameOverSubState extends MusicBeatSubState * For example, the bf-pixel script sets this to `-pixel` * and the pico-playable script sets this to `Pico`. */ - public static var musicSuffix:String = ""; + public static var musicSuffix:String = ''; /** * Which alternate "blue ball" sound effect to use. */ - public static var blueBallSuffix:String = ""; + public static var blueBallSuffix:String = ''; + + static var blueballed:Bool = false; /** * The boyfriend character. */ - var boyfriend:BaseCharacter; + var boyfriend:Null<BaseCharacter> = null; /** * The invisible object in the scene which the camera focuses on. @@ -82,7 +85,8 @@ class GameOverSubState extends MusicBeatSubState var transparent:Bool; - final CAMERA_ZOOM_DURATION:Float = 0.5; + static final CAMERA_ZOOM_DURATION:Float = 0.5; + var targetCameraZoom:Float = 1.0; public function new(params:GameOverParams) @@ -91,6 +95,8 @@ class GameOverSubState extends MusicBeatSubState this.isChartingMode = params?.isChartingMode ?? false; transparent = params.transparent; + + cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); } /** @@ -101,14 +107,15 @@ class GameOverSubState extends MusicBeatSubState animationSuffix = ''; musicSuffix = ''; blueBallSuffix = ''; + blueballed = false; } - override public function create():Void + public override function create():Void { if (instance != null) { // TODO: Do something in this case? IDK. - trace('WARNING: GameOverSubState instance already exists. This should not happen.'); + FlxG.log.warn('WARNING: GameOverSubState instance already exists. This should not happen.'); } instance = this; @@ -121,7 +128,7 @@ class GameOverSubState extends MusicBeatSubState var playState = PlayState.instance; // Add a black background to the screen. - var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); + var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); // We make this transparent so that we can see the stage underneath during debugging, // but it's normally opaque. bg.alpha = transparent ? 0.25 : 1.0; @@ -138,21 +145,10 @@ class GameOverSubState extends MusicBeatSubState boyfriend.isDead = true; add(boyfriend); boyfriend.resetCharacter(); - - // Assign a camera follow point to the boyfriend's position. - cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); - cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; - cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y; - var offsets:Array<Float> = boyfriend.getDeathCameraOffsets(); - cameraFollowPoint.x += offsets[0]; - cameraFollowPoint.y += offsets[1]; - add(cameraFollowPoint); - - FlxG.camera.target = null; - FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01); - targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom(); } + setCameraTarget(); + // // Set up the audio // @@ -161,6 +157,27 @@ class GameOverSubState extends MusicBeatSubState Conductor.instance.update(0); } + @:nullSafety(Off) + function setCameraTarget():Void + { + // Assign a camera follow point to the boyfriend's position. + cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); + cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; + cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y; + var offsets:Array<Float> = boyfriend.getDeathCameraOffsets(); + cameraFollowPoint.x += offsets[0]; + cameraFollowPoint.y += offsets[1]; + add(cameraFollowPoint); + + FlxG.camera.target = null; + FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE / 2); + targetCameraZoom = (PlayState?.instance?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom(); + } + + /** + * Forcibly reset the camera zoom level to that of the current stage. + * This prevents camera zoom events from adversely affecting the game over state. + */ public function resetCameraZoom():Void { // Apply camera zoom level from stage data. @@ -175,7 +192,7 @@ class GameOverSubState extends MusicBeatSubState { hasStartedAnimation = true; - if (PlayState.instance.isMinimalMode) + if (boyfriend == null || PlayState.instance.isMinimalMode) { // Play the "blue balled" sound. May play a variant if one has been assigned. playBlueBalledSFX(); @@ -205,10 +222,10 @@ class GameOverSubState extends MusicBeatSubState // MOBILE ONLY: Restart the level when tapping Boyfriend. if (FlxG.onMobile) { - var touch = FlxG.touches.getFirst(); + var touch:FlxTouch = FlxG.touches.getFirst(); if (touch != null) { - if (touch.overlaps(boyfriend)) + if (boyfriend == null || touch.overlaps(boyfriend)) { confirmDeath(); } @@ -228,7 +245,7 @@ class GameOverSubState extends MusicBeatSubState blueballed = false; PlayState.instance.deathCounter = 0; // PlayState.seenCutscene = false; // old thing... - gameOverMusic.stop(); + if (gameOverMusic != null) gameOverMusic.stop(); if (isChartingMode) { @@ -238,11 +255,11 @@ class GameOverSubState extends MusicBeatSubState } else if (PlayStatePlaylist.isStoryMode) { - FlxG.switchState(() -> new StoryMenuState()); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker))); } else { - FlxG.switchState(() -> new FreeplayState()); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(sticker))); } } @@ -252,7 +269,7 @@ class GameOverSubState extends MusicBeatSubState // This enables the stepHit and beatHit events. Conductor.instance.update(gameOverMusic.time); } - else + else if (boyfriend != null) { if (PlayState.instance.isMinimalMode) { @@ -299,7 +316,7 @@ class GameOverSubState extends MusicBeatSubState isEnding = true; startDeathMusic(1.0, true); // isEnding changes this function's behavior. - if (PlayState.instance.isMinimalMode) {} + if (PlayState.instance.isMinimalMode || boyfriend == null) {} else { boyfriend.playAnimation('deathConfirm' + animationSuffix, true); @@ -313,7 +330,7 @@ class GameOverSubState extends MusicBeatSubState FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true); PlayState.instance.needsReset = true; - if (PlayState.instance.isMinimalMode) {} + if (PlayState.instance.isMinimalMode || boyfriend == null) {} else { // Readd Boyfriend to the stage. @@ -332,7 +349,7 @@ class GameOverSubState extends MusicBeatSubState } } - public override function dispatchEvent(event:ScriptEvent) + public override function dispatchEvent(event:ScriptEvent):Void { super.dispatchEvent(event); @@ -345,11 +362,11 @@ class GameOverSubState extends MusicBeatSubState */ function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null<String> { - var basePath = 'gameplay/gameover/gameOver'; - if (starting) basePath += 'Start'; - else if (ending) basePath += 'End'; + var basePath:String = 'gameplay/gameover/gameOver'; + if (ending) basePath += 'End'; + else if (starting) basePath += 'Start'; - var musicPath = Paths.music(basePath + suffix); + var musicPath:String = Paths.music(basePath + suffix); while (!Assets.exists(musicPath) && suffix.length > 0) { suffix = suffix.split('-').slice(0, -1).join('-'); @@ -362,23 +379,26 @@ class GameOverSubState extends MusicBeatSubState /** * Starts the death music at the appropriate volume. - * @param startingVolume + * @param startingVolume The initial volume for the music. + * @param force Whether or not to force the music to restart. */ public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void { - var musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding); - var onComplete = null; + var musicPath:Null<String> = resolveMusicPath(musicSuffix, isStarting, isEnding); + var onComplete:() -> Void = () -> {}; + if (isStarting) { if (musicPath == null) { + // Looked for starting music and didn't find it. Use middle music instead. isStarting = false; musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding); } else { onComplete = function() { - isStarting = false; + isStarting = true; // We need to force to ensure that the non-starting music plays. startDeathMusic(1.0, true); }; @@ -387,13 +407,16 @@ class GameOverSubState extends MusicBeatSubState if (musicPath == null) { - trace('Could not find game over music!'); + FlxG.log.warn('[GAMEOVER] Could not find game over music at path ($musicPath)!'); return; } else if (gameOverMusic == null || !gameOverMusic.playing || force) { if (gameOverMusic != null) gameOverMusic.stop(); + gameOverMusic = FunkinSound.load(musicPath); + if (gameOverMusic == null) return; + gameOverMusic.volume = startingVolume; gameOverMusic.looped = !(isEnding || isStarting); gameOverMusic.onComplete = onComplete; @@ -406,13 +429,11 @@ class GameOverSubState extends MusicBeatSubState } } - static var blueballed:Bool = false; - /** * Play the sound effect that occurs when * boyfriend's testicles get utterly annihilated. */ - public static function playBlueBalledSFX() + public static function playBlueBalledSFX():Void { blueballed = true; if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix))) @@ -431,7 +452,7 @@ class GameOverSubState extends MusicBeatSubState * Week 7-specific hardcoded behavior, to play a custom death quote. * TODO: Make this a module somehow. */ - function playJeffQuote() + function playJeffQuote():Void { var randomCensor:Array<Int> = []; @@ -446,20 +467,27 @@ class GameOverSubState extends MusicBeatSubState }); } - public override function destroy() + public override function destroy():Void { super.destroy(); - if (gameOverMusic != null) gameOverMusic.stop(); - gameOverMusic = null; + if (gameOverMusic != null) + { + gameOverMusic.stop(); + gameOverMusic = null; + } + blueballed = false; instance = null; } public override function toString():String { - return "GameOverSubState"; + return 'GameOverSubState'; } } +/** + * Parameters used to instantiate a GameOverSubState. + */ typedef GameOverParams = { var isChartingMode:Bool; diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index f16aa00d8..fc1d01377 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -12,6 +12,7 @@ import flixel.tweens.FlxTween; import flixel.util.FlxColor; import funkin.audio.FunkinSound; import funkin.data.song.SongRegistry; +import funkin.ui.freeplay.FreeplayState; import funkin.graphics.FunkinSprite; import funkin.play.cutscene.VideoCutscene; import funkin.play.PlayState; @@ -72,8 +73,8 @@ class PauseSubState extends MusicBeatSubState */ static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array<PauseMenuEntry> = [ {text: 'Resume', callback: resume}, - {text: 'Restart Cutscene', callback: restartVideoCutscene}, {text: 'Skip Cutscene', callback: skipVideoCutscene}, + {text: 'Restart Cutscene', callback: restartVideoCutscene}, {text: 'Exit to Menu', callback: quitToMenu}, ]; @@ -230,7 +231,7 @@ class PauseSubState extends MusicBeatSubState */ function startPauseMusic():Void { - var pauseMusicPath:String = Paths.music('breakfast$musicSuffix'); + var pauseMusicPath:String = Paths.music('breakfast$musicSuffix/breakfast$musicSuffix'); pauseMusic = FunkinSound.load(pauseMusicPath, true, true); if (pauseMusic == null) @@ -440,7 +441,7 @@ class PauseSubState extends MusicBeatSubState var entries:Array<PauseMenuEntry> = []; if (PlayState.instance.currentChart != null) { - var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation); + var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation, true); trace('DIFFICULTIES: ${difficultiesInVariation}'); for (difficulty in difficultiesInVariation) { @@ -567,6 +568,8 @@ class PauseSubState extends MusicBeatSubState PlayStatePlaylist.campaignDifficulty = difficulty; PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty; + FreeplayState.rememberedDifficulty = difficulty; + PlayState.instance.needsReset = true; state.close(); @@ -658,7 +661,7 @@ class PauseSubState extends MusicBeatSubState } else { - state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker))); + state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker))); } } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index fd6463bb1..d3246228b 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. @@ -728,6 +736,10 @@ class PlayState extends MusicBeatSubState #end initialized = true; + + // This step ensures z-indexes are applied properly, + // and it's important to call it last so all elements get affected. + refresh(); } public override function draw():Void @@ -822,9 +834,12 @@ class PlayState extends MusicBeatSubState inputSpitter = []; // Reset music properly. - FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; - FlxG.sound.music.pitch = playbackRate; - FlxG.sound.music.pause(); + if (FlxG.sound.music != null) + { + FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; + FlxG.sound.music.pitch = playbackRate; + FlxG.sound.music.pause(); + } if (!overrideMusic) { @@ -840,7 +855,7 @@ class PlayState extends MusicBeatSubState vocals.pause(); vocals.time = 0; - FlxG.sound.music.volume = 1; + if (FlxG.sound.music != null) FlxG.sound.music.volume = 1; vocals.volume = 1; vocals.playerVolume = 1; vocals.opponentVolume = 1; @@ -857,8 +872,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; @@ -953,11 +968,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); } @@ -967,6 +983,7 @@ class PlayState extends MusicBeatSubState FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation()); } FlxG.watch.addQuick('health', health); + FlxG.watch.addQuick('cameraBopIntensity', cameraBopIntensity); // TODO: Add a song event for Handle GF dance speed. @@ -1363,15 +1380,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}}'); @@ -1534,10 +1551,11 @@ class PlayState extends MusicBeatSubState function loadStage(id:String):Void { currentStage = StageRegistry.instance.fetchEntry(id); - currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory. if (currentStage != null) { + currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory. + // Actually create and position the sprites. var event:ScriptEvent = new ScriptEvent(CREATE, false); ScriptEventDispatcher.callEvent(currentStage, event); @@ -1562,12 +1580,11 @@ class PlayState extends MusicBeatSubState { if (PlayState.instance.isMinimalMode) return; // Apply camera zoom level from stage data. - defaultCameraZoom = currentStage.camZoom; - currentCameraZoom = defaultCameraZoom; + currentCameraZoom = stageZoom; FlxG.camera.zoom = currentCameraZoom; - // Reset additive zoom. - additiveCameraZoom = 0; + // Reset bop multiplier. + cameraBopMultiplier = 1.0; } /** @@ -1720,8 +1737,6 @@ class PlayState extends MusicBeatSubState playerStrumline.fadeInArrows(); opponentStrumline.fadeInArrows(); } - - this.refresh(); } /** @@ -2222,8 +2237,8 @@ class PlayState extends MusicBeatSubState holdNote.handledMiss = true; // Mute vocals and play miss animation, but don't penalize. - vocals.playerVolume = 0; - if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true); + // vocals.playerVolume = 0; + // if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true); } } } @@ -2377,13 +2392,6 @@ class PlayState extends MusicBeatSubState // Display the combo meter and add the calculation to the score. popUpScore(note, event.score, event.judgement, event.healthChange); - - if (note.isHoldNote && note.holdNoteSprite != null) - { - playerStrumline.playNoteHoldCover(note.holdNoteSprite); - } - - vocals.playerVolume = 1; } /** @@ -2441,7 +2449,8 @@ class PlayState extends MusicBeatSubState if (Highscore.tallies.combo != 0) { // Break the combo. - Highscore.tallies.combo = comboPopUps.displayCombo(0); + if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0); + Highscore.tallies.combo = 0; } if (playSound) @@ -2568,32 +2577,38 @@ class PlayState extends MusicBeatSubState */ function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void { - vocals.playerVolume = 1; - if (daRating == 'miss') { // If daRating is 'miss', that means we made a mistake and should not continue. - trace('[WARNING] popUpScore judged a note as a miss!'); + FlxG.log.warn('popUpScore judged a note as a miss!'); // TODO: Remove this. - comboPopUps.displayRating('miss'); + // comboPopUps.displayRating('miss'); return; } + vocals.playerVolume = 1; + var isComboBreak = false; switch (daRating) { case 'sick': Highscore.tallies.sick += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK; case 'good': Highscore.tallies.good += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK; case 'bad': Highscore.tallies.bad += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; case 'shit': Highscore.tallies.shit += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; + default: + FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!'); } health += healthChange; @@ -2601,18 +2616,18 @@ class PlayState extends MusicBeatSubState if (isComboBreak) { // Break the combo, but don't increment tallies.misses. - Highscore.tallies.combo = comboPopUps.displayCombo(0); + if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0); + Highscore.tallies.combo = 0; } else { Highscore.tallies.combo++; - Highscore.tallies.totalNotesHit++; if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; } playerStrumline.hitNote(daNote, !isComboBreak); - if (daRating == "sick") + if (daRating == 'sick') { playerStrumline.playNoteSplash(daNote.noteData.getDirection()); } @@ -2658,6 +2673,13 @@ class PlayState extends MusicBeatSubState } comboPopUps.displayRating(daRating); if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo); + + if (daNote.isHoldNote && daNote.holdNoteSprite != null) + { + playerStrumline.playNoteHoldCover(daNote.holdNoteSprite); + } + + vocals.playerVolume = 1; } /** @@ -2724,7 +2746,7 @@ class PlayState extends MusicBeatSubState */ public function endSong(rightGoddamnNow:Bool = false):Void { - FlxG.sound.music.volume = 0; + if (FlxG.sound.music != null) FlxG.sound.music.volume = 0; vocals.volume = 0; mayPauseGame = false; @@ -2742,6 +2764,8 @@ class PlayState extends MusicBeatSubState deathCounter = 0; + var isNewHighscore = false; + if (currentSong != null && currentSong.validScore) { // crackhead double thingie, sets whether was new highscore, AND saves the song! @@ -2766,17 +2790,20 @@ class PlayState extends MusicBeatSubState // adds current song data into the tallies for the level (story levels) Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel); - if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) + if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) { Save.instance.setSongScore(currentSong.id, currentDifficulty, data); #if newgrounds NGio.postScore(score, currentSong.id); #end + isNewHighscore = true; } } if (PlayStatePlaylist.isStoryMode) { + isNewHighscore = false; + PlayStatePlaylist.campaignScore += songScore; // Pop the next song ID from the list. @@ -2785,18 +2812,6 @@ class PlayState extends MusicBeatSubState if (targetSongId == null) { - FunkinSound.playMusic('freakyMenu', - { - overrideExisting: true, - restartTrack: false - }); - - // transIn = FlxTransitionableState.defaultTransIn; - // transOut = FlxTransitionableState.defaultTransOut; - - // TODO: Rework week unlock logic. - // StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true; - if (currentSong.validScore) { NGio.unlockMedal(60961); @@ -2826,6 +2841,7 @@ class PlayState extends MusicBeatSubState #if newgrounds NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}'); #end + isNewHighscore = true; } } @@ -2837,11 +2853,11 @@ class PlayState extends MusicBeatSubState { if (rightGoddamnNow) { - moveToResultsScreen(); + moveToResultsScreen(isNewHighscore); } else { - zoomIntoResultsScreen(); + zoomIntoResultsScreen(isNewHighscore); } } } @@ -2854,7 +2870,7 @@ class PlayState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - FlxG.sound.music.stop(); + if (FlxG.sound.music != null) FlxG.sound.music.stop(); vocals.stop(); // TODO: Softcode this cutscene. @@ -2902,11 +2918,11 @@ class PlayState extends MusicBeatSubState { if (rightGoddamnNow) { - moveToResultsScreen(); + moveToResultsScreen(isNewHighscore); } else { - zoomIntoResultsScreen(); + zoomIntoResultsScreen(isNewHighscore); } } } @@ -2980,7 +2996,7 @@ class PlayState extends MusicBeatSubState /** * Play the camera zoom animation and then move to the results screen once it's done. */ - function zoomIntoResultsScreen():Void + function zoomIntoResultsScreen(isNewHighscore:Bool):Void { trace('WENT TO RESULTS SCREEN!'); @@ -3037,7 +3053,7 @@ class PlayState extends MusicBeatSubState { ease: FlxEase.expoIn, onComplete: function(_) { - moveToResultsScreen(); + moveToResultsScreen(isNewHighscore); } }); }); @@ -3046,7 +3062,7 @@ class PlayState extends MusicBeatSubState /** * Move to the results screen right goddamn now. */ - function moveToResultsScreen():Void + function moveToResultsScreen(isNewHighscore:Bool):Void { persistentUpdate = false; vocals.stop(); @@ -3058,7 +3074,24 @@ class PlayState extends MusicBeatSubState { storyMode: PlayStatePlaylist.isStoryMode, title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), - tallies: talliesToUse, + scoreData: + { + score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore, + tallies: + { + sick: talliesToUse.sick, + good: talliesToUse.good, + bad: talliesToUse.bad, + shit: talliesToUse.shit, + missed: talliesToUse.missed, + combo: talliesToUse.combo, + maxCombo: talliesToUse.maxCombo, + totalNotesHit: talliesToUse.totalNotesHit, + totalNotes: talliesToUse.totalNotes, + }, + accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, + }, + isNewHighscore: isNewHighscore }); res.camera = camHUD; openSubState(res); @@ -3096,6 +3129,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:Null<Float->Float>):Void + { + cameraFollowPoint.setPosition(x, y); + tweenCameraToFollowPoint(duration, ease); + } + /** * Disables camera following and tweens the camera to the follow point manually. */ @@ -3137,38 +3179,24 @@ class PlayState extends MusicBeatSubState /** * Tweens the camera zoom to the desired amount. */ - public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?directMode:Bool, ?ease:Null<Float->Float>):Void + public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?direct:Bool, ?ease:Null<Float->Float>):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}); } } @@ -3205,7 +3233,10 @@ class PlayState extends MusicBeatSubState // Don't go back in time to before the song started. targetTimeMs = Math.max(0, targetTimeMs); - FlxG.sound.music.time = targetTimeMs; + if (FlxG.sound.music != null) + { + FlxG.sound.music.time = targetTimeMs; + } handleSkippedNotes(); SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition); diff --git a/source/funkin/play/PlayStatePlaylist.hx b/source/funkin/play/PlayStatePlaylist.hx index 3b0fb01f6..e47a6288a 100644 --- a/source/funkin/play/PlayStatePlaylist.hx +++ b/source/funkin/play/PlayStatePlaylist.hx @@ -5,12 +5,13 @@ package funkin.play; * * TODO: Add getters/setters for all these properties to validate them. */ +@:nullSafety class PlayStatePlaylist { /** * Whether the game is currently in Story Mode. If false, we are in Free Play Mode. */ - public static var isStoryMode(default, default):Bool = false; + public static var isStoryMode:Bool = false; /** * The loist of upcoming songs to be played. @@ -31,8 +32,9 @@ class PlayStatePlaylist /** * The internal ID of the current playlist, for example `week4` or `weekend-1`. + * @default `null`, used when no playlist is loaded */ - public static var campaignId:String = 'unknown'; + public static var campaignId:Null<String> = null; public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY; @@ -45,7 +47,7 @@ class PlayStatePlaylist playlistSongIds = []; campaignScore = 0; campaignTitle = 'UNKNOWN'; - campaignId = 'unknown'; + campaignId = null; campaignDifficulty = Constants.DEFAULT_DIFFICULTY; } } diff --git a/source/funkin/play/ResultScore.hx b/source/funkin/play/ResultScore.hx new file mode 100644 index 000000000..d5d5a6567 --- /dev/null +++ b/source/funkin/play/ResultScore.hx @@ -0,0 +1,140 @@ +package funkin.play; + +import flixel.FlxSprite; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; + +class ResultScore extends FlxTypedSpriteGroup<ScoreNum> +{ + public var scoreShit(default, set):Int = 0; + + function set_scoreShit(val):Int + { + if (group == null || group.members == null) return val; + var loopNum:Int = group.members.length - 1; + var dumbNumb = Std.parseInt(Std.string(val)); + var prevNum:ScoreNum; + + while (dumbNumb > 0) + { + group.members[loopNum].digit = dumbNumb % 10; + + // var funnyNum = group.members[loopNum]; + // prevNum = group.members[loopNum + 1]; + + // if (prevNum != null) + // { + // funnyNum.x = prevNum.x - (funnyNum.width * 0.7); + // } + + // funnyNum.y = (funnyNum.baseY - (funnyNum.height / 2)) + 73; + // funnyNum.x = (funnyNum.baseX - (funnyNum.width / 2)) + 450; // this plus value is hand picked lol! + + dumbNumb = Math.floor(dumbNumb / 10); + loopNum--; + } + + while (loopNum > 0) + { + group.members[loopNum].digit = 10; + loopNum--; + } + + return val; + } + + public function animateNumbers():Void + { + for (i in group.members) + { + i.playAnim(); + } + } + + public function new(x:Float, y:Float, digitCount:Int, scoreShit:Int = 100) + { + super(x, y); + + for (i in 0...digitCount) + { + add(new ScoreNum(x + (65 * i), y)); + } + + this.scoreShit = scoreShit; + } + + public function updateScore(scoreNew:Int) + { + scoreShit = scoreNew; + } +} + +class ScoreNum extends FlxSprite +{ + public var digit(default, set):Int = 10; + + function set_digit(val):Int + { + if (val >= 0 && animation.curAnim != null && animation.curAnim.name != numToString[val]) + { + animation.play(numToString[val], true, false, 0); + updateHitbox(); + + switch (val) + { + case 1: + // offset.x -= 15; + case 5: + // set offsets + // offset.x += 0; + // offset.y += 10; + + case 7: + // offset.y += 6; + case 4: + // offset.y += 5; + case 9: + // offset.y += 5; + default: + centerOffsets(false); + } + } + + return digit = val; + } + + public function playAnim():Void + { + animation.play(numToString[digit], true, false, 0); + } + + public var baseY:Float = 0; + public var baseX:Float = 0; + + var numToString:Array<String> = [ + "ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "DISABLED" + ]; + + public function new(x:Float, y:Float) + { + super(x, y); + + baseY = y; + baseX = x; + + frames = Paths.getSparrowAtlas('resultScreen/score-digital-numbers'); + + for (i in 0...10) + { + var stringNum:String = numToString[i]; + animation.addByPrefix(stringNum, '$stringNum DIGITAL', 24, false); + } + + animation.addByPrefix('DISABLED', 'DISABLED', 24, false); + + this.digit = 10; + + animation.play(numToString[digit], true); + + updateHitbox(); + } +} diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 821f4ba3c..591020955 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -1,5 +1,6 @@ package funkin.play; +import funkin.util.MathUtil; import funkin.ui.story.StoryMenuState; import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.FlxSprite; @@ -10,12 +11,15 @@ import flixel.math.FlxPoint; import funkin.ui.MusicBeatSubState; import flixel.math.FlxRect; import flixel.text.FlxBitmapText; +import funkin.ui.freeplay.FreeplayScore; import flixel.tweens.FlxEase; import funkin.ui.freeplay.FreeplayState; import flixel.tweens.FlxTween; import funkin.audio.FunkinSound; import flixel.util.FlxGradient; import flixel.util.FlxTimer; +import funkin.save.Save; +import funkin.save.Save.SaveScoreData; import funkin.graphics.shaders.LeftMaskShader; import funkin.play.components.TallyCounter; @@ -42,12 +46,15 @@ class ResultState extends MusicBeatSubState override function create():Void { - if (params.tallies.sick == params.tallies.totalNotesHit - && params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT; - else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50) - resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending! - else - resultsVariation = NORMAL; + /* + if (params.scoreData.sick == params.scoreData.totalNotesHit + && params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT; + else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50) + resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending! + else + resultsVariation = NORMAL; + */ + resultsVariation = NORMAL; FunkinSound.playMusic('results$resultsVariation', { @@ -73,34 +80,34 @@ class ResultState extends MusicBeatSubState bgFlash.visible = false; add(bgFlash); - var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared")); - bfGfExcellent.visible = false; - add(bfGfExcellent); + // var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared")); + // bfGfExcellent.visible = false; + // add(bfGfExcellent); + // + // var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared")); + // bfPerfect.visible = false; + // add(bfPerfect); + // + // var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared")); + // bfSHIT.visible = false; + // add(bfSHIT); + // + // bfGfExcellent.anim.onComplete = () -> { + // bfGfExcellent.anim.curFrame = 28; + // bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! + // }; + // + // bfPerfect.anim.onComplete = () -> { + // bfPerfect.anim.curFrame = 136; + // bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! + // }; + // + // bfSHIT.anim.onComplete = () -> { + // bfSHIT.anim.curFrame = 150; + // bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce! + // }; - var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared")); - bfPerfect.visible = false; - add(bfPerfect); - - var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared")); - bfSHIT.visible = false; - add(bfSHIT); - - bfGfExcellent.anim.onComplete = () -> { - bfGfExcellent.anim.curFrame = 28; - bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! - }; - - bfPerfect.anim.onComplete = () -> { - bfPerfect.anim.curFrame = 136; - bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! - }; - - bfSHIT.anim.onComplete = () -> { - bfSHIT.anim.curFrame = 150; - bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce! - }; - - var gf:FlxSprite = FunkinSprite.createSparrow(500, 300, 'resultScreen/resultGirlfriendGOOD'); + var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD'); gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); gf.visible = false; gf.animation.finishCallback = _ -> { @@ -130,12 +137,16 @@ class ResultState extends MusicBeatSubState var diffSpr:String = switch (PlayState.instance.currentDifficulty) { - case 'EASY': + case 'easy': 'difEasy'; - case 'NORMAL': + case 'normal': 'difNormal'; - case 'HARD': + case 'hard': 'difHard'; + case 'erect': + 'difErect'; + case 'nightmare': + 'difNightmare'; case _: 'difNormal'; } @@ -178,7 +189,7 @@ class ResultState extends MusicBeatSubState scorePopin.visible = false; add(scorePopin); - var highscoreNew:FlxSprite = new FlxSprite(280, 580); + var highscoreNew:FlxSprite = new FlxSprite(310, 570); highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew"); highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24); highscoreNew.visible = false; @@ -195,29 +206,33 @@ class ResultState extends MusicBeatSubState * NOTE: We display how many notes were HIT, not how many notes there were in total. * */ - var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit); + var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.scoreData.tallies.totalNotesHit); ratingGrp.add(totalHit); - var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.tallies.maxCombo); + var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.scoreData.tallies.maxCombo); ratingGrp.add(maxCombo); hStuf += 2; var extraYOffset:Float = 5; - var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.tallies.sick, 0xFF89E59E); + var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E); ratingGrp.add(tallySick); - var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5); + var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.scoreData.tallies.good, 0xFF89C9E5); ratingGrp.add(tallyGood); - var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xFFE6CF8A); + var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.scoreData.tallies.bad, 0xFFE6CF8A); ratingGrp.add(tallyBad); - var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A); + var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.scoreData.tallies.shit, 0xFFE68C8A); ratingGrp.add(tallyShit); - var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.tallies.missed, 0xFFC68AE6); + var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6); ratingGrp.add(tallyMissed); + var score:ResultScore = new ResultScore(35, 305, 10, params.scoreData.score); + score.visible = false; + add(score); + for (ind => rating in ratingGrp.members) { rating.visible = false; @@ -233,18 +248,29 @@ class ResultState extends MusicBeatSubState ratingsPopin.animation.finishCallback = anim -> { scorePopin.animation.play("score"); + scorePopin.animation.finishCallback = anim -> { + score.visible = true; + score.animateNumbers(); + }; scorePopin.visible = true; - highscoreNew.visible = true; - highscoreNew.animation.play("new"); - FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); + if (params.isNewHighscore) + { + highscoreNew.visible = true; + highscoreNew.animation.play("new"); + FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); + } + else + { + highscoreNew.visible = false; + } }; switch (resultsVariation) { - case SHIT: - bfSHIT.visible = true; - bfSHIT.playAnimation(""); + // case SHIT: + // bfSHIT.visible = true; + // bfSHIT.playAnimation(""); case NORMAL: boyfriend.animation.play('fall'); @@ -266,9 +292,9 @@ class ResultState extends MusicBeatSubState gf.animation.play('clap', true); gf.visible = true; }); - case PERFECT: - bfPerfect.visible = true; - bfPerfect.playAnimation(""); + // case PERFECT: + // bfPerfect.visible = true; + // bfPerfect.playAnimation(""); // bfGfExcellent.visible = true; // bfGfExcellent.playAnimation(""); @@ -276,8 +302,6 @@ class ResultState extends MusicBeatSubState } }); - if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!"); - super.create(); } @@ -365,7 +389,7 @@ class ResultState extends MusicBeatSubState } else { - openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new FreeplayState(null, sticker))); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker))); } } @@ -393,8 +417,13 @@ typedef ResultsStateParams = */ var title:String; + /** + * Whether the displayed score is a new highscore + */ + var isNewHighscore:Bool; + /** * The score, accuracy, and judgements. */ - var tallies:Highscore.Tallies; + var scoreData:SaveScoreData; }; 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 39fc192a0..724bf0cb9 100644 --- a/source/funkin/play/components/PopUpStuff.hx +++ b/source/funkin/play/components/PopUpStuff.hx @@ -85,7 +85,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite> comboSpr.velocity.y -= 150; comboSpr.velocity.x += FlxG.random.int(1, 10); - add(comboSpr); + // add(comboSpr); if (PlayState.instance.currentStageId.startsWith('school')) { diff --git a/source/funkin/play/components/TallyCounter.hx b/source/funkin/play/components/TallyCounter.hx index 77e6ef4ec..35a8f3f51 100644 --- a/source/funkin/play/components/TallyCounter.hx +++ b/source/funkin/play/components/TallyCounter.hx @@ -6,6 +6,8 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.math.FlxMath; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; +import flixel.text.FlxText.FlxTextAlign; +import funkin.util.MathUtil; /** * Numerical counters used next to each judgement in the Results screen. @@ -13,18 +15,23 @@ import flixel.tweens.FlxTween; class TallyCounter extends FlxTypedSpriteGroup<FlxSprite> { public var curNumber:Float = 0; - public var neededNumber:Int = 0; + public var flavour:Int = 0xFFFFFFFF; - public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF) + public var align:FlxTextAlign = FlxTextAlign.LEFT; + + public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF, align:FlxTextAlign = FlxTextAlign.LEFT) { super(x, y); + this.align = align; + this.flavour = flavour; this.neededNumber = neededNumber; - drawNumbers(); + + if (curNumber == neededNumber) drawNumbers(); } var tmr:Float = 0; @@ -41,6 +48,8 @@ class TallyCounter extends FlxTypedSpriteGroup<FlxSprite> var seperatedScore:Array<Int> = []; var tempCombo:Int = Math.round(curNumber); + var fullNumberDigits:Int = Std.int(Math.max(1, Math.ceil(MathUtil.logBase(10, neededNumber)))); + while (tempCombo != 0) { seperatedScore.push(tempCombo % 10); @@ -55,7 +64,13 @@ class TallyCounter extends FlxTypedSpriteGroup<FlxSprite> { if (ind >= members.length) { - var numb:TallyNumber = new TallyNumber(ind * 43, 0, num); + var xPos = ind * (43 * this.scale.x); + if (this.align == FlxTextAlign.RIGHT) + { + xPos -= (fullNumberDigits * (43 * this.scale.x)); + } + var numb:TallyNumber = new TallyNumber(xPos, 0, num); + numb.scale.set(this.scale.x, this.scale.y); add(numb); numb.color = flavour; } 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 c520c3e25..2c59eaba0 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..1bcac9ad3 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<Bool> = data.getBool('useTween'); - if (useTween == null) useTween = false; var duration:Null<Float> = data.getFloat('duration'); if (duration == null) duration = 4.0; var ease:Null<String> = 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:Null<Float->Float> = 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.cancelCameraFollowTween(); + 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:Null<Float->Float> = 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<Float> = 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:Null<Float->Float> = 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/notes/NoteHoldCover.hx b/source/funkin/play/notes/NoteHoldCover.hx index 52ae97d4f..7bed8a08c 100644 --- a/source/funkin/play/notes/NoteHoldCover.hx +++ b/source/funkin/play/notes/NoteHoldCover.hx @@ -77,13 +77,6 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> public override function update(elapsed):Void { super.update(elapsed); - if ((!holdNote.alive || holdNote.missedNote) && !glow.animation.curAnim.name.startsWith('holdCoverEnd')) - { - // If alive is false, the hold note was held to completion. - // If missedNote is true, the hold note was "dropped". - - playEnd(); - } } public function playStart():Void diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 9a6699c43..2b4e09370 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -295,6 +295,11 @@ class Strumline extends FlxSpriteGroup { if (noteData.length == 0) return; + // Ensure note data gets reset if the song happens to loop. + // NOTE: I had to remove this line because it was causing notes visible during the countdown to be placed multiple times. + // I don't remember what bug I was trying to fix by adding this. + // if (conductorInUse.currentStep == 0) nextNoteIndex = 0; + var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0; var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS; var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS; @@ -365,8 +370,6 @@ class Strumline extends FlxSpriteGroup // Hold note is offscreen, kill it. holdNote.visible = false; holdNote.kill(); // Do not destroy! Recycling is faster. - - // The cover will see this and clean itself up. } else if (holdNote.hitNote && holdNote.sustainLength <= 0) { @@ -380,10 +383,16 @@ class Strumline extends FlxSpriteGroup playStatic(holdNote.noteDirection); } - if (holdNote.cover != null) + if (holdNote.cover != null && isPlayer) { holdNote.cover.playEnd(); } + else if (holdNote.cover != null) + { + // *lightning* *zap* *crackle* + holdNote.cover.visible = false; + holdNote.cover.kill(); + } holdNote.visible = false; holdNote.kill(); @@ -405,6 +414,13 @@ class Strumline extends FlxSpriteGroup { holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2; } + + // Clean up the cover. + if (holdNote.cover != null) + { + holdNote.cover.visible = false; + holdNote.cover.kill(); + } } else if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote) { @@ -822,7 +838,7 @@ class Strumline extends FlxSpriteGroup { // The note sprite pool is full and all note splashes are active. // We have to create a new note. - result = new SustainTrail(0, 100, noteStyle); + result = new SustainTrail(0, 0, noteStyle); this.holdNotes.add(result); } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 0248e09ee..d219dc2f6 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -404,11 +404,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta * * @param variationId Optionally filter by a single variation. * @param variationIds Optionally filter by multiple variations. + * @param showLocked Include charts which are not unlocked * @param showHidden Include charts which are not accessible to the player. * * @return The list of difficulties. */ - public function listDifficulties(?variationId:String, ?variationIds:Array<String>, showHidden:Bool = false):Array<String> + public function listDifficulties(?variationId:String, ?variationIds:Array<String>, showLocked:Bool = false, showHidden:Bool = false):Array<String> { if (variationIds == null) variationIds = []; if (variationId != null) variationIds.push(variationId); diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index db42b0dd3..eb9eb1810 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -223,7 +223,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements if (propSprite.frames == null || propSprite.frames.numFrames == 0) { - trace(' ERROR: Could not build texture for prop.'); + @:privateAccess + trace(' ERROR: Could not build texture for prop. Check the asset path (${Paths.currentLevel ?? 'default'}, ${dataProp.assetPath}).'); continue; } diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 73ba8efa0..bfbda2a02 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -9,12 +9,13 @@ import funkin.save.migrator.SaveDataMigrator; import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle; import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; import thx.semver.Version; +import funkin.util.SerializerUtil; @:nullSafety class Save { // Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null. - public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2"; + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.3"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. @@ -391,6 +392,22 @@ class Save */ public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null<SaveScoreData> { + if (data.scores?.levels == null) + { + if (data.scores == null) + { + data.scores = + { + songs: [], + levels: [] + }; + } + else + { + data.scores.levels = []; + } + } + var level = data.scores.levels.get(levelId); if (level == null) { @@ -641,6 +658,9 @@ class Save { trace("[SAVE] Loading save from slot " + slot + "..."); + // Prevent crashes if the save data is corrupted. + SerializerUtil.initSerializer(); + FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH); if (FlxG.save.isEmpty()) @@ -650,9 +670,9 @@ class Save if (legacySaveData != null) { trace('[SAVE] Found legacy save data, converting...'); - var gameSave = SaveDataMigrator.migrate(legacySaveData); + var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData); @:privateAccess - FlxG.save.mergeData(gameSave.data); + FlxG.save.mergeData(gameSave.data, true); } else { @@ -664,7 +684,7 @@ class Save trace('[SAVE] Loaded save data.'); @:privateAccess var gameSave = SaveDataMigrator.migrate(FlxG.save.data); - FlxG.save.mergeData(gameSave.data); + FlxG.save.mergeData(gameSave.data, true); } } @@ -673,7 +693,7 @@ class Save trace("[SAVE] Checking for legacy save data..."); var legacySave:FlxSave = new FlxSave(); legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY); - if (legacySave?.data == null) + if (legacySave.isEmpty()) { trace("[SAVE] No legacy save data found."); return null; diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index 00637d52a..3ed59e726 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -3,6 +3,7 @@ package funkin.save.migrator; import funkin.save.Save; import funkin.save.migrator.RawSaveData_v1_0_0; import thx.semver.Version; +import funkin.util.StructureUtil; import funkin.util.VersionUtil; @:nullSafety @@ -26,7 +27,7 @@ class SaveDataMigrator if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE)) { // Simply import the structured data. - var save:Save = new Save(inputData); + var save:Save = new Save(StructureUtil.deepMerge(Save.getDefault(), inputData)); return save; } else diff --git a/source/funkin/ui/credits/CreditsData.hx b/source/funkin/ui/credits/CreditsData.hx new file mode 100644 index 000000000..bf7f13ad5 --- /dev/null +++ b/source/funkin/ui/credits/CreditsData.hx @@ -0,0 +1,34 @@ +package funkin.ui.credits; + +/** + * The members of the Funkin' Crew, organized by their roles. + */ +typedef CreditsData = +{ + var entries:Array<CreditsDataRole>; +} + +/** + * The members of a specific role on the Funkin' Crew. + */ +typedef CreditsDataRole = +{ + @:optional + var header:String; + + @:optional + @:default([]) + var body:Array<CreditsDataMember>; + + @:optional + @:default(false) + var appendBackers:Bool; +} + +/** + * A member of a specific person on the Funkin' Crew. + */ +typedef CreditsDataMember = +{ + var line:String; +} diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx new file mode 100644 index 000000000..2240ec50e --- /dev/null +++ b/source/funkin/ui/credits/CreditsDataHandler.hx @@ -0,0 +1,142 @@ +package funkin.ui.credits; + +import funkin.data.JsonFile; + +using StringTools; + +@:nullSafety +class CreditsDataHandler +{ + public static final BACKER_PUBLIC_URL:String = 'https://funkin.me/backers'; + + #if HARDCODED_CREDITS + static final CREDITS_DATA_PATH:String = "assets/exclude/data/credits.json"; + #else + static final CREDITS_DATA_PATH:String = "assets/data/credits.json"; + #end + + public static function debugPrint(data:Null<CreditsData>):Void + { + if (data == null) + { + trace('CreditsData(NULL)'); + return; + } + + if (data.entries == null || data.entries.length == 0) + { + trace('CreditsData(EMPTY)'); + return; + } + + var entryCount = data.entries.length; + var lineCount = 0; + for (entry in data.entries) + { + lineCount += entry?.body?.length ?? 0; + } + + trace('CreditsData($entryCount entries containing $lineCount lines)'); + } + + /** + * If for some reason the full credits won't load, + * use this hardcoded data for the original Funkin' Crew. + * + * @return `CreditsData` + */ + public static inline function getFallback():CreditsData + { + return { + entries: [ + { + header: 'Founders', + body: [ + {line: 'ninjamuffin99'}, + {line: 'PhantomArcade'}, + {line: 'KawaiSprite'}, + {line: 'evilsk8r'}, + ] + } + ] + }; + } + + public static function fetchBackerEntries():Array<String> + { + // TODO: Implement a web request. + // We can't just grab the current Kickstarter data and include it in builds, + // because we don't want to deadname people who haven't logged into the portal yet. + // It can be async and paginated for performance! + return []; + } + + #if HARDCODED_CREDITS + /** + * The data for the credits. + * Hardcoded into game via a macro at compile time. + */ + public static final CREDITS_DATA:Null<CreditsData> = #if macro null #else CreditsDataMacro.loadCreditsData() #end; + #else + + /** + * The data for the credits. + * Loaded dynamically from the game folder when needed. + * Nullable because data may fail to parse. + */ + public static var CREDITS_DATA(get, default):Null<CreditsData> = null; + + static function get_CREDITS_DATA():Null<CreditsData> + { + if (CREDITS_DATA == null) CREDITS_DATA = parseCreditsData(fetchCreditsData()); + + return CREDITS_DATA; + } + + static function fetchCreditsData():funkin.data.JsonFile + { + #if !macro + var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim(); + + return { + fileName: CREDITS_DATA_PATH, + contents: rawJson + }; + #else + return { + fileName: CREDITS_DATA_PATH, + contents: null + }; + #end + } + + static function parseCreditsData(file:JsonFile):Null<CreditsData> + { + #if !macro + if (file.contents == null) return null; + + var parser = new json2object.JsonParser<CreditsData>(); + parser.ignoreUnknownVariables = false; + trace('[CREDITS] Parsing credits data from ${CREDITS_DATA_PATH}'); + parser.fromJson(file.contents, file.fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, file.fileName); + return null; + } + return parser.value; + #else + return null; + #end + } + + static function printErrors(errors:Array<json2object.Error>, id:String = ''):Void + { + trace('[CREDITS] Failed to parse credits data: ${id}'); + + for (error in errors) + funkin.data.DataError.printError(error); + } + #end +} diff --git a/source/funkin/ui/credits/CreditsDataMacro.hx b/source/funkin/ui/credits/CreditsDataMacro.hx new file mode 100644 index 000000000..c97770eef --- /dev/null +++ b/source/funkin/ui/credits/CreditsDataMacro.hx @@ -0,0 +1,67 @@ +package funkin.ui.credits; + +#if macro +import haxe.macro.Context; +#end + +@:access(funkin.ui.credits.CreditsDataHandler) +class CreditsDataMacro +{ + public static macro function loadCreditsData():haxe.macro.Expr.ExprOf<CreditsData> + { + #if !display + trace('Hardcoding credits data...'); + var json = CreditsDataMacro.fetchJSON(); + + if (json == null) + { + Context.info('[WARN] Could not fetch JSON data for credits.', Context.currentPos()); + return macro $v{CreditsDataHandler.getFallback()}; + } + + var creditsData = CreditsDataMacro.parseJSON(json); + + if (creditsData == null) + { + Context.info('[WARN] Could not parse JSON data for credits.', Context.currentPos()); + return macro $v{CreditsDataHandler.getFallback()}; + } + + CreditsDataHandler.debugPrint(creditsData); + return macro $v{creditsData}; + // return macro $v{null}; + #else + // `#if display` is used for code completion. In this case we return + // a minimal value to keep code completion fast. + return macro $v{CreditsDataHandler.getFallback()}; + #end + } + + #if macro + static function fetchJSON():Null<String> + { + return sys.io.File.getContent(CreditsDataHandler.CREDITS_DATA_PATH); + } + + /** + * Parse the JSON data for the credits. + * + * @param json The string data to parse. + * @return The parsed data. + */ + static function parseJSON(json:String):Null<CreditsData> + { + try + { + // TODO: Use something with better validation but that still works at macro time. + return haxe.Json.parse(json); + } + catch (e) + { + trace('[ERROR] Failed to parse JSON data for credits.'); + trace(e); + return null; + } + } + #end +} diff --git a/source/funkin/ui/credits/CreditsState.hx b/source/funkin/ui/credits/CreditsState.hx new file mode 100644 index 000000000..d43e25114 --- /dev/null +++ b/source/funkin/ui/credits/CreditsState.hx @@ -0,0 +1,213 @@ +package funkin.ui.credits; + +import flixel.text.FlxText; +import flixel.util.FlxColor; +import funkin.audio.FunkinSound; +import flixel.FlxSprite; +import flixel.group.FlxSpriteGroup; + +/** + * The state used to display the credits scroll. + * AAA studios often fail to credit properly, and we're better than them! + */ +class CreditsState extends MusicBeatState +{ + /** + * The height the credits should start at. + * Make this an instanced variable so it gets set by the constructor. + */ + final STARTING_HEIGHT = FlxG.height; + + /** + * The padding on each side of the screen. + */ + static final SCREEN_PAD = 24; + + /** + * The width of the screen the credits should maximally fill up. + * Make this an instanced variable so it gets set by the constructor. + */ + final FULL_WIDTH = FlxG.width - (SCREEN_PAD * 2); + + /** + * The font to use to display the text. + * To use a font from the `assets` folder, use `Paths.font(...)`. + * Choose something that will render Unicode properly. + */ + static final CREDITS_FONT = 'Arial'; + + /** + * The size of the font. + */ + static final CREDITS_FONT_SIZE = 48; + + static final CREDITS_HEADER_FONT_SIZE = 72; + + /** + * The color of the text itself. + */ + static final CREDITS_FONT_COLOR = FlxColor.WHITE; + + /** + * The color of the text's outline. + */ + static final CREDITS_FONT_STROKE_COLOR = FlxColor.BLACK; + + /** + * The speed the credits scroll at, in pixels per second. + */ + static final CREDITS_SCROLL_BASE_SPEED = 25.0; + + /** + * The speed the credits scroll at while the button is held, in pixels per second. + */ + static final CREDITS_SCROLL_FAST_SPEED = CREDITS_SCROLL_BASE_SPEED * 4.0; + + /** + * The actual sprites and text used to display the credits. + */ + var creditsGroup:FlxSpriteGroup; + + var scrollPaused:Bool = false; + + public function new() + { + super(); + } + + public override function create():Void + { + super.create(); + + // Background + var bg = new FlxSprite(Paths.image('menuDesat')); + bg.scrollFactor.x = 0; + bg.scrollFactor.y = 0; + bg.setGraphicSize(Std.int(FlxG.width)); + bg.updateHitbox(); + bg.x = 0; + bg.y = 0; + bg.visible = true; + bg.color = 0xFFB57EDC; // Lavender + add(bg); + + // TODO: Once we need to display Kickstarter backers, + // make this use a recycled pool so we don't kill peformance. + creditsGroup = new FlxSpriteGroup(); + creditsGroup.x = SCREEN_PAD; + creditsGroup.y = STARTING_HEIGHT; + + buildCreditsGroup(); + + add(creditsGroup); + + // Music + FunkinSound.playMusic('freeplayRandom', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: true, + loop: true + }); + FlxG.sound.music.fadeIn(2, 0, 0.8); + } + + function buildCreditsGroup():Void + { + var y = 0; + + for (entry in CreditsDataHandler.CREDITS_DATA.entries) + { + if (entry.header != null) + { + creditsGroup.add(buildCreditsLine(entry.header, y, true, CreditsSide.Center)); + y += CREDITS_HEADER_FONT_SIZE; + } + + for (line in entry?.body ?? []) + { + creditsGroup.add(buildCreditsLine(line.line, y, false, CreditsSide.Center)); + y += CREDITS_FONT_SIZE; + } + + if (entry.appendBackers) + { + var backers = CreditsDataHandler.fetchBackerEntries(); + for (backer in backers) + { + creditsGroup.add(buildCreditsLine(backer, y, false, CreditsSide.Center)); + y += CREDITS_FONT_SIZE; + } + } + + // Padding between each role. + y += CREDITS_FONT_SIZE * 2; + } + } + + function buildCreditsLine(text:String, yPos:Float, header:Bool, side:CreditsSide = CreditsSide.Center):FlxText + { + // CreditsSide.Center: Full screen width + // CreditsSide.Left: Left half of screen + // CreditsSide.Right: Right half of screen + var xPos = (side == CreditsSide.Right) ? (FULL_WIDTH / 2) : 0; + var width = (side == CreditsSide.Center) ? FULL_WIDTH : (FULL_WIDTH / 2); + var size = header ? CREDITS_HEADER_FONT_SIZE : CREDITS_FONT_SIZE; + + var creditsLine:FlxText = new FlxText(xPos, yPos, width, text); + creditsLine.setFormat(CREDITS_FONT, size, CREDITS_FONT_COLOR, FlxTextAlign.CENTER, FlxTextBorderStyle.OUTLINE, CREDITS_FONT_STROKE_COLOR, true); + + return creditsLine; + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (!scrollPaused) + { + // TODO: Replace with whatever the special note button is. + if (controls.ACCEPT || FlxG.keys.pressed.SPACE) + { + // Move the whole group. + creditsGroup.y -= CREDITS_SCROLL_FAST_SPEED * elapsed; + } + else + { + // Move the whole group. + creditsGroup.y -= CREDITS_SCROLL_BASE_SPEED * elapsed; + } + } + + if (controls.BACK || hasEnded()) + { + exit(); + } + else if (controls.PAUSE) + { + scrollPaused = !scrollPaused; + } + } + + function hasEnded():Bool + { + return creditsGroup.y < -creditsGroup.height; + } + + function exit():Void + { + FlxG.switchState(new funkin.ui.mainmenu.MainMenuState()); + } + + public override function destroy():Void + { + super.destroy(); + } +} + +enum CreditsSide +{ + Left; + Center; + Right; +} diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 4bb3cd839..4e572a26f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -6,7 +6,6 @@ import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxSubState; -import flixel.graphics.FlxGraphic; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup; import flixel.input.gamepad.FlxGamepadInputID; @@ -15,6 +14,7 @@ import flixel.input.mouse.FlxMouseEvent; import flixel.math.FlxMath; import flixel.math.FlxPoint; import flixel.math.FlxRect; +import flixel.sound.FlxSound; import flixel.system.debug.log.LogStyle; import flixel.system.FlxAssets.FlxSoundAsset; import flixel.text.FlxText; @@ -26,8 +26,6 @@ import flixel.util.FlxSort; import flixel.util.FlxTimer; import funkin.audio.FunkinSound; import funkin.audio.visualize.PolygonSpectogram; -import funkin.audio.visualize.PolygonVisGroup; -import funkin.audio.VoicesGroup; import funkin.audio.VoicesGroup; import funkin.audio.waveform.WaveformSprite; import funkin.data.notestyle.NoteStyleRegistry; @@ -53,6 +51,7 @@ import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.components.HealthIcon; import funkin.play.notes.NoteSprite; import funkin.play.PlayState; +import funkin.play.PlayStatePlaylist; import funkin.play.song.Song; import funkin.save.Save; import funkin.ui.debug.charting.commands.AddEventsCommand; @@ -91,6 +90,7 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; import funkin.ui.mainmenu.MainMenuState; +import funkin.ui.transition.LoadingState; import funkin.util.Constants; import funkin.util.FileUtil; import funkin.util.logging.CrashHandler; @@ -5655,30 +5655,31 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } catch (e) { - this.error("Could Not Playtest", 'Got an error trying to playtest the song.\n${e}'); + this.error('Could Not Playtest', 'Got an error trying to playtest the song.\n${e}'); return; } - // TODO: Rework asset system so we can remove this. + // TODO: Rework asset system so we can remove this jank. switch (currentSongStage) { case 'mainStage': - Paths.setCurrentLevel('week1'); + PlayStatePlaylist.campaignId = 'week1'; case 'spookyMansion': - Paths.setCurrentLevel('week2'); + PlayStatePlaylist.campaignId = 'week2'; case 'phillyTrain': - Paths.setCurrentLevel('week3'); + PlayStatePlaylist.campaignId = 'week3'; case 'limoRide': - Paths.setCurrentLevel('week4'); + PlayStatePlaylist.campaignId = 'week4'; case 'mallXmas' | 'mallEvil': - Paths.setCurrentLevel('week5'); + PlayStatePlaylist.campaignId = 'week5'; case 'school' | 'schoolEvil': - Paths.setCurrentLevel('week6'); + PlayStatePlaylist.campaignId = 'week6'; case 'tankmanBattlefield': - Paths.setCurrentLevel('week7'); + PlayStatePlaylist.campaignId = 'week7'; case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2': - Paths.setCurrentLevel('weekend1'); + PlayStatePlaylist.campaignId = 'weekend1'; } + Paths.setCurrentLevel(PlayStatePlaylist.campaignId); subStateClosed.add(reviveUICamera); subStateClosed.add(resetConductorAfterTest); @@ -5686,7 +5687,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState FlxTransitionableState.skipNextTransIn = false; FlxTransitionableState.skipNextTransOut = false; - var targetState = new PlayState( + var targetStateParams = { targetSong: targetSong, targetDifficulty: selectedDifficulty, @@ -5697,14 +5698,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState startTimestamp: startTimestamp, playbackRate: playbackRate, overrideMusic: true, - }); + }; // Override music. if (audioInstTrack != null) { FlxG.sound.music = audioInstTrack; } - targetState.vocals = audioVocalTrackGroup; // Kill and replace the UI camera so it doesn't get destroyed during the state transition. uiCamera.kill(); @@ -5714,7 +5714,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState this.persistentUpdate = false; this.persistentDraw = false; stopWelcomeMusic(); - openSubState(targetState); + + LoadingState.loadPlayState(targetStateParams, false, true, function(targetState) { + targetState.vocals = audioVocalTrackGroup; + }); } /** 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/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx index 557875596..0308cd871 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx @@ -73,7 +73,7 @@ class ChartEditorImportExportHandler state.loadInstFromAsset(Paths.inst(songId, '-$variation'), variation); } - for (difficultyId in song.listDifficulties(variation)) + for (difficultyId in song.listDifficulties(variation, true, true)) { var diff:Null<SongDifficulty> = song.getDifficulty(difficultyId, variation); if (diff == null) continue; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index 8c7b1a8c1..f82bc3c1f 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -308,16 +308,6 @@ class ChartEditorToolboxHandler state.playtestBotPlayMode = checkboxBotPlay.selected; }; - var checkboxDebugger:Null<CheckBox> = toolbox.findComponent('playtestDebuggerCheckbox', CheckBox); - - if (checkboxDebugger == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestDebuggerCheckbox component.'; - - state.enabledDebuggerPopup = checkboxDebugger.selected; - - checkboxDebugger.onClick = _ -> { - state.enabledDebuggerPopup = checkboxDebugger.selected; - }; - var checkboxSongScripts:Null<CheckBox> = toolbox.findComponent('playtestSongScriptsCheckbox', CheckBox); if (checkboxSongScripts == null) diff --git a/source/funkin/ui/debug/latency/LatencyState.hx b/source/funkin/ui/debug/latency/LatencyState.hx index 7b2eabb1c..620f0edd7 100644 --- a/source/funkin/ui/debug/latency/LatencyState.hx +++ b/source/funkin/ui/debug/latency/LatencyState.hx @@ -171,10 +171,7 @@ class LatencyState extends MusicBeatSubState trace(FlxG.sound.music._channel.position); */ - #if FLX_DEBUG - funnyStatsGraph.update(FlxG.sound.music.time % 500); - realStats.update(swagSong.getTimeWithDiff() % 500); - #end + // localConductor.update(swagSong.time, false); if (FlxG.keys.justPressed.S) { diff --git a/source/funkin/ui/freeplay/Album.hx b/source/funkin/ui/freeplay/Album.hx index 7291c7357..3060d3eb8 100644 --- a/source/funkin/ui/freeplay/Album.hx +++ b/source/funkin/ui/freeplay/Album.hx @@ -1,6 +1,7 @@ package funkin.ui.freeplay; import funkin.data.freeplay.AlbumData; +import funkin.data.animation.AnimationData; import funkin.data.freeplay.AlbumRegistry; import funkin.data.IRegistryEntry; import flixel.graphics.FlxGraphic; @@ -75,6 +76,16 @@ class Album implements IRegistryEntry<AlbumData> return _data.albumTitleAsset; } + public function hasAlbumTitleAnimations() + { + return _data.albumTitleAnimations.length > 0; + } + + public function getAlbumTitleAnimations():Array<AnimationData> + { + return _data.albumTitleAnimations; + } + public function toString():String { return 'Album($id)'; diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx index a1e63c9a1..6b963a242 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; @@ -7,6 +8,7 @@ import flixel.tweens.FlxTween; import flixel.util.FlxTimer; import flixel.tweens.FlxEase; import funkin.data.freeplay.AlbumRegistry; +import funkin.util.assets.FlxAnimationUtil; import funkin.graphics.FunkinSprite; import funkin.util.SortUtil; import openfl.utils.Assets; @@ -21,9 +23,9 @@ class AlbumRoll extends FlxSpriteGroup * The ID of the album to display. * Modify this value to automatically update the album art and title. */ - public var albumId(default, set):String; + public var albumId(default, set):Null<String>; - function set_albumId(value:String):String + function set_albumId(value:Null<String>):Null<String> { if (this.albumId != value) { @@ -34,30 +36,47 @@ class AlbumRoll extends FlxSpriteGroup return value; } - var albumArt:FunkinSprite; - var albumTitle:FunkinSprite; - var difficultyStars:DifficultyStars; + var newAlbumArt:FlxAtlasSprite; + // var difficultyStars:DifficultyStars; var _exitMovers:Null<FreeplayState.ExitMoverData>; var albumData:Album; + final animNames:Map<String, String> = [ + "volume1-active" => "ENTRANCE", + "volume2-active" => "ENTRANCE VOL2", + "volume3-active" => "ENTRANCE VOL3", + "volume1-trans" => "VOL1 TRANS", + "volume2-trans" => "VOL2 TRANS", + "volume3-trans" => "VOL3 TRANS", + "volume1-idle" => "VOL1 STILL", + "volume2-idle" => "VOL2 STILL", + "volume3-idle" => "VOL3 STILL", + ]; + public function new() { super(); - albumTitle = new FunkinSprite(947, 491); - albumTitle.visible = true; - albumTitle.zIndex = 200; - add(albumTitle); + newAlbumArt = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum")); + newAlbumArt.visible = false; + newAlbumArt.onAnimationFinish.add(onAlbumFinish); - difficultyStars = new DifficultyStars(140, 39); + add(newAlbumArt); - difficultyStars.stars.visible = true; - albumTitle.visible = false; - // albumArtist.visible = false; + // difficultyStars = new DifficultyStars(140, 39); + // difficultyStars.stars.visible = false; + // add(difficultyStars); + } - // var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite')); + function onAlbumFinish(animName:String):Void + { + // Play the idle animation for the current album. + newAlbumArt.playAnimation(animNames.get('$albumId-idle'), false, false, true); + + // End on the last frame and don't continue until playAnimation is called again. + // newAlbumArt.anim.pause(); } /** @@ -65,6 +84,12 @@ class AlbumRoll extends FlxSpriteGroup */ function updateAlbum():Void { + if (albumId == null) + { + // difficultyStars.stars.visible = false; + return; + } + albumData = AlbumRegistry.instance.fetchEntry(albumId); if (albumData == null) @@ -74,33 +99,8 @@ class AlbumRoll extends FlxSpriteGroup return; }; - if (albumArt != null) - { - FlxTween.cancelTweensOf(albumArt); - albumArt.visible = false; - albumArt.destroy(); - remove(albumArt); - } - - // Paths.animateAtlas('freeplay/albumRoll'), - albumArt = FunkinSprite.create(1500, 360, albumData.getAlbumArtAssetKey()); - albumArt.setGraphicSize(262, 262); // Magic number for size IG - albumArt.zIndex = 100; - - // playIntro(); - add(albumArt); - applyExitMovers(); - if (Assets.exists(Paths.image(albumData.getAlbumTitleAssetKey()))) - { - albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey())); - } - else - { - albumTitle.visible = false; - } - refresh(); } @@ -126,67 +126,46 @@ class AlbumRoll extends FlxSpriteGroup if (exitMovers == null) return; - exitMovers.set([albumArt], + exitMovers.set([newAlbumArt], { x: FlxG.width, speed: 0.4, wait: 0 }); - exitMovers.set([albumTitle], - { - x: FlxG.width, - speed: 0.2, - wait: 0.1 - }); - - /* - exitMovers.set([albumArtist], - { - x: FlxG.width * 1.1, - speed: 0.2, - wait: 0.2 - }); - */ - exitMovers.set([difficultyStars], - { - x: FlxG.width * 1.2, - speed: 0.2, - wait: 0.3 - }); } + var titleTimer:Null<FlxTimer> = null; + /** * Play the intro animation on the album art. */ public function playIntro():Void { - albumArt.visible = true; - FlxTween.tween(albumArt, {x: 950, y: 320, angle: -340}, 0.5, {ease: FlxEase.elasticOut}); + newAlbumArt.visible = true; + newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false); - albumTitle.visible = false; + // difficultyStars.stars.visible = false; new FlxTimer().start(0.75, function(_) { - showTitle(); + // showTitle(); + // showStars(); }); } - public function setDifficultyStars(?difficulty:Int):Void + public function skipIntro():Void { - if (difficulty == null) return; - - difficultyStars.difficulty = difficulty; + newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false); } - public function showTitle():Void - { - albumTitle.visible = true; - } - - /** - * Make the album stars visible. - */ - public function showStars():Void - { - // albumArtist.visible = false; - difficultyStars.stars.visible = false; - } + // public function setDifficultyStars(?difficulty:Int):Void + // { + // if (difficulty == null) return; + // difficultyStars.difficulty = difficulty; + // } + // /** + // * Make the album stars visible. + // */ + // public function showStars():Void + // { + // difficultyStars.stars.visible = false; // true; + // } } diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx index 33f264301..5f1144fab 100644 --- a/source/funkin/ui/freeplay/DJBoyfriend.hx +++ b/source/funkin/ui/freeplay/DJBoyfriend.hx @@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite var gotSpooked:Bool = false; - static final SPOOK_PERIOD:Float = 10.0; - static final TV_PERIOD:Float = 10.0; + static final SPOOK_PERIOD:Float = 120.0; + static final TV_PERIOD:Float = 180.0; // Time since dad last SPOOKED you. var timeSinceSpook:Float = 0; @@ -43,7 +43,14 @@ class DJBoyfriend extends FlxAtlasSprite switch (name) { case "Boyfriend DJ watchin tv OG": - if (number == 85) runTvLogic(); + if (number == 80) + { + FunkinSound.playOnce(Paths.sound('remote_click')); + } + if (number == 85) + { + runTvLogic(); + } default: } }; @@ -219,19 +226,17 @@ class DJBoyfriend extends FlxAtlasSprite if (cartoonSnd == null) { // tv is OFF, but getting turned on - // Eric got FUCKING TROLLED there is no `tv_on` or `channel_switch` sound! - // FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { - // }); - loadCartoon(); + FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { + loadCartoon(); + }); } else { // plays it smidge after the click - // new FlxTimer().start(0.1, function(_) { - // // FunkinSound.playOnce(Paths.sound('channel_switch')); - // }); - cartoonSnd.destroy(); - loadCartoon(); + FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() { + cartoonSnd.destroy(); + loadCartoon(); + }); } // loadCartoon(); 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/FreeplayScore.hx b/source/funkin/ui/freeplay/FreeplayScore.hx index 413b182e0..da4c9f5d4 100644 --- a/source/funkin/ui/freeplay/FreeplayScore.hx +++ b/source/funkin/ui/freeplay/FreeplayScore.hx @@ -42,11 +42,11 @@ class FreeplayScore extends FlxTypedSpriteGroup<ScoreNum> return val; } - public function new(x:Float, y:Float, scoreShit:Int = 100) + public function new(x:Float, y:Float, digitCount:Int, scoreShit:Int = 100) { super(x, y); - for (i in 0...7) + for (i in 0...digitCount) { add(new ScoreNum(x + (45 * i), y, 0)); } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 6cb0d1d9a..3ac441212 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -133,8 +133,8 @@ class FreeplayState extends MusicBeatSubState var stickerSubState:StickerSubState; - static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY; - static var rememberedSongId:Null<String> = null; + public static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY; + public static var rememberedSongId:Null<String> = 'tutorial'; public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { @@ -145,7 +145,7 @@ class FreeplayState extends MusicBeatSubState stickerSubState = stickers; } - super(); + super(FlxColor.TRANSPARENT); } override function create():Void @@ -195,7 +195,7 @@ class FreeplayState extends MusicBeatSubState var song:Song = SongRegistry.instance.fetchEntry(songId); // Only display songs which actually have available charts for the current character. - var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations); + var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false); if (availableDifficultiesForSong.length == 0) continue; songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations)); @@ -380,7 +380,7 @@ class FreeplayState extends MusicBeatSubState } albumRoll = new AlbumRoll(); - albumRoll.albumId = 'volume1'; + albumRoll.albumId = null; add(albumRoll); albumRoll.applyExitMovers(exitMovers); @@ -425,7 +425,7 @@ class FreeplayState extends MusicBeatSubState tmr.time = FlxG.random.float(20, 60); }, 0); - fp = new FreeplayScore(460, 60, 100); + fp = new FreeplayScore(460, 60, 7, 100); fp.visible = false; add(fp); @@ -470,11 +470,7 @@ class FreeplayState extends MusicBeatSubState albumRoll.playIntro(); new FlxTimer().start(0.75, function(_) { - albumRoll.showTitle(); - }); - - new FlxTimer().start(35 / 24, function(_) { - albumRoll.showStars(); + // albumRoll.showTitle(); }); FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut}); @@ -536,21 +532,18 @@ class FreeplayState extends MusicBeatSubState }); } + var currentFilter:SongFilter = null; + var currentFilteredSongs:Array<FreeplaySongData> = []; + /** * Given the current filter, rebuild the current song list. * * @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite) * @param force + * @param onlyIfChanged Only apply the filter if the song list has changed */ - public function generateSongList(?filterStuff:SongFilter, force:Bool = false):Void + public function generateSongList(filterStuff:Null<SongFilter>, force:Bool = false, onlyIfChanged:Bool = true):Void { - curSelected = 1; - - for (cap in grpCapsules.members) - { - cap.kill(); - } - var tempSongs:Array<FreeplaySongData> = songs; if (filterStuff != null) @@ -582,6 +575,35 @@ class FreeplayState extends MusicBeatSubState } } + // Filter further by current selected difficulty. + if (currentDifficulty != null) + { + tempSongs = tempSongs.filter(song -> { + if (song == null) return true; // Random + return song.songDifficulties.contains(currentDifficulty); + }); + } + + if (onlyIfChanged) + { + // == performs equality by reference + if (tempSongs.isEqualUnordered(currentFilteredSongs)) return; + } + + // Only now do we know that the filter is actually changing. + + rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId ?? rememberedSongId; + + for (cap in grpCapsules.members) + { + cap.kill(); + } + + currentFilter = filterStuff; + + currentFilteredSongs = tempSongs; + curSelected = 0; + var hsvShader:HSVShader = new HSVShader(); var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); @@ -616,14 +638,7 @@ class FreeplayState extends MusicBeatSubState funnyMenu.favIcon.visible = tempSongs[i].isFav; funnyMenu.hsvShader = hsvShader; - if (i < 8) - { - funnyMenu.initJumpIn(Math.min(i, 4), force); - } - else - { - funnyMenu.forcePosition(); - } + funnyMenu.forcePosition(); grpCapsules.add(funnyMenu); } @@ -658,11 +673,12 @@ class FreeplayState extends MusicBeatSubState if (FlxG.keys.justPressed.F) { - if (songs[curSelected] != null) + var targetSong = grpCapsules.members[curSelected]?.songData; + if (targetSong != null) { var realShit:Int = curSelected; - songs[curSelected].isFav = !songs[curSelected].isFav; - if (songs[curSelected].isFav) + targetSong.isFav = !targetSong.isFav; + if (targetSong.isFav) { FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, { @@ -854,11 +870,13 @@ class FreeplayState extends MusicBeatSubState { dj.resetAFKTimer(); changeDiff(-1); + generateSongList(currentFilter, true); } if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) { dj.resetAFKTimer(); changeDiff(1); + generateSongList(currentFilter, true); } if (controls.BACK && !typing.hasFocus) @@ -877,6 +895,8 @@ class FreeplayState extends MusicBeatSubState for (spr in grpSpr) { + if (spr == null) continue; + var funnyMoveShit:MoveData = moveData; if (moveData.x == null) funnyMoveShit.x = spr.x; @@ -899,7 +919,7 @@ class FreeplayState extends MusicBeatSubState if (Type.getClass(FlxG.state) == MainMenuState) { - FlxG.state.persistentUpdate = true; + FlxG.state.persistentUpdate = false; FlxG.state.persistentDraw = true; } @@ -908,6 +928,11 @@ class FreeplayState extends MusicBeatSubState FlxTransitionableState.skipNextTransOut = true; if (Type.getClass(FlxG.state) == MainMenuState) { + FunkinSound.playMusic('freakyMenu', + { + overrideExisting: true, + restartTrack: false + }); close(); } else @@ -926,7 +951,7 @@ class FreeplayState extends MusicBeatSubState public override function destroy():Void { super.destroy(); - var daSong:Null<FreeplaySongData> = songs[curSelected]; + var daSong:Null<FreeplaySongData> = currentFilteredSongs[curSelected]; if (daSong != null) { clearDaCache(daSong.songName); @@ -948,10 +973,10 @@ class FreeplayState extends MusicBeatSubState currentDifficulty = diffIdsCurrent[currentDifficultyIndex]; - var daSong:Null<FreeplaySongData> = songs[curSelected]; + var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].songData; if (daSong != null) { - var songScore:SaveScoreData = Save.instance.getSongScore(songs[curSelected].songId, currentDifficulty); + var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore?.accuracy ?? 0.0; rememberedDifficulty = currentDifficulty; @@ -1011,15 +1036,12 @@ class FreeplayState extends MusicBeatSubState } } - // Set the difficulty star count on the right. - albumRoll.setDifficultyStars(daSong?.songRating); - // Set the album graphic and play the animation if relevant. - var newAlbumId:String = daSong?.albumId ?? Constants.DEFAULT_ALBUM_ID; + var newAlbumId:String = daSong?.albumId; if (albumRoll.albumId != newAlbumId) { albumRoll.albumId = newAlbumId; - albumRoll.playIntro(); + albumRoll.skipIntro(); } } @@ -1103,6 +1125,12 @@ class FreeplayState extends MusicBeatSubState targetVariation: targetVariation, practiceMode: false, minimalMode: false, + + #if (debug || FORCE_DEBUG_VERSION) + botPlayMode: FlxG.keys.pressed.SHIFT, + #else + botPlayMode: false, + #end // TODO: Make these an option! It's currently only accessible via chart editor. // startTimestamp: 0.0, // playbackRate: 0.5, @@ -1115,20 +1143,18 @@ class FreeplayState extends MusicBeatSubState { if (rememberedSongId != null) { - curSelected = songs.findIndex(function(song) { + curSelected = currentFilteredSongs.findIndex(function(song) { if (song == null) return false; return song.songId == rememberedSongId; }); + + if (curSelected == -1) curSelected = 0; } if (rememberedDifficulty != null) { currentDifficulty = rememberedDifficulty; } - - // Set the difficulty star count on the right. - var daSong:Null<FreeplaySongData> = songs[curSelected]; - albumRoll.setDifficultyStars(daSong?.songRating ?? 0); } function changeSelection(change:Int = 0):Void @@ -1156,8 +1182,10 @@ class FreeplayState extends MusicBeatSubState { intendedScore = 0; intendedCompletion = 0.0; + diffIdsCurrent = diffIdsTotal; rememberedSongId = null; rememberedDifficulty = null; + albumRoll.albumId = null; } for (index => capsule in grpCapsules.members) @@ -1195,12 +1223,33 @@ class FreeplayState extends MusicBeatSubState }); if (didReplace) { + FunkinSound.playMusic('freakyMenu', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: false + }); FlxG.sound.music.fadeIn(2, 0, 0.8); } } grpCapsules.members[curSelected].selected = true; } } + + /** + * Build an instance of `FreeplayState` that is above the `MainMenuState`. + * @return The MainMenuState with the FreeplayState as a substate. + */ + public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState + { + var result = new MainMenuState(); + result.persistentUpdate = false; + result.persistentDraw = true; + + result.openSubState(new FreeplayState(params, stickers)); + + return result; + } } /** @@ -1307,7 +1356,7 @@ class FreeplaySongData public var songName(default, null):String = ''; public var songCharacter(default, null):String = ''; public var songRating(default, null):Int = 0; - public var albumId(default, null):String = ''; + public var albumId(default, null):Null<String> = null; public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; public var displayedVariations(default, null):Array<String> = [Constants.DEFAULT_VARIATION]; @@ -1333,7 +1382,7 @@ class FreeplaySongData function updateValues(variations:Array<String>):Void { - this.songDifficulties = song.listDifficulties(variations); + this.songDifficulties = song.listDifficulties(variations, false, false); if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY; var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations); @@ -1341,7 +1390,15 @@ class FreeplaySongData this.songName = songDifficulty.songName; this.songCharacter = songDifficulty.characters.opponent; this.songRating = songDifficulty.difficultyRating; - this.albumId = songDifficulty.album; + if (songDifficulty.album == null) + { + FlxG.log.warn('No album for: ${songDifficulty.songName}'); + this.albumId = Constants.DEFAULT_ALBUM_ID; + } + else + { + this.albumId = songDifficulty.album; + } } } diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index bffa821b3..f8b3d7ac3 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -83,10 +83,10 @@ class SongMenuItem extends FlxSpriteGroup diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00')); diffRatingSprite.shader = grayscaleShader; + diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y); // TODO: Readd once ratings are fully implemented // add(diffRatingSprite); - diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y); - grpHide.add(diffRatingSprite); + // grpHide.add(diffRatingSprite); songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled)); add(songText); diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index a8c2039ab..d2d8adeca 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; @@ -51,12 +52,10 @@ class MainMenuState extends MusicBeatState transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; - if (!(FlxG?.sound?.music?.playing ?? false)) - { - playMenuMusic(); - } + playMenuMusic(); - persistentUpdate = persistentDraw = true; + persistentUpdate = false; + persistentDraw = true; var bg:FlxSprite = new FlxSprite(Paths.image('menuBG')); bg.scrollFactor.x = 0; @@ -69,7 +68,7 @@ class MainMenuState extends MusicBeatState camFollow = new FlxObject(0, 0, 1, 1); add(camFollow); - magenta = new FlxSprite(Paths.image('menuDesat')); + magenta = new FlxSprite(Paths.image('menuBGMagenta')); magenta.scrollFactor.x = bg.scrollFactor.x; magenta.scrollFactor.y = bg.scrollFactor.y; magenta.setGraphicSize(Std.int(bg.width)); @@ -77,7 +76,6 @@ class MainMenuState extends MusicBeatState magenta.x = bg.x; magenta.y = bg.y; magenta.visible = false; - magenta.color = 0xFFfd719b; // TODO: Why doesn't this line compile I'm going fucking feral @@ -109,14 +107,21 @@ class MainMenuState extends MusicBeatState }); #if CAN_OPEN_LINKS + // In order to prevent popup blockers from triggering, + // we need to open the link as an immediate result of a keypress event, + // so we can't wait for the flicker animation to complete. var hasPopupBlocker = #if web true #else false #end; - createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker); + createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker); #end createMenuItem('options', 'mainmenu/options', function() { startExitState(() -> new funkin.ui.options.OptionsState()); }); + createMenuItem('credits', 'mainmenu/credits', function() { + startExitState(() -> new funkin.ui.credits.CreditsState()); + }); + // Reset position of menu items. var spacing = 160; var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2; @@ -125,6 +130,9 @@ class MainMenuState extends MusicBeatState var menuItem = menuItems.members[i]; menuItem.x = FlxG.width / 2; menuItem.y = top + spacing * i; + menuItem.scrollFactor.x = 0.0; + // This one affects how much the menu items move when you scroll between them. + menuItem.scrollFactor.y = 0.4; } resetCamStuff(); @@ -166,6 +174,7 @@ class MainMenuState extends MusicBeatState { FlxG.cameras.reset(new FunkinCamera()); FlxG.camera.follow(camFollow, null, 0.06); + FlxG.camera.snapToTarget(); } function createMenuItem(name:String, atlas:String, callback:Void->Void, fireInstantly:Bool = false):Void @@ -212,6 +221,11 @@ class MainMenuState extends MusicBeatState { WindowUtil.openURL(Constants.URL_ITCH); } + + function selectMerch() + { + WindowUtil.openURL(Constants.URL_MERCH); + } #end #if newgrounds @@ -311,8 +325,6 @@ class MainMenuState extends MusicBeatState // Open the debug menu, defaults to ` / ~ if (controls.DEBUG_MENU) { - this.persistentUpdate = false; - this.persistentDraw = false; FlxG.state.openSubState(new DebugMenuSubState()); } diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx index 0f33a0780..a00b28dbb 100644 --- a/source/funkin/ui/options/OptionsState.hx +++ b/source/funkin/ui/options/OptionsState.hx @@ -23,8 +23,7 @@ class OptionsState extends MusicBeatState override function create() { - var menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat')); - menuBG.color = 0xFFea71fd; + var menuBG = new FlxSprite().loadGraphic(Paths.image('menuBGBlue')); menuBG.setGraphicSize(Std.int(menuBG.width * 1.1)); menuBG.updateHitbox(); menuBG.screenCenter(); diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index 626fb8e52..8f454aa1a 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -169,7 +169,7 @@ class Level implements IRegistryEntry<LevelData> if (firstSong != null) { // Don't display alternate characters in Story Mode. Only show `default` and `erect` variations. - for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect'])) + for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect'], false, false)) { difficulties.push(difficulty); } diff --git a/source/funkin/ui/title/AttractState.hx b/source/funkin/ui/title/AttractState.hx index 0af97afd9..a42a6c3d9 100644 --- a/source/funkin/ui/title/AttractState.hx +++ b/source/funkin/ui/title/AttractState.hx @@ -17,7 +17,7 @@ import funkin.ui.MusicBeatState; */ class AttractState extends MusicBeatState { - static final ATTRACT_VIDEO_PATH:String = Paths.videos('kickstarterTrailer'); + static final ATTRACT_VIDEO_PATH:String = Paths.stripLibrary(Paths.videos('kickstarterTrailer', 'shared')); public override function create():Void { @@ -29,10 +29,12 @@ class AttractState extends MusicBeatState } #if html5 + trace('Playing web video ${ATTRACT_VIDEO_PATH}'); playVideoHTML5(ATTRACT_VIDEO_PATH); #end #if hxCodec + trace('Playing native video ${ATTRACT_VIDEO_PATH}'); playVideoNative(ATTRACT_VIDEO_PATH); #end } diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 1a4e13ab1..e76e66003 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -220,7 +220,7 @@ class TitleState extends MusicBeatState function playMenuMusic():Void { - var shouldFadeIn = (FlxG.sound.music == null); + var shouldFadeIn:Bool = (FlxG.sound.music == null); // Load music. Includes logic to handle BPM changes. FunkinSound.playMusic('freakyMenu', { @@ -229,7 +229,7 @@ class TitleState extends MusicBeatState restartTrack: true }); // Fade from 0.0 to 0.7 over 4 seconds - if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 0.7); + if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 1.0); } function getIntroTextShit():Array<Array<String>> @@ -290,18 +290,6 @@ class TitleState extends MusicBeatState // do controls.PAUSE | controls.ACCEPT instead? var pressedEnter:Bool = FlxG.keys.justPressed.ENTER; - if (FlxG.onMobile) - { - for (touch in FlxG.touches.list) - { - if (touch.justPressed) - { - FlxG.switchState(() -> new FreeplayState()); - pressedEnter = true; - } - } - } - var gamepad:FlxGamepad = FlxG.gamepads.lastActive; if (gamepad != null) diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index 980c264e3..347190993 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -22,10 +22,12 @@ import openfl.filters.ShaderFilter; import openfl.utils.Assets; import flixel.util.typeLimit.NextState; -class LoadingState extends MusicBeatState +class LoadingState extends MusicBeatSubState { inline static var MIN_TIME = 1.0; + var asSubState:Bool = false; + var target:NextState; var playParams:Null<PlayStateParams>; var stopMusic:Bool = false; @@ -178,7 +180,16 @@ class LoadingState extends MusicBeatState FlxG.sound.music = null; } - FlxG.switchState(target); + if (asSubState) + { + this.close(); + // We will assume the target is a valid substate. + FlxG.state.openSubState(cast target); + } + else + { + FlxG.switchState(target); + } } static function getSongPath():String @@ -190,17 +201,41 @@ class LoadingState extends MusicBeatState * Starts the transition to a new `PlayState` to start a new song. * First switches to the `LoadingState` if assets need to be loaded. * @param params The parameters for the next `PlayState`. + * @param asSubState Whether to open as a substate rather than switching to the `PlayState`. * @param shouldStopMusic Whether to stop the current music while loading. */ - public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false):Void + public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false, asSubState = false, ?onConstruct:PlayState->Void):Void { Paths.setCurrentLevel(PlayStatePlaylist.campaignId); - var playStateCtor:NextState = () -> new PlayState(params); + var playStateCtor:() -> PlayState = function() { + return new PlayState(params); + }; + + if (onConstruct != null) + { + playStateCtor = function() { + var result = new PlayState(params); + onConstruct(result); + return result; + }; + } #if NO_PRELOAD_ALL // Switch to loading state while we load assets (default on HTML5 target). - var loadStateCtor:NextState = () -> new LoadingState(playStateCtor, shouldStopMusic, params); - FlxG.switchState(loadStateCtor); + var loadStateCtor = function() { + var result = new LoadingState(playStateCtor, shouldStopMusic, params); + @:privateAccess + result.asSubState = asSubState; + return result; + } + if (asSubState) + { + FlxG.state.openSubState(cast loadStateCtor()); + } + else + { + FlxG.switchState(loadStateCtor); + } #else // All assets preloaded, switch directly to play state (defualt on other targets). if (shouldStopMusic && FlxG.sound.music != null) @@ -210,14 +245,42 @@ class LoadingState extends MusicBeatState } // Load and cache the song's charts. - if (params?.targetSong != null) + // Don't do this if we already provided the music and charts. + if (params?.targetSong != null && !params.overrideMusic) { params.targetSong.cacheCharts(true); } + var shouldPreloadLevelAssets:Bool = !(params?.minimalMode ?? false); + + if (shouldPreloadLevelAssets) preloadLevelAssets(); + + if (asSubState) + { + FlxG.state.openSubState(cast playStateCtor()); + } + else + { + FlxG.switchState(playStateCtor); + } + #end + } + + #if NO_PRELOAD_ALL + static function isSoundLoaded(path:String):Bool + { + return Assets.cache.hasSound(path); + } + + static function isLibraryLoaded(library:String):Bool + { + return Assets.getLibrary(library) != null; + } + #else + static function preloadLevelAssets():Void + { // TODO: This section is a hack! Redo this later when we have a proper asset caching system. FunkinSprite.preparePurgeCache(); - FunkinSprite.cacheTexture(Paths.image('combo')); FunkinSprite.cacheTexture(Paths.image('healthBar')); FunkinSprite.cacheTexture(Paths.image('menuDesat')); FunkinSprite.cacheTexture(Paths.image('combo')); @@ -247,7 +310,10 @@ class LoadingState extends MusicBeatState // List all image assets in the level's library. // This is crude and I want to remove it when we have a proper asset caching system. // TODO: Get rid of this junk! - var library = openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId); + var library = PlayStatePlaylist.campaignId != null ? openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId) : null; + + if (library == null) return; // We don't need to do anymore precaching. + var assets = library.list(lime.utils.AssetType.IMAGE); trace('Got ${assets.length} assets: ${assets}'); @@ -278,20 +344,6 @@ class LoadingState extends MusicBeatState // FunkinSprite.cacheAllSongTextures(stage) FunkinSprite.purgeCache(); - - FlxG.switchState(playStateCtor); - #end - } - - #if NO_PRELOAD_ALL - static function isSoundLoaded(path:String):Bool - { - return Assets.cache.hasSound(path); - } - - static function isLibraryLoaded(library:String):Bool - { - return Assets.getLibrary(library) != null; } #end diff --git a/source/funkin/ui/transition/preload/FunkinPreloader.hx b/source/funkin/ui/transition/preload/FunkinPreloader.hx index 89a8c1140..9f509df9c 100644 --- a/source/funkin/ui/transition/preload/FunkinPreloader.hx +++ b/source/funkin/ui/transition/preload/FunkinPreloader.hx @@ -1,5 +1,10 @@ package funkin.ui.transition.preload; +import openfl.filters.GlowFilter; +import openfl.display.SpreadMethod; +import openfl.display.GradientType; +import openfl.geom.Matrix; +import openfl.filters.BlurFilter; import openfl.events.MouseEvent; import flash.display.Bitmap; import flash.display.BitmapData; @@ -46,7 +51,7 @@ class FunkinPreloader extends FlxBasePreloader */ static final BAR_PADDING:Float = 20; - static final BAR_HEIGHT:Int = 20; + static final BAR_HEIGHT:Int = 12; /** * Logo takes this long (in seconds) to fade in. @@ -108,13 +113,22 @@ class FunkinPreloader extends FlxBasePreloader #if TOUCH_HERE_TO_PLAY var touchHereToPlay:Bitmap; #end + var progressBarPieces:Array<Sprite>; var progressBar:Bitmap; var progressLeftText:TextField; var progressRightText:TextField; + var dspText:TextField; + var enhancedText:TextField; + var stereoText:TextField; + + var vfdShader:VFDOverlay; + var box:Sprite; + var progressLines:Sprite; + public function new() { - super(Constants.PRELOADER_MIN_STAGE_TIME, Constants.SITE_LOCK); + super(Constants.PRELOADER_MIN_STAGE_TIME); // We can't even call trace() yet, until Flixel loads. trace('Initializing custom preloader...'); @@ -146,7 +160,7 @@ class FunkinPreloader extends FlxBasePreloader bmp.x = (this._width - bmp.width) / 2; bmp.y = (this._height - bmp.height) / 2; }); - addChild(logo); + // addChild(logo); #if TOUCH_HERE_TO_PLAY touchHereToPlay = createBitmap(TouchHereToPlayImage, function(bmp:Bitmap) { @@ -160,16 +174,48 @@ class FunkinPreloader extends FlxBasePreloader addChild(touchHereToPlay); #end + var amountOfPieces:Int = 16; + progressBarPieces = []; + var maxBarWidth = this._width - BAR_PADDING * 2; + var pieceWidth = maxBarWidth / amountOfPieces; + var pieceGap:Int = 8; + + progressLines = new openfl.display.Sprite(); + progressLines.graphics.lineStyle(2, Constants.COLOR_PRELOADER_BAR); + progressLines.graphics.drawRect(-2, 480, this._width + 4, 30); + addChild(progressLines); + + var progressBarPiece = new Sprite(); + progressBarPiece.graphics.beginFill(Constants.COLOR_PRELOADER_BAR); + progressBarPiece.graphics.drawRoundRect(0, 0, pieceWidth - pieceGap, BAR_HEIGHT, 4, 4); + progressBarPiece.graphics.endFill(); + + for (i in 0...amountOfPieces) + { + var piece = new Sprite(); + piece.graphics.beginFill(Constants.COLOR_PRELOADER_BAR); + piece.graphics.drawRoundRect(0, 0, pieceWidth - pieceGap, BAR_HEIGHT, 4, 4); + piece.graphics.endFill(); + + piece.x = i * (piece.width + pieceGap); + piece.y = this._height - BAR_PADDING - BAR_HEIGHT - 200; + addChild(piece); + progressBarPieces.push(piece); + } + // Create the progress bar. - progressBar = new Bitmap(new BitmapData(1, BAR_HEIGHT, true, Constants.COLOR_PRELOADER_BAR)); - progressBar.x = BAR_PADDING; - progressBar.y = this._height - BAR_PADDING - BAR_HEIGHT; - addChild(progressBar); + // progressBar = new Bitmap(new BitmapData(1, BAR_HEIGHT, true, Constants.COLOR_PRELOADER_BAR)); + // progressBar.x = BAR_PADDING; + // progressBar.y = this._height - BAR_PADDING - BAR_HEIGHT; + // addChild(progressBar); // Create the progress message. progressLeftText = new TextField(); + dspText = new TextField(); + enhancedText = new TextField(); + stereoText = new TextField(); - var progressLeftTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true); + var progressLeftTextFormat = new TextFormat("DS-Digital", 32, Constants.COLOR_PRELOADER_BAR, true); progressLeftTextFormat.align = TextFormatAlign.LEFT; progressLeftText.defaultTextFormat = progressLeftTextFormat; @@ -177,13 +223,14 @@ class FunkinPreloader extends FlxBasePreloader progressLeftText.width = this._width - BAR_PADDING * 2; progressLeftText.text = 'Downloading assets...'; progressLeftText.x = BAR_PADDING; - progressLeftText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4; + progressLeftText.y = this._height - BAR_PADDING - BAR_HEIGHT - 290; + // progressLeftText.shader = new VFDOverlay(); addChild(progressLeftText); // Create the progress %. progressRightText = new TextField(); - var progressRightTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true); + var progressRightTextFormat = new TextFormat("DS-Digital", 16, Constants.COLOR_PRELOADER_BAR, true); progressRightTextFormat.align = TextFormatAlign.RIGHT; progressRightText.defaultTextFormat = progressRightTextFormat; @@ -193,6 +240,60 @@ class FunkinPreloader extends FlxBasePreloader progressRightText.x = BAR_PADDING; progressRightText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4; addChild(progressRightText); + + box = new Sprite(); + box.graphics.beginFill(Constants.COLOR_PRELOADER_BAR, 1); + box.graphics.drawRoundRect(0, 0, 64, 20, 5, 5); + box.graphics.drawRoundRect(70, 0, 58, 20, 5, 5); + box.graphics.endFill(); + box.graphics.beginFill(Constants.COLOR_PRELOADER_BAR, 0.1); + box.graphics.drawRoundRect(0, 0, 128, 20, 5, 5); + box.graphics.endFill(); + box.x = 880; + box.y = 440; + addChild(box); + + dspText.selectable = false; + dspText.textColor = 0x000000; + dspText.width = this._width; + dspText.height = 20; + dspText.text = 'DSP'; + dspText.x = 10; + dspText.y = -5; + box.addChild(dspText); + + enhancedText.selectable = false; + enhancedText.textColor = Constants.COLOR_PRELOADER_BAR; + enhancedText.width = this._width; + enhancedText.height = 100; + enhancedText.text = 'ENHANCED'; + enhancedText.x = -100; + enhancedText.y = 0; + box.addChild(enhancedText); + + stereoText.selectable = false; + stereoText.textColor = Constants.COLOR_PRELOADER_BAR; + stereoText.width = this._width; + stereoText.height = 100; + stereoText.text = 'STEREO'; + stereoText.x = 0; + stereoText.y = -40; + box.addChild(stereoText); + + // var dummyMatrix:openfl.geom.Matrix = new Matrix(); + // dummyMatrix.createGradientBox(this._width, this._height * 0.1, 90 * Math.PI / 180); + + // var gradient:Sprite = new Sprite(); + // gradient.graphics.beginGradientFill(GradientType.LINEAR, [0xFFFFFF, 0x000000], [1, 1], [0, 255], dummyMatrix, SpreadMethod.REFLECT); + // gradient.graphics.drawRect(0, 0, this._width, this._height); + // gradient.graphics.endFill(); + // addChild(gradient); + + var vfdBitmap:Bitmap = new Bitmap(new BitmapData(this._width, this._height, true, 0xFFFFFFFF)); + addChild(vfdBitmap); + + vfdShader = new VFDOverlay(); + vfdBitmap.shader = vfdShader; } var lastElapsed:Float = 0.0; @@ -200,6 +301,8 @@ class FunkinPreloader extends FlxBasePreloader override function update(percent:Float):Void { var elapsed:Float = (Date.now().getTime() - this._startTime) / 1000.0; + + vfdShader.update(elapsed * 100); // trace('Time since last frame: ' + (lastElapsed - elapsed)); downloadingAssetsPercent = percent; @@ -748,12 +851,19 @@ class FunkinPreloader extends FlxBasePreloader else { renderLogoFadeIn(elapsed); + + // Render progress bar + var maxWidth = this._width - BAR_PADDING * 2; + var barWidth = maxWidth * percent; + var piecesToRender:Int = Std.int(percent * progressBarPieces.length); + + for (i => piece in progressBarPieces) + { + piece.alpha = i <= piecesToRender ? 0.9 : 0.1; + } } - // Render progress bar - var maxWidth = this._width - BAR_PADDING * 2; - var barWidth = maxWidth * percent; - progressBar.width = barWidth; + // progressBar.width = barWidth; // Cycle ellipsis count to show loading var ellipsisCount:Int = Std.int(elapsed / ELLIPSIS_TIME) % 3 + 1; @@ -766,29 +876,29 @@ class FunkinPreloader extends FlxBasePreloader { // case FunkinPreloaderState.NotStarted: default: - updateProgressLeftText('Loading (0/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Loading \n0/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.DownloadingAssets: - updateProgressLeftText('Downloading assets (1/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Downloading assets \n1/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.PreloadingPlayAssets: - updateProgressLeftText('Preloading assets (2/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Preloading assets \n2/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.InitializingScripts: - updateProgressLeftText('Initializing scripts (3/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Initializing scripts \n3/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.CachingGraphics: - updateProgressLeftText('Caching graphics (4/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Caching graphics \n4/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.CachingAudio: - updateProgressLeftText('Caching audio (5/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Caching audio \n5/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.CachingData: - updateProgressLeftText('Caching data (6/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Caching data \n6/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.ParsingSpritesheets: - updateProgressLeftText('Parsing spritesheets (7/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Parsing spritesheets \n7/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.ParsingStages: - updateProgressLeftText('Parsing stages (8/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Parsing stages \n8/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.ParsingCharacters: - updateProgressLeftText('Parsing characters (9/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Parsing characters \n9/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.ParsingSongs: - updateProgressLeftText('Parsing songs (10/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Parsing songs \n10/$TOTAL_STEPS $ellipsis'); case FunkinPreloaderState.Complete: - updateProgressLeftText('Finishing up ($TOTAL_STEPS/$TOTAL_STEPS)$ellipsis'); + updateProgressLeftText('Finishing up \n$TOTAL_STEPS/$TOTAL_STEPS $ellipsis'); #if TOUCH_HERE_TO_PLAY case FunkinPreloaderState.TouchHereToPlay: updateProgressLeftText(null); @@ -815,10 +925,21 @@ class FunkinPreloader extends FlxBasePreloader else if (progressLeftText.text != text) { // We have to keep updating the text format, because the font can take a frame or two to load. - var progressLeftTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true); + var progressLeftTextFormat = new TextFormat("DS-Digital", 32, Constants.COLOR_PRELOADER_BAR, true); progressLeftTextFormat.align = TextFormatAlign.LEFT; progressLeftText.defaultTextFormat = progressLeftTextFormat; progressLeftText.text = text; + + dspText.defaultTextFormat = new TextFormat("Quantico", 20, 0x000000, false); + dspText.text = 'DSP\t\t\t\t\tFNF'; // fukin dum.... + dspText.textColor = 0x000000; + + enhancedText.defaultTextFormat = new TextFormat("Inconsolata Black", 16, Constants.COLOR_PRELOADER_BAR, false); + enhancedText.text = 'ENHANCED'; + enhancedText.textColor = Constants.COLOR_PRELOADER_BAR; + + stereoText.defaultTextFormat = new TextFormat("Inconsolata Bold", 36, Constants.COLOR_PRELOADER_BAR, false); + stereoText.text = 'NATURAL STEREO'; } } } @@ -845,9 +966,17 @@ class FunkinPreloader extends FlxBasePreloader logo.y = (this._height - logo.height) / 2; // Fade out progress bar too. - progressBar.alpha = logo.alpha; + // progressBar.alpha = logo.alpha; progressLeftText.alpha = logo.alpha; progressRightText.alpha = logo.alpha; + box.alpha = logo.alpha; + dspText.alpha = logo.alpha; + enhancedText.alpha = logo.alpha; + stereoText.alpha = logo.alpha; + progressLines.alpha = logo.alpha; + + for (piece in progressBarPieces) + piece.alpha = logo.alpha; return elapsedFinished; } @@ -901,8 +1030,8 @@ class FunkinPreloader extends FlxBasePreloader { // Ensure the graphics are properly destroyed and GC'd. removeChild(logo); - removeChild(progressBar); - logo = progressBar = null; + // removeChild(progressBar); + logo = null; super.destroy(); } diff --git a/source/funkin/ui/transition/preload/VFDOverlay.hx b/source/funkin/ui/transition/preload/VFDOverlay.hx new file mode 100644 index 000000000..1792c56ec --- /dev/null +++ b/source/funkin/ui/transition/preload/VFDOverlay.hx @@ -0,0 +1,70 @@ +package funkin.ui.transition.preload; + +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import openfl.display.GraphicsShader; + +class VFDOverlay extends GraphicsShader +{ + public var elapsedTime(default, set):Float = 0; + + function set_elapsedTime(value:Float):Float + { + u_time.value = [value]; + return value; + } + + @:glFragmentSource('#pragma header + const vec2 s = vec2(1, 1.7320508); + + uniform float u_time; + + float rand(float co) { return fract(sin(co*(91.3458)) * 47453.5453); } + float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); } + + void main(void) { + vec4 col = texture2D (bitmap, openfl_TextureCoordv); + vec2 game_res = vec2(1280.0, 720.0); + const float tileAmount = 10.; + + vec2 uv = (2. * openfl_TextureCoordv.xy * -1.); + uv *= 50.; + + vec4 hexCenter = floor(vec4(uv, uv - vec2(0.5, 1.0)) / s.xyxy) + 0.5; + vec4 offset = vec4(uv - hexCenter.xy * s, uv - (hexCenter.zw + 0.5) * s) + 0.0; + vec4 hexInfo = dot(offset.xy, offset.xy) < dot(offset.zw, offset.zw) ? vec4(offset.xy, hexCenter.xy) : vec4(offset.zw, hexCenter.zw); + + // Distance to the nearest edge of a hexagon + vec2 p = abs(hexInfo.xy) ; + float edgeDist = max(dot(p, normalize(vec2(1.0, sqrt(3.0)))), p.x); + float edgeWidth = 0.05 * tileAmount; // Adjust edge width based on tile amount + float edgeSharpness = 0.011 * tileAmount; + + float outline = smoothstep(edgeWidth - edgeSharpness, edgeWidth, edgeDist); + float color_mix = mix(0.0, 0.3, outline); // Mix black outline with white fill + + float flicker = (sin(u_time) * 0.05) + 1.0; + float sinshit = smoothstep(-3.0, 1.0, sin(uv.y * 3.)); + + col = vec4(vec3(0.0), color_mix); + col = mix(col, vec4(0., 0., 0., sinshit), 0.5 * flicker); + + float specs = rand(uv.xy); + vec4 noise = vec4(0., 0., 0., specs); + col = mix(col, noise, 0.1); + + gl_FragColor = col; + } + ') + public function new() + { + super(); + + this.elapsedTime = 0; + } + + public function update(elapsed:Float):Void + { + this.elapsedTime += elapsed; + } +} diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 5d355f2da..75766a75a 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -60,6 +60,11 @@ class Constants */ // ============================== + /** + * Link to buy merch for the game. + */ + public static final URL_MERCH:String = 'https://needlejuicerecords.com/pages/friday-night-funkin'; + /** * Preloader sitelock. * Matching is done by `FlxStringUtil.getDomain`, so any URL on the domain will work. @@ -135,7 +140,7 @@ class Constants /** * Color for the preloader progress bar */ - public static final COLOR_PRELOADER_BAR:FlxColor = 0xFF00FF00; + public static final COLOR_PRELOADER_BAR:FlxColor = 0xFFA4FF11; /** * Color for the preloader site lock background @@ -181,6 +186,12 @@ class Constants */ public static final DEFAULT_DIFFICULTY_LIST:Array<String> = ['easy', 'normal', 'hard']; + /** + * List of all difficulties used by the base game. + * Includes Erect and Nightmare. + */ + public static final DEFAULT_DIFFICULTY_LIST_FULL:Array<String> = ['easy', 'normal', 'hard', 'erect', 'nightmare']; + /** * Default player character for charts. */ @@ -212,9 +223,10 @@ class Constants public static final DEFAULT_VARIATION_LIST:Array<String> = ['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). @@ -346,7 +358,7 @@ class Constants * The progress bare is automatically rescaled to match. */ #if debug - public static final PRELOADER_MIN_STAGE_TIME:Float = 1.0; + public static final PRELOADER_MIN_STAGE_TIME:Float = 0.0; #else public static final PRELOADER_MIN_STAGE_TIME:Float = 0.1; #end @@ -514,4 +526,10 @@ class Constants * The vertical offset of the strumline from the top edge of the screen. */ public static final STRUMLINE_Y_OFFSET:Float = 24; + + /** + * The rate at which the camera lerps to its target. + * 0.04 = 4% of distance per frame. + */ + public static final DEFAULT_CAMERA_FOLLOW_RATE:Float = 0.04; } 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/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx index c87d3f6c0..fa602cc73 100644 --- a/source/funkin/util/SerializerUtil.hx +++ b/source/funkin/util/SerializerUtil.hx @@ -63,6 +63,31 @@ class SerializerUtil } } + public static function initSerializer():Void + { + haxe.Unserializer.DEFAULT_RESOLVER = new FunkinTypeResolver(); + } + + /** + * Serialize a Haxe object using the built-in Serializer. + * @param input The object to serialize + * @return The serialized object as a string + */ + public static function fromHaxeObject(input:Dynamic):String + { + return haxe.Serializer.run(input); + } + + /** + * Convert a serialized Haxe object back into a Haxe object. + * @param input The serialized object as a string + * @return The deserialized object + */ + public static function toHaxeObject(input:String):Dynamic + { + return haxe.Unserializer.run(input); + } + /** * Customize how certain types are serialized when converting to JSON. */ @@ -90,3 +115,26 @@ class SerializerUtil return result; } } + +class FunkinTypeResolver +{ + public function new() + { + // Blank constructor. + } + + public function resolveClass(name:String):Class<Dynamic> + { + if (name == 'Dynamic') + { + FlxG.log.warn('Found invalid class type in save data, indicates partial save corruption.'); + return null; + } + return Type.resolveClass(name); + }; + + public function resolveEnum(name:String):Enum<Dynamic> + { + return Type.resolveEnum(name); + }; +} diff --git a/source/funkin/util/StructureUtil.hx b/source/funkin/util/StructureUtil.hx new file mode 100644 index 000000000..2f0c3818a --- /dev/null +++ b/source/funkin/util/StructureUtil.hx @@ -0,0 +1,136 @@ +package funkin.util; + +import funkin.util.tools.MapTools; +import haxe.DynamicAccess; + +/** + * Utilities for working with anonymous structures. + */ +class StructureUtil +{ + /** + * Merge two structures, with the second overwriting the first. + * Performs a SHALLOW clone, where child structures are not merged. + * @param a The base structure. + * @param b The new structure. + * @return The merged structure. + */ + public static function merge(a:Dynamic, b:Dynamic):Dynamic + { + var result:DynamicAccess<Dynamic> = Reflect.copy(a); + + for (field in Reflect.fields(b)) + { + result.set(field, Reflect.field(b, field)); + } + + return result; + } + + public static function toMap(a:Dynamic):haxe.ds.Map<String, Dynamic> + { + var result:haxe.ds.Map<String, Dynamic> = []; + + for (field in Reflect.fields(a)) + { + result.set(field, Reflect.field(a, field)); + } + + return result; + } + + public static function isMap(a:Dynamic):Bool + { + return Std.isOfType(a, haxe.Constraints.IMap); + } + + public static function isObject(a:Dynamic):Bool + { + switch (Type.typeof(a)) + { + case TObject: + return true; + default: + return false; + } + } + + public static function isPrimitive(a:Dynamic):Bool + { + switch (Type.typeof(a)) + { + case TInt | TFloat | TBool: + return true; + case TClass(c): + return false; + case TEnum(e): + return false; + case TObject: + return false; + case TFunction: + return false; + case TNull: + return true; + case TUnknown: + return false; + default: + return false; + } + } + + /** + * Merge two structures, with the second overwriting the first. + * Performs a DEEP clone, where child structures are also merged recursively. + * @param a The base structure. + * @param b The new structure. + * @return The merged structure. + */ + public static function deepMerge(a:Dynamic, b:Dynamic):Dynamic + { + if (a == null) return b; + if (b == null) return null; + if (isPrimitive(a) && isPrimitive(b)) return b; + if (isMap(b)) + { + if (isMap(a)) + { + return MapTools.merge(a, b); + } + else + { + return StructureUtil.toMap(a).merge(b); + } + } + if (!Reflect.isObject(a) || !Reflect.isObject(b)) return b; + if (Std.isOfType(b, haxe.ds.StringMap)) + { + if (Std.isOfType(a, haxe.ds.StringMap)) + { + return MapTools.merge(a, b); + } + else + { + return StructureUtil.toMap(a).merge(b); + } + } + + var result:DynamicAccess<Dynamic> = Reflect.copy(a); + + for (field in Reflect.fields(b)) + { + if (Reflect.isObject(b)) + { + // Note that isObject also returns true for class instances, + // but we just assume that's not a problem here. + result.set(field, deepMerge(Reflect.field(result, field), Reflect.field(b, field))); + } + else + { + // If we're here, b[field] is a primitive. + result.set(field, Reflect.field(b, field)); + } + } + + return result; + } +} diff --git a/source/funkin/util/tools/MapTools.hx b/source/funkin/util/tools/MapTools.hx index 1399fb791..b98cb0adf 100644 --- a/source/funkin/util/tools/MapTools.hx +++ b/source/funkin/util/tools/MapTools.hx @@ -33,6 +33,24 @@ class MapTools return map.copy(); } + /** + * Create a new map which is a combination of the two given maps. + * @param a The base map. + * @param b The other map. The values from this take precedence. + * @return The combined map. + */ + public static function merge<K, T>(a:Map<K, T>, b:Map<K, T>):Map<K, T> + { + var result = a.copy(); + + for (pair in b.keyValueIterator()) + { + result.set(pair.key, pair.value); + } + + return result; + } + /** * Create a new array with clones of all elements of the given array, to prevent modifying the original. */