mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-03-23 02:19:46 +00:00
Merge branch 'rewrite/master' into input-offsets
This commit is contained in:
commit
0f29f2f7a6
6
.github/actions/setup-haxeshit/action.yml
vendored
6
.github/actions/setup-haxeshit/action.yml
vendored
|
@ -3,9 +3,9 @@ description: "sets up haxe shit, using HMM!"
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- uses: krdlab/setup-haxe@v1.5.1
|
- uses: funkincrew/ci-haxe@v3
|
||||||
with:
|
with:
|
||||||
haxe-version: 4.3.1
|
haxe-version: 4.3.3
|
||||||
- name: Config haxelib
|
- name: Config haxelib
|
||||||
run: |
|
run: |
|
||||||
haxelib config
|
haxelib config
|
||||||
|
@ -19,7 +19,7 @@ runs:
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: dependency install cache
|
- name: dependency install cache
|
||||||
id: cache-hmm
|
id: cache-hmm
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: .haxelib
|
path: .haxelib
|
||||||
key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }}
|
key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }}
|
||||||
|
|
58
.github/actions/upload-itch/action.yml
vendored
58
.github/actions/upload-itch/action.yml
vendored
|
@ -13,32 +13,32 @@ inputs:
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Install butler Windows
|
- name: Install butler Windows
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default
|
curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default
|
||||||
7z x butler.zip
|
7z x butler.zip
|
||||||
./butler -v
|
./butler -v
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Install butler Mac
|
- name: Install butler Mac
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: |
|
||||||
curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default
|
curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default
|
||||||
unzip butler.zip
|
unzip butler.zip
|
||||||
./butler -V
|
./butler -V
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Install butler Linux
|
- name: Install butler Linux
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
|
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
|
||||||
unzip butler.zip
|
unzip butler.zip
|
||||||
chmod +x butler
|
chmod +x butler
|
||||||
./butler -V
|
./butler -V
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Upload game to itch.io
|
- name: Upload game to itch.io
|
||||||
env:
|
env:
|
||||||
BUTLER_API_KEY: ${{inputs.butler-key}}
|
BUTLER_API_KEY: ${{inputs.butler-key}}
|
||||||
run: |
|
run: |
|
||||||
./butler login
|
./butler login
|
||||||
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME}
|
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
151
.github/workflows/build-shit.yml
vendored
151
.github/workflows/build-shit.yml
vendored
|
@ -4,59 +4,33 @@ on:
|
||||||
push:
|
push:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_date:
|
|
||||||
runs-on: [self-hosted, linux]
|
|
||||||
container: ubuntu:latest
|
|
||||||
name: Check latest commit
|
|
||||||
outputs:
|
|
||||||
should_run: ${{ steps.should_run.outputs.should_run }}
|
|
||||||
steps:
|
|
||||||
- name: ensure git cli is installed
|
|
||||||
run: apt update && apt install sudo git -y
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: 'recursive'
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.GH_RO_PAT }}
|
|
||||||
- name: check whether submodules exist
|
|
||||||
run: |
|
|
||||||
git config --global --add safe.directory $GITHUB_WORKSPACE
|
|
||||||
|
|
||||||
# debug output
|
|
||||||
echo gh=${{ github.sha }}
|
|
||||||
echo head=$(git rev-parse HEAD)
|
|
||||||
echo art=$(git -C art rev-parse HEAD)
|
|
||||||
echo assets=$(git -C assets rev-parse HEAD)
|
|
||||||
|
|
||||||
# checks if HEAD commit hash in submodules is diff from current repo, and therefore exists
|
|
||||||
test $(git rev-parse HEAD) != $(git -C art rev-parse HEAD)
|
|
||||||
test $(git rev-parse HEAD) != $(git -C assets rev-parse HEAD)
|
|
||||||
- id: should_run
|
|
||||||
continue-on-error: true
|
|
||||||
name: check latest commit is less than a day
|
|
||||||
if: ${{ github.event_name == 'schedule' }}
|
|
||||||
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
|
|
||||||
create-nightly-html5:
|
create-nightly-html5:
|
||||||
needs: check_date
|
|
||||||
if: ${{ needs.check_date.outputs.should_run != 'false'}}
|
|
||||||
runs-on: [self-hosted, linux]
|
runs-on: [self-hosted, linux]
|
||||||
container: ubuntu:latest
|
container: ubuntu:23.10
|
||||||
steps:
|
steps:
|
||||||
- name: prepare container
|
- name: prepare container
|
||||||
run: |
|
run: |
|
||||||
apt update
|
apt update
|
||||||
apt install sudo git curl unzip -y
|
apt install sudo git curl unzip -y
|
||||||
echo $GITHUB_WORKSPACE
|
|
||||||
git config --global --add safe.directory $GITHUB_WORKSPACE
|
git config --global --add safe.directory $GITHUB_WORKSPACE
|
||||||
- uses: actions/checkout@v4
|
- name: get token from gh app
|
||||||
|
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:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
fetch-depth: 0
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
token: ${{ secrets.GH_RO_PAT }}
|
|
||||||
- uses: ./.github/actions/setup-haxeshit
|
- uses: ./.github/actions/setup-haxeshit
|
||||||
- name: Build game
|
- name: gather game dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
|
sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
|
||||||
|
- name: build game
|
||||||
|
run: |
|
||||||
haxelib run lime build html5 -release --times
|
haxelib run lime build html5 -release --times
|
||||||
ls
|
ls
|
||||||
- uses: ./.github/actions/upload-itch
|
- uses: ./.github/actions/upload-itch
|
||||||
|
@ -65,32 +39,34 @@ jobs:
|
||||||
build-dir: export/release/html5/bin
|
build-dir: export/release/html5/bin
|
||||||
target: html5
|
target: html5
|
||||||
create-nightly-win:
|
create-nightly-win:
|
||||||
needs: check_date
|
|
||||||
if: ${{ needs.check_date.outputs.should_run != 'false'}}
|
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
actions: write
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: get token from gh app
|
||||||
|
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:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
fetch-depth: 0
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
token: ${{ secrets.GH_RO_PAT }}
|
|
||||||
- uses: ./.github/actions/setup-haxeshit
|
- uses: ./.github/actions/setup-haxeshit
|
||||||
- name: Make HXCPP cache dir
|
- name: Make HXCPP cache dir
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ${{ runner.temp }}\hxcpp_cache
|
mkdir -p ${{ runner.temp }}\hxcpp_cache
|
||||||
- name: Restore build cache
|
- name: Restore build cache
|
||||||
id: cache-build-win
|
id: cache-build-win
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
.haxelib
|
.haxelib
|
||||||
export
|
export
|
||||||
${{ runner.temp }}\hxcpp_cache
|
${{ runner.temp }}\hxcpp_cache
|
||||||
key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
|
key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
|
||||||
- name: Build game
|
- name: build game
|
||||||
run: |
|
run: |
|
||||||
haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
|
haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
|
||||||
dir
|
dir
|
||||||
|
@ -101,20 +77,81 @@ jobs:
|
||||||
butler-key: ${{ secrets.BUTLER_API_KEY }}
|
butler-key: ${{ secrets.BUTLER_API_KEY }}
|
||||||
build-dir: export/release/windows/bin
|
build-dir: export/release/windows/bin
|
||||||
target: win
|
target: win
|
||||||
|
create-nightly-mac:
|
||||||
|
runs-on: [self-hosted, macos]
|
||||||
|
steps:
|
||||||
|
- name: prepare container
|
||||||
|
run: |
|
||||||
|
git config --global --add safe.directory $GITHUB_WORKSPACE
|
||||||
|
- name: get token from gh app
|
||||||
|
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 }}
|
||||||
|
- uses: ./.github/actions/setup-haxeshit
|
||||||
|
- name: Make HXCPP cache dir
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ runner.temp }}/hxcpp_cache
|
||||||
|
- name: restore build cache
|
||||||
|
id: cache-build-win
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.haxelib
|
||||||
|
export
|
||||||
|
${{ runner.temp }}/hxcpp_cache
|
||||||
|
key: ${{ runner.os }}-build-mac-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
|
||||||
|
- name: Build game
|
||||||
|
run: |
|
||||||
|
haxelib run lime build macos -release --times
|
||||||
|
ls
|
||||||
|
env:
|
||||||
|
HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache"
|
||||||
|
- uses: ./.github/actions/upload-itch
|
||||||
|
with:
|
||||||
|
butler-key: ${{ secrets.BUTLER_API_KEY}}
|
||||||
|
build-dir: export/release/macos/bin
|
||||||
|
target: macos
|
||||||
|
|
||||||
# test-unit-win:
|
# test-unit-win:
|
||||||
# needs: create-nightly-win
|
# needs: create-nightly-win
|
||||||
# runs-on: windows-latest
|
# runs-on: windows-latest
|
||||||
# permissions:
|
|
||||||
# contents: write
|
|
||||||
# actions: write
|
|
||||||
# steps:
|
# steps:
|
||||||
# - uses: actions/checkout@v4
|
# - name: get token from gh app
|
||||||
|
# uses: actions/create-github-app-token@v1
|
||||||
|
# id: app_token
|
||||||
# with:
|
# with:
|
||||||
# submodules: 'recursive'
|
# app-id: ${{ vars.APP_ID }}
|
||||||
# fetch-depth: 0
|
# private-key: ${{ secrets.APP_PEM }}
|
||||||
# token: ${{ secrets.GH_RO_PAT }}
|
# owner: ${{ github.repository_owner }}
|
||||||
|
# - name: checkout repo
|
||||||
|
# uses: funkincrew/ci-checkout@v6
|
||||||
|
# with:
|
||||||
|
# submodules: 'recursive'
|
||||||
|
# token: ${{ steps.app_token.outputs.token }}
|
||||||
|
# - name: Make HXCPP cache dir
|
||||||
|
# run: |
|
||||||
|
# mkdir -p ${{ runner.temp }}\hxcpp_cache
|
||||||
|
# - name: Restore build cache
|
||||||
|
# id: cache-build-win
|
||||||
|
# uses: actions/cache@v4
|
||||||
|
# with:
|
||||||
|
# path: |
|
||||||
|
# .haxelib
|
||||||
|
# export
|
||||||
|
# ${{ runner.temp }}\hxcpp_cache
|
||||||
|
# key: ${{ runner.os }}-test-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
|
||||||
# - uses: ./.github/actions/setup-haxeshit
|
# - uses: ./.github/actions/setup-haxeshit
|
||||||
# - name: Run unit tests
|
# - name: Run unit tests
|
||||||
# run: |
|
# run: |
|
||||||
# cd ./tests/unit/
|
# cd ./tests/unit/
|
||||||
# ./start-win-native.bat
|
# ./start-win-native.bat
|
||||||
|
# env:
|
||||||
|
# HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
|
||||||
|
|
|
@ -111,6 +111,7 @@
|
||||||
<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
|
<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
|
||||||
<haxelib name="thx.semver" /> <!-- Version string handling -->
|
<haxelib name="thx.semver" /> <!-- Version string handling -->
|
||||||
|
|
||||||
|
<haxelib name="hmm" /> <!-- Read library version data at compile time so it can be baked into logs -->
|
||||||
<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
|
<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
|
||||||
|
|
||||||
<!--Disable the Flixel core focus lost screen-->
|
<!--Disable the Flixel core focus lost screen-->
|
||||||
|
@ -130,8 +131,8 @@
|
||||||
<haxedef name="message.reporting" value="pretty" />
|
<haxedef name="message.reporting" value="pretty" />
|
||||||
|
|
||||||
<!-- _________________________________ Custom _______________________________ -->
|
<!-- _________________________________ Custom _______________________________ -->
|
||||||
<!-- Disable trace() calls in release builds to bump up performance. -->
|
<!-- Disable trace() calls in release builds to bump up performance.
|
||||||
<haxeflag name="--no-traces" unless="debug" />
|
<haxeflag name="- -no-traces" unless="debug" />-->
|
||||||
<!-- HScript relies heavily on Reflection, which means we can't use DCE. -->
|
<!-- HScript relies heavily on Reflection, which means we can't use DCE. -->
|
||||||
<haxeflag name="-dce no" />
|
<haxeflag name="-dce no" />
|
||||||
<!-- Ensure all Funkin' classes are available at runtime. -->
|
<!-- Ensure all Funkin' classes are available at runtime. -->
|
||||||
|
|
12
hmm.json
12
hmm.json
|
@ -11,7 +11,7 @@
|
||||||
"name": "flixel",
|
"name": "flixel",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"dir": null,
|
"dir": null,
|
||||||
"ref": "a83738673e7edbf8acba3a1426af284dfe6719fe",
|
"ref": "07c6018008801972d12275690fc144fcc22e3de6",
|
||||||
"url": "https://github.com/FunkinCrew/flixel"
|
"url": "https://github.com/FunkinCrew/flixel"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
"name": "flxanimate",
|
"name": "flxanimate",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"dir": null,
|
"dir": null,
|
||||||
"ref": "d7c5621be742e2c98d523dfe5af7528835eaff1e",
|
"ref": "9bacdd6ea39f5e3a33b0f5dfb7bc583fe76060d4",
|
||||||
"url": "https://github.com/FunkinCrew/flxanimate"
|
"url": "https://github.com/FunkinCrew/flxanimate"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -54,14 +54,14 @@
|
||||||
"name": "haxeui-core",
|
"name": "haxeui-core",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"dir": null,
|
"dir": null,
|
||||||
"ref": "5086e59e7551d775ed4d1fb0188e31de22d1312b",
|
"ref": "5b2d5b8e7e470cf637953e1369c80a1f42016a75",
|
||||||
"url": "https://github.com/haxeui/haxeui-core"
|
"url": "https://github.com/haxeui/haxeui-core"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "haxeui-flixel",
|
"name": "haxeui-flixel",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"dir": null,
|
"dir": null,
|
||||||
"ref": "2b9cff727999b53ed292b1675ac1c9089ac77600",
|
"ref": "e9f880522e27134b29df4067f82df7d7e5237b70",
|
||||||
"url": "https://github.com/haxeui/haxeui-flixel"
|
"url": "https://github.com/haxeui/haxeui-flixel"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
"name": "lime",
|
"name": "lime",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"dir": null,
|
"dir": null,
|
||||||
"ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
|
"ref": "fff39ba6fc64969cd51987ef7491d9345043dc5d",
|
||||||
"url": "https://github.com/FunkinCrew/lime"
|
"url": "https://github.com/FunkinCrew/lime"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -149,7 +149,7 @@
|
||||||
"name": "polymod",
|
"name": "polymod",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"dir": null,
|
"dir": null,
|
||||||
"ref": "80d1d309803c1b111866524f9769325e3b8b0b1b",
|
"ref": "cb11a95d0159271eb3587428cf4b9602e46dc469",
|
||||||
"url": "https://github.com/larsiusprime/polymod"
|
"url": "https://github.com/larsiusprime/polymod"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -21,7 +21,6 @@ abstract Tallies(RawTallies)
|
||||||
bad: 0,
|
bad: 0,
|
||||||
good: 0,
|
good: 0,
|
||||||
sick: 0,
|
sick: 0,
|
||||||
killer: 0,
|
|
||||||
totalNotes: 0,
|
totalNotes: 0,
|
||||||
totalNotesHit: 0,
|
totalNotesHit: 0,
|
||||||
maxCombo: 0,
|
maxCombo: 0,
|
||||||
|
@ -43,7 +42,6 @@ typedef RawTallies =
|
||||||
var bad:Int;
|
var bad:Int;
|
||||||
var good:Int;
|
var good:Int;
|
||||||
var sick:Int;
|
var sick:Int;
|
||||||
var killer:Int;
|
|
||||||
var maxCombo:Int;
|
var maxCombo:Int;
|
||||||
var isNewHighscore:Bool;
|
var isNewHighscore:Bool;
|
||||||
|
|
||||||
|
|
|
@ -20,11 +20,11 @@ import openfl.display.BitmapData;
|
||||||
import funkin.data.level.LevelRegistry;
|
import funkin.data.level.LevelRegistry;
|
||||||
import funkin.data.notestyle.NoteStyleRegistry;
|
import funkin.data.notestyle.NoteStyleRegistry;
|
||||||
import funkin.data.event.SongEventRegistry;
|
import funkin.data.event.SongEventRegistry;
|
||||||
|
import funkin.data.stage.StageRegistry;
|
||||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||||
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
||||||
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
||||||
import funkin.data.song.SongRegistry;
|
import funkin.data.song.SongRegistry;
|
||||||
import funkin.play.stage.StageData.StageDataParser;
|
|
||||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||||
import funkin.modding.module.ModuleHandler;
|
import funkin.modding.module.ModuleHandler;
|
||||||
import funkin.ui.title.TitleState;
|
import funkin.ui.title.TitleState;
|
||||||
|
@ -217,8 +217,9 @@ class InitState extends FlxState
|
||||||
ConversationDataParser.loadConversationCache();
|
ConversationDataParser.loadConversationCache();
|
||||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||||
SpeakerDataParser.loadSpeakerCache();
|
SpeakerDataParser.loadSpeakerCache();
|
||||||
StageDataParser.loadStageCache();
|
StageRegistry.instance.loadEntries();
|
||||||
CharacterDataParser.loadCharacterCache();
|
CharacterDataParser.loadCharacterCache();
|
||||||
|
|
||||||
ModuleHandler.buildModuleCallbacks();
|
ModuleHandler.buildModuleCallbacks();
|
||||||
ModuleHandler.loadModuleCache();
|
ModuleHandler.loadModuleCache();
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,26 @@ class FunkinSound extends FlxSound
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user clicks to focus on the window.
|
||||||
|
*/
|
||||||
|
override function onFocus():Void
|
||||||
|
{
|
||||||
|
if (!_alreadyPaused && this._shouldPlay)
|
||||||
|
{
|
||||||
|
resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user tabs away from the window.
|
||||||
|
*/
|
||||||
|
override function onFocusLost():Void
|
||||||
|
{
|
||||||
|
_alreadyPaused = _paused;
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
|
||||||
public override function resume():FunkinSound
|
public override function resume():FunkinSound
|
||||||
{
|
{
|
||||||
if (this._time < 0)
|
if (this._time < 0)
|
||||||
|
|
|
@ -132,6 +132,12 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override function destroy()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
super.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all sounds from the group.
|
* Remove all sounds from the group.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -8,8 +8,7 @@ class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
|
||||||
{
|
{
|
||||||
public var playerVis:PolygonSpectogram;
|
public var playerVis:PolygonSpectogram;
|
||||||
public var opponentVis:PolygonSpectogram;
|
public var opponentVis:PolygonSpectogram;
|
||||||
|
public var instVis:PolygonSpectogram;
|
||||||
var instVis:PolygonSpectogram;
|
|
||||||
|
|
||||||
public function new()
|
public function new()
|
||||||
{
|
{
|
||||||
|
@ -51,6 +50,43 @@ class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
|
||||||
instVis = vis;
|
instVis = vis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function clearPlayerVis():Void
|
||||||
|
{
|
||||||
|
if (playerVis != null)
|
||||||
|
{
|
||||||
|
remove(playerVis);
|
||||||
|
playerVis.destroy();
|
||||||
|
playerVis = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearOpponentVis():Void
|
||||||
|
{
|
||||||
|
if (opponentVis != null)
|
||||||
|
{
|
||||||
|
remove(opponentVis);
|
||||||
|
opponentVis.destroy();
|
||||||
|
opponentVis = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearInstVis():Void
|
||||||
|
{
|
||||||
|
if (instVis != null)
|
||||||
|
{
|
||||||
|
remove(instVis);
|
||||||
|
instVis.destroy();
|
||||||
|
instVis = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearAllVis():Void
|
||||||
|
{
|
||||||
|
clearPlayerVis();
|
||||||
|
clearOpponentVis();
|
||||||
|
clearInstVis();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides the add function to add a visualizer to the group.
|
* Overrides the add function to add a visualizer to the group.
|
||||||
* @param vis The visualizer to add.
|
* @param vis The visualizer to add.
|
||||||
|
|
0
source/funkin/data/character/TODO.md
Normal file
0
source/funkin/data/character/TODO.md
Normal file
0
source/funkin/data/conversation/TODO.md
Normal file
0
source/funkin/data/conversation/TODO.md
Normal file
0
source/funkin/data/dialogue/TODO.md
Normal file
0
source/funkin/data/dialogue/TODO.md
Normal file
|
@ -15,7 +15,7 @@ abstract SongEventSchema(SongEventSchemaRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
@:arrayAccess
|
@:arrayAccess
|
||||||
public inline function getByName(name:String):SongEventSchemaField
|
public function getByName(name:String):SongEventSchemaField
|
||||||
{
|
{
|
||||||
for (field in this)
|
for (field in this)
|
||||||
{
|
{
|
||||||
|
@ -41,6 +41,32 @@ abstract SongEventSchema(SongEventSchemaRaw)
|
||||||
{
|
{
|
||||||
return this[k] = v;
|
return this[k] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function stringifyFieldValue(name:String, value:Dynamic):String
|
||||||
|
{
|
||||||
|
var field:SongEventSchemaField = getByName(name);
|
||||||
|
if (field == null) return 'Unknown';
|
||||||
|
|
||||||
|
switch (field.type)
|
||||||
|
{
|
||||||
|
case SongEventFieldType.STRING:
|
||||||
|
return Std.string(value);
|
||||||
|
case SongEventFieldType.INTEGER:
|
||||||
|
return Std.string(value);
|
||||||
|
case SongEventFieldType.FLOAT:
|
||||||
|
return Std.string(value);
|
||||||
|
case SongEventFieldType.BOOL:
|
||||||
|
return Std.string(value);
|
||||||
|
case SongEventFieldType.ENUM:
|
||||||
|
for (key in field.keys.keys())
|
||||||
|
{
|
||||||
|
if (field.keys.get(key) == value) return key;
|
||||||
|
}
|
||||||
|
return Std.string(value);
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
|
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
|
||||||
|
|
|
@ -7,9 +7,9 @@ import funkin.ui.story.ScriptedLevel;
|
||||||
class LevelRegistry extends BaseRegistry<Level, LevelData>
|
class LevelRegistry extends BaseRegistry<Level, LevelData>
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The current version string for the stage data format.
|
* The current version string for the level data format.
|
||||||
* Handle breaking changes by incrementing this value
|
* Handle breaking changes by incrementing this value
|
||||||
* and adding migration to the `migrateStageData()` function.
|
* and adding migration to the `migrateLevelData()` function.
|
||||||
*/
|
*/
|
||||||
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.0";
|
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.0";
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package funkin.data.song;
|
package funkin.data.song;
|
||||||
|
|
||||||
import funkin.data.event.SongEventRegistry;
|
import funkin.data.event.SongEventRegistry;
|
||||||
|
import funkin.play.event.SongEvent;
|
||||||
import funkin.data.event.SongEventSchema;
|
import funkin.data.event.SongEventSchema;
|
||||||
import funkin.data.song.SongRegistry;
|
import funkin.data.song.SongRegistry;
|
||||||
import thx.semver.Version;
|
import thx.semver.Version;
|
||||||
|
@ -38,10 +39,11 @@ class SongMetadata implements ICloneable<SongMetadata>
|
||||||
public var looped:Bool;
|
public var looped:Bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumental and vocal offsets. Optional, defaults to 0.
|
* Instrumental and vocal offsets.
|
||||||
|
* Defaults to an empty SongOffsets object.
|
||||||
*/
|
*/
|
||||||
@:optional
|
@:optional
|
||||||
public var offsets:SongOffsets;
|
public var offsets:Null<SongOffsets>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data relating to the song's gameplay.
|
* Data relating to the song's gameplay.
|
||||||
|
@ -93,7 +95,7 @@ class SongMetadata implements ICloneable<SongMetadata>
|
||||||
result.version = this.version;
|
result.version = this.version;
|
||||||
result.timeFormat = this.timeFormat;
|
result.timeFormat = this.timeFormat;
|
||||||
result.divisions = this.divisions;
|
result.divisions = this.divisions;
|
||||||
result.offsets = this.offsets.clone();
|
result.offsets = this.offsets != null ? this.offsets.clone() : new SongOffsets(); // if no song offsets found (aka null), so just create new ones
|
||||||
result.timeChanges = this.timeChanges.deepClone();
|
result.timeChanges = this.timeChanges.deepClone();
|
||||||
result.looped = this.looped;
|
result.looped = this.looped;
|
||||||
result.playData = this.playData.clone();
|
result.playData = this.playData.clone();
|
||||||
|
@ -701,6 +703,11 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public inline function getHandler():Null<SongEvent>
|
||||||
|
{
|
||||||
|
return SongEventRegistry.getEvent(this.event);
|
||||||
|
}
|
||||||
|
|
||||||
public inline function getSchema():Null<SongEventSchema>
|
public inline function getSchema():Null<SongEventSchema>
|
||||||
{
|
{
|
||||||
return SongEventRegistry.getEventSchema(this.event);
|
return SongEventRegistry.getEventSchema(this.event);
|
||||||
|
@ -751,6 +758,39 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
||||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function buildTooltip():String
|
||||||
|
{
|
||||||
|
var eventHandler = getHandler();
|
||||||
|
var eventSchema = getSchema();
|
||||||
|
|
||||||
|
if (eventSchema == null) return 'Unknown Event: ${this.event}';
|
||||||
|
|
||||||
|
var result = '${eventHandler.getTitle()}';
|
||||||
|
|
||||||
|
var defaultKey = eventSchema.getFirstField()?.name;
|
||||||
|
var valueStruct:haxe.DynamicAccess<Dynamic> = valueAsStruct(defaultKey);
|
||||||
|
|
||||||
|
for (pair in valueStruct.keyValueIterator())
|
||||||
|
{
|
||||||
|
var key = pair.key;
|
||||||
|
var value = pair.value;
|
||||||
|
|
||||||
|
var title = eventSchema.getByName(key)?.title ?? 'UnknownField';
|
||||||
|
|
||||||
|
if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value));
|
||||||
|
var valueStr = eventSchema.stringifyFieldValue(key, value) ?? 'UnknownValue';
|
||||||
|
|
||||||
|
result += '\n- ${title}: ${valueStr}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clone():SongEventData
|
||||||
|
{
|
||||||
|
return new SongEventData(this.time, this.event, this.value);
|
||||||
|
}
|
||||||
|
|
||||||
@:op(A == B)
|
@:op(A == B)
|
||||||
public function op_equals(other:SongEventData):Bool
|
public function op_equals(other:SongEventData):Bool
|
||||||
{
|
{
|
||||||
|
@ -826,7 +866,13 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||||
@:alias("l")
|
@:alias("l")
|
||||||
@:default(0)
|
@:default(0)
|
||||||
@:optional
|
@:optional
|
||||||
public var length:Float;
|
public var length(default, set):Float;
|
||||||
|
|
||||||
|
function set_length(value:Float):Float
|
||||||
|
{
|
||||||
|
_stepLength = null;
|
||||||
|
return length = value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The kind of the note.
|
* The kind of the note.
|
||||||
|
@ -883,6 +929,11 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||||
return _stepTime = Conductor.instance.getTimeInSteps(this.time);
|
return _stepTime = Conductor.instance.getTimeInSteps(this.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The length of the note, if applicable, in steps.
|
||||||
|
* Calculated from the length and the BPM.
|
||||||
|
* Cached for performance. Set to `null` to recalculate.
|
||||||
|
*/
|
||||||
@:jignored
|
@:jignored
|
||||||
var _stepLength:Null<Float> = null;
|
var _stepLength:Null<Float> = null;
|
||||||
|
|
||||||
|
@ -907,9 +958,14 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var lengthMs:Float = Conductor.instance.getStepTimeInMs(value) - this.time;
|
var endStep:Float = getStepTime() + value;
|
||||||
|
var endMs:Float = Conductor.instance.getStepTimeInMs(endStep);
|
||||||
|
var lengthMs:Float = endMs - this.time;
|
||||||
|
|
||||||
this.length = lengthMs;
|
this.length = lengthMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recalculate the step length next time it's requested.
|
||||||
_stepLength = null;
|
_stepLength = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -980,6 +1036,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
||||||
@:op(A == B)
|
@:op(A == B)
|
||||||
public function op_equals(other:SongNoteData):Bool
|
public function op_equals(other:SongNoteData):Bool
|
||||||
{
|
{
|
||||||
|
// Handle the case where one value is null.
|
||||||
|
if (this == null) return other == null;
|
||||||
|
if (other == null) return false;
|
||||||
|
|
||||||
if (this.kind == '')
|
if (this.kind == '')
|
||||||
{
|
{
|
||||||
if (other.kind != '' && other.kind != 'normal') return false;
|
if (other.kind != '' && other.kind != 'normal') return false;
|
||||||
|
@ -995,6 +1055,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
||||||
@:op(A != B)
|
@:op(A != B)
|
||||||
public function op_notEquals(other:SongNoteData):Bool
|
public function op_notEquals(other:SongNoteData):Bool
|
||||||
{
|
{
|
||||||
|
// Handle the case where one value is null.
|
||||||
|
if (this == null) return other == null;
|
||||||
|
if (other == null) return false;
|
||||||
|
|
||||||
if (this.kind == '')
|
if (this.kind == '')
|
||||||
{
|
{
|
||||||
if (other.kind != '' && other.kind != 'normal') return true;
|
if (other.kind != '' && other.kind != 'normal') return true;
|
||||||
|
@ -1010,24 +1074,32 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
||||||
@:op(A > B)
|
@:op(A > B)
|
||||||
public function op_greaterThan(other:SongNoteData):Bool
|
public function op_greaterThan(other:SongNoteData):Bool
|
||||||
{
|
{
|
||||||
|
if (other == null) return false;
|
||||||
|
|
||||||
return this.time > other.time;
|
return this.time > other.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
@:op(A < B)
|
@:op(A < B)
|
||||||
public function op_lessThan(other:SongNoteData):Bool
|
public function op_lessThan(other:SongNoteData):Bool
|
||||||
{
|
{
|
||||||
|
if (other == null) return false;
|
||||||
|
|
||||||
return this.time < other.time;
|
return this.time < other.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
@:op(A >= B)
|
@:op(A >= B)
|
||||||
public function op_greaterThanOrEquals(other:SongNoteData):Bool
|
public function op_greaterThanOrEquals(other:SongNoteData):Bool
|
||||||
{
|
{
|
||||||
|
if (other == null) return false;
|
||||||
|
|
||||||
return this.time >= other.time;
|
return this.time >= other.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
@:op(A <= B)
|
@:op(A <= B)
|
||||||
public function op_lessThanOrEquals(other:SongNoteData):Bool
|
public function op_lessThanOrEquals(other:SongNoteData):Bool
|
||||||
{
|
{
|
||||||
|
if (other == null) return false;
|
||||||
|
|
||||||
return this.time <= other.time;
|
return this.time <= other.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -273,7 +273,7 @@ class SongDataUtils
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter a list of notes to only include notes whose data is within the given range.
|
* Filter a list of notes to only include notes whose data is within the given range, inclusive.
|
||||||
*/
|
*/
|
||||||
public static function getNotesInDataRange(notes:Array<SongNoteData>, start:Int, end:Int):Array<SongNoteData>
|
public static function getNotesInDataRange(notes:Array<SongNoteData>, start:Int, end:Int):Array<SongNoteData>
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,6 +12,7 @@ import funkin.util.VersionUtil;
|
||||||
|
|
||||||
using funkin.data.song.migrator.SongDataMigrator;
|
using funkin.data.song.migrator.SongDataMigrator;
|
||||||
|
|
||||||
|
@:nullSafety
|
||||||
class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +32,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
|
|
||||||
public static final SONG_MUSIC_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
|
public static final SONG_MUSIC_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
|
||||||
|
|
||||||
public static var DEFAULT_GENERATEDBY(get, null):String;
|
public static var DEFAULT_GENERATEDBY(get, never):String;
|
||||||
|
|
||||||
static function get_DEFAULT_GENERATEDBY():String
|
static function get_DEFAULT_GENERATEDBY():String
|
||||||
{
|
{
|
||||||
|
@ -88,7 +89,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var entry:Song = createEntry(entryId);
|
var entry:Null<Song> = createEntry(entryId);
|
||||||
if (entry != null)
|
if (entry != null)
|
||||||
{
|
{
|
||||||
trace(' Loaded entry data: ${entry}');
|
trace(' Loaded entry data: ${entry}');
|
||||||
|
@ -126,7 +127,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||||
|
|
||||||
var parser = new json2object.JsonParser<SongMetadata>();
|
var parser = new json2object.JsonParser<SongMetadata>();
|
||||||
parser.ignoreUnknownVariables = false;
|
parser.ignoreUnknownVariables = true;
|
||||||
|
|
||||||
switch (loadEntryMetadataFile(id, variation))
|
switch (loadEntryMetadataFile(id, variation))
|
||||||
{
|
{
|
||||||
|
@ -149,7 +150,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||||
|
|
||||||
var parser = new json2object.JsonParser<SongMetadata>();
|
var parser = new json2object.JsonParser<SongMetadata>();
|
||||||
parser.ignoreUnknownVariables = false;
|
parser.ignoreUnknownVariables = true;
|
||||||
parser.fromJson(contents, fileName);
|
parser.fromJson(contents, fileName);
|
||||||
|
|
||||||
if (parser.errors.length > 0)
|
if (parser.errors.length > 0)
|
||||||
|
@ -209,7 +210,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||||
|
|
||||||
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
||||||
parser.ignoreUnknownVariables = false;
|
parser.ignoreUnknownVariables = true;
|
||||||
|
|
||||||
switch (loadEntryMetadataFile(id, variation))
|
switch (loadEntryMetadataFile(id, variation))
|
||||||
{
|
{
|
||||||
|
@ -231,7 +232,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||||
|
|
||||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||||
parser.ignoreUnknownVariables = false;
|
parser.ignoreUnknownVariables = true;
|
||||||
|
|
||||||
switch (loadEntryMetadataFile(id, variation))
|
switch (loadEntryMetadataFile(id, variation))
|
||||||
{
|
{
|
||||||
|
@ -251,7 +252,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||||
{
|
{
|
||||||
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
||||||
parser.ignoreUnknownVariables = false;
|
parser.ignoreUnknownVariables = true;
|
||||||
parser.fromJson(contents, fileName);
|
parser.fromJson(contents, fileName);
|
||||||
|
|
||||||
if (parser.errors.length > 0)
|
if (parser.errors.length > 0)
|
||||||
|
@ -265,7 +266,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||||
{
|
{
|
||||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||||
parser.ignoreUnknownVariables = false;
|
parser.ignoreUnknownVariables = true;
|
||||||
parser.fromJson(contents, fileName);
|
parser.fromJson(contents, fileName);
|
||||||
|
|
||||||
if (parser.errors.length > 0)
|
if (parser.errors.length > 0)
|
||||||
|
@ -346,7 +347,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||||
|
|
||||||
var parser = new json2object.JsonParser<SongChartData>();
|
var parser = new json2object.JsonParser<SongChartData>();
|
||||||
parser.ignoreUnknownVariables = false;
|
parser.ignoreUnknownVariables = true;
|
||||||
|
|
||||||
switch (loadEntryChartFile(id, variation))
|
switch (loadEntryChartFile(id, variation))
|
||||||
{
|
{
|
||||||
|
@ -369,7 +370,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||||
|
|
||||||
var parser = new json2object.JsonParser<SongChartData>();
|
var parser = new json2object.JsonParser<SongChartData>();
|
||||||
parser.ignoreUnknownVariables = false;
|
parser.ignoreUnknownVariables = true;
|
||||||
parser.fromJson(contents, fileName);
|
parser.fromJson(contents, fileName);
|
||||||
|
|
||||||
if (parser.errors.length > 0)
|
if (parser.errors.length > 0)
|
||||||
|
@ -455,7 +456,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
{
|
{
|
||||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||||
var entryStr:Null<String> = loadEntryMetadataFile(id, variation)?.contents;
|
var entryStr:Null<String> = loadEntryMetadataFile(id, variation)?.contents;
|
||||||
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
|
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
|
||||||
return entryVersion;
|
return entryVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,7 +464,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||||
{
|
{
|
||||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||||
var entryStr:Null<String> = loadEntryChartFile(id, variation)?.contents;
|
var entryStr:Null<String> = loadEntryChartFile(id, variation)?.contents;
|
||||||
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
|
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
|
||||||
return entryVersion;
|
return entryVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
0
source/funkin/data/speaker/TODO.md
Normal file
0
source/funkin/data/speaker/TODO.md
Normal file
199
source/funkin/data/stage/StageData.hx
Normal file
199
source/funkin/data/stage/StageData.hx
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
package funkin.data.stage;
|
||||||
|
|
||||||
|
import funkin.data.animation.AnimationData;
|
||||||
|
|
||||||
|
@:nullSafety
|
||||||
|
class StageData
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The sematic version number of the stage data JSON format.
|
||||||
|
* Supports fancy comparisons like NPM does it's neat.
|
||||||
|
*/
|
||||||
|
@:default(funkin.data.stage.StageRegistry.STAGE_DATA_VERSION)
|
||||||
|
public var version:String;
|
||||||
|
|
||||||
|
public var name:String = 'Unknown';
|
||||||
|
public var props:Array<StageDataProp> = [];
|
||||||
|
public var characters:StageDataCharacters;
|
||||||
|
|
||||||
|
@:default(1.0)
|
||||||
|
@:optional
|
||||||
|
public var cameraZoom:Null<Float>;
|
||||||
|
|
||||||
|
public function new()
|
||||||
|
{
|
||||||
|
this.version = StageRegistry.STAGE_DATA_VERSION;
|
||||||
|
this.characters = makeDefaultCharacters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDefaultCharacters():StageDataCharacters
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
bf:
|
||||||
|
{
|
||||||
|
zIndex: 0,
|
||||||
|
position: [0, 0],
|
||||||
|
cameraOffsets: [-100, -100]
|
||||||
|
},
|
||||||
|
dad:
|
||||||
|
{
|
||||||
|
zIndex: 0,
|
||||||
|
position: [0, 0],
|
||||||
|
cameraOffsets: [100, -100]
|
||||||
|
},
|
||||||
|
gf:
|
||||||
|
{
|
||||||
|
zIndex: 0,
|
||||||
|
position: [0, 0],
|
||||||
|
cameraOffsets: [0, 0]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this StageData into a JSON string.
|
||||||
|
*/
|
||||||
|
public function serialize(pretty:Bool = true):String
|
||||||
|
{
|
||||||
|
var writer = new json2object.JsonWriter<StageData>();
|
||||||
|
return writer.write(this, pretty ? ' ' : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef StageDataCharacters =
|
||||||
|
{
|
||||||
|
var bf:StageDataCharacter;
|
||||||
|
var dad:StageDataCharacter;
|
||||||
|
var gf:StageDataCharacter;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef StageDataProp =
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name of the prop for later lookup by scripts.
|
||||||
|
* Optional; if unspecified, the prop can't be referenced by scripts.
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
var name:String;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The asset used to display the prop.
|
||||||
|
* NOTE: As of Stage data v1.0.1, you can also use a color here to create a rectangle, like "#ff0000".
|
||||||
|
* In this case, the `scale` property will be used to determine the size of the prop.
|
||||||
|
*/
|
||||||
|
var assetPath:String;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position of the prop as an [x, y] array of two floats.
|
||||||
|
*/
|
||||||
|
var position:Array<Float>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A number determining the stack order of the prop, relative to other props and the characters in the stage.
|
||||||
|
* Props with lower numbers render below those with higher numbers.
|
||||||
|
* This is just like CSS, it isn't hard.
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
@:default(0)
|
||||||
|
var zIndex:Int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to true, anti-aliasing will be forcibly disabled on the sprite.
|
||||||
|
* This prevents blurry images on pixel-art levels.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
@:default(false)
|
||||||
|
var isPixel:Bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
|
||||||
|
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
|
||||||
|
*/
|
||||||
|
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
|
||||||
|
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
|
||||||
|
@:optional
|
||||||
|
var scale:haxe.ds.Either<Float, Array<Float>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The alpha of the prop, as a float.
|
||||||
|
* @default 1.0
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
@:default(1.0)
|
||||||
|
var alpha:Float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If not zero, this prop will play an animation every X beats of the song.
|
||||||
|
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
|
||||||
|
* they will alternated between, otherwise the `idle` animation will be used.
|
||||||
|
*
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
@:default(0)
|
||||||
|
@:optional
|
||||||
|
var danceEvery:Int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
|
||||||
|
* Represented as an [x, y] array of two floats.
|
||||||
|
* [1, 1] means the prop moves 1:1 with the camera.
|
||||||
|
* [0.5, 0.5] means the prop half as much as the camera.
|
||||||
|
* [0, 0] means the prop is not moved.
|
||||||
|
* @default [0, 0]
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
@:default([0, 0])
|
||||||
|
var scroll:Array<Float>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional array of animations which the prop can play.
|
||||||
|
* @default Prop has no animations.
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
@:default([])
|
||||||
|
var animations:Array<AnimationData>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If animations are used, this is the name of the animation to play first.
|
||||||
|
* @default Don't play an animation.
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
var startingAnimation:Null<String>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The animation type to use.
|
||||||
|
* Options: "sparrow", "packer"
|
||||||
|
* @default "sparrow"
|
||||||
|
*/
|
||||||
|
@:default("sparrow")
|
||||||
|
@:optional
|
||||||
|
var animType:String;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef StageDataCharacter =
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* A number determining the stack order of the character, relative to props and other characters in the stage.
|
||||||
|
* Again, just like CSS.
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
@:default(0)
|
||||||
|
var zIndex:Int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position to render the character at.
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
@:default([0, 0])
|
||||||
|
var position:Array<Float>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The camera offsets to apply when focusing on the character on this stage.
|
||||||
|
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
var cameraOffsets:Array<Float>;
|
||||||
|
};
|
103
source/funkin/data/stage/StageRegistry.hx
Normal file
103
source/funkin/data/stage/StageRegistry.hx
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package funkin.data.stage;
|
||||||
|
|
||||||
|
import funkin.data.stage.StageData;
|
||||||
|
import funkin.play.stage.Stage;
|
||||||
|
import funkin.play.stage.ScriptedStage;
|
||||||
|
|
||||||
|
class StageRegistry extends BaseRegistry<Stage, StageData>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current version string for the stage data format.
|
||||||
|
* Handle breaking changes by incrementing this value
|
||||||
|
* and adding migration to the `migrateStageData()` function.
|
||||||
|
*/
|
||||||
|
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.1";
|
||||||
|
|
||||||
|
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
|
||||||
|
|
||||||
|
public static final instance:StageRegistry = new StageRegistry();
|
||||||
|
|
||||||
|
public function new()
|
||||||
|
{
|
||||||
|
super('STAGE', 'stages', STAGE_DATA_VERSION_RULE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||||
|
*/
|
||||||
|
public function parseEntryData(id:String):Null<StageData>
|
||||||
|
{
|
||||||
|
// JsonParser does not take type parameters,
|
||||||
|
// otherwise this function would be in BaseRegistry.
|
||||||
|
var parser = new json2object.JsonParser<StageData>();
|
||||||
|
parser.ignoreUnknownVariables = false;
|
||||||
|
|
||||||
|
switch (loadEntryFile(id))
|
||||||
|
{
|
||||||
|
case {fileName: fileName, contents: contents}:
|
||||||
|
parser.fromJson(contents, fileName);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parser.errors.length > 0)
|
||||||
|
{
|
||||||
|
printErrors(parser.errors, id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parser.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and validate the JSON data and produce the corresponding data object.
|
||||||
|
*
|
||||||
|
* NOTE: Must be implemented on the implementation class.
|
||||||
|
* @param contents The JSON as a string.
|
||||||
|
* @param fileName An optional file name for error reporting.
|
||||||
|
*/
|
||||||
|
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<StageData>
|
||||||
|
{
|
||||||
|
var parser = new json2object.JsonParser<StageData>();
|
||||||
|
parser.ignoreUnknownVariables = false;
|
||||||
|
parser.fromJson(contents, fileName);
|
||||||
|
|
||||||
|
if (parser.errors.length > 0)
|
||||||
|
{
|
||||||
|
printErrors(parser.errors, fileName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parser.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScriptedEntry(clsName:String):Stage
|
||||||
|
{
|
||||||
|
return ScriptedStage.init(clsName, "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScriptedClassNames():Array<String>
|
||||||
|
{
|
||||||
|
return ScriptedStage.listScriptClasses();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of all the stages from the base game, in order.
|
||||||
|
* TODO: Should this be hardcoded?
|
||||||
|
*/
|
||||||
|
public function listBaseGameStageIds():Array<String>
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
"mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets",
|
||||||
|
"phillyBlazin",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of all installed story weeks that are not from the base game.
|
||||||
|
*/
|
||||||
|
public function listModdedStageIds():Array<String>
|
||||||
|
{
|
||||||
|
return listEntryIds().filter(function(id:String):Bool {
|
||||||
|
return listBaseGameStageIds().indexOf(id) == -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
53
source/funkin/graphics/FunkinSprite.hx
Normal file
53
source/funkin/graphics/FunkinSprite.hx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package funkin.graphics;
|
||||||
|
|
||||||
|
import flixel.FlxSprite;
|
||||||
|
import flixel.util.FlxColor;
|
||||||
|
import flixel.graphics.FlxGraphic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An FlxSprite with additional functionality.
|
||||||
|
*/
|
||||||
|
class FunkinSprite extends FlxSprite
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param x Starting X position
|
||||||
|
* @param y Starting Y position
|
||||||
|
*/
|
||||||
|
public function new(?x:Float = 0, ?y:Float = 0)
|
||||||
|
{
|
||||||
|
super(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acts similarly to `makeGraphic`, but with improved memory usage,
|
||||||
|
* at the expense of not being able to paint onto the sprite.
|
||||||
|
*
|
||||||
|
* @param width The target width of the sprite.
|
||||||
|
* @param height The target height of the sprite.
|
||||||
|
* @param color The color to fill the sprite with.
|
||||||
|
*/
|
||||||
|
public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite
|
||||||
|
{
|
||||||
|
var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}');
|
||||||
|
frames = graphic.imageFrame;
|
||||||
|
scale.set(width / 2, height / 2);
|
||||||
|
updateHitbox();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure scale is applied when cloning a sprite.
|
||||||
|
* The default `clone()` method acts kinda weird TBH.
|
||||||
|
* @return A clone of this sprite.
|
||||||
|
*/
|
||||||
|
public override function clone():FunkinSprite
|
||||||
|
{
|
||||||
|
var result = new FunkinSprite(this.x, this.y);
|
||||||
|
result.frames = this.frames;
|
||||||
|
result.scale.set(this.scale.x, this.scale.y);
|
||||||
|
result.updateHitbox();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,11 +4,12 @@ import funkin.util.macro.ClassMacro;
|
||||||
import funkin.modding.module.ModuleHandler;
|
import funkin.modding.module.ModuleHandler;
|
||||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||||
import funkin.data.song.SongData;
|
import funkin.data.song.SongData;
|
||||||
import funkin.play.stage.StageData;
|
import funkin.data.stage.StageData;
|
||||||
import polymod.Polymod;
|
import polymod.Polymod;
|
||||||
import polymod.backends.PolymodAssets.PolymodAssetType;
|
import polymod.backends.PolymodAssets.PolymodAssetType;
|
||||||
import polymod.format.ParseRules.TextFileFormat;
|
import polymod.format.ParseRules.TextFileFormat;
|
||||||
import funkin.data.event.SongEventRegistry;
|
import funkin.data.event.SongEventRegistry;
|
||||||
|
import funkin.data.stage.StageRegistry;
|
||||||
import funkin.util.FileUtil;
|
import funkin.util.FileUtil;
|
||||||
import funkin.data.level.LevelRegistry;
|
import funkin.data.level.LevelRegistry;
|
||||||
import funkin.data.notestyle.NoteStyleRegistry;
|
import funkin.data.notestyle.NoteStyleRegistry;
|
||||||
|
@ -275,7 +276,7 @@ class PolymodHandler
|
||||||
ConversationDataParser.loadConversationCache();
|
ConversationDataParser.loadConversationCache();
|
||||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||||
SpeakerDataParser.loadSpeakerCache();
|
SpeakerDataParser.loadSpeakerCache();
|
||||||
StageDataParser.loadStageCache();
|
StageRegistry.instance.loadEntries();
|
||||||
CharacterDataParser.loadCharacterCache();
|
CharacterDataParser.loadCharacterCache();
|
||||||
ModuleHandler.loadModuleCache();
|
ModuleHandler.loadModuleCache();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import flixel.sound.FlxSound;
|
||||||
import funkin.ui.story.StoryMenuState;
|
import funkin.ui.story.StoryMenuState;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import flixel.util.FlxTimer;
|
import flixel.util.FlxTimer;
|
||||||
|
import funkin.graphics.FunkinSprite;
|
||||||
import funkin.ui.MusicBeatSubState;
|
import funkin.ui.MusicBeatSubState;
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
import funkin.modding.events.ScriptEventDispatcher;
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
|
@ -22,6 +23,12 @@ import funkin.play.character.BaseCharacter;
|
||||||
*/
|
*/
|
||||||
class GameOverSubState extends MusicBeatSubState
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which alternate animation on the character to use.
|
* Which alternate animation on the character to use.
|
||||||
* You can set this via script.
|
* You can set this via script.
|
||||||
|
@ -87,6 +94,13 @@ class GameOverSubState extends MusicBeatSubState
|
||||||
|
|
||||||
override public function create()
|
override public function create()
|
||||||
{
|
{
|
||||||
|
if (instance != null)
|
||||||
|
{
|
||||||
|
// TODO: Do something in this case? IDK.
|
||||||
|
trace('WARNING: GameOverSubState instance already exists. This should not happen.');
|
||||||
|
}
|
||||||
|
instance = this;
|
||||||
|
|
||||||
super.create();
|
super.create();
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -94,11 +108,12 @@ class GameOverSubState extends MusicBeatSubState
|
||||||
//
|
//
|
||||||
|
|
||||||
// Add a black background to the screen.
|
// Add a black background to the screen.
|
||||||
var bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
|
var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
||||||
// We make this transparent so that we can see the stage underneath during debugging,
|
// We make this transparent so that we can see the stage underneath during debugging,
|
||||||
// but it's normally opaque.
|
// but it's normally opaque.
|
||||||
bg.alpha = transparent ? 0.25 : 1.0;
|
bg.alpha = transparent ? 0.25 : 1.0;
|
||||||
bg.scrollFactor.set();
|
bg.scrollFactor.set();
|
||||||
|
bg.screenCenter();
|
||||||
add(bg);
|
add(bg);
|
||||||
|
|
||||||
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
|
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
|
||||||
|
@ -220,6 +235,7 @@ class GameOverSubState extends MusicBeatSubState
|
||||||
playJeffQuote();
|
playJeffQuote();
|
||||||
// Start music at lower volume
|
// Start music at lower volume
|
||||||
startDeathMusic(0.2, false);
|
startDeathMusic(0.2, false);
|
||||||
|
boyfriend.playAnimation('deathLoop' + animationSuffix);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Start music at normal volume once the initial death animation finishes.
|
// Start music at normal volume once the initial death animation finishes.
|
||||||
|
@ -280,10 +296,10 @@ class GameOverSubState extends MusicBeatSubState
|
||||||
*/
|
*/
|
||||||
function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
|
function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
|
||||||
{
|
{
|
||||||
var musicPath = Paths.music('gameOver' + musicSuffix);
|
var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix);
|
||||||
if (isEnding)
|
if (isEnding)
|
||||||
{
|
{
|
||||||
musicPath = Paths.music('gameOverEnd' + musicSuffix);
|
musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix);
|
||||||
}
|
}
|
||||||
if (!gameOverMusic.playing || force)
|
if (!gameOverMusic.playing || force)
|
||||||
{
|
{
|
||||||
|
@ -303,7 +319,7 @@ class GameOverSubState extends MusicBeatSubState
|
||||||
public static function playBlueBalledSFX()
|
public static function playBlueBalledSFX()
|
||||||
{
|
{
|
||||||
blueballed = true;
|
blueballed = true;
|
||||||
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + blueBallSuffix));
|
FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
|
||||||
}
|
}
|
||||||
|
|
||||||
var playingJeffQuote:Bool = false;
|
var playingJeffQuote:Bool = false;
|
||||||
|
@ -326,6 +342,11 @@ class GameOverSubState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override function toString():String
|
||||||
|
{
|
||||||
|
return "GameOverSubState";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef GameOverParams =
|
typedef GameOverParams =
|
||||||
|
|
|
@ -50,11 +50,11 @@ import funkin.play.notes.SustainTrail;
|
||||||
import funkin.play.scoring.Scoring;
|
import funkin.play.scoring.Scoring;
|
||||||
import funkin.play.song.Song;
|
import funkin.play.song.Song;
|
||||||
import funkin.data.song.SongRegistry;
|
import funkin.data.song.SongRegistry;
|
||||||
|
import funkin.data.stage.StageRegistry;
|
||||||
import funkin.data.song.SongData.SongEventData;
|
import funkin.data.song.SongData.SongEventData;
|
||||||
import funkin.data.song.SongData.SongNoteData;
|
import funkin.data.song.SongData.SongNoteData;
|
||||||
import funkin.data.song.SongData.SongCharacterData;
|
import funkin.data.song.SongData.SongCharacterData;
|
||||||
import funkin.play.stage.Stage;
|
import funkin.play.stage.Stage;
|
||||||
import funkin.play.stage.StageData.StageDataParser;
|
|
||||||
import funkin.ui.transition.LoadingState;
|
import funkin.ui.transition.LoadingState;
|
||||||
import funkin.play.components.PopUpStuff;
|
import funkin.play.components.PopUpStuff;
|
||||||
import funkin.ui.options.PreferencesMenu;
|
import funkin.ui.options.PreferencesMenu;
|
||||||
|
@ -1353,7 +1353,8 @@ class PlayState extends MusicBeatSubState
|
||||||
*/
|
*/
|
||||||
function loadStage(id:String):Void
|
function loadStage(id:String):Void
|
||||||
{
|
{
|
||||||
currentStage = StageDataParser.fetchStage(id);
|
currentStage = StageRegistry.instance.fetchEntry(id);
|
||||||
|
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
|
||||||
|
|
||||||
if (currentStage != null)
|
if (currentStage != null)
|
||||||
{
|
{
|
||||||
|
@ -1791,7 +1792,64 @@ class PlayState extends MusicBeatSubState
|
||||||
{
|
{
|
||||||
if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return;
|
if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return;
|
||||||
|
|
||||||
opponentStrumline.processNotes(null, dispatchEvent);
|
// Process notes on the opponent's side.
|
||||||
|
for (note in opponentStrumline.notes.members)
|
||||||
|
{
|
||||||
|
if (note == null) continue;
|
||||||
|
|
||||||
|
// TODO: Does this properly account for offsets?
|
||||||
|
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
|
||||||
|
var hitWindowCenter = note.strumTime;
|
||||||
|
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
|
||||||
|
|
||||||
|
if (Conductor.instance.songPosition > hitWindowEnd)
|
||||||
|
{
|
||||||
|
if (note.hasMissed) continue;
|
||||||
|
|
||||||
|
note.tooEarly = false;
|
||||||
|
note.mayHit = false;
|
||||||
|
note.hasMissed = true;
|
||||||
|
|
||||||
|
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
|
||||||
|
}
|
||||||
|
else if (Conductor.instance.songPosition > hitWindowCenter)
|
||||||
|
{
|
||||||
|
if (note.hasBeenHit) continue;
|
||||||
|
|
||||||
|
// Call an event to allow canceling the note hit.
|
||||||
|
// NOTE: This is what handles the character animations!
|
||||||
|
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, 0, true);
|
||||||
|
dispatchEvent(event);
|
||||||
|
|
||||||
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||||
|
if (event.eventCanceled) continue;
|
||||||
|
|
||||||
|
// Command the opponent to hit the note on time.
|
||||||
|
// NOTE: This is what handles the strumline and cleaning up the note itself!
|
||||||
|
opponentStrumline.hitNote(note);
|
||||||
|
|
||||||
|
if (note.holdNoteSprite != null)
|
||||||
|
{
|
||||||
|
opponentStrumline.playNoteHoldCover(note.holdNoteSprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Conductor.instance.songPosition > hitWindowStart)
|
||||||
|
{
|
||||||
|
if (note.hasBeenHit || note.hasMissed) continue;
|
||||||
|
|
||||||
|
note.tooEarly = false;
|
||||||
|
note.mayHit = true;
|
||||||
|
note.hasMissed = false;
|
||||||
|
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
note.tooEarly = true;
|
||||||
|
note.mayHit = false;
|
||||||
|
note.hasMissed = false;
|
||||||
|
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process hold notes on the opponent's side.
|
// Process hold notes on the opponent's side.
|
||||||
for (holdNote in opponentStrumline.holdNotes.members)
|
for (holdNote in opponentStrumline.holdNotes.members)
|
||||||
|
@ -1808,11 +1866,77 @@ class PlayState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Potential penalty for dropping a hold note?
|
if (holdNote.missedNote && !holdNote.handledMiss)
|
||||||
// if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; }
|
{
|
||||||
|
// When the opponent drops a hold note.
|
||||||
|
holdNote.handledMiss = true;
|
||||||
|
|
||||||
|
// We dropped a hold note.
|
||||||
|
// Mute vocals and play miss animation, but don't penalize.
|
||||||
|
vocals.opponentVolume = 0;
|
||||||
|
currentStage.getOpponent().playSingAnimation(holdNote.noteData.getDirection(), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playerStrumline.processNotes(onNoteMiss, dispatchEvent);
|
// Process notes on the player's side.
|
||||||
|
for (note in playerStrumline.notes.members)
|
||||||
|
{
|
||||||
|
if (note == null) continue;
|
||||||
|
|
||||||
|
if (note.hasBeenHit)
|
||||||
|
{
|
||||||
|
note.tooEarly = false;
|
||||||
|
note.mayHit = false;
|
||||||
|
note.hasMissed = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
|
||||||
|
var hitWindowCenter = note.strumTime;
|
||||||
|
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
|
||||||
|
|
||||||
|
if (Conductor.instance.songPosition > hitWindowEnd)
|
||||||
|
{
|
||||||
|
note.tooEarly = false;
|
||||||
|
note.mayHit = false;
|
||||||
|
note.hasMissed = true;
|
||||||
|
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
|
||||||
|
}
|
||||||
|
else if (Conductor.instance.songPosition > hitWindowStart)
|
||||||
|
{
|
||||||
|
note.tooEarly = false;
|
||||||
|
note.mayHit = true;
|
||||||
|
note.hasMissed = false;
|
||||||
|
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
note.tooEarly = true;
|
||||||
|
note.mayHit = false;
|
||||||
|
note.hasMissed = false;
|
||||||
|
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This becomes true when the note leaves the hit window.
|
||||||
|
// It might still be on screen.
|
||||||
|
if (note.hasMissed && !note.handledMiss)
|
||||||
|
{
|
||||||
|
// Call an event to allow canceling the note miss.
|
||||||
|
// NOTE: This is what handles the character animations!
|
||||||
|
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, 0, true);
|
||||||
|
dispatchEvent(event);
|
||||||
|
|
||||||
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||||
|
if (event.eventCanceled) continue;
|
||||||
|
|
||||||
|
// Judge the miss.
|
||||||
|
// NOTE: This is what handles the scoring.
|
||||||
|
trace('Missed note! ${note.noteData}');
|
||||||
|
onNoteMiss(note);
|
||||||
|
|
||||||
|
note.handledMiss = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process hold notes on the player's side.
|
// Process hold notes on the player's side.
|
||||||
// This handles scoring so we don't need it on the opponent's side.
|
// This handles scoring so we don't need it on the opponent's side.
|
||||||
|
@ -1828,8 +1952,15 @@ class PlayState extends MusicBeatSubState
|
||||||
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
|
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Potential penalty for dropping a hold note?
|
if (holdNote.missedNote && !holdNote.handledMiss)
|
||||||
// if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; }
|
{
|
||||||
|
// The player dropped a hold note.
|
||||||
|
holdNote.handledMiss = true;
|
||||||
|
|
||||||
|
// Mute vocals and play miss animation, but don't penalize.
|
||||||
|
vocals.playerVolume = 0;
|
||||||
|
currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1921,8 +2052,6 @@ class PlayState extends MusicBeatSubState
|
||||||
trace('Hit note! ${targetNote.noteData}');
|
trace('Hit note! ${targetNote.noteData}');
|
||||||
goodNoteHit(targetNote, input);
|
goodNoteHit(targetNote, input);
|
||||||
|
|
||||||
targetNote.visible = false;
|
|
||||||
targetNote.kill();
|
|
||||||
notesInDirection.remove(targetNote);
|
notesInDirection.remove(targetNote);
|
||||||
|
|
||||||
// Play the strumline animation.
|
// Play the strumline animation.
|
||||||
|
@ -1954,15 +2083,8 @@ class PlayState extends MusicBeatSubState
|
||||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||||
if (event.eventCanceled) return;
|
if (event.eventCanceled) return;
|
||||||
|
|
||||||
Highscore.tallies.combo++;
|
|
||||||
Highscore.tallies.totalNotesHit++;
|
|
||||||
|
|
||||||
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
|
|
||||||
|
|
||||||
popUpScore(note, input);
|
popUpScore(note, input);
|
||||||
|
|
||||||
playerStrumline.hitNote(note);
|
|
||||||
|
|
||||||
if (note.isHoldNote && note.holdNoteSprite != null)
|
if (note.isHoldNote && note.holdNoteSprite != null)
|
||||||
{
|
{
|
||||||
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
|
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
|
||||||
|
@ -1978,8 +2100,6 @@ class PlayState extends MusicBeatSubState
|
||||||
function onNoteMiss(note:NoteSprite):Void
|
function onNoteMiss(note:NoteSprite):Void
|
||||||
{
|
{
|
||||||
// a MISS is when you let a note scroll past you!!
|
// a MISS is when you let a note scroll past you!!
|
||||||
Highscore.tallies.missed++;
|
|
||||||
|
|
||||||
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true);
|
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true);
|
||||||
dispatchEvent(event);
|
dispatchEvent(event);
|
||||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||||
|
@ -2027,8 +2147,11 @@ class PlayState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
vocals.playerVolume = 0;
|
vocals.playerVolume = 0;
|
||||||
|
|
||||||
|
Highscore.tallies.missed++;
|
||||||
|
|
||||||
if (Highscore.tallies.combo != 0)
|
if (Highscore.tallies.combo != 0)
|
||||||
{
|
{
|
||||||
|
// Break the combo.
|
||||||
Highscore.tallies.combo = comboPopUps.displayCombo(0);
|
Highscore.tallies.combo = comboPopUps.displayCombo(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2164,8 +2287,10 @@ class PlayState extends MusicBeatSubState
|
||||||
vocals.playerVolume = 1;
|
vocals.playerVolume = 1;
|
||||||
|
|
||||||
// Calculate the input latency (do this as late as possible).
|
// Calculate the input latency (do this as late as possible).
|
||||||
var inputLatencyMs:Float = haxe.Int64.toInt(PreciseInputManager.getCurrentTimestamp() - input.timestamp) / 1000.0 / 1000.0;
|
// trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
|
||||||
trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
|
var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
|
||||||
|
var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
|
||||||
|
// trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
|
||||||
|
|
||||||
// Get the offset and compensate for input latency.
|
// Get the offset and compensate for input latency.
|
||||||
// Round inward (trim remainder) for consistency.
|
// Round inward (trim remainder) for consistency.
|
||||||
|
@ -2174,29 +2299,51 @@ class PlayState extends MusicBeatSubState
|
||||||
var score = Scoring.scoreNote(noteDiff, PBOT1);
|
var score = Scoring.scoreNote(noteDiff, PBOT1);
|
||||||
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
|
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
|
||||||
|
|
||||||
|
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!');
|
||||||
|
// TODO: Remove this.
|
||||||
|
comboPopUps.displayRating('miss');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isComboBreak = false;
|
||||||
switch (daRating)
|
switch (daRating)
|
||||||
{
|
{
|
||||||
case 'killer':
|
|
||||||
Highscore.tallies.killer += 1;
|
|
||||||
health += Constants.HEALTH_KILLER_BONUS;
|
|
||||||
case 'sick':
|
case 'sick':
|
||||||
Highscore.tallies.sick += 1;
|
Highscore.tallies.sick += 1;
|
||||||
health += Constants.HEALTH_SICK_BONUS;
|
health += Constants.HEALTH_SICK_BONUS;
|
||||||
|
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
|
||||||
case 'good':
|
case 'good':
|
||||||
Highscore.tallies.good += 1;
|
Highscore.tallies.good += 1;
|
||||||
health += Constants.HEALTH_GOOD_BONUS;
|
health += Constants.HEALTH_GOOD_BONUS;
|
||||||
|
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
|
||||||
case 'bad':
|
case 'bad':
|
||||||
Highscore.tallies.bad += 1;
|
Highscore.tallies.bad += 1;
|
||||||
health += Constants.HEALTH_BAD_BONUS;
|
health += Constants.HEALTH_BAD_BONUS;
|
||||||
|
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
|
||||||
case 'shit':
|
case 'shit':
|
||||||
Highscore.tallies.shit += 1;
|
Highscore.tallies.shit += 1;
|
||||||
health += Constants.HEALTH_SHIT_BONUS;
|
health += Constants.HEALTH_SHIT_BONUS;
|
||||||
case 'miss':
|
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
|
||||||
Highscore.tallies.missed += 1;
|
|
||||||
health -= Constants.HEALTH_MISS_PENALTY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (daRating == "sick" || daRating == "killer")
|
if (isComboBreak)
|
||||||
|
{
|
||||||
|
// Break the combo, but don't increment tallies.misses.
|
||||||
|
Highscore.tallies.combo = comboPopUps.displayCombo(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")
|
||||||
{
|
{
|
||||||
playerStrumline.playNoteSplash(daNote.noteData.getDirection());
|
playerStrumline.playNoteSplash(daNote.noteData.getDirection());
|
||||||
}
|
}
|
||||||
|
@ -2332,7 +2479,6 @@ class PlayState extends MusicBeatSubState
|
||||||
score: songScore,
|
score: songScore,
|
||||||
tallies:
|
tallies:
|
||||||
{
|
{
|
||||||
killer: Highscore.tallies.killer,
|
|
||||||
sick: Highscore.tallies.sick,
|
sick: Highscore.tallies.sick,
|
||||||
good: Highscore.tallies.good,
|
good: Highscore.tallies.good,
|
||||||
bad: Highscore.tallies.bad,
|
bad: Highscore.tallies.bad,
|
||||||
|
@ -2383,7 +2529,6 @@ class PlayState extends MusicBeatSubState
|
||||||
tallies:
|
tallies:
|
||||||
{
|
{
|
||||||
// TODO: Sum up the values for the whole level!
|
// TODO: Sum up the values for the whole level!
|
||||||
killer: 0,
|
|
||||||
sick: 0,
|
sick: 0,
|
||||||
good: 0,
|
good: 0,
|
||||||
bad: 0,
|
bad: 0,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import flixel.math.FlxMath;
|
||||||
import flixel.math.FlxPoint.FlxCallbackPoint;
|
import flixel.math.FlxPoint.FlxCallbackPoint;
|
||||||
import flixel.math.FlxPoint;
|
import flixel.math.FlxPoint;
|
||||||
import flixel.math.FlxRect;
|
import flixel.math.FlxRect;
|
||||||
|
import funkin.graphics.FunkinSprite;
|
||||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import flixel.util.FlxDestroyUtil;
|
import flixel.util.FlxDestroyUtil;
|
||||||
|
@ -621,7 +622,7 @@ class AnimateAtlasCharacter extends BaseCharacter
|
||||||
* This functionality isn't supported in SpriteGroup
|
* This functionality isn't supported in SpriteGroup
|
||||||
* @return this sprite group
|
* @return this sprite group
|
||||||
*/
|
*/
|
||||||
public override function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite
|
public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite
|
||||||
{
|
{
|
||||||
#if FLX_DEBUG
|
#if FLX_DEBUG
|
||||||
throw "This function is not supported in FlxSpriteGroup";
|
throw "This function is not supported in FlxSpriteGroup";
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package funkin.play.character;
|
package funkin.play.character;
|
||||||
|
|
||||||
|
import flixel.graphics.frames.FlxAtlasFrames;
|
||||||
import flixel.graphics.frames.FlxFramesCollection;
|
import flixel.graphics.frames.FlxFramesCollection;
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
import funkin.util.assets.FlxAnimationUtil;
|
import funkin.util.assets.FlxAnimationUtil;
|
||||||
|
@ -7,35 +8,17 @@ import funkin.play.character.CharacterData.CharacterRenderType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For some characters which use Sparrow atlases, the spritesheets need to be split
|
* For some characters which use Sparrow atlases, the spritesheets need to be split
|
||||||
* into multiple files. This character renderer handles by showing the appropriate sprite.
|
* into multiple files. This character renderer concatenates these together into a single sprite.
|
||||||
*
|
*
|
||||||
* Examples in base game include BF Holding GF (most of the sprites are in one file
|
* Examples in base game include BF Holding GF (most of the sprites are in one file
|
||||||
* but the death animation is in a separate file).
|
* but the death animation is in a separate file).
|
||||||
* Only example I can think of in mods is Tricky (which has a separate file for each animation).
|
* Only example I can think of in mods is Tricky (which has a separate file for each animation).
|
||||||
*
|
*
|
||||||
* BaseCharacter has game logic, SparrowCharacter has only rendering logic.
|
* BaseCharacter has game logic, MultiSparrowCharacter has only rendering logic.
|
||||||
* KEEP THEM SEPARATE!
|
* KEEP THEM SEPARATE!
|
||||||
*
|
|
||||||
* TODO: Rewrite this to use a single frame collection.
|
|
||||||
* @see https://github.com/HaxeFlixel/flixel/issues/2587#issuecomment-1179620637
|
|
||||||
*/
|
*/
|
||||||
class MultiSparrowCharacter extends BaseCharacter
|
class MultiSparrowCharacter extends BaseCharacter
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* The actual group which holds all spritesheets this character uses.
|
|
||||||
*/
|
|
||||||
var members:Map<String, FlxFramesCollection> = new Map<String, FlxFramesCollection>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map between animation names and what frame collection the animation should use.
|
|
||||||
*/
|
|
||||||
var animAssetPath:Map<String, String> = new Map<String, String>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current frame collection being used.
|
|
||||||
*/
|
|
||||||
var activeMember:String;
|
|
||||||
|
|
||||||
public function new(id:String)
|
public function new(id:String)
|
||||||
{
|
{
|
||||||
super(id, CharacterRenderType.MultiSparrow);
|
super(id, CharacterRenderType.MultiSparrow);
|
||||||
|
@ -51,7 +34,7 @@ class MultiSparrowCharacter extends BaseCharacter
|
||||||
|
|
||||||
function buildSprites():Void
|
function buildSprites():Void
|
||||||
{
|
{
|
||||||
buildSpritesheets();
|
buildSpritesheet();
|
||||||
buildAnimations();
|
buildAnimations();
|
||||||
|
|
||||||
if (_data.isPixel)
|
if (_data.isPixel)
|
||||||
|
@ -66,95 +49,49 @@ class MultiSparrowCharacter extends BaseCharacter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSpritesheets():Void
|
function buildSpritesheet():Void
|
||||||
{
|
{
|
||||||
// TODO: This currently works by creating like 5 frame collections and switching between them.
|
var assetList = [];
|
||||||
// It would be better to refactor this to simply concatenate the frame collections together.
|
|
||||||
|
|
||||||
// Build the list of asset paths to use.
|
|
||||||
// Ignore nulls and duplicates.
|
|
||||||
var assetList = [_data.assetPath];
|
|
||||||
for (anim in _data.animations)
|
for (anim in _data.animations)
|
||||||
{
|
{
|
||||||
if (anim.assetPath != null && !assetList.contains(anim.assetPath))
|
if (anim.assetPath != null && !assetList.contains(anim.assetPath))
|
||||||
{
|
{
|
||||||
assetList.push(anim.assetPath);
|
assetList.push(anim.assetPath);
|
||||||
}
|
}
|
||||||
animAssetPath.set(anim.name, anim.assetPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the Sparrow atlas for each path and store them in the members map.
|
var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath, 'shared');
|
||||||
|
|
||||||
|
if (texture == null)
|
||||||
|
{
|
||||||
|
trace('Multi-Sparrow atlas could not load PRIMARY texture: ${_data.assetPath}');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('Creating multi-sparrow atlas: ${_data.assetPath}');
|
||||||
|
texture.parent.destroyOnNoUse = false;
|
||||||
|
}
|
||||||
|
|
||||||
for (asset in assetList)
|
for (asset in assetList)
|
||||||
{
|
{
|
||||||
var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared');
|
var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset, 'shared');
|
||||||
// If we don't do this, the unused textures will be removed as soon as they're loaded.
|
// If we don't do this, the unused textures will be removed as soon as they're loaded.
|
||||||
|
|
||||||
if (texture == null)
|
if (subTexture == null)
|
||||||
{
|
{
|
||||||
trace('Multi-Sparrow atlas could not load texture: ${asset}');
|
trace('Multi-Sparrow atlas could not load subtexture: ${asset}');
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
trace('Adding multi-sparrow atlas: ${asset}');
|
trace('Concatenating multi-sparrow atlas: ${asset}');
|
||||||
texture.parent.destroyOnNoUse = false;
|
subTexture.parent.destroyOnNoUse = false;
|
||||||
members.set(asset, texture);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
texture.addAtlas(subTexture);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the default frame collection to start.
|
this.frames = texture;
|
||||||
loadFramesByAssetPath(_data.assetPath);
|
this.setScale(_data.scale);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace this sprite's animation frames with the ones at this asset path.
|
|
||||||
*/
|
|
||||||
function loadFramesByAssetPath(assetPath:String):Void
|
|
||||||
{
|
|
||||||
if (_data.assetPath == null)
|
|
||||||
{
|
|
||||||
trace('[ERROR] Multi-Sparrow character has no default asset path!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (assetPath == null)
|
|
||||||
{
|
|
||||||
// trace('Asset path is null, falling back to default. This is normal!');
|
|
||||||
loadFramesByAssetPath(_data.assetPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.activeMember == assetPath)
|
|
||||||
{
|
|
||||||
// trace('Already using this asset path: ${assetPath}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (members.exists(assetPath))
|
|
||||||
{
|
|
||||||
// Switch to a new set of sprites.
|
|
||||||
// trace('Loading frames from asset path: ${assetPath}');
|
|
||||||
this.frames = members.get(assetPath);
|
|
||||||
this.activeMember = assetPath;
|
|
||||||
this.setScale(_data.scale);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace('[WARN] MultiSparrow character ${characterId} could not find asset path: ${assetPath}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace this sprite's animation frames with the ones needed to play this animation.
|
|
||||||
*/
|
|
||||||
function loadFramesByAnimName(animName)
|
|
||||||
{
|
|
||||||
if (animAssetPath.exists(animName))
|
|
||||||
{
|
|
||||||
loadFramesByAssetPath(animAssetPath.get(animName));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace('[WARN] MultiSparrow character ${characterId} could not find animation: ${animName}');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAnimations()
|
function buildAnimations()
|
||||||
|
@ -164,7 +101,6 @@ class MultiSparrowCharacter extends BaseCharacter
|
||||||
// We need to swap to the proper frame collection before adding the animations, I think?
|
// We need to swap to the proper frame collection before adding the animations, I think?
|
||||||
for (anim in _data.animations)
|
for (anim in _data.animations)
|
||||||
{
|
{
|
||||||
loadFramesByAnimName(anim.name);
|
|
||||||
FlxAnimationUtil.addAtlasAnimation(this, anim);
|
FlxAnimationUtil.addAtlasAnimation(this, anim);
|
||||||
|
|
||||||
if (anim.offsets == null)
|
if (anim.offsets == null)
|
||||||
|
@ -187,37 +123,6 @@ class MultiSparrowCharacter extends BaseCharacter
|
||||||
// unless we're forcing a new animation.
|
// unless we're forcing a new animation.
|
||||||
if (!this.canPlayOtherAnims && !ignoreOther) return;
|
if (!this.canPlayOtherAnims && !ignoreOther) return;
|
||||||
|
|
||||||
loadFramesByAnimName(name);
|
|
||||||
super.playAnimation(name, restart, ignoreOther, reverse);
|
super.playAnimation(name, restart, ignoreOther, reverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
override function set_frames(value:FlxFramesCollection):FlxFramesCollection
|
|
||||||
{
|
|
||||||
// DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK
|
|
||||||
// WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM
|
|
||||||
// if (animation != null)
|
|
||||||
// {
|
|
||||||
// animation.destroyAnimations();
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
graphic = value.parent;
|
|
||||||
this.frames = value;
|
|
||||||
this.frame = value.getByIndex(0);
|
|
||||||
// this.numFrames = value.numFrames;
|
|
||||||
resetHelpers();
|
|
||||||
this.bakedRotationAngle = 0;
|
|
||||||
this.animation.frameIndex = 0;
|
|
||||||
graphicLoaded();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.frames = null;
|
|
||||||
this.frame = null;
|
|
||||||
this.graphic = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.frames;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import funkin.data.song.SongData.SongNoteData;
|
||||||
import funkin.play.notes.notestyle.NoteStyle;
|
import funkin.play.notes.notestyle.NoteStyle;
|
||||||
import flixel.graphics.frames.FlxAtlasFrames;
|
import flixel.graphics.frames.FlxAtlasFrames;
|
||||||
import flixel.FlxSprite;
|
import flixel.FlxSprite;
|
||||||
|
import funkin.graphics.shaders.HSVShader;
|
||||||
|
|
||||||
class NoteSprite extends FlxSprite
|
class NoteSprite extends FlxSprite
|
||||||
{
|
{
|
||||||
|
@ -11,6 +12,8 @@ class NoteSprite extends FlxSprite
|
||||||
|
|
||||||
public var holdNoteSprite:SustainTrail;
|
public var holdNoteSprite:SustainTrail;
|
||||||
|
|
||||||
|
var hsvShader:HSVShader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time at which the note should be hit, in milliseconds.
|
* The time at which the note should be hit, in milliseconds.
|
||||||
*/
|
*/
|
||||||
|
@ -102,6 +105,8 @@ class NoteSprite extends FlxSprite
|
||||||
this.strumTime = strumTime;
|
this.strumTime = strumTime;
|
||||||
this.direction = direction;
|
this.direction = direction;
|
||||||
|
|
||||||
|
this.hsvShader = new HSVShader();
|
||||||
|
|
||||||
if (this.strumTime < 0) this.strumTime = 0;
|
if (this.strumTime < 0) this.strumTime = 0;
|
||||||
|
|
||||||
setupNoteGraphic(noteStyle);
|
setupNoteGraphic(noteStyle);
|
||||||
|
@ -116,16 +121,57 @@ class NoteSprite extends FlxSprite
|
||||||
|
|
||||||
setGraphicSize(Strumline.STRUMLINE_SIZE);
|
setGraphicSize(Strumline.STRUMLINE_SIZE);
|
||||||
updateHitbox();
|
updateHitbox();
|
||||||
|
|
||||||
|
this.shader = hsvShader;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if FLX_DEBUG
|
||||||
|
/**
|
||||||
|
* Call this to override how debug bounding boxes are drawn for this sprite.
|
||||||
|
*/
|
||||||
|
public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void
|
||||||
|
{
|
||||||
|
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
|
||||||
|
|
||||||
|
var gfx = beginDrawDebug(camera);
|
||||||
|
|
||||||
|
var rect = getBoundingBox(camera);
|
||||||
|
trace('note sprite bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height);
|
||||||
|
|
||||||
|
gfx.lineStyle(2, 0xFFFF66FF, 0.5); // thickness, color, alpha
|
||||||
|
gfx.drawRect(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
|
||||||
|
gfx.lineStyle(2, 0xFFFFFF66, 0.5); // thickness, color, alpha
|
||||||
|
gfx.drawRect(rect.x, rect.y + rect.height / 2, rect.width, 1);
|
||||||
|
|
||||||
|
endDrawDebug(camera);
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
|
||||||
|
public function desaturate():Void
|
||||||
|
{
|
||||||
|
this.hsvShader.saturation = 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHue(hue:Float):Void
|
||||||
|
{
|
||||||
|
this.hsvShader.hue = hue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override function revive():Void
|
public override function revive():Void
|
||||||
{
|
{
|
||||||
super.revive();
|
super.revive();
|
||||||
|
this.visible = true;
|
||||||
|
this.alpha = 1.0;
|
||||||
this.active = false;
|
this.active = false;
|
||||||
this.tooEarly = false;
|
this.tooEarly = false;
|
||||||
this.hasBeenHit = false;
|
this.hasBeenHit = false;
|
||||||
this.mayHit = false;
|
this.mayHit = false;
|
||||||
this.hasMissed = false;
|
this.hasMissed = false;
|
||||||
|
|
||||||
|
this.hsvShader.hue = 1.0;
|
||||||
|
this.hsvShader.saturation = 1.0;
|
||||||
|
this.hsvShader.value = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override function kill():Void
|
public override function kill():Void
|
||||||
|
|
|
@ -397,7 +397,7 @@ class Strumline extends FlxSpriteGroup
|
||||||
// Update rendering of notes.
|
// Update rendering of notes.
|
||||||
for (note in notes.members)
|
for (note in notes.members)
|
||||||
{
|
{
|
||||||
if (note == null || !note.alive || note.hasBeenHit) continue;
|
if (note == null || !note.alive) continue;
|
||||||
|
|
||||||
var vwoosh:Bool = note.holdNoteSprite == null;
|
var vwoosh:Bool = note.holdNoteSprite == null;
|
||||||
// Set the note's position.
|
// Set the note's position.
|
||||||
|
@ -424,7 +424,7 @@ class Strumline extends FlxSpriteGroup
|
||||||
playStatic(holdNote.noteDirection);
|
playStatic(holdNote.noteDirection);
|
||||||
holdNote.missedNote = true;
|
holdNote.missedNote = true;
|
||||||
holdNote.visible = true;
|
holdNote.visible = true;
|
||||||
holdNote.alpha = 0.0;
|
holdNote.alpha = 0.0; // Completely hide the dropped hold note.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,10 +465,6 @@ class Strumline extends FlxSpriteGroup
|
||||||
|
|
||||||
var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Constants.PIXELS_PER_MS;
|
var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Constants.PIXELS_PER_MS;
|
||||||
|
|
||||||
trace('yOffset: ' + yOffset);
|
|
||||||
trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength);
|
|
||||||
trace('holdNote.sustainLength: ' + holdNote.sustainLength);
|
|
||||||
|
|
||||||
var vwoosh:Bool = false;
|
var vwoosh:Bool = false;
|
||||||
|
|
||||||
if (Preferences.downscroll)
|
if (Preferences.downscroll)
|
||||||
|
@ -612,11 +608,24 @@ class Strumline extends FlxSpriteGroup
|
||||||
this.noteData.insertionSort(compareNoteData.bind(FlxSort.ASCENDING));
|
this.noteData.insertionSort(compareNoteData.bind(FlxSort.ASCENDING));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hitNote(note:NoteSprite):Void
|
/**
|
||||||
|
* @param note The note to hit.
|
||||||
|
* @param removeNote True to remove the note immediately, false to make it transparent and let it move offscreen.
|
||||||
|
*/
|
||||||
|
public function hitNote(note:NoteSprite, removeNote:Bool = true):Void
|
||||||
{
|
{
|
||||||
playConfirm(note.direction);
|
playConfirm(note.direction);
|
||||||
note.hasBeenHit = true;
|
note.hasBeenHit = true;
|
||||||
killNote(note);
|
|
||||||
|
if (removeNote)
|
||||||
|
{
|
||||||
|
killNote(note);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
note.alpha = 0.5;
|
||||||
|
note.desaturate();
|
||||||
|
}
|
||||||
|
|
||||||
if (note.holdNoteSprite != null)
|
if (note.holdNoteSprite != null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -47,6 +47,11 @@ class SustainTrail extends FlxSprite
|
||||||
*/
|
*/
|
||||||
public var missedNote:Bool = false;
|
public var missedNote:Bool = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to `true` after handling additional logic for missing notes.
|
||||||
|
*/
|
||||||
|
public var handledMiss:Bool = false;
|
||||||
|
|
||||||
// maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support!
|
// maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support!
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,6 +87,9 @@ class SustainTrail extends FlxSprite
|
||||||
|
|
||||||
public var isPixel:Bool;
|
public var isPixel:Bool;
|
||||||
|
|
||||||
|
var graphicWidth:Float = 0;
|
||||||
|
var graphicHeight:Float = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
|
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
|
||||||
* @param NoteData
|
* @param NoteData
|
||||||
|
@ -110,8 +118,8 @@ class SustainTrail extends FlxSprite
|
||||||
zoom *= 0.7;
|
zoom *= 0.7;
|
||||||
|
|
||||||
// CALCULATE SIZE
|
// CALCULATE SIZE
|
||||||
width = graphic.width / 8 * zoom; // amount of notes * 2
|
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
|
||||||
height = sustainHeight(sustainLength, getScrollSpeed());
|
graphicHeight = sustainHeight(sustainLength, getScrollSpeed());
|
||||||
// instead of scrollSpeed, PlayState.SONG.speed
|
// instead of scrollSpeed, PlayState.SONG.speed
|
||||||
|
|
||||||
flipY = Preferences.downscroll;
|
flipY = Preferences.downscroll;
|
||||||
|
@ -148,12 +156,21 @@ class SustainTrail extends FlxSprite
|
||||||
|
|
||||||
if (sustainLength == s) return s;
|
if (sustainLength == s) return s;
|
||||||
|
|
||||||
height = sustainHeight(s, getScrollSpeed());
|
graphicHeight = sustainHeight(s, getScrollSpeed());
|
||||||
this.sustainLength = s;
|
this.sustainLength = s;
|
||||||
updateClipping();
|
updateClipping();
|
||||||
|
updateHitbox();
|
||||||
return this.sustainLength;
|
return this.sustainLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override function updateHitbox():Void
|
||||||
|
{
|
||||||
|
width = graphicWidth;
|
||||||
|
height = graphicHeight;
|
||||||
|
offset.set(0, 0);
|
||||||
|
origin.set(width * 0.5, height * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up new vertex and UV data to clip the trail.
|
* Sets up new vertex and UV data to clip the trail.
|
||||||
* If flipY is true, top and bottom bounds swap places.
|
* If flipY is true, top and bottom bounds swap places.
|
||||||
|
@ -161,7 +178,7 @@ class SustainTrail extends FlxSprite
|
||||||
*/
|
*/
|
||||||
public function updateClipping(songTime:Float = 0):Void
|
public function updateClipping(songTime:Float = 0):Void
|
||||||
{
|
{
|
||||||
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height);
|
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight);
|
||||||
if (clipHeight <= 0.1)
|
if (clipHeight <= 0.1)
|
||||||
{
|
{
|
||||||
visible = false;
|
visible = false;
|
||||||
|
@ -178,10 +195,10 @@ class SustainTrail extends FlxSprite
|
||||||
// ===HOLD VERTICES==
|
// ===HOLD VERTICES==
|
||||||
// Top left
|
// Top left
|
||||||
vertices[0 * 2] = 0.0; // Inline with left side
|
vertices[0 * 2] = 0.0; // Inline with left side
|
||||||
vertices[0 * 2 + 1] = flipY ? clipHeight : height - clipHeight;
|
vertices[0 * 2 + 1] = flipY ? clipHeight : graphicHeight - clipHeight;
|
||||||
|
|
||||||
// Top right
|
// Top right
|
||||||
vertices[1 * 2] = width;
|
vertices[1 * 2] = graphicWidth;
|
||||||
vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex
|
vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex
|
||||||
|
|
||||||
// Bottom left
|
// Bottom left
|
||||||
|
@ -197,7 +214,7 @@ class SustainTrail extends FlxSprite
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom right
|
// Bottom right
|
||||||
vertices[3 * 2] = width;
|
vertices[3 * 2] = graphicWidth;
|
||||||
vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex
|
vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex
|
||||||
|
|
||||||
// ===HOLD UVs===
|
// ===HOLD UVs===
|
||||||
|
@ -233,7 +250,7 @@ class SustainTrail extends FlxSprite
|
||||||
|
|
||||||
// Bottom left
|
// Bottom left
|
||||||
vertices[6 * 2] = vertices[2 * 2]; // Inline with left side
|
vertices[6 * 2] = vertices[2 * 2]; // Inline with left side
|
||||||
vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (height + graphic.height * (bottomClip - endOffset) * zoom);
|
vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (graphicHeight + graphic.height * (bottomClip - endOffset) * zoom);
|
||||||
|
|
||||||
// Bottom right
|
// Bottom right
|
||||||
vertices[7 * 2] = vertices[3 * 2]; // Inline with right side
|
vertices[7 * 2] = vertices[3 * 2]; // Inline with right side
|
||||||
|
@ -277,6 +294,10 @@ class SustainTrail extends FlxSprite
|
||||||
getScreenPosition(_point, camera).subtractPoint(offset);
|
getScreenPosition(_point, camera).subtractPoint(offset);
|
||||||
camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing);
|
camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if FLX_DEBUG
|
||||||
|
if (FlxG.debugger.drawDebug) drawDebug();
|
||||||
|
#end
|
||||||
}
|
}
|
||||||
|
|
||||||
public override function kill():Void
|
public override function kill():Void
|
||||||
|
@ -305,6 +326,7 @@ class SustainTrail extends FlxSprite
|
||||||
|
|
||||||
hitNote = false;
|
hitNote = false;
|
||||||
missedNote = false;
|
missedNote = false;
|
||||||
|
handledMiss = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override public function destroy():Void
|
override public function destroy():Void
|
||||||
|
|
|
@ -158,8 +158,8 @@ class Scoring
|
||||||
|
|
||||||
return switch (absTiming)
|
return switch (absTiming)
|
||||||
{
|
{
|
||||||
case(_ < PBOT1_KILLER_THRESHOLD) => true:
|
// case(_ < PBOT1_KILLER_THRESHOLD) => true:
|
||||||
'killer';
|
// 'killer';
|
||||||
case(_ < PBOT1_SICK_THRESHOLD) => true:
|
case(_ < PBOT1_SICK_THRESHOLD) => true:
|
||||||
'sick';
|
'sick';
|
||||||
case(_ < PBOT1_GOOD_THRESHOLD) => true:
|
case(_ < PBOT1_GOOD_THRESHOLD) => true:
|
||||||
|
|
|
@ -174,7 +174,10 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||||
difficulty.timeChanges = metadata.timeChanges;
|
difficulty.timeChanges = metadata.timeChanges;
|
||||||
difficulty.looped = metadata.looped;
|
difficulty.looped = metadata.looped;
|
||||||
difficulty.generatedBy = metadata.generatedBy;
|
difficulty.generatedBy = metadata.generatedBy;
|
||||||
difficulty.offsets = metadata.offsets;
|
difficulty.offsets = metadata?.offsets ?? new SongOffsets();
|
||||||
|
|
||||||
|
difficulty.difficultyRating = metadata.playData.ratings.get(diffId) ?? 0;
|
||||||
|
difficulty.album = metadata.playData.album;
|
||||||
|
|
||||||
difficulty.stage = metadata.playData.stage;
|
difficulty.stage = metadata.playData.stage;
|
||||||
difficulty.noteStyle = metadata.playData.noteStyle;
|
difficulty.noteStyle = metadata.playData.noteStyle;
|
||||||
|
@ -232,6 +235,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||||
difficulty.timeChanges = metadata.timeChanges;
|
difficulty.timeChanges = metadata.timeChanges;
|
||||||
difficulty.looped = metadata.looped;
|
difficulty.looped = metadata.looped;
|
||||||
difficulty.generatedBy = metadata.generatedBy;
|
difficulty.generatedBy = metadata.generatedBy;
|
||||||
|
difficulty.offsets = metadata?.offsets ?? new SongOffsets();
|
||||||
|
|
||||||
difficulty.stage = metadata.playData.stage;
|
difficulty.stage = metadata.playData.stage;
|
||||||
difficulty.noteStyle = metadata.playData.noteStyle;
|
difficulty.noteStyle = metadata.playData.noteStyle;
|
||||||
|
@ -405,6 +409,9 @@ class SongDifficulty
|
||||||
|
|
||||||
public var scrollSpeed:Float = Constants.DEFAULT_SCROLLSPEED;
|
public var scrollSpeed:Float = Constants.DEFAULT_SCROLLSPEED;
|
||||||
|
|
||||||
|
public var difficultyRating:Int = 0;
|
||||||
|
public var album:Null<String> = null;
|
||||||
|
|
||||||
public function new(song:Song, diffId:String, variation:String)
|
public function new(song:Song, diffId:String, variation:String)
|
||||||
{
|
{
|
||||||
this.song = song;
|
this.song = song;
|
||||||
|
|
|
@ -5,13 +5,16 @@ import flixel.group.FlxSpriteGroup;
|
||||||
import flixel.math.FlxPoint;
|
import flixel.math.FlxPoint;
|
||||||
import flixel.system.FlxAssets.FlxShader;
|
import flixel.system.FlxAssets.FlxShader;
|
||||||
import flixel.util.FlxSort;
|
import flixel.util.FlxSort;
|
||||||
|
import flixel.util.FlxColor;
|
||||||
import funkin.modding.IScriptedClass;
|
import funkin.modding.IScriptedClass;
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
import funkin.modding.events.ScriptEventType;
|
import funkin.modding.events.ScriptEventType;
|
||||||
import funkin.modding.events.ScriptEventDispatcher;
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.play.character.BaseCharacter;
|
import funkin.play.character.BaseCharacter;
|
||||||
import funkin.play.stage.StageData.StageDataCharacter;
|
import funkin.data.IRegistryEntry;
|
||||||
import funkin.play.stage.StageData.StageDataParser;
|
import funkin.data.stage.StageData;
|
||||||
|
import funkin.data.stage.StageData.StageDataCharacter;
|
||||||
|
import funkin.data.stage.StageRegistry;
|
||||||
import funkin.play.stage.StageProp;
|
import funkin.play.stage.StageProp;
|
||||||
import funkin.util.SortUtil;
|
import funkin.util.SortUtil;
|
||||||
import funkin.util.assets.FlxAnimationUtil;
|
import funkin.util.assets.FlxAnimationUtil;
|
||||||
|
@ -23,14 +26,25 @@ typedef StagePropGroup = FlxTypedSpriteGroup<StageProp>;
|
||||||
*
|
*
|
||||||
* A Stage is comprised of one or more props, each of which is a FlxSprite.
|
* A Stage is comprised of one or more props, each of which is a FlxSprite.
|
||||||
*/
|
*/
|
||||||
class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements IRegistryEntry<StageData>
|
||||||
{
|
{
|
||||||
public final stageId:String;
|
public final id:String;
|
||||||
public final stageName:String;
|
|
||||||
|
|
||||||
final _data:StageData;
|
public final _data:StageData;
|
||||||
|
|
||||||
public var camZoom:Float = 1.0;
|
public var stageName(get, never):String;
|
||||||
|
|
||||||
|
function get_stageName():String
|
||||||
|
{
|
||||||
|
return _data?.name ?? 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
public var camZoom(get, never):Float;
|
||||||
|
|
||||||
|
function get_camZoom():Float
|
||||||
|
{
|
||||||
|
return _data?.cameraZoom ?? 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
var namedProps:Map<String, StageProp> = new Map<String, StageProp>();
|
var namedProps:Map<String, StageProp> = new Map<String, StageProp>();
|
||||||
var characters:Map<String, BaseCharacter> = new Map<String, BaseCharacter>();
|
var characters:Map<String, BaseCharacter> = new Map<String, BaseCharacter>();
|
||||||
|
@ -41,21 +55,18 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
||||||
* They're used to cache the data needed to build the stage,
|
* They're used to cache the data needed to build the stage,
|
||||||
* then accessed and fleshed out when the stage needs to be built.
|
* then accessed and fleshed out when the stage needs to be built.
|
||||||
*
|
*
|
||||||
* @param stageId
|
* @param id
|
||||||
*/
|
*/
|
||||||
public function new(stageId:String)
|
public function new(id:String)
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.stageId = stageId;
|
this.id = id;
|
||||||
_data = StageDataParser.parseStageData(this.stageId);
|
_data = _fetchData(id);
|
||||||
|
|
||||||
if (_data == null)
|
if (_data == null)
|
||||||
{
|
{
|
||||||
throw 'Could not find stage data for stageId: $stageId';
|
throw 'Could not find stage data for stage id: $id';
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.stageName = _data.name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,9 +140,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
||||||
*/
|
*/
|
||||||
function buildStage():Void
|
function buildStage():Void
|
||||||
{
|
{
|
||||||
trace('Building stage for display: ${this.stageId}');
|
trace('Building stage for display: ${this.id}');
|
||||||
|
|
||||||
this.camZoom = _data.cameraZoom;
|
|
||||||
|
|
||||||
this.debugIconGroup = new FlxSpriteGroup();
|
this.debugIconGroup = new FlxSpriteGroup();
|
||||||
|
|
||||||
|
@ -139,6 +148,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
||||||
{
|
{
|
||||||
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
|
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
|
||||||
|
|
||||||
|
var isSolidColor = dataProp.assetPath.startsWith('#');
|
||||||
var isAnimated = dataProp.animations.length > 0;
|
var isAnimated = dataProp.animations.length > 0;
|
||||||
|
|
||||||
var propSprite:StageProp;
|
var propSprite:StageProp;
|
||||||
|
@ -162,6 +172,22 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
||||||
propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath);
|
propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (isSolidColor)
|
||||||
|
{
|
||||||
|
var width:Int = 1;
|
||||||
|
var height:Int = 1;
|
||||||
|
switch (dataProp.scale)
|
||||||
|
{
|
||||||
|
case Left(value):
|
||||||
|
width = Std.int(value);
|
||||||
|
height = Std.int(value);
|
||||||
|
|
||||||
|
case Right(values):
|
||||||
|
width = Std.int(values[0]);
|
||||||
|
height = Std.int(values[1]);
|
||||||
|
}
|
||||||
|
propSprite.makeSolidColor(width, height, FlxColor.fromString(dataProp.assetPath));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Initalize static sprite.
|
// Initalize static sprite.
|
||||||
|
@ -177,13 +203,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (dataProp.scale)
|
if (!isSolidColor)
|
||||||
{
|
{
|
||||||
case Left(value):
|
switch (dataProp.scale)
|
||||||
propSprite.scale.set(value);
|
{
|
||||||
|
case Left(value):
|
||||||
|
propSprite.scale.set(value);
|
||||||
|
|
||||||
case Right(values):
|
case Right(values):
|
||||||
propSprite.scale.set(values[0], values[1]);
|
propSprite.scale.set(values[0], values[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
propSprite.updateHitbox();
|
propSprite.updateHitbox();
|
||||||
|
|
||||||
|
@ -195,15 +224,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
||||||
// If pixel, disable antialiasing.
|
// If pixel, disable antialiasing.
|
||||||
propSprite.antialiasing = !dataProp.isPixel;
|
propSprite.antialiasing = !dataProp.isPixel;
|
||||||
|
|
||||||
switch (dataProp.scroll)
|
propSprite.scrollFactor.x = dataProp.scroll[0];
|
||||||
{
|
propSprite.scrollFactor.y = dataProp.scroll[1];
|
||||||
case Left(value):
|
|
||||||
propSprite.scrollFactor.x = value;
|
|
||||||
propSprite.scrollFactor.y = value;
|
|
||||||
case Right(values):
|
|
||||||
propSprite.scrollFactor.x = values[0];
|
|
||||||
propSprite.scrollFactor.y = values[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
propSprite.zIndex = dataProp.zIndex;
|
propSprite.zIndex = dataProp.zIndex;
|
||||||
|
|
||||||
|
@ -731,6 +753,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
||||||
return Sprite;
|
return Sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static function _fetchData(id:String):Null<StageData>
|
||||||
|
{
|
||||||
|
return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));
|
||||||
|
}
|
||||||
|
|
||||||
public function onScriptEvent(event:ScriptEvent) {}
|
public function onScriptEvent(event:ScriptEvent) {}
|
||||||
|
|
||||||
public function onPause(event:PauseScriptEvent) {}
|
public function onPause(event:PauseScriptEvent) {}
|
||||||
|
|
|
@ -1,548 +0,0 @@
|
||||||
package funkin.play.stage;
|
|
||||||
|
|
||||||
import funkin.data.animation.AnimationData;
|
|
||||||
import funkin.play.stage.ScriptedStage;
|
|
||||||
import funkin.play.stage.Stage;
|
|
||||||
import funkin.util.VersionUtil;
|
|
||||||
import funkin.util.assets.DataAssets;
|
|
||||||
import haxe.Json;
|
|
||||||
import openfl.Assets;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains utilities for loading and parsing stage data.
|
|
||||||
*/
|
|
||||||
class StageDataParser
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The current version string for the stage data format.
|
|
||||||
* Handle breaking changes by incrementing this value
|
|
||||||
* and adding migration to the `migrateStageData()` function.
|
|
||||||
*/
|
|
||||||
public static final STAGE_DATA_VERSION:String = "1.0.0";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current version rule check for the stage data format.
|
|
||||||
*/
|
|
||||||
public static final STAGE_DATA_VERSION_RULE:String = "1.0.x";
|
|
||||||
|
|
||||||
static final stageCache:Map<String, Stage> = new Map<String, Stage>();
|
|
||||||
|
|
||||||
static final DEFAULT_STAGE_ID = 'UNKNOWN';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and preloads the game's stage data and scripts when the game starts.
|
|
||||||
*
|
|
||||||
* If you want to force stages to be reloaded, you can just call this function again.
|
|
||||||
*/
|
|
||||||
public static function loadStageCache():Void
|
|
||||||
{
|
|
||||||
// Clear any stages that are cached if there were any.
|
|
||||||
clearStageCache();
|
|
||||||
trace("Loading stage cache...");
|
|
||||||
|
|
||||||
//
|
|
||||||
// SCRIPTED STAGES
|
|
||||||
//
|
|
||||||
var scriptedStageClassNames:Array<String> = ScriptedStage.listScriptClasses();
|
|
||||||
trace(' Instantiating ${scriptedStageClassNames.length} scripted stages...');
|
|
||||||
for (stageCls in scriptedStageClassNames)
|
|
||||||
{
|
|
||||||
var stage:Stage = ScriptedStage.init(stageCls, DEFAULT_STAGE_ID);
|
|
||||||
if (stage != null)
|
|
||||||
{
|
|
||||||
trace(' Loaded scripted stage: ${stage.stageName}');
|
|
||||||
// Disable the rendering logic for stage until it's loaded.
|
|
||||||
// Note that kill() =/= destroy()
|
|
||||||
stage.kill();
|
|
||||||
|
|
||||||
// Then store it.
|
|
||||||
stageCache.set(stage.stageId, stage);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace(' Failed to instantiate scripted stage class: ${stageCls}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// UNSCRIPTED STAGES
|
|
||||||
//
|
|
||||||
var stageIdList:Array<String> = DataAssets.listDataFilesInPath('stages/');
|
|
||||||
var unscriptedStageIds:Array<String> = stageIdList.filter(function(stageId:String):Bool {
|
|
||||||
return !stageCache.exists(stageId);
|
|
||||||
});
|
|
||||||
trace(' Instantiating ${unscriptedStageIds.length} non-scripted stages...');
|
|
||||||
for (stageId in unscriptedStageIds)
|
|
||||||
{
|
|
||||||
var stage:Stage;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
stage = new Stage(stageId);
|
|
||||||
if (stage != null)
|
|
||||||
{
|
|
||||||
trace(' Loaded stage data: ${stage.stageName}');
|
|
||||||
stageCache.set(stageId, stage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
trace(' An error occurred while loading stage data: ${stageId}');
|
|
||||||
// Assume error was already logged.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trace(' Successfully loaded ${Lambda.count(stageCache)} stages.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fetchStage(stageId:String):Null<Stage>
|
|
||||||
{
|
|
||||||
if (stageCache.exists(stageId))
|
|
||||||
{
|
|
||||||
trace('Successfully fetch stage: ${stageId}');
|
|
||||||
var stage:Stage = stageCache.get(stageId);
|
|
||||||
stage.revive();
|
|
||||||
return stage;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace('Failed to fetch stage, not found in cache: ${stageId}');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static function clearStageCache():Void
|
|
||||||
{
|
|
||||||
if (stageCache != null)
|
|
||||||
{
|
|
||||||
for (stage in stageCache)
|
|
||||||
{
|
|
||||||
stage.destroy();
|
|
||||||
}
|
|
||||||
stageCache.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a stage's JSON file, parse its data, and return it.
|
|
||||||
*
|
|
||||||
* @param stageId The stage to load.
|
|
||||||
* @return The stage data, or null if validation failed.
|
|
||||||
*/
|
|
||||||
public static function parseStageData(stageId:String):Null<StageData>
|
|
||||||
{
|
|
||||||
var rawJson:String = loadStageFile(stageId);
|
|
||||||
|
|
||||||
var stageData:StageData = migrateStageData(rawJson, stageId);
|
|
||||||
|
|
||||||
return validateStageData(stageId, stageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function listStageIds():Array<String>
|
|
||||||
{
|
|
||||||
return stageCache.keys().array();
|
|
||||||
}
|
|
||||||
|
|
||||||
static function loadStageFile(stagePath:String):String
|
|
||||||
{
|
|
||||||
var stageFilePath:String = Paths.json('stages/${stagePath}');
|
|
||||||
var rawJson = Assets.getText(stageFilePath).trim();
|
|
||||||
|
|
||||||
while (!rawJson.endsWith("}"))
|
|
||||||
{
|
|
||||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
static function migrateStageData(rawJson:String, stageId:String):Null<StageData>
|
|
||||||
{
|
|
||||||
// If you update the stage data format in a breaking way,
|
|
||||||
// handle migration here by checking the `version` value.
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var parser = new json2object.JsonParser<StageData>();
|
|
||||||
parser.ignoreUnknownVariables = false;
|
|
||||||
parser.fromJson(rawJson, '$stageId.json');
|
|
||||||
|
|
||||||
if (parser.errors.length > 0)
|
|
||||||
{
|
|
||||||
trace('[STAGE] Failed to parse stage data');
|
|
||||||
|
|
||||||
for (error in parser.errors)
|
|
||||||
funkin.data.DataError.printError(error);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return parser.value;
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
trace(' Error parsing data for stage: ${stageId}');
|
|
||||||
trace(' ${e}');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static final DEFAULT_ANIMTYPE:String = "sparrow";
|
|
||||||
static final DEFAULT_CAMERAZOOM:Float = 1.0;
|
|
||||||
static final DEFAULT_DANCEEVERY:Int = 0;
|
|
||||||
static final DEFAULT_ISPIXEL:Bool = false;
|
|
||||||
static final DEFAULT_NAME:String = "Untitled Stage";
|
|
||||||
static final DEFAULT_OFFSETS:Array<Float> = [0, 0];
|
|
||||||
static final DEFAULT_CAMERA_OFFSETS_BF:Array<Float> = [-100, -100];
|
|
||||||
static final DEFAULT_CAMERA_OFFSETS_DAD:Array<Float> = [150, -100];
|
|
||||||
static final DEFAULT_POSITION:Array<Float> = [0, 0];
|
|
||||||
static final DEFAULT_SCALE:Float = 1.0;
|
|
||||||
static final DEFAULT_ALPHA:Float = 1.0;
|
|
||||||
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
|
|
||||||
static final DEFAULT_ZINDEX:Int = 0;
|
|
||||||
|
|
||||||
static final DEFAULT_CHARACTER_DATA:StageDataCharacter =
|
|
||||||
{
|
|
||||||
zIndex: DEFAULT_ZINDEX,
|
|
||||||
position: DEFAULT_POSITION,
|
|
||||||
cameraOffsets: DEFAULT_OFFSETS,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set unspecified parameters to their defaults.
|
|
||||||
* If the parameter is mandatory, print an error message.
|
|
||||||
* @param id
|
|
||||||
* @param input
|
|
||||||
* @return The validated stage data
|
|
||||||
*/
|
|
||||||
static function validateStageData(id:String, input:StageData):Null<StageData>
|
|
||||||
{
|
|
||||||
if (input == null)
|
|
||||||
{
|
|
||||||
trace('ERROR: Could not parse stage data for "${id}".');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.version == null)
|
|
||||||
{
|
|
||||||
trace('ERROR: Could not load stage data for "$id": missing version');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!VersionUtil.validateVersionStr(input.version, STAGE_DATA_VERSION_RULE))
|
|
||||||
{
|
|
||||||
trace('ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.name == null)
|
|
||||||
{
|
|
||||||
trace('WARN: Stage data for "$id" missing name');
|
|
||||||
input.name = DEFAULT_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.cameraZoom == null)
|
|
||||||
{
|
|
||||||
input.cameraZoom = DEFAULT_CAMERAZOOM;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.props == null)
|
|
||||||
{
|
|
||||||
input.props = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (inputProp in input.props)
|
|
||||||
{
|
|
||||||
// It's fine for inputProp.name to be null
|
|
||||||
|
|
||||||
if (inputProp.assetPath == null)
|
|
||||||
{
|
|
||||||
trace('ERROR: Could not load stage data for "$id": missing assetPath for prop "${inputProp.name}"');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputProp.position == null)
|
|
||||||
{
|
|
||||||
inputProp.position = DEFAULT_POSITION;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputProp.zIndex == null)
|
|
||||||
{
|
|
||||||
inputProp.zIndex = DEFAULT_ZINDEX;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputProp.isPixel == null)
|
|
||||||
{
|
|
||||||
inputProp.isPixel = DEFAULT_ISPIXEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputProp.danceEvery == null)
|
|
||||||
{
|
|
||||||
inputProp.danceEvery = DEFAULT_DANCEEVERY;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputProp.animType == null)
|
|
||||||
{
|
|
||||||
inputProp.animType = DEFAULT_ANIMTYPE;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (inputProp.scale)
|
|
||||||
{
|
|
||||||
case null:
|
|
||||||
inputProp.scale = Right([DEFAULT_SCALE, DEFAULT_SCALE]);
|
|
||||||
case Left(value):
|
|
||||||
inputProp.scale = Right([value, value]);
|
|
||||||
case Right(_):
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (inputProp.scroll)
|
|
||||||
{
|
|
||||||
case null:
|
|
||||||
inputProp.scroll = Right(DEFAULT_SCROLL);
|
|
||||||
case Left(value):
|
|
||||||
inputProp.scroll = Right([value, value]);
|
|
||||||
case Right(_):
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputProp.alpha == null)
|
|
||||||
{
|
|
||||||
inputProp.alpha = DEFAULT_ALPHA;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputProp.animations == null)
|
|
||||||
{
|
|
||||||
inputProp.animations = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputProp.animations.length == 0 && inputProp.startingAnimation != null)
|
|
||||||
{
|
|
||||||
trace('ERROR: Could not load stage data for "$id": missing animations for prop "${inputProp.name}"');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (inputAnimation in inputProp.animations)
|
|
||||||
{
|
|
||||||
if (inputAnimation.name == null)
|
|
||||||
{
|
|
||||||
trace('ERROR: Could not load stage data for "$id": missing animation name for prop "${inputProp.name}"');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputAnimation.frameRate == null)
|
|
||||||
{
|
|
||||||
inputAnimation.frameRate = 24;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputAnimation.offsets == null)
|
|
||||||
{
|
|
||||||
inputAnimation.offsets = DEFAULT_OFFSETS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputAnimation.looped == null)
|
|
||||||
{
|
|
||||||
inputAnimation.looped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputAnimation.flipX == null)
|
|
||||||
{
|
|
||||||
inputAnimation.flipX = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputAnimation.flipY == null)
|
|
||||||
{
|
|
||||||
inputAnimation.flipY = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.characters == null)
|
|
||||||
{
|
|
||||||
trace('ERROR: Could not load stage data for "$id": missing characters');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.characters.bf == null)
|
|
||||||
{
|
|
||||||
input.characters.bf = DEFAULT_CHARACTER_DATA;
|
|
||||||
}
|
|
||||||
if (input.characters.dad == null)
|
|
||||||
{
|
|
||||||
input.characters.dad = DEFAULT_CHARACTER_DATA;
|
|
||||||
}
|
|
||||||
if (input.characters.gf == null)
|
|
||||||
{
|
|
||||||
input.characters.gf = DEFAULT_CHARACTER_DATA;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (inputCharacter in [input.characters.bf, input.characters.dad, input.characters.gf])
|
|
||||||
{
|
|
||||||
if (inputCharacter.position == null || inputCharacter.position.length != 2)
|
|
||||||
{
|
|
||||||
inputCharacter.position = [0, 0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All good!
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StageData
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The sematic version number of the stage data JSON format.
|
|
||||||
* Supports fancy comparisons like NPM does it's neat.
|
|
||||||
*/
|
|
||||||
public var version:String;
|
|
||||||
|
|
||||||
public var name:String;
|
|
||||||
public var cameraZoom:Null<Float>;
|
|
||||||
public var props:Array<StageDataProp>;
|
|
||||||
public var characters:StageDataCharacters;
|
|
||||||
|
|
||||||
public function new()
|
|
||||||
{
|
|
||||||
this.version = StageDataParser.STAGE_DATA_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert this StageData into a JSON string.
|
|
||||||
*/
|
|
||||||
public function serialize(pretty:Bool = true):String
|
|
||||||
{
|
|
||||||
var writer = new json2object.JsonWriter<StageData>();
|
|
||||||
return writer.write(this, pretty ? ' ' : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef StageDataCharacters =
|
|
||||||
{
|
|
||||||
var bf:StageDataCharacter;
|
|
||||||
var dad:StageDataCharacter;
|
|
||||||
var gf:StageDataCharacter;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef StageDataProp =
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name of the prop for later lookup by scripts.
|
|
||||||
* Optional; if unspecified, the prop can't be referenced by scripts.
|
|
||||||
*/
|
|
||||||
@:optional
|
|
||||||
var name:String;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The asset used to display the prop.
|
|
||||||
*/
|
|
||||||
var assetPath:String;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The position of the prop as an [x, y] array of two floats.
|
|
||||||
*/
|
|
||||||
var position:Array<Float>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A number determining the stack order of the prop, relative to other props and the characters in the stage.
|
|
||||||
* Props with lower numbers render below those with higher numbers.
|
|
||||||
* This is just like CSS, it isn't hard.
|
|
||||||
* @default 0
|
|
||||||
*/
|
|
||||||
@:optional
|
|
||||||
@:default(0)
|
|
||||||
var zIndex:Int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If set to true, anti-aliasing will be forcibly disabled on the sprite.
|
|
||||||
* This prevents blurry images on pixel-art levels.
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
@:optional
|
|
||||||
@:default(false)
|
|
||||||
var isPixel:Bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
|
|
||||||
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
|
|
||||||
*/
|
|
||||||
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
|
|
||||||
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
|
|
||||||
@:optional
|
|
||||||
var scale:haxe.ds.Either<Float, Array<Float>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The alpha of the prop, as a float.
|
|
||||||
* @default 1.0
|
|
||||||
*/
|
|
||||||
@:optional
|
|
||||||
@:default(1.0)
|
|
||||||
var alpha:Float;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If not zero, this prop will play an animation every X beats of the song.
|
|
||||||
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
|
|
||||||
* they will alternated between, otherwise the `idle` animation will be used.
|
|
||||||
*
|
|
||||||
* @default 0
|
|
||||||
*/
|
|
||||||
@:default(0)
|
|
||||||
@:optional
|
|
||||||
var danceEvery:Int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
|
|
||||||
* Represented as a float or as an [x, y] array of two floats.
|
|
||||||
* [1, 1] means the prop moves 1:1 with the camera.
|
|
||||||
* [0.5, 0.5] means the prop half as much as the camera.
|
|
||||||
* [0, 0] means the prop is not moved.
|
|
||||||
* @default [0, 0]
|
|
||||||
*/
|
|
||||||
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
|
|
||||||
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
|
|
||||||
@:optional
|
|
||||||
var scroll:haxe.ds.Either<Float, Array<Float>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An optional array of animations which the prop can play.
|
|
||||||
* @default Prop has no animations.
|
|
||||||
*/
|
|
||||||
@:optional
|
|
||||||
var animations:Array<AnimationData>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If animations are used, this is the name of the animation to play first.
|
|
||||||
* @default Don't play an animation.
|
|
||||||
*/
|
|
||||||
@:optional
|
|
||||||
var startingAnimation:Null<String>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The animation type to use.
|
|
||||||
* Options: "sparrow", "packer"
|
|
||||||
* @default "sparrow"
|
|
||||||
*/
|
|
||||||
@:default("sparrow")
|
|
||||||
@:optional
|
|
||||||
var animType:String;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef StageDataCharacter =
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A number determining the stack order of the character, relative to props and other characters in the stage.
|
|
||||||
* Again, just like CSS.
|
|
||||||
* @default 0
|
|
||||||
*/
|
|
||||||
var zIndex:Int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The position to render the character at.
|
|
||||||
*/
|
|
||||||
var position:Array<Float>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The camera offsets to apply when focusing on the character on this stage.
|
|
||||||
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
|
|
||||||
*/
|
|
||||||
var cameraOffsets:Array<Float>;
|
|
||||||
};
|
|
|
@ -1,10 +1,10 @@
|
||||||
package funkin.play.stage;
|
package funkin.play.stage;
|
||||||
|
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
import flixel.FlxSprite;
|
import funkin.graphics.FunkinSprite;
|
||||||
import funkin.modding.IScriptedClass.IStateStageProp;
|
import funkin.modding.IScriptedClass.IStateStageProp;
|
||||||
|
|
||||||
class StageProp extends FlxSprite implements IStateStageProp
|
class StageProp extends FunkinSprite implements IStateStageProp
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* An internal name for this prop.
|
* An internal name for this prop.
|
||||||
|
|
|
@ -14,7 +14,7 @@ import thx.semver.Version;
|
||||||
@:forward(volume, mute)
|
@:forward(volume, mute)
|
||||||
abstract Save(RawSaveData)
|
abstract Save(RawSaveData)
|
||||||
{
|
{
|
||||||
// Version 2.0.1 adds attributes to `optionsChartEditor`, that should return default values if they are null.
|
// Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
|
||||||
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2";
|
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2";
|
||||||
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
|
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
|
||||||
|
|
||||||
|
@ -110,9 +110,7 @@ abstract Save(RawSaveData)
|
||||||
metronomeVolume: 1.0,
|
metronomeVolume: 1.0,
|
||||||
hitsoundsEnabledPlayer: true,
|
hitsoundsEnabledPlayer: true,
|
||||||
hitsoundsEnabledOpponent: true,
|
hitsoundsEnabledOpponent: true,
|
||||||
instVolume: 1.0,
|
themeMusic: true
|
||||||
voicesVolume: 1.0,
|
|
||||||
playbackSpeed: 1.0,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -349,38 +347,21 @@ abstract Save(RawSaveData)
|
||||||
return this.optionsChartEditor.hitsoundsEnabledOpponent;
|
return this.optionsChartEditor.hitsoundsEnabledOpponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public var chartEditorInstVolume(get, set):Float;
|
public var chartEditorThemeMusic(get, set):Bool;
|
||||||
|
|
||||||
function get_chartEditorInstVolume():Float
|
function get_chartEditorThemeMusic():Bool
|
||||||
{
|
{
|
||||||
if (this.optionsChartEditor.instVolume == null) this.optionsChartEditor.instVolume = 1.0;
|
if (this.optionsChartEditor.themeMusic == null) this.optionsChartEditor.themeMusic = true;
|
||||||
|
|
||||||
return this.optionsChartEditor.instVolume;
|
return this.optionsChartEditor.themeMusic;
|
||||||
}
|
}
|
||||||
|
|
||||||
function set_chartEditorInstVolume(value:Float):Float
|
function set_chartEditorThemeMusic(value:Bool):Bool
|
||||||
{
|
{
|
||||||
// Set and apply.
|
// Set and apply.
|
||||||
this.optionsChartEditor.instVolume = value;
|
this.optionsChartEditor.themeMusic = value;
|
||||||
flush();
|
flush();
|
||||||
return this.optionsChartEditor.instVolume;
|
return this.optionsChartEditor.themeMusic;
|
||||||
}
|
|
||||||
|
|
||||||
public var chartEditorVoicesVolume(get, set):Float;
|
|
||||||
|
|
||||||
function get_chartEditorVoicesVolume():Float
|
|
||||||
{
|
|
||||||
if (this.optionsChartEditor.voicesVolume == null) this.optionsChartEditor.voicesVolume = 1.0;
|
|
||||||
|
|
||||||
return this.optionsChartEditor.voicesVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_chartEditorVoicesVolume(value:Float):Float
|
|
||||||
{
|
|
||||||
// Set and apply.
|
|
||||||
this.optionsChartEditor.voicesVolume = value;
|
|
||||||
flush();
|
|
||||||
return this.optionsChartEditor.voicesVolume;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var chartEditorPlaybackSpeed(get, set):Float;
|
public var chartEditorPlaybackSpeed(get, set):Float;
|
||||||
|
@ -776,7 +757,6 @@ typedef SaveScoreData =
|
||||||
|
|
||||||
typedef SaveScoreTallyData =
|
typedef SaveScoreTallyData =
|
||||||
{
|
{
|
||||||
var killer:Int;
|
|
||||||
var sick:Int;
|
var sick:Int;
|
||||||
var good:Int;
|
var good:Int;
|
||||||
var bad:Int;
|
var bad:Int;
|
||||||
|
@ -1041,6 +1021,12 @@ typedef SaveDataChartEditorOptions =
|
||||||
*/
|
*/
|
||||||
var ?hitsoundsEnabledOpponent:Bool;
|
var ?hitsoundsEnabledOpponent:Bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme music in the Chart Editor.
|
||||||
|
* @default `true`
|
||||||
|
*/
|
||||||
|
var ?themeMusic:Bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumental volume in the Chart Editor.
|
* Instrumental volume in the Chart Editor.
|
||||||
* @default `1.0`
|
* @default `1.0`
|
||||||
|
|
|
@ -120,7 +120,6 @@ class SaveDataMigrator
|
||||||
accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
|
accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
|
||||||
tallies:
|
tallies:
|
||||||
{
|
{
|
||||||
killer: 0,
|
|
||||||
sick: 0,
|
sick: 0,
|
||||||
good: 0,
|
good: 0,
|
||||||
bad: 0,
|
bad: 0,
|
||||||
|
@ -140,7 +139,6 @@ class SaveDataMigrator
|
||||||
accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
|
accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
|
||||||
tallies:
|
tallies:
|
||||||
{
|
{
|
||||||
killer: 0,
|
|
||||||
sick: 0,
|
sick: 0,
|
||||||
good: 0,
|
good: 0,
|
||||||
bad: 0,
|
bad: 0,
|
||||||
|
@ -160,7 +158,6 @@ class SaveDataMigrator
|
||||||
accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
|
accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
|
||||||
tallies:
|
tallies:
|
||||||
{
|
{
|
||||||
killer: 0,
|
|
||||||
sick: 0,
|
sick: 0,
|
||||||
good: 0,
|
good: 0,
|
||||||
bad: 0,
|
bad: 0,
|
||||||
|
@ -183,7 +180,6 @@ class SaveDataMigrator
|
||||||
accuracy: 0,
|
accuracy: 0,
|
||||||
tallies:
|
tallies:
|
||||||
{
|
{
|
||||||
killer: 0,
|
|
||||||
sick: 0,
|
sick: 0,
|
||||||
good: 0,
|
good: 0,
|
||||||
bad: 0,
|
bad: 0,
|
||||||
|
@ -209,7 +205,6 @@ class SaveDataMigrator
|
||||||
accuracy: 0,
|
accuracy: 0,
|
||||||
tallies:
|
tallies:
|
||||||
{
|
{
|
||||||
killer: 0,
|
|
||||||
sick: 0,
|
sick: 0,
|
||||||
good: 0,
|
good: 0,
|
||||||
bad: 0,
|
bad: 0,
|
||||||
|
@ -235,7 +230,6 @@ class SaveDataMigrator
|
||||||
accuracy: 0,
|
accuracy: 0,
|
||||||
tallies:
|
tallies:
|
||||||
{
|
{
|
||||||
killer: 0,
|
|
||||||
sick: 0,
|
sick: 0,
|
||||||
good: 0,
|
good: 0,
|
||||||
bad: 0,
|
bad: 0,
|
||||||
|
|
|
@ -274,4 +274,5 @@ enum abstract AtlasFont(String) from String to String
|
||||||
{
|
{
|
||||||
var DEFAULT = "default";
|
var DEFAULT = "default";
|
||||||
var BOLD = "bold";
|
var BOLD = "bold";
|
||||||
|
var FREEPLAY_CLEAR = "freeplay-clear";
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -59,6 +59,12 @@ class AddEventsCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (events.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
var len:Int = events.length;
|
var len:Int = events.length;
|
||||||
|
|
|
@ -59,6 +59,12 @@ class AddNotesCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (notes.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
if (notes.length == 1)
|
if (notes.length == 1)
|
||||||
|
|
|
@ -64,6 +64,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
|
||||||
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (targetBPM != previousBPM);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
return 'Change Starting BPM to ${targetBPM}';
|
return 'Change Starting BPM to ${targetBPM}';
|
||||||
|
|
|
@ -6,6 +6,8 @@ package funkin.ui.debug.charting.commands;
|
||||||
*
|
*
|
||||||
* To make a functionality compatible with the undo/redo history, create a new class
|
* To make a functionality compatible with the undo/redo history, create a new class
|
||||||
* that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())`
|
* that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())`
|
||||||
|
*
|
||||||
|
* NOTE: Make the constructor very simple, as it may be called without executing by the command palette.
|
||||||
*/
|
*/
|
||||||
interface ChartEditorCommand
|
interface ChartEditorCommand
|
||||||
{
|
{
|
||||||
|
@ -22,6 +24,15 @@ interface ChartEditorCommand
|
||||||
*/
|
*/
|
||||||
public function undo(state:ChartEditorState):Void;
|
public function undo(state:ChartEditorState):Void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether or not this command should be appended to the in the undo/redo history.
|
||||||
|
* Generally this should be true, it should only be false if the command is minor and non-destructive,
|
||||||
|
* like copying to the clipboard.
|
||||||
|
*
|
||||||
|
* Called after `execute()` is performed.
|
||||||
|
*/
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a short description of the action (for the UI).
|
* Get a short description of the action (for the UI).
|
||||||
* For example, return `Add Left Note` to display `Undo Add Left Note` in the menu.
|
* For example, return `Add Left Note` to display `Undo Add Left Note` in the menu.
|
||||||
|
|
144
source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
Normal file
144
source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
package funkin.ui.debug.charting.commands;
|
||||||
|
|
||||||
|
import funkin.data.song.SongData.SongNoteData;
|
||||||
|
import funkin.data.song.SongData.SongEventData;
|
||||||
|
import funkin.data.song.SongDataUtils;
|
||||||
|
import flixel.tweens.FlxEase;
|
||||||
|
import flixel.tweens.FlxTween;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command that copies a given set of notes and song events to the clipboard,
|
||||||
|
* without deleting them from the chart editor.
|
||||||
|
*/
|
||||||
|
@:nullSafety
|
||||||
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
|
class CopyItemsCommand implements ChartEditorCommand
|
||||||
|
{
|
||||||
|
var notes:Array<SongNoteData>;
|
||||||
|
var events:Array<SongEventData>;
|
||||||
|
|
||||||
|
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
|
||||||
|
{
|
||||||
|
this.notes = notes;
|
||||||
|
this.events = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(state:ChartEditorState):Void
|
||||||
|
{
|
||||||
|
// Calculate a single time offset for all the notes and events.
|
||||||
|
var timeOffset:Null<Int> = state.currentNoteSelection.length > 0 ? Std.int(state.currentNoteSelection[0].time) : null;
|
||||||
|
if (state.currentEventSelection.length > 0)
|
||||||
|
{
|
||||||
|
if (timeOffset == null || state.currentEventSelection[0].time < timeOffset)
|
||||||
|
{
|
||||||
|
timeOffset = Std.int(state.currentEventSelection[0].time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SongDataUtils.writeItemsToClipboard(
|
||||||
|
{
|
||||||
|
notes: SongDataUtils.buildNoteClipboard(state.currentNoteSelection, timeOffset),
|
||||||
|
events: SongDataUtils.buildEventClipboard(state.currentEventSelection, timeOffset),
|
||||||
|
});
|
||||||
|
|
||||||
|
performVisuals(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function performVisuals(state:ChartEditorState):Void
|
||||||
|
{
|
||||||
|
if (state.currentNoteSelection.length > 0)
|
||||||
|
{
|
||||||
|
// Display the "Copied Notes" text.
|
||||||
|
if (state.txtCopyNotif != null)
|
||||||
|
{
|
||||||
|
state.txtCopyNotif.visible = true;
|
||||||
|
state.txtCopyNotif.text = "Copied " + state.currentNoteSelection.length + " notes to clipboard";
|
||||||
|
state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2);
|
||||||
|
state.txtCopyNotif.y = FlxG.mouse.y - 16;
|
||||||
|
FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5,
|
||||||
|
{
|
||||||
|
type: FlxTween.ONESHOT,
|
||||||
|
ease: FlxEase.quadOut,
|
||||||
|
onComplete: function(_) {
|
||||||
|
state.txtCopyNotif.visible = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wiggle the notes.
|
||||||
|
for (note in state.renderedNotes.members)
|
||||||
|
{
|
||||||
|
if (state.isNoteSelected(note.noteData))
|
||||||
|
{
|
||||||
|
FlxTween.globalManager.cancelTweensOf(note);
|
||||||
|
FlxTween.globalManager.cancelTweensOf(note.scale);
|
||||||
|
note.playNoteAnimation();
|
||||||
|
var prevX:Float = note.scale.x;
|
||||||
|
var prevY:Float = note.scale.y;
|
||||||
|
|
||||||
|
note.scale.x *= 1.2;
|
||||||
|
note.scale.y *= 1.2;
|
||||||
|
|
||||||
|
note.angle = FlxG.random.bool() ? -10 : 10;
|
||||||
|
FlxTween.tween(note, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut});
|
||||||
|
|
||||||
|
FlxTween.tween(note.scale, {"y": prevX, "x": prevY}, 0.7,
|
||||||
|
{
|
||||||
|
ease: FlxEase.elasticOut,
|
||||||
|
onComplete: function(_) {
|
||||||
|
note.playNoteAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wiggle the events.
|
||||||
|
for (event in state.renderedEvents.members)
|
||||||
|
{
|
||||||
|
if (state.isEventSelected(event.eventData))
|
||||||
|
{
|
||||||
|
FlxTween.globalManager.cancelTweensOf(event);
|
||||||
|
FlxTween.globalManager.cancelTweensOf(event.scale);
|
||||||
|
event.playAnimation();
|
||||||
|
var prevX:Float = event.scale.x;
|
||||||
|
var prevY:Float = event.scale.y;
|
||||||
|
|
||||||
|
event.scale.x *= 1.2;
|
||||||
|
event.scale.y *= 1.2;
|
||||||
|
|
||||||
|
event.angle = FlxG.random.bool() ? -10 : 10;
|
||||||
|
FlxTween.tween(event, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut});
|
||||||
|
|
||||||
|
FlxTween.tween(event.scale, {"y": prevX, "x": prevY}, 0.7,
|
||||||
|
{
|
||||||
|
ease: FlxEase.elasticOut,
|
||||||
|
onComplete: function(_) {
|
||||||
|
event.playAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function undo(state:ChartEditorState):Void
|
||||||
|
{
|
||||||
|
// This command is not undoable. Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is not undoable. Don't add it to the history.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toString():String
|
||||||
|
{
|
||||||
|
var len:Int = notes.length + events.length;
|
||||||
|
|
||||||
|
if (notes.length == 0) return 'Copy $len Events to Clipboard';
|
||||||
|
else if (events.length == 0) return 'Copy $len Notes to Clipboard';
|
||||||
|
else
|
||||||
|
return 'Copy $len Items to Clipboard';
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,6 +56,12 @@ class CutItemsCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Always add it to the history.
|
||||||
|
return (notes.length > 0 || events.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
var len:Int = notes.length + events.length;
|
var len:Int = notes.length + events.length;
|
||||||
|
|
|
@ -10,17 +10,16 @@ import funkin.data.song.SongData.SongEventData;
|
||||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
class DeselectAllItemsCommand implements ChartEditorCommand
|
class DeselectAllItemsCommand implements ChartEditorCommand
|
||||||
{
|
{
|
||||||
var previousNoteSelection:Array<SongNoteData>;
|
var previousNoteSelection:Array<SongNoteData> = [];
|
||||||
var previousEventSelection:Array<SongEventData>;
|
var previousEventSelection:Array<SongEventData> = [];
|
||||||
|
|
||||||
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
|
public function new() {}
|
||||||
{
|
|
||||||
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
|
|
||||||
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function execute(state:ChartEditorState):Void
|
public function execute(state:ChartEditorState):Void
|
||||||
{
|
{
|
||||||
|
this.previousNoteSelection = state.currentNoteSelection;
|
||||||
|
this.previousEventSelection = state.currentEventSelection;
|
||||||
|
|
||||||
state.currentNoteSelection = [];
|
state.currentNoteSelection = [];
|
||||||
state.currentEventSelection = [];
|
state.currentEventSelection = [];
|
||||||
|
|
||||||
|
@ -35,6 +34,12 @@ class DeselectAllItemsCommand implements ChartEditorCommand
|
||||||
state.noteDisplayDirty = true;
|
state.noteDisplayDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (previousNoteSelection.length > 0 || previousEventSelection.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
return 'Deselect All Items';
|
return 'Deselect All Items';
|
||||||
|
|
|
@ -45,16 +45,27 @@ class DeselectItemsCommand implements ChartEditorCommand
|
||||||
state.notePreviewDirty = true;
|
state.notePreviewDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (notes.length > 0 || events.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
var noteCount = notes.length + events.length;
|
var isPlural = (notes.length + events.length) > 1;
|
||||||
|
var notesOnly = (notes.length > 0 && events.length == 0);
|
||||||
|
var eventsOnly = (notes.length == 0 && events.length > 0);
|
||||||
|
|
||||||
if (noteCount == 1)
|
if (notesOnly)
|
||||||
{
|
{
|
||||||
var dir:String = notes[0].getDirectionName();
|
return 'Deselect ${notes.length} ${isPlural ? 'Notes' : 'Note'}';
|
||||||
return 'Deselect $dir Items';
|
}
|
||||||
|
else if (eventsOnly)
|
||||||
|
{
|
||||||
|
return 'Deselect ${events.length} ${isPlural ? 'Events' : 'Event'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Deselect ${noteCount} Items';
|
return 'Deselect ${notes.length + events.length} Items';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,17 +13,25 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
||||||
var note:SongNoteData;
|
var note:SongNoteData;
|
||||||
var oldLength:Float;
|
var oldLength:Float;
|
||||||
var newLength:Float;
|
var newLength:Float;
|
||||||
|
var unit:Unit;
|
||||||
|
|
||||||
public function new(note:SongNoteData, newLength:Float)
|
public function new(note:SongNoteData, newLength:Float, unit:Unit = MILLISECONDS)
|
||||||
{
|
{
|
||||||
this.note = note;
|
this.note = note;
|
||||||
this.oldLength = note.length;
|
this.oldLength = note.length;
|
||||||
this.newLength = newLength;
|
this.newLength = newLength;
|
||||||
|
this.unit = unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(state:ChartEditorState):Void
|
public function execute(state:ChartEditorState):Void
|
||||||
{
|
{
|
||||||
note.length = newLength;
|
switch (unit)
|
||||||
|
{
|
||||||
|
case MILLISECONDS:
|
||||||
|
this.note.length = newLength;
|
||||||
|
case STEPS:
|
||||||
|
this.note.setStepLength(newLength);
|
||||||
|
}
|
||||||
|
|
||||||
state.saveDataDirty = true;
|
state.saveDataDirty = true;
|
||||||
state.noteDisplayDirty = true;
|
state.noteDisplayDirty = true;
|
||||||
|
@ -36,7 +44,8 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
||||||
{
|
{
|
||||||
state.playSound(Paths.sound('chartingSounds/undo'));
|
state.playSound(Paths.sound('chartingSounds/undo'));
|
||||||
|
|
||||||
note.length = oldLength;
|
// Always use milliseconds for undoing
|
||||||
|
this.note.length = oldLength;
|
||||||
|
|
||||||
state.saveDataDirty = true;
|
state.saveDataDirty = true;
|
||||||
state.noteDisplayDirty = true;
|
state.noteDisplayDirty = true;
|
||||||
|
@ -45,8 +54,31 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (oldLength != newLength);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
return 'Extend Note Length';
|
if (oldLength == 0)
|
||||||
|
{
|
||||||
|
return 'Add Hold to Note';
|
||||||
|
}
|
||||||
|
else if (newLength == 0)
|
||||||
|
{
|
||||||
|
return 'Remove Hold from Note';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 'Extend Hold Note Length';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Unit
|
||||||
|
{
|
||||||
|
MILLISECONDS;
|
||||||
|
STEPS;
|
||||||
|
}
|
||||||
|
|
|
@ -51,6 +51,12 @@ class FlipNotesCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (notes.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
var len:Int = notes.length;
|
var len:Int = notes.length;
|
||||||
|
|
|
@ -12,19 +12,19 @@ import funkin.data.song.SongDataUtils;
|
||||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
class InvertSelectedItemsCommand implements ChartEditorCommand
|
class InvertSelectedItemsCommand implements ChartEditorCommand
|
||||||
{
|
{
|
||||||
var previousNoteSelection:Array<SongNoteData>;
|
var previousNoteSelection:Array<SongNoteData> = [];
|
||||||
var previousEventSelection:Array<SongEventData>;
|
var previousEventSelection:Array<SongEventData> = [];
|
||||||
|
|
||||||
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
|
public function new() {}
|
||||||
{
|
|
||||||
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
|
|
||||||
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function execute(state:ChartEditorState):Void
|
public function execute(state:ChartEditorState):Void
|
||||||
{
|
{
|
||||||
|
this.previousNoteSelection = state.currentNoteSelection;
|
||||||
|
this.previousEventSelection = state.currentEventSelection;
|
||||||
|
|
||||||
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection);
|
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection);
|
||||||
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
|
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
|
||||||
|
|
||||||
state.noteDisplayDirty = true;
|
state.noteDisplayDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,12 @@ class InvertSelectedItemsCommand implements ChartEditorCommand
|
||||||
state.noteDisplayDirty = true;
|
state.noteDisplayDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (previousNoteSelection.length > 0 || previousEventSelection.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
return 'Invert Selected Items';
|
return 'Invert Selected Items';
|
||||||
|
|
|
@ -65,6 +65,12 @@ class MoveEventsCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (events.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
var len:Int = events.length;
|
var len:Int = events.length;
|
||||||
|
|
|
@ -88,6 +88,12 @@ class MoveItemsCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (notes.length > 0 || events.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
var len:Int = notes.length + events.length;
|
var len:Int = notes.length + events.length;
|
||||||
|
|
|
@ -67,6 +67,12 @@ class MoveNotesCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (notes.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
var len:Int = notes.length;
|
var len:Int = notes.length;
|
||||||
|
|
|
@ -71,6 +71,12 @@ class PasteItemsCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (addedNotes.length > 0 || addedEvents.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
|
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
|
||||||
|
|
|
@ -48,6 +48,12 @@ class RemoveEventsCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (events.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
if (events.length == 1 && events[0] != null)
|
if (events.length == 1 && events[0] != null)
|
||||||
|
|
|
@ -62,6 +62,12 @@ class RemoveItemsCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (notes.length > 0 || events.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
return 'Remove ${notes.length + events.length} Items';
|
return 'Remove ${notes.length + events.length} Items';
|
||||||
|
|
|
@ -50,6 +50,12 @@ class RemoveNotesCommand implements ChartEditorCommand
|
||||||
state.sortChartData();
|
state.sortChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (notes.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
if (notes.length == 1 && notes[0] != null)
|
if (notes.length == 1 && notes[0] != null)
|
||||||
|
|
|
@ -10,19 +10,25 @@ import funkin.data.song.SongData.SongEventData;
|
||||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
class SelectAllItemsCommand implements ChartEditorCommand
|
class SelectAllItemsCommand implements ChartEditorCommand
|
||||||
{
|
{
|
||||||
var previousNoteSelection:Array<SongNoteData>;
|
var shouldSelectNotes:Bool;
|
||||||
var previousEventSelection:Array<SongEventData>;
|
var shouldSelectEvents:Bool;
|
||||||
|
|
||||||
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
|
var previousNoteSelection:Array<SongNoteData> = [];
|
||||||
|
var previousEventSelection:Array<SongEventData> = [];
|
||||||
|
|
||||||
|
public function new(shouldSelectNotes:Bool, shouldSelectEvents:Bool)
|
||||||
{
|
{
|
||||||
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
|
this.shouldSelectNotes = shouldSelectNotes;
|
||||||
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
|
this.shouldSelectEvents = shouldSelectEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(state:ChartEditorState):Void
|
public function execute(state:ChartEditorState):Void
|
||||||
{
|
{
|
||||||
state.currentNoteSelection = state.currentSongChartNoteData;
|
this.previousNoteSelection = state.currentNoteSelection;
|
||||||
state.currentEventSelection = state.currentSongChartEventData;
|
this.previousEventSelection = state.currentEventSelection;
|
||||||
|
|
||||||
|
state.currentNoteSelection = shouldSelectNotes ? state.currentSongChartNoteData : [];
|
||||||
|
state.currentEventSelection = shouldSelectEvents ? state.currentSongChartEventData : [];
|
||||||
|
|
||||||
state.noteDisplayDirty = true;
|
state.noteDisplayDirty = true;
|
||||||
}
|
}
|
||||||
|
@ -35,8 +41,29 @@ class SelectAllItemsCommand implements ChartEditorCommand
|
||||||
state.noteDisplayDirty = true;
|
state.noteDisplayDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (state.currentNoteSelection.length > 0 || state.currentEventSelection.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
return 'Select All Items';
|
if (shouldSelectNotes && !shouldSelectEvents)
|
||||||
|
{
|
||||||
|
return 'Select All Notes';
|
||||||
|
}
|
||||||
|
else if (shouldSelectEvents && !shouldSelectNotes)
|
||||||
|
{
|
||||||
|
return 'Select All Events';
|
||||||
|
}
|
||||||
|
else if (shouldSelectNotes && shouldSelectEvents)
|
||||||
|
{
|
||||||
|
return 'Select All Notes and Events';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 'Select Nothing (Huh?)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,10 @@ class SelectItemsCommand implements ChartEditorCommand
|
||||||
var notes:Array<SongNoteData>;
|
var notes:Array<SongNoteData>;
|
||||||
var events:Array<SongEventData>;
|
var events:Array<SongEventData>;
|
||||||
|
|
||||||
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
|
public function new(?notes:Array<SongNoteData>, ?events:Array<SongEventData>)
|
||||||
{
|
{
|
||||||
this.notes = notes;
|
this.notes = notes ?? [];
|
||||||
this.events = events;
|
this.events = events ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(state:ChartEditorState):Void
|
public function execute(state:ChartEditorState):Void
|
||||||
|
@ -72,6 +72,12 @@ class SelectItemsCommand implements ChartEditorCommand
|
||||||
state.notePreviewDirty = true;
|
state.notePreviewDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// This command is undoable. Add to the history if we actually performed an action.
|
||||||
|
return (notes.length > 0 || events.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
var len:Int = notes.length + events.length;
|
var len:Int = notes.length + events.length;
|
||||||
|
|
|
@ -13,20 +13,20 @@ class SetItemSelectionCommand implements ChartEditorCommand
|
||||||
{
|
{
|
||||||
var notes:Array<SongNoteData>;
|
var notes:Array<SongNoteData>;
|
||||||
var events:Array<SongEventData>;
|
var events:Array<SongEventData>;
|
||||||
var previousNoteSelection:Array<SongNoteData>;
|
var previousNoteSelection:Array<SongNoteData> = [];
|
||||||
var previousEventSelection:Array<SongEventData>;
|
var previousEventSelection:Array<SongEventData> = [];
|
||||||
|
|
||||||
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, previousNoteSelection:Array<SongNoteData>,
|
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
|
||||||
previousEventSelection:Array<SongEventData>)
|
|
||||||
{
|
{
|
||||||
this.notes = notes;
|
this.notes = notes;
|
||||||
this.events = events;
|
this.events = events;
|
||||||
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
|
|
||||||
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(state:ChartEditorState):Void
|
public function execute(state:ChartEditorState):Void
|
||||||
{
|
{
|
||||||
|
this.previousNoteSelection = state.currentNoteSelection;
|
||||||
|
this.previousEventSelection = state.currentEventSelection;
|
||||||
|
|
||||||
state.currentNoteSelection = notes;
|
state.currentNoteSelection = notes;
|
||||||
state.currentEventSelection = events;
|
state.currentEventSelection = events;
|
||||||
|
|
||||||
|
@ -67,8 +67,14 @@ class SetItemSelectionCommand implements ChartEditorCommand
|
||||||
state.noteDisplayDirty = true;
|
state.noteDisplayDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// Add to the history if we actually performed an action.
|
||||||
|
return (state.currentNoteSelection != previousNoteSelection && state.currentEventSelection != previousEventSelection);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
return 'Select ${notes.length} Items';
|
return 'Select ${notes.length + events.length} Items';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,12 @@ class SwitchDifficultyCommand implements ChartEditorCommand
|
||||||
state.notePreviewDirty = true;
|
state.notePreviewDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||||
|
{
|
||||||
|
// Add to the history if we actually performed an action.
|
||||||
|
return (prevVariation != newVariation || prevDifficulty != newDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
public function toString():String
|
public function toString():String
|
||||||
{
|
{
|
||||||
return 'Switch Difficulty';
|
return 'Switch Difficulty';
|
||||||
|
|
|
@ -11,6 +11,9 @@ import flixel.graphics.frames.FlxFramesCollection;
|
||||||
import flixel.graphics.frames.FlxTileFrames;
|
import flixel.graphics.frames.FlxTileFrames;
|
||||||
import flixel.math.FlxPoint;
|
import flixel.math.FlxPoint;
|
||||||
import funkin.data.song.SongData.SongEventData;
|
import funkin.data.song.SongData.SongEventData;
|
||||||
|
import haxe.ui.tooltips.ToolTipRegionOptions;
|
||||||
|
import funkin.util.HaxeUIUtil;
|
||||||
|
import haxe.ui.tooltips.ToolTipManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sprite that can be used to display a song event in a chart.
|
* A sprite that can be used to display a song event in a chart.
|
||||||
|
@ -36,6 +39,13 @@ class ChartEditorEventSprite extends FlxSprite
|
||||||
|
|
||||||
public var overrideStepTime(default, set):Null<Float> = null;
|
public var overrideStepTime(default, set):Null<Float> = null;
|
||||||
|
|
||||||
|
public var tooltip:ToolTipRegionOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this sprite is a "ghost" sprite used when hovering to place a new event.
|
||||||
|
*/
|
||||||
|
public var isGhost:Bool = false;
|
||||||
|
|
||||||
function set_overrideStepTime(value:Null<Float>):Null<Float>
|
function set_overrideStepTime(value:Null<Float>):Null<Float>
|
||||||
{
|
{
|
||||||
if (overrideStepTime == value) return overrideStepTime;
|
if (overrideStepTime == value) return overrideStepTime;
|
||||||
|
@ -45,12 +55,14 @@ class ChartEditorEventSprite extends FlxSprite
|
||||||
return overrideStepTime;
|
return overrideStepTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function new(parent:ChartEditorState)
|
public function new(parent:ChartEditorState, isGhost:Bool = false)
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.parentState = parent;
|
this.parentState = parent;
|
||||||
|
this.isGhost = isGhost;
|
||||||
|
|
||||||
|
this.tooltip = HaxeUIUtil.buildTooltip('N/A');
|
||||||
this.frames = buildFrames();
|
this.frames = buildFrames();
|
||||||
|
|
||||||
buildAnimations();
|
buildAnimations();
|
||||||
|
@ -119,8 +131,10 @@ class ChartEditorEventSprite extends FlxSprite
|
||||||
return DEFAULT_EVENT;
|
return DEFAULT_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function playAnimation(name:String):Void
|
public function playAnimation(?name:String):Void
|
||||||
{
|
{
|
||||||
|
if (name == null) name = eventData?.event ?? DEFAULT_EVENT;
|
||||||
|
|
||||||
var correctedName = correctAnimationName(name);
|
var correctedName = correctAnimationName(name);
|
||||||
this.animation.play(correctedName);
|
this.animation.play(correctedName);
|
||||||
refresh();
|
refresh();
|
||||||
|
@ -140,6 +154,7 @@ class ChartEditorEventSprite extends FlxSprite
|
||||||
// Disown parent. MAKE SURE TO REVIVE BEFORE REUSING
|
// Disown parent. MAKE SURE TO REVIVE BEFORE REUSING
|
||||||
this.kill();
|
this.kill();
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
|
updateTooltipPosition();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -149,6 +164,8 @@ class ChartEditorEventSprite extends FlxSprite
|
||||||
this.eventData = value;
|
this.eventData = value;
|
||||||
// Update the position to match the note data.
|
// Update the position to match the note data.
|
||||||
updateEventPosition();
|
updateEventPosition();
|
||||||
|
// Update the tooltip text.
|
||||||
|
this.tooltip.tipData = {text: this.eventData.buildTooltip()};
|
||||||
return this.eventData;
|
return this.eventData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,6 +184,31 @@ class ChartEditorEventSprite extends FlxSprite
|
||||||
this.x += origin.x;
|
this.x += origin.x;
|
||||||
this.y += origin.y;
|
this.y += origin.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateTooltipPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateTooltipPosition():Void
|
||||||
|
{
|
||||||
|
// No tooltip for ghost sprites.
|
||||||
|
if (this.isGhost) return;
|
||||||
|
|
||||||
|
if (this.eventData == null)
|
||||||
|
{
|
||||||
|
// Disable the tooltip.
|
||||||
|
ToolTipManager.instance.unregisterTooltipRegion(this.tooltip);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Update the position.
|
||||||
|
this.tooltip.left = this.x;
|
||||||
|
this.tooltip.top = this.y;
|
||||||
|
this.tooltip.width = this.width;
|
||||||
|
this.tooltip.height = this.height;
|
||||||
|
|
||||||
|
// Enable the tooltip.
|
||||||
|
ToolTipManager.instance.registerTooltipRegion(this.tooltip);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -39,6 +39,17 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
||||||
setup();
|
setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override function updateHitbox():Void
|
||||||
|
{
|
||||||
|
// Expand the clickable hitbox to the full column width, then nudge to the left to re-center it.
|
||||||
|
width = ChartEditorState.GRID_SIZE;
|
||||||
|
height = graphicHeight;
|
||||||
|
|
||||||
|
var xOffset = (ChartEditorState.GRID_SIZE - graphicWidth) / 2;
|
||||||
|
offset.set(-xOffset, 0);
|
||||||
|
origin.set(width * 0.5, height * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the height directly, to a value in pixels.
|
* Set the height directly, to a value in pixels.
|
||||||
* @param h The desired height in pixels.
|
* @param h The desired height in pixels.
|
||||||
|
@ -52,6 +63,25 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
||||||
fullSustainLength = sustainLength;
|
fullSustainLength = sustainLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if FLX_DEBUG
|
||||||
|
/**
|
||||||
|
* Call this to override how debug bounding boxes are drawn for this sprite.
|
||||||
|
*/
|
||||||
|
public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void
|
||||||
|
{
|
||||||
|
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
|
||||||
|
|
||||||
|
var rect = getBoundingBox(camera);
|
||||||
|
trace('hold note bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height);
|
||||||
|
|
||||||
|
var gfx = beginDrawDebug(camera);
|
||||||
|
debugBoundingBoxColor = 0xffFF66FF;
|
||||||
|
gfx.lineStyle(2, color, 0.5); // thickness, color, alpha
|
||||||
|
gfx.drawRect(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
endDrawDebug(camera);
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
|
||||||
function setup():Void
|
function setup():Void
|
||||||
{
|
{
|
||||||
strumTime = 999999999;
|
strumTime = 999999999;
|
||||||
|
@ -60,7 +90,9 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
||||||
active = true;
|
active = true;
|
||||||
visible = true;
|
visible = true;
|
||||||
alpha = 1.0;
|
alpha = 1.0;
|
||||||
width = graphic.width / 8 * zoom; // amount of notes * 2
|
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
|
||||||
|
|
||||||
|
updateHitbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override function revive():Void
|
public override function revive():Void
|
||||||
|
@ -154,7 +186,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
||||||
}
|
}
|
||||||
|
|
||||||
this.x += ChartEditorState.GRID_SIZE / 2;
|
this.x += ChartEditorState.GRID_SIZE / 2;
|
||||||
this.x -= this.width / 2;
|
this.x -= this.graphicWidth / 2;
|
||||||
|
|
||||||
this.y += ChartEditorState.GRID_SIZE / 2;
|
this.y += ChartEditorState.GRID_SIZE / 2;
|
||||||
|
|
||||||
|
@ -163,5 +195,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
||||||
this.x += origin.x;
|
this.x += origin.x;
|
||||||
this.y += origin.y;
|
this.y += origin.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Account for expanded clickable hitbox.
|
||||||
|
this.x += this.offset.x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,9 +70,9 @@ class ChartEditorNotePreview extends FlxSprite
|
||||||
* @param event The data for the event.
|
* @param event The data for the event.
|
||||||
* @param songLengthInMs The total length of the song in milliseconds.
|
* @param songLengthInMs The total length of the song in milliseconds.
|
||||||
*/
|
*/
|
||||||
public function addEvent(event:SongEventData, songLengthInMs:Int):Void
|
public function addEvent(event:SongEventData, songLengthInMs:Int, ?isSelection:Bool = false):Void
|
||||||
{
|
{
|
||||||
drawNote(-1, false, Std.int(event.time), songLengthInMs);
|
drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,6 +114,19 @@ class ChartEditorNotePreview extends FlxSprite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an array of selected events to the preview.
|
||||||
|
* @param events The data for the events.
|
||||||
|
* @param songLengthInMs The total length of the song in milliseconds.
|
||||||
|
*/
|
||||||
|
public function addSelectedEvents(events:Array<SongEventData>, songLengthInMs:Int):Void
|
||||||
|
{
|
||||||
|
for (event in events)
|
||||||
|
{
|
||||||
|
addEvent(event, songLengthInMs, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws a note on the preview.
|
* Draws a note on the preview.
|
||||||
* @param dir Note data.
|
* @param dir Note data.
|
||||||
|
|
|
@ -1,20 +1,33 @@
|
||||||
package funkin.ui.debug.charting.components;
|
package funkin.ui.debug.charting.components;
|
||||||
|
|
||||||
|
import flixel.addons.display.FlxSliceSprite;
|
||||||
import flixel.FlxSprite;
|
import flixel.FlxSprite;
|
||||||
import funkin.data.song.SongData.SongNoteData;
|
import flixel.math.FlxRect;
|
||||||
import funkin.data.song.SongData.SongEventData;
|
import funkin.data.song.SongData.SongEventData;
|
||||||
|
import funkin.data.song.SongData.SongNoteData;
|
||||||
|
import funkin.ui.debug.charting.handlers.ChartEditorThemeHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sprite that can be used to display a square over a selected note or event in the chart.
|
* A sprite that can be used to display a square over a selected note or event in the chart.
|
||||||
* Designed to be used and reused efficiently. Has no gameplay functionality.
|
* Designed to be used and reused efficiently. Has no gameplay functionality.
|
||||||
*/
|
*/
|
||||||
class ChartEditorSelectionSquareSprite extends FlxSprite
|
@:nullSafety
|
||||||
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
|
class ChartEditorSelectionSquareSprite extends FlxSliceSprite
|
||||||
{
|
{
|
||||||
public var noteData:Null<SongNoteData>;
|
public var noteData:Null<SongNoteData>;
|
||||||
public var eventData:Null<SongEventData>;
|
public var eventData:Null<SongEventData>;
|
||||||
|
|
||||||
public function new()
|
public function new(chartEditorState:ChartEditorState)
|
||||||
{
|
{
|
||||||
super();
|
super(chartEditorState.selectionSquareBitmap,
|
||||||
|
new FlxRect(ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH
|
||||||
|
+ 4, ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH
|
||||||
|
+ 4,
|
||||||
|
ChartEditorState.GRID_SIZE
|
||||||
|
- (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8),
|
||||||
|
ChartEditorState.GRID_SIZE
|
||||||
|
- (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8)),
|
||||||
|
32, 32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,10 @@ class ChartEditorEventContextMenu extends ChartEditorBaseContextMenu
|
||||||
|
|
||||||
function initialize()
|
function initialize()
|
||||||
{
|
{
|
||||||
|
contextmenuEdit.onClick = function(_) {
|
||||||
|
chartEditorState.showToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
|
||||||
|
}
|
||||||
|
|
||||||
contextmenuDelete.onClick = function(_) {
|
contextmenuDelete.onClick = function(_) {
|
||||||
chartEditorState.performCommand(new RemoveEventsCommand([data]));
|
chartEditorState.performCommand(new RemoveEventsCommand([data]));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package funkin.ui.debug.charting.contextmenus;
|
||||||
|
|
||||||
|
import haxe.ui.containers.menus.Menu;
|
||||||
|
import haxe.ui.containers.menus.MenuItem;
|
||||||
|
import haxe.ui.core.Screen;
|
||||||
|
import funkin.data.song.SongData.SongNoteData;
|
||||||
|
import funkin.ui.debug.charting.commands.FlipNotesCommand;
|
||||||
|
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
|
||||||
|
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
|
||||||
|
|
||||||
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
|
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/hold-note.xml"))
|
||||||
|
class ChartEditorHoldNoteContextMenu extends ChartEditorBaseContextMenu
|
||||||
|
{
|
||||||
|
var contextmenuFlip:MenuItem;
|
||||||
|
var contextmenuDelete:MenuItem;
|
||||||
|
|
||||||
|
var data:SongNoteData;
|
||||||
|
|
||||||
|
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData)
|
||||||
|
{
|
||||||
|
super(chartEditorState2, xPos2, yPos2);
|
||||||
|
this.data = data;
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize():Void
|
||||||
|
{
|
||||||
|
// NOTE: Remember to use commands here to ensure undo/redo works properly
|
||||||
|
contextmenuFlip.onClick = function(_) {
|
||||||
|
chartEditorState.performCommand(new FlipNotesCommand([data]));
|
||||||
|
}
|
||||||
|
|
||||||
|
contextmenuRemoveHold.onClick = function(_) {
|
||||||
|
chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
contextmenuDelete.onClick = function(_) {
|
||||||
|
chartEditorState.performCommand(new RemoveNotesCommand([data]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import haxe.ui.core.Screen;
|
||||||
import funkin.data.song.SongData.SongNoteData;
|
import funkin.data.song.SongData.SongNoteData;
|
||||||
import funkin.ui.debug.charting.commands.FlipNotesCommand;
|
import funkin.ui.debug.charting.commands.FlipNotesCommand;
|
||||||
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
|
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
|
||||||
|
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
|
||||||
|
|
||||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml"))
|
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml"))
|
||||||
|
@ -31,6 +32,10 @@ class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu
|
||||||
chartEditorState.performCommand(new FlipNotesCommand([data]));
|
chartEditorState.performCommand(new FlipNotesCommand([data]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contextmenuAddHold.onClick = function(_) {
|
||||||
|
chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 4, STEPS));
|
||||||
|
}
|
||||||
|
|
||||||
contextmenuDelete.onClick = function(_) {
|
contextmenuDelete.onClick = function(_) {
|
||||||
chartEditorState.performCommand(new RemoveNotesCommand([data]));
|
chartEditorState.performCommand(new RemoveNotesCommand([data]));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import haxe.ui.notifications.NotificationType;
|
||||||
|
|
||||||
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
|
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
|
||||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-chart.xml"))
|
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-chart.xml"))
|
||||||
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
class ChartEditorUploadChartDialog extends ChartEditorBaseDialog
|
class ChartEditorUploadChartDialog extends ChartEditorBaseDialog
|
||||||
{
|
{
|
||||||
var dropHandlers:Array<DialogDropTarget> = [];
|
var dropHandlers:Array<DialogDropTarget> = [];
|
||||||
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
package funkin.ui.debug.charting.dialogs;
|
||||||
|
|
||||||
|
import funkin.input.Cursor;
|
||||||
|
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
|
||||||
|
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams;
|
||||||
|
import funkin.util.FileUtil;
|
||||||
|
import funkin.play.character.CharacterData;
|
||||||
|
import haxe.io.Path;
|
||||||
|
import haxe.ui.components.Button;
|
||||||
|
import haxe.ui.components.Label;
|
||||||
|
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||||
|
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
|
||||||
|
import haxe.ui.containers.Box;
|
||||||
|
import haxe.ui.containers.dialogs.Dialogs;
|
||||||
|
import haxe.ui.core.Component;
|
||||||
|
import haxe.ui.notifications.NotificationManager;
|
||||||
|
import haxe.ui.notifications.NotificationType;
|
||||||
|
|
||||||
|
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
|
||||||
|
|
||||||
|
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals.xml"))
|
||||||
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
|
class ChartEditorUploadVocalsDialog extends ChartEditorBaseDialog
|
||||||
|
{
|
||||||
|
var dropHandlers:Array<DialogDropTarget> = [];
|
||||||
|
|
||||||
|
var vocalContainer:Component;
|
||||||
|
var dialogCancel:Button;
|
||||||
|
var dialogNoVocals:Button;
|
||||||
|
var dialogContinue:Button;
|
||||||
|
|
||||||
|
var charIds:Array<String>;
|
||||||
|
var instId:String;
|
||||||
|
var hasClearedVocals:Bool = false;
|
||||||
|
|
||||||
|
public function new(state2:ChartEditorState, charIds:Array<String>, params2:DialogParams)
|
||||||
|
{
|
||||||
|
super(state2, params2);
|
||||||
|
|
||||||
|
this.charIds = charIds;
|
||||||
|
this.instId = chartEditorState.currentInstrumentalId;
|
||||||
|
|
||||||
|
dialogCancel.onClick = function(_) {
|
||||||
|
hideDialog(DialogButton.CANCEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogNoVocals.onClick = function(_) {
|
||||||
|
// Dismiss
|
||||||
|
chartEditorState.wipeVocalData();
|
||||||
|
hideDialog(DialogButton.APPLY);
|
||||||
|
};
|
||||||
|
|
||||||
|
dialogContinue.onClick = function(_) {
|
||||||
|
// Dismiss
|
||||||
|
hideDialog(DialogButton.APPLY);
|
||||||
|
};
|
||||||
|
|
||||||
|
buildDropHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDropHandlers():Void
|
||||||
|
{
|
||||||
|
for (charKey in charIds)
|
||||||
|
{
|
||||||
|
trace('Adding vocal upload for character ${charKey}');
|
||||||
|
|
||||||
|
var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey);
|
||||||
|
var charName:String = charMetadata?.name ?? charKey;
|
||||||
|
|
||||||
|
var vocalsEntry = new ChartEditorUploadVocalsEntry(charName);
|
||||||
|
|
||||||
|
var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null};
|
||||||
|
|
||||||
|
var onDropFile:String->Void = function(pathStr:String) {
|
||||||
|
trace('Selected file: $pathStr');
|
||||||
|
var path:Path = new Path(pathStr);
|
||||||
|
|
||||||
|
if (chartEditorState.loadVocalsFromPath(path, charKey, this.instId, !this.hasClearedVocals))
|
||||||
|
{
|
||||||
|
this.hasClearedVocals = true;
|
||||||
|
// Tell the user the load was successful.
|
||||||
|
chartEditorState.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${chartEditorState.selectedVariation}');
|
||||||
|
#if FILE_DROP_SUPPORTED
|
||||||
|
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
|
||||||
|
#else
|
||||||
|
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}';
|
||||||
|
#end
|
||||||
|
|
||||||
|
dialogNoVocals.hidden = true;
|
||||||
|
chartEditorState.removeDropHandler(dropHandler);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('Failed to load vocal track (${path.file}.${path.ext})');
|
||||||
|
|
||||||
|
chartEditorState.error('Failed to Load Vocals',
|
||||||
|
'Failed to load vocal track (${path.file}.${path.ext}) for variation (${chartEditorState.selectedVariation})');
|
||||||
|
|
||||||
|
#if FILE_DROP_SUPPORTED
|
||||||
|
vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
|
||||||
|
#else
|
||||||
|
vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
|
||||||
|
#end
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
vocalsEntry.onClick = function(_event) {
|
||||||
|
Dialogs.openBinaryFile('Open $charName Vocals', [
|
||||||
|
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
|
||||||
|
if (selectedFile != null && selectedFile.bytes != null)
|
||||||
|
{
|
||||||
|
trace('Selected file: ' + selectedFile.name);
|
||||||
|
|
||||||
|
if (chartEditorState.loadVocalsFromBytes(selectedFile.bytes, charKey, this.instId, !this.hasClearedVocals))
|
||||||
|
{
|
||||||
|
hasClearedVocals = true;
|
||||||
|
// Tell the user the load was successful.
|
||||||
|
chartEditorState.success('Loaded Vocals',
|
||||||
|
'Loaded vocals for $charName (${selectedFile.name}), variation ${chartEditorState.selectedVariation}');
|
||||||
|
|
||||||
|
#if FILE_DROP_SUPPORTED
|
||||||
|
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
|
||||||
|
#else
|
||||||
|
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}';
|
||||||
|
#end
|
||||||
|
|
||||||
|
dialogNoVocals.hidden = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('Failed to load vocal track (${selectedFile.fullPath})');
|
||||||
|
|
||||||
|
chartEditorState.error('Failed to Load Vocals',
|
||||||
|
'Failed to load vocal track (${selectedFile.name}) for variation (${chartEditorState.selectedVariation})');
|
||||||
|
|
||||||
|
#if FILE_DROP_SUPPORTED
|
||||||
|
vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
|
||||||
|
#else
|
||||||
|
vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
|
||||||
|
#end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dropHandler.handler = onDropFile;
|
||||||
|
|
||||||
|
// onDropFile
|
||||||
|
#if FILE_DROP_SUPPORTED
|
||||||
|
dropHandlers.push(dropHandler);
|
||||||
|
#end
|
||||||
|
|
||||||
|
vocalContainer.addComponent(vocalsEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function build(state:ChartEditorState, charIds:Array<String>, ?closable:Bool, ?modal:Bool):ChartEditorUploadVocalsDialog
|
||||||
|
{
|
||||||
|
var dialog = new ChartEditorUploadVocalsDialog(state, charIds,
|
||||||
|
{
|
||||||
|
closable: closable ?? false,
|
||||||
|
modal: modal ?? true
|
||||||
|
});
|
||||||
|
|
||||||
|
for (dropTarget in dialog.dropHandlers)
|
||||||
|
{
|
||||||
|
state.addDropHandler(dropTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.showDialog(modal ?? true);
|
||||||
|
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override function onClose(event:DialogEvent):Void
|
||||||
|
{
|
||||||
|
super.onClose(event);
|
||||||
|
|
||||||
|
if (event.button != DialogButton.APPLY && !this.closable)
|
||||||
|
{
|
||||||
|
// User cancelled the wizard! Back to the welcome dialog.
|
||||||
|
chartEditorState.openWelcomeDialog(this.closable);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (dropTarget in dropHandlers)
|
||||||
|
{
|
||||||
|
chartEditorState.removeDropHandler(dropTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override function lock():Void
|
||||||
|
{
|
||||||
|
super.lock();
|
||||||
|
this.dialogCancel.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override function unlock():Void
|
||||||
|
{
|
||||||
|
super.unlock();
|
||||||
|
this.dialogCancel.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when clicking the Upload Chart box.
|
||||||
|
*/
|
||||||
|
public function onClickChartBox():Void
|
||||||
|
{
|
||||||
|
if (this.locked) return;
|
||||||
|
|
||||||
|
this.lock();
|
||||||
|
// TODO / BUG: File filtering not working on mac finder dialog, so we don't use it for now
|
||||||
|
#if !mac
|
||||||
|
FileUtil.browseForBinaryFile('Open Chart', [FileUtil.FILE_EXTENSION_INFO_FNFC], onSelectFile, onCancelBrowse);
|
||||||
|
#else
|
||||||
|
FileUtil.browseForBinaryFile('Open Chart', null, onSelectFile, onCancelBrowse);
|
||||||
|
#end
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a file is selected by dropping a file onto the Upload Chart box.
|
||||||
|
*/
|
||||||
|
function onDropFileChartBox(pathStr:String):Void
|
||||||
|
{
|
||||||
|
var path:Path = new Path(pathStr);
|
||||||
|
trace('Dropped file (${path})');
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(chartEditorState, path.toString());
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
chartEditorState.success('Loaded Chart',
|
||||||
|
result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}');
|
||||||
|
this.hideDialog(DialogButton.APPLY);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()})');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err)
|
||||||
|
{
|
||||||
|
chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()}): ${err}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a file is selected by the dialog displayed when clicking the Upload Chart box.
|
||||||
|
*/
|
||||||
|
function onSelectFile(selectedFile:SelectedFileInfo):Void
|
||||||
|
{
|
||||||
|
this.unlock();
|
||||||
|
|
||||||
|
if (selectedFile != null && selectedFile.bytes != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(chartEditorState, selectedFile.bytes);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
chartEditorState.success('Loaded Chart',
|
||||||
|
result.length == 0 ? 'Loaded chart (${selectedFile.name})' : 'Loaded chart (${selectedFile.name})\n${result.join("\n")}');
|
||||||
|
|
||||||
|
if (selectedFile.fullPath != null) chartEditorState.currentWorkingFilePath = selectedFile.fullPath;
|
||||||
|
this.hideDialog(DialogButton.APPLY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err)
|
||||||
|
{
|
||||||
|
chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${selectedFile.name}): ${err}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancelBrowse():Void
|
||||||
|
{
|
||||||
|
this.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals-entry.xml"))
|
||||||
|
class ChartEditorUploadVocalsEntry extends Box
|
||||||
|
{
|
||||||
|
public var vocalsEntryLabel:Label;
|
||||||
|
|
||||||
|
var charName:String;
|
||||||
|
|
||||||
|
public function new(charName:String)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.charName = charName;
|
||||||
|
|
||||||
|
#if FILE_DROP_SUPPORTED
|
||||||
|
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
|
||||||
|
#else
|
||||||
|
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
|
||||||
|
#end
|
||||||
|
|
||||||
|
this.onMouseOver = function(_event) {
|
||||||
|
// if (this.locked) return;
|
||||||
|
this.swapClass('upload-bg', 'upload-bg-hover');
|
||||||
|
Cursor.cursorMode = Pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onMouseOut = function(_event) {
|
||||||
|
this.swapClass('upload-bg-hover', 'upload-bg');
|
||||||
|
Cursor.cursorMode = Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -188,8 +188,9 @@ class ChartEditorAudioHandler
|
||||||
state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
|
state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
|
||||||
state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
|
state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
|
||||||
state.audioVisGroup.playerVis.detail = 1;
|
state.audioVisGroup.playerVis.detail = 1;
|
||||||
|
state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
||||||
|
|
||||||
state.audioVocalTrackGroup.playerVoicesOffset = state.currentSongOffsets.getVocalOffset(charId);
|
state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffset;
|
||||||
return true;
|
return true;
|
||||||
case DAD:
|
case DAD:
|
||||||
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
|
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
|
||||||
|
@ -199,8 +200,9 @@ class ChartEditorAudioHandler
|
||||||
state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
|
state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
|
||||||
state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
|
state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
|
||||||
state.audioVisGroup.opponentVis.detail = 1;
|
state.audioVisGroup.opponentVis.detail = 1;
|
||||||
|
state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
||||||
|
|
||||||
state.audioVocalTrackGroup.opponentVoicesOffset = state.currentSongOffsets.getVocalOffset(charId);
|
state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffset;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
case OTHER:
|
case OTHER:
|
||||||
|
@ -221,6 +223,10 @@ class ChartEditorAudioHandler
|
||||||
{
|
{
|
||||||
state.audioVocalTrackGroup.clear();
|
state.audioVocalTrackGroup.clear();
|
||||||
}
|
}
|
||||||
|
if (state.audioVisGroup != null)
|
||||||
|
{
|
||||||
|
state.audioVisGroup.clearAllVis();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,7 @@ package funkin.ui.debug.charting.handlers;
|
||||||
|
|
||||||
import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu;
|
import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu;
|
||||||
import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu;
|
import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu;
|
||||||
|
import funkin.ui.debug.charting.contextmenus.ChartEditorHoldNoteContextMenu;
|
||||||
import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu;
|
import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu;
|
||||||
import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu;
|
import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu;
|
||||||
import haxe.ui.containers.menus.Menu;
|
import haxe.ui.containers.menus.Menu;
|
||||||
|
@ -23,16 +24,33 @@ class ChartEditorContextMenuHandler
|
||||||
displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos));
|
displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opened when shift+right-clicking a selection of multiple items.
|
||||||
|
*/
|
||||||
public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
|
public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
|
||||||
{
|
{
|
||||||
displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos));
|
displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opened when shift+right-clicking a single note.
|
||||||
|
*/
|
||||||
public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
|
public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
|
||||||
{
|
{
|
||||||
displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data));
|
displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opened when shift+right-clicking a single hold note.
|
||||||
|
*/
|
||||||
|
public static function openHoldNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
|
||||||
|
{
|
||||||
|
displayMenu(state, new ChartEditorHoldNoteContextMenu(state, xPos, yPos, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opened when shift+right-clicking a single event.
|
||||||
|
*/
|
||||||
public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData)
|
public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData)
|
||||||
{
|
{
|
||||||
displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data));
|
displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data));
|
||||||
|
|
|
@ -13,12 +13,13 @@ import funkin.play.character.BaseCharacter;
|
||||||
import funkin.play.character.CharacterData;
|
import funkin.play.character.CharacterData;
|
||||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||||
import funkin.play.song.Song;
|
import funkin.play.song.Song;
|
||||||
import funkin.play.stage.StageData;
|
import funkin.data.stage.StageData;
|
||||||
import funkin.ui.debug.charting.dialogs.ChartEditorAboutDialog;
|
import funkin.ui.debug.charting.dialogs.ChartEditorAboutDialog;
|
||||||
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
|
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
|
||||||
import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu;
|
import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu;
|
||||||
import funkin.ui.debug.charting.dialogs.ChartEditorUploadChartDialog;
|
import funkin.ui.debug.charting.dialogs.ChartEditorUploadChartDialog;
|
||||||
import funkin.ui.debug.charting.dialogs.ChartEditorWelcomeDialog;
|
import funkin.ui.debug.charting.dialogs.ChartEditorWelcomeDialog;
|
||||||
|
import funkin.ui.debug.charting.dialogs.ChartEditorUploadVocalsDialog;
|
||||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||||
import funkin.util.Constants;
|
import funkin.util.Constants;
|
||||||
import funkin.util.DateUtil;
|
import funkin.util.DateUtil;
|
||||||
|
@ -59,11 +60,8 @@ using Lambda;
|
||||||
class ChartEditorDialogHandler
|
class ChartEditorDialogHandler
|
||||||
{
|
{
|
||||||
// Paths to HaxeUI layout files for each dialog.
|
// Paths to HaxeUI layout files for each dialog.
|
||||||
static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart');
|
|
||||||
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
|
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
|
||||||
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
|
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
|
||||||
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals');
|
|
||||||
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
|
|
||||||
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts');
|
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts');
|
||||||
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry');
|
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry');
|
||||||
static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart');
|
static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart');
|
||||||
|
@ -105,6 +103,56 @@ class ChartEditorDialogHandler
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and opens a dialog letting the user browse for a chart file to open.
|
||||||
|
* @param state The current chart editor state.
|
||||||
|
* @param closable Whether the dialog can be closed by the user.
|
||||||
|
* @return The dialog that was opened.
|
||||||
|
*/
|
||||||
|
public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
|
||||||
|
{
|
||||||
|
var dialog = ChartEditorUploadChartDialog.build(state, closable);
|
||||||
|
|
||||||
|
dialog.zIndex = 1000;
|
||||||
|
state.isHaxeUIDialogOpen = true;
|
||||||
|
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and opens a dialog where the user uploads vocals for the current song.
|
||||||
|
* @param state The current chart editor state.
|
||||||
|
* @param closable Whether the dialog can be closed by the user.
|
||||||
|
* @return The dialog that was opened.
|
||||||
|
*/
|
||||||
|
public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
|
||||||
|
{
|
||||||
|
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
|
||||||
|
|
||||||
|
var hasClearedVocals:Bool = false;
|
||||||
|
|
||||||
|
var charIdsForVocals:Array<String> = [charData.player, charData.opponent];
|
||||||
|
|
||||||
|
var dialog = ChartEditorUploadVocalsDialog.build(state, charIdsForVocals, closable);
|
||||||
|
|
||||||
|
dialog.zIndex = 1000;
|
||||||
|
state.isHaxeUIDialogOpen = true;
|
||||||
|
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and opens the dialog for selecting a character.
|
||||||
|
*/
|
||||||
|
public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null<Menu>
|
||||||
|
{
|
||||||
|
var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition);
|
||||||
|
|
||||||
|
menu.zIndex = 1000;
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds and opens a dialog letting the user know a backup is available, and prompting them to load it.
|
* Builds and opens a dialog letting the user know a backup is available, and prompting them to load it.
|
||||||
*/
|
*/
|
||||||
|
@ -186,22 +234,6 @@ class ChartEditorDialogHandler
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds and opens a dialog letting the user browse for a chart file to open.
|
|
||||||
* @param state The current chart editor state.
|
|
||||||
* @param closable Whether the dialog can be closed by the user.
|
|
||||||
* @return The dialog that was opened.
|
|
||||||
*/
|
|
||||||
public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
|
|
||||||
{
|
|
||||||
var dialog = ChartEditorUploadChartDialog.build(state, closable);
|
|
||||||
|
|
||||||
dialog.zIndex = 1000;
|
|
||||||
state.isHaxeUIDialogOpen = true;
|
|
||||||
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the wizard for opening an existing chart from individual files.
|
* Open the wizard for opening an existing chart from individual files.
|
||||||
* @param state
|
* @param state
|
||||||
|
@ -288,15 +320,6 @@ class ChartEditorDialogHandler
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null<Menu>
|
|
||||||
{
|
|
||||||
var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition);
|
|
||||||
|
|
||||||
menu.zIndex = 1000;
|
|
||||||
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void
|
public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void
|
||||||
{
|
{
|
||||||
// Step 1. Song Metadata
|
// Step 1. Song Metadata
|
||||||
|
@ -699,150 +722,6 @@ class ChartEditorDialogHandler
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds and opens a dialog where the user uploads vocals for the current song.
|
|
||||||
* @param state The current chart editor state.
|
|
||||||
* @param closable Whether the dialog can be closed by the user.
|
|
||||||
* @return The dialog that was opened.
|
|
||||||
*/
|
|
||||||
public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
|
|
||||||
{
|
|
||||||
var instId:String = state.currentInstrumentalId;
|
|
||||||
var charIdsForVocals:Array<String> = [];
|
|
||||||
|
|
||||||
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
|
|
||||||
|
|
||||||
var hasClearedVocals:Bool = false;
|
|
||||||
|
|
||||||
charIdsForVocals.push(charData.player);
|
|
||||||
charIdsForVocals.push(charData.opponent);
|
|
||||||
|
|
||||||
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable);
|
|
||||||
if (dialog == null) throw 'Could not locate Upload Vocals dialog';
|
|
||||||
|
|
||||||
var dialogContainer:Null<Component> = dialog.findComponent('vocalContainer');
|
|
||||||
if (dialogContainer == null) throw 'Could not locate vocalContainer in Upload Vocals dialog';
|
|
||||||
|
|
||||||
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
|
|
||||||
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Vocals dialog';
|
|
||||||
buttonCancel.onClick = function(_) {
|
|
||||||
dialog.hideDialog(DialogButton.CANCEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
var dialogNoVocals:Null<Button> = dialog.findComponent('dialogNoVocals', Button);
|
|
||||||
if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog';
|
|
||||||
dialogNoVocals.onClick = function(_) {
|
|
||||||
// Dismiss
|
|
||||||
state.wipeVocalData();
|
|
||||||
dialog.hideDialog(DialogButton.APPLY);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (charKey in charIdsForVocals)
|
|
||||||
{
|
|
||||||
trace('Adding vocal upload for character ${charKey}');
|
|
||||||
var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey);
|
|
||||||
var charName:String = charMetadata != null ? charMetadata.name : charKey;
|
|
||||||
|
|
||||||
var vocalsEntry:Component = RuntimeComponentBuilder.fromAsset(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
|
|
||||||
|
|
||||||
var vocalsEntryLabel:Null<Label> = vocalsEntry.findComponent('vocalsEntryLabel', Label);
|
|
||||||
if (vocalsEntryLabel == null) throw 'Could not locate vocalsEntryLabel in Upload Vocals dialog';
|
|
||||||
#if FILE_DROP_SUPPORTED
|
|
||||||
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
|
|
||||||
#else
|
|
||||||
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
|
|
||||||
#end
|
|
||||||
|
|
||||||
var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null};
|
|
||||||
|
|
||||||
var onDropFile:String->Void = function(pathStr:String) {
|
|
||||||
trace('Selected file: $pathStr');
|
|
||||||
var path:Path = new Path(pathStr);
|
|
||||||
|
|
||||||
if (state.loadVocalsFromPath(path, charKey, instId, !hasClearedVocals))
|
|
||||||
{
|
|
||||||
hasClearedVocals = true;
|
|
||||||
// Tell the user the load was successful.
|
|
||||||
state.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}');
|
|
||||||
#if FILE_DROP_SUPPORTED
|
|
||||||
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
|
|
||||||
#else
|
|
||||||
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}';
|
|
||||||
#end
|
|
||||||
|
|
||||||
dialogNoVocals.hidden = true;
|
|
||||||
state.removeDropHandler(dropHandler);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace('Failed to load vocal track (${path.file}.${path.ext})');
|
|
||||||
|
|
||||||
state.error('Failed to Load Vocals', 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})');
|
|
||||||
|
|
||||||
#if FILE_DROP_SUPPORTED
|
|
||||||
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
|
|
||||||
#else
|
|
||||||
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
|
|
||||||
#end
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dropHandler.handler = onDropFile;
|
|
||||||
|
|
||||||
vocalsEntry.onClick = function(_event) {
|
|
||||||
Dialogs.openBinaryFile('Open $charName Vocals', [
|
|
||||||
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
|
|
||||||
if (selectedFile != null && selectedFile.bytes != null)
|
|
||||||
{
|
|
||||||
trace('Selected file: ' + selectedFile.name);
|
|
||||||
|
|
||||||
if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId, !hasClearedVocals))
|
|
||||||
{
|
|
||||||
hasClearedVocals = true;
|
|
||||||
// Tell the user the load was successful.
|
|
||||||
state.success('Loaded Vocals', 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}');
|
|
||||||
|
|
||||||
#if FILE_DROP_SUPPORTED
|
|
||||||
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
|
|
||||||
#else
|
|
||||||
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}';
|
|
||||||
#end
|
|
||||||
|
|
||||||
dialogNoVocals.hidden = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace('Failed to load vocal track (${selectedFile.fullPath})');
|
|
||||||
|
|
||||||
state.error('Failed to Load Vocals', 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})');
|
|
||||||
|
|
||||||
#if FILE_DROP_SUPPORTED
|
|
||||||
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
|
|
||||||
#else
|
|
||||||
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
|
|
||||||
#end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// onDropFile
|
|
||||||
#if FILE_DROP_SUPPORTED
|
|
||||||
addDropHandler(dropHandler);
|
|
||||||
#end
|
|
||||||
dialogContainer.addComponent(vocalsEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
|
|
||||||
if (dialogContinue == null) throw 'Could not locate dialogContinue button in Upload Vocals dialog';
|
|
||||||
dialogContinue.onClick = function(_) {
|
|
||||||
// Dismiss
|
|
||||||
dialog.hideDialog(DialogButton.APPLY);
|
|
||||||
};
|
|
||||||
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds and opens a dialog where the user upload the JSON files for a song.
|
* Builds and opens a dialog where the user upload the JSON files for a song.
|
||||||
* @param state The current chart editor state.
|
* @param state The current chart editor state.
|
||||||
|
|
|
@ -2,6 +2,10 @@ package funkin.ui.debug.charting.handlers;
|
||||||
|
|
||||||
import funkin.util.PlatformUtil;
|
import funkin.util.PlatformUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles modifying the shortcut text of menu items based on the current platform.
|
||||||
|
* On MacOS, `Ctrl`, `Alt`, and `Shift` are replaced with `⌘` (or `^`), `⌥`, and `⇧`, respectively.
|
||||||
|
*/
|
||||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
class ChartEditorShortcutHandler
|
class ChartEditorShortcutHandler
|
||||||
{
|
{
|
||||||
|
@ -18,7 +22,8 @@ class ChartEditorShortcutHandler
|
||||||
state.menubarItemCopy.shortcutText = ctrlOrCmd('C');
|
state.menubarItemCopy.shortcutText = ctrlOrCmd('C');
|
||||||
state.menubarItemPaste.shortcutText = ctrlOrCmd('V');
|
state.menubarItemPaste.shortcutText = ctrlOrCmd('V');
|
||||||
|
|
||||||
state.menubarItemSelectAll.shortcutText = ctrlOrCmd('A');
|
state.menubarItemSelectAllNotes.shortcutText = ctrlOrCmd('A');
|
||||||
|
state.menubarItemSelectAllEvents.shortcutText = ctrlOrCmd(alt('A'));
|
||||||
state.menubarItemSelectInverse.shortcutText = ctrlOrCmd('I');
|
state.menubarItemSelectInverse.shortcutText = ctrlOrCmd('I');
|
||||||
state.menubarItemSelectNone.shortcutText = ctrlOrCmd('D');
|
state.menubarItemSelectNone.shortcutText = ctrlOrCmd('D');
|
||||||
state.menubarItemSelectBeforeCursor.shortcutText = shift('Home');
|
state.menubarItemSelectBeforeCursor.shortcutText = shift('Home');
|
||||||
|
|
|
@ -52,7 +52,7 @@ class ChartEditorThemeHandler
|
||||||
// Border on the square highlighting selected notes.
|
// Border on the square highlighting selected notes.
|
||||||
static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933;
|
static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933;
|
||||||
static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933;
|
static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933;
|
||||||
static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
|
public static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
|
||||||
|
|
||||||
// Fill on the square highlighting selected notes.
|
// Fill on the square highlighting selected notes.
|
||||||
// Make sure this is transparent so you can see the notes underneath.
|
// Make sure this is transparent so you can see the notes underneath.
|
||||||
|
@ -317,6 +317,12 @@ class ChartEditorThemeHandler
|
||||||
ChartEditorState.GRID_SIZE
|
ChartEditorState.GRID_SIZE
|
||||||
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 8)),
|
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 8)),
|
||||||
32, 32);
|
32, 32);
|
||||||
|
|
||||||
|
state.selectionBoxSprite.scrollFactor.set(0, 0);
|
||||||
|
state.selectionBoxSprite.zIndex = 30;
|
||||||
|
state.add(state.selectionBoxSprite);
|
||||||
|
|
||||||
|
state.setSelectionBoxBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
static function updateNotePreview(state:ChartEditorState):Void
|
static function updateNotePreview(state:ChartEditorState):Void
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package funkin.ui.debug.charting.handlers;
|
package funkin.ui.debug.charting.handlers;
|
||||||
|
|
||||||
import funkin.play.stage.StageData.StageDataParser;
|
import funkin.data.stage.StageData;
|
||||||
import funkin.play.stage.StageData;
|
|
||||||
import funkin.play.character.CharacterData;
|
import funkin.play.character.CharacterData;
|
||||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||||
import haxe.ui.components.HorizontalSlider;
|
import haxe.ui.components.HorizontalSlider;
|
||||||
|
@ -16,8 +15,7 @@ import funkin.play.character.CharacterData;
|
||||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||||
import funkin.play.event.SongEvent;
|
import funkin.play.event.SongEvent;
|
||||||
import funkin.play.song.SongSerializer;
|
import funkin.play.song.SongSerializer;
|
||||||
import funkin.play.stage.StageData;
|
import funkin.data.stage.StageData;
|
||||||
import funkin.play.stage.StageData.StageDataParser;
|
|
||||||
import haxe.ui.RuntimeComponentBuilder;
|
import haxe.ui.RuntimeComponentBuilder;
|
||||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||||
import funkin.ui.haxeui.components.CharacterPlayer;
|
import funkin.ui.haxeui.components.CharacterPlayer;
|
||||||
|
@ -38,6 +36,7 @@ import haxe.ui.containers.dialogs.Dialog.DialogEvent;
|
||||||
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
|
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
|
||||||
import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
|
import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
|
||||||
import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
|
import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
|
||||||
|
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
|
||||||
import haxe.ui.containers.Frame;
|
import haxe.ui.containers.Frame;
|
||||||
import haxe.ui.containers.Grid;
|
import haxe.ui.containers.Grid;
|
||||||
import haxe.ui.containers.TreeView;
|
import haxe.ui.containers.TreeView;
|
||||||
|
@ -86,7 +85,7 @@ class ChartEditorToolboxHandler
|
||||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
|
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
|
||||||
onShowToolboxPlaytestProperties(state, toolbox);
|
onShowToolboxPlaytestProperties(state, toolbox);
|
||||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
|
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
|
||||||
onShowToolboxDifficulty(state, toolbox);
|
cast(toolbox, ChartEditorBaseToolbox).refresh();
|
||||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||||
// TODO: Fix this.
|
// TODO: Fix this.
|
||||||
cast(toolbox, ChartEditorBaseToolbox).refresh();
|
cast(toolbox, ChartEditorBaseToolbox).refresh();
|
||||||
|
@ -125,8 +124,6 @@ class ChartEditorToolboxHandler
|
||||||
onHideToolboxEventData(state, toolbox);
|
onHideToolboxEventData(state, toolbox);
|
||||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
|
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
|
||||||
onHideToolboxPlaytestProperties(state, toolbox);
|
onHideToolboxPlaytestProperties(state, toolbox);
|
||||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
|
|
||||||
onHideToolboxDifficulty(state, toolbox);
|
|
||||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||||
onHideToolboxMetadata(state, toolbox);
|
onHideToolboxMetadata(state, toolbox);
|
||||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
||||||
|
@ -311,8 +308,6 @@ class ChartEditorToolboxHandler
|
||||||
|
|
||||||
static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||||
|
|
||||||
static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
|
||||||
|
|
||||||
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||||
|
|
||||||
static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||||
|
@ -360,91 +355,15 @@ class ChartEditorToolboxHandler
|
||||||
return toolbox;
|
return toolbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
|
||||||
{
|
{
|
||||||
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
var toolbox:ChartEditorBaseToolbox = ChartEditorDifficultyToolbox.build(state);
|
||||||
|
|
||||||
if (toolbox == null) return null;
|
if (toolbox == null) return null;
|
||||||
|
|
||||||
// Starting position.
|
|
||||||
toolbox.x = 125;
|
|
||||||
toolbox.y = 200;
|
|
||||||
|
|
||||||
toolbox.onDialogClosed = function(event:UIEvent) {
|
|
||||||
state.menubarItemToggleToolboxDifficulty.selected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var difficultyToolboxAddVariation:Null<Button> = toolbox.findComponent('difficultyToolboxAddVariation', Button);
|
|
||||||
if (difficultyToolboxAddVariation == null)
|
|
||||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddVariation component.';
|
|
||||||
var difficultyToolboxAddDifficulty:Null<Button> = toolbox.findComponent('difficultyToolboxAddDifficulty', Button);
|
|
||||||
if (difficultyToolboxAddDifficulty == null)
|
|
||||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddDifficulty component.';
|
|
||||||
var difficultyToolboxSaveMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxSaveMetadata', Button);
|
|
||||||
if (difficultyToolboxSaveMetadata == null)
|
|
||||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveMetadata component.';
|
|
||||||
var difficultyToolboxSaveChart:Null<Button> = toolbox.findComponent('difficultyToolboxSaveChart', Button);
|
|
||||||
if (difficultyToolboxSaveChart == null)
|
|
||||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveChart component.';
|
|
||||||
// var difficultyToolboxSaveAll:Null<Button> = toolbox.findComponent('difficultyToolboxSaveAll', Button);
|
|
||||||
// if (difficultyToolboxSaveAll == null) throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveAll component.';
|
|
||||||
var difficultyToolboxLoadMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxLoadMetadata', Button);
|
|
||||||
if (difficultyToolboxLoadMetadata == null)
|
|
||||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadMetadata component.';
|
|
||||||
var difficultyToolboxLoadChart:Null<Button> = toolbox.findComponent('difficultyToolboxLoadChart', Button);
|
|
||||||
if (difficultyToolboxLoadChart == null)
|
|
||||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadChart component.';
|
|
||||||
|
|
||||||
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
|
|
||||||
state.openAddVariationDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
|
|
||||||
state.openAddDifficultyDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
|
|
||||||
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
|
|
||||||
FileUtil.writeFileReference('${state.currentSongId}$vari-metadata.json', state.currentSongMetadata.serialize());
|
|
||||||
};
|
|
||||||
|
|
||||||
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
|
|
||||||
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
|
|
||||||
FileUtil.writeFileReference('${state.currentSongId}$vari-chart.json', state.currentSongChartData.serialize());
|
|
||||||
};
|
|
||||||
|
|
||||||
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
|
|
||||||
// Replace metadata for current variation.
|
|
||||||
SongSerializer.importSongMetadataAsync(function(songMetadata) {
|
|
||||||
state.currentSongMetadata = songMetadata;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
|
|
||||||
// Replace chart data for current variation.
|
|
||||||
SongSerializer.importSongChartDataAsync(function(songChartData) {
|
|
||||||
state.currentSongChartData = songChartData;
|
|
||||||
state.noteDisplayDirty = true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
state.difficultySelectDirty = true;
|
|
||||||
|
|
||||||
return toolbox;
|
return toolbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
static function onShowToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void
|
|
||||||
{
|
|
||||||
// Update the selected difficulty when reopening the toolbox.
|
|
||||||
var treeView:Null<TreeView> = toolbox.findComponent('difficultyToolboxTree');
|
|
||||||
if (treeView == null) return;
|
|
||||||
|
|
||||||
var current = state.getCurrentTreeDifficultyNode(treeView);
|
|
||||||
if (current == null) return;
|
|
||||||
treeView.selectedNode = current;
|
|
||||||
trace('selected node: ${treeView.selectedNode}');
|
|
||||||
}
|
|
||||||
|
|
||||||
static function buildToolboxMetadataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
|
static function buildToolboxMetadataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
|
||||||
{
|
{
|
||||||
var toolbox:ChartEditorBaseToolbox = ChartEditorMetadataToolbox.build(state);
|
var toolbox:ChartEditorBaseToolbox = ChartEditorMetadataToolbox.build(state);
|
||||||
|
|
|
@ -0,0 +1,239 @@
|
||||||
|
package funkin.ui.debug.charting.toolboxes;
|
||||||
|
|
||||||
|
import funkin.play.character.BaseCharacter.CharacterType;
|
||||||
|
import funkin.play.character.CharacterData;
|
||||||
|
import funkin.data.stage.StageData;
|
||||||
|
import funkin.data.stage.StageRegistry;
|
||||||
|
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
|
||||||
|
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||||
|
import haxe.ui.components.Button;
|
||||||
|
import haxe.ui.components.CheckBox;
|
||||||
|
import haxe.ui.containers.dialogs.Dialogs;
|
||||||
|
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||||
|
import funkin.data.song.SongData.SongMetadata;
|
||||||
|
import haxe.ui.components.DropDown;
|
||||||
|
import haxe.ui.components.HorizontalSlider;
|
||||||
|
import funkin.util.FileUtil;
|
||||||
|
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
|
||||||
|
import funkin.play.song.SongSerializer;
|
||||||
|
import haxe.ui.components.Label;
|
||||||
|
import haxe.ui.components.NumberStepper;
|
||||||
|
import haxe.ui.components.Slider;
|
||||||
|
import haxe.ui.components.TextField;
|
||||||
|
import funkin.play.stage.Stage;
|
||||||
|
import haxe.ui.containers.Box;
|
||||||
|
import haxe.ui.containers.TreeView;
|
||||||
|
import haxe.ui.containers.TreeViewNode;
|
||||||
|
import haxe.ui.containers.Frame;
|
||||||
|
import haxe.ui.events.UIEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The toolbox which allows viewing the list of difficulties, switching to a specific one,
|
||||||
|
* and adding/removing variations and difficulties.
|
||||||
|
*/
|
||||||
|
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
|
||||||
|
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||||
|
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/difficulty.xml"))
|
||||||
|
class ChartEditorDifficultyToolbox extends ChartEditorBaseToolbox
|
||||||
|
{
|
||||||
|
var difficultyToolboxTree:TreeView;
|
||||||
|
var difficultyToolboxAddVariation:Button;
|
||||||
|
var difficultyToolboxAddDifficulty:Button;
|
||||||
|
var difficultyToolboxRemoveDifficulty:Button;
|
||||||
|
var difficultyToolboxSaveMetadata:Button;
|
||||||
|
var difficultyToolboxSaveChart:Button;
|
||||||
|
var difficultyToolboxLoadMetadata:Button;
|
||||||
|
var difficultyToolboxLoadChart:Button;
|
||||||
|
|
||||||
|
public function new(chartEditorState2:ChartEditorState)
|
||||||
|
{
|
||||||
|
super(chartEditorState2);
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
|
||||||
|
this.onDialogClosed = onClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose(event:UIEvent)
|
||||||
|
{
|
||||||
|
chartEditorState.menubarItemToggleToolboxDifficulty.selected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize():Void
|
||||||
|
{
|
||||||
|
// Starting position.
|
||||||
|
// TODO: Save and load this.
|
||||||
|
this.x = 150;
|
||||||
|
this.y = 250;
|
||||||
|
|
||||||
|
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
|
||||||
|
chartEditorState.openAddVariationDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
|
||||||
|
chartEditorState.openAddDifficultyDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
difficultyToolboxRemoveDifficulty.onClick = function(_:UIEvent) {
|
||||||
|
var currentVariation:String = chartEditorState.selectedVariation;
|
||||||
|
var currentDifficulty:String = chartEditorState.selectedDifficulty;
|
||||||
|
|
||||||
|
trace('Removing difficulty "$currentVariation:$currentDifficulty"');
|
||||||
|
|
||||||
|
var callback = (button) -> {
|
||||||
|
switch (button)
|
||||||
|
{
|
||||||
|
case DialogButton.YES:
|
||||||
|
// Remove the difficulty.
|
||||||
|
chartEditorState.removeDifficulty(currentVariation, currentDifficulty);
|
||||||
|
refresh();
|
||||||
|
case DialogButton.NO: // Do nothing.
|
||||||
|
default: // Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialogs.messageBox("Are you sure? This cannot be undone.", "Remove Difficulty", MessageBoxType.TYPE_YESNO, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
|
||||||
|
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
|
||||||
|
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-metadata.json', chartEditorState.currentSongMetadata.serialize());
|
||||||
|
};
|
||||||
|
|
||||||
|
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
|
||||||
|
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
|
||||||
|
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-chart.json', chartEditorState.currentSongChartData.serialize());
|
||||||
|
};
|
||||||
|
|
||||||
|
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
|
||||||
|
// Replace metadata for current variation.
|
||||||
|
SongSerializer.importSongMetadataAsync(function(songMetadata) {
|
||||||
|
chartEditorState.currentSongMetadata = songMetadata;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
|
||||||
|
// Replace chart data for current variation.
|
||||||
|
SongSerializer.importSongChartDataAsync(function(songChartData) {
|
||||||
|
chartEditorState.currentSongChartData = songChartData;
|
||||||
|
chartEditorState.noteDisplayDirty = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the tree view and rebuild it with the current song metadata (variation and difficulty list).
|
||||||
|
*/
|
||||||
|
public function updateTree():Void
|
||||||
|
{
|
||||||
|
// Clear the tree view so we can rebuild it.
|
||||||
|
difficultyToolboxTree.clearNodes();
|
||||||
|
|
||||||
|
// , icon: 'haxeui-core/styles/default/haxeui_tiny.png'
|
||||||
|
var treeSong:TreeViewNode = difficultyToolboxTree.addNode({id: 'stv_song', text: 'S: ${chartEditorState.currentSongName}'});
|
||||||
|
treeSong.expanded = true;
|
||||||
|
|
||||||
|
for (curVariation in chartEditorState.availableVariations)
|
||||||
|
{
|
||||||
|
var variationMetadata:Null<SongMetadata> = chartEditorState.songMetadata.get(curVariation);
|
||||||
|
if (variationMetadata == null) continue;
|
||||||
|
|
||||||
|
var treeVariation:TreeViewNode = treeSong.addNode(
|
||||||
|
{
|
||||||
|
id: 'stv_variation_$curVariation',
|
||||||
|
text: 'V: ${curVariation.toTitleCase()}'
|
||||||
|
});
|
||||||
|
treeVariation.expanded = true;
|
||||||
|
|
||||||
|
var difficultyList:Array<String> = variationMetadata.playData.difficulties;
|
||||||
|
|
||||||
|
for (difficulty in difficultyList)
|
||||||
|
{
|
||||||
|
var _treeDifficulty:TreeViewNode = treeVariation.addNode(
|
||||||
|
{
|
||||||
|
id: 'stv_difficulty_${curVariation}_$difficulty',
|
||||||
|
text: 'D: ${difficulty.toTitleCase()}'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
difficultyToolboxTree.onChange = onTreeChange;
|
||||||
|
refreshTreeSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the selected item in the tree to the current variation/difficulty.
|
||||||
|
*
|
||||||
|
* @param targetNode The node to select. If null, the current variation/difficulty will be used.
|
||||||
|
*/
|
||||||
|
public function refreshTreeSelection():Void
|
||||||
|
{
|
||||||
|
var targetNode = getCurrentTreeNode();
|
||||||
|
if (targetNode != null) difficultyToolboxTree.selectedNode = targetNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the node in the tree representing the current variation/difficulty.
|
||||||
|
*/
|
||||||
|
function getCurrentTreeNode():TreeViewNode
|
||||||
|
{
|
||||||
|
return
|
||||||
|
difficultyToolboxTree.findNodeByPath('stv_song/stv_variation_$chartEditorState.selectedVariation/stv_difficulty_${chartEditorState.selectedVariation}_$chartEditorState.selectedDifficulty',
|
||||||
|
'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an item in the tree is selected. Updates the current variation/difficulty.
|
||||||
|
*/
|
||||||
|
function onTreeChange(event:UIEvent):Void
|
||||||
|
{
|
||||||
|
// Get the newly selected node.
|
||||||
|
var treeView:TreeView = cast event.target;
|
||||||
|
var targetNode:TreeViewNode = difficultyToolboxTree.selectedNode;
|
||||||
|
|
||||||
|
if (targetNode == null)
|
||||||
|
{
|
||||||
|
trace('No target node!');
|
||||||
|
// Reset the user's selection.
|
||||||
|
refreshTreeSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (targetNode.data.id.split('_')[1])
|
||||||
|
{
|
||||||
|
case 'difficulty':
|
||||||
|
var variation:String = targetNode.data.id.split('_')[2];
|
||||||
|
var difficulty:String = targetNode.data.id.split('_')[3];
|
||||||
|
|
||||||
|
if (variation != null && difficulty != null)
|
||||||
|
{
|
||||||
|
trace('Changing difficulty to "$variation:$difficulty"');
|
||||||
|
chartEditorState.selectedVariation = variation;
|
||||||
|
chartEditorState.selectedDifficulty = difficulty;
|
||||||
|
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||||
|
refreshTreeSelection();
|
||||||
|
}
|
||||||
|
// case 'song':
|
||||||
|
// case 'variation':
|
||||||
|
default:
|
||||||
|
// Reset the user's selection.
|
||||||
|
trace('Selected wrong node type, resetting selection.');
|
||||||
|
refreshTreeSelection();
|
||||||
|
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override function refresh():Void
|
||||||
|
{
|
||||||
|
super.refresh();
|
||||||
|
|
||||||
|
refreshTreeSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function build(chartEditorState:ChartEditorState):ChartEditorDifficultyToolbox
|
||||||
|
{
|
||||||
|
return new ChartEditorDifficultyToolbox(chartEditorState);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ package funkin.ui.debug.charting.toolboxes;
|
||||||
|
|
||||||
import funkin.play.character.BaseCharacter.CharacterType;
|
import funkin.play.character.BaseCharacter.CharacterType;
|
||||||
import funkin.play.character.CharacterData;
|
import funkin.play.character.CharacterData;
|
||||||
import funkin.play.stage.StageData;
|
import funkin.data.stage.StageData;
|
||||||
import funkin.play.event.SongEvent;
|
import funkin.play.event.SongEvent;
|
||||||
import funkin.data.event.SongEventSchema;
|
import funkin.data.event.SongEventSchema;
|
||||||
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
|
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
|
||||||
|
|
|
@ -2,7 +2,8 @@ package funkin.ui.debug.charting.toolboxes;
|
||||||
|
|
||||||
import funkin.play.character.BaseCharacter.CharacterType;
|
import funkin.play.character.BaseCharacter.CharacterType;
|
||||||
import funkin.play.character.CharacterData;
|
import funkin.play.character.CharacterData;
|
||||||
import funkin.play.stage.StageData;
|
import funkin.data.stage.StageData;
|
||||||
|
import funkin.data.stage.StageRegistry;
|
||||||
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
|
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
|
||||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||||
import haxe.ui.components.Button;
|
import haxe.ui.components.Button;
|
||||||
|
@ -13,6 +14,7 @@ import haxe.ui.components.Label;
|
||||||
import haxe.ui.components.NumberStepper;
|
import haxe.ui.components.NumberStepper;
|
||||||
import haxe.ui.components.Slider;
|
import haxe.ui.components.Slider;
|
||||||
import haxe.ui.components.TextField;
|
import haxe.ui.components.TextField;
|
||||||
|
import funkin.play.stage.Stage;
|
||||||
import haxe.ui.containers.Box;
|
import haxe.ui.containers.Box;
|
||||||
import haxe.ui.containers.Frame;
|
import haxe.ui.containers.Frame;
|
||||||
import haxe.ui.events.UIEvent;
|
import haxe.ui.events.UIEvent;
|
||||||
|
@ -148,7 +150,12 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
||||||
inputOffsetVocal.onChange = function(event:UIEvent) {
|
inputOffsetVocal.onChange = function(event:UIEvent) {
|
||||||
if (event.value == null) return;
|
if (event.value == null) return;
|
||||||
|
|
||||||
chartEditorState.currentSongMetadata.offsets.setVocalOffset(chartEditorState.currentSongMetadata.playData.characters.player, event.value);
|
chartEditorState.currentVocalOffset = event.value;
|
||||||
|
if (chartEditorState.audioVocalTrackGroup != null)
|
||||||
|
{
|
||||||
|
chartEditorState.audioVocalTrackGroup.playerVoicesOffset = event.value;
|
||||||
|
chartEditorState.audioVocalTrackGroup.opponentVoicesOffset = event.value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
inputScrollSpeed.onChange = function(event:UIEvent) {
|
inputScrollSpeed.onChange = function(event:UIEvent) {
|
||||||
var valid:Bool = event.target.value != null && event.target.value > 0;
|
var valid:Bool = event.target.value != null && event.target.value > 0;
|
||||||
|
@ -189,6 +196,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
||||||
inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
|
inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
|
||||||
inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
|
inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
|
||||||
inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
|
inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
|
||||||
|
inputOffsetInst.value = chartEditorState.currentSongMetadata.offsets.getInstrumentalOffset();
|
||||||
|
inputOffsetVocal.value = chartEditorState.currentSongMetadata.offsets.getVocalOffset(chartEditorState.currentSongMetadata.playData.characters.player);
|
||||||
inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed;
|
inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed;
|
||||||
labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
|
labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
|
||||||
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
|
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
|
||||||
|
@ -199,11 +208,11 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
||||||
inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature};
|
inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature};
|
||||||
|
|
||||||
var stageId:String = chartEditorState.currentSongMetadata.playData.stage;
|
var stageId:String = chartEditorState.currentSongMetadata.playData.stage;
|
||||||
var stageData:Null<StageData> = StageDataParser.parseStageData(stageId);
|
var stage:Null<Stage> = StageRegistry.instance.fetchEntry(stageId);
|
||||||
if (inputStage != null)
|
if (inputStage != null)
|
||||||
{
|
{
|
||||||
inputStage.value = (stageData != null) ?
|
inputStage.value = (stage != null) ?
|
||||||
{id: stageId, text: stageData.name} :
|
{id: stage.id, text: stage.stageName} :
|
||||||
{id: "mainStage", text: "Main Stage"};
|
{id: "mainStage", text: "Main Stage"};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,11 @@ package funkin.ui.debug.charting.util;
|
||||||
|
|
||||||
import funkin.data.notestyle.NoteStyleRegistry;
|
import funkin.data.notestyle.NoteStyleRegistry;
|
||||||
import funkin.play.notes.notestyle.NoteStyle;
|
import funkin.play.notes.notestyle.NoteStyle;
|
||||||
import funkin.play.stage.StageData;
|
import funkin.data.stage.StageData;
|
||||||
import funkin.play.stage.StageData.StageDataParser;
|
import funkin.data.stage.StageRegistry;
|
||||||
import funkin.play.character.CharacterData;
|
import funkin.play.character.CharacterData;
|
||||||
import haxe.ui.components.DropDown;
|
import haxe.ui.components.DropDown;
|
||||||
|
import funkin.play.stage.Stage;
|
||||||
import funkin.play.character.BaseCharacter.CharacterType;
|
import funkin.play.character.BaseCharacter.CharacterType;
|
||||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||||
|
|
||||||
|
@ -60,16 +61,16 @@ class ChartEditorDropdowns
|
||||||
{
|
{
|
||||||
dropDown.dataSource.clear();
|
dropDown.dataSource.clear();
|
||||||
|
|
||||||
var stageIds:Array<String> = StageDataParser.listStageIds();
|
var stageIds:Array<String> = StageRegistry.instance.listEntryIds();
|
||||||
|
|
||||||
var returnValue:DropDownEntry = {id: "mainStage", text: "Main Stage"};
|
var returnValue:DropDownEntry = {id: "mainStage", text: "Main Stage"};
|
||||||
|
|
||||||
for (stageId in stageIds)
|
for (stageId in stageIds)
|
||||||
{
|
{
|
||||||
var stage:Null<StageData> = StageDataParser.parseStageData(stageId);
|
var stage:Null<Stage> = StageRegistry.instance.fetchEntry(stageId);
|
||||||
if (stage == null) continue;
|
if (stage == null) continue;
|
||||||
|
|
||||||
var value = {id: stageId, text: stage.name};
|
var value = {id: stage.id, text: stage.stageName};
|
||||||
if (startingStageId == stageId) returnValue = value;
|
if (startingStageId == stageId) returnValue = value;
|
||||||
|
|
||||||
dropDown.dataSource.add(value);
|
dropDown.dataSource.add(value);
|
||||||
|
|
|
@ -5,15 +5,17 @@ import flixel.input.mouse.FlxMouseEvent;
|
||||||
import flixel.math.FlxPoint;
|
import flixel.math.FlxPoint;
|
||||||
import funkin.play.character.BaseCharacter;
|
import funkin.play.character.BaseCharacter;
|
||||||
import funkin.play.PlayState;
|
import funkin.play.PlayState;
|
||||||
import funkin.play.stage.StageData;
|
import funkin.data.stage.StageData;
|
||||||
import funkin.play.stage.StageProp;
|
import funkin.play.stage.StageProp;
|
||||||
import funkin.graphics.shaders.StrokeShader;
|
import funkin.graphics.shaders.StrokeShader;
|
||||||
import funkin.ui.haxeui.HaxeUISubState;
|
import funkin.ui.haxeui.HaxeUISubState;
|
||||||
import funkin.ui.debug.stage.StageEditorCommand;
|
import funkin.ui.debug.stage.StageEditorCommand;
|
||||||
import funkin.util.SerializerUtil;
|
import funkin.util.SerializerUtil;
|
||||||
|
import funkin.data.stage.StageRegistry;
|
||||||
import funkin.util.MouseUtil;
|
import funkin.util.MouseUtil;
|
||||||
import haxe.ui.containers.ListView;
|
import haxe.ui.containers.ListView;
|
||||||
import haxe.ui.core.Component;
|
import haxe.ui.core.Component;
|
||||||
|
import funkin.graphics.FunkinSprite;
|
||||||
import haxe.ui.events.UIEvent;
|
import haxe.ui.events.UIEvent;
|
||||||
import haxe.ui.RuntimeComponentBuilder;
|
import haxe.ui.RuntimeComponentBuilder;
|
||||||
import openfl.events.Event;
|
import openfl.events.Event;
|
||||||
|
@ -354,7 +356,13 @@ class StageOffsetSubState extends HaxeUISubState
|
||||||
|
|
||||||
function prepStageStuff():String
|
function prepStageStuff():String
|
||||||
{
|
{
|
||||||
var stageLol:StageData = StageDataParser.parseStageData(PlayState.instance.currentStageId);
|
var stageLol:StageData = StageRegistry.instance.fetchEntry(PlayState.instance.currentStageId)?._data;
|
||||||
|
|
||||||
|
if (stageLol == null)
|
||||||
|
{
|
||||||
|
FlxG.log.error("Stage not found in registry!");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
for (prop in stageLol.props)
|
for (prop in stageLol.props)
|
||||||
{
|
{
|
||||||
|
@ -378,6 +386,6 @@ class StageOffsetSubState extends HaxeUISubState
|
||||||
stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x);
|
stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x);
|
||||||
stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y);
|
stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y);
|
||||||
|
|
||||||
return SerializerUtil.toJSON(stageLol);
|
return stageLol.serialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package funkin.ui.freeplay;
|
package funkin.ui.freeplay;
|
||||||
|
|
||||||
import funkin.input.Controls;
|
|
||||||
import flash.text.TextField;
|
import flash.text.TextField;
|
||||||
import flixel.addons.display.FlxGridOverlay;
|
import flixel.addons.display.FlxGridOverlay;
|
||||||
import flixel.addons.transition.FlxTransitionableState;
|
import flixel.addons.transition.FlxTransitionableState;
|
||||||
|
@ -23,33 +22,35 @@ import flixel.tweens.FlxTween;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import flixel.util.FlxSpriteUtil;
|
import flixel.util.FlxSpriteUtil;
|
||||||
import flixel.util.FlxTimer;
|
import flixel.util.FlxTimer;
|
||||||
import funkin.input.Controls.Control;
|
|
||||||
import funkin.data.level.LevelRegistry;
|
import funkin.data.level.LevelRegistry;
|
||||||
import funkin.data.song.SongRegistry;
|
import funkin.data.song.SongRegistry;
|
||||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||||
import funkin.graphics.shaders.AngleMask;
|
import funkin.graphics.shaders.AngleMask;
|
||||||
import funkin.graphics.shaders.HSVShader;
|
import funkin.graphics.shaders.HSVShader;
|
||||||
import funkin.graphics.shaders.PureColor;
|
import funkin.graphics.shaders.PureColor;
|
||||||
import funkin.util.MathUtil;
|
|
||||||
import funkin.graphics.shaders.StrokeShader;
|
import funkin.graphics.shaders.StrokeShader;
|
||||||
|
import funkin.input.Controls;
|
||||||
|
import funkin.input.Controls.Control;
|
||||||
import funkin.play.components.HealthIcon;
|
import funkin.play.components.HealthIcon;
|
||||||
import funkin.play.PlayState;
|
import funkin.play.PlayState;
|
||||||
import funkin.play.PlayStatePlaylist;
|
import funkin.play.PlayStatePlaylist;
|
||||||
import funkin.play.song.Song;
|
import funkin.play.song.Song;
|
||||||
import funkin.save.Save;
|
import funkin.save.Save;
|
||||||
import funkin.save.Save.SaveScoreData;
|
import funkin.save.Save.SaveScoreData;
|
||||||
|
import funkin.ui.AtlasText;
|
||||||
import funkin.ui.freeplay.BGScrollingText;
|
import funkin.ui.freeplay.BGScrollingText;
|
||||||
import funkin.ui.freeplay.DifficultyStars;
|
import funkin.ui.freeplay.DifficultyStars;
|
||||||
import funkin.ui.freeplay.DJBoyfriend;
|
import funkin.ui.freeplay.DJBoyfriend;
|
||||||
import funkin.ui.freeplay.FreeplayScore;
|
import funkin.ui.freeplay.FreeplayScore;
|
||||||
import funkin.ui.freeplay.LetterSort;
|
import funkin.ui.freeplay.LetterSort;
|
||||||
import funkin.ui.freeplay.SongMenuItem;
|
import funkin.ui.freeplay.SongMenuItem;
|
||||||
|
import funkin.ui.mainmenu.MainMenuState;
|
||||||
import funkin.ui.MusicBeatState;
|
import funkin.ui.MusicBeatState;
|
||||||
import funkin.ui.MusicBeatSubState;
|
import funkin.ui.MusicBeatSubState;
|
||||||
import funkin.ui.mainmenu.MainMenuState;
|
|
||||||
import funkin.ui.transition.LoadingState;
|
import funkin.ui.transition.LoadingState;
|
||||||
import funkin.ui.transition.StickerSubState;
|
import funkin.ui.transition.StickerSubState;
|
||||||
import funkin.util.MathUtil;
|
import funkin.util.MathUtil;
|
||||||
|
import funkin.util.MathUtil;
|
||||||
import lime.app.Future;
|
import lime.app.Future;
|
||||||
import lime.utils.Assets;
|
import lime.utils.Assets;
|
||||||
|
|
||||||
|
@ -64,7 +65,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
|
var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
|
||||||
|
|
||||||
var fp:FreeplayScore;
|
var fp:FreeplayScore;
|
||||||
var txtCompletion:FlxText;
|
var txtCompletion:AtlasText;
|
||||||
var lerpCompletion:Float = 0;
|
var lerpCompletion:Float = 0;
|
||||||
var intendedCompletion:Float = 0;
|
var intendedCompletion:Float = 0;
|
||||||
var lerpScore:Float = 0;
|
var lerpScore:Float = 0;
|
||||||
|
@ -87,6 +88,8 @@ class FreeplayState extends MusicBeatSubState
|
||||||
var grpCapsules:FlxTypedGroup<SongMenuItem>;
|
var grpCapsules:FlxTypedGroup<SongMenuItem>;
|
||||||
var curCapsule:SongMenuItem;
|
var curCapsule:SongMenuItem;
|
||||||
var curPlaying:Bool = false;
|
var curPlaying:Bool = false;
|
||||||
|
var ostName:FlxText;
|
||||||
|
var difficultyStars:DifficultyStars;
|
||||||
|
|
||||||
var dj:DJBoyfriend;
|
var dj:DJBoyfriend;
|
||||||
|
|
||||||
|
@ -150,15 +153,10 @@ class FreeplayState extends MusicBeatSubState
|
||||||
for (songId in LevelRegistry.instance.parseEntryData(levelId).songs)
|
for (songId in LevelRegistry.instance.parseEntryData(levelId).songs)
|
||||||
{
|
{
|
||||||
var song:Song = SongRegistry.instance.fetchEntry(songId);
|
var song:Song = SongRegistry.instance.fetchEntry(songId);
|
||||||
var songBaseDifficulty:SongDifficulty = song.getDifficulty(Constants.DEFAULT_DIFFICULTY);
|
|
||||||
|
|
||||||
var songName = songBaseDifficulty.songName;
|
songs.push(new FreeplaySongData(levelId, songId, song));
|
||||||
var songOpponent = songBaseDifficulty.characters.opponent;
|
|
||||||
var songDifficulties = song.listDifficulties();
|
|
||||||
|
|
||||||
songs.push(new FreeplaySongData(songId, songName, levelId, songOpponent, songDifficulties));
|
for (difficulty in song.listDifficulties())
|
||||||
|
|
||||||
for (difficulty in songDifficulties)
|
|
||||||
{
|
{
|
||||||
diffIdsTotal.pushUnique(difficulty);
|
diffIdsTotal.pushUnique(difficulty);
|
||||||
}
|
}
|
||||||
|
@ -334,6 +332,8 @@ class FreeplayState extends MusicBeatSubState
|
||||||
if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true;
|
if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: This is an AtlasSprite because we use an animation to bring it into view.
|
||||||
|
// TODO: Add the ability to select the album graphic.
|
||||||
var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll"));
|
var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll"));
|
||||||
albumArt.visible = false;
|
albumArt.visible = false;
|
||||||
add(albumArt);
|
add(albumArt);
|
||||||
|
@ -347,7 +347,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
|
|
||||||
var albumTitle:FlxSprite = new FlxSprite(947, 491).loadGraphic(Paths.image('freeplay/albumTitle-fnfvol1'));
|
var albumTitle:FlxSprite = new FlxSprite(947, 491).loadGraphic(Paths.image('freeplay/albumTitle-fnfvol1'));
|
||||||
var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
|
var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
|
||||||
var difficultyStars:DifficultyStars = new DifficultyStars(140, 39);
|
difficultyStars = new DifficultyStars(140, 39);
|
||||||
|
|
||||||
difficultyStars.stars.visible = false;
|
difficultyStars.stars.visible = false;
|
||||||
albumTitle.visible = false;
|
albumTitle.visible = false;
|
||||||
|
@ -382,11 +382,16 @@ class FreeplayState extends MusicBeatSubState
|
||||||
add(overhangStuff);
|
add(overhangStuff);
|
||||||
FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut});
|
FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut});
|
||||||
|
|
||||||
var fnfFreeplay:FlxText = new FlxText(0, 12, 0, "FREEPLAY", 48);
|
var fnfFreeplay:FlxText = new FlxText(8, 8, 0, "FREEPLAY", 48);
|
||||||
fnfFreeplay.font = "VCR OSD Mono";
|
fnfFreeplay.font = "VCR OSD Mono";
|
||||||
fnfFreeplay.visible = false;
|
fnfFreeplay.visible = false;
|
||||||
|
|
||||||
exitMovers.set([overhangStuff, fnfFreeplay],
|
ostName = new FlxText(8, 8, FlxG.width - 8 - 8, "OFFICIAL OST", 48);
|
||||||
|
ostName.font = "VCR OSD Mono";
|
||||||
|
ostName.alignment = RIGHT;
|
||||||
|
ostName.visible = false;
|
||||||
|
|
||||||
|
exitMovers.set([overhangStuff, fnfFreeplay, ostName],
|
||||||
{
|
{
|
||||||
y: -overhangStuff.height,
|
y: -overhangStuff.height,
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -397,8 +402,9 @@ class FreeplayState extends MusicBeatSubState
|
||||||
var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2);
|
var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2);
|
||||||
fnfFreeplay.shader = sillyStroke;
|
fnfFreeplay.shader = sillyStroke;
|
||||||
add(fnfFreeplay);
|
add(fnfFreeplay);
|
||||||
|
add(ostName);
|
||||||
|
|
||||||
var fnfHighscoreSpr:FlxSprite = new FlxSprite(890, 70);
|
var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70);
|
||||||
fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
|
fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
|
||||||
fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
|
fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
|
||||||
fnfHighscoreSpr.visible = false;
|
fnfHighscoreSpr.visible = false;
|
||||||
|
@ -415,8 +421,10 @@ class FreeplayState extends MusicBeatSubState
|
||||||
fp.visible = false;
|
fp.visible = false;
|
||||||
add(fp);
|
add(fp);
|
||||||
|
|
||||||
txtCompletion = new FlxText(1200, 77, 0, "0", 32);
|
var clearBoxSprite:FlxSprite = new FlxSprite(1165, 65).loadGraphic(Paths.image('freeplay/clearBox'));
|
||||||
txtCompletion.font = "VCR OSD Mono";
|
add(clearBoxSprite);
|
||||||
|
|
||||||
|
txtCompletion = new AtlasText(1185, 87, "69", AtlasFont.FREEPLAY_CLEAR);
|
||||||
txtCompletion.visible = false;
|
txtCompletion.visible = false;
|
||||||
add(txtCompletion);
|
add(txtCompletion);
|
||||||
|
|
||||||
|
@ -485,6 +493,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
new FlxTimer().start(1 / 24, function(handShit) {
|
new FlxTimer().start(1 / 24, function(handShit) {
|
||||||
fnfHighscoreSpr.visible = true;
|
fnfHighscoreSpr.visible = true;
|
||||||
fnfFreeplay.visible = true;
|
fnfFreeplay.visible = true;
|
||||||
|
ostName.visible = true;
|
||||||
fp.visible = true;
|
fp.visible = true;
|
||||||
fp.updateScore(0);
|
fp.updateScore(0);
|
||||||
|
|
||||||
|
@ -674,9 +683,32 @@ class FreeplayState extends MusicBeatSubState
|
||||||
lerpScore = MathUtil.coolLerp(lerpScore, intendedScore, 0.2);
|
lerpScore = MathUtil.coolLerp(lerpScore, intendedScore, 0.2);
|
||||||
lerpCompletion = MathUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9);
|
lerpCompletion = MathUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9);
|
||||||
|
|
||||||
|
if (Math.isNaN(lerpScore))
|
||||||
|
{
|
||||||
|
lerpScore = intendedScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.isNaN(lerpCompletion))
|
||||||
|
{
|
||||||
|
lerpCompletion = intendedCompletion;
|
||||||
|
}
|
||||||
|
|
||||||
fp.updateScore(Std.int(lerpScore));
|
fp.updateScore(Std.int(lerpScore));
|
||||||
|
|
||||||
txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
|
txtCompletion.text = '${Math.floor(lerpCompletion * 100)}';
|
||||||
|
|
||||||
|
// Right align the completion percentage
|
||||||
|
switch (txtCompletion.text.length)
|
||||||
|
{
|
||||||
|
case 3:
|
||||||
|
txtCompletion.x = 1185 - 10;
|
||||||
|
case 2:
|
||||||
|
txtCompletion.x = 1185;
|
||||||
|
case 1:
|
||||||
|
txtCompletion.x = 1185 + 24;
|
||||||
|
default:
|
||||||
|
txtCompletion.x = 1185;
|
||||||
|
}
|
||||||
|
|
||||||
handleInputs(elapsed);
|
handleInputs(elapsed);
|
||||||
}
|
}
|
||||||
|
@ -913,6 +945,11 @@ class FreeplayState extends MusicBeatSubState
|
||||||
intendedCompletion = 0.0;
|
intendedCompletion = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intendedCompletion == Math.POSITIVE_INFINITY || intendedCompletion == Math.NEGATIVE_INFINITY || Math.isNaN(intendedCompletion))
|
||||||
|
{
|
||||||
|
intendedCompletion = 0;
|
||||||
|
}
|
||||||
|
|
||||||
grpDifficulties.group.forEach(function(diffSprite) {
|
grpDifficulties.group.forEach(function(diffSprite) {
|
||||||
diffSprite.visible = false;
|
diffSprite.visible = false;
|
||||||
});
|
});
|
||||||
|
@ -938,6 +975,27 @@ class FreeplayState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (change != 0)
|
||||||
|
{
|
||||||
|
// Update the song capsules to reflect the new difficulty info.
|
||||||
|
for (songCapsule in grpCapsules.members)
|
||||||
|
{
|
||||||
|
if (songCapsule == null) continue;
|
||||||
|
if (songCapsule.songData != null)
|
||||||
|
{
|
||||||
|
songCapsule.songData.currentDifficulty = currentDifficulty;
|
||||||
|
songCapsule.init(null, null, songCapsule.songData);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
songCapsule.init(null, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the difficulty star count on the right.
|
||||||
|
difficultyStars.difficulty = daSong?.songRating ?? difficultyStars.difficulty; // yay haxe 4.3
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
|
// Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
|
||||||
|
@ -1046,6 +1104,10 @@ class FreeplayState extends MusicBeatSubState
|
||||||
{
|
{
|
||||||
currentDifficulty = rememberedDifficulty;
|
currentDifficulty = rememberedDifficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the difficulty star count on the right.
|
||||||
|
var daSong = songs[curSelected];
|
||||||
|
difficultyStars.difficulty = daSong?.songRating ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSelection(change:Int = 0)
|
function changeSelection(change:Int = 0)
|
||||||
|
@ -1176,19 +1238,47 @@ class FreeplaySongData
|
||||||
{
|
{
|
||||||
public var isFav:Bool = false;
|
public var isFav:Bool = false;
|
||||||
|
|
||||||
public var songId:String = "";
|
var song:Song;
|
||||||
public var songName:String = "";
|
|
||||||
public var levelId:String = "";
|
|
||||||
public var songCharacter:String = "";
|
|
||||||
public var songDifficulties:Array<String> = [];
|
|
||||||
|
|
||||||
public function new(songId:String, songName:String, levelId:String, songCharacter:String, songDifficulties:Array<String>)
|
public var levelId(default, null):String = "";
|
||||||
|
public var songId(default, null):String = "";
|
||||||
|
|
||||||
|
public var songDifficulties(default, null):Array<String> = [];
|
||||||
|
|
||||||
|
public var songName(default, null):String = "";
|
||||||
|
public var songCharacter(default, null):String = "";
|
||||||
|
public var songRating(default, null):Int = 0;
|
||||||
|
|
||||||
|
public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
|
||||||
|
|
||||||
|
function set_currentDifficulty(value:String):String
|
||||||
|
{
|
||||||
|
if (currentDifficulty == value) return value;
|
||||||
|
|
||||||
|
currentDifficulty = value;
|
||||||
|
updateValues();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function new(levelId:String, songId:String, song:Song)
|
||||||
{
|
{
|
||||||
this.songId = songId;
|
|
||||||
this.songName = songName;
|
|
||||||
this.levelId = levelId;
|
this.levelId = levelId;
|
||||||
this.songCharacter = songCharacter;
|
this.songId = songId;
|
||||||
this.songDifficulties = songDifficulties;
|
this.song = song;
|
||||||
|
|
||||||
|
updateValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValues():Void
|
||||||
|
{
|
||||||
|
this.songDifficulties = song.listDifficulties();
|
||||||
|
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
|
||||||
|
|
||||||
|
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty);
|
||||||
|
if (songDifficulty == null) return;
|
||||||
|
this.songName = songDifficulty.songName;
|
||||||
|
this.songCharacter = songDifficulty.characters.opponent;
|
||||||
|
this.songRating = songDifficulty.difficultyRating;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,11 +35,6 @@ class SongMenuItem extends FlxSpriteGroup
|
||||||
|
|
||||||
var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect"];
|
var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect"];
|
||||||
|
|
||||||
// lol...
|
|
||||||
var diffRanks:Array<String> = [
|
|
||||||
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "14", "15"
|
|
||||||
];
|
|
||||||
|
|
||||||
public var targetPos:FlxPoint = new FlxPoint();
|
public var targetPos:FlxPoint = new FlxPoint();
|
||||||
public var doLerp:Bool = false;
|
public var doLerp:Bool = false;
|
||||||
public var doJumpIn:Bool = false;
|
public var doJumpIn:Bool = false;
|
||||||
|
@ -47,10 +42,12 @@ class SongMenuItem extends FlxSpriteGroup
|
||||||
public var doJumpOut:Bool = false;
|
public var doJumpOut:Bool = false;
|
||||||
|
|
||||||
public var onConfirm:Void->Void;
|
public var onConfirm:Void->Void;
|
||||||
public var diffGrayscale:Grayscale;
|
public var grayscaleShader:Grayscale;
|
||||||
|
|
||||||
public var hsvShader(default, set):HSVShader;
|
public var hsvShader(default, set):HSVShader;
|
||||||
|
|
||||||
|
var diffRatingSprite:FlxSprite;
|
||||||
|
|
||||||
public function new(x:Float, y:Float)
|
public function new(x:Float, y:Float)
|
||||||
{
|
{
|
||||||
super(x, y);
|
super(x, y);
|
||||||
|
@ -75,26 +72,30 @@ class SongMenuItem extends FlxSpriteGroup
|
||||||
add(ranking);
|
add(ranking);
|
||||||
grpHide.add(ranking);
|
grpHide.add(ranking);
|
||||||
|
|
||||||
diffGrayscale = new Grayscale(1);
|
|
||||||
|
|
||||||
var diffRank = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRankings/diff" + FlxG.random.getObject(diffRanks)));
|
|
||||||
diffRank.shader = diffGrayscale;
|
|
||||||
diffRank.visible = false;
|
|
||||||
add(diffRank);
|
|
||||||
diffRank.origin.set(capsule.origin.x - diffRank.x, capsule.origin.y - diffRank.y);
|
|
||||||
grpHide.add(diffRank);
|
|
||||||
|
|
||||||
switch (rank)
|
switch (rank)
|
||||||
{
|
{
|
||||||
case "perfect":
|
case "perfect":
|
||||||
ranking.x -= 10;
|
ranking.x -= 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grayscaleShader = new Grayscale(1);
|
||||||
|
|
||||||
|
diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRatings/diff00"));
|
||||||
|
diffRatingSprite.shader = grayscaleShader;
|
||||||
|
diffRatingSprite.visible = false;
|
||||||
|
add(diffRatingSprite);
|
||||||
|
diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y);
|
||||||
|
grpHide.add(diffRatingSprite);
|
||||||
|
|
||||||
songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled));
|
songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled));
|
||||||
add(songText);
|
add(songText);
|
||||||
grpHide.add(songText);
|
grpHide.add(songText);
|
||||||
|
|
||||||
pixelIcon = new FlxSprite(155, 15);
|
// TODO: Use value from metadata instead of random.
|
||||||
|
updateDifficultyRating(FlxG.random.int(0, 15));
|
||||||
|
|
||||||
|
pixelIcon = new FlxSprite(160, 35);
|
||||||
|
|
||||||
pixelIcon.makeGraphic(32, 32, 0x00000000);
|
pixelIcon.makeGraphic(32, 32, 0x00000000);
|
||||||
pixelIcon.antialiasing = false;
|
pixelIcon.antialiasing = false;
|
||||||
pixelIcon.active = false;
|
pixelIcon.active = false;
|
||||||
|
@ -113,6 +114,12 @@ class SongMenuItem extends FlxSpriteGroup
|
||||||
setVisibleGrp(false);
|
setVisibleGrp(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateDifficultyRating(newRating:Int)
|
||||||
|
{
|
||||||
|
var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
|
||||||
|
diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
|
||||||
|
}
|
||||||
|
|
||||||
function set_hsvShader(value:HSVShader):HSVShader
|
function set_hsvShader(value:HSVShader):HSVShader
|
||||||
{
|
{
|
||||||
this.hsvShader = value;
|
this.hsvShader = value;
|
||||||
|
@ -149,16 +156,17 @@ class SongMenuItem extends FlxSpriteGroup
|
||||||
updateSelected();
|
updateSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function init(x:Float, y:Float, songData:Null<FreeplaySongData>)
|
public function init(?x:Float, ?y:Float, songData:Null<FreeplaySongData>)
|
||||||
{
|
{
|
||||||
this.x = x;
|
if (x != null) this.x = x;
|
||||||
this.y = y;
|
if (y != null) this.y = y;
|
||||||
this.songData = songData;
|
this.songData = songData;
|
||||||
|
|
||||||
// Update capsule text.
|
// Update capsule text.
|
||||||
songText.text = songData?.songName ?? 'Random';
|
songText.text = songData?.songName ?? 'Random';
|
||||||
// Update capsule character.
|
// Update capsule character.
|
||||||
if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
|
if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
|
||||||
|
updateDifficultyRating(songData?.songRating ?? 0);
|
||||||
// Update opacity, offsets, etc.
|
// Update opacity, offsets, etc.
|
||||||
updateSelected();
|
updateSelected();
|
||||||
}
|
}
|
||||||
|
@ -200,7 +208,14 @@ class SongMenuItem extends FlxSpriteGroup
|
||||||
|
|
||||||
pixelIcon.loadGraphic(Paths.image(charPath));
|
pixelIcon.loadGraphic(Paths.image(charPath));
|
||||||
pixelIcon.scale.x = pixelIcon.scale.y = 2;
|
pixelIcon.scale.x = pixelIcon.scale.y = 2;
|
||||||
pixelIcon.origin.x = 100;
|
|
||||||
|
switch (char)
|
||||||
|
{
|
||||||
|
case "parents-christmas":
|
||||||
|
pixelIcon.origin.x = 140;
|
||||||
|
default:
|
||||||
|
pixelIcon.origin.x = 100;
|
||||||
|
}
|
||||||
// pixelIcon.origin.x = capsule.origin.x;
|
// pixelIcon.origin.x = capsule.origin.x;
|
||||||
// pixelIcon.offset.x -= pixelIcon.origin.x;
|
// pixelIcon.offset.x -= pixelIcon.origin.x;
|
||||||
}
|
}
|
||||||
|
@ -336,7 +351,7 @@ class SongMenuItem extends FlxSpriteGroup
|
||||||
|
|
||||||
function updateSelected():Void
|
function updateSelected():Void
|
||||||
{
|
{
|
||||||
diffGrayscale.setAmount(this.selected ? 0 : 0.8);
|
grayscaleShader.setAmount(this.selected ? 0 : 0.8);
|
||||||
songText.alpha = this.selected ? 1 : 0.6;
|
songText.alpha = this.selected ? 1 : 0.6;
|
||||||
songText.blurredText.visible = this.selected ? true : false;
|
songText.blurredText.visible = this.selected ? true : false;
|
||||||
capsule.offset.x = this.selected ? 0 : -5;
|
capsule.offset.x = this.selected ? 0 : -5;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import flixel.group.FlxGroup;
|
||||||
import flixel.input.actions.FlxActionInput;
|
import flixel.input.actions.FlxActionInput;
|
||||||
import flixel.input.gamepad.FlxGamepadInputID;
|
import flixel.input.gamepad.FlxGamepadInputID;
|
||||||
import flixel.input.keyboard.FlxKey;
|
import flixel.input.keyboard.FlxKey;
|
||||||
|
import funkin.graphics.FunkinSprite;
|
||||||
import funkin.input.Controls;
|
import funkin.input.Controls;
|
||||||
import funkin.ui.AtlasText;
|
import funkin.ui.AtlasText;
|
||||||
import funkin.ui.MenuList;
|
import funkin.ui.MenuList;
|
||||||
|
@ -61,8 +62,8 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page
|
||||||
|
|
||||||
if (FlxG.gamepads.numActiveGamepads > 0)
|
if (FlxG.gamepads.numActiveGamepads > 0)
|
||||||
{
|
{
|
||||||
var devicesBg:FlxSprite = new FlxSprite();
|
var devicesBg:FunkinSprite = new FunkinSprite();
|
||||||
devicesBg.makeGraphic(FlxG.width, 100, 0xFFFAFD6D);
|
devicesBg.makeSolidColor(FlxG.width, 100, 0xFFFAFD6D);
|
||||||
add(devicesBg);
|
add(devicesBg);
|
||||||
deviceList = new TextMenuList(Horizontal, None);
|
deviceList = new TextMenuList(Horizontal, None);
|
||||||
add(deviceList);
|
add(deviceList);
|
||||||
|
|
|
@ -180,9 +180,9 @@ class Level implements IRegistryEntry<LevelData>
|
||||||
return difficulties;
|
return difficulties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildProps():Array<LevelProp>
|
public function buildProps(?existingProps:Array<LevelProp>):Array<LevelProp>
|
||||||
{
|
{
|
||||||
var props:Array<LevelProp> = [];
|
var props:Array<LevelProp> = existingProps == null ? [] : [for (x in existingProps) x];
|
||||||
|
|
||||||
if (_data.props.length == 0) return props;
|
if (_data.props.length == 0) return props;
|
||||||
|
|
||||||
|
@ -190,11 +190,22 @@ class Level implements IRegistryEntry<LevelData>
|
||||||
{
|
{
|
||||||
var propData = _data.props[propIndex];
|
var propData = _data.props[propIndex];
|
||||||
|
|
||||||
var propSprite:Null<LevelProp> = LevelProp.build(propData);
|
// Attempt to reuse the `LevelProp` object.
|
||||||
if (propSprite == null) continue;
|
// This prevents animations from resetting.
|
||||||
|
var existingProp:Null<LevelProp> = props[propIndex];
|
||||||
|
if (existingProp != null)
|
||||||
|
{
|
||||||
|
existingProp.propData = propData;
|
||||||
|
existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var propSprite:Null<LevelProp> = LevelProp.build(propData);
|
||||||
|
if (propSprite == null) continue;
|
||||||
|
|
||||||
propSprite.x += FlxG.width * 0.25 * propIndex;
|
propSprite.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
|
||||||
props.push(propSprite);
|
props.push(propSprite);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return props;
|
return props;
|
||||||
|
|
|
@ -6,9 +6,26 @@ import funkin.data.level.LevelData;
|
||||||
|
|
||||||
class LevelProp extends Bopper
|
class LevelProp extends Bopper
|
||||||
{
|
{
|
||||||
public function new(danceEvery:Int)
|
public var propData(default, set):Null<LevelPropData> = null;
|
||||||
|
|
||||||
|
function set_propData(value:LevelPropData):LevelPropData
|
||||||
{
|
{
|
||||||
super(danceEvery);
|
// Only reset the prop if the asset path has changed.
|
||||||
|
if (propData == null || value.assetPath != this.propData.assetPath)
|
||||||
|
{
|
||||||
|
this.visible = (value != null);
|
||||||
|
this.propData = value;
|
||||||
|
danceEvery = this.propData.danceEvery;
|
||||||
|
applyData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.propData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function new(propData:LevelPropData)
|
||||||
|
{
|
||||||
|
super(propData.danceEvery);
|
||||||
|
this.propData = propData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function playConfirm():Void
|
public function playConfirm():Void
|
||||||
|
@ -16,50 +33,51 @@ class LevelProp extends Bopper
|
||||||
playAnimation('confirm', true, true);
|
playAnimation('confirm', true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function build(propData:Null<LevelPropData>):Null<LevelProp>
|
function applyData():Void
|
||||||
{
|
{
|
||||||
if (propData == null) return null;
|
|
||||||
|
|
||||||
var isAnimated:Bool = propData.animations.length > 0;
|
var isAnimated:Bool = propData.animations.length > 0;
|
||||||
var prop:LevelProp = new LevelProp(propData.danceEvery);
|
|
||||||
|
|
||||||
if (isAnimated)
|
if (isAnimated)
|
||||||
{
|
{
|
||||||
// Initalize sprite frames.
|
// Initalize sprite frames.
|
||||||
// Sparrow atlas only LEL.
|
// Sparrow atlas only LEL.
|
||||||
prop.frames = Paths.getSparrowAtlas(propData.assetPath);
|
this.frames = Paths.getSparrowAtlas(propData.assetPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Initalize static sprite.
|
// Initalize static sprite.
|
||||||
prop.loadGraphic(Paths.image(propData.assetPath));
|
this.loadGraphic(Paths.image(propData.assetPath));
|
||||||
|
|
||||||
// Disables calls to update() for a performance boost.
|
// Disables calls to update() for a performance boost.
|
||||||
prop.active = false;
|
this.active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop.frames == null || prop.frames.numFrames == 0)
|
if (this.frames == null || this.frames.numFrames == 0)
|
||||||
{
|
{
|
||||||
trace('ERROR: Could not build texture for level prop (${propData.assetPath}).');
|
trace('ERROR: Could not build texture for level prop (${propData.assetPath}).');
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var scale:Float = propData.scale * (propData.isPixel ? 6 : 1);
|
var scale:Float = propData.scale * (propData.isPixel ? 6 : 1);
|
||||||
prop.scale.set(scale, scale);
|
this.scale.set(scale, scale);
|
||||||
prop.antialiasing = !propData.isPixel;
|
this.antialiasing = !propData.isPixel;
|
||||||
prop.alpha = propData.alpha;
|
this.alpha = propData.alpha;
|
||||||
prop.x = propData.offsets[0];
|
this.x = propData.offsets[0];
|
||||||
prop.y = propData.offsets[1];
|
this.y = propData.offsets[1];
|
||||||
|
|
||||||
FlxAnimationUtil.addAtlasAnimations(prop, propData.animations);
|
FlxAnimationUtil.addAtlasAnimations(this, propData.animations);
|
||||||
for (propAnim in propData.animations)
|
for (propAnim in propData.animations)
|
||||||
{
|
{
|
||||||
prop.setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
|
this.setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
prop.dance();
|
this.dance();
|
||||||
prop.animation.paused = true;
|
this.animation.paused = true;
|
||||||
|
}
|
||||||
|
|
||||||
return prop;
|
public static function build(propData:Null<LevelPropData>):Null<LevelProp>
|
||||||
|
{
|
||||||
|
if (propData == null) return null;
|
||||||
|
|
||||||
|
return new LevelProp(propData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import flixel.group.FlxGroup.FlxTypedGroup;
|
||||||
import flixel.text.FlxText;
|
import flixel.text.FlxText;
|
||||||
import flixel.addons.transition.FlxTransitionableState;
|
import flixel.addons.transition.FlxTransitionableState;
|
||||||
import flixel.tweens.FlxEase;
|
import flixel.tweens.FlxEase;
|
||||||
|
import funkin.graphics.FunkinSprite;
|
||||||
import funkin.ui.MusicBeatState;
|
import funkin.ui.MusicBeatState;
|
||||||
import flixel.tweens.FlxTween;
|
import flixel.tweens.FlxTween;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
|
@ -153,7 +154,7 @@ class StoryMenuState extends MusicBeatState
|
||||||
|
|
||||||
updateBackground();
|
updateBackground();
|
||||||
|
|
||||||
var black:FlxSprite = new FlxSprite(levelBackground.x, 0).makeGraphic(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK);
|
var black:FunkinSprite = new FunkinSprite(levelBackground.x, 0).makeSolidColor(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK);
|
||||||
black.zIndex = levelBackground.zIndex - 1;
|
black.zIndex = levelBackground.zIndex - 1;
|
||||||
add(black);
|
add(black);
|
||||||
|
|
||||||
|
@ -636,8 +637,7 @@ class StoryMenuState extends MusicBeatState
|
||||||
|
|
||||||
function updateProps():Void
|
function updateProps():Void
|
||||||
{
|
{
|
||||||
levelProps.clear();
|
for (prop in currentLevel.buildProps(levelProps.members))
|
||||||
for (prop in currentLevel.buildProps())
|
|
||||||
{
|
{
|
||||||
prop.zIndex = 1000;
|
prop.zIndex = 1000;
|
||||||
levelProps.add(prop);
|
levelProps.add(prop);
|
||||||
|
|
|
@ -15,7 +15,7 @@ class OutdatedSubState extends MusicBeatState
|
||||||
override function create()
|
override function create()
|
||||||
{
|
{
|
||||||
super.create();
|
super.create();
|
||||||
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
|
var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
|
||||||
add(bg);
|
add(bg);
|
||||||
var ver = "v" + Application.current.meta.get('version');
|
var ver = "v" + Application.current.meta.get('version');
|
||||||
var txt:FlxText = new FlxText(0, 0, FlxG.width,
|
var txt:FlxText = new FlxText(0, 0, FlxG.width,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import funkin.audio.visualize.SpectogramSprite;
|
||||||
import funkin.graphics.shaders.ColorSwap;
|
import funkin.graphics.shaders.ColorSwap;
|
||||||
import funkin.graphics.shaders.LeftMaskShader;
|
import funkin.graphics.shaders.LeftMaskShader;
|
||||||
import funkin.data.song.SongRegistry;
|
import funkin.data.song.SongRegistry;
|
||||||
|
import funkin.graphics.FunkinSprite;
|
||||||
import funkin.ui.MusicBeatState;
|
import funkin.ui.MusicBeatState;
|
||||||
import funkin.data.song.SongData.SongMusicData;
|
import funkin.data.song.SongData.SongMusicData;
|
||||||
import funkin.graphics.shaders.TitleOutline;
|
import funkin.graphics.shaders.TitleOutline;
|
||||||
|
@ -118,7 +119,8 @@ class TitleState extends MusicBeatState
|
||||||
|
|
||||||
persistentUpdate = true;
|
persistentUpdate = true;
|
||||||
|
|
||||||
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
|
var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
|
||||||
|
bg.screenCenter();
|
||||||
add(bg);
|
add(bg);
|
||||||
|
|
||||||
logoBl = new FlxSprite(-150, -100);
|
logoBl = new FlxSprite(-150, -100);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import funkin.play.song.Song.SongDifficulty;
|
||||||
import funkin.ui.mainmenu.MainMenuState;
|
import funkin.ui.mainmenu.MainMenuState;
|
||||||
import funkin.ui.MusicBeatState;
|
import funkin.ui.MusicBeatState;
|
||||||
import haxe.io.Path;
|
import haxe.io.Path;
|
||||||
|
import funkin.graphics.FunkinSprite;
|
||||||
import lime.app.Future;
|
import lime.app.Future;
|
||||||
import lime.app.Promise;
|
import lime.app.Promise;
|
||||||
import lime.utils.AssetLibrary;
|
import lime.utils.AssetLibrary;
|
||||||
|
@ -42,7 +43,7 @@ class LoadingState extends MusicBeatState
|
||||||
|
|
||||||
override function create():Void
|
override function create():Void
|
||||||
{
|
{
|
||||||
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, 0xFFcaff4d);
|
var bg:FlxSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d);
|
||||||
add(bg);
|
add(bg);
|
||||||
|
|
||||||
funkay = new FlxSprite();
|
funkay = new FlxSprite();
|
||||||
|
@ -53,7 +54,7 @@ class LoadingState extends MusicBeatState
|
||||||
funkay.scrollFactor.set();
|
funkay.scrollFactor.set();
|
||||||
funkay.screenCenter();
|
funkay.screenCenter();
|
||||||
|
|
||||||
loadBar = new FlxSprite(0, FlxG.height - 20).makeGraphic(FlxG.width, 10, 0xFFff16d2);
|
loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(FlxG.width, 10, 0xFFff16d2);
|
||||||
loadBar.screenCenter(X);
|
loadBar.screenCenter(X);
|
||||||
add(loadBar);
|
add(loadBar);
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ class Constants
|
||||||
public static final URL_KICKSTARTER:String = 'https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/';
|
public static final URL_KICKSTARTER:String = 'https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GIT REPO DATA
|
* REPOSITORY DATA
|
||||||
*/
|
*/
|
||||||
// ==============================
|
// ==============================
|
||||||
|
|
||||||
|
@ -86,6 +86,11 @@ class Constants
|
||||||
public static final GIT_HASH:String = funkin.util.macro.GitCommit.getGitCommitHash();
|
public static final GIT_HASH:String = funkin.util.macro.GitCommit.getGitCommitHash();
|
||||||
#end
|
#end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current library versions, as provided by hmm.
|
||||||
|
*/
|
||||||
|
public static final LIBRARY_VERSIONS:Array<String> = funkin.util.macro.HaxelibVersions.getLibraryVersions();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* COLORS
|
* COLORS
|
||||||
*/
|
*/
|
||||||
|
@ -358,6 +363,12 @@ class Constants
|
||||||
*/
|
*/
|
||||||
public static final SCORE_HOLD_BONUS_PER_SECOND:Float = 250.0;
|
public static final SCORE_HOLD_BONUS_PER_SECOND:Float = 250.0;
|
||||||
|
|
||||||
|
public static final JUDGEMENT_KILLER_COMBO_BREAK:Bool = false;
|
||||||
|
public static final JUDGEMENT_SICK_COMBO_BREAK:Bool = false;
|
||||||
|
public static final JUDGEMENT_GOOD_COMBO_BREAK:Bool = false;
|
||||||
|
public static final JUDGEMENT_BAD_COMBO_BREAK:Bool = true;
|
||||||
|
public static final JUDGEMENT_SHIT_COMBO_BREAK:Bool = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FILE EXTENSIONS
|
* FILE EXTENSIONS
|
||||||
*/
|
*/
|
||||||
|
|
17
source/funkin/util/HaxeUIUtil.hx
Normal file
17
source/funkin/util/HaxeUIUtil.hx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package funkin.util;
|
||||||
|
|
||||||
|
import haxe.ui.tooltips.ToolTipRegionOptions;
|
||||||
|
|
||||||
|
class HaxeUIUtil
|
||||||
|
{
|
||||||
|
public static function buildTooltip(text:String, ?left:Float, ?top:Float, ?width:Float, ?height:Float):ToolTipRegionOptions
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
tipData: {text: text},
|
||||||
|
left: left ?? 0.0,
|
||||||
|
top: top ?? 0.0,
|
||||||
|
width: width ?? 0.0,
|
||||||
|
height: height ?? 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -123,6 +123,17 @@ class CrashHandler
|
||||||
|
|
||||||
fullContents += '=====================\n';
|
fullContents += '=====================\n';
|
||||||
|
|
||||||
|
fullContents += 'Haxelibs: \n';
|
||||||
|
|
||||||
|
for (lib in Constants.LIBRARY_VERSIONS)
|
||||||
|
{
|
||||||
|
fullContents += '- ${lib}\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
fullContents += '\n';
|
||||||
|
|
||||||
|
fullContents += '=====================\n';
|
||||||
|
|
||||||
fullContents += '\n';
|
fullContents += '\n';
|
||||||
|
|
||||||
fullContents += message;
|
fullContents += message;
|
||||||
|
|
67
source/funkin/util/macro/HaxelibVersions.hx
Normal file
67
source/funkin/util/macro/HaxelibVersions.hx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package funkin.util.macro;
|
||||||
|
|
||||||
|
import haxe.io.Path;
|
||||||
|
|
||||||
|
class HaxelibVersions
|
||||||
|
{
|
||||||
|
public static macro function getLibraryVersions():haxe.macro.Expr.ExprOf<Array<String>>
|
||||||
|
{
|
||||||
|
#if !display
|
||||||
|
return macro $v{formatHmmData(readHmmData())};
|
||||||
|
#else
|
||||||
|
// `#if display` is used for code completion. In this case returning an
|
||||||
|
// empty string is good enough; We don't want to call functions on every hint.
|
||||||
|
var commitHash:Array<String> = [];
|
||||||
|
return macro $v{commitHash};
|
||||||
|
#end
|
||||||
|
}
|
||||||
|
|
||||||
|
#if (macro)
|
||||||
|
static function readHmmData():hmm.HmmConfig
|
||||||
|
{
|
||||||
|
return hmm.HmmConfig.HmmConfigs.readHmmJsonOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function formatHmmData(hmmData:hmm.HmmConfig):Array<String>
|
||||||
|
{
|
||||||
|
var result:Array<String> = [];
|
||||||
|
|
||||||
|
for (library in hmmData.dependencies)
|
||||||
|
{
|
||||||
|
switch (library)
|
||||||
|
{
|
||||||
|
case Haxelib(name, version):
|
||||||
|
result.push('${name} haxelib(${o(version)})');
|
||||||
|
case Git(name, url, ref, dir):
|
||||||
|
result.push('${name} git(${url}/${o(dir, '')}:${o(ref)})');
|
||||||
|
case Mercurial(name, url, ref, dir):
|
||||||
|
result.push('${name} mercurial(${url}/${o(dir, '')}:${o(ref)})');
|
||||||
|
case Dev(name, path):
|
||||||
|
result.push('${name} dev(${path})');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function o(option:haxe.ds.Option<String>, defaultValue:String = 'None'):String
|
||||||
|
{
|
||||||
|
switch (option)
|
||||||
|
{
|
||||||
|
case Some(value):
|
||||||
|
return value;
|
||||||
|
case None:
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static function readLibraryCurrentVersion(libraryName:String):String
|
||||||
|
{
|
||||||
|
var path = Path.join([Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']);
|
||||||
|
// This is compile time so we should always have Sys available.
|
||||||
|
var result = sys.io.File.getContent(path);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
}
|
|
@ -1,32 +1,42 @@
|
||||||
package funkin.util.tools;
|
package funkin.util.tools;
|
||||||
|
|
||||||
|
import haxe.Int64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://github.com/fponticelli/thx.core/blob/master/src/thx/Int64s.hx
|
* Why `haxe.Int64` doesn't have a built-in `toFloat` function is beyond me.
|
||||||
*/
|
*/
|
||||||
class Int64Tools
|
class Int64Tools
|
||||||
{
|
{
|
||||||
static var min = haxe.Int64.make(0x80000000, 0);
|
private inline static var MAX_32_PRECISION:Float = 4294967296.0;
|
||||||
static var one = haxe.Int64.make(0, 1);
|
|
||||||
static var two = haxe.Int64.ofInt(2);
|
|
||||||
static var zero = haxe.Int64.make(0, 0);
|
|
||||||
static var ten = haxe.Int64.ofInt(10);
|
|
||||||
|
|
||||||
public static function toFloat(i:haxe.Int64):Float
|
public static function fromFloat(f:Float):Int64
|
||||||
{
|
{
|
||||||
var isNegative = false;
|
var h = Std.int(f / MAX_32_PRECISION);
|
||||||
if (i < 0)
|
var l = Std.int(f);
|
||||||
|
return Int64.make(h, l);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function toFloat(i:Int64):Float
|
||||||
|
{
|
||||||
|
var f:Float = Int64.getLow(i);
|
||||||
|
if (f < 0) f += MAX_32_PRECISION;
|
||||||
|
return (Int64.getHigh(i) * MAX_32_PRECISION + f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isToIntSafe(i:Int64):Bool
|
||||||
|
{
|
||||||
|
return i.high != i.low >> 31;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function toIntSafe(i:Int64):Int
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (i < min) return -9223372036854775808.0; // most -ve value can't be made +ve
|
return Int64.toInt(i);
|
||||||
isNegative = true;
|
|
||||||
i = -i;
|
|
||||||
}
|
}
|
||||||
var multiplier = 1.0, ret = 0.0;
|
catch (e:Dynamic)
|
||||||
for (_ in 0...64)
|
|
||||||
{
|
{
|
||||||
if (haxe.Int64.and(i, one) != zero) ret += multiplier;
|
throw 'Could not represent value "${Int64.toStr(i)}" as an integer.';
|
||||||
multiplier *= 2.0;
|
|
||||||
i = haxe.Int64.shr(i, 1);
|
|
||||||
}
|
}
|
||||||
return (isNegative ? -1 : 1) * ret;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue