mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-03-20 17:09:21 +00:00
Merge branch 'rewrite/master' into rewrite/weekend-1-gameplay-shaderdev
This commit is contained in:
commit
eea8dde455
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:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: krdlab/setup-haxe@v1.5.1
|
||||
- uses: funkincrew/ci-haxe@v3
|
||||
with:
|
||||
haxe-version: 4.3.1
|
||||
haxe-version: 4.3.3
|
||||
- name: Config haxelib
|
||||
run: |
|
||||
haxelib config
|
||||
|
@ -19,7 +19,7 @@ runs:
|
|||
shell: bash
|
||||
- name: dependency install cache
|
||||
id: cache-hmm
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .haxelib
|
||||
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:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install butler Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default
|
||||
7z x butler.zip
|
||||
./butler -v
|
||||
shell: bash
|
||||
- name: Install butler Mac
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default
|
||||
unzip butler.zip
|
||||
./butler -V
|
||||
shell: bash
|
||||
- name: Install butler Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
|
||||
unzip butler.zip
|
||||
chmod +x butler
|
||||
./butler -V
|
||||
shell: bash
|
||||
- name: Upload game to itch.io
|
||||
env:
|
||||
BUTLER_API_KEY: ${{inputs.butler-key}}
|
||||
run: |
|
||||
./butler login
|
||||
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME}
|
||||
shell: bash
|
||||
- name: Install butler Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default
|
||||
7z x butler.zip
|
||||
./butler -v
|
||||
shell: bash
|
||||
- name: Install butler Mac
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default
|
||||
unzip butler.zip
|
||||
./butler -V
|
||||
shell: bash
|
||||
- name: Install butler Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
|
||||
unzip butler.zip
|
||||
chmod +x butler
|
||||
./butler -V
|
||||
shell: bash
|
||||
- name: Upload game to itch.io
|
||||
env:
|
||||
BUTLER_API_KEY: ${{inputs.butler-key}}
|
||||
run: |
|
||||
./butler login
|
||||
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME}
|
||||
shell: bash
|
||||
|
|
151
.github/workflows/build-shit.yml
vendored
151
.github/workflows/build-shit.yml
vendored
|
@ -4,59 +4,33 @@ on:
|
|||
push:
|
||||
|
||||
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:
|
||||
needs: check_date
|
||||
if: ${{ needs.check_date.outputs.should_run != 'false'}}
|
||||
runs-on: [self-hosted, linux]
|
||||
container: ubuntu:latest
|
||||
container: ubuntu:23.10
|
||||
steps:
|
||||
- name: prepare container
|
||||
run: |
|
||||
apt update
|
||||
apt install sudo git curl unzip -y
|
||||
echo $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:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_RO_PAT }}
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
- uses: ./.github/actions/setup-haxeshit
|
||||
- name: Build game
|
||||
- name: gather game dependencies
|
||||
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
|
||||
- name: build game
|
||||
run: |
|
||||
haxelib run lime build html5 -release --times
|
||||
ls
|
||||
- uses: ./.github/actions/upload-itch
|
||||
|
@ -65,32 +39,34 @@ jobs:
|
|||
build-dir: export/release/html5/bin
|
||||
target: html5
|
||||
create-nightly-win:
|
||||
needs: check_date
|
||||
if: ${{ needs.check_date.outputs.should_run != 'false'}}
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
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:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_RO_PAT }}
|
||||
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@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.haxelib
|
||||
export
|
||||
${{ runner.temp }}\hxcpp_cache
|
||||
key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
|
||||
- name: Build game
|
||||
- name: build game
|
||||
run: |
|
||||
haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
|
||||
dir
|
||||
|
@ -101,20 +77,81 @@ jobs:
|
|||
butler-key: ${{ secrets.BUTLER_API_KEY }}
|
||||
build-dir: export/release/windows/bin
|
||||
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:
|
||||
# needs: create-nightly-win
|
||||
# runs-on: windows-latest
|
||||
# permissions:
|
||||
# contents: write
|
||||
# actions: write
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: get token from gh app
|
||||
# uses: actions/create-github-app-token@v1
|
||||
# id: app_token
|
||||
# with:
|
||||
# submodules: 'recursive'
|
||||
# fetch-depth: 0
|
||||
# token: ${{ secrets.GH_RO_PAT }}
|
||||
# app-id: ${{ vars.APP_ID }}
|
||||
# private-key: ${{ secrets.APP_PEM }}
|
||||
# owner: ${{ github.repository_owner }}
|
||||
# - name: checkout repo
|
||||
# uses: funkincrew/ci-checkout@v6
|
||||
# with:
|
||||
# submodules: 'recursive'
|
||||
# token: ${{ steps.app_token.outputs.token }}
|
||||
# - name: 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
|
||||
# - name: Run unit tests
|
||||
# run: |
|
||||
# cd ./tests/unit/
|
||||
# ./start-win-native.bat
|
||||
# env:
|
||||
# HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
|
||||
|
|
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
|
@ -110,6 +110,11 @@
|
|||
"target": "windows",
|
||||
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Conversation Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DDIALOGUE"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Chart Editor)",
|
||||
"target": "windows",
|
||||
|
@ -125,6 +130,11 @@
|
|||
"target": "windows",
|
||||
"args": ["-debug", "-DLATENCY"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Waveform Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DWAVEFORM"]
|
||||
},
|
||||
{
|
||||
"label": "HTML5 / Debug",
|
||||
"target": "html5",
|
||||
|
|
14
Project.xml
14
Project.xml
|
@ -52,6 +52,7 @@
|
|||
<library name="week7" preload="false" />
|
||||
<library name="weekend1" preload="false" />
|
||||
</section>
|
||||
<library name="art" preload="false" />
|
||||
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg" if="web" />
|
||||
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3" unless="web" />
|
||||
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg" if="web" />
|
||||
|
@ -82,14 +83,15 @@
|
|||
If we can exclude the `mods` folder from the manifest, we can re-enable this line.
|
||||
<assets path='example_mods' rename='mods' embed='false' exclude="*.md" />
|
||||
-->
|
||||
<assets path="art/readme.txt" rename="do NOT readme.txt" />
|
||||
<assets path="CHANGELOG.md" rename="changelog.txt" />
|
||||
<assets path="art/readme.txt" rename="do NOT readme.txt" library="art"/>
|
||||
<assets path="CHANGELOG.md" rename="changelog.txt" library="art"/>
|
||||
<!-- NOTE FOR FUTURE SELF SINCE FONTS ARE ALWAYS FUCKY
|
||||
TO FIX ONE OF THEM, I CONVERTED IT TO OTF. DUNNO IF YOU NEED TO
|
||||
THEN UHHH I USED THE NAME OF THE FONT WITH SETFORMAT() ON THE TEXT!!!
|
||||
NOT USING A DIRECT THING TO THE ASSET!!!
|
||||
-->
|
||||
<assets path="assets/fonts" embed="true" />
|
||||
|
||||
<!-- _______________________________ Libraries ______________________________ -->
|
||||
<haxelib name="lime" /> <!-- Game engine backend -->
|
||||
<haxelib name="openfl" /> <!-- Game engine backend -->
|
||||
|
@ -108,9 +110,9 @@
|
|||
<haxelib name="hxCodec" /> <!-- Video playback -->
|
||||
|
||||
<haxelib name="json2object" /> <!-- JSON parsing -->
|
||||
<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
|
||||
<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 -->
|
||||
|
||||
<!--Disable the Flixel core focus lost screen-->
|
||||
|
@ -127,11 +129,11 @@
|
|||
<haxeflag name="-w" value="-WDeprecated" />
|
||||
|
||||
<!-- Haxe 4.3.0+: Enable pretty syntax errors and stuff. -->
|
||||
<haxedef name="message-reporting" value="pretty" />
|
||||
<haxedef name="message.reporting" value="pretty" />
|
||||
|
||||
<!-- _________________________________ Custom _______________________________ -->
|
||||
<!-- Disable trace() calls in release builds to bump up performance. -->
|
||||
<haxeflag name="--no-traces" unless="debug" />
|
||||
<!-- Disable trace() calls in release builds to bump up performance.
|
||||
<haxeflag name="- -no-traces" unless="debug" />-->
|
||||
<!-- HScript relies heavily on Reflection, which means we can't use DCE. -->
|
||||
<haxeflag name="-dce no" />
|
||||
<!-- Ensure all Funkin' classes are available at runtime. -->
|
||||
|
|
2
art
2
art
|
@ -1 +1 @@
|
|||
Subproject commit 1656bea5370c65879aaeb323e329f403c78071c5
|
||||
Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit 2cba3162990db807be054889b23cf214a1d8dd2d
|
||||
Subproject commit 6825d762bd007a50405a4e38939862f5ba70481f
|
21
hmm.json
21
hmm.json
|
@ -11,14 +11,14 @@
|
|||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "9bdea914f3d0485b9b3ec158f28875b5ac95d476",
|
||||
"ref": "07c6018008801972d12275690fc144fcc22e3de6",
|
||||
"url": "https://github.com/FunkinCrew/flixel"
|
||||
},
|
||||
{
|
||||
"name": "flixel-addons",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "fd3aecdeb5635fa0428dffee204fc78fc26b5885",
|
||||
"ref": "a523c3b56622f0640933944171efed46929e360e",
|
||||
"url": "https://github.com/FunkinCrew/flixel-addons"
|
||||
},
|
||||
{
|
||||
|
@ -37,7 +37,7 @@
|
|||
"name": "flxanimate",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d7c5621be742e2c98d523dfe5af7528835eaff1e",
|
||||
"ref": "9bacdd6ea39f5e3a33b0f5dfb7bc583fe76060d4",
|
||||
"url": "https://github.com/FunkinCrew/flxanimate"
|
||||
},
|
||||
{
|
||||
|
@ -54,14 +54,14 @@
|
|||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "7021f1fbab928268d9196a73e7f47461ca3c3e4d",
|
||||
"ref": "8a7846b",
|
||||
"url": "https://github.com/haxeui/haxeui-core"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "26b6bb132c92dfa9b77b4a61eaeda8f9a9efda98",
|
||||
"ref": "e9f880522e27134b29df4067f82df7d7e5237b70",
|
||||
"url": "https://github.com/haxeui/haxeui-flixel"
|
||||
},
|
||||
{
|
||||
|
@ -100,14 +100,14 @@
|
|||
"name": "json2object",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a0a78b60c41e47bae8bfa422488a199a58b4474e",
|
||||
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
|
||||
"url": "https://github.com/FunkinCrew/json2object"
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
|
||||
"ref": "fff39ba6fc64969cd51987ef7491d9345043dc5d",
|
||||
"url": "https://github.com/FunkinCrew/lime"
|
||||
},
|
||||
{
|
||||
|
@ -149,18 +149,13 @@
|
|||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "80d1d309803c1b111866524f9769325e3b8b0b1b",
|
||||
"ref": "6cec79e4f322fbb262170594ed67ab72b4714810",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
"name": "thx.semver",
|
||||
"type": "haxelib",
|
||||
"version": "0.2.2"
|
||||
},
|
||||
{
|
||||
"name": "tink_json",
|
||||
"type": "haxelib",
|
||||
"version": "0.11.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -112,5 +112,6 @@ class Main extends Sprite
|
|||
Toolkit.theme = 'dark'; // don't be cringe
|
||||
Toolkit.autoScale = false;
|
||||
funkin.input.Cursor.registerHaxeUICursors();
|
||||
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import funkin.data.song.SongDataUtils;
|
|||
* A core class which handles musical timing throughout the game,
|
||||
* both in gameplay and in menus.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Conductor
|
||||
{
|
||||
// onBeatHit is called every quarter note
|
||||
|
@ -28,29 +29,53 @@ class Conductor
|
|||
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
||||
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
||||
|
||||
/**
|
||||
* The current instance of the Conductor.
|
||||
* If one doesn't currently exist, a new one will be created.
|
||||
*
|
||||
* You can also do stuff like store a reference to the Conductor and pass it around or temporarily replace it,
|
||||
* or have a second Conductor running at the same time, or other weird stuff like that if you need to.
|
||||
*/
|
||||
public static var instance:Conductor = new Conductor();
|
||||
|
||||
/**
|
||||
* Signal fired when the current Conductor instance advances to a new measure.
|
||||
*/
|
||||
public static var measureHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the current Conductor instance advances to a new beat.
|
||||
*/
|
||||
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the current Conductor instance advances to a new step.
|
||||
*/
|
||||
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* The list of time changes in the song.
|
||||
* There should be at least one time change (at the beginning of the song) to define the BPM.
|
||||
*/
|
||||
static var timeChanges:Array<SongTimeChange> = [];
|
||||
var timeChanges:Array<SongTimeChange> = [];
|
||||
|
||||
/**
|
||||
* The most recent time change for the current song position.
|
||||
*/
|
||||
public static var currentTimeChange(default, null):SongTimeChange;
|
||||
public var currentTimeChange(default, null):Null<SongTimeChange>;
|
||||
|
||||
/**
|
||||
* The current position in the song in milliseconds.
|
||||
* Update this every frame based on the audio position using `Conductor.update()`.
|
||||
* Update this every frame based on the audio position using `Conductor.instance.update()`.
|
||||
*/
|
||||
public static var songPosition(default, null):Float = 0;
|
||||
public var songPosition(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* Beats per minute of the current song at the current time.
|
||||
*/
|
||||
public static var bpm(get, never):Float;
|
||||
public var bpm(get, never):Float;
|
||||
|
||||
static function get_bpm():Float
|
||||
function get_bpm():Float
|
||||
{
|
||||
if (bpmOverride != null) return bpmOverride;
|
||||
|
||||
|
@ -62,9 +87,9 @@ class Conductor
|
|||
/**
|
||||
* Beats per minute of the current song at the start time.
|
||||
*/
|
||||
public static var startingBPM(get, never):Float;
|
||||
public var startingBPM(get, never):Float;
|
||||
|
||||
static function get_startingBPM():Float
|
||||
function get_startingBPM():Float
|
||||
{
|
||||
if (bpmOverride != null) return bpmOverride;
|
||||
|
||||
|
@ -78,14 +103,14 @@ class Conductor
|
|||
* The current value set by `forceBPM`.
|
||||
* If false, BPM is determined by time changes.
|
||||
*/
|
||||
static var bpmOverride:Null<Float> = null;
|
||||
var bpmOverride:Null<Float> = null;
|
||||
|
||||
/**
|
||||
* Duration of a measure in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public static var measureLengthMs(get, never):Float;
|
||||
public var measureLengthMs(get, never):Float;
|
||||
|
||||
static function get_measureLengthMs():Float
|
||||
function get_measureLengthMs():Float
|
||||
{
|
||||
return beatLengthMs * timeSignatureNumerator;
|
||||
}
|
||||
|
@ -93,9 +118,9 @@ class Conductor
|
|||
/**
|
||||
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public static var beatLengthMs(get, never):Float;
|
||||
public var beatLengthMs(get, never):Float;
|
||||
|
||||
static function get_beatLengthMs():Float
|
||||
function get_beatLengthMs():Float
|
||||
{
|
||||
// Tied directly to BPM.
|
||||
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
|
||||
|
@ -104,25 +129,25 @@ class Conductor
|
|||
/**
|
||||
* Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public static var stepLengthMs(get, never):Float;
|
||||
public var stepLengthMs(get, never):Float;
|
||||
|
||||
static function get_stepLengthMs():Float
|
||||
function get_stepLengthMs():Float
|
||||
{
|
||||
return beatLengthMs / timeSignatureNumerator;
|
||||
}
|
||||
|
||||
public static var timeSignatureNumerator(get, never):Int;
|
||||
public var timeSignatureNumerator(get, never):Int;
|
||||
|
||||
static function get_timeSignatureNumerator():Int
|
||||
function get_timeSignatureNumerator():Int
|
||||
{
|
||||
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM;
|
||||
|
||||
return currentTimeChange.timeSignatureNum;
|
||||
}
|
||||
|
||||
public static var timeSignatureDenominator(get, never):Int;
|
||||
public var timeSignatureDenominator(get, never):Int;
|
||||
|
||||
static function get_timeSignatureDenominator():Int
|
||||
function get_timeSignatureDenominator():Int
|
||||
{
|
||||
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN;
|
||||
|
||||
|
@ -132,44 +157,44 @@ class Conductor
|
|||
/**
|
||||
* Current position in the song, in measures.
|
||||
*/
|
||||
public static var currentMeasure(default, null):Int;
|
||||
public var currentMeasure(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in beats.
|
||||
*/
|
||||
public static var currentBeat(default, null):Int;
|
||||
public var currentBeat(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in steps.
|
||||
*/
|
||||
public static var currentStep(default, null):Int;
|
||||
public var currentStep(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in measures and fractions of a measure.
|
||||
*/
|
||||
public static var currentMeasureTime(default, null):Float;
|
||||
public var currentMeasureTime(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in beats and fractions of a measure.
|
||||
*/
|
||||
public static var currentBeatTime(default, null):Float;
|
||||
public var currentBeatTime(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in steps and fractions of a step.
|
||||
*/
|
||||
public static var currentStepTime(default, null):Float;
|
||||
public var currentStepTime(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* An offset tied to the current chart file to compensate for a delay in the instrumental.
|
||||
*/
|
||||
public static var instrumentalOffset:Float = 0;
|
||||
public var instrumentalOffset:Float = 0;
|
||||
|
||||
/**
|
||||
* The instrumental offset, in terms of steps.
|
||||
*/
|
||||
public static var instrumentalOffsetSteps(get, never):Float;
|
||||
public var instrumentalOffsetSteps(get, never):Float;
|
||||
|
||||
static function get_instrumentalOffsetSteps():Float
|
||||
function get_instrumentalOffsetSteps():Float
|
||||
{
|
||||
var startingStepLengthMs:Float = ((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
|
||||
|
@ -179,19 +204,19 @@ class Conductor
|
|||
/**
|
||||
* An offset tied to the file format of the audio file being played.
|
||||
*/
|
||||
public static var formatOffset:Float = 0;
|
||||
public var formatOffset:Float = 0;
|
||||
|
||||
/**
|
||||
* An offset set by the user to compensate for input lag.
|
||||
*/
|
||||
public static var inputOffset:Float = 0;
|
||||
public var inputOffset:Float = 0;
|
||||
|
||||
/**
|
||||
* The number of beats in a measure. May be fractional depending on the time signature.
|
||||
*/
|
||||
public static var beatsPerMeasure(get, never):Float;
|
||||
public var beatsPerMeasure(get, never):Float;
|
||||
|
||||
static function get_beatsPerMeasure():Float
|
||||
function get_beatsPerMeasure():Float
|
||||
{
|
||||
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
|
||||
return stepsPerMeasure / Constants.STEPS_PER_BEAT;
|
||||
|
@ -201,30 +226,15 @@ class Conductor
|
|||
* The number of steps in a measure.
|
||||
* TODO: I don't think this can be fractional?
|
||||
*/
|
||||
public static var stepsPerMeasure(get, never):Int;
|
||||
public var stepsPerMeasure(get, never):Int;
|
||||
|
||||
static function get_stepsPerMeasure():Int
|
||||
function get_stepsPerMeasure():Int
|
||||
{
|
||||
// TODO: Is this always an integer?
|
||||
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal fired when the Conductor advances to a new measure.
|
||||
*/
|
||||
public static var measureHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the Conductor advances to a new beat.
|
||||
*/
|
||||
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the Conductor advances to a new step.
|
||||
*/
|
||||
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
function new() {}
|
||||
public function new() {}
|
||||
|
||||
/**
|
||||
* Forcibly defines the current BPM of the song.
|
||||
|
@ -235,7 +245,7 @@ class Conductor
|
|||
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
||||
* you should have a metadata file for it instead.
|
||||
*/
|
||||
public static function forceBPM(?bpm:Float = null)
|
||||
public function forceBPM(?bpm:Float = null)
|
||||
{
|
||||
if (bpm != null)
|
||||
{
|
||||
|
@ -246,7 +256,7 @@ class Conductor
|
|||
// trace('[CONDUCTOR] Resetting BPM to default');
|
||||
}
|
||||
|
||||
Conductor.bpmOverride = bpm;
|
||||
this.bpmOverride = bpm;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -256,29 +266,29 @@ class Conductor
|
|||
* @param songPosition The current position in the song in milliseconds.
|
||||
* Leave blank to use the FlxG.sound.music position.
|
||||
*/
|
||||
public static function update(?songPosition:Float)
|
||||
public function update(?songPos:Float)
|
||||
{
|
||||
if (songPosition == null)
|
||||
if (songPos == null)
|
||||
{
|
||||
// Take into account instrumental and file format song offsets.
|
||||
songPosition = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0;
|
||||
songPos = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0;
|
||||
}
|
||||
|
||||
var oldMeasure = currentMeasure;
|
||||
var oldBeat = currentBeat;
|
||||
var oldStep = currentStep;
|
||||
var oldMeasure = this.currentMeasure;
|
||||
var oldBeat = this.currentBeat;
|
||||
var oldStep = this.currentStep;
|
||||
|
||||
// Set the song position we are at (for purposes of calculating note positions, etc).
|
||||
Conductor.songPosition = songPosition;
|
||||
this.songPosition = songPos;
|
||||
|
||||
currentTimeChange = timeChanges[0];
|
||||
if (Conductor.songPosition > 0.0)
|
||||
if (this.songPosition > 0.0)
|
||||
{
|
||||
for (i in 0...timeChanges.length)
|
||||
{
|
||||
if (songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i];
|
||||
if (this.songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i];
|
||||
|
||||
if (songPosition < timeChanges[i].timeStamp) break;
|
||||
if (this.songPosition < timeChanges[i].timeStamp) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,45 +296,49 @@ class Conductor
|
|||
{
|
||||
trace('WARNING: Conductor is broken, timeChanges is empty.');
|
||||
}
|
||||
else if (currentTimeChange != null && Conductor.songPosition > 0.0)
|
||||
else if (currentTimeChange != null && this.songPosition > 0.0)
|
||||
{
|
||||
// roundDecimal prevents representing 8 as 7.9999999
|
||||
currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
|
||||
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
currentStep = Math.floor(currentStepTime);
|
||||
currentBeat = Math.floor(currentBeatTime);
|
||||
currentMeasure = Math.floor(currentMeasureTime);
|
||||
this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
|
||||
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
this.currentStep = Math.floor(currentStepTime);
|
||||
this.currentBeat = Math.floor(currentBeatTime);
|
||||
this.currentMeasure = Math.floor(currentMeasureTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
|
||||
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
currentStep = Math.floor(currentStepTime);
|
||||
currentBeat = Math.floor(currentBeatTime);
|
||||
currentMeasure = Math.floor(currentMeasureTime);
|
||||
this.currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
|
||||
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
this.currentStep = Math.floor(currentStepTime);
|
||||
this.currentBeat = Math.floor(currentBeatTime);
|
||||
this.currentMeasure = Math.floor(currentMeasureTime);
|
||||
}
|
||||
|
||||
// FlxSignals are really cool.
|
||||
if (currentStep != oldStep)
|
||||
// Only fire the signal if we are THE Conductor.
|
||||
if (this == Conductor.instance)
|
||||
{
|
||||
stepHit.dispatch();
|
||||
}
|
||||
// FlxSignals are really cool.
|
||||
if (currentStep != oldStep)
|
||||
{
|
||||
Conductor.stepHit.dispatch();
|
||||
}
|
||||
|
||||
if (currentBeat != oldBeat)
|
||||
{
|
||||
beatHit.dispatch();
|
||||
}
|
||||
if (currentBeat != oldBeat)
|
||||
{
|
||||
Conductor.beatHit.dispatch();
|
||||
}
|
||||
|
||||
if (currentMeasure != oldMeasure)
|
||||
{
|
||||
measureHit.dispatch();
|
||||
if (currentMeasure != oldMeasure)
|
||||
{
|
||||
Conductor.measureHit.dispatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
|
||||
public function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
|
||||
{
|
||||
timeChanges = [];
|
||||
|
||||
|
@ -338,24 +352,21 @@ class Conductor
|
|||
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
|
||||
if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0;
|
||||
|
||||
if (currentTimeChange.beatTime == null)
|
||||
if (currentTimeChange.timeStamp <= 0.0)
|
||||
{
|
||||
if (currentTimeChange.timeStamp <= 0.0)
|
||||
{
|
||||
currentTimeChange.beatTime = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calculate the beat time of this timestamp.
|
||||
currentTimeChange.beatTime = 0.0;
|
||||
currentTimeChange.beatTime = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calculate the beat time of this timestamp.
|
||||
currentTimeChange.beatTime = 0.0;
|
||||
|
||||
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
|
||||
{
|
||||
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
|
||||
currentTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
|
||||
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
|
||||
4);
|
||||
}
|
||||
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
|
||||
{
|
||||
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
|
||||
currentTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
|
||||
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
|
||||
4);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,13 +379,13 @@ class Conductor
|
|||
}
|
||||
|
||||
// Update currentStepTime
|
||||
Conductor.update(Conductor.songPosition);
|
||||
this.update(Conductor.instance.songPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in milliseconds, return a time in steps.
|
||||
*/
|
||||
public static function getTimeInSteps(ms:Float):Float
|
||||
public function getTimeInSteps(ms:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
|
@ -411,7 +422,7 @@ class Conductor
|
|||
/**
|
||||
* Given a time in steps and fractional steps, return a time in milliseconds.
|
||||
*/
|
||||
public static function getStepTimeInMs(stepTime:Float):Float
|
||||
public function getStepTimeInMs(stepTime:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
|
@ -447,7 +458,7 @@ class Conductor
|
|||
/**
|
||||
* Given a time in beats and fractional beats, return a time in milliseconds.
|
||||
*/
|
||||
public static function getBeatTimeInMs(beatTime:Float):Float
|
||||
public function getBeatTimeInMs(beatTime:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
|
@ -480,13 +491,20 @@ class Conductor
|
|||
}
|
||||
}
|
||||
|
||||
public static function watchQuick():Void
|
||||
{
|
||||
FlxG.watch.addQuick("songPosition", Conductor.instance.songPosition);
|
||||
FlxG.watch.addQuick("bpm", Conductor.instance.bpm);
|
||||
FlxG.watch.addQuick("currentMeasureTime", Conductor.instance.currentMeasureTime);
|
||||
FlxG.watch.addQuick("currentBeatTime", Conductor.instance.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentStepTime", Conductor.instance.currentStepTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the Conductor, replacing the current instance with a fresh one.
|
||||
*/
|
||||
public static function reset():Void
|
||||
{
|
||||
beatHit.removeAll();
|
||||
stepHit.removeAll();
|
||||
|
||||
mapTimeChanges([]);
|
||||
forceBPM(null);
|
||||
update(0);
|
||||
Conductor.instance = new Conductor();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ abstract Tallies(RawTallies)
|
|||
bad: 0,
|
||||
good: 0,
|
||||
sick: 0,
|
||||
killer: 0,
|
||||
totalNotes: 0,
|
||||
totalNotesHit: 0,
|
||||
maxCombo: 0,
|
||||
|
@ -43,7 +42,6 @@ typedef RawTallies =
|
|||
var bad:Int;
|
||||
var good:Int;
|
||||
var sick:Int;
|
||||
var killer:Int;
|
||||
var maxCombo:Int;
|
||||
var isNewHighscore:Bool;
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@ import funkin.play.PlayStatePlaylist;
|
|||
import openfl.display.BitmapData;
|
||||
import funkin.data.level.LevelRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
||||
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.data.dialogue.ConversationRegistry;
|
||||
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||
import funkin.data.dialogue.SpeakerRegistry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.ui.title.TitleState;
|
||||
|
@ -197,25 +197,40 @@ class InitState extends FlxState
|
|||
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
|
||||
#end
|
||||
|
||||
//
|
||||
// FLIXEL PLUGINS
|
||||
//
|
||||
funkin.util.plugins.EvacuateDebugPlugin.initialize();
|
||||
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
|
||||
funkin.util.plugins.WatchPlugin.initialize();
|
||||
|
||||
//
|
||||
// GAME DATA PARSING
|
||||
//
|
||||
|
||||
// NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
|
||||
// NOTE: Registries must be imported and not referenced with fully qualified names,
|
||||
// to ensure build macros work properly.
|
||||
trace('Parsing game data...');
|
||||
var perfStart = haxe.Timer.stamp();
|
||||
SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
|
||||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
SongEventParser.loadEventCache();
|
||||
ConversationDataParser.loadConversationCache();
|
||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||
SpeakerDataParser.loadSpeakerCache();
|
||||
StageDataParser.loadStageCache();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
ConversationRegistry.instance.loadEntries();
|
||||
DialogueBoxRegistry.instance.loadEntries();
|
||||
SpeakerRegistry.instance.loadEntries();
|
||||
StageRegistry.instance.loadEntries();
|
||||
|
||||
// TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers.
|
||||
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||
|
||||
ModuleHandler.buildModuleCallbacks();
|
||||
ModuleHandler.loadModuleCache();
|
||||
|
||||
ModuleHandler.callOnCreate();
|
||||
|
||||
var perfEnd = haxe.Timer.stamp();
|
||||
|
||||
trace('Parsing game data took ${Math.floor((perfEnd - perfStart) * 1000)}ms.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -233,8 +248,12 @@ class InitState extends FlxState
|
|||
startLevel(defineLevel(), defineDifficulty());
|
||||
#elseif FREEPLAY // -DFREEPLAY
|
||||
FlxG.switchState(new FreeplayState());
|
||||
#elseif DIALOGUE // -DDIALOGUE
|
||||
FlxG.switchState(new funkin.ui.debug.dialogue.ConversationDebugState());
|
||||
#elseif ANIMATE // -DANIMATE
|
||||
FlxG.switchState(new funkin.ui.debug.anim.FlxAnimateTest());
|
||||
#elseif WAVEFORM // -DWAVEFORM
|
||||
FlxG.switchState(new funkin.ui.debug.WaveformTestState());
|
||||
#elseif CHARTING // -DCHARTING
|
||||
FlxG.switchState(new funkin.ui.debug.charting.ChartEditorState());
|
||||
#elseif STAGEBUILD // -DSTAGEBUILD
|
||||
|
|
|
@ -7,6 +7,10 @@ import flash.utils.ByteArray;
|
|||
import flixel.sound.FlxSound;
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import funkin.util.tools.ICloneable;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import flixel.math.FlxMath;
|
||||
import openfl.Assets;
|
||||
#if (openfl >= "8.0.0")
|
||||
import openfl.utils.AssetType;
|
||||
|
@ -17,10 +21,38 @@ import openfl.utils.AssetType;
|
|||
* - Delayed playback via negative song position.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinSound extends FlxSound
|
||||
class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
||||
{
|
||||
static final MAX_VOLUME:Float = 2.0;
|
||||
|
||||
static var cache(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
|
||||
|
||||
public var muted(default, set):Bool = false;
|
||||
|
||||
function set_muted(value:Bool):Bool
|
||||
{
|
||||
if (value == muted) return value;
|
||||
muted = value;
|
||||
updateTransform();
|
||||
return value;
|
||||
}
|
||||
|
||||
override function set_volume(value:Float):Float
|
||||
{
|
||||
// Uncap the volume.
|
||||
fixMaxVolume();
|
||||
_volume = FlxMath.bound(value, 0.0, MAX_VOLUME);
|
||||
updateTransform();
|
||||
return _volume;
|
||||
}
|
||||
|
||||
public var paused(get, never):Bool;
|
||||
|
||||
function get_paused():Bool
|
||||
{
|
||||
return this._paused;
|
||||
}
|
||||
|
||||
public var isPlaying(get, never):Bool;
|
||||
|
||||
function get_isPlaying():Bool
|
||||
|
@ -28,6 +60,24 @@ class FunkinSound extends FlxSound
|
|||
return this.playing || this._shouldPlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waveform data for this sound.
|
||||
* This is lazily loaded, so it will be built the first time it is accessed.
|
||||
*/
|
||||
public var waveformData(get, never):WaveformData;
|
||||
|
||||
var _waveformData:Null<WaveformData> = null;
|
||||
|
||||
function get_waveformData():WaveformData
|
||||
{
|
||||
if (_waveformData == null)
|
||||
{
|
||||
_waveformData = WaveformDataParser.interpretFlxSound(this);
|
||||
if (_waveformData == null) throw 'Could not interpret waveform data!';
|
||||
}
|
||||
return _waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we in a state where the song should play but time is negative?
|
||||
*/
|
||||
|
@ -63,6 +113,30 @@ class FunkinSound extends FlxSound
|
|||
}
|
||||
}
|
||||
|
||||
public function togglePlayback():FunkinSound
|
||||
{
|
||||
if (playing)
|
||||
{
|
||||
pause();
|
||||
}
|
||||
else
|
||||
{
|
||||
resume();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
function fixMaxVolume():Void
|
||||
{
|
||||
#if lime_openal
|
||||
// This code is pretty fragile, it reaches through 5 layers of private access.
|
||||
@:privateAccess
|
||||
var handle = this?._channel?.__source?.__backend?.handle;
|
||||
if (handle == null) return;
|
||||
lime.media.openal.AL.sourcef(handle, lime.media.openal.AL.MAX_GAIN, MAX_VOLUME);
|
||||
#end
|
||||
}
|
||||
|
||||
public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound
|
||||
{
|
||||
if (!exists) return this;
|
||||
|
@ -107,6 +181,26 @@ class FunkinSound extends FlxSound
|
|||
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
|
||||
{
|
||||
if (this._time < 0)
|
||||
|
@ -120,6 +214,37 @@ class FunkinSound extends FlxSound
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call after adjusting the volume to update the sound channel's settings.
|
||||
*/
|
||||
@:allow(flixel.sound.FlxSoundGroup)
|
||||
override function updateTransform():Void
|
||||
{
|
||||
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end
|
||||
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
|
||||
|
||||
if (_channel != null) _channel.soundTransform = _transform;
|
||||
}
|
||||
|
||||
public function clone():FunkinSound
|
||||
{
|
||||
var sound:FunkinSound = new FunkinSound();
|
||||
|
||||
// Clone the sound by creating one with the same data buffer.
|
||||
// Reusing the `Sound` object directly causes issues with playback.
|
||||
@:privateAccess
|
||||
sound._sound = openfl.media.Sound.fromAudioBuffer(this._sound.__buffer);
|
||||
|
||||
// Call init to ensure the FlxSound is properly initialized.
|
||||
sound.init(this.looped, this.autoDestroy, this.onComplete);
|
||||
|
||||
// Oh yeah, the waveform data is the same too!
|
||||
@:privateAccess
|
||||
sound._waveformData = this._waveformData;
|
||||
|
||||
return sound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinSound` object.
|
||||
*
|
||||
|
|
|
@ -3,6 +3,7 @@ package funkin.audio;
|
|||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.sound.FlxSound;
|
||||
import funkin.audio.FunkinSound;
|
||||
import flixel.tweens.FlxTween;
|
||||
|
||||
/**
|
||||
* A group of FunkinSounds that are all synced together.
|
||||
|
@ -14,8 +15,12 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
|
||||
public var volume(get, set):Float;
|
||||
|
||||
public var muted(get, set):Bool;
|
||||
|
||||
public var pitch(get, set):Float;
|
||||
|
||||
public var playing(get, never):Bool;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
@ -122,6 +127,26 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade in all the sounds in the group.
|
||||
*/
|
||||
public function fadeIn(duration:Float, ?from:Float = 0.0, ?to:Float = 1.0, ?onComplete:FlxTween->Void):Void
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
sound.fadeIn(duration, from, to, onComplete);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade out all the sounds in the group.
|
||||
*/
|
||||
public function fadeOut(duration:Float, ?to:Float = 0.0, ?onComplete:FlxTween->Void):Void
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
sound.fadeOut(duration, to, onComplete);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all the sounds in the group.
|
||||
*/
|
||||
|
@ -132,6 +157,12 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
});
|
||||
}
|
||||
|
||||
public override function destroy()
|
||||
{
|
||||
stop();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all sounds from the group.
|
||||
*/
|
||||
|
@ -159,6 +190,13 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
return time;
|
||||
}
|
||||
|
||||
function get_playing():Bool
|
||||
{
|
||||
if (getFirstAlive != null) return getFirstAlive().playing;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
function get_volume():Float
|
||||
{
|
||||
if (getFirstAlive() != null) return getFirstAlive().volume;
|
||||
|
@ -176,6 +214,22 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
return volume;
|
||||
}
|
||||
|
||||
function get_muted():Bool
|
||||
{
|
||||
if (getFirstAlive() != null) return getFirstAlive().muted;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
function set_muted(muted:Bool):Bool
|
||||
{
|
||||
forEachAlive(function(snd:FunkinSound) {
|
||||
snd.muted = muted;
|
||||
});
|
||||
|
||||
return muted;
|
||||
}
|
||||
|
||||
function get_pitch():Float
|
||||
{
|
||||
#if FLX_PITCH
|
||||
|
|
|
@ -2,6 +2,8 @@ package funkin.audio;
|
|||
|
||||
import funkin.audio.FunkinSound;
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
|
||||
class VoicesGroup extends SoundGroup
|
||||
{
|
||||
|
@ -104,6 +106,50 @@ class VoicesGroup extends SoundGroup
|
|||
return opponentVolume = volume;
|
||||
}
|
||||
|
||||
public function getPlayerVoice(index:Int = 0):Null<FunkinSound>
|
||||
{
|
||||
return playerVoices.members[index];
|
||||
}
|
||||
|
||||
public function getOpponentVoice(index:Int = 0):Null<FunkinSound>
|
||||
{
|
||||
return opponentVoices.members[index];
|
||||
}
|
||||
|
||||
public function getPlayerVoiceWaveform():Null<WaveformData>
|
||||
{
|
||||
if (playerVoices.members.length == 0) return null;
|
||||
|
||||
return playerVoices.members[0].waveformData;
|
||||
}
|
||||
|
||||
public function getOpponentVoiceWaveform():Null<WaveformData>
|
||||
{
|
||||
if (opponentVoices.members.length == 0) return null;
|
||||
|
||||
return opponentVoices.members[0].waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The length of the player's vocal track, in milliseconds.
|
||||
*/
|
||||
public function getPlayerVoiceLength():Float
|
||||
{
|
||||
if (playerVoices.members.length == 0) return 0.0;
|
||||
|
||||
return playerVoices.members[0].length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The length of the opponent's vocal track, in milliseconds.
|
||||
*/
|
||||
public function getOpponentVoiceLength():Float
|
||||
{
|
||||
if (opponentVoices.members.length == 0) return 0.0;
|
||||
|
||||
return opponentVoices.members[0].length;
|
||||
}
|
||||
|
||||
public override function clear():Void
|
||||
{
|
||||
playerVoices.clear();
|
||||
|
|
|
@ -64,7 +64,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
|
|||
|
||||
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples));
|
||||
else
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, vis.numSamples));
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples));
|
||||
|
||||
var fftSamples:Array<Float> = [];
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ class PolygonSpectogram extends MeshRender
|
|||
coolPoint.x = (curAud.balanced * waveAmplitude);
|
||||
coolPoint.y = (i / funnyPixels * daHeight);
|
||||
|
||||
add_quad(prevPoint.x, prevPoint.y, prevPoint.x
|
||||
build_quad(prevPoint.x, prevPoint.y, prevPoint.x
|
||||
+ thickness, prevPoint.y, coolPoint.x, coolPoint.y, coolPoint.x
|
||||
+ thickness, coolPoint.y
|
||||
+ thickness);
|
||||
|
|
|
@ -8,8 +8,7 @@ class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
|
|||
{
|
||||
public var playerVis:PolygonSpectogram;
|
||||
public var opponentVis:PolygonSpectogram;
|
||||
|
||||
var instVis:PolygonSpectogram;
|
||||
public var instVis:PolygonSpectogram;
|
||||
|
||||
public function new()
|
||||
{
|
||||
|
@ -51,6 +50,43 @@ class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
|
|||
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.
|
||||
* @param vis The visualizer to add.
|
||||
|
|
|
@ -164,7 +164,7 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
|
|||
|
||||
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples));
|
||||
else
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
|
||||
var fftSamples:Array<Float> = [];
|
||||
var i = remappedShit;
|
||||
|
@ -235,15 +235,15 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
|
|||
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples));
|
||||
else
|
||||
{
|
||||
if (curTime == Conductor.songPosition)
|
||||
if (curTime == Conductor.instance.songPosition)
|
||||
{
|
||||
wavOptimiz = 3;
|
||||
return; // already did shit, so finishes function early
|
||||
}
|
||||
|
||||
curTime = Conductor.songPosition;
|
||||
curTime = Conductor.instance.songPosition;
|
||||
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
}
|
||||
|
||||
wavOptimiz = 8;
|
||||
|
|
336
source/funkin/audio/waveform/WaveformData.hx
Normal file
336
source/funkin/audio/waveform/WaveformData.hx
Normal file
|
@ -0,0 +1,336 @@
|
|||
package funkin.audio.waveform;
|
||||
|
||||
import funkin.util.MathUtil;
|
||||
|
||||
@:nullSafety
|
||||
class WaveformData
|
||||
{
|
||||
static final DEFAULT_VERSION:Int = 2;
|
||||
|
||||
/**
|
||||
* The version of the waveform data format.
|
||||
* @default `2` (-1 if not specified/invalid)
|
||||
*/
|
||||
public var version(default, null):Int = -1;
|
||||
|
||||
/**
|
||||
* The number of channels in the waveform.
|
||||
*/
|
||||
public var channels(default, null):Int = 1;
|
||||
|
||||
@:alias('sample_rate')
|
||||
public var sampleRate(default, null):Int = 44100;
|
||||
|
||||
/**
|
||||
* Number of input audio samples per output waveform data point.
|
||||
* At base zoom level this is number of samples per pixel.
|
||||
* Lower values can more accurately represent the waveform when zoomed in, but take more data.
|
||||
*/
|
||||
@:alias('samples_per_pixel')
|
||||
public var samplesPerPoint(default, null):Int = 256;
|
||||
|
||||
/**
|
||||
* Number of bits to use for each sample value. Valid values are `8` and `16`.
|
||||
*/
|
||||
public var bits(default, null):Int = 16;
|
||||
|
||||
/**
|
||||
* The length of the data array, in points.
|
||||
*/
|
||||
public var length(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Array of Int16 values representing the waveform.
|
||||
* TODO: Use an `openfl.Vector` for performance.
|
||||
*/
|
||||
public var data(default, null):Array<Int> = [];
|
||||
|
||||
@:jignored
|
||||
var channelData:Null<Array<WaveformDataChannel>> = null;
|
||||
|
||||
public function new(?version:Int, channels:Int, sampleRate:Int, samplesPerPoint:Int, bits:Int, length:Int, data:Array<Int>)
|
||||
{
|
||||
this.version = version ?? DEFAULT_VERSION;
|
||||
this.channels = channels;
|
||||
this.sampleRate = sampleRate;
|
||||
this.samplesPerPoint = samplesPerPoint;
|
||||
this.bits = bits;
|
||||
this.length = length;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
function buildChannelData():Array<WaveformDataChannel>
|
||||
{
|
||||
channelData = [];
|
||||
for (i in 0...channels)
|
||||
{
|
||||
channelData.push(new WaveformDataChannel(this, i));
|
||||
}
|
||||
return channelData;
|
||||
}
|
||||
|
||||
public function channel(index:Int)
|
||||
{
|
||||
return (channelData == null) ? buildChannelData()[index] : channelData[index];
|
||||
}
|
||||
|
||||
public function get(index:Int):Int
|
||||
{
|
||||
return data[index] ?? 0;
|
||||
}
|
||||
|
||||
public function set(index:Int, value:Int)
|
||||
{
|
||||
data[index] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum possible value for a waveform data point.
|
||||
* The minimum possible value is (-1 * maxSampleValue)
|
||||
*/
|
||||
public function maxSampleValue():Int
|
||||
{
|
||||
if (_maxSampleValue != 0) return _maxSampleValue;
|
||||
return _maxSampleValue = Std.int(Math.pow(2, bits));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the value because `Math.pow` is expensive and the value gets used a lot.
|
||||
*/
|
||||
@:jignored
|
||||
var _maxSampleValue:Int = 0;
|
||||
|
||||
/**
|
||||
* @return The length of the waveform in samples.
|
||||
*/
|
||||
public function lenSamples():Int
|
||||
{
|
||||
return length * samplesPerPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The length of the waveform in seconds.
|
||||
*/
|
||||
public function lenSeconds():Float
|
||||
{
|
||||
return inline lenSamples() / sampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the time in seconds, return the waveform data point index.
|
||||
*/
|
||||
public function secondsToIndex(seconds:Float):Int
|
||||
{
|
||||
return Std.int(seconds * inline pointsPerSecond());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a waveform data point index, return the time in seconds.
|
||||
*/
|
||||
public function indexToSeconds(index:Int):Float
|
||||
{
|
||||
return index / inline pointsPerSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of data points this waveform data provides per second of audio.
|
||||
*/
|
||||
public inline function pointsPerSecond():Float
|
||||
{
|
||||
return sampleRate / samplesPerPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the percentage progress through the waveform, return the waveform data point index.
|
||||
*/
|
||||
public function percentToIndex(percent:Float):Int
|
||||
{
|
||||
return Std.int(percent * length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a waveform data point index, return the percentage progress through the waveform.
|
||||
*/
|
||||
public function indexToPercent(index:Int):Float
|
||||
{
|
||||
return index / length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resample the waveform data to create a new WaveformData object matching the desired `samplesPerPoint` value.
|
||||
* This is useful for zooming in/out of the waveform in a performant manner.
|
||||
*
|
||||
* @param newSamplesPerPoint The new value for `samplesPerPoint`.
|
||||
*/
|
||||
public function resample(newSamplesPerPoint:Int):WaveformData
|
||||
{
|
||||
var result = this.clone();
|
||||
|
||||
var ratio = newSamplesPerPoint / samplesPerPoint;
|
||||
if (ratio == 1) return result;
|
||||
if (ratio < 1) trace('[WARNING] Downsampling will result in a low precision.');
|
||||
|
||||
var inputSampleCount = this.lenSamples();
|
||||
var outputSampleCount = Std.int(inputSampleCount * ratio);
|
||||
|
||||
var inputPointCount = this.length;
|
||||
var outputPointCount = Std.int(inputPointCount / ratio);
|
||||
var outputChannelCount = this.channels;
|
||||
|
||||
// TODO: Actually figure out the dumbass logic for this.
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WaveformData whose data represents the two waveforms overlayed.
|
||||
*/
|
||||
public function merge(that:WaveformData):WaveformData
|
||||
{
|
||||
var result = this.clone([]);
|
||||
|
||||
for (channelIndex in 0...this.channels)
|
||||
{
|
||||
var thisChannel = this.channel(channelIndex);
|
||||
var thatChannel = that.channel(channelIndex);
|
||||
var resultChannel = result.channel(channelIndex);
|
||||
|
||||
for (index in 0...this.length)
|
||||
{
|
||||
var thisMinSample = thisChannel.minSample(index);
|
||||
var thatMinSample = thatChannel.minSample(index);
|
||||
|
||||
var thisMaxSample = thisChannel.maxSample(index);
|
||||
var thatMaxSample = thatChannel.maxSample(index);
|
||||
|
||||
resultChannel.setMinSample(index, Std.int(Math.min(thisMinSample, thatMinSample)));
|
||||
resultChannel.setMaxSample(index, Std.int(Math.max(thisMaxSample, thatMaxSample)));
|
||||
}
|
||||
}
|
||||
|
||||
@:privateAccess
|
||||
result.length = this.length;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WaveformData whose parameters match the current object.
|
||||
*/
|
||||
public function clone(?newData:Array<Int> = null):WaveformData
|
||||
{
|
||||
if (newData == null)
|
||||
{
|
||||
newData = this.data.clone();
|
||||
}
|
||||
|
||||
var clone = new WaveformData(this.version, this.channels, this.sampleRate, this.samplesPerPoint, this.bits, newData.length, newData);
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
@:nullSafety
|
||||
class WaveformDataChannel
|
||||
{
|
||||
var parent:WaveformData;
|
||||
var channelId:Int;
|
||||
|
||||
public function new(parent:WaveformData, channelId:Int)
|
||||
{
|
||||
this.parent = parent;
|
||||
this.channelId = channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a given minimum point at an index.
|
||||
*/
|
||||
public function minSample(i:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2;
|
||||
return inline parent.get(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function minSampleMapped(i:Int)
|
||||
{
|
||||
return inline minSample(i) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum value within the range of samples.
|
||||
* NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead.
|
||||
*/
|
||||
public function minSampleRange(start:Int, end:Int)
|
||||
{
|
||||
var min = inline parent.maxSampleValue();
|
||||
for (i in start...end)
|
||||
{
|
||||
var sample = inline minSample(i);
|
||||
if (sample < min) min = sample;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function minSampleRangeMapped(start:Int, end:Int)
|
||||
{
|
||||
return inline minSampleRange(start, end) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a given maximum point at an index.
|
||||
*/
|
||||
public function maxSample(i:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
||||
return inline parent.get(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function maxSampleMapped(i:Int)
|
||||
{
|
||||
return inline maxSample(i) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum value within the range of samples.
|
||||
* NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead.
|
||||
*/
|
||||
public function maxSampleRange(start:Int, end:Int)
|
||||
{
|
||||
var max = -(inline parent.maxSampleValue());
|
||||
for (i in start...end)
|
||||
{
|
||||
var sample = inline maxSample(i);
|
||||
if (sample > max) max = sample;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function maxSampleRangeMapped(start:Int, end:Int)
|
||||
{
|
||||
return inline maxSampleRange(start, end) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
public function setMinSample(i:Int, value:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2;
|
||||
inline parent.set(offset, value);
|
||||
}
|
||||
|
||||
public function setMaxSample(i:Int, value:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
||||
inline parent.set(offset, value);
|
||||
}
|
||||
}
|
145
source/funkin/audio/waveform/WaveformDataParser.hx
Normal file
145
source/funkin/audio/waveform/WaveformDataParser.hx
Normal file
|
@ -0,0 +1,145 @@
|
|||
package funkin.audio.waveform;
|
||||
|
||||
class WaveformDataParser
|
||||
{
|
||||
static final INT16_MAX:Int = 32767;
|
||||
static final INT16_MIN:Int = -32768;
|
||||
|
||||
static final INT8_MAX:Int = 127;
|
||||
static final INT8_MIN:Int = -128;
|
||||
|
||||
public static function interpretFlxSound(sound:flixel.sound.FlxSound):Null<WaveformData>
|
||||
{
|
||||
if (sound == null) return null;
|
||||
|
||||
// Method 1. This only works if the sound has been played before.
|
||||
@:privateAccess
|
||||
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__source?.buffer;
|
||||
|
||||
if (soundBuffer == null)
|
||||
{
|
||||
// Method 2. This works if the sound has not been played before.
|
||||
@:privateAccess
|
||||
soundBuffer = sound?._sound?.__buffer;
|
||||
|
||||
if (soundBuffer == null)
|
||||
{
|
||||
trace('[WAVEFORM] Failed to interpret FlxSound: ${sound}');
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// trace('[WAVEFORM] Method 2 worked.');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// trace('[WAVEFORM] Method 1 worked.');
|
||||
}
|
||||
|
||||
return interpretAudioBuffer(soundBuffer);
|
||||
}
|
||||
|
||||
public static function interpretAudioBuffer(soundBuffer:lime.media.AudioBuffer):Null<WaveformData>
|
||||
{
|
||||
var sampleRate = soundBuffer.sampleRate;
|
||||
var channels = soundBuffer.channels;
|
||||
var bitsPerSample = soundBuffer.bitsPerSample;
|
||||
var samplesPerPoint:Int = 256; // I don't think we need to configure this.
|
||||
var pointsPerSecond:Float = sampleRate / samplesPerPoint; // 172 samples per second for most songs is plenty precise while still being performant..
|
||||
|
||||
// TODO: Make this work better on HTML5.
|
||||
var soundData:lime.utils.Int16Array = cast soundBuffer.data;
|
||||
|
||||
var soundDataRawLength:Int = soundData.length;
|
||||
var soundDataSampleCount:Int = Std.int(Math.ceil(soundDataRawLength / channels / (bitsPerSample == 16 ? 2 : 1)));
|
||||
var outputPointCount:Int = Std.int(Math.ceil(soundDataSampleCount / samplesPerPoint));
|
||||
|
||||
// trace('Interpreting audio buffer:');
|
||||
// trace(' sampleRate: ${sampleRate}');
|
||||
// trace(' channels: ${channels}');
|
||||
// trace(' bitsPerSample: ${bitsPerSample}');
|
||||
// trace(' samplesPerPoint: ${samplesPerPoint}');
|
||||
// trace(' pointsPerSecond: ${pointsPerSecond}');
|
||||
// trace(' soundDataRawLength: ${soundDataRawLength}');
|
||||
// trace(' soundDataSampleCount: ${soundDataSampleCount}');
|
||||
// trace(' soundDataRawLength/4: ${soundDataRawLength / 4}');
|
||||
// trace(' outputPointCount: ${outputPointCount}');
|
||||
|
||||
var minSampleValue:Int = bitsPerSample == 16 ? INT16_MIN : INT8_MIN;
|
||||
var maxSampleValue:Int = bitsPerSample == 16 ? INT16_MAX : INT8_MAX;
|
||||
|
||||
var outputData:Array<Int> = [];
|
||||
|
||||
var perfStart = haxe.Timer.stamp();
|
||||
|
||||
for (pointIndex in 0...outputPointCount)
|
||||
{
|
||||
// minChannel1, maxChannel1, minChannel2, maxChannel2, ...
|
||||
var values:Array<Int> = [];
|
||||
|
||||
for (i in 0...channels)
|
||||
{
|
||||
values.push(bitsPerSample == 16 ? INT16_MAX : INT8_MAX);
|
||||
values.push(bitsPerSample == 16 ? INT16_MIN : INT8_MIN);
|
||||
}
|
||||
|
||||
var rangeStart = pointIndex * samplesPerPoint;
|
||||
var rangeEnd = rangeStart + samplesPerPoint;
|
||||
if (rangeEnd > soundDataSampleCount) rangeEnd = soundDataSampleCount;
|
||||
|
||||
for (sampleIndex in rangeStart...rangeEnd)
|
||||
{
|
||||
for (channelIndex in 0...channels)
|
||||
{
|
||||
var sampleIndex:Int = sampleIndex * channels + channelIndex;
|
||||
var sampleValue = soundData[sampleIndex];
|
||||
|
||||
if (sampleValue < values[channelIndex * 2]) values[(channelIndex * 2)] = sampleValue;
|
||||
if (sampleValue > values[channelIndex * 2 + 1]) values[(channelIndex * 2) + 1] = sampleValue;
|
||||
}
|
||||
}
|
||||
|
||||
// We now have the min and max values for the range.
|
||||
for (value in values)
|
||||
outputData.push(value);
|
||||
}
|
||||
|
||||
var outputDataLength:Int = Std.int(outputData.length / channels / 2);
|
||||
var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData);
|
||||
|
||||
var perfEnd = haxe.Timer.stamp();
|
||||
trace('[WAVEFORM] Interpreted audio buffer in ${perfEnd - perfStart} seconds.');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static function parseWaveformData(path:String):Null<WaveformData>
|
||||
{
|
||||
var rawJson:String = openfl.Assets.getText(path).trim();
|
||||
return parseWaveformDataString(rawJson, path);
|
||||
}
|
||||
|
||||
public static function parseWaveformDataString(contents:String, ?fileName:String):Null<WaveformData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<WaveformData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
trace('[WAVEFORM] Parsing waveform data: ${contents}');
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
static function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
|
||||
{
|
||||
trace('[WAVEFORM] Failed to parse waveform data: ${id}');
|
||||
|
||||
for (error in errors)
|
||||
funkin.data.DataError.printError(error);
|
||||
}
|
||||
}
|
449
source/funkin/audio/waveform/WaveformSprite.hx
Normal file
449
source/funkin/audio/waveform/WaveformSprite.hx
Normal file
|
@ -0,0 +1,449 @@
|
|||
package funkin.audio.waveform;
|
||||
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import funkin.graphics.rendering.MeshRender;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
class WaveformSprite extends MeshRender
|
||||
{
|
||||
static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE;
|
||||
static final DEFAULT_DURATION:Float = 5.0;
|
||||
static final DEFAULT_ORIENTATION:WaveformOrientation = HORIZONTAL;
|
||||
static final DEFAULT_X:Float = 0.0;
|
||||
static final DEFAULT_Y:Float = 0.0;
|
||||
static final DEFAULT_WIDTH:Float = 100.0;
|
||||
static final DEFAULT_HEIGHT:Float = 100.0;
|
||||
|
||||
/**
|
||||
* Set this to true to tell the waveform to rebuild itself.
|
||||
* Do this any time the data or drawable area of the waveform changes.
|
||||
* This often (but not always) needs to be done every frame.
|
||||
*/
|
||||
var isWaveformDirty:Bool = true;
|
||||
|
||||
/**
|
||||
* If true, force the waveform to redraw every frame.
|
||||
* Useful if the waveform's clipRect is constantly changing.
|
||||
*/
|
||||
public var forceUpdate:Bool = false;
|
||||
|
||||
public var waveformData(default, set):Null<WaveformData>;
|
||||
|
||||
function set_waveformData(value:Null<WaveformData>):Null<WaveformData>
|
||||
{
|
||||
if (waveformData == value) return value;
|
||||
|
||||
waveformData = value;
|
||||
isWaveformDirty = true;
|
||||
return waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The color to render the waveform with.
|
||||
*/
|
||||
public var waveformColor(default, set):FlxColor;
|
||||
|
||||
function set_waveformColor(value:FlxColor):FlxColor
|
||||
{
|
||||
if (waveformColor == value) return value;
|
||||
|
||||
waveformColor = value;
|
||||
// We don't need to dirty the waveform geometry, just rebuild the texture.
|
||||
rebuildGraphic();
|
||||
return waveformColor;
|
||||
}
|
||||
|
||||
public var orientation(default, set):WaveformOrientation;
|
||||
|
||||
function set_orientation(value:WaveformOrientation):WaveformOrientation
|
||||
{
|
||||
if (orientation == value) return value;
|
||||
|
||||
orientation = value;
|
||||
isWaveformDirty = true;
|
||||
return orientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time, in seconds, at which the waveform starts.
|
||||
*/
|
||||
public var time(default, set):Float;
|
||||
|
||||
function set_time(value:Float)
|
||||
{
|
||||
if (time == value) return value;
|
||||
|
||||
time = value;
|
||||
isWaveformDirty = true;
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* The duration, in seconds, that the waveform represents.
|
||||
* The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents.
|
||||
*/
|
||||
public var duration(default, set):Float;
|
||||
|
||||
function set_duration(value:Float)
|
||||
{
|
||||
if (duration == value) return value;
|
||||
|
||||
duration = value;
|
||||
isWaveformDirty = true;
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the physical size of the waveform with `this.height = value`.
|
||||
*/
|
||||
override function set_height(value:Float):Float
|
||||
{
|
||||
if (height == value) return super.set_height(value);
|
||||
|
||||
isWaveformDirty = true;
|
||||
return super.set_height(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the physical size of the waveform with `this.width = value`.
|
||||
*/
|
||||
override function set_width(value:Float):Float
|
||||
{
|
||||
if (width == value) return super.set_width(value);
|
||||
|
||||
isWaveformDirty = true;
|
||||
return super.set_width(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum size, in pixels, that a waveform will display with.
|
||||
* Useful for preventing the waveform from becoming too small to see.
|
||||
*
|
||||
* NOTE: This is technically doubled since it's applied above and below the center of the waveform.
|
||||
*/
|
||||
public var minWaveformSize:Int = 1;
|
||||
|
||||
/**
|
||||
* A multiplier on the size of the waveform.
|
||||
* Still capped at the width and height set for the sprite.
|
||||
*/
|
||||
public var amplitude:Float = 1.0;
|
||||
|
||||
public function new(?waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
super(DEFAULT_X, DEFAULT_Y, DEFAULT_COLOR);
|
||||
this.waveformColor = color ?? DEFAULT_COLOR;
|
||||
this.width = DEFAULT_WIDTH;
|
||||
this.height = DEFAULT_HEIGHT;
|
||||
|
||||
this.waveformData = waveformData;
|
||||
this.orientation = orientation ?? DEFAULT_ORIENTATION;
|
||||
this.time = 0.0;
|
||||
this.duration = duration ?? DEFAULT_DURATION;
|
||||
|
||||
this.forceUpdate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually tell the waveform to rebuild itself, even if none of its properties have changed.
|
||||
*/
|
||||
public function markDirty():Void
|
||||
{
|
||||
isWaveformDirty = true;
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (forceUpdate || isWaveformDirty)
|
||||
{
|
||||
// Recalculate the waveform vertices.
|
||||
drawWaveform();
|
||||
isWaveformDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildGraphic():Void
|
||||
{
|
||||
// The waveform is rendered using a single colored pixel as a texture.
|
||||
// If you want something more elaborate, make sure to modify `build_vertex` below to use the UVs you want.
|
||||
makeGraphic(1, 1, this.waveformColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param offsetX Horizontal offset to draw the waveform at, in samples.
|
||||
*/
|
||||
function drawWaveform():Void
|
||||
{
|
||||
// For each sample in the waveform...
|
||||
// Add a MAX vertex and a MIN vertex.
|
||||
// If previous MAX/MIN is empty, store.
|
||||
// If previous MAX/MIN is not empty, draw a quad using current and previous MAX/MIN. Then store current MAX/MIN.
|
||||
// Continue until end of waveform.
|
||||
|
||||
this.clear();
|
||||
|
||||
if (waveformData == null) return;
|
||||
|
||||
// Center point of the waveform. When horizontal this is half the height, when vertical this is half the width.
|
||||
var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2);
|
||||
|
||||
var oneSecondInIndices:Int = waveformData.secondsToIndex(1);
|
||||
|
||||
var startTime:Float = time;
|
||||
var endTime:Float = time + duration;
|
||||
|
||||
var startIndex:Int = waveformData.secondsToIndex(startTime);
|
||||
var endIndex:Int = waveformData.secondsToIndex(endTime);
|
||||
|
||||
var pixelsPerIndex:Float = (orientation == HORIZONTAL ? this.width : this.height) / (endIndex - startIndex);
|
||||
var indexesPerPixel:Float = 1 / pixelsPerIndex;
|
||||
|
||||
var topLeftVertexIndex:Int = -1;
|
||||
var topRightVertexIndex:Int = -1;
|
||||
var bottomLeftVertexIndex:Int = -1;
|
||||
var bottomRightVertexIndex:Int = -1;
|
||||
|
||||
if (clipRect != null)
|
||||
{
|
||||
topLeftVertexIndex = this.build_vertex(clipRect.x, clipRect.y);
|
||||
topRightVertexIndex = this.build_vertex(clipRect.x + clipRect.width, clipRect.y);
|
||||
bottomLeftVertexIndex = this.build_vertex(clipRect.x, clipRect.y + clipRect.height);
|
||||
bottomRightVertexIndex = this.build_vertex(clipRect.x + clipRect.width, clipRect.y + clipRect.height);
|
||||
}
|
||||
|
||||
if (pixelsPerIndex >= 1.0)
|
||||
{
|
||||
// Each index is at least one pixel wide, so we render each index.
|
||||
var prevVertexTopIndex:Int = -1;
|
||||
var prevVertexBottomIndex:Int = -1;
|
||||
for (i in startIndex...endIndex)
|
||||
{
|
||||
var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex);
|
||||
|
||||
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
|
||||
|
||||
if (isBeforeClipRect) continue;
|
||||
|
||||
var isAfterClipRect:Bool = (clipRect != null)
|
||||
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
|
||||
|
||||
if (isAfterClipRect)
|
||||
{
|
||||
break;
|
||||
};
|
||||
|
||||
var sampleMax:Float = Math.min(waveformData.channel(0).maxSampleMapped(i) * amplitude, 1.0);
|
||||
var sampleMin:Float = Math.max(waveformData.channel(0).minSampleMapped(i) * amplitude, -1.0);
|
||||
var sampleMaxSize:Float = sampleMax * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMaxSize < minWaveformSize) sampleMaxSize = minWaveformSize;
|
||||
var sampleMinSize:Float = sampleMin * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMinSize > -minWaveformSize) sampleMinSize = -minWaveformSize;
|
||||
var vertexTopY:Int = Std.int(waveformCenterPos - sampleMaxSize);
|
||||
var vertexBottomY:Int = Std.int(waveformCenterPos - sampleMinSize);
|
||||
|
||||
if (vertexBottomY - vertexTopY < minWaveformSize) vertexTopY = vertexBottomY - minWaveformSize;
|
||||
|
||||
var vertexTopIndex:Int = -1;
|
||||
var vertexBottomIndex:Int = -1;
|
||||
|
||||
if (clipRect != null)
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(pixelPos, vertexTopY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(pixelPos, vertexBottomY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(vertexTopY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(vertexBottomY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(pixelPos, vertexTopY);
|
||||
vertexBottomIndex = this.build_vertex(pixelPos, vertexBottomY);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(vertexTopY, pixelPos);
|
||||
vertexBottomIndex = this.build_vertex(vertexBottomY, pixelPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't render if we don't have a previous different set of vertices to create a quad from.
|
||||
if (prevVertexTopIndex != -1
|
||||
&& prevVertexBottomIndex != -1
|
||||
&& prevVertexTopIndex != vertexTopIndex
|
||||
&& prevVertexBottomIndex != vertexBottomIndex)
|
||||
{
|
||||
switch (orientation) // the line of code that makes you gay
|
||||
{
|
||||
case HORIZONTAL:
|
||||
this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex);
|
||||
case VERTICAL:
|
||||
this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex);
|
||||
}
|
||||
}
|
||||
|
||||
prevVertexTopIndex = vertexTopIndex;
|
||||
prevVertexBottomIndex = vertexBottomIndex;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Indexes are less than one pixel wide, so for each pixel we render the maximum of the samples that fall within it.
|
||||
var prevVertexTopIndex:Int = -1;
|
||||
var prevVertexBottomIndex:Int = -1;
|
||||
var waveformLengthPixels:Int = orientation == HORIZONTAL ? Std.int(this.width) : Std.int(this.height);
|
||||
for (i in 0...waveformLengthPixels)
|
||||
{
|
||||
var pixelPos:Int = i;
|
||||
|
||||
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
|
||||
|
||||
if (isBeforeClipRect) continue;
|
||||
|
||||
var isAfterClipRect:Bool = (clipRect != null)
|
||||
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
|
||||
|
||||
if (isAfterClipRect)
|
||||
{
|
||||
break;
|
||||
};
|
||||
|
||||
// Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming.
|
||||
var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex);
|
||||
var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex);
|
||||
|
||||
var sampleMax:Float = Math.min(waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * amplitude, 1.0);
|
||||
var sampleMin:Float = Math.max(waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * amplitude, -1.0);
|
||||
var sampleMaxSize:Float = sampleMax * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMaxSize < minWaveformSize) sampleMaxSize = minWaveformSize;
|
||||
var sampleMinSize:Float = sampleMin * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMinSize > -minWaveformSize) sampleMinSize = -minWaveformSize;
|
||||
var vertexTopY:Int = Std.int(waveformCenterPos - sampleMaxSize);
|
||||
var vertexBottomY:Int = Std.int(waveformCenterPos - sampleMinSize);
|
||||
|
||||
var vertexTopIndex:Int = -1;
|
||||
var vertexBottomIndex:Int = -1;
|
||||
|
||||
if (clipRect != null)
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(pixelPos, vertexTopY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(pixelPos, vertexBottomY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(vertexTopY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(vertexBottomY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(pixelPos, vertexTopY);
|
||||
vertexBottomIndex = this.build_vertex(pixelPos, vertexBottomY);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(vertexTopY, pixelPos);
|
||||
vertexBottomIndex = this.build_vertex(vertexBottomY, pixelPos);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1)
|
||||
{
|
||||
switch (orientation)
|
||||
{
|
||||
case HORIZONTAL:
|
||||
this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex);
|
||||
case VERTICAL:
|
||||
this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex);
|
||||
}
|
||||
}
|
||||
prevVertexTopIndex = vertexTopIndex;
|
||||
prevVertexBottomIndex = vertexBottomIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildClippedVertex(x:Int, y:Int, topLeftVertexIndex:Int, topRightVertexIndex:Int, bottomLeftVertexIndex:Int, bottomRightVertexIndex:Int):Int
|
||||
{
|
||||
var shouldClipXLeft = x < clipRect.x;
|
||||
var shouldClipXRight = x > (clipRect.x + clipRect.width);
|
||||
var shouldClipYTop = y < clipRect.y;
|
||||
var shouldClipYBottom = y > (clipRect.y + clipRect.height);
|
||||
|
||||
// If the vertex is fully outside the clipRect, use a pre-existing vertex.
|
||||
// Else, if the vertex is outside the clipRect on one axis, create a new vertex constrained on that axis.
|
||||
// Else, create a whole new vertex.
|
||||
if (shouldClipXLeft && shouldClipYTop)
|
||||
{
|
||||
return topLeftVertexIndex;
|
||||
}
|
||||
else if (shouldClipXRight && shouldClipYTop)
|
||||
{
|
||||
return topRightVertexIndex;
|
||||
}
|
||||
else if (shouldClipXLeft && shouldClipYBottom)
|
||||
{
|
||||
return bottomLeftVertexIndex;
|
||||
}
|
||||
else if (shouldClipXRight && shouldClipYBottom)
|
||||
{
|
||||
return bottomRightVertexIndex;
|
||||
}
|
||||
else if (shouldClipXLeft)
|
||||
{
|
||||
return this.build_vertex(clipRect.x, y);
|
||||
}
|
||||
else if (shouldClipXRight)
|
||||
{
|
||||
return this.build_vertex(clipRect.x + clipRect.width, y);
|
||||
}
|
||||
else if (shouldClipYTop)
|
||||
{
|
||||
return this.build_vertex(x, clipRect.y);
|
||||
}
|
||||
else if (shouldClipYBottom)
|
||||
{
|
||||
return this.build_vertex(x, clipRect.y + clipRect.height);
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.build_vertex(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
return new WaveformSprite(data, orientation, color, duration);
|
||||
}
|
||||
|
||||
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
// TODO: Build waveform data from FunkinSound.
|
||||
var data = null;
|
||||
|
||||
return buildFromWaveformData(data, orientation, color, duration);
|
||||
}
|
||||
}
|
||||
|
||||
enum WaveformOrientation
|
||||
{
|
||||
HORIZONTAL;
|
||||
VERTICAL;
|
||||
}
|
|
@ -46,6 +46,9 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
this.entries = new Map<String, T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Create a `loadEntriesAsync()` function.
|
||||
*/
|
||||
public function loadEntries():Void
|
||||
{
|
||||
clearEntries();
|
||||
|
@ -54,7 +57,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
// SCRIPTED ENTRIES
|
||||
//
|
||||
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
|
||||
log('Registering ${scriptedEntryClassNames.length} scripted entries...');
|
||||
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
|
||||
|
||||
for (entryCls in scriptedEntryClassNames)
|
||||
{
|
||||
|
@ -78,7 +81,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
|
||||
return !entries.exists(entryId);
|
||||
});
|
||||
log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
|
||||
log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
|
||||
for (entryId in unscriptedEntryIds)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -120,6 +120,71 @@ class DataParse
|
|||
}
|
||||
}
|
||||
|
||||
public static function backdropData(json:Json, name:String):funkin.data.dialogue.ConversationData.BackdropData
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
var result:Dynamic = {};
|
||||
var backdropType:String = '';
|
||||
|
||||
for (field in fields)
|
||||
{
|
||||
switch (field.name)
|
||||
{
|
||||
case 'type':
|
||||
backdropType = Tools.getValue(field.value);
|
||||
}
|
||||
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||
}
|
||||
|
||||
switch (backdropType)
|
||||
{
|
||||
case 'solid':
|
||||
return SOLID(result);
|
||||
default:
|
||||
throw 'Expected Backdrop property $name to be specify a valid "type", but it was "${backdropType}".';
|
||||
}
|
||||
|
||||
return null;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function outroData(json:Json, name:String):Null<funkin.data.dialogue.ConversationData.OutroData>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
var result:Dynamic = {};
|
||||
var outroType:String = '';
|
||||
|
||||
for (field in fields)
|
||||
{
|
||||
switch (field.name)
|
||||
{
|
||||
case 'type':
|
||||
outroType = Tools.getValue(field.value);
|
||||
}
|
||||
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||
}
|
||||
|
||||
switch (outroType)
|
||||
{
|
||||
case 'none':
|
||||
return NONE(result);
|
||||
case 'fade':
|
||||
return FADE(result);
|
||||
default:
|
||||
throw 'Expected Outro property $name to be specify a valid "type", but it was "${outroType}".';
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a `Either<Float, LegacyScrollSpeeds>`.
|
||||
* Used by the FNF legacy JSON importer.
|
||||
|
@ -178,7 +243,31 @@ class DataParse
|
|||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
return cast Tools.getValue(json);
|
||||
var result:LegacyNoteSection =
|
||||
{
|
||||
mustHitSection: false,
|
||||
sectionNotes: [],
|
||||
};
|
||||
for (field in fields)
|
||||
{
|
||||
switch (field.name)
|
||||
{
|
||||
case 'sectionNotes':
|
||||
result.sectionNotes = legacyNotes(field.value, field.name);
|
||||
|
||||
case 'mustHitSection':
|
||||
result.mustHitSection = Tools.getValue(field.value);
|
||||
case 'typeOfSection':
|
||||
result.typeOfSection = Tools.getValue(field.value);
|
||||
case 'lengthInSteps':
|
||||
result.lengthInSteps = Tools.getValue(field.value);
|
||||
case 'changeBPM':
|
||||
result.changeBPM = Tools.getValue(field.value);
|
||||
case 'bpm':
|
||||
result.bpm = Tools.getValue(field.value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
|
@ -189,7 +278,12 @@ class DataParse
|
|||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
return cast Tools.getValue(json);
|
||||
var result = {};
|
||||
for (field in fields)
|
||||
{
|
||||
Reflect.setField(result, field.name, legacyNoteSectionArray(field.value, field.name));
|
||||
}
|
||||
return result;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
|
@ -211,13 +305,13 @@ class DataParse
|
|||
switch (json.value)
|
||||
{
|
||||
case JArray(values):
|
||||
// var time:Null<Float> = values[0] == null ? null : Tools.getValue(values[0]);
|
||||
// var data:Null<Int> = values[1] == null ? null : Tools.getValue(values[1]);
|
||||
// var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
|
||||
// var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
|
||||
var time:Null<Float> = values[0] == null ? null : Tools.getValue(values[0]);
|
||||
var data:Null<Int> = values[1] == null ? null : Tools.getValue(values[1]);
|
||||
var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
|
||||
var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
|
||||
|
||||
// return new LegacyNote(time, data, length, alt);
|
||||
return null;
|
||||
return new LegacyNote(time, data, length, alt);
|
||||
// return null;
|
||||
default:
|
||||
throw 'Expected property $name to be a note, but it was ${json.value}.';
|
||||
}
|
||||
|
|
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
168
source/funkin/data/dialogue/ConversationData.hx
Normal file
168
source/funkin/data/dialogue/ConversationData.hx
Normal file
|
@ -0,0 +1,168 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
/**
|
||||
* A type definition for the data for a specific conversation.
|
||||
* It includes things like what dialogue boxes to use, what text to display, and what animations to play.
|
||||
* @see https://lib.haxe.org/p/json2object/
|
||||
*/
|
||||
typedef ConversationData =
|
||||
{
|
||||
/**
|
||||
* Semantic version for conversation data.
|
||||
*/
|
||||
public var version:String;
|
||||
|
||||
/**
|
||||
* Data on the backdrop for the conversation.
|
||||
*/
|
||||
@:jcustomparse(funkin.data.DataParse.backdropData)
|
||||
public var backdrop:BackdropData;
|
||||
|
||||
/**
|
||||
* Data on the outro for the conversation.
|
||||
*/
|
||||
@:jcustomparse(funkin.data.DataParse.outroData)
|
||||
@:optional
|
||||
public var outro:Null<OutroData>;
|
||||
|
||||
/**
|
||||
* Data on the music for the conversation.
|
||||
*/
|
||||
@:optional
|
||||
public var music:Null<MusicData>;
|
||||
|
||||
/**
|
||||
* Data for each line of dialogue in the conversation.
|
||||
*/
|
||||
public var dialogue:Array<DialogueEntryData>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data on the backdrop for the conversation, behind the dialogue box.
|
||||
* A custom parser distinguishes between backdrop types based on the `type` field.
|
||||
*/
|
||||
enum BackdropData
|
||||
{
|
||||
SOLID(data:BackdropData_Solid); // 'solid'
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for a Solid color backdrop.
|
||||
*/
|
||||
typedef BackdropData_Solid =
|
||||
{
|
||||
/**
|
||||
* Used to distinguish between backdrop types. Should always be `solid` for this type.
|
||||
*/
|
||||
var type:String;
|
||||
|
||||
/**
|
||||
* The color of the backdrop.
|
||||
*/
|
||||
var color:String;
|
||||
|
||||
/**
|
||||
* Fade-in time for the backdrop.
|
||||
* @default No fade-in
|
||||
*/
|
||||
@:optional
|
||||
@:default(0.0)
|
||||
var fadeTime:Float;
|
||||
};
|
||||
|
||||
enum OutroData
|
||||
{
|
||||
NONE(data:OutroData_None); // 'none'
|
||||
FADE(data:OutroData_Fade); // 'fade'
|
||||
}
|
||||
|
||||
typedef OutroData_None =
|
||||
{
|
||||
/**
|
||||
* Used to distinguish between outro types. Should always be `none` for this type.
|
||||
*/
|
||||
var type:String;
|
||||
}
|
||||
|
||||
typedef OutroData_Fade =
|
||||
{
|
||||
/**
|
||||
* Used to distinguish between outro types. Should always be `fade` for this type.
|
||||
*/
|
||||
var type:String;
|
||||
|
||||
/**
|
||||
* The time to fade out the conversation.
|
||||
* @default 1 second
|
||||
*/
|
||||
@:optional
|
||||
@:default(1.0)
|
||||
var fadeTime:Float;
|
||||
}
|
||||
|
||||
typedef MusicData =
|
||||
{
|
||||
/**
|
||||
* The asset to play for the music.
|
||||
*/
|
||||
var asset:String;
|
||||
|
||||
/**
|
||||
* The time to fade in the music.
|
||||
*/
|
||||
@:optional
|
||||
@:default(0.0)
|
||||
var fadeTime:Float;
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
var looped:Bool;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data on a single line of dialogue in a conversation.
|
||||
*/
|
||||
typedef DialogueEntryData =
|
||||
{
|
||||
/**
|
||||
* Which speaker is speaking.
|
||||
* @see `SpeakerData.hx`
|
||||
*/
|
||||
public var speaker:String;
|
||||
|
||||
/**
|
||||
* The animation the speaker should play for this line of dialogue.
|
||||
*/
|
||||
public var speakerAnimation:String;
|
||||
|
||||
/**
|
||||
* Which dialogue box to use for this line of dialogue.
|
||||
* @see `DialogueBoxData.hx`
|
||||
*/
|
||||
public var box:String;
|
||||
|
||||
/**
|
||||
* Which animation to play for the dialogue box.
|
||||
*/
|
||||
public var boxAnimation:String;
|
||||
|
||||
/**
|
||||
* The text that will display for this line of dialogue.
|
||||
* Text will automatically wrap.
|
||||
* When the user advances the dialogue, the next entry in the array will concatenate on.
|
||||
* Advancing when the last entry is displayed will move to the next `DialogueEntryData`,
|
||||
* or end the conversation if there are no more.
|
||||
*/
|
||||
public var text:Array<String>;
|
||||
|
||||
/**
|
||||
* The relative speed at which text gets "typed out".
|
||||
* Setting `speed` to `1.5` would make it look like the character is speaking quickly,
|
||||
* and setting `speed` to `0.5` would make it look like the character is emphasizing each word.
|
||||
*/
|
||||
@:optional
|
||||
@:default(1.0)
|
||||
public var speed:Float;
|
||||
};
|
81
source/funkin/data/dialogue/ConversationRegistry.hx
Normal file
81
source/funkin/data/dialogue/ConversationRegistry.hx
Normal file
|
@ -0,0 +1,81 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.play.cutscene.dialogue.Conversation;
|
||||
import funkin.data.dialogue.ConversationData;
|
||||
import funkin.play.cutscene.dialogue.ScriptedConversation;
|
||||
|
||||
class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>
|
||||
{
|
||||
/**
|
||||
* The current version string for the dialogue box data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateConversationData()` function.
|
||||
*/
|
||||
public static final CONVERSATION_DATA_VERSION:thx.semver.Version = "1.0.0";
|
||||
|
||||
public static final CONVERSATION_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
|
||||
|
||||
public static final instance:ConversationRegistry = new ConversationRegistry();
|
||||
|
||||
public function new()
|
||||
{
|
||||
super('CONVERSATION', 'dialogue/conversations', CONVERSATION_DATA_VERSION_RULE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryData(id:String):Null<ConversationData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<ConversationData>();
|
||||
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<ConversationData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<ConversationData>();
|
||||
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):Conversation
|
||||
{
|
||||
return ScriptedConversation.init(clsName, "unknown");
|
||||
}
|
||||
|
||||
function getScriptedClassNames():Array<String>
|
||||
{
|
||||
return ScriptedConversation.listScriptClasses();
|
||||
}
|
||||
}
|
128
source/funkin/data/dialogue/DialogueBoxData.hx
Normal file
128
source/funkin/data/dialogue/DialogueBoxData.hx
Normal file
|
@ -0,0 +1,128 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
/**
|
||||
* A type definition for the data for a conversation text box.
|
||||
* It includes things like the sprite to use, and the font and color for the text.
|
||||
* The actual text is included in the ConversationData.
|
||||
* @see https://lib.haxe.org/p/json2object/
|
||||
*/
|
||||
typedef DialogueBoxData =
|
||||
{
|
||||
/**
|
||||
* Semantic version for dialogue box data.
|
||||
*/
|
||||
public var version:String;
|
||||
|
||||
/**
|
||||
* A human readable name for the dialogue box type.
|
||||
*/
|
||||
public var name:String;
|
||||
|
||||
/**
|
||||
* The asset path for the sprite to use for the dialogue box.
|
||||
* Takes a static sprite or a sprite sheet.
|
||||
*/
|
||||
public var assetPath:String;
|
||||
|
||||
/**
|
||||
* Whether to horizontally flip the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var flipX:Bool;
|
||||
|
||||
/**
|
||||
* Whether to vertically flip the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var flipY:Bool;
|
||||
|
||||
/**
|
||||
* Whether to disable anti-aliasing for the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var isPixel:Bool;
|
||||
|
||||
/**
|
||||
* The relative horizontal and vertical offsets for the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
public var offsets:Array<Float>;
|
||||
|
||||
/**
|
||||
* Info about how to display text in the dialogue box.
|
||||
*/
|
||||
public var text:DialogueBoxTextData;
|
||||
|
||||
/**
|
||||
* Multiply the size of the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(1)
|
||||
public var scale:Float;
|
||||
|
||||
/**
|
||||
* If using a spritesheet for the dialogue box, the animations to use.
|
||||
*/
|
||||
@:optional
|
||||
@:default([])
|
||||
public var animations:Array<AnimationData>;
|
||||
}
|
||||
|
||||
typedef DialogueBoxTextData =
|
||||
{
|
||||
/**
|
||||
* The position of the text in teh box.
|
||||
*/
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
var offsets:Array<Float>;
|
||||
|
||||
/**
|
||||
* The width of the
|
||||
*/
|
||||
@:optional
|
||||
@:default(300)
|
||||
var width:Int;
|
||||
|
||||
/**
|
||||
* The font size to use for the text.
|
||||
*/
|
||||
@:optional
|
||||
@:default(32)
|
||||
var size:Int;
|
||||
|
||||
/**
|
||||
* The color to use for the text.
|
||||
* Use a string that can be translated to a color, like `#FF0000` for red.
|
||||
*/
|
||||
@:optional
|
||||
@:default("#000000")
|
||||
var color:String;
|
||||
|
||||
/**
|
||||
* The font to use for the text.
|
||||
* @since v1.1.0
|
||||
* @default `Arial`, make sure to switch this!
|
||||
*/
|
||||
@:optional
|
||||
@:default("Arial")
|
||||
var fontFamily:String;
|
||||
|
||||
/**
|
||||
* The color to use for the shadow of the text. Use transparent to disable.
|
||||
*/
|
||||
var shadowColor:String;
|
||||
|
||||
/**
|
||||
* The width of the shadow of the text.
|
||||
*/
|
||||
@:optional
|
||||
@:default(0)
|
||||
var shadowWidth:Int;
|
||||
};
|
81
source/funkin/data/dialogue/DialogueBoxRegistry.hx
Normal file
81
source/funkin/data/dialogue/DialogueBoxRegistry.hx
Normal file
|
@ -0,0 +1,81 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.play.cutscene.dialogue.DialogueBox;
|
||||
import funkin.data.dialogue.DialogueBoxData;
|
||||
import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
|
||||
|
||||
class DialogueBoxRegistry extends BaseRegistry<DialogueBox, DialogueBoxData>
|
||||
{
|
||||
/**
|
||||
* The current version string for the dialogue box data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateDialogueBoxData()` function.
|
||||
*/
|
||||
public static final DIALOGUEBOX_DATA_VERSION:thx.semver.Version = "1.1.0";
|
||||
|
||||
public static final DIALOGUEBOX_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
|
||||
|
||||
public static final instance:DialogueBoxRegistry = new DialogueBoxRegistry();
|
||||
|
||||
public function new()
|
||||
{
|
||||
super('DIALOGUEBOX', 'dialogue/boxes', DIALOGUEBOX_DATA_VERSION_RULE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryData(id:String):Null<DialogueBoxData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<DialogueBoxData>();
|
||||
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<DialogueBoxData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<DialogueBoxData>();
|
||||
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):DialogueBox
|
||||
{
|
||||
return ScriptedDialogueBox.init(clsName, "unknown");
|
||||
}
|
||||
|
||||
function getScriptedClassNames():Array<String>
|
||||
{
|
||||
return ScriptedDialogueBox.listScriptClasses();
|
||||
}
|
||||
}
|
68
source/funkin/data/dialogue/SpeakerData.hx
Normal file
68
source/funkin/data/dialogue/SpeakerData.hx
Normal file
|
@ -0,0 +1,68 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
/**
|
||||
* A type definition for a specific speaker in a conversation.
|
||||
* It includes things like what sprite to use and its available animations.
|
||||
* @see https://lib.haxe.org/p/json2object/
|
||||
*/
|
||||
typedef SpeakerData =
|
||||
{
|
||||
/**
|
||||
* Semantic version of the speaker data.
|
||||
*/
|
||||
public var version:String;
|
||||
|
||||
/**
|
||||
* A human-readable name for the speaker.
|
||||
*/
|
||||
public var name:String;
|
||||
|
||||
/**
|
||||
* The path to the asset to use for the speaker's sprite.
|
||||
*/
|
||||
public var assetPath:String;
|
||||
|
||||
/**
|
||||
* Whether the sprite should be flipped horizontally.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var flipX:Bool;
|
||||
|
||||
/**
|
||||
* Whether the sprite should be flipped vertically.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var flipY:Bool;
|
||||
|
||||
/**
|
||||
* Whether to disable anti-aliasing for the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var isPixel:Bool;
|
||||
|
||||
/**
|
||||
* The offsets to apply to the sprite's position.
|
||||
*/
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
public var offsets:Array<Float>;
|
||||
|
||||
/**
|
||||
* The scale to apply to the sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(1.0)
|
||||
public var scale:Float;
|
||||
|
||||
/**
|
||||
* The available animations for the speaker.
|
||||
*/
|
||||
@:optional
|
||||
@:default([])
|
||||
public var animations:Array<AnimationData>;
|
||||
}
|
81
source/funkin/data/dialogue/SpeakerRegistry.hx
Normal file
81
source/funkin/data/dialogue/SpeakerRegistry.hx
Normal file
|
@ -0,0 +1,81 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.play.cutscene.dialogue.Speaker;
|
||||
import funkin.data.dialogue.SpeakerData;
|
||||
import funkin.play.cutscene.dialogue.ScriptedSpeaker;
|
||||
|
||||
class SpeakerRegistry extends BaseRegistry<Speaker, SpeakerData>
|
||||
{
|
||||
/**
|
||||
* The current version string for the speaker data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateSpeakerData()` function.
|
||||
*/
|
||||
public static final SPEAKER_DATA_VERSION:thx.semver.Version = "1.0.0";
|
||||
|
||||
public static final SPEAKER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
|
||||
|
||||
public static final instance:SpeakerRegistry = new SpeakerRegistry();
|
||||
|
||||
public function new()
|
||||
{
|
||||
super('SPEAKER', 'dialogue/speakers', SPEAKER_DATA_VERSION_RULE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryData(id:String):Null<SpeakerData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<SpeakerData>();
|
||||
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<SpeakerData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SpeakerData>();
|
||||
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):Speaker
|
||||
{
|
||||
return ScriptedSpeaker.init(clsName, "unknown");
|
||||
}
|
||||
|
||||
function getScriptedClassNames():Array<String>
|
||||
{
|
||||
return ScriptedSpeaker.listScriptClasses();
|
||||
}
|
||||
}
|
0
source/funkin/data/dialogue/TODO.md
Normal file
0
source/funkin/data/dialogue/TODO.md
Normal file
|
@ -1,7 +1,7 @@
|
|||
package funkin.data.event;
|
||||
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.play.event.ScriptedSongEvent;
|
||||
|
@ -9,7 +9,7 @@ import funkin.play.event.ScriptedSongEvent;
|
|||
/**
|
||||
* This class statically handles the parsing of internal and scripted song event handlers.
|
||||
*/
|
||||
class SongEventParser
|
||||
class SongEventRegistry
|
||||
{
|
||||
/**
|
||||
* Every built-in event class must be added to this list.
|
||||
|
@ -160,84 +160,3 @@ class SongEventParser
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract SongEventFieldType(String) from String to String
|
||||
{
|
||||
/**
|
||||
* The STRING type will display as a text field.
|
||||
*/
|
||||
var STRING = "string";
|
||||
|
||||
/**
|
||||
* The INTEGER type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var INTEGER = "integer";
|
||||
|
||||
/**
|
||||
* The FLOAT type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var FLOAT = "float";
|
||||
|
||||
/**
|
||||
* The BOOL type will display as a checkbox.
|
||||
*/
|
||||
var BOOL = "bool";
|
||||
|
||||
/**
|
||||
* The ENUM type will display as a dropdown.
|
||||
* Make sure to specify the `keys` field in the schema.
|
||||
*/
|
||||
var ENUM = "enum";
|
||||
}
|
||||
|
||||
typedef SongEventSchemaField =
|
||||
{
|
||||
/**
|
||||
* The name of the property as it should be saved in the event data.
|
||||
*/
|
||||
name:String,
|
||||
|
||||
/**
|
||||
* The title of the field to display in the UI.
|
||||
*/
|
||||
title:String,
|
||||
|
||||
/**
|
||||
* The type of the field.
|
||||
*/
|
||||
type:SongEventFieldType,
|
||||
|
||||
/**
|
||||
* Used only for ENUM values.
|
||||
* The key is the display name and the value is the actual value.
|
||||
*/
|
||||
?keys:Map<String, Dynamic>,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The minimum value that can be entered.
|
||||
* @default No minimum
|
||||
*/
|
||||
?min:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The maximum value that can be entered.
|
||||
* @default No maximum
|
||||
*/
|
||||
?max:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The step value that will be used when incrementing/decrementing the value.
|
||||
* @default `0.1`
|
||||
*/
|
||||
?step:Float,
|
||||
|
||||
/**
|
||||
* An optional default value for the field.
|
||||
*/
|
||||
?defaultValue:Dynamic,
|
||||
}
|
||||
|
||||
typedef SongEventSchema = Array<SongEventSchemaField>;
|
177
source/funkin/data/event/SongEventSchema.hx
Normal file
177
source/funkin/data/event/SongEventSchema.hx
Normal file
|
@ -0,0 +1,177 @@
|
|||
package funkin.data.event;
|
||||
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.play.event.ScriptedSongEvent;
|
||||
|
||||
@:forward(name, title, type, keys, min, max, step, units, defaultValue, iterator)
|
||||
abstract SongEventSchema(SongEventSchemaRaw)
|
||||
{
|
||||
/**
|
||||
* These units look better when placed immediately next to the value, rather than after a space.
|
||||
*/
|
||||
static final NO_SPACE_UNITS:Array<String> = ['x', '°', '%'];
|
||||
|
||||
public function new(?fields:Array<SongEventSchemaField>)
|
||||
{
|
||||
this = fields;
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public function getByName(name:String):SongEventSchemaField
|
||||
{
|
||||
for (field in this)
|
||||
{
|
||||
if (field.name == name) return field;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getFirstField():SongEventSchemaField
|
||||
{
|
||||
return this[0];
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public inline function get(key:Int)
|
||||
{
|
||||
return this[key];
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField
|
||||
{
|
||||
return this[k] = v;
|
||||
}
|
||||
|
||||
public function stringifyFieldValue(name:String, value:Dynamic, addUnits:Bool = true):String
|
||||
{
|
||||
var field:SongEventSchemaField = getByName(name);
|
||||
if (field == null) return 'Unknown';
|
||||
|
||||
switch (field.type)
|
||||
{
|
||||
case SongEventFieldType.STRING:
|
||||
return Std.string(value);
|
||||
case SongEventFieldType.INTEGER:
|
||||
var returnValue:String = Std.string(value);
|
||||
if (addUnits) return addUnitsToString(returnValue, field);
|
||||
return returnValue;
|
||||
case SongEventFieldType.FLOAT:
|
||||
var returnValue:String = Std.string(value);
|
||||
if (addUnits) return addUnitsToString(returnValue, field);
|
||||
return returnValue;
|
||||
case SongEventFieldType.BOOL:
|
||||
return Std.string(value);
|
||||
case SongEventFieldType.ENUM:
|
||||
var valueString:String = Std.string(value);
|
||||
for (key in field.keys.keys())
|
||||
{
|
||||
// Comparing these values as strings because comparing Dynamic variables is jank.
|
||||
if (Std.string(field.keys.get(key)) == valueString) return key;
|
||||
}
|
||||
return valueString;
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function addUnitsToString(value:String, field:SongEventSchemaField)
|
||||
{
|
||||
if (field.units == null || field.units == '') return value;
|
||||
|
||||
var unit:String = field.units;
|
||||
|
||||
return value + (NO_SPACE_UNITS.contains(unit) ? '' : ' ') + '${unit}';
|
||||
}
|
||||
}
|
||||
|
||||
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
|
||||
|
||||
typedef SongEventSchemaField =
|
||||
{
|
||||
/**
|
||||
* The name of the property as it should be saved in the event data.
|
||||
*/
|
||||
name:String,
|
||||
|
||||
/**
|
||||
* The title of the field to display in the UI.
|
||||
*/
|
||||
title:String,
|
||||
|
||||
/**
|
||||
* The type of the field.
|
||||
*/
|
||||
type:SongEventFieldType,
|
||||
|
||||
/**
|
||||
* Used only for ENUM values.
|
||||
* The key is the display name and the value is the actual value.
|
||||
*/
|
||||
?keys:Map<String, Dynamic>,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The minimum value that can be entered.
|
||||
* @default No minimum
|
||||
*/
|
||||
?min:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The maximum value that can be entered.
|
||||
* @default No maximum
|
||||
*/
|
||||
?max:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The step value that will be used when incrementing/decrementing the value.
|
||||
* @default `0.1`
|
||||
*/
|
||||
?step:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The units that the value is expressed in (pixels, percent, etc).
|
||||
*/
|
||||
?units:String,
|
||||
|
||||
/**
|
||||
* An optional default value for the field.
|
||||
*/
|
||||
?defaultValue:Dynamic,
|
||||
}
|
||||
|
||||
enum abstract SongEventFieldType(String) from String to String
|
||||
{
|
||||
/**
|
||||
* The STRING type will display as a text field.
|
||||
*/
|
||||
var STRING = "string";
|
||||
|
||||
/**
|
||||
* The INTEGER type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var INTEGER = "integer";
|
||||
|
||||
/**
|
||||
* The FLOAT type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var FLOAT = "float";
|
||||
|
||||
/**
|
||||
* The BOOL type will display as a checkbox.
|
||||
*/
|
||||
var BOOL = "bool";
|
||||
|
||||
/**
|
||||
* The ENUM type will display as a dropdown.
|
||||
* Make sure to specify the `keys` field in the schema.
|
||||
*/
|
||||
var ENUM = "enum";
|
||||
}
|
|
@ -7,9 +7,9 @@ import funkin.ui.story.ScriptedLevel;
|
|||
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
|
||||
* 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";
|
||||
|
||||
|
@ -30,6 +30,7 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
|
|||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<LevelData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
|
@ -57,6 +58,7 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
|
|||
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<LevelData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<LevelData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
|
|
@ -35,6 +35,7 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
|
|||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<NoteStyleData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
|
@ -62,6 +63,7 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
|
|||
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<NoteStyleData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<NoteStyleData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package funkin.data.song;
|
||||
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import thx.semver.Version;
|
||||
import funkin.util.tools.ICloneable;
|
||||
|
||||
/**
|
||||
* Data containing information about a song.
|
||||
|
@ -9,7 +13,7 @@ import thx.semver.Version;
|
|||
* Data which is only necessary in-game should be stored in the SongChartData.
|
||||
*/
|
||||
@:nullSafety
|
||||
class SongMetadata
|
||||
class SongMetadata implements ICloneable<SongMetadata>
|
||||
{
|
||||
/**
|
||||
* A semantic versioning string for the song data format.
|
||||
|
@ -35,10 +39,11 @@ class SongMetadata
|
|||
public var looped:Bool;
|
||||
|
||||
/**
|
||||
* Instrumental and vocal offsets. Optional, defaults to 0.
|
||||
* Instrumental and vocal offsets.
|
||||
* Defaults to an empty SongOffsets object.
|
||||
*/
|
||||
@:optional
|
||||
public var offsets:SongOffsets;
|
||||
public var offsets:Null<SongOffsets>;
|
||||
|
||||
/**
|
||||
* Data relating to the song's gameplay.
|
||||
|
@ -84,16 +89,16 @@ class SongMetadata
|
|||
* @param newVariation Set to a new variation ID to change the new metadata.
|
||||
* @return The cloned SongMetadata
|
||||
*/
|
||||
public function clone(?newVariation:String = null):SongMetadata
|
||||
public function clone():SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
|
||||
var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.variation);
|
||||
result.version = this.version;
|
||||
result.timeFormat = this.timeFormat;
|
||||
result.divisions = this.divisions;
|
||||
result.offsets = this.offsets;
|
||||
result.timeChanges = this.timeChanges;
|
||||
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.looped = this.looped;
|
||||
result.playData = this.playData;
|
||||
result.playData = this.playData.clone();
|
||||
result.generatedBy = this.generatedBy;
|
||||
|
||||
return result;
|
||||
|
@ -128,7 +133,7 @@ enum abstract SongTimeFormat(String) from String to String
|
|||
var MILLISECONDS = 'ms';
|
||||
}
|
||||
|
||||
class SongTimeChange
|
||||
class SongTimeChange implements ICloneable<SongTimeChange>
|
||||
{
|
||||
public static final DEFAULT_SONGTIMECHANGE:SongTimeChange = new SongTimeChange(0, 100);
|
||||
|
||||
|
@ -149,7 +154,7 @@ class SongTimeChange
|
|||
*/
|
||||
@:optional
|
||||
@:alias("b")
|
||||
public var beatTime:Null<Float>;
|
||||
public var beatTime:Float;
|
||||
|
||||
/**
|
||||
* Quarter notes per minute (float). Cannot be empty in the first element of the list,
|
||||
|
@ -195,6 +200,11 @@ class SongTimeChange
|
|||
this.beatTuplets = beatTuplets == null ? DEFAULT_BEAT_TUPLETS : beatTuplets;
|
||||
}
|
||||
|
||||
public function clone():SongTimeChange
|
||||
{
|
||||
return new SongTimeChange(this.timeStamp, this.bpm, this.timeSignatureNum, this.timeSignatureDen, this.beatTime, this.beatTuplets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -209,7 +219,7 @@ class SongTimeChange
|
|||
* These are intended to correct for issues with the chart, or with the song's audio (for example a 10ms delay before the song starts).
|
||||
* This is independent of the offsets applied in the user's settings, which are applied after these offsets and intended to correct for the user's hardware.
|
||||
*/
|
||||
class SongOffsets
|
||||
class SongOffsets implements ICloneable<SongOffsets>
|
||||
{
|
||||
/**
|
||||
* The offset, in milliseconds, to apply to the song's instrumental relative to the chart.
|
||||
|
@ -279,6 +289,15 @@ class SongOffsets
|
|||
return value;
|
||||
}
|
||||
|
||||
public function clone():SongOffsets
|
||||
{
|
||||
var result:SongOffsets = new SongOffsets(this.instrumental);
|
||||
result.altInstrumentals = this.altInstrumentals.clone();
|
||||
result.vocals = this.vocals.clone();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -292,7 +311,7 @@ class SongOffsets
|
|||
* Metadata for a song only used for the music.
|
||||
* For example, the menu music.
|
||||
*/
|
||||
class SongMusicData
|
||||
class SongMusicData implements ICloneable<SongMusicData>
|
||||
{
|
||||
/**
|
||||
* A semantic versioning string for the song data format.
|
||||
|
@ -346,13 +365,13 @@ class SongMusicData
|
|||
this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
}
|
||||
|
||||
public function clone(?newVariation:String = null):SongMusicData
|
||||
public function clone():SongMusicData
|
||||
{
|
||||
var result:SongMusicData = new SongMusicData(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
|
||||
var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation);
|
||||
result.version = this.version;
|
||||
result.timeFormat = this.timeFormat;
|
||||
result.divisions = this.divisions;
|
||||
result.timeChanges = this.timeChanges;
|
||||
result.timeChanges = this.timeChanges.clone();
|
||||
result.looped = this.looped;
|
||||
result.generatedBy = this.generatedBy;
|
||||
|
||||
|
@ -368,7 +387,7 @@ class SongMusicData
|
|||
}
|
||||
}
|
||||
|
||||
class SongPlayData
|
||||
class SongPlayData implements ICloneable<SongPlayData>
|
||||
{
|
||||
/**
|
||||
* The variations this song has. The associated metadata files should exist.
|
||||
|
@ -412,11 +431,46 @@ class SongPlayData
|
|||
@:optional
|
||||
public var album:Null<String>;
|
||||
|
||||
/**
|
||||
* The start time for the audio preview in Freeplay.
|
||||
* Defaults to 0 seconds in.
|
||||
* @since `2.2.2`
|
||||
*/
|
||||
@:optional
|
||||
@:default(0)
|
||||
public var previewStart:Int;
|
||||
|
||||
/**
|
||||
* The end time for the audio preview in Freeplay.
|
||||
* Defaults to 15 seconds in.
|
||||
* @since `2.2.2`
|
||||
*/
|
||||
@:optional
|
||||
@:default(15000)
|
||||
public var previewEnd:Int;
|
||||
|
||||
public function new()
|
||||
{
|
||||
ratings = new Map<String, Int>();
|
||||
}
|
||||
|
||||
public function clone():SongPlayData
|
||||
{
|
||||
// TODO: This sucks! If you forget to update this you get weird behavior.
|
||||
var result:SongPlayData = new SongPlayData();
|
||||
result.songVariations = this.songVariations.clone();
|
||||
result.difficulties = this.difficulties.clone();
|
||||
result.characters = this.characters.clone();
|
||||
result.stage = this.stage;
|
||||
result.noteStyle = this.noteStyle;
|
||||
result.ratings = this.ratings.clone();
|
||||
result.album = this.album;
|
||||
result.previewStart = this.previewStart;
|
||||
result.previewEnd = this.previewEnd;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -430,7 +484,7 @@ class SongPlayData
|
|||
* Information about the characters used in this variation of the song.
|
||||
* Create a new variation if you want to change the characters.
|
||||
*/
|
||||
class SongCharacterData
|
||||
class SongCharacterData implements ICloneable<SongCharacterData>
|
||||
{
|
||||
@:optional
|
||||
@:default('')
|
||||
|
@ -460,6 +514,14 @@ class SongCharacterData
|
|||
this.instrumental = instrumental;
|
||||
}
|
||||
|
||||
public function clone():SongCharacterData
|
||||
{
|
||||
var result:SongCharacterData = new SongCharacterData(this.player, this.girlfriend, this.opponent, this.instrumental);
|
||||
result.altInstrumentals = this.altInstrumentals.clone();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -469,7 +531,7 @@ class SongCharacterData
|
|||
}
|
||||
}
|
||||
|
||||
class SongChartData
|
||||
class SongChartData implements ICloneable<SongChartData>
|
||||
{
|
||||
@:default(funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION)
|
||||
@:jcustomparse(funkin.data.DataParse.semverVersion)
|
||||
|
@ -539,6 +601,24 @@ class SongChartData
|
|||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function clone():SongChartData
|
||||
{
|
||||
// We have to manually perform the deep clone here because Map.deepClone() doesn't work.
|
||||
var noteDataClone:Map<String, Array<SongNoteData>> = new Map<String, Array<SongNoteData>>();
|
||||
for (key in this.notes.keys())
|
||||
{
|
||||
noteDataClone.set(key, this.notes.get(key).deepClone());
|
||||
}
|
||||
var eventDataClone:Array<SongEventData> = this.events.deepClone();
|
||||
|
||||
var result:SongChartData = new SongChartData(this.scrollSpeed.clone(), eventDataClone, noteDataClone);
|
||||
result.version = this.version;
|
||||
result.generatedBy = this.generatedBy;
|
||||
result.variation = this.variation;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -548,7 +628,7 @@ class SongChartData
|
|||
}
|
||||
}
|
||||
|
||||
class SongEventDataRaw
|
||||
class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
{
|
||||
/**
|
||||
* The timestamp of the event. The timestamp is in the format of the song's time format.
|
||||
|
@ -602,14 +682,19 @@ class SongEventDataRaw
|
|||
{
|
||||
if (_stepTime != null && !force) return _stepTime;
|
||||
|
||||
return _stepTime = Conductor.getTimeInSteps(this.time);
|
||||
return _stepTime = Conductor.instance.getTimeInSteps(this.time);
|
||||
}
|
||||
|
||||
public function clone():SongEventDataRaw
|
||||
{
|
||||
return new SongEventDataRaw(this.time, this.event, this.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap SongEventData in an abstract so we can overload operators.
|
||||
*/
|
||||
@:forward(time, event, value, activated, getStepTime)
|
||||
@:forward(time, event, value, activated, getStepTime, clone)
|
||||
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
|
||||
{
|
||||
public function new(time:Float, event:String, value:Dynamic = null)
|
||||
|
@ -617,6 +702,38 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
this = new SongEventDataRaw(time, event, value);
|
||||
}
|
||||
|
||||
public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic
|
||||
{
|
||||
if (this.value == null) return {};
|
||||
if (Std.isOfType(this.value, Array))
|
||||
{
|
||||
var result:haxe.DynamicAccess<Dynamic> = {};
|
||||
result.set(defaultKey, this.value);
|
||||
return cast result;
|
||||
}
|
||||
else if (Reflect.isObject(this.value))
|
||||
{
|
||||
// We enter this case if the value is a struct.
|
||||
return cast this.value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result:haxe.DynamicAccess<Dynamic> = {};
|
||||
result.set(defaultKey, this.value);
|
||||
return cast result;
|
||||
}
|
||||
}
|
||||
|
||||
public inline function getHandler():Null<SongEvent>
|
||||
{
|
||||
return SongEventRegistry.getEvent(this.event);
|
||||
}
|
||||
|
||||
public inline function getSchema():Null<SongEventSchema>
|
||||
{
|
||||
return SongEventRegistry.getEventSchema(this.event);
|
||||
}
|
||||
|
||||
public inline function getDynamic(key:String):Null<Dynamic>
|
||||
{
|
||||
return this.value == null ? null : Reflect.field(this.value, key);
|
||||
|
@ -662,6 +779,34 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
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);
|
||||
|
@ -712,7 +857,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
}
|
||||
}
|
||||
|
||||
class SongNoteDataRaw
|
||||
class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
{
|
||||
/**
|
||||
* The timestamp of the note. The timestamp is in the format of the song's time format.
|
||||
|
@ -742,12 +887,18 @@ class SongNoteDataRaw
|
|||
@:alias("l")
|
||||
@:default(0)
|
||||
@: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.
|
||||
* This can allow the note to include information used for custom behavior.
|
||||
* Defaults to blank or `"normal"`.
|
||||
* Defaults to blank or `Constants.DEFAULT_DIFFICULTY`.
|
||||
*/
|
||||
@:alias("k")
|
||||
@:default("normal")
|
||||
|
@ -796,9 +947,14 @@ class SongNoteDataRaw
|
|||
{
|
||||
if (_stepTime != null && !force) return _stepTime;
|
||||
|
||||
return _stepTime = Conductor.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
|
||||
var _stepLength:Null<Float> = null;
|
||||
|
||||
|
@ -812,7 +968,7 @@ class SongNoteDataRaw
|
|||
|
||||
if (_stepLength != null && !force) return _stepLength;
|
||||
|
||||
return _stepLength = Conductor.getTimeInSteps(this.time + this.length) - getStepTime();
|
||||
return _stepLength = Conductor.instance.getTimeInSteps(this.time + this.length) - getStepTime();
|
||||
}
|
||||
|
||||
public function setStepLength(value:Float):Void
|
||||
|
@ -823,11 +979,21 @@ class SongNoteDataRaw
|
|||
}
|
||||
else
|
||||
{
|
||||
var lengthMs:Float = Conductor.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;
|
||||
}
|
||||
|
||||
// Recalculate the step length next time it's requested.
|
||||
_stepLength = null;
|
||||
}
|
||||
|
||||
public function clone():SongNoteDataRaw
|
||||
{
|
||||
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -891,6 +1057,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
@:op(A == B)
|
||||
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 (other.kind != '' && other.kind != 'normal') return false;
|
||||
|
@ -906,6 +1076,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
@:op(A != B)
|
||||
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 (other.kind != '' && other.kind != 'normal') return true;
|
||||
|
@ -921,24 +1095,32 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
@:op(A > B)
|
||||
public function op_greaterThan(other:SongNoteData):Bool
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
return this.time > other.time;
|
||||
}
|
||||
|
||||
@:op(A < B)
|
||||
public function op_lessThan(other:SongNoteData):Bool
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
return this.time < other.time;
|
||||
}
|
||||
|
||||
@:op(A >= B)
|
||||
public function op_greaterThanOrEquals(other:SongNoteData):Bool
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
return this.time >= other.time;
|
||||
}
|
||||
|
||||
@:op(A <= B)
|
||||
public function op_lessThanOrEquals(other:SongNoteData):Bool
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
return this.time <= other.time;
|
||||
}
|
||||
|
||||
|
|
|
@ -153,8 +153,8 @@ class SongDataUtils
|
|||
public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int = null):Array<SongNoteData>
|
||||
{
|
||||
if (notes.length == 0) return notes;
|
||||
if (timeOffset == null) timeOffset = -Std.int(notes[0].time);
|
||||
return offsetSongNoteData(sortNotes(notes), timeOffset);
|
||||
if (timeOffset == null) timeOffset = Std.int(notes[0].time);
|
||||
return offsetSongNoteData(sortNotes(notes), -timeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -165,8 +165,8 @@ class SongDataUtils
|
|||
public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int = null):Array<SongEventData>
|
||||
{
|
||||
if (events.length == 0) return events;
|
||||
if (timeOffset == null) timeOffset = -Std.int(events[0].time);
|
||||
return offsetSongEventData(sortEvents(events), timeOffset);
|
||||
if (timeOffset == null) timeOffset = Std.int(events[0].time);
|
||||
return offsetSongEventData(sortEvents(events), -timeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -230,6 +230,7 @@ class SongDataUtils
|
|||
trace('Read ${notesString.length} characters from clipboard.');
|
||||
|
||||
var parser = new json2object.JsonParser<SongClipboardItems>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(notesString, 'clipboard');
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
|
@ -272,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>
|
||||
{
|
||||
|
|
|
@ -12,6 +12,7 @@ import funkin.util.VersionUtil;
|
|||
|
||||
using funkin.data.song.migrator.SongDataMigrator;
|
||||
|
||||
@:nullSafety
|
||||
class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||
{
|
||||
/**
|
||||
|
@ -19,7 +20,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateStageData()` function.
|
||||
*/
|
||||
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.1";
|
||||
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.2";
|
||||
|
||||
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x";
|
||||
|
||||
|
@ -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 var DEFAULT_GENERATEDBY(get, null):String;
|
||||
public static var DEFAULT_GENERATEDBY(get, never):String;
|
||||
|
||||
static function get_DEFAULT_GENERATEDBY():String
|
||||
{
|
||||
|
@ -57,7 +58,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
// SCRIPTED ENTRIES
|
||||
//
|
||||
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
|
||||
log('Registering ${scriptedEntryClassNames.length} scripted entries...');
|
||||
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
|
||||
|
||||
for (entryCls in scriptedEntryClassNames)
|
||||
{
|
||||
|
@ -83,12 +84,12 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
|
||||
return !entries.exists(entryId);
|
||||
});
|
||||
log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
|
||||
log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
|
||||
for (entryId in unscriptedEntryIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry:Song = createEntry(entryId);
|
||||
var entry:Null<Song> = createEntry(entryId);
|
||||
if (entry != null)
|
||||
{
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
|
@ -126,6 +127,8 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata>();
|
||||
parser.ignoreUnknownVariables = true;
|
||||
|
||||
switch (loadEntryMetadataFile(id, variation))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
|
@ -147,6 +150,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata>();
|
||||
parser.ignoreUnknownVariables = true;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -206,6 +210,8 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
||||
parser.ignoreUnknownVariables = true;
|
||||
|
||||
switch (loadEntryMetadataFile(id, variation))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
|
@ -226,6 +232,8 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||
parser.ignoreUnknownVariables = true;
|
||||
|
||||
switch (loadEntryMetadataFile(id, variation))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
|
@ -244,6 +252,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
||||
parser.ignoreUnknownVariables = true;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -257,6 +266,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||
parser.ignoreUnknownVariables = true;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -272,6 +282,8 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMusicData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
|
||||
switch (loadMusicDataFile(id, variation))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
|
@ -291,6 +303,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
public function parseMusicDataRaw(contents:String, ?fileName:String = 'raw'):Null<SongMusicData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongMusicData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -334,6 +347,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongChartData>();
|
||||
parser.ignoreUnknownVariables = true;
|
||||
|
||||
switch (loadEntryChartFile(id, variation))
|
||||
{
|
||||
|
@ -356,6 +370,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongChartData>();
|
||||
parser.ignoreUnknownVariables = true;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -441,7 +456,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
{
|
||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -449,7 +464,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
{
|
||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ class ChartManifestData
|
|||
public static function deserialize(contents:String):Null<ChartManifestData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<ChartManifestData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(contents, 'manifest.json');
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
|
|
@ -19,7 +19,8 @@ class LegacySongData
|
|||
|
||||
@:jcustomparse(funkin.data.DataParse.eitherLegacyScrollSpeeds)
|
||||
public var speed:Either<Float, LegacyScrollSpeeds>;
|
||||
public var stageDefault:String;
|
||||
@:optional
|
||||
public var stageDefault:Null<String>;
|
||||
public var bpm:Float;
|
||||
|
||||
@:jcustomparse(funkin.data.DataParse.eitherLegacyNoteData)
|
||||
|
|
|
@ -14,6 +14,7 @@ class FNFLegacyImporter
|
|||
public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):FNFLegacyData
|
||||
{
|
||||
var parser = new json2object.JsonParser<FNFLegacyData>();
|
||||
parser.ignoreUnknownVariables = true; // Set to true to ignore extra variables that might be included in the JSON.
|
||||
parser.fromJson(input, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -185,15 +186,34 @@ class FNFLegacyImporter
|
|||
return result;
|
||||
}
|
||||
|
||||
static final STRUMLINE_SIZE = 4;
|
||||
|
||||
static function migrateNoteSections(input:Array<LegacyNoteSection>):Array<SongNoteData>
|
||||
{
|
||||
var result:Array<SongNoteData> = [];
|
||||
|
||||
for (section in input)
|
||||
{
|
||||
var mustHitSection = section.mustHitSection ?? false;
|
||||
for (note in section.sectionNotes)
|
||||
{
|
||||
result.push(new SongNoteData(note.time, note.data, note.length, note.getKind()));
|
||||
// Handle the dumb logic for mustHitSection.
|
||||
var noteData = note.data;
|
||||
|
||||
// Flip notes if mustHitSection is FALSE (not true lol).
|
||||
if (!mustHitSection)
|
||||
{
|
||||
if (noteData >= STRUMLINE_SIZE)
|
||||
{
|
||||
noteData -= STRUMLINE_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
noteData += STRUMLINE_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(new SongNoteData(note.time, noteData, note.length, note.getKind()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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;
|
||||
}
|
||||
}
|
|
@ -12,22 +12,19 @@ class MeshRender extends FlxStrip
|
|||
public var vertex_count(default, null):Int = 0;
|
||||
public var index_count(default, null):Int = 0;
|
||||
|
||||
var tri_offset:Int = 0;
|
||||
|
||||
public function new(x, y, ?col:FlxColor = FlxColor.WHITE)
|
||||
{
|
||||
super(x, y);
|
||||
makeGraphic(1, 1, col);
|
||||
}
|
||||
|
||||
public inline function start()
|
||||
/**
|
||||
* Add a vertex.
|
||||
*/
|
||||
public inline function build_vertex(x:Float, y:Float, u:Float = 0, v:Float = 0):Int
|
||||
{
|
||||
tri_offset = vertex_count;
|
||||
}
|
||||
|
||||
public inline function add_vertex(x:Float, y:Float, u:Float = 0, v:Float = 0)
|
||||
{
|
||||
final pos = vertex_count << 1;
|
||||
final index = vertex_count;
|
||||
final pos = index << 1;
|
||||
|
||||
vertices[pos] = x;
|
||||
vertices[pos + 1] = y;
|
||||
|
@ -36,48 +33,72 @@ class MeshRender extends FlxStrip
|
|||
uvtData[pos + 1] = v;
|
||||
|
||||
vertex_count++;
|
||||
return index;
|
||||
}
|
||||
|
||||
public function add_tri(a:Int, b:Int, c:Int)
|
||||
/**
|
||||
* Build a triangle from three vertex indexes.
|
||||
* @param a
|
||||
* @param b
|
||||
* @param c
|
||||
*/
|
||||
public function add_tri(a:Int, b:Int, c:Int):Void
|
||||
{
|
||||
indices[index_count] = a + tri_offset;
|
||||
indices[index_count + 1] = b + tri_offset;
|
||||
indices[index_count + 2] = c + tri_offset;
|
||||
indices[index_count] = a;
|
||||
indices[index_count + 1] = b;
|
||||
indices[index_count + 2] = c;
|
||||
|
||||
index_count += 3;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* top left - a
|
||||
*
|
||||
* top right - b
|
||||
*
|
||||
* bottom left - c
|
||||
*
|
||||
* bottom right - d
|
||||
*/
|
||||
public function add_quad(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, dx:Float, dy:Float, au:Float = 0, av:Float = 0, bu:Float = 0,
|
||||
bv:Float = 0, cu:Float = 0, cv:Float = 0, du:Float = 0, dv:Float = 0)
|
||||
public function build_tri(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, au:Float = 0, av:Float = 0, bu:Float = 0, bv:Float = 0, cu:Float = 0,
|
||||
cv:Float = 0):Void
|
||||
{
|
||||
start();
|
||||
// top left
|
||||
add_vertex(bx, by, bu, bv);
|
||||
// top right
|
||||
add_vertex(ax, ay, au, av);
|
||||
// bottom left
|
||||
add_vertex(cx, cy, cu, cv);
|
||||
// bottom right
|
||||
add_vertex(dx, dy, du, dv);
|
||||
add_tri(build_vertex(ax, ay, au, av), build_vertex(bx, by, bu, bv), build_vertex(cx, cy, cu, cv));
|
||||
}
|
||||
|
||||
add_tri(0, 1, 2);
|
||||
add_tri(0, 2, 3);
|
||||
/**
|
||||
* @param a top left vertex
|
||||
* @param b top right vertex
|
||||
* @param c bottom right vertex
|
||||
* @param d bottom left vertex
|
||||
*/
|
||||
public function add_quad(a:Int, b:Int, c:Int, d:Int):Void
|
||||
{
|
||||
add_tri(a, b, c);
|
||||
add_tri(a, c, d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a quad from four points.
|
||||
*
|
||||
* top right - a
|
||||
* top left - b
|
||||
* bottom right - c
|
||||
* bottom left - d
|
||||
*/
|
||||
public function build_quad(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, dx:Float, dy:Float, au:Float = 0, av:Float = 0, bu:Float = 0,
|
||||
bv:Float = 0, cu:Float = 0, cv:Float = 0, du:Float = 0, dv:Float = 0):Void
|
||||
{
|
||||
// top left
|
||||
var b = build_vertex(bx, by, bu, bv);
|
||||
// top right
|
||||
var a = build_vertex(ax, ay, au, av);
|
||||
// bottom left
|
||||
var c = build_vertex(cx, cy, cu, cv);
|
||||
// bottom right
|
||||
var d = build_vertex(dx, dy, du, dv);
|
||||
|
||||
add_tri(a, b, c);
|
||||
add_tri(a, c, d);
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
vertices.length = 0;
|
||||
indices.length = 0;
|
||||
uvtData.length = 0;
|
||||
colors.length = 0;
|
||||
vertex_count = 0;
|
||||
index_count = 0;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,18 @@ class Cursor
|
|||
Cursor.cursorMode = null;
|
||||
}
|
||||
|
||||
public static inline function toggle():Void
|
||||
{
|
||||
if (FlxG.mouse.visible)
|
||||
{
|
||||
hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
public static final CURSOR_DEFAULT_PARAMS:CursorParams =
|
||||
{
|
||||
graphic: "assets/images/cursor/cursor-default.png",
|
||||
|
|
|
@ -2,20 +2,21 @@ package funkin.modding;
|
|||
|
||||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.data.stage.StageData;
|
||||
import polymod.Polymod;
|
||||
import polymod.backends.PolymodAssets.PolymodAssetType;
|
||||
import polymod.format.ParseRules.TextFileFormat;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.data.level.LevelRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
||||
import funkin.data.dialogue.ConversationRegistry;
|
||||
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||
import funkin.data.dialogue.SpeakerRegistry;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.save.Save;
|
||||
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
||||
import funkin.data.song.SongRegistry;
|
||||
|
||||
class PolymodHandler
|
||||
|
@ -207,8 +208,8 @@ class PolymodHandler
|
|||
{
|
||||
return {
|
||||
assetLibraryPaths: [
|
||||
"default" => "preload", "shared" => "", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2", "week3" => "week3",
|
||||
"week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
|
||||
"default" => "preload", "shared" => "shared", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2",
|
||||
"week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
|
||||
],
|
||||
coreAssetRedirect: CORE_FOLDER,
|
||||
}
|
||||
|
@ -271,12 +272,12 @@ class PolymodHandler
|
|||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
SongEventParser.loadEventCache();
|
||||
ConversationDataParser.loadConversationCache();
|
||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||
SpeakerDataParser.loadSpeakerCache();
|
||||
StageDataParser.loadStageCache();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
SongEventRegistry.loadEventCache();
|
||||
ConversationRegistry.instance.loadEntries();
|
||||
DialogueBoxRegistry.instance.loadEntries();
|
||||
SpeakerRegistry.instance.loadEntries();
|
||||
StageRegistry.instance.loadEntries();
|
||||
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||
ModuleHandler.loadModuleCache();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class Countdown
|
|||
stopCountdown();
|
||||
|
||||
PlayState.instance.isInCountdown = true;
|
||||
Conductor.update(PlayState.instance.startTimestamp + Conductor.beatLengthMs * -5);
|
||||
Conductor.instance.update(PlayState.instance.startTimestamp + Conductor.instance.beatLengthMs * -5);
|
||||
// Handle onBeatHit events manually
|
||||
// @:privateAccess
|
||||
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
|
||||
|
@ -48,7 +48,7 @@ class Countdown
|
|||
// The timer function gets called based on the beat of the song.
|
||||
countdownTimer = new FlxTimer();
|
||||
|
||||
countdownTimer.start(Conductor.beatLengthMs / 1000, function(tmr:FlxTimer) {
|
||||
countdownTimer.start(Conductor.instance.beatLengthMs / 1000, function(tmr:FlxTimer) {
|
||||
if (PlayState.instance == null)
|
||||
{
|
||||
tmr.cancel();
|
||||
|
@ -158,7 +158,7 @@ class Countdown
|
|||
{
|
||||
stopCountdown();
|
||||
// This will trigger PlayState.startSong()
|
||||
Conductor.update(0);
|
||||
Conductor.instance.update(0);
|
||||
// PlayState.isInCountdown = false;
|
||||
}
|
||||
|
||||
|
@ -225,7 +225,7 @@ class Countdown
|
|||
countdownSprite.screenCenter();
|
||||
|
||||
// Fade sprite in, then out, then destroy it.
|
||||
FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.beatLengthMs / 1000,
|
||||
FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.instance.beatLengthMs / 1000,
|
||||
{
|
||||
ease: FlxEase.cubeInOut,
|
||||
onComplete: function(twn:FlxTween) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import flixel.sound.FlxSound;
|
|||
import funkin.ui.story.StoryMenuState;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.ui.MusicBeatSubState;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
|
@ -22,6 +23,12 @@ import funkin.play.character.BaseCharacter;
|
|||
*/
|
||||
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.
|
||||
* You can set this via script.
|
||||
|
@ -64,9 +71,16 @@ class GameOverSubState extends MusicBeatSubState
|
|||
*/
|
||||
var isEnding:Bool = false;
|
||||
|
||||
public function new()
|
||||
var isChartingMode:Bool = false;
|
||||
|
||||
var transparent:Bool;
|
||||
|
||||
public function new(params:GameOverParams)
|
||||
{
|
||||
super();
|
||||
|
||||
this.isChartingMode = params?.isChartingMode ?? false;
|
||||
transparent = params.transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,6 +94,13 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
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();
|
||||
|
||||
//
|
||||
|
@ -87,10 +108,12 @@ class GameOverSubState extends MusicBeatSubState
|
|||
//
|
||||
|
||||
// Add a black background to the screen.
|
||||
// We make this transparent so that we can see the stage underneath during debugging.
|
||||
var bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
|
||||
bg.alpha = 0.25;
|
||||
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,
|
||||
// but it's normally opaque.
|
||||
bg.alpha = transparent ? 0.25 : 1.0;
|
||||
bg.scrollFactor.set();
|
||||
bg.screenCenter();
|
||||
add(bg);
|
||||
|
||||
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
|
||||
|
@ -121,7 +144,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
gameOverMusic.stop();
|
||||
|
||||
// The conductor now represents the BPM of the game over music.
|
||||
Conductor.update(0);
|
||||
Conductor.instance.update(0);
|
||||
}
|
||||
|
||||
var hasStartedAnimation:Bool = false;
|
||||
|
@ -176,16 +199,27 @@ class GameOverSubState extends MusicBeatSubState
|
|||
// PlayState.seenCutscene = false; // old thing...
|
||||
gameOverMusic.stop();
|
||||
|
||||
if (PlayStatePlaylist.isStoryMode) FlxG.switchState(new StoryMenuState());
|
||||
if (isChartingMode)
|
||||
{
|
||||
this.close();
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
|
||||
PlayState.instance.close(); // This only works because PlayState is a substate!
|
||||
}
|
||||
else if (PlayStatePlaylist.isStoryMode)
|
||||
{
|
||||
FlxG.switchState(new StoryMenuState());
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.switchState(new FreeplayState());
|
||||
}
|
||||
}
|
||||
|
||||
if (gameOverMusic.playing)
|
||||
{
|
||||
// Match the conductor to the music.
|
||||
// This enables the stepHit and beatHit events.
|
||||
Conductor.update(gameOverMusic.time);
|
||||
Conductor.instance.update(gameOverMusic.time);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -201,6 +235,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
playJeffQuote();
|
||||
// Start music at lower volume
|
||||
startDeathMusic(0.2, false);
|
||||
boyfriend.playAnimation('deathLoop' + animationSuffix);
|
||||
}
|
||||
default:
|
||||
// Start music at normal volume once the initial death animation finishes.
|
||||
|
@ -261,15 +296,16 @@ class GameOverSubState extends MusicBeatSubState
|
|||
*/
|
||||
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)
|
||||
{
|
||||
musicPath = Paths.music('gameOverEnd' + musicSuffix);
|
||||
musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix);
|
||||
}
|
||||
if (!gameOverMusic.playing || force)
|
||||
{
|
||||
gameOverMusic.loadEmbedded(musicPath);
|
||||
gameOverMusic.volume = startingVolume;
|
||||
gameOverMusic.looped = !isEnding;
|
||||
gameOverMusic.play();
|
||||
}
|
||||
}
|
||||
|
@ -283,7 +319,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
public static function playBlueBalledSFX()
|
||||
{
|
||||
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;
|
||||
|
@ -306,4 +342,15 @@ class GameOverSubState extends MusicBeatSubState
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override function toString():String
|
||||
{
|
||||
return "GameOverSubState";
|
||||
}
|
||||
}
|
||||
|
||||
typedef GameOverParams =
|
||||
{
|
||||
var isChartingMode:Bool;
|
||||
var transparent:Bool;
|
||||
}
|
||||
|
|
|
@ -47,9 +47,11 @@ import funkin.ui.mainmenu.MainMenuState;
|
|||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.cutscene.dialogue.Conversation;
|
||||
import funkin.data.dialogue.ConversationRegistry;
|
||||
import funkin.play.cutscene.VanillaCutscenes;
|
||||
import funkin.play.cutscene.VideoCutscene;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.play.notes.NoteSprite;
|
||||
import funkin.play.notes.NoteDirection;
|
||||
import funkin.play.notes.NoteSprite;
|
||||
|
@ -59,11 +61,11 @@ import funkin.play.notes.notestyle.NoteStyle;
|
|||
import funkin.play.scoring.Scoring;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.play.stage.Stage;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.ui.transition.LoadingState;
|
||||
import funkin.play.components.PopUpStuff;
|
||||
import funkin.ui.options.PreferencesMenu;
|
||||
|
@ -571,15 +573,15 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
// Prepare the Conductor.
|
||||
Conductor.forceBPM(null);
|
||||
Conductor.instance.forceBPM(null);
|
||||
|
||||
if (currentChart.offsets != null)
|
||||
{
|
||||
Conductor.instrumentalOffset = currentChart.offsets.getInstrumentalOffset();
|
||||
Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset();
|
||||
}
|
||||
|
||||
Conductor.mapTimeChanges(currentChart.timeChanges);
|
||||
Conductor.update((Conductor.beatLengthMs * -5) + startTimestamp);
|
||||
Conductor.instance.mapTimeChanges(currentChart.timeChanges);
|
||||
Conductor.instance.update((Conductor.instance.beatLengthMs * -5) + startTimestamp);
|
||||
|
||||
// The song is now loaded. We can continue to initialize the play state.
|
||||
initCameras();
|
||||
|
@ -744,7 +746,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
// Reset music properly.
|
||||
|
||||
FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instrumentalOffset);
|
||||
FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instance.instrumentalOffset);
|
||||
FlxG.sound.music.pause();
|
||||
|
||||
if (!overrideMusic)
|
||||
|
@ -795,22 +797,22 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
if (isInCountdown)
|
||||
{
|
||||
Conductor.update(Conductor.songPosition + elapsed * 1000);
|
||||
if (Conductor.songPosition >= (startTimestamp)) startSong();
|
||||
Conductor.instance.update(Conductor.instance.songPosition + elapsed * 1000);
|
||||
if (Conductor.instance.songPosition >= (startTimestamp)) startSong();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Constants.EXT_SOUND == 'mp3')
|
||||
{
|
||||
Conductor.formatOffset = Constants.MP3_DELAY_MS;
|
||||
Conductor.instance.formatOffset = Constants.MP3_DELAY_MS;
|
||||
}
|
||||
else
|
||||
{
|
||||
Conductor.formatOffset = 0.0;
|
||||
Conductor.instance.formatOffset = 0.0;
|
||||
}
|
||||
|
||||
Conductor.update(); // Normal conductor update.
|
||||
Conductor.instance.update(); // Normal conductor update.
|
||||
}
|
||||
|
||||
var androidPause:Bool = false;
|
||||
|
@ -932,7 +934,11 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
#end
|
||||
|
||||
var gameOverSubState = new GameOverSubState();
|
||||
var gameOverSubState = new GameOverSubState(
|
||||
{
|
||||
isChartingMode: isChartingMode,
|
||||
transparent: persistentDraw
|
||||
});
|
||||
FlxTransitionableSubState.skipNextTransIn = true;
|
||||
FlxTransitionableSubState.skipNextTransOut = true;
|
||||
openSubState(gameOverSubState);
|
||||
|
@ -948,7 +954,7 @@ class PlayState extends MusicBeatSubState
|
|||
// TODO: Check that these work even when songPosition is less than 0.
|
||||
if (songEvents != null && songEvents.length > 0)
|
||||
{
|
||||
var songEventsToActivate:Array<SongEventData> = SongEventParser.queryEvents(songEvents, Conductor.songPosition);
|
||||
var songEventsToActivate:Array<SongEventData> = SongEventRegistry.queryEvents(songEvents, Conductor.instance.songPosition);
|
||||
|
||||
if (songEventsToActivate.length > 0)
|
||||
{
|
||||
|
@ -956,7 +962,7 @@ class PlayState extends MusicBeatSubState
|
|||
for (event in songEventsToActivate)
|
||||
{
|
||||
// If an event is trying to play, but it's over 5 seconds old, skip it.
|
||||
if (event.time - Conductor.songPosition < -5000)
|
||||
if (event.time - Conductor.instance.songPosition < -5000)
|
||||
{
|
||||
event.activated = true;
|
||||
continue;
|
||||
|
@ -967,7 +973,7 @@ class PlayState extends MusicBeatSubState
|
|||
// Calling event.cancelEvent() skips the event. Neat!
|
||||
if (!eventEvent.eventCanceled)
|
||||
{
|
||||
SongEventParser.handleEvent(event);
|
||||
SongEventRegistry.handleEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1058,7 +1064,7 @@ class PlayState extends MusicBeatSubState
|
|||
if (startTimer.finished)
|
||||
{
|
||||
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true,
|
||||
currentSongLengthMs - Conductor.songPosition);
|
||||
currentSongLengthMs - Conductor.instance.songPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1082,12 +1088,12 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause)
|
||||
{
|
||||
if (Conductor.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
|
||||
if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
|
||||
+ ' ('
|
||||
+ storyDifficultyText
|
||||
+ ')', iconRPC, true,
|
||||
currentSongLengthMs
|
||||
- Conductor.songPosition);
|
||||
- Conductor.instance.songPosition);
|
||||
else
|
||||
DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
|
||||
}
|
||||
|
@ -1160,17 +1166,17 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (!startingSong
|
||||
&& FlxG.sound.music != null
|
||||
&& (Math.abs(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset)) > 200
|
||||
|| Math.abs(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset)) > 200))
|
||||
&& (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200
|
||||
|| Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200))
|
||||
{
|
||||
trace("VOCALS NEED RESYNC");
|
||||
if (vocals != null) trace(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset));
|
||||
trace(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset));
|
||||
if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
|
||||
trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
|
||||
resyncVocals();
|
||||
}
|
||||
|
||||
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.currentStep));
|
||||
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.currentStep));
|
||||
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
|
||||
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1191,14 +1197,14 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
// Only zoom camera if we are zoomed by less than 35%.
|
||||
if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.currentBeat % cameraZoomRate == 0)
|
||||
if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.instance.currentBeat % cameraZoomRate == 0)
|
||||
{
|
||||
// Zoom camera in (1.5%)
|
||||
FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom;
|
||||
// Hud zooms double (3%)
|
||||
camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom;
|
||||
}
|
||||
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.currentBeat} % ${cameraZoomRate} == ${Conductor.currentBeat % cameraZoomRate}}');
|
||||
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
|
||||
|
||||
// That combo milestones that got spoiled that one time.
|
||||
// Comes with NEAT visual and audio effects.
|
||||
|
@ -1211,13 +1217,13 @@ class PlayState extends MusicBeatSubState
|
|||
// TODO: Re-enable combo text (how to do this without sections?).
|
||||
// if (currentSong != null)
|
||||
// {
|
||||
// shouldShowComboText = (Conductor.currentBeat % 8 == 7);
|
||||
// var daSection = .getSong()[Std.int(Conductor.currentBeat / 16)];
|
||||
// shouldShowComboText = (Conductor.instance.currentBeat % 8 == 7);
|
||||
// var daSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16)];
|
||||
// shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection);
|
||||
// shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5);
|
||||
//
|
||||
// var daNextSection = .getSong()[Std.int(Conductor.currentBeat / 16) + 1];
|
||||
// var isEndOfSong = .getSong().length < Std.int(Conductor.currentBeat / 16);
|
||||
// var daNextSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16) + 1];
|
||||
// var isEndOfSong = .getSong().length < Std.int(Conductor.instance.currentBeat / 16);
|
||||
// shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection));
|
||||
// }
|
||||
|
||||
|
@ -1230,7 +1236,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
|
||||
|
||||
new FlxTimer().start(((Conductor.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) {
|
||||
new FlxTimer().start(((Conductor.instance.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) {
|
||||
animShit.forceFinish();
|
||||
});
|
||||
}
|
||||
|
@ -1267,10 +1273,10 @@ class PlayState extends MusicBeatSubState
|
|||
if (currentStage == null) return;
|
||||
|
||||
// TODO: Add HEY! song events to Tutorial.
|
||||
if (Conductor.currentBeat % 16 == 15
|
||||
if (Conductor.instance.currentBeat % 16 == 15
|
||||
&& currentStage.getDad().characterId == 'gf'
|
||||
&& Conductor.currentBeat > 16
|
||||
&& Conductor.currentBeat < 48)
|
||||
&& Conductor.instance.currentBeat > 16
|
||||
&& Conductor.instance.currentBeat < 48)
|
||||
{
|
||||
currentStage.getBoyfriend().playAnimation('hey', true);
|
||||
currentStage.getDad().playAnimation('cheer', true);
|
||||
|
@ -1359,7 +1365,8 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
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)
|
||||
{
|
||||
|
@ -1581,7 +1588,7 @@ class PlayState extends MusicBeatSubState
|
|||
trace('Song difficulty could not be loaded.');
|
||||
}
|
||||
|
||||
// Conductor.forceBPM(currentChart.getStartingBPM());
|
||||
// Conductor.instance.forceBPM(currentChart.getStartingBPM());
|
||||
|
||||
if (!overrideMusic)
|
||||
{
|
||||
|
@ -1613,7 +1620,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
// Reset song events.
|
||||
songEvents = currentChart.getEvents();
|
||||
SongEventParser.resetEvents(songEvents);
|
||||
SongEventRegistry.resetEvents(songEvents);
|
||||
|
||||
// Reset the notes on each strumline.
|
||||
var playerNoteData:Array<SongNoteData> = [];
|
||||
|
@ -1667,7 +1674,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
isInCutscene = true;
|
||||
|
||||
currentConversation = ConversationDataParser.fetchConversation(conversationId);
|
||||
currentConversation = ConversationRegistry.instance.fetchEntry(conversationId);
|
||||
if (currentConversation == null) return;
|
||||
|
||||
currentConversation.completeCallback = onConversationComplete;
|
||||
|
@ -1712,7 +1719,7 @@ class PlayState extends MusicBeatSubState
|
|||
FlxG.sound.music.onComplete = endSong;
|
||||
// A negative instrumental offset means the song skips the first few milliseconds of the track.
|
||||
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
|
||||
FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset;
|
||||
FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
|
||||
|
||||
trace('Playing vocals...');
|
||||
add(vocals);
|
||||
|
@ -1728,7 +1735,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (startTimestamp > 0)
|
||||
{
|
||||
// FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset;
|
||||
// FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
|
||||
handleSkippedNotes();
|
||||
}
|
||||
}
|
||||
|
@ -1802,11 +1809,12 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
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.songPosition > hitWindowEnd)
|
||||
if (Conductor.instance.songPosition > hitWindowEnd)
|
||||
{
|
||||
if (note.hasMissed) continue;
|
||||
|
||||
|
@ -1816,7 +1824,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
|
||||
}
|
||||
else if (Conductor.songPosition > hitWindowCenter)
|
||||
else if (Conductor.instance.songPosition > hitWindowCenter)
|
||||
{
|
||||
if (note.hasBeenHit) continue;
|
||||
|
||||
|
@ -1837,7 +1845,7 @@ class PlayState extends MusicBeatSubState
|
|||
opponentStrumline.playNoteHoldCover(note.holdNoteSprite);
|
||||
}
|
||||
}
|
||||
else if (Conductor.songPosition > hitWindowStart)
|
||||
else if (Conductor.instance.songPosition > hitWindowStart)
|
||||
{
|
||||
if (note.hasBeenHit || note.hasMissed) continue;
|
||||
|
||||
|
@ -1870,27 +1878,43 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Potential penalty for dropping a hold note?
|
||||
// if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; }
|
||||
if (holdNote.missedNote && !holdNote.handledMiss)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Process notes on the player's side.
|
||||
for (note in playerStrumline.notes.members)
|
||||
{
|
||||
if (note == null || note.hasBeenHit) continue;
|
||||
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.songPosition > hitWindowEnd)
|
||||
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.songPosition > hitWindowStart)
|
||||
else if (Conductor.instance.songPosition > hitWindowStart)
|
||||
{
|
||||
note.tooEarly = false;
|
||||
note.mayHit = true;
|
||||
|
@ -1940,8 +1964,15 @@ class PlayState extends MusicBeatSubState
|
|||
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
|
||||
}
|
||||
|
||||
// TODO: Potential penalty for dropping a hold note?
|
||||
// if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; }
|
||||
if (holdNote.missedNote && !holdNote.handledMiss)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1957,7 +1988,7 @@ class PlayState extends MusicBeatSubState
|
|||
if (note == null || note.hasBeenHit) continue;
|
||||
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
|
||||
|
||||
if (Conductor.songPosition > hitWindowEnd)
|
||||
if (Conductor.instance.songPosition > hitWindowEnd)
|
||||
{
|
||||
// We have passed this note.
|
||||
// Flag the note for deletion without actually penalizing the player.
|
||||
|
@ -2033,8 +2064,6 @@ class PlayState extends MusicBeatSubState
|
|||
trace('Hit note! ${targetNote.noteData}');
|
||||
goodNoteHit(targetNote, input);
|
||||
|
||||
targetNote.visible = false;
|
||||
targetNote.kill();
|
||||
notesInDirection.remove(targetNote);
|
||||
|
||||
// Play the strumline animation.
|
||||
|
@ -2066,15 +2095,8 @@ class PlayState extends MusicBeatSubState
|
|||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
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);
|
||||
|
||||
playerStrumline.hitNote(note);
|
||||
|
||||
if (note.isHoldNote && note.holdNoteSprite != null)
|
||||
{
|
||||
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
|
||||
|
@ -2090,8 +2112,6 @@ class PlayState extends MusicBeatSubState
|
|||
function onNoteMiss(note:NoteSprite):Void
|
||||
{
|
||||
// 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);
|
||||
dispatchEvent(event);
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
|
@ -2121,7 +2141,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: indices[i],
|
||||
l: 20
|
||||
});
|
||||
|
@ -2131,7 +2151,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: -1,
|
||||
l: 20
|
||||
});
|
||||
|
@ -2139,8 +2159,11 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
vocals.playerVolume = 0;
|
||||
|
||||
Highscore.tallies.missed++;
|
||||
|
||||
if (Highscore.tallies.combo != 0)
|
||||
{
|
||||
// Break the combo.
|
||||
Highscore.tallies.combo = comboPopUps.displayCombo(0);
|
||||
}
|
||||
|
||||
|
@ -2192,7 +2215,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: indices[i],
|
||||
l: 20
|
||||
});
|
||||
|
@ -2276,39 +2299,63 @@ class PlayState extends MusicBeatSubState
|
|||
vocals.playerVolume = 1;
|
||||
|
||||
// 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('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
|
||||
// trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
|
||||
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.
|
||||
// Round inward (trim remainder) for consistency.
|
||||
var noteDiff:Int = Std.int(Conductor.songPosition - daNote.noteData.time - inputLatencyMs);
|
||||
var noteDiff:Int = Std.int(Conductor.instance.songPosition - daNote.noteData.time - inputLatencyMs);
|
||||
|
||||
var score = Scoring.scoreNote(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)
|
||||
{
|
||||
case 'killer':
|
||||
Highscore.tallies.killer += 1;
|
||||
health += Constants.HEALTH_KILLER_BONUS;
|
||||
case 'sick':
|
||||
Highscore.tallies.sick += 1;
|
||||
health += Constants.HEALTH_SICK_BONUS;
|
||||
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
|
||||
case 'good':
|
||||
Highscore.tallies.good += 1;
|
||||
health += Constants.HEALTH_GOOD_BONUS;
|
||||
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
|
||||
case 'bad':
|
||||
Highscore.tallies.bad += 1;
|
||||
health += Constants.HEALTH_BAD_BONUS;
|
||||
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
|
||||
case 'shit':
|
||||
Highscore.tallies.shit += 1;
|
||||
health += Constants.HEALTH_SHIT_BONUS;
|
||||
case 'miss':
|
||||
Highscore.tallies.missed += 1;
|
||||
health -= Constants.HEALTH_MISS_PENALTY;
|
||||
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
@ -2336,7 +2383,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: indices[i],
|
||||
l: 20
|
||||
});
|
||||
|
@ -2346,7 +2393,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: -1,
|
||||
l: 20
|
||||
});
|
||||
|
@ -2444,7 +2491,6 @@ class PlayState extends MusicBeatSubState
|
|||
score: songScore,
|
||||
tallies:
|
||||
{
|
||||
killer: Highscore.tallies.killer,
|
||||
sick: Highscore.tallies.sick,
|
||||
good: Highscore.tallies.good,
|
||||
bad: Highscore.tallies.bad,
|
||||
|
@ -2495,7 +2541,6 @@ class PlayState extends MusicBeatSubState
|
|||
tallies:
|
||||
{
|
||||
// TODO: Sum up the values for the whole level!
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -2745,15 +2790,15 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
FlxG.sound.music.pause();
|
||||
|
||||
var targetTimeSteps:Float = Conductor.currentStepTime + (Conductor.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections);
|
||||
var targetTimeMs:Float = Conductor.getStepTimeInMs(targetTimeSteps);
|
||||
var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections);
|
||||
var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps);
|
||||
|
||||
FlxG.sound.music.time = targetTimeMs;
|
||||
|
||||
handleSkippedNotes();
|
||||
// regenNoteData(FlxG.sound.music.time);
|
||||
|
||||
Conductor.update(FlxG.sound.music.time);
|
||||
Conductor.instance.update(FlxG.sound.music.time);
|
||||
|
||||
resyncVocals();
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import flixel.math.FlxMath;
|
|||
import flixel.math.FlxPoint.FlxCallbackPoint;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.math.FlxRect;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxDestroyUtil;
|
||||
|
@ -621,7 +622,7 @@ class AnimateAtlasCharacter extends BaseCharacter
|
|||
* This functionality isn't supported in SpriteGroup
|
||||
* @return this sprite group
|
||||
*/
|
||||
public override function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite
|
||||
public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite
|
||||
{
|
||||
#if FLX_DEBUG
|
||||
throw "This function is not supported in FlxSpriteGroup";
|
||||
|
|
|
@ -367,7 +367,7 @@ class BaseCharacter extends Bopper
|
|||
// This lets you add frames to the end of the sing animation to ease back into the idle!
|
||||
|
||||
holdTimer += event.elapsed;
|
||||
var singTimeSec:Float = singTimeSec * (Conductor.beatLengthMs * 0.001); // x beats, to ms.
|
||||
var singTimeSec:Float = singTimeSec * (Conductor.instance.beatLengthMs * 0.001); // x beats, to ms.
|
||||
|
||||
if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ class CharacterDataParser
|
|||
{
|
||||
// Clear any stages that are cached if there were any.
|
||||
clearCharacterCache();
|
||||
trace('Loading character cache...');
|
||||
trace('[CHARACTER] Parsing all entries...');
|
||||
|
||||
//
|
||||
// UNSCRIPTED CHARACTERS
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
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
|
||||
* 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
|
||||
* 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).
|
||||
*
|
||||
* BaseCharacter has game logic, SparrowCharacter has only rendering logic.
|
||||
* BaseCharacter has game logic, MultiSparrowCharacter has only rendering logic.
|
||||
* 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
|
||||
{
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
super(id, CharacterRenderType.MultiSparrow);
|
||||
|
@ -51,7 +34,7 @@ class MultiSparrowCharacter extends BaseCharacter
|
|||
|
||||
function buildSprites():Void
|
||||
{
|
||||
buildSpritesheets();
|
||||
buildSpritesheet();
|
||||
buildAnimations();
|
||||
|
||||
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.
|
||||
// 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];
|
||||
var assetList = [];
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.assetPath != null && !assetList.contains(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)
|
||||
{
|
||||
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 (texture == null)
|
||||
if (subTexture == null)
|
||||
{
|
||||
trace('Multi-Sparrow atlas could not load texture: ${asset}');
|
||||
trace('Multi-Sparrow atlas could not load subtexture: ${asset}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Adding multi-sparrow atlas: ${asset}');
|
||||
texture.parent.destroyOnNoUse = false;
|
||||
members.set(asset, texture);
|
||||
trace('Concatenating multi-sparrow atlas: ${asset}');
|
||||
subTexture.parent.destroyOnNoUse = false;
|
||||
}
|
||||
|
||||
texture.addAtlas(subTexture);
|
||||
}
|
||||
|
||||
// Use the default frame collection to start.
|
||||
loadFramesByAssetPath(_data.assetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}');
|
||||
}
|
||||
this.frames = texture;
|
||||
this.setScale(_data.scale);
|
||||
}
|
||||
|
||||
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?
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
loadFramesByAnimName(anim.name);
|
||||
FlxAnimationUtil.addAtlasAnimation(this, anim);
|
||||
|
||||
if (anim.offsets == null)
|
||||
|
@ -187,37 +123,6 @@ class MultiSparrowCharacter extends BaseCharacter
|
|||
// unless we're forcing a new animation.
|
||||
if (!this.canPlayOtherAnims && !ignoreOther) return;
|
||||
|
||||
loadFramesByAnimName(name);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class ComboMilestone extends FlxTypedSpriteGroup<FlxSprite>
|
|||
{
|
||||
if (onScreenTime < 0.9)
|
||||
{
|
||||
new FlxTimer().start((Conductor.beatLengthMs / 1000) * 0.25, function(tmr) {
|
||||
new FlxTimer().start((Conductor.instance.beatLengthMs / 1000) * 0.25, function(tmr) {
|
||||
forceFinish();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
|||
remove(rating, true);
|
||||
rating.destroy();
|
||||
},
|
||||
startDelay: Conductor.beatLengthMs * 0.001
|
||||
startDelay: Conductor.instance.beatLengthMs * 0.001
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
|||
remove(comboSpr, true);
|
||||
comboSpr.destroy();
|
||||
},
|
||||
startDelay: Conductor.beatLengthMs * 0.001
|
||||
startDelay: Conductor.instance.beatLengthMs * 0.001
|
||||
});
|
||||
|
||||
var seperatedScore:Array<Int> = [];
|
||||
|
@ -157,7 +157,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
|||
remove(numScore, true);
|
||||
numScore.destroy();
|
||||
},
|
||||
startDelay: Conductor.beatLengthMs * 0.002
|
||||
startDelay: Conductor.instance.beatLengthMs * 0.002
|
||||
});
|
||||
|
||||
daLoop++;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import funkin.data.IRegistryEntry;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.sound.FlxSound;
|
||||
|
@ -13,27 +15,30 @@ import funkin.modding.IScriptedClass.IEventHandler;
|
|||
import funkin.play.cutscene.dialogue.DialogueBox;
|
||||
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData;
|
||||
import flixel.addons.display.FlxPieDial;
|
||||
import funkin.data.dialogue.ConversationData;
|
||||
import funkin.data.dialogue.ConversationData.DialogueEntryData;
|
||||
import funkin.data.dialogue.ConversationRegistry;
|
||||
import funkin.data.dialogue.SpeakerData;
|
||||
import funkin.data.dialogue.SpeakerRegistry;
|
||||
import funkin.data.dialogue.DialogueBoxData;
|
||||
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||
|
||||
/**
|
||||
* A high-level handler for dialogue.
|
||||
*
|
||||
* This shit is great for modders but it's pretty elaborate for how much it'll actually be used, lolol. -Eric
|
||||
*/
|
||||
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
||||
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<ConversationData>
|
||||
{
|
||||
static final CONVERSATION_SKIP_TIMER:Float = 1.5;
|
||||
|
||||
var skipHeldTimer:Float = 0.0;
|
||||
|
||||
/**
|
||||
* DATA
|
||||
* The ID of the conversation.
|
||||
*/
|
||||
/**
|
||||
* The ID of the associated dialogue.
|
||||
*/
|
||||
public final conversationId:String;
|
||||
public final id:String;
|
||||
|
||||
/**
|
||||
* The current state of the conversation.
|
||||
|
@ -41,9 +46,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
var state:ConversationState = ConversationState.Start;
|
||||
|
||||
/**
|
||||
* The data for the associated dialogue.
|
||||
* Conversation data as parsed from the JSON file.
|
||||
*/
|
||||
var conversationData:ConversationData;
|
||||
public final _data:ConversationData;
|
||||
|
||||
/**
|
||||
* The current entry in the dialogue.
|
||||
|
@ -54,7 +59,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function get_currentDialogueEntryCount():Int
|
||||
{
|
||||
return conversationData.dialogue.length;
|
||||
return _data.dialogue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,10 +78,10 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function get_currentDialogueEntryData():DialogueEntryData
|
||||
{
|
||||
if (conversationData == null || conversationData.dialogue == null) return null;
|
||||
if (currentDialogueEntry < 0 || currentDialogueEntry >= conversationData.dialogue.length) return null;
|
||||
if (_data == null || _data.dialogue == null) return null;
|
||||
if (currentDialogueEntry < 0 || currentDialogueEntry >= _data.dialogue.length) return null;
|
||||
|
||||
return conversationData.dialogue[currentDialogueEntry];
|
||||
return _data.dialogue[currentDialogueEntry];
|
||||
}
|
||||
|
||||
var currentDialogueLineString(get, never):String;
|
||||
|
@ -94,7 +99,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
/**
|
||||
* GRAPHICS
|
||||
*/
|
||||
var backdrop:FlxSprite;
|
||||
var backdrop:FunkinSprite;
|
||||
|
||||
var currentSpeaker:Speaker;
|
||||
|
||||
|
@ -102,14 +107,17 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
var skipTimer:FlxPieDial;
|
||||
|
||||
public function new(conversationId:String)
|
||||
public function new(id:String)
|
||||
{
|
||||
super();
|
||||
|
||||
this.conversationId = conversationId;
|
||||
this.conversationData = ConversationDataParser.parseConversationData(this.conversationId);
|
||||
this.id = id;
|
||||
this._data = _fetchData(id);
|
||||
|
||||
if (conversationData == null) throw 'Could not load conversation data for conversation ID "$conversationId"';
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not parse conversation data for id: $id';
|
||||
}
|
||||
}
|
||||
|
||||
public function onCreate(event:ScriptEvent):Void
|
||||
|
@ -125,14 +133,14 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function setupMusic():Void
|
||||
{
|
||||
if (conversationData.music == null) return;
|
||||
if (_data.music == null) return;
|
||||
|
||||
music = new FlxSound().loadEmbedded(Paths.music(conversationData.music.asset), true, true);
|
||||
music = new FlxSound().loadEmbedded(Paths.music(_data.music.asset), true, true);
|
||||
music.volume = 0;
|
||||
|
||||
if (conversationData.music.fadeTime > 0.0)
|
||||
if (_data.music.fadeTime > 0.0)
|
||||
{
|
||||
FlxTween.tween(music, {volume: 1.0}, conversationData.music.fadeTime, {ease: FlxEase.linear});
|
||||
FlxTween.tween(music, {volume: 1.0}, _data.music.fadeTime, {ease: FlxEase.linear});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -145,19 +153,20 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function setupBackdrop():Void
|
||||
{
|
||||
backdrop = new FlxSprite(0, 0);
|
||||
backdrop = new FunkinSprite(0, 0);
|
||||
|
||||
if (conversationData.backdrop == null) return;
|
||||
if (_data.backdrop == null) return;
|
||||
|
||||
// Play intro
|
||||
switch (conversationData?.backdrop.type)
|
||||
switch (_data.backdrop)
|
||||
{
|
||||
case SOLID:
|
||||
backdrop.makeGraphic(Std.int(FlxG.width), Std.int(FlxG.height), FlxColor.fromString(conversationData.backdrop.data.color));
|
||||
if (conversationData.backdrop.data.fadeTime > 0.0)
|
||||
case SOLID(backdropData):
|
||||
var targetColor:FlxColor = FlxColor.fromString(backdropData.color);
|
||||
backdrop.makeSolidColor(Std.int(FlxG.width), Std.int(FlxG.height), targetColor);
|
||||
if (backdropData.fadeTime > 0.0)
|
||||
{
|
||||
backdrop.alpha = 0.0;
|
||||
FlxTween.tween(backdrop, {alpha: 1.0}, conversationData.backdrop.data.fadeTime, {ease: FlxEase.linear});
|
||||
FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: FlxEase.linear});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -190,9 +199,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
var nextSpeakerId:String = currentDialogueEntryData.speaker;
|
||||
|
||||
// Skip the next steps if the current speaker is already displayed.
|
||||
if (currentSpeaker != null && nextSpeakerId == currentSpeaker.speakerId) return;
|
||||
if (currentSpeaker != null && nextSpeakerId == currentSpeaker.id) return;
|
||||
|
||||
var nextSpeaker:Speaker = SpeakerDataParser.fetchSpeaker(nextSpeakerId);
|
||||
var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
|
||||
|
||||
if (currentSpeaker != null)
|
||||
{
|
||||
|
@ -241,7 +250,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
var nextDialogueBoxId:String = currentDialogueEntryData?.box;
|
||||
|
||||
// Skip the next steps if the current speaker is already displayed.
|
||||
if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.dialogueBoxId) return;
|
||||
if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.id) return;
|
||||
|
||||
if (currentDialogueBox != null)
|
||||
{
|
||||
|
@ -250,7 +259,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
currentDialogueBox = null;
|
||||
}
|
||||
|
||||
var nextDialogueBox:DialogueBox = DialogueBoxDataParser.fetchDialogueBox(nextDialogueBoxId);
|
||||
var nextDialogueBox:DialogueBox = DialogueBoxRegistry.instance.fetchEntry(nextDialogueBoxId);
|
||||
|
||||
if (nextDialogueBox == null)
|
||||
{
|
||||
|
@ -378,20 +387,18 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
public function startOutro():Void
|
||||
{
|
||||
switch (conversationData?.outro?.type)
|
||||
switch (_data?.outro)
|
||||
{
|
||||
case FADE:
|
||||
var fadeTime:Float = conversationData?.outro.data.fadeTime ?? 1.0;
|
||||
|
||||
outroTween = FlxTween.tween(this, {alpha: 0.0}, fadeTime,
|
||||
case FADE(outroData):
|
||||
outroTween = FlxTween.tween(this, {alpha: 0.0}, outroData.fadeTime,
|
||||
{
|
||||
type: ONESHOT, // holy shit like the game no way
|
||||
startDelay: 0,
|
||||
onComplete: (_) -> endOutro(),
|
||||
});
|
||||
|
||||
FlxTween.tween(this.music, {volume: 0.0}, fadeTime);
|
||||
case NONE:
|
||||
FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);
|
||||
case NONE(_):
|
||||
// Immediately clean up.
|
||||
endOutro();
|
||||
default:
|
||||
|
@ -400,7 +407,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
}
|
||||
}
|
||||
|
||||
public var completeCallback:Void->Void;
|
||||
public var completeCallback:() -> Void;
|
||||
|
||||
public function endOutro():Void
|
||||
{
|
||||
|
@ -596,7 +603,12 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
public override function toString():String
|
||||
{
|
||||
return 'Conversation($conversationId)';
|
||||
return 'Conversation($id)';
|
||||
}
|
||||
|
||||
static function _fetchData(id:String):Null<ConversationData>
|
||||
{
|
||||
return ConversationRegistry.instance.parseEntryDataWithMigration(id, ConversationRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,240 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import funkin.util.SerializerUtil;
|
||||
|
||||
/**
|
||||
* Data about a conversation.
|
||||
* Includes what speakers are in the conversation, and what phrases they say.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class ConversationData
|
||||
{
|
||||
public var version:String;
|
||||
public var backdrop:BackdropData;
|
||||
public var outro:OutroData;
|
||||
public var music:MusicData;
|
||||
public var dialogue:Array<DialogueEntryData>;
|
||||
|
||||
public function new(version:String, backdrop:BackdropData, outro:OutroData, music:MusicData, dialogue:Array<DialogueEntryData>)
|
||||
{
|
||||
this.version = version;
|
||||
this.backdrop = backdrop;
|
||||
this.outro = outro;
|
||||
this.music = music;
|
||||
this.dialogue = dialogue;
|
||||
}
|
||||
|
||||
public static function fromString(i:String):ConversationData
|
||||
{
|
||||
if (i == null || i == '') return null;
|
||||
var data:
|
||||
{
|
||||
version:String,
|
||||
backdrop:Dynamic, // TODO: tink.Json doesn't like when these are typed
|
||||
?outro:Dynamic, // TODO: tink.Json doesn't like when these are typed
|
||||
?music:Dynamic, // TODO: tink.Json doesn't like when these are typed
|
||||
dialogue:Array<Dynamic> // TODO: tink.Json doesn't like when these are typed
|
||||
} = tink.Json.parse(i);
|
||||
return fromJson(data);
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):ConversationData
|
||||
{
|
||||
// TODO: Check version and perform migrations if necessary.
|
||||
if (j == null) return null;
|
||||
return new ConversationData(j.version, BackdropData.fromJson(j.backdrop), OutroData.fromJson(j.outro), MusicData.fromJson(j.music),
|
||||
j.dialogue.map(d -> DialogueEntryData.fromJson(d)));
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
version: this.version,
|
||||
backdrop: this.backdrop.toJson(),
|
||||
dialogue: this.dialogue.map(d -> d.toJson())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about a single dialogue entry.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class DialogueEntryData
|
||||
{
|
||||
/**
|
||||
* The speaker who says this phrase.
|
||||
*/
|
||||
public var speaker:String;
|
||||
|
||||
/**
|
||||
* The animation the speaker will play.
|
||||
*/
|
||||
public var speakerAnimation:String;
|
||||
|
||||
/**
|
||||
* The text box that will appear.
|
||||
*/
|
||||
public var box:String;
|
||||
|
||||
/**
|
||||
* The animation the dialogue box will play.
|
||||
*/
|
||||
public var boxAnimation:String;
|
||||
|
||||
/**
|
||||
* The lines of text that will appear in the text box.
|
||||
*/
|
||||
public var text:Array<String>;
|
||||
|
||||
/**
|
||||
* The relative speed at which the text will scroll.
|
||||
* @default 1.0
|
||||
*/
|
||||
public var speed:Float = 1.0;
|
||||
|
||||
public function new(speaker:String, speakerAnimation:String, box:String, boxAnimation:String, text:Array<String>, speed:Float = null)
|
||||
{
|
||||
this.speaker = speaker;
|
||||
this.speakerAnimation = speakerAnimation;
|
||||
this.box = box;
|
||||
this.boxAnimation = boxAnimation;
|
||||
this.text = text;
|
||||
if (speed != null) this.speed = speed;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):DialogueEntryData
|
||||
{
|
||||
if (j == null) return null;
|
||||
return new DialogueEntryData(j.speaker, j.speakerAnimation, j.box, j.boxAnimation, j.text, j.speed);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
var result:Dynamic =
|
||||
{
|
||||
speaker: this.speaker,
|
||||
speakerAnimation: this.speakerAnimation,
|
||||
box: this.box,
|
||||
boxAnimation: this.boxAnimation,
|
||||
text: this.text,
|
||||
};
|
||||
|
||||
if (this.speed != 1.0) result.speed = this.speed;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about a backdrop.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.BackdropData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class BackdropData
|
||||
{
|
||||
public var type:BackdropType;
|
||||
public var data:Dynamic;
|
||||
|
||||
public function new(typeStr:String, data:Dynamic)
|
||||
{
|
||||
this.type = typeStr;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):BackdropData
|
||||
{
|
||||
if (j == null) return null;
|
||||
return new BackdropData(j.type, j.data);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
type: this.type,
|
||||
data: this.data
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract BackdropType(String) from String to String
|
||||
{
|
||||
public var SOLID:BackdropType = 'solid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about a music track.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.MusicData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class MusicData
|
||||
{
|
||||
public var asset:String;
|
||||
|
||||
public var fadeTime:Float;
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var looped:Bool;
|
||||
|
||||
public function new(asset:String, looped:Bool, fadeTime:Float = 0.0)
|
||||
{
|
||||
this.asset = asset;
|
||||
this.looped = looped;
|
||||
this.fadeTime = fadeTime;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):MusicData
|
||||
{
|
||||
if (j == null) return null;
|
||||
return new MusicData(j.asset, j.looped, j.fadeTime);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
asset: this.asset,
|
||||
looped: this.looped,
|
||||
fadeTime: this.fadeTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about an outro.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.OutroData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class OutroData
|
||||
{
|
||||
public var type:OutroType;
|
||||
public var data:Dynamic;
|
||||
|
||||
public function new(?typeStr:String, data:Dynamic)
|
||||
{
|
||||
this.type = typeStr ?? OutroType.NONE;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):OutroData
|
||||
{
|
||||
if (j == null) return null;
|
||||
return new OutroData(j.type, j.data);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
type: this.type,
|
||||
data: this.data
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract OutroType(String) from String to String
|
||||
{
|
||||
public var NONE:OutroType = 'none';
|
||||
public var FADE:OutroType = 'fade';
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import openfl.Assets;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.play.cutscene.dialogue.ScriptedConversation;
|
||||
|
||||
/**
|
||||
* Contains utilities for loading and parsing conversation data.
|
||||
* TODO: Refactor to use the json2object + BaseRegistry system that actually validates things for you.
|
||||
*/
|
||||
class ConversationDataParser
|
||||
{
|
||||
public static final CONVERSATION_DATA_VERSION:String = '1.0.0';
|
||||
public static final CONVERSATION_DATA_VERSION_RULE:String = '1.0.x';
|
||||
|
||||
static final conversationCache:Map<String, Conversation> = new Map<String, Conversation>();
|
||||
static final conversationScriptedClass:Map<String, String> = new Map<String, String>();
|
||||
|
||||
static final DEFAULT_CONVERSATION_ID:String = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* Parses and preloads the game's conversation data and scripts when the game starts.
|
||||
*
|
||||
* If you want to force conversations to be reloaded, you can just call this function again.
|
||||
*/
|
||||
public static function loadConversationCache():Void
|
||||
{
|
||||
clearConversationCache();
|
||||
trace('Loading dialogue conversation cache...');
|
||||
|
||||
//
|
||||
// SCRIPTED CONVERSATIONS
|
||||
//
|
||||
var scriptedConversationClassNames:Array<String> = ScriptedConversation.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedConversationClassNames.length} scripted conversations...');
|
||||
for (conversationCls in scriptedConversationClassNames)
|
||||
{
|
||||
var conversation:Conversation = ScriptedConversation.init(conversationCls, DEFAULT_CONVERSATION_ID);
|
||||
if (conversation != null)
|
||||
{
|
||||
trace(' Loaded scripted conversation: ${conversationCls}');
|
||||
// Disable the rendering logic for conversation until it's loaded.
|
||||
// Note that kill() =/= destroy()
|
||||
conversation.kill();
|
||||
|
||||
// Then store it.
|
||||
conversationCache.set(conversation.conversationId, conversation);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted conversation class: ${conversationCls}');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UNSCRIPTED CONVERSATIONS
|
||||
//
|
||||
// Scripts refers to code here, not the actual dialogue.
|
||||
var conversationIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/conversations/');
|
||||
// Filter out conversations that are scripted.
|
||||
var unscriptedConversationIds:Array<String> = conversationIdList.filter(function(conversationId:String):Bool {
|
||||
return !conversationCache.exists(conversationId);
|
||||
});
|
||||
trace(' Fetching data for ${unscriptedConversationIds.length} conversations...');
|
||||
for (conversationId in unscriptedConversationIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var conversation:Conversation = new Conversation(conversationId);
|
||||
// Say something offensive to kill the conversation.
|
||||
// We will revive it later.
|
||||
conversation.kill();
|
||||
if (conversation != null)
|
||||
{
|
||||
trace(' Loaded conversation data: ${conversation.conversationId}');
|
||||
conversationCache.set(conversation.conversationId, conversation);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data for a conversation and returns a Conversation instance,
|
||||
* ready to be displayed.
|
||||
* @param conversationId The ID of the conversation to fetch.
|
||||
* @return The conversation instance, or null if the conversation was not found.
|
||||
*/
|
||||
public static function fetchConversation(conversationId:String):Null<Conversation>
|
||||
{
|
||||
if (conversationId != null && conversationId != '' && conversationCache.exists(conversationId))
|
||||
{
|
||||
trace('Successfully fetched conversation: ${conversationId}');
|
||||
var conversation:Conversation = conversationCache.get(conversationId);
|
||||
// ...ANYway...
|
||||
conversation.revive();
|
||||
return conversation;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Failed to fetch conversation, not found in cache: ${conversationId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function clearConversationCache():Void
|
||||
{
|
||||
if (conversationCache != null)
|
||||
{
|
||||
for (conversation in conversationCache)
|
||||
{
|
||||
conversation.destroy();
|
||||
}
|
||||
conversationCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static function listConversationIds():Array<String>
|
||||
{
|
||||
return conversationCache.keys().array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a conversation's JSON file, parse its data, and return it.
|
||||
*
|
||||
* @param conversationId The conversation to load.
|
||||
* @return The conversation data, or null if validation failed.
|
||||
*/
|
||||
public static function parseConversationData(conversationId:String):Null<ConversationData>
|
||||
{
|
||||
trace('Parsing conversation data: ${conversationId}');
|
||||
var rawJson:String = loadConversationFile(conversationId);
|
||||
|
||||
try
|
||||
{
|
||||
var conversationData:ConversationData = ConversationData.fromString(rawJson);
|
||||
return conversationData;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace('Failed to parse conversation ($conversationId).');
|
||||
trace(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function loadConversationFile(conversationPath:String):String
|
||||
{
|
||||
var conversationFilePath:String = Paths.json('dialogue/conversations/${conversationPath}');
|
||||
var rawJson:String = Assets.getText(conversationFilePath).trim();
|
||||
|
||||
while (!rawJson.endsWith('}') && rawJson.length > 0)
|
||||
{
|
||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||
}
|
||||
|
||||
return rawJson;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import funkin.data.IRegistryEntry;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import flixel.text.FlxText;
|
||||
|
@ -9,18 +10,21 @@ import funkin.util.assets.FlxAnimationUtil;
|
|||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.data.dialogue.DialogueBoxData;
|
||||
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||
|
||||
class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
||||
class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<DialogueBoxData>
|
||||
{
|
||||
public final dialogueBoxId:String;
|
||||
public final id:String;
|
||||
|
||||
public var dialogueBoxName(get, never):String;
|
||||
|
||||
function get_dialogueBoxName():String
|
||||
{
|
||||
return boxData?.name ?? 'UNKNOWN';
|
||||
return _data.name ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
var boxData:DialogueBoxData;
|
||||
public final _data:DialogueBoxData;
|
||||
|
||||
/**
|
||||
* Offset the speaker's sprite by this much when playing each animation.
|
||||
|
@ -88,13 +92,16 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
return this.speed;
|
||||
}
|
||||
|
||||
public function new(dialogueBoxId:String)
|
||||
public function new(id:String)
|
||||
{
|
||||
super();
|
||||
this.dialogueBoxId = dialogueBoxId;
|
||||
this.boxData = DialogueBoxDataParser.parseDialogueBoxData(this.dialogueBoxId);
|
||||
this.id = id;
|
||||
this._data = _fetchData(id);
|
||||
|
||||
if (boxData == null) throw 'Could not load dialogue box data for box ID "$dialogueBoxId"';
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not parse dialogue box data for id: $id';
|
||||
}
|
||||
}
|
||||
|
||||
public function onCreate(event:ScriptEvent):Void
|
||||
|
@ -115,18 +122,18 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function loadSpritesheet():Void
|
||||
{
|
||||
trace('[DIALOGUE BOX] Loading spritesheet ${boxData.assetPath} for ${dialogueBoxId}');
|
||||
trace('[DIALOGUE BOX] Loading spritesheet ${_data.assetPath} for ${id}');
|
||||
|
||||
var tex:FlxFramesCollection = Paths.getSparrowAtlas(boxData.assetPath);
|
||||
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
|
||||
if (tex == null)
|
||||
{
|
||||
trace('Could not load Sparrow sprite: ${boxData.assetPath}');
|
||||
trace('Could not load Sparrow sprite: ${_data.assetPath}');
|
||||
return;
|
||||
}
|
||||
|
||||
this.boxSprite.frames = tex;
|
||||
|
||||
if (boxData.isPixel)
|
||||
if (_data.isPixel)
|
||||
{
|
||||
this.boxSprite.antialiasing = false;
|
||||
}
|
||||
|
@ -135,9 +142,10 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
this.boxSprite.antialiasing = true;
|
||||
}
|
||||
|
||||
this.flipX = boxData.flipX;
|
||||
this.globalOffsets = boxData.offsets;
|
||||
this.setScale(boxData.scale);
|
||||
this.flipX = _data.flipX;
|
||||
this.flipY = _data.flipY;
|
||||
this.globalOffsets = _data.offsets;
|
||||
this.setScale(_data.scale);
|
||||
}
|
||||
|
||||
public function setText(newText:String):Void
|
||||
|
@ -184,11 +192,11 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function loadAnimations():Void
|
||||
{
|
||||
trace('[DIALOGUE BOX] Loading ${boxData.animations.length} animations for ${dialogueBoxId}');
|
||||
trace('[DIALOGUE BOX] Loading ${_data.animations.length} animations for ${id}');
|
||||
|
||||
FlxAnimationUtil.addAtlasAnimations(this.boxSprite, boxData.animations);
|
||||
FlxAnimationUtil.addAtlasAnimations(this.boxSprite, _data.animations);
|
||||
|
||||
for (anim in boxData.animations)
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.offsets == null)
|
||||
{
|
||||
|
@ -201,7 +209,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
}
|
||||
|
||||
var animNames:Array<String> = this.boxSprite?.animation?.getNameList() ?? [];
|
||||
trace('[DIALOGUE BOX] Successfully loaded ${animNames.length} animations for ${dialogueBoxId}');
|
||||
trace('[DIALOGUE BOX] Successfully loaded ${animNames.length} animations for ${id}');
|
||||
|
||||
boxSprite.animation.callback = this.onAnimationFrame;
|
||||
boxSprite.animation.finishCallback = this.onAnimationFinished;
|
||||
|
@ -234,16 +242,16 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
function loadText():Void
|
||||
{
|
||||
textDisplay = new FlxTypeText(0, 0, 300, '', 32);
|
||||
textDisplay.fieldWidth = boxData.text.width;
|
||||
textDisplay.setFormat('Pixel Arial 11 Bold', boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW,
|
||||
FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false);
|
||||
textDisplay.borderSize = boxData.text.shadowWidth ?? 2;
|
||||
textDisplay.fieldWidth = _data.text.width;
|
||||
textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW,
|
||||
FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false);
|
||||
textDisplay.borderSize = _data.text.shadowWidth ?? 2;
|
||||
textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
|
||||
|
||||
textDisplay.completeCallback = onTypingComplete;
|
||||
|
||||
textDisplay.x += boxData.text.offsets[0];
|
||||
textDisplay.y += boxData.text.offsets[1];
|
||||
textDisplay.x += _data.text.offsets[0];
|
||||
textDisplay.y += _data.text.offsets[1];
|
||||
|
||||
add(textDisplay);
|
||||
}
|
||||
|
@ -374,4 +382,14 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
}
|
||||
|
||||
public function onScriptEvent(event:ScriptEvent):Void {}
|
||||
|
||||
public override function toString():String
|
||||
{
|
||||
return 'DialogueBox($id)';
|
||||
}
|
||||
|
||||
static function _fetchData(id:String):Null<DialogueBoxData>
|
||||
{
|
||||
return DialogueBoxRegistry.instance.parseEntryDataWithMigration(id, DialogueBoxRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.util.SerializerUtil;
|
||||
|
||||
/**
|
||||
* Data about a text box.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class DialogueBoxData
|
||||
{
|
||||
public var version:String;
|
||||
public var name:String;
|
||||
public var assetPath:String;
|
||||
public var flipX:Bool;
|
||||
public var flipY:Bool;
|
||||
public var isPixel:Bool;
|
||||
public var offsets:Array<Float>;
|
||||
public var text:DialogueBoxTextData;
|
||||
public var scale:Float;
|
||||
public var animations:Array<AnimationData>;
|
||||
|
||||
public function new(version:String, name:String, assetPath:String, flipX:Bool = false, flipY:Bool = false, isPixel:Bool = false, offsets:Null<Array<Float>>,
|
||||
text:DialogueBoxTextData, scale:Float = 1.0, animations:Array<AnimationData>)
|
||||
{
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.assetPath = assetPath;
|
||||
this.flipX = flipX;
|
||||
this.flipY = flipY;
|
||||
this.isPixel = isPixel;
|
||||
this.offsets = offsets ?? [0, 0];
|
||||
this.text = text;
|
||||
this.scale = scale;
|
||||
this.animations = animations;
|
||||
}
|
||||
|
||||
public static function fromString(i:String):DialogueBoxData
|
||||
{
|
||||
if (i == null || i == '') return null;
|
||||
var data:
|
||||
{
|
||||
version:String,
|
||||
name:String,
|
||||
assetPath:String,
|
||||
flipX:Bool,
|
||||
flipY:Bool,
|
||||
isPixel:Bool,
|
||||
?offsets:Array<Float>,
|
||||
text:Dynamic,
|
||||
scale:Float,
|
||||
animations:Array<AnimationData>
|
||||
} = tink.Json.parse(i);
|
||||
return fromJson(data);
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):DialogueBoxData
|
||||
{
|
||||
// TODO: Check version and perform migrations if necessary.
|
||||
if (j == null) return null;
|
||||
return new DialogueBoxData(j.version, j.name, j.assetPath, j.flipX, j.flipY, j.isPixel, j.offsets, DialogueBoxTextData.fromJson(j.text), j.scale,
|
||||
j.animations);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
version: this.version,
|
||||
name: this.name,
|
||||
assetPath: this.assetPath,
|
||||
flipX: this.flipX,
|
||||
flipY: this.flipY,
|
||||
isPixel: this.isPixel,
|
||||
offsets: this.offsets,
|
||||
scale: this.scale,
|
||||
animations: this.animations
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about text in a text box.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxTextData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class DialogueBoxTextData
|
||||
{
|
||||
public var offsets:Array<Float>;
|
||||
public var width:Int;
|
||||
public var size:Int;
|
||||
public var color:String;
|
||||
public var shadowColor:Null<String>;
|
||||
public var shadowWidth:Null<Int>;
|
||||
|
||||
public function new(offsets:Null<Array<Float>>, ?width:Int, ?size:Int, color:String, ?shadowColor:String, shadowWidth:Null<Int>)
|
||||
{
|
||||
this.offsets = offsets ?? [0, 0];
|
||||
this.width = width ?? 300;
|
||||
this.size = size ?? 32;
|
||||
this.color = color;
|
||||
this.shadowColor = shadowColor;
|
||||
this.shadowWidth = shadowWidth;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):DialogueBoxTextData
|
||||
{
|
||||
// TODO: Check version and perform migrations if necessary.
|
||||
if (j == null) return null;
|
||||
return new DialogueBoxTextData(j.offsets, j.width, j.size, j.color, j.shadowColor, j.shadowWidth);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
offsets: this.offsets,
|
||||
width: this.width,
|
||||
size: this.size,
|
||||
color: this.color,
|
||||
shadowColor: this.shadowColor,
|
||||
shadowWidth: this.shadowWidth,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import openfl.Assets;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.play.cutscene.dialogue.DialogueBox;
|
||||
import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
|
||||
|
||||
/**
|
||||
* Contains utilities for loading and parsing dialogueBox data.
|
||||
*/
|
||||
class DialogueBoxDataParser
|
||||
{
|
||||
public static final DIALOGUE_BOX_DATA_VERSION:String = '1.0.0';
|
||||
public static final DIALOGUE_BOX_DATA_VERSION_RULE:String = '1.0.x';
|
||||
|
||||
static final dialogueBoxCache:Map<String, DialogueBox> = new Map<String, DialogueBox>();
|
||||
|
||||
static final dialogueBoxScriptedClass:Map<String, String> = new Map<String, String>();
|
||||
|
||||
static final DEFAULT_DIALOGUE_BOX_ID:String = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* Parses and preloads the game's dialogueBox data and scripts when the game starts.
|
||||
*
|
||||
* If you want to force dialogue boxes to be reloaded, you can just call this function again.
|
||||
*/
|
||||
public static function loadDialogueBoxCache():Void
|
||||
{
|
||||
clearDialogueBoxCache();
|
||||
trace('Loading dialogue box cache...');
|
||||
|
||||
//
|
||||
// SCRIPTED CONVERSATIONS
|
||||
//
|
||||
var scriptedDialogueBoxClassNames:Array<String> = ScriptedDialogueBox.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedDialogueBoxClassNames.length} scripted dialogue boxes...');
|
||||
for (dialogueBoxCls in scriptedDialogueBoxClassNames)
|
||||
{
|
||||
var dialogueBox:DialogueBox = ScriptedDialogueBox.init(dialogueBoxCls, DEFAULT_DIALOGUE_BOX_ID);
|
||||
if (dialogueBox != null)
|
||||
{
|
||||
trace(' Loaded scripted dialogue box: ${dialogueBox.dialogueBoxName}');
|
||||
// Disable the rendering logic for dialogueBox until it's loaded.
|
||||
// Note that kill() =/= destroy()
|
||||
dialogueBox.kill();
|
||||
|
||||
// Then store it.
|
||||
dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted dialogueBox class: ${dialogueBoxCls}');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UNSCRIPTED CONVERSATIONS
|
||||
//
|
||||
// Scripts refers to code here, not the actual dialogue.
|
||||
var dialogueBoxIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/boxes/');
|
||||
// Filter out dialogue boxes that are scripted.
|
||||
var unscriptedDialogueBoxIds:Array<String> = dialogueBoxIdList.filter(function(dialogueBoxId:String):Bool {
|
||||
return !dialogueBoxCache.exists(dialogueBoxId);
|
||||
});
|
||||
trace(' Fetching data for ${unscriptedDialogueBoxIds.length} dialogue boxes...');
|
||||
for (dialogueBoxId in unscriptedDialogueBoxIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dialogueBox:DialogueBox = new DialogueBox(dialogueBoxId);
|
||||
if (dialogueBox != null)
|
||||
{
|
||||
trace(' Loaded dialogueBox data: ${dialogueBox.dialogueBoxName}');
|
||||
dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data for a dialogueBox and returns a DialogueBox instance,
|
||||
* ready to be displayed.
|
||||
* @param dialogueBoxId The ID of the dialogueBox to fetch.
|
||||
* @return The dialogueBox instance, or null if the dialogueBox was not found.
|
||||
*/
|
||||
public static function fetchDialogueBox(dialogueBoxId:String):Null<DialogueBox>
|
||||
{
|
||||
if (dialogueBoxId != null && dialogueBoxId != '' && dialogueBoxCache.exists(dialogueBoxId))
|
||||
{
|
||||
trace('Successfully fetched dialogueBox: ${dialogueBoxId}');
|
||||
var dialogueBox:DialogueBox = dialogueBoxCache.get(dialogueBoxId);
|
||||
dialogueBox.revive();
|
||||
return dialogueBox;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Failed to fetch dialogueBox, not found in cache: ${dialogueBoxId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function clearDialogueBoxCache():Void
|
||||
{
|
||||
if (dialogueBoxCache != null)
|
||||
{
|
||||
for (dialogueBox in dialogueBoxCache)
|
||||
{
|
||||
dialogueBox.destroy();
|
||||
}
|
||||
dialogueBoxCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static function listDialogueBoxIds():Array<String>
|
||||
{
|
||||
return dialogueBoxCache.keys().array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a dialogueBox's JSON file, parse its data, and return it.
|
||||
*
|
||||
* @param dialogueBoxId The dialogueBox to load.
|
||||
* @return The dialogueBox data, or null if validation failed.
|
||||
*/
|
||||
public static function parseDialogueBoxData(dialogueBoxId:String):Null<DialogueBoxData>
|
||||
{
|
||||
var rawJson:String = loadDialogueBoxFile(dialogueBoxId);
|
||||
|
||||
try
|
||||
{
|
||||
var dialogueBoxData:DialogueBoxData = DialogueBoxData.fromString(rawJson);
|
||||
return dialogueBoxData;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace('Failed to parse dialogueBox ($dialogueBoxId).');
|
||||
trace(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function loadDialogueBoxFile(dialogueBoxPath:String):String
|
||||
{
|
||||
var dialogueBoxFilePath:String = Paths.json('dialogue/boxes/${dialogueBoxPath}');
|
||||
var rawJson:String = Assets.getText(dialogueBoxFilePath).trim();
|
||||
|
||||
while (!rawJson.endsWith('}') && rawJson.length > 0)
|
||||
{
|
||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||
}
|
||||
|
||||
return rawJson;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,10 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
/**
|
||||
* A script that can be tied to a Conversation.
|
||||
* Create a scripted class that extends Conversation to use this.
|
||||
* This allows you to customize how a specific conversation appears and behaves.
|
||||
* Someone clever could use this to add branching dialogue I think.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedConversation extends Conversation implements polymod.hscript.HScriptedClass {}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
/**
|
||||
* A script that can be tied to a DialogueBox.
|
||||
* Create a scripted class that extends DialogueBox to use this.
|
||||
* This allows you to customize how a specific dialogue box appears.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedDialogueBox extends DialogueBox implements polymod.hscript.HScriptedClass {}
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import funkin.data.IRegistryEntry;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||
import funkin.data.dialogue.SpeakerData;
|
||||
import funkin.data.dialogue.SpeakerRegistry;
|
||||
|
||||
/**
|
||||
* The character sprite which displays during dialogue.
|
||||
*
|
||||
* Most conversations have two speakers, with one being flipped.
|
||||
*/
|
||||
class Speaker extends FlxSprite implements IDialogueScriptedClass
|
||||
class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRegistryEntry<SpeakerData>
|
||||
{
|
||||
/**
|
||||
* The internal ID for this speaker.
|
||||
*/
|
||||
public final speakerId:String;
|
||||
public final id:String;
|
||||
|
||||
/**
|
||||
* The full data for a speaker.
|
||||
*/
|
||||
var speakerData:SpeakerData;
|
||||
public final _data:SpeakerData;
|
||||
|
||||
/**
|
||||
* A readable name for this speaker.
|
||||
|
@ -30,7 +33,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
|
||||
function get_speakerName():String
|
||||
{
|
||||
return speakerData.name;
|
||||
return _data.name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,14 +78,17 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
return globalOffsets = value;
|
||||
}
|
||||
|
||||
public function new(speakerId:String)
|
||||
public function new(id:String)
|
||||
{
|
||||
super();
|
||||
|
||||
this.speakerId = speakerId;
|
||||
this.speakerData = SpeakerDataParser.parseSpeakerData(this.speakerId);
|
||||
this.id = id;
|
||||
this._data = _fetchData(id);
|
||||
|
||||
if (speakerData == null) throw 'Could not load speaker data for speaker ID "$speakerId"';
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not parse speaker data for id: $id';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,18 +108,18 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
|
||||
function loadSpritesheet():Void
|
||||
{
|
||||
trace('[SPEAKER] Loading spritesheet ${speakerData.assetPath} for ${speakerId}');
|
||||
trace('[SPEAKER] Loading spritesheet ${_data.assetPath} for ${id}');
|
||||
|
||||
var tex:FlxFramesCollection = Paths.getSparrowAtlas(speakerData.assetPath);
|
||||
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
|
||||
if (tex == null)
|
||||
{
|
||||
trace('Could not load Sparrow sprite: ${speakerData.assetPath}');
|
||||
trace('Could not load Sparrow sprite: ${_data.assetPath}');
|
||||
return;
|
||||
}
|
||||
|
||||
this.frames = tex;
|
||||
|
||||
if (speakerData.isPixel)
|
||||
if (_data.isPixel)
|
||||
{
|
||||
this.antialiasing = false;
|
||||
}
|
||||
|
@ -122,9 +128,10 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
this.antialiasing = true;
|
||||
}
|
||||
|
||||
this.flipX = speakerData.flipX;
|
||||
this.globalOffsets = speakerData.offsets;
|
||||
this.setScale(speakerData.scale);
|
||||
this.flipX = _data.flipX;
|
||||
this.flipY = _data.flipY;
|
||||
this.globalOffsets = _data.offsets;
|
||||
this.setScale(_data.scale);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,11 +148,11 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
|
||||
function loadAnimations():Void
|
||||
{
|
||||
trace('[SPEAKER] Loading ${speakerData.animations.length} animations for ${speakerId}');
|
||||
trace('[SPEAKER] Loading ${_data.animations.length} animations for ${id}');
|
||||
|
||||
FlxAnimationUtil.addAtlasAnimations(this, speakerData.animations);
|
||||
FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
|
||||
|
||||
for (anim in speakerData.animations)
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.offsets == null)
|
||||
{
|
||||
|
@ -158,7 +165,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
}
|
||||
|
||||
var animNames:Array<String> = this.animation.getNameList();
|
||||
trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${speakerId}');
|
||||
trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${id}');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -271,4 +278,14 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
}
|
||||
|
||||
public function onScriptEvent(event:ScriptEvent):Void {}
|
||||
|
||||
public override function toString():String
|
||||
{
|
||||
return 'Speaker($id)';
|
||||
}
|
||||
|
||||
static function _fetchData(id:String):Null<SpeakerData>
|
||||
{
|
||||
return SpeakerRegistry.instance.parseEntryDataWithMigration(id, SpeakerRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
/**
|
||||
* Data about a conversation.
|
||||
* Includes what speakers are in the conversation, and what phrases they say.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.SpeakerData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class SpeakerData
|
||||
{
|
||||
public var version:String;
|
||||
public var name:String;
|
||||
public var assetPath:String;
|
||||
public var flipX:Bool;
|
||||
public var isPixel:Bool;
|
||||
public var offsets:Array<Float>;
|
||||
public var scale:Float;
|
||||
public var animations:Array<AnimationData>;
|
||||
|
||||
public function new(version:String, name:String, assetPath:String, animations:Array<AnimationData>, ?offsets:Array<Float>, flipX:Bool = false,
|
||||
isPixel:Bool = false, ?scale:Float = 1.0)
|
||||
{
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.assetPath = assetPath;
|
||||
this.animations = animations;
|
||||
|
||||
this.offsets = offsets;
|
||||
if (this.offsets == null || this.offsets == []) this.offsets = [0, 0];
|
||||
|
||||
this.flipX = flipX;
|
||||
this.isPixel = isPixel;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public static function fromString(i:String):SpeakerData
|
||||
{
|
||||
if (i == null || i == '') return null;
|
||||
var data:
|
||||
{
|
||||
version:String,
|
||||
name:String,
|
||||
assetPath:String,
|
||||
animations:Array<AnimationData>,
|
||||
?offsets:Array<Float>,
|
||||
?flipX:Bool,
|
||||
?isPixel:Bool,
|
||||
?scale:Float
|
||||
} = tink.Json.parse(i);
|
||||
return fromJson(data);
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):SpeakerData
|
||||
{
|
||||
// TODO: Check version and perform migrations if necessary.
|
||||
if (j == null) return null;
|
||||
return new SpeakerData(j.version, j.name, j.assetPath, j.animations, j.offsets, j.flipX, j.isPixel, j.scale);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
var result:Dynamic =
|
||||
{
|
||||
version: this.version,
|
||||
name: this.name,
|
||||
assetPath: this.assetPath,
|
||||
animations: this.animations,
|
||||
flipX: this.flipX,
|
||||
isPixel: this.isPixel
|
||||
};
|
||||
|
||||
if (this.scale != 1.0) result.scale = this.scale;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import openfl.Assets;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.play.cutscene.dialogue.Speaker;
|
||||
import funkin.play.cutscene.dialogue.ScriptedSpeaker;
|
||||
|
||||
/**
|
||||
* Contains utilities for loading and parsing speaker data.
|
||||
*/
|
||||
class SpeakerDataParser
|
||||
{
|
||||
public static final SPEAKER_DATA_VERSION:String = '1.0.0';
|
||||
public static final SPEAKER_DATA_VERSION_RULE:String = '1.0.x';
|
||||
|
||||
static final speakerCache:Map<String, Speaker> = new Map<String, Speaker>();
|
||||
|
||||
static final speakerScriptedClass:Map<String, String> = new Map<String, String>();
|
||||
|
||||
static final DEFAULT_SPEAKER_ID:String = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* Parses and preloads the game's speaker data and scripts when the game starts.
|
||||
*
|
||||
* If you want to force speakers to be reloaded, you can just call this function again.
|
||||
*/
|
||||
public static function loadSpeakerCache():Void
|
||||
{
|
||||
clearSpeakerCache();
|
||||
trace('Loading dialogue speaker cache...');
|
||||
|
||||
//
|
||||
// SCRIPTED CONVERSATIONS
|
||||
//
|
||||
var scriptedSpeakerClassNames:Array<String> = ScriptedSpeaker.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedSpeakerClassNames.length} scripted speakers...');
|
||||
for (speakerCls in scriptedSpeakerClassNames)
|
||||
{
|
||||
var speaker:Speaker = ScriptedSpeaker.init(speakerCls, DEFAULT_SPEAKER_ID);
|
||||
if (speaker != null)
|
||||
{
|
||||
trace(' Loaded scripted speaker: ${speaker.speakerName}');
|
||||
// Disable the rendering logic for speaker until it's loaded.
|
||||
// Note that kill() =/= destroy()
|
||||
speaker.kill();
|
||||
|
||||
// Then store it.
|
||||
speakerCache.set(speaker.speakerId, speaker);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted speaker class: ${speakerCls}');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UNSCRIPTED CONVERSATIONS
|
||||
//
|
||||
// Scripts refers to code here, not the actual dialogue.
|
||||
var speakerIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/speakers/');
|
||||
// Filter out speakers that are scripted.
|
||||
var unscriptedSpeakerIds:Array<String> = speakerIdList.filter(function(speakerId:String):Bool {
|
||||
return !speakerCache.exists(speakerId);
|
||||
});
|
||||
trace(' Fetching data for ${unscriptedSpeakerIds.length} speakers...');
|
||||
for (speakerId in unscriptedSpeakerIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var speaker:Speaker = new Speaker(speakerId);
|
||||
if (speaker != null)
|
||||
{
|
||||
trace(' Loaded speaker data: ${speaker.speakerName}');
|
||||
speakerCache.set(speaker.speakerId, speaker);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data for a speaker and returns a Speaker instance,
|
||||
* ready to be displayed.
|
||||
* @param speakerId The ID of the speaker to fetch.
|
||||
* @return The speaker instance, or null if the speaker was not found.
|
||||
*/
|
||||
public static function fetchSpeaker(speakerId:String):Null<Speaker>
|
||||
{
|
||||
if (speakerId != null && speakerId != '' && speakerCache.exists(speakerId))
|
||||
{
|
||||
trace('Successfully fetched speaker: ${speakerId}');
|
||||
var speaker:Speaker = speakerCache.get(speakerId);
|
||||
speaker.revive();
|
||||
return speaker;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Failed to fetch speaker, not found in cache: ${speakerId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function clearSpeakerCache():Void
|
||||
{
|
||||
if (speakerCache != null)
|
||||
{
|
||||
for (speaker in speakerCache)
|
||||
{
|
||||
speaker.destroy();
|
||||
}
|
||||
speakerCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static function listSpeakerIds():Array<String>
|
||||
{
|
||||
return speakerCache.keys().array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a speaker's JSON file, parse its data, and return it.
|
||||
*
|
||||
* @param speakerId The speaker to load.
|
||||
* @return The speaker data, or null if validation failed.
|
||||
*/
|
||||
public static function parseSpeakerData(speakerId:String):Null<SpeakerData>
|
||||
{
|
||||
var rawJson:String = loadSpeakerFile(speakerId);
|
||||
|
||||
try
|
||||
{
|
||||
var speakerData:SpeakerData = SpeakerData.fromString(rawJson);
|
||||
return speakerData;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace('Failed to parse speaker ($speakerId).');
|
||||
trace(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function loadSpeakerFile(speakerPath:String):String
|
||||
{
|
||||
var speakerFilePath:String = Paths.json('dialogue/speakers/${speakerPath}');
|
||||
var rawJson:String = Assets.getText(speakerFilePath).trim();
|
||||
|
||||
while (!rawJson.endsWith('}') && rawJson.length > 0)
|
||||
{
|
||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||
}
|
||||
|
||||
return rawJson;
|
||||
}
|
||||
}
|
|
@ -5,8 +5,8 @@ import funkin.data.song.SongData;
|
|||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
|
||||
/**
|
||||
* This class represents a handler for a type of song event.
|
||||
|
@ -132,13 +132,13 @@ class FocusCameraSongEvent extends SongEvent
|
|||
*/
|
||||
public override function getEventSchema():SongEventSchema
|
||||
{
|
||||
return [
|
||||
return new SongEventSchema([
|
||||
{
|
||||
name: "char",
|
||||
title: "Character",
|
||||
title: "Target",
|
||||
defaultValue: 0,
|
||||
type: SongEventFieldType.ENUM,
|
||||
keys: ["Position" => -1, "Boyfriend" => 0, "Dad" => 1, "Girlfriend" => 2]
|
||||
keys: ["Position" => -1, "Player" => 0, "Opponent" => 1, "Girlfriend" => 2]
|
||||
},
|
||||
{
|
||||
name: "x",
|
||||
|
@ -146,6 +146,7 @@ class FocusCameraSongEvent extends SongEvent
|
|||
defaultValue: 0,
|
||||
step: 10.0,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: "px"
|
||||
},
|
||||
{
|
||||
name: "y",
|
||||
|
@ -153,7 +154,8 @@ class FocusCameraSongEvent extends SongEvent
|
|||
defaultValue: 0,
|
||||
step: 10.0,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: "px"
|
||||
}
|
||||
];
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import funkin.data.song.SongData;
|
|||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
|
||||
class PlayAnimationSongEvent extends SongEvent
|
||||
{
|
||||
|
@ -89,7 +89,7 @@ class PlayAnimationSongEvent extends SongEvent
|
|||
*/
|
||||
public override function getEventSchema():SongEventSchema
|
||||
{
|
||||
return [
|
||||
return new SongEventSchema([
|
||||
{
|
||||
name: 'target',
|
||||
title: 'Target',
|
||||
|
@ -108,6 +108,6 @@ class PlayAnimationSongEvent extends SongEvent
|
|||
type: SongEventFieldType.BOOL,
|
||||
defaultValue: false
|
||||
}
|
||||
];
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import funkin.data.song.SongData;
|
|||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
|
||||
/**
|
||||
* This class represents a handler for configuring camera bop intensity and rate.
|
||||
|
@ -72,21 +72,23 @@ class SetCameraBopSongEvent extends SongEvent
|
|||
*/
|
||||
public override function getEventSchema():SongEventSchema
|
||||
{
|
||||
return [
|
||||
return new SongEventSchema([
|
||||
{
|
||||
name: 'intensity',
|
||||
title: 'Intensity',
|
||||
defaultValue: 1.0,
|
||||
step: 0.1,
|
||||
type: SongEventFieldType.FLOAT
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'x'
|
||||
},
|
||||
{
|
||||
name: 'rate',
|
||||
title: 'Rate (beats/zoom)',
|
||||
title: 'Rate',
|
||||
defaultValue: 4,
|
||||
step: 1,
|
||||
type: SongEventFieldType.INTEGER,
|
||||
units: 'beats/zoom'
|
||||
}
|
||||
];
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package funkin.play.event;
|
||||
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
|
||||
/**
|
||||
* This class represents a handler for a type of song event.
|
||||
|
|
|
@ -8,8 +8,8 @@ import funkin.data.song.SongData;
|
|||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
|
||||
/**
|
||||
* This class represents a handler for camera zoom events.
|
||||
|
@ -79,7 +79,8 @@ class ZoomCameraSongEvent extends SongEvent
|
|||
return;
|
||||
}
|
||||
|
||||
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), {ease: easeFunction});
|
||||
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.instance.stepLengthMs * duration / 1000),
|
||||
{ease: easeFunction});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,20 +100,22 @@ class ZoomCameraSongEvent extends SongEvent
|
|||
*/
|
||||
public override function getEventSchema():SongEventSchema
|
||||
{
|
||||
return [
|
||||
return new SongEventSchema([
|
||||
{
|
||||
name: 'zoom',
|
||||
title: 'Zoom Level',
|
||||
defaultValue: 1.0,
|
||||
step: 0.1,
|
||||
type: SongEventFieldType.FLOAT
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'x'
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
title: 'Duration (in steps)',
|
||||
title: 'Duration',
|
||||
defaultValue: 4.0,
|
||||
step: 0.5,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'steps'
|
||||
},
|
||||
{
|
||||
name: 'ease',
|
||||
|
@ -145,6 +148,6 @@ class ZoomCameraSongEvent extends SongEvent
|
|||
'Elastic In/Out' => 'elasticInOut',
|
||||
]
|
||||
}
|
||||
];
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import funkin.data.song.SongData.SongNoteData;
|
|||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.FlxSprite;
|
||||
import funkin.graphics.shaders.HSVShader;
|
||||
|
||||
class NoteSprite extends FlxSprite
|
||||
{
|
||||
|
@ -11,6 +12,8 @@ class NoteSprite extends FlxSprite
|
|||
|
||||
public var holdNoteSprite:SustainTrail;
|
||||
|
||||
var hsvShader:HSVShader;
|
||||
|
||||
/**
|
||||
* The time at which the note should be hit, in milliseconds.
|
||||
*/
|
||||
|
@ -102,6 +105,8 @@ class NoteSprite extends FlxSprite
|
|||
this.strumTime = strumTime;
|
||||
this.direction = direction;
|
||||
|
||||
this.hsvShader = new HSVShader();
|
||||
|
||||
if (this.strumTime < 0) this.strumTime = 0;
|
||||
|
||||
setupNoteGraphic(noteStyle);
|
||||
|
@ -116,16 +121,57 @@ class NoteSprite extends FlxSprite
|
|||
|
||||
setGraphicSize(Strumline.STRUMLINE_SIZE);
|
||||
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
|
||||
{
|
||||
super.revive();
|
||||
this.visible = true;
|
||||
this.alpha = 1.0;
|
||||
this.active = false;
|
||||
this.tooEarly = false;
|
||||
this.hasBeenHit = false;
|
||||
this.mayHit = false;
|
||||
this.hasMissed = false;
|
||||
|
||||
this.hsvShader.hue = 1.0;
|
||||
this.hsvShader.saturation = 1.0;
|
||||
this.hsvShader.value = 1.0;
|
||||
}
|
||||
|
||||
public override function kill():Void
|
||||
|
|
|
@ -274,10 +274,12 @@ class Strumline extends FlxSpriteGroup
|
|||
static function calculateNoteYPos(strumTime:Float, vwoosh:Bool = true):Float
|
||||
{
|
||||
// Make the note move faster visually as it moves offscreen.
|
||||
var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
|
||||
// var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
|
||||
// ^^^ commented this out... do NOT make it move faster as it moves offscreen!
|
||||
var vwoosh:Float = 1.0;
|
||||
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
|
||||
|
||||
return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
|
||||
return Constants.PIXELS_PER_MS * (Conductor.instance.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
|
||||
}
|
||||
|
||||
function updateNotes():Void
|
||||
|
@ -285,8 +287,8 @@ class Strumline extends FlxSpriteGroup
|
|||
if (noteData.length == 0) return;
|
||||
|
||||
var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0;
|
||||
var hitWindowStart:Float = Conductor.songPosition - Constants.HIT_WINDOW_MS;
|
||||
var renderWindowStart:Float = Conductor.songPosition + RENDER_DISTANCE_MS;
|
||||
var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS;
|
||||
var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS;
|
||||
|
||||
for (noteIndex in nextNoteIndex...noteData.length)
|
||||
{
|
||||
|
@ -314,7 +316,7 @@ class Strumline extends FlxSpriteGroup
|
|||
// Update rendering of notes.
|
||||
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;
|
||||
// Set the note's position.
|
||||
|
@ -333,7 +335,7 @@ class Strumline extends FlxSpriteGroup
|
|||
{
|
||||
if (holdNote == null || !holdNote.alive) continue;
|
||||
|
||||
if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote)
|
||||
if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote)
|
||||
{
|
||||
if (isPlayer && !isKeyHeld(holdNote.noteDirection))
|
||||
{
|
||||
|
@ -341,13 +343,13 @@ class Strumline extends FlxSpriteGroup
|
|||
playStatic(holdNote.noteDirection);
|
||||
holdNote.missedNote = true;
|
||||
holdNote.visible = true;
|
||||
holdNote.alpha = 0.0;
|
||||
holdNote.alpha = 0.0; // Completely hide the dropped hold note.
|
||||
}
|
||||
}
|
||||
|
||||
var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Constants.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8;
|
||||
|
||||
if (holdNote.missedNote && Conductor.songPosition >= renderWindowEnd)
|
||||
if (holdNote.missedNote && Conductor.instance.songPosition >= renderWindowEnd)
|
||||
{
|
||||
// Hold note is offscreen, kill it.
|
||||
holdNote.visible = false;
|
||||
|
@ -382,10 +384,6 @@ class Strumline extends FlxSpriteGroup
|
|||
|
||||
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;
|
||||
|
||||
if (Preferences.downscroll)
|
||||
|
@ -397,13 +395,13 @@ class Strumline extends FlxSpriteGroup
|
|||
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2;
|
||||
}
|
||||
}
|
||||
else if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote)
|
||||
else if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote)
|
||||
{
|
||||
// Hold note is currently being hit, clip it off.
|
||||
holdConfirm(holdNote.noteDirection);
|
||||
holdNote.visible = true;
|
||||
|
||||
holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.songPosition;
|
||||
holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.instance.songPosition;
|
||||
|
||||
if (holdNote.sustainLength <= 10)
|
||||
{
|
||||
|
@ -529,11 +527,24 @@ class Strumline extends FlxSpriteGroup
|
|||
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);
|
||||
note.hasBeenHit = true;
|
||||
killNote(note);
|
||||
|
||||
if (removeNote)
|
||||
{
|
||||
killNote(note);
|
||||
}
|
||||
else
|
||||
{
|
||||
note.alpha = 0.5;
|
||||
note.desaturate();
|
||||
}
|
||||
|
||||
if (note.holdNoteSprite != null)
|
||||
{
|
||||
|
|
|
@ -47,6 +47,11 @@ class SustainTrail extends FlxSprite
|
|||
*/
|
||||
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!
|
||||
|
||||
/**
|
||||
|
@ -82,6 +87,9 @@ class SustainTrail extends FlxSprite
|
|||
|
||||
public var isPixel:Bool;
|
||||
|
||||
var graphicWidth:Float = 0;
|
||||
var graphicHeight:Float = 0;
|
||||
|
||||
/**
|
||||
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
|
||||
* @param NoteData
|
||||
|
@ -110,8 +118,8 @@ class SustainTrail extends FlxSprite
|
|||
zoom *= 0.7;
|
||||
|
||||
// CALCULATE SIZE
|
||||
width = graphic.width / 8 * zoom; // amount of notes * 2
|
||||
height = sustainHeight(sustainLength, getScrollSpeed());
|
||||
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
|
||||
graphicHeight = sustainHeight(sustainLength, getScrollSpeed());
|
||||
// instead of scrollSpeed, PlayState.SONG.speed
|
||||
|
||||
flipY = Preferences.downscroll;
|
||||
|
@ -148,10 +156,19 @@ class SustainTrail extends FlxSprite
|
|||
|
||||
if (sustainLength == s) return s;
|
||||
|
||||
height = sustainHeight(s, getScrollSpeed());
|
||||
// updateColorTransform();
|
||||
graphicHeight = sustainHeight(s, getScrollSpeed());
|
||||
this.sustainLength = s;
|
||||
updateClipping();
|
||||
return sustainLength = s;
|
||||
updateHitbox();
|
||||
return this.sustainLength;
|
||||
}
|
||||
|
||||
public override function updateHitbox():Void
|
||||
{
|
||||
width = graphicWidth;
|
||||
height = graphicHeight;
|
||||
offset.set(0, 0);
|
||||
origin.set(width * 0.5, height * 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -161,14 +178,16 @@ class SustainTrail extends FlxSprite
|
|||
*/
|
||||
public function updateClipping(songTime:Float = 0):Void
|
||||
{
|
||||
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height);
|
||||
if (clipHeight == 0)
|
||||
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight);
|
||||
if (clipHeight <= 0.1)
|
||||
{
|
||||
visible = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
visible = true;
|
||||
}
|
||||
|
||||
var bottomHeight:Float = graphic.height * zoom * endOffset;
|
||||
var partHeight:Float = clipHeight - bottomHeight;
|
||||
|
@ -176,10 +195,10 @@ class SustainTrail extends FlxSprite
|
|||
// ===HOLD VERTICES==
|
||||
// Top left
|
||||
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
|
||||
vertices[1 * 2] = width;
|
||||
vertices[1 * 2] = graphicWidth;
|
||||
vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex
|
||||
|
||||
// Bottom left
|
||||
|
@ -195,7 +214,7 @@ class SustainTrail extends FlxSprite
|
|||
}
|
||||
|
||||
// Bottom right
|
||||
vertices[3 * 2] = width;
|
||||
vertices[3 * 2] = graphicWidth;
|
||||
vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex
|
||||
|
||||
// ===HOLD UVs===
|
||||
|
@ -231,7 +250,7 @@ class SustainTrail extends FlxSprite
|
|||
|
||||
// Bottom left
|
||||
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
|
||||
vertices[7 * 2] = vertices[3 * 2]; // Inline with right side
|
||||
|
@ -275,6 +294,10 @@ class SustainTrail extends FlxSprite
|
|||
getScreenPosition(_point, camera).subtractPoint(offset);
|
||||
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
|
||||
|
@ -303,6 +326,7 @@ class SustainTrail extends FlxSprite
|
|||
|
||||
hitNote = false;
|
||||
missedNote = false;
|
||||
handledMiss = false;
|
||||
}
|
||||
|
||||
override public function destroy():Void
|
||||
|
|
|
@ -158,8 +158,8 @@ class Scoring
|
|||
|
||||
return switch (absTiming)
|
||||
{
|
||||
case(_ < PBOT1_KILLER_THRESHOLD) => true:
|
||||
'killer';
|
||||
// case(_ < PBOT1_KILLER_THRESHOLD) => true:
|
||||
// 'killer';
|
||||
case(_ < PBOT1_SICK_THRESHOLD) => true:
|
||||
'sick';
|
||||
case(_ < PBOT1_GOOD_THRESHOLD) => true:
|
||||
|
|
|
@ -174,7 +174,10 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
difficulty.timeChanges = metadata.timeChanges;
|
||||
difficulty.looped = metadata.looped;
|
||||
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.noteStyle = metadata.playData.noteStyle;
|
||||
|
@ -232,6 +235,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
difficulty.timeChanges = metadata.timeChanges;
|
||||
difficulty.looped = metadata.looped;
|
||||
difficulty.generatedBy = metadata.generatedBy;
|
||||
difficulty.offsets = metadata?.offsets ?? new SongOffsets();
|
||||
|
||||
difficulty.stage = metadata.playData.stage;
|
||||
difficulty.noteStyle = metadata.playData.noteStyle;
|
||||
|
@ -405,6 +409,9 @@ class SongDifficulty
|
|||
|
||||
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)
|
||||
{
|
||||
this.song = song;
|
||||
|
|
|
@ -12,13 +12,16 @@ import flixel.math.FlxPoint;
|
|||
import flixel.system.FlxAssets.FlxShader;
|
||||
import flixel.util.FlxSort;
|
||||
import openfl.display.BitmapData;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.modding.IScriptedClass;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.events.ScriptEventType;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.stage.StageData.StageDataCharacter;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.data.IRegistryEntry;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.data.stage.StageData.StageDataCharacter;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.play.stage.StageProp;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
|
@ -30,14 +33,25 @@ typedef StagePropGroup = FlxTypedSpriteGroup<StageProp>;
|
|||
*
|
||||
* 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 stageName:String;
|
||||
public final id: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 frameBufferMan:FrameBufferManager;
|
||||
|
||||
|
@ -55,21 +69,18 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
* They're used to cache the data needed to build the stage,
|
||||
* 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();
|
||||
|
||||
this.stageId = stageId;
|
||||
_data = StageDataParser.parseStageData(this.stageId);
|
||||
this.id = id;
|
||||
_data = _fetchData(id);
|
||||
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not find stage data for stageId: $stageId';
|
||||
}
|
||||
else
|
||||
{
|
||||
this.stageName = _data.name;
|
||||
throw 'Could not find stage data for stage id: $id';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,9 +158,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
*/
|
||||
function buildStage():Void
|
||||
{
|
||||
trace('Building stage for display: ${this.stageId}');
|
||||
|
||||
this.camZoom = _data.cameraZoom;
|
||||
trace('Building stage for display: ${this.id}');
|
||||
|
||||
this.debugIconGroup = new FlxSpriteGroup();
|
||||
|
||||
|
@ -157,6 +166,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
{
|
||||
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
|
||||
|
||||
var isSolidColor = dataProp.assetPath.startsWith('#');
|
||||
var isAnimated = dataProp.animations.length > 0;
|
||||
|
||||
var propSprite:StageProp;
|
||||
|
@ -180,6 +190,22 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
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
|
||||
{
|
||||
// Initalize static sprite.
|
||||
|
@ -195,13 +221,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
continue;
|
||||
}
|
||||
|
||||
switch (dataProp.scale)
|
||||
if (!isSolidColor)
|
||||
{
|
||||
case Left(value):
|
||||
propSprite.scale.set(value);
|
||||
switch (dataProp.scale)
|
||||
{
|
||||
case Left(value):
|
||||
propSprite.scale.set(value);
|
||||
|
||||
case Right(values):
|
||||
propSprite.scale.set(values[0], values[1]);
|
||||
case Right(values):
|
||||
propSprite.scale.set(values[0], values[1]);
|
||||
}
|
||||
}
|
||||
propSprite.updateHitbox();
|
||||
|
||||
|
@ -213,15 +242,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
// If pixel, disable antialiasing.
|
||||
propSprite.antialiasing = !dataProp.isPixel;
|
||||
|
||||
switch (dataProp.scroll)
|
||||
{
|
||||
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.scrollFactor.x = dataProp.scroll[0];
|
||||
propSprite.scrollFactor.y = dataProp.scroll[1];
|
||||
|
||||
propSprite.zIndex = dataProp.zIndex;
|
||||
|
||||
|
@ -801,6 +823,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
}
|
||||
}
|
||||
|
||||
static function _fetchData(id:String):Null<StageData>
|
||||
{
|
||||
return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
|
||||
public function onScriptEvent(event:ScriptEvent) {}
|
||||
|
||||
public function onPause(event:PauseScriptEvent) {}
|
||||
|
|
|
@ -1,547 +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.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;
|
||||
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import flixel.FlxSprite;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.modding.IScriptedClass.IStateStageProp;
|
||||
|
||||
class StageProp extends FlxSprite implements IStateStageProp
|
||||
class StageProp extends FunkinSprite implements IStateStageProp
|
||||
{
|
||||
/**
|
||||
* An internal name for this prop.
|
||||
|
|
|
@ -14,8 +14,8 @@ import thx.semver.Version;
|
|||
@:forward(volume, mute)
|
||||
abstract Save(RawSaveData)
|
||||
{
|
||||
// Version 2.0.1 adds attributes to `optionsChartEditor`, that should return default values if they are null.
|
||||
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.1";
|
||||
// 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_RULE:thx.semver.VersionRule = "2.0.x";
|
||||
|
||||
// We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
|
||||
|
@ -108,9 +108,7 @@ abstract Save(RawSaveData)
|
|||
metronomeVolume: 1.0,
|
||||
hitsoundsEnabledPlayer: true,
|
||||
hitsoundsEnabledOpponent: true,
|
||||
instVolume: 1.0,
|
||||
voicesVolume: 1.0,
|
||||
playbackSpeed: 1.0,
|
||||
themeMusic: true
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -347,38 +345,21 @@ abstract Save(RawSaveData)
|
|||
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.
|
||||
this.optionsChartEditor.instVolume = value;
|
||||
this.optionsChartEditor.themeMusic = value;
|
||||
flush();
|
||||
return this.optionsChartEditor.instVolume;
|
||||
}
|
||||
|
||||
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;
|
||||
return this.optionsChartEditor.themeMusic;
|
||||
}
|
||||
|
||||
public var chartEditorPlaybackSpeed(get, set):Float;
|
||||
|
@ -774,7 +755,6 @@ typedef SaveScoreData =
|
|||
|
||||
typedef SaveScoreTallyData =
|
||||
{
|
||||
var killer:Int;
|
||||
var sick:Int;
|
||||
var good:Int;
|
||||
var bad:Int;
|
||||
|
@ -1027,6 +1007,12 @@ typedef SaveDataChartEditorOptions =
|
|||
*/
|
||||
var ?hitsoundsEnabledOpponent:Bool;
|
||||
|
||||
/**
|
||||
* Theme music in the Chart Editor.
|
||||
* @default `true`
|
||||
*/
|
||||
var ?themeMusic:Bool;
|
||||
|
||||
/**
|
||||
* Instrumental volume in the Chart Editor.
|
||||
* @default `1.0`
|
||||
|
|
|
@ -120,7 +120,6 @@ class SaveDataMigrator
|
|||
accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -140,7 +139,6 @@ class SaveDataMigrator
|
|||
accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -160,7 +158,6 @@ class SaveDataMigrator
|
|||
accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -183,7 +180,6 @@ class SaveDataMigrator
|
|||
accuracy: 0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -209,7 +205,6 @@ class SaveDataMigrator
|
|||
accuracy: 0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -235,7 +230,6 @@ class SaveDataMigrator
|
|||
accuracy: 0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
|
|
@ -274,4 +274,5 @@ enum abstract AtlasFont(String) from String to String
|
|||
{
|
||||
var DEFAULT = "default";
|
||||
var BOLD = "bold";
|
||||
var FREEPLAY_CLEAR = "freeplay-clear";
|
||||
}
|
||||
|
|
|
@ -80,25 +80,11 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
|
|||
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
||||
}
|
||||
|
||||
function handleQuickWatch():Void
|
||||
{
|
||||
// Display Conductor info in the watch window.
|
||||
FlxG.watch.addQuick("songPosition", Conductor.songPosition);
|
||||
FlxG.watch.addQuick("songPositionNoOffset", Conductor.songPosition + Conductor.instrumentalOffset);
|
||||
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
|
||||
FlxG.watch.addQuick("bpm", Conductor.bpm);
|
||||
FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
handleControls();
|
||||
handleFunctionControls();
|
||||
handleQuickWatch();
|
||||
|
||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||
}
|
||||
|
@ -139,7 +125,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
|
|||
|
||||
public function stepHit():Bool
|
||||
{
|
||||
var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep);
|
||||
var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
|
@ -150,7 +136,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
|
|||
|
||||
public function beatHit():Bool
|
||||
{
|
||||
var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep);
|
||||
var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
|
|
|
@ -65,12 +65,8 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
|
|||
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
||||
|
||||
// Display Conductor info in the watch window.
|
||||
FlxG.watch.addQuick("songPosition", Conductor.songPosition);
|
||||
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
|
||||
FlxG.watch.addQuick("bpm", Conductor.bpm);
|
||||
FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
|
||||
Conductor.watchQuick();
|
||||
|
||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||
}
|
||||
|
@ -99,7 +95,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
|
|||
*/
|
||||
public function stepHit():Bool
|
||||
{
|
||||
var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep);
|
||||
var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
|
@ -115,7 +111,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
|
|||
*/
|
||||
public function beatHit():Bool
|
||||
{
|
||||
var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep);
|
||||
var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
|
|
191
source/funkin/ui/debug/WaveformTestState.hx
Normal file
191
source/funkin/ui/debug/WaveformTestState.hx
Normal file
|
@ -0,0 +1,191 @@
|
|||
package funkin.ui.debug;
|
||||
|
||||
import flixel.math.FlxRect;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformSprite;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import funkin.graphics.rendering.MeshRender;
|
||||
|
||||
class WaveformTestState extends MusicBeatState
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
var waveformData:WaveformData;
|
||||
var waveformData2:WaveformData;
|
||||
|
||||
var waveformAudio:FunkinSound;
|
||||
|
||||
// var waveformSprite:WaveformSprite;
|
||||
// var waveformSprite2:WaveformSprite;
|
||||
var timeMarker:FlxSprite;
|
||||
|
||||
var polygonSprite:MeshRender;
|
||||
var vertexCount:Int = 3;
|
||||
|
||||
public override function create():Void
|
||||
{
|
||||
super.create();
|
||||
|
||||
var testSprite = new FlxSprite(0, 0);
|
||||
testSprite.loadGraphic(Paths.image('funkay'));
|
||||
testSprite.updateHitbox();
|
||||
testSprite.clipRect = new FlxRect(0, 0, FlxG.width, FlxG.height);
|
||||
// add(testSprite);
|
||||
|
||||
waveformAudio = FunkinSound.load(Paths.inst('bopeebo', '-erect'));
|
||||
|
||||
waveformData = WaveformDataParser.interpretFlxSound(waveformAudio);
|
||||
|
||||
polygonSprite = new MeshRender(FlxG.width / 2, FlxG.height / 2, FlxColor.WHITE);
|
||||
|
||||
setPolygonVertices(vertexCount);
|
||||
add(polygonSprite);
|
||||
|
||||
// waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6"));
|
||||
// waveformSprite.duration = 5.0 * 160;
|
||||
// waveformSprite.width = FlxG.width * 160;
|
||||
// waveformSprite.height = FlxG.height; // / 2;
|
||||
// waveformSprite.amplitude = 2.0;
|
||||
// waveformSprite.minWaveformSize = 25;
|
||||
// waveformSprite.clipRect = new FlxRect(0, 0, FlxG.width, FlxG.height);
|
||||
// add(waveformSprite);
|
||||
//
|
||||
// waveformSprite2 = WaveformSprite.buildFromWaveformData(waveformData2, HORIZONTAL, FlxColor.fromString("#FF0000"), 5.0);
|
||||
// waveformSprite2.width = FlxG.width;
|
||||
// waveformSprite2.height = FlxG.height / 2;
|
||||
// waveformSprite2.y = FlxG.height / 2;
|
||||
// add(waveformSprite2);
|
||||
|
||||
timeMarker = new FlxSprite(0, FlxG.height * 1 / 6);
|
||||
timeMarker.makeGraphic(1, Std.int(FlxG.height * 2 / 3), FlxColor.RED);
|
||||
add(timeMarker);
|
||||
|
||||
// drawWaveform(time, duration);
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (FlxG.keys.justPressed.SPACE)
|
||||
{
|
||||
if (waveformAudio.isPlaying)
|
||||
{
|
||||
waveformAudio.stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
waveformAudio.play();
|
||||
}
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.ENTER)
|
||||
{
|
||||
// if (waveformSprite.orientation == HORIZONTAL)
|
||||
// {
|
||||
// // waveformSprite.orientation = VERTICAL;
|
||||
// // waveformSprite2.orientation = VERTICAL;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // waveformSprite.orientation = HORIZONTAL;
|
||||
// // waveformSprite2.orientation = HORIZONTAL;
|
||||
// }
|
||||
}
|
||||
|
||||
if (waveformAudio.isPlaying)
|
||||
{
|
||||
// waveformSprite takes a time in fractional seconds, not milliseconds.
|
||||
var timeSeconds = waveformAudio.time / 1000;
|
||||
// waveformSprite.time = timeSeconds;
|
||||
// waveformSprite2.time = timeSeconds;
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.UP)
|
||||
{
|
||||
vertexCount += 1;
|
||||
setPolygonVertices(vertexCount);
|
||||
// waveformSprite.duration += 1.0;
|
||||
// waveformSprite2.duration += 1.0;
|
||||
}
|
||||
if (FlxG.keys.justPressed.DOWN)
|
||||
{
|
||||
vertexCount -= 1;
|
||||
setPolygonVertices(vertexCount);
|
||||
// waveformSprite.duration -= 1.0;
|
||||
// waveformSprite2.duration -= 1.0;
|
||||
}
|
||||
if (FlxG.keys.justPressed.LEFT)
|
||||
{
|
||||
// waveformSprite.time -= 1.0;
|
||||
// waveformSprite2.time -= 1.0;
|
||||
}
|
||||
if (FlxG.keys.justPressed.RIGHT)
|
||||
{
|
||||
// waveformSprite.time += 1.0;
|
||||
// waveformSprite2.time += 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
function setPolygonVertices(count:Int)
|
||||
{
|
||||
polygonSprite.clear();
|
||||
|
||||
var size = 100.0;
|
||||
|
||||
// Build a polygon with count vertices.
|
||||
|
||||
var vertices:Array<Array<Float>> = [];
|
||||
|
||||
var angle = 0.0;
|
||||
|
||||
for (i in 0...count)
|
||||
{
|
||||
var x = Math.cos(angle) * size;
|
||||
var y = Math.sin(angle) * size;
|
||||
|
||||
vertices.push([x, y]);
|
||||
|
||||
angle += 2 * Math.PI / count;
|
||||
}
|
||||
|
||||
trace('vertices: ${vertices}');
|
||||
|
||||
var centerVertex = polygonSprite.build_vertex(0, 0);
|
||||
var firstVertex = -1;
|
||||
var lastVertex = -1;
|
||||
|
||||
for (vertex in vertices)
|
||||
{
|
||||
var x = vertex[0];
|
||||
var y = vertex[1];
|
||||
|
||||
var newVertex = polygonSprite.build_vertex(x, y);
|
||||
|
||||
if (firstVertex == -1)
|
||||
{
|
||||
firstVertex = newVertex;
|
||||
}
|
||||
|
||||
if (lastVertex != -1)
|
||||
{
|
||||
polygonSprite.add_tri(centerVertex, lastVertex, newVertex);
|
||||
}
|
||||
|
||||
lastVertex = newVertex;
|
||||
}
|
||||
|
||||
polygonSprite.add_tri(centerVertex, lastVertex, firstVertex);
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
super.destroy();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -59,6 +59,12 @@ class AddEventsCommand implements ChartEditorCommand
|
|||
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
|
||||
{
|
||||
var len:Int = events.length;
|
||||
|
|
|
@ -59,6 +59,12 @@ class AddNotesCommand implements ChartEditorCommand
|
|||
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
|
||||
{
|
||||
if (notes.length == 1)
|
||||
|
|
|
@ -34,7 +34,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
|
|||
|
||||
state.currentSongMetadata.timeChanges = timeChanges;
|
||||
|
||||
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.scrollPositionInPixels = 0;
|
||||
|
||||
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
|
@ -51,7 +56,18 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
|
|||
|
||||
state.currentSongMetadata.timeChanges = timeChanges;
|
||||
|
||||
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.scrollPositionInPixels = 0;
|
||||
|
||||
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
|
||||
|
|
|
@ -6,6 +6,8 @@ package funkin.ui.debug.charting.commands;
|
|||
*
|
||||
* To make a functionality compatible with the undo/redo history, create a new class
|
||||
* 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
|
||||
{
|
||||
|
@ -22,6 +24,15 @@ interface ChartEditorCommand
|
|||
*/
|
||||
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).
|
||||
* For example, return `Add Left Note` to display `Undo Add Left Note` in the menu.
|
||||
|
|
170
source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
Normal file
170
source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
Normal file
|
@ -0,0 +1,170 @@
|
|||
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
|
||||
{
|
||||
var hasNotes:Bool = false;
|
||||
var hasEvents:Bool = false;
|
||||
|
||||
// Wiggle copied notes.
|
||||
if (state.currentNoteSelection.length > 0)
|
||||
{
|
||||
hasNotes = true;
|
||||
|
||||
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 copied events.
|
||||
if (state.currentEventSelection.length > 0)
|
||||
{
|
||||
hasEvents = true;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display the "Copied Notes" text.
|
||||
if ((hasNotes || hasEvents) && state.txtCopyNotif != null)
|
||||
{
|
||||
var copiedString:String = '';
|
||||
if (hasNotes)
|
||||
{
|
||||
var copiedNotes:Int = state.currentNoteSelection.length;
|
||||
copiedString += '${copiedNotes} note';
|
||||
if (copiedNotes > 1) copiedString += 's';
|
||||
|
||||
if (hasEvents) copiedString += ' and ';
|
||||
}
|
||||
if (hasEvents)
|
||||
{
|
||||
var copiedEvents:Int = state.currentEventSelection.length;
|
||||
copiedString += '${state.currentEventSelection.length} event';
|
||||
if (copiedEvents > 1) copiedString += 's';
|
||||
}
|
||||
|
||||
state.txtCopyNotif.visible = true;
|
||||
state.txtCopyNotif.text = 'Copied ${copiedString} 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
var len:Int = notes.length + events.length;
|
||||
|
|
|
@ -10,17 +10,16 @@ import funkin.data.song.SongData.SongEventData;
|
|||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class DeselectAllItemsCommand implements ChartEditorCommand
|
||||
{
|
||||
var previousNoteSelection:Array<SongNoteData>;
|
||||
var previousEventSelection:Array<SongEventData>;
|
||||
var previousNoteSelection:Array<SongNoteData> = [];
|
||||
var previousEventSelection:Array<SongEventData> = [];
|
||||
|
||||
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
|
||||
{
|
||||
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
|
||||
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
|
||||
}
|
||||
public function new() {}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
this.previousNoteSelection = state.currentNoteSelection;
|
||||
this.previousEventSelection = state.currentEventSelection;
|
||||
|
||||
state.currentNoteSelection = [];
|
||||
state.currentEventSelection = [];
|
||||
|
||||
|
@ -35,6 +34,12 @@ class DeselectAllItemsCommand implements ChartEditorCommand
|
|||
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
|
||||
{
|
||||
return 'Deselect All Items';
|
||||
|
|
|
@ -45,16 +45,27 @@ class DeselectItemsCommand implements ChartEditorCommand
|
|||
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
|
||||
{
|
||||
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 $dir Items';
|
||||
return 'Deselect ${notes.length} ${isPlural ? 'Notes' : 'Note'}';
|
||||
}
|
||||
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 oldLength: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.oldLength = note.length;
|
||||
this.newLength = newLength;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
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.noteDisplayDirty = true;
|
||||
|
@ -36,7 +44,8 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
|||
{
|
||||
state.playSound(Paths.sound('chartingSounds/undo'));
|
||||
|
||||
note.length = oldLength;
|
||||
// Always use milliseconds for undoing
|
||||
this.note.length = oldLength;
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -45,8 +54,31 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
|||
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
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
var len:Int = notes.length;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue