Merge remote-tracking branch 'origin/rewrite/master' into input-offsets

This commit is contained in:
EliteMasterEric
2024-03-23 14:38:50 -04:00
139 changed files with 5780 additions and 2401 deletions
+32 -7
View File
@@ -3,21 +3,46 @@ description: "sets up haxe shit, using HMM!"
runs:
using: "composite"
steps:
- uses: funkincrew/ci-haxe@v3
- name: Install Haxe lol
uses: funkincrew/ci-haxe@v3.1.0
with:
haxe-version: 4.3.3
- name: Config haxelib
run: |
haxelib config
haxelib --never install haxelib 4.1.0 --global
haxelib --never deleterepo || true
haxelib --never newrepo
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
haxelib --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
shell: bash
- name: Installing Haxe lol
- name: Gather debug info
run: |
cat << EOF >> "$GITHUB_STEP_SUMMARY"
## haxe
- version: \`$(haxe -version)\`
- exe: \`$(which haxe)\`
## haxelib
- version: \`$(haxelib version)\`
- exe: \`$(which haxelib)\`
- path: \`$HAXEPATH\`
### local
- config: \`$(haxelib config)\`
- path: \`$(haxelib path haxelib || true)\`
### global
- config: \`$(haxelib config --global)\`
- path: \`$(haxelib path haxelib --global || true)\`
### system
- version: \`$(haxelib --system version)\`
- local: \`$(haxelib --system config)\`
- global: \`$(haxelib --system config --global)\`
EOF
shell: bash
- name: Install hmm
# hmm only supports global installs
run: |
haxe -version
haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git master
haxelib version
haxelib --global install hmm
shell: bash
- name: dependency install cache
- name: Restore cached dependencies
id: cache-hmm
uses: actions/cache@v4
with:
+44 -71
View File
@@ -8,71 +8,80 @@ jobs:
runs-on: [self-hosted, linux]
container: ubuntu:23.10
steps:
- name: prepare container
- name: Install tools missing in container
run: |
apt update
apt install sudo git curl unzip -y
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: get token from gh app
apt install -y sudo git curl unzip
- name: Fix git config on posix runner
run: |
git config --global --add safe.directory ${{ github.workspace }}
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- uses: ./.github/actions/setup-haxeshit
- name: gather game dependencies
- name: Install Haxe, dependencies
uses: ./.github/actions/setup-haxeshit
- name: Install native 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
apt install -y \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
libasound2-dev
- name: Build game
run: |
haxelib run lime build html5 -release --times -DGITHUB_BUILD
ls
- uses: ./.github/actions/upload-itch
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/html5/bin
target: html5
create-nightly-win:
runs-on: windows-latest
runs-on: [self-hosted, windows]
defaults:
run:
shell: bash
steps:
- name: get token from gh app
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
- 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
- name: Install Haxe, dependencies
uses: ./.github/actions/setup-haxeshit
- name: Setup build cache
run: |
mkdir -p ${{ runner.temp }}\hxcpp_cache
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-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: build game
${{ runner.temp }}/hxcpp_cache
key: ${{ runner.os }}-build-win-${{ github.ref_name }}
- name: Build game
run: |
haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER -DGITHUB_BUILD
dir
haxelib run lime build windows -v -release -DGITHUB_BUILD
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
- uses: ./.github/actions/upload-itch
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY }}
build-dir: export/release/windows/bin
@@ -80,78 +89,42 @@ jobs:
create-nightly-mac:
runs-on: [self-hosted, macos]
steps:
- name: prepare container
- name: Fix git config on posix runner
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: get token from gh app
git config --global --add safe.directory ${{ github.workspace }}
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
- 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
- name: Install Haxe, dependencies
uses: ./.github/actions/setup-haxeshit
- name: Setup build cache
run: |
mkdir -p ${{ runner.temp }}/hxcpp_cache
- name: restore build 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') }}
key: ${{ runner.os }}-build-mac-${{ github.ref_name }}
- name: Build game
run: |
haxelib run lime build macos -release --times -DGITHUB_BUILD
ls
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache"
- uses: ./.github/actions/upload-itch
- name: Upload build artifacts
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
# steps:
# - 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 }}
# - 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"
+3
View File
@@ -1,7 +1,10 @@
.DS_STORE
.haxelib/
.vs/
APIStuff.hx
dump/
export/
RECOVER_*.fla
shitAudio/
.build_time
.swp
+2 -6
View File
@@ -1,10 +1,6 @@
[submodule "assets"]
path = assets
url = https://github.com/FunkinCrew/Funkin-history-rewrite-assets
branch = master
update = merge
url = https://github.com/FunkinCrew/Funkin-Assets-secret
[submodule "art"]
path = art
url = https://github.com/FunkinCrew/Funkin-history-rewrite-art
branch = master
update = merge
url = https://github.com/FunkinCrew/Funkin-Art-secret
+11
View File
@@ -0,0 +1,11 @@
# Ignore artifacts
export
# Ignore all JSONS in the images folder (including FlxAnimate JSONs)
assets/preload/images
assets/shared/images
assets/weekend1/images
# Don't ignore data files
# TODO: These don't work.
!assets/preload/data/
+27 -12
View File
@@ -71,6 +71,7 @@
"haxe.displayPort": "auto",
"haxe.enableCompilationServer": false,
"haxe.enableServerView": true,
"haxe.displayServer": {
"arguments": ["-v"]
},
@@ -84,7 +85,7 @@
},
"projectManager.git.baseFolders": ["./"],
"haxecheckstyle.sourceFolders": ["src", "Source"],
"haxecheckstyle.sourceFolders": ["src", "source"],
"haxecheckstyle.externalSourceRoots": [],
"haxecheckstyle.configurationFile": "checkstyle.json",
"haxecheckstyle.codeSimilarityBufferSize": 100,
@@ -93,7 +94,7 @@
{
"label": "Windows / Debug",
"target": "windows",
"args": ["-debug"]
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug",
@@ -103,7 +104,7 @@
{
"label": "Windows / Debug (FlxAnimate Test)",
"target": "windows",
"args": ["-debug", "-DANIMATE"]
"args": ["-debug", "-DANIMATE", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
@@ -113,7 +114,7 @@
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
"args": ["-debug", "-DFREEPLAY"]
"args": ["-debug", "-DFREEPLAY", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
@@ -123,7 +124,11 @@
{
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
"target": "windows",
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
"args": [
"-debug",
"-DSONG=bopeebo -DDIFFICULTY=normal",
"-DFORCE_DEBUG_VERSION"
]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
@@ -133,7 +138,7 @@
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
"args": ["-debug", "-DDIALOGUE"]
"args": ["-debug", "-DDIALOGUE", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Conversation Test)",
@@ -143,7 +148,7 @@
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
"args": ["-debug", "-DCHARTING"]
"args": ["-debug", "-DCHARTING", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Chart Editor)",
@@ -153,7 +158,7 @@
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
"args": ["-debug", "-DANIMDEBUG"]
"args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
@@ -163,7 +168,7 @@
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
"args": ["-debug", "-DLATENCY"]
"args": ["-debug", "-DLATENCY", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Latency Test)",
@@ -173,7 +178,17 @@
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
"args": ["-debug", "-DWAVEFORM"]
"args": ["-debug", "-DWAVEFORM", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "Windows / Release",
"target": "windows",
"args": ["-release"]
},
{
"label": "Windows / Release (GitHub Actions)",
"target": "windows",
"args": ["-release", "-DGITHUB_BUILD"]
},
{
"label": "HashLink / Debug (Waveform Test)",
@@ -183,12 +198,12 @@
{
"label": "HTML5 / Debug",
"target": "html5",
"args": ["-debug"]
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
"args": ["-debug", "-watch"]
"args": ["-debug", "-watch", "-DFORCE_DEBUG_VERSION"]
}
],
"cmake.configureOnOpen": false,
+33 -30
View File
@@ -22,8 +22,8 @@
<set name="BUILD_DIR" value="export/release" unless="debug" />
<set name="BUILD_DIR" value="export/32bit" if="32bit" />
<classpath name="source" />
<assets path="assets/preload" rename="assets" exclude="*.ogg" if="web" />
<assets path="assets/preload" rename="assets" exclude="*.mp3" unless="web" />
<assets path="assets/preload" rename="assets" exclude="*.ogg|*.wav" if="web" />
<assets path="assets/preload" rename="assets" exclude="*.mp3|*.wav" unless="web" />
<define name="PRELOAD_ALL" unless="web" />
<define name="NO_PRELOAD_ALL" unless="PRELOAD_ALL" />
<section if="PRELOAD_ALL">
@@ -53,28 +53,28 @@
<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" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.mp3|*.wav" unless="web" />
<!-- <assets path='example_mods' rename='mods' embed='false'/> -->
<!--
AUTOMATICALLY MOVING EXAMPLE MODS INTO THE BUILD CAUSES ISSUES
@@ -91,8 +91,11 @@
NOT USING A DIRECT THING TO THE ASSET!!!
-->
<assets path="assets/fonts" embed="true" />
<!-- If compiled via github actions, enable force debug -->
<set name="FORCE_DEBUG_VERSION" if="GITHUB_BUILD"/>
<!-- If compiled via github actions, show debug version number. -->
<define name="FORCE_DEBUG_VERSION" if="GITHUB_BUILD" />
<define name="NO_REDIRECT_ASSETS_FOLDER" if="GITHUB_BUILD" />
<!-- _______________________________ Libraries ______________________________ -->
<haxelib name="lime" /> <!-- Game engine backend -->
<haxelib name="openfl" /> <!-- Game engine backend -->
@@ -108,7 +111,7 @@
<haxelib name="flixel-text-input" /> <!-- Improved text field rendering for HaxeUI -->
<haxelib name="polymod" /> <!-- Modding framework -->
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
<haxelib name="hxCodec" if="desktop release" /> <!-- Video playback -->
<haxelib name="hxCodec" if="desktop" /> <!-- Video playback -->
<haxelib name="json2object" /> <!-- JSON parsing -->
<haxelib name="thx.semver" /> <!-- Version string handling -->
@@ -194,7 +197,7 @@
<!-- Uncomment this to wipe your input settings. -->
<!-- <haxedef name="CLEAR_INPUT_SAVE"/> -->
<section if="debug" unless="NO_REDIRECT_ASSETS_FOLDER || html5">
<section if="debug" unless="NO_REDIRECT_ASSETS_FOLDER || html5 || GITHUB_BUILD">
<!--
Use the parent assets folder rather than the exported one
No more will we accidentally undo our changes!
@@ -216,9 +219,9 @@
<postbuild haxe="source/Postbuild.hx"/> -->
<!-- Enable this on platforms which do not support dropping files onto the window. -->
<set name="FILE_DROP_UNSUPPORTED" if="mac" />
<haxedef name="FILE_DROP_UNSUPPORTED" if="mac" />
<section unless="FILE_DROP_UNSUPPORTED">
<set name="FILE_DROP_SUPPORTED" />
<haxedef name="FILE_DROP_SUPPORTED" />
</section>
<!-- Options for Polymod -->
+1 -1
Submodule assets updated: e76470d667...5267439151
+10 -39
View File
@@ -23,10 +23,10 @@
},
{
"props": {
"allowSingleArgParens": false,
"allowReturn": false,
"allowFunction": false,
"allowCurlyBody": false
"allowSingleArgParens": true,
"allowReturn": true,
"allowFunction": true,
"allowCurlyBody": true
},
"type": "ArrowFunction"
},
@@ -79,8 +79,8 @@
{
"props": {
"ignoreExtern": true,
"format": "^[A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$",
"tokens": []
"format": "^[a-z][A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$",
"tokens": ["INLINE", "NOTINLINE"]
},
"type": "ConstantName"
},
@@ -178,13 +178,7 @@
"fieldType": "BOTH",
"requireReturn": true,
"ignoreOverride": true,
"tokens": [
"ABSTRACT_DEF",
"CLASS_DEF",
"ENUM_DEF",
"INTERFACE_DEF",
"TYPEDEF_DEF"
],
"tokens": ["ABSTRACT_DEF", "CLASS_DEF", "ENUM_DEF", "INTERFACE_DEF"],
"modifier": "PUBLIC",
"excludeNames": ["new", "toString"]
},
@@ -198,10 +192,6 @@
},
"type": "FileLength"
},
{
"props": {},
"type": "Final"
},
{
"props": {
"option": "upperCase"
@@ -375,7 +365,7 @@
},
{
"props": {
"max": 1
"max": 4
},
"type": "NestedIfDepth"
},
@@ -387,7 +377,7 @@
},
{
"props": {
"option": "nullDefault"
"option": "questionMark"
},
"type": "NullableParameter"
},
@@ -396,7 +386,6 @@
"oldFunctionTypePolicy": "none",
"unaryOpPolicy": "none",
"intervalOpPolicy": "none",
"newFunctionTypePolicy": "around",
"ternaryOpPolicy": "around",
"boolOpPolicy": "around",
@@ -589,19 +578,7 @@
},
{
"props": {
"ignoreEnumAbstractValues": true
},
"type": "Type"
},
{
"props": {
"tokens": [
"ABSTRACT_DEF",
"CLASS_DEF",
"ENUM_DEF",
"INTERFACE_DEF",
"TYPEDEF_DEF"
]
"tokens": ["CLASS_DEF", "ENUM_DEF", "INTERFACE_DEF", "TYPEDEF_DEF"]
},
"type": "TypeDocComment"
},
@@ -630,12 +607,6 @@
},
"type": "UnusedLocalVar"
},
{
"props": {
"severity": "IGNORE"
},
"type": "VariableInitialisation"
},
{
"props": {
"typeHintPolicy": "enforce_all",
+6 -6
View File
@@ -11,7 +11,7 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "4d054bd10b05bb1309a0ba3427ffa5378e0b4b99",
"ref": "5823c46b4e7410372d58b99f8d5c52fc18b3cb3d",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@@ -54,14 +54,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "8a7846b",
"ref": "0212d8fdfcafeb5f0d5a41e1ddba8ff21d0e183b",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "e9f880522e27134b29df4067f82df7d7e5237b70",
"ref": "63a906a6148958dbfde8c7b48d90b0693767fd95",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
@@ -73,8 +73,8 @@
"name": "hxCodec",
"type": "git",
"dir": null,
"ref": "c8c47e706ad82a423783006ed901b6d93c89a421",
"url": "https://github.com/polybiusproxy/hxCodec"
"ref": "387e1665d6feb5762358134f168e6ebfe46acec8",
"url": "https://github.com/FunkinCrew/hxCodec"
},
{
"name": "hxcpp",
@@ -146,7 +146,7 @@
"name": "polymod",
"type": "git",
"dir": null,
"ref": "d5a3b8995f64d20b95f844454e8c3b38c3d3a9fa",
"ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac",
"url": "https://github.com/larsiusprime/polymod"
},
{
+30 -31
View File
@@ -1,31 +1,30 @@
{
"disableFormatting": false,
"indentation": {
"character": " ",
"tabWidth": 2,
"indentCaseLabels": true
},
"lineEnds": {
"anonFunctionCurly": {
"emptyCurly": "break",
"leftCurly": "after",
"rightCurly": "both"
},
"leftCurly": "both",
"rightCurly": "both"
},
"sameLine": {
"ifBody": "same",
"ifElse": "next",
"doWhile": "next",
"tryBody": "next",
"tryCatch": "next"
},
"whitespace": {
"switchPolicy": "around"
}
}
{
"disableFormatting": false,
"indentation": {
"character": " ",
"tabWidth": 2,
"indentCaseLabels": true
},
"lineEnds": {
"anonFunctionCurly": {
"emptyCurly": "break",
"leftCurly": "after",
"rightCurly": "both"
},
"leftCurly": "both",
"rightCurly": "both"
},
"sameLine": {
"ifBody": "same",
"ifElse": "next",
"doWhile": "next",
"tryBody": "next",
"tryCatch": "next"
},
"whitespace": {
"switchPolicy": "around"
}
}
+24 -12
View File
@@ -11,9 +11,11 @@ import openfl.display.Sprite;
import openfl.events.Event;
import openfl.Lib;
import openfl.media.Video;
import funkin.util.CLIUtil;
import openfl.net.NetStream;
/**
* The main class which initializes HaxeFlixel and starts the game in its initial state.
*/
class Main extends Sprite
{
var gameWidth:Int = 1280; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom).
@@ -76,35 +78,45 @@ class Main extends Sprite
var netStream:NetStream;
var overlay:Sprite;
/**
* A frame counter displayed at the top left.
*/
public static var fpsCounter:FPS;
/**
* A RAM counter displayed at the top left.
*/
public static var memoryCounter:MemoryCounter;
function setupGame():Void
{
/**
* The `zoom` argument of FlxGame was removed in the dev branch of Flixel,
* since it was considered confusing and unintuitive.
* If you want to change how the game scales when you resize the window,
* you can use `FlxG.scaleMode`.
* -Eric
*/
initHaxeUI();
// addChild gets called by the user settings code.
fpsCounter = new FPS(10, 3, 0xFFFFFF);
// addChild(fpsCounter); // Handled by Preferences.init
#if !html5
// addChild gets called by the user settings code.
// TODO: disabled on HTML5 (todo: find another method that works?)
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
// addChild(memoryCounter);
#end
// George recommends binding the save before FlxGame is created.
Save.load();
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen);
addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen));
// FlxG.game._customSoundTray wants just the class, it calls new from
// create() in there, which gets called when it's added to stage
// which is why it needs to be added before addChild(game) here
@:privateAccess
game._customSoundTray = funkin.ui.options.FunkinSoundTray;
addChild(game);
#if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#else
trace('hxcpp_debug_server is disabled! This build does not support debugging.');
#end
}
+33 -4
View File
@@ -1,11 +1,40 @@
package source; // Yeah, I know...
import sys.FileSystem;
import sys.io.File;
/**
* A script which executes after the game is built.
*/
class Postbuild
{
static function main()
{
trace('Postbuild');
static inline final BUILD_TIME_FILE:String = '.build_time';
// TODO: Maybe put a 'Build took X seconds' message here?
static function main():Void
{
printBuildTime();
}
static function printBuildTime():Void
{
// get buildEnd before fs operations since they are blocking
var end:Float = Sys.time();
if (FileSystem.exists(BUILD_TIME_FILE))
{
var fi:sys.io.FileInput = File.read(BUILD_TIME_FILE);
var start:Float = fi.readDouble();
fi.close();
sys.FileSystem.deleteFile(BUILD_TIME_FILE);
var buildTime:Float = roundToTwoDecimals(end - start);
trace('Build took: ${buildTime} seconds');
}
}
static function roundToTwoDecimals(value:Float):Float
{
return Math.round(value * 100) / 100;
}
}
+18 -2
View File
@@ -1,9 +1,25 @@
package source; // Yeah, I know...
import sys.io.File;
/**
* A script which executes before the game is built.
*/
class Prebuild
{
static function main()
static inline final BUILD_TIME_FILE:String = '.build_time';
static function main():Void
{
trace('Prebuild');
saveBuildTime();
trace('Building...');
}
static function saveBuildTime():Void
{
var fo:sys.io.FileOutput = File.write(BUILD_TIME_FILE);
var now:Float = Sys.time();
fo.writeDouble(now);
fo.close();
}
}
@@ -1,234 +0,0 @@
package flixel.addons.transition;
import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
/**
* A `FlxSubState` which can perform visual transitions
*
* Usage:
*
* First, extend `FlxTransitionableSubState` as ie, `FooState`.
*
* Method 1:
*
* ```haxe
* var in:TransitionData = new TransitionData(...); // add your data where "..." is
* var out:TransitionData = new TransitionData(...);
*
* FlxG.switchState(() -> new FooState(in,out));
* ```
*
* Method 2:
*
* ```haxe
* FlxTransitionableSubState.defaultTransIn = new TransitionData(...);
* FlxTransitionableSubState.defaultTransOut = new TransitionData(...);
*
* FlxG.switchState(() -> new FooState());
* ```
*/
class FlxTransitionableSubState extends FlxSubState
{
// global default transitions for ALL states, used if transIn/transOut are null
public static var defaultTransIn(get, set):TransitionData;
static function get_defaultTransIn():TransitionData
{
return FlxTransitionableState.defaultTransIn;
}
static function set_defaultTransIn(value:TransitionData):TransitionData
{
return FlxTransitionableState.defaultTransIn = value;
}
public static var defaultTransOut(get, set):TransitionData;
static function get_defaultTransOut():TransitionData
{
return FlxTransitionableState.defaultTransOut;
}
static function set_defaultTransOut(value:TransitionData):TransitionData
{
return FlxTransitionableState.defaultTransOut = value;
}
public static var skipNextTransIn(get, set):Bool;
static function get_skipNextTransIn():Bool
{
return FlxTransitionableState.skipNextTransIn;
}
static function set_skipNextTransIn(value:Bool):Bool
{
return FlxTransitionableState.skipNextTransIn = value;
}
public static var skipNextTransOut(get, set):Bool;
static function get_skipNextTransOut():Bool
{
return FlxTransitionableState.skipNextTransOut;
}
static function set_skipNextTransOut(value:Bool):Bool
{
return FlxTransitionableState.skipNextTransOut = value;
}
// beginning & ending transitions for THIS state:
public var transIn:TransitionData;
public var transOut:TransitionData;
public var hasTransIn(get, never):Bool;
public var hasTransOut(get, never):Bool;
/**
* Create a state with the ability to do visual transitions
* @param TransIn Plays when the state begins
* @param TransOut Plays when the state ends
*/
public function new(?TransIn:TransitionData, ?TransOut:TransitionData)
{
transIn = TransIn;
transOut = TransOut;
if (transIn == null && defaultTransIn != null)
{
transIn = defaultTransIn;
}
if (transOut == null && defaultTransOut != null)
{
transOut = defaultTransOut;
}
super();
}
override function destroy():Void
{
super.destroy();
transIn = null;
transOut = null;
_onExit = null;
}
override function create():Void
{
super.create();
transitionIn();
}
override function startOutro(onOutroComplete:() -> Void)
{
if (!hasTransOut) onOutroComplete();
else if (!_exiting)
{
// play the exit transition, and when it's done call FlxG.switchState
_exiting = true;
transitionOut(onOutroComplete);
if (skipNextTransOut)
{
skipNextTransOut = false;
finishTransOut();
}
}
}
/**
* Starts the in-transition. Can be called manually at any time.
*/
public function transitionIn():Void
{
if (transIn != null && transIn.type != NONE)
{
if (skipNextTransIn)
{
skipNextTransIn = false;
if (finishTransIn != null)
{
finishTransIn();
}
return;
}
var _trans = createTransition(transIn);
_trans.setStatus(FULL);
openSubState(_trans);
_trans.finishCallback = finishTransIn;
_trans.start(OUT);
}
}
/**
* Starts the out-transition. Can be called manually at any time.
*/
public function transitionOut(?OnExit:Void->Void):Void
{
_onExit = OnExit;
if (hasTransOut)
{
var _trans = createTransition(transOut);
_trans.setStatus(EMPTY);
openSubState(_trans);
_trans.finishCallback = finishTransOut;
_trans.start(IN);
}
else
{
_onExit();
}
}
var transOutFinished:Bool = false;
var _exiting:Bool = false;
var _onExit:Void->Void;
function get_hasTransIn():Bool
{
return transIn != null && transIn.type != NONE;
}
function get_hasTransOut():Bool
{
return transOut != null && transOut.type != NONE;
}
function createTransition(data:TransitionData):Transition
{
return switch (data.type)
{
case TILES: new Transition(data);
case FADE: new Transition(data);
default: null;
}
}
function finishTransIn()
{
closeSubState();
}
function finishTransOut()
{
transOutFinished = true;
if (!_exiting)
{
closeSubState();
}
if (_onExit != null)
{
_onExit();
}
}
}
+38 -22
View File
@@ -3,7 +3,6 @@ package funkin;
import funkin.util.Constants;
import flixel.util.FlxSignal;
import flixel.math.FlxMath;
import funkin.play.song.Song.SongDifficulty;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongDataUtils;
import funkin.save.Save;
@@ -142,6 +141,9 @@ class Conductor
return beatLengthMs / timeSignatureNumerator;
}
/**
* The numerator for the current time signature (the `3` in `3/4`).
*/
public var timeSignatureNumerator(get, never):Int;
function get_timeSignatureNumerator():Int
@@ -151,6 +153,9 @@ class Conductor
return currentTimeChange.timeSignatureNum;
}
/**
* The denominator for the current time signature (the `4` in `3/4`).
*/
public var timeSignatureDenominator(get, never):Int;
function get_timeSignatureDenominator():Int
@@ -284,7 +289,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 function forceBPM(?bpm:Float = null)
public function forceBPM(?bpm:Float):Void
{
if (bpm != null)
{
@@ -292,7 +297,7 @@ class Conductor
}
else
{
// trace('[CONDUCTOR] Resetting BPM to default');
trace('[CONDUCTOR] Resetting BPM to default');
}
this.bpmOverride = bpm;
@@ -310,15 +315,15 @@ class Conductor
{
if (songPos == null)
{
// Take into account instrumental and file format song offsets.
songPos = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset + audioVisualOffset) : 0.0;
songPos = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0;
}
else
songPos += applyOffsets ? instrumentalOffset + formatOffset + audioVisualOffset : 0;
var oldMeasure = this.currentMeasure;
var oldBeat = this.currentBeat;
var oldStep = this.currentStep;
// Take into account instrumental and file format song offsets.
songPos += applyOffsets ? (instrumentalOffset + formatOffset + audioVisualOffset) : 0;
var oldMeasure:Float = this.currentMeasure;
var oldBeat:Float = this.currentBeat;
var oldStep:Float = this.currentStep;
// Set the song position we are at (for purposes of calculating note positions, etc).
this.songPosition = songPos;
@@ -409,39 +414,43 @@ class Conductor
return this.songPosition;
}
public function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
/**
* Apply the `SongTimeChange` data from the song metadata to this Conductor.
* @param songTimeChanges The SongTimeChanges.
*/
public function mapTimeChanges(songTimeChanges:Array<SongTimeChange>):Void
{
timeChanges = [];
// Sort in place just in case it's out of order.
SongDataUtils.sortTimeChanges(songTimeChanges);
for (currentTimeChange in songTimeChanges)
for (songTimeChange in songTimeChanges)
{
// TODO: Maybe handle this different?
// Do we care about BPM at negative timestamps?
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0;
if (songTimeChange.timeStamp < 0.0) songTimeChange.timeStamp = 0.0;
if (currentTimeChange.timeStamp <= 0.0)
if (songTimeChange.timeStamp <= 0.0)
{
currentTimeChange.beatTime = 0.0;
songTimeChange.beatTime = 0.0;
}
else
{
// Calculate the beat time of this timestamp.
currentTimeChange.beatTime = 0.0;
songTimeChange.beatTime = 0.0;
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
if (songTimeChange.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),
songTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
+ ((songTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
4);
}
}
timeChanges.push(currentTimeChange);
timeChanges.push(songTimeChange);
}
if (timeChanges.length > 0)
@@ -455,6 +464,8 @@ class Conductor
/**
* Given a time in milliseconds, return a time in steps.
* @param ms The time in milliseconds.
* @return The time in steps.
*/
public function getTimeInSteps(ms:Float):Float
{
@@ -484,7 +495,7 @@ class Conductor
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs;
resultStep += resultFractionalStep; // Math.floor();
resultStep += resultFractionalStep;
return resultStep;
}
@@ -492,6 +503,8 @@ class Conductor
/**
* Given a time in steps and fractional steps, return a time in milliseconds.
* @param stepTime The time in steps.
* @return The time in milliseconds.
*/
public function getStepTimeInMs(stepTime:Float):Float
{
@@ -528,6 +541,8 @@ class Conductor
/**
* Given a time in beats and fractional beats, return a time in milliseconds.
* @param beatTime The time in beats.
* @return The time in milliseconds.
*/
public function getBeatTimeInMs(beatTime:Float):Float
{
@@ -563,7 +578,8 @@ class Conductor
}
/**
* @param conductorToUse defaults to Conductor.instance if null
* Adds Conductor fields to the Flixel debugger variable display.
* @param conductorToUse The conductor to use. Defaults to `Conductor.instance`.
*/
public static function watchQuick(?conductorToUse:Conductor):Void
{
+39 -1
View File
@@ -5,7 +5,42 @@ package funkin;
*/
class Highscore
{
/**
* Keeps track of notes hit for the current song
* and how accurate you were with each note (bad, missed, shit, etc.)
*/
public static var tallies:Tallies = new Tallies();
/**
* Keeps track of notes hit for the current WEEK / level
* for use with storymode, or likely any other "playlist" esque option
*/
public static var talliesLevel:Tallies = new Tallies();
/**
* Produces a new Tallies object which represents the sum of two existing Tallies
* @param newTally The first tally
* @param baseTally The second tally
* @return The combined tally
*/
public static function combineTallies(newTally:Tallies, baseTally:Tallies):Tallies
{
var combinedTally:Tallies = new Tallies();
combinedTally.missed = newTally.missed + baseTally.missed;
combinedTally.shit = newTally.shit + baseTally.shit;
combinedTally.bad = newTally.bad + baseTally.bad;
combinedTally.good = newTally.good + baseTally.good;
combinedTally.sick = newTally.sick + baseTally.sick;
combinedTally.totalNotes = newTally.totalNotes + baseTally.totalNotes;
combinedTally.totalNotesHit = newTally.totalNotesHit + baseTally.totalNotesHit;
// Current combo = use most recent.
combinedTally.combo = newTally.combo;
// Max combo = use maximum value.
combinedTally.maxCombo = Std.int(Math.max(newTally.maxCombo, baseTally.maxCombo));
return combinedTally;
}
}
@:forward
@@ -29,6 +64,9 @@ abstract Tallies(RawTallies)
}
}
/**
* A structure object containing the data for highscore tallies.
*/
typedef RawTallies =
{
var combo:Int;
@@ -51,7 +89,7 @@ typedef RawTallies =
var totalNotesHit:Int;
/**
* How many notes PASSED BY AND/OR HIT!!!
* How many notes in the current chart
*/
var totalNotes:Int;
}
+110 -84
View File
@@ -12,7 +12,6 @@ import flixel.math.FlxRect;
import flixel.FlxSprite;
import flixel.system.debug.log.LogStyle;
import flixel.util.FlxColor;
import funkin.ui.options.PreferencesMenu;
import funkin.util.macro.MacroUtil;
import funkin.util.WindowUtil;
import funkin.play.PlayStatePlaylist;
@@ -24,13 +23,15 @@ import funkin.data.stage.StageRegistry;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState;
import funkin.util.CLIUtil;
import funkin.util.CLIUtil.CLIParams;
import funkin.ui.transition.LoadingState;
import funkin.util.TimerUtil;
import funkin.util.TrackerUtil;
#if discord_rpc
import Discord.DiscordClient;
#end
@@ -66,7 +67,7 @@ class InitState extends FlxState
/**
* Setup a bunch of important Flixel stuff.
*/
function setupShit()
function setupShit():Void
{
//
// GAME SETUP
@@ -88,70 +89,7 @@ class InitState extends FlxState
// Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30;
//
// FLIXEL DEBUG SETUP
//
#if (debug || FORCE_DEBUG_VERSION)
// Disable using ~ to open the console (we use that for the Editor menu)
FlxG.debugger.toggleKeys = [F2];
// Adds an additional Close Debugger button.
// This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol!
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
FlxG.debugger.visible = false;
});
// Adds a red button to the debugger.
// This pauses the game AND the music! This ensures the Conductor stops.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() {
if (FlxG.vcr.paused)
{
FlxG.vcr.resume();
for (snd in FlxG.sound.list)
{
snd.resume();
}
FlxG.sound.music.resume();
}
else
{
FlxG.vcr.pause();
for (snd in FlxG.sound.list)
{
snd.pause();
}
FlxG.sound.music.pause();
}
});
// Adds a blue button to the debugger.
// This skips forward in the song.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() {
FlxG.game.debugger.vcr.onStep();
for (snd in FlxG.sound.list)
{
snd.pause();
snd.time += FlxG.elapsed * 1000;
}
FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000;
});
#end
// Make errors and warnings less annoying.
#if FORCE_DEBUG_VERSION
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
#end
setupFlixelDebug();
//
// FLIXEL TRANSITIONS
@@ -202,8 +140,12 @@ class InitState extends FlxState
//
// Plugins provide a useful interface for globally active Flixel objects,
// that receive update events regardless of the current state.
// TODO: Move Module behavior to a Flixel plugin.
// TODO: Move scripted Module behavior to a Flixel plugin.
#if debug
funkin.util.plugins.MemoryGCPlugin.initialize();
#end
funkin.util.plugins.EvacuateDebugPlugin.initialize();
funkin.util.plugins.ForceCrashPlugin.initialize();
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
funkin.util.plugins.ScreenshotPlugin.initialize();
funkin.util.plugins.VolumePlugin.initialize();
@@ -216,7 +158,7 @@ class InitState extends FlxState
// 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();
var perfStart:Float = TimerUtil.start();
SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries();
@@ -224,18 +166,18 @@ class InitState extends FlxState
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries();
AlbumRegistry.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.
// TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers and more prone to syntax errors.
// Move it to use a BaseRegistry.
CharacterDataParser.loadCharacterCache();
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
ModuleHandler.callOnCreate();
var perfEnd = haxe.Timer.stamp();
trace('Parsing game data took ${Math.floor((perfEnd - perfStart) * 1000)}ms.');
trace('Parsing game data took: ${TimerUtil.ms(perfStart)}');
}
/**
@@ -247,25 +189,35 @@ class InitState extends FlxState
*/
function startGame():Void
{
#if SONG // -DSONG=bopeebo
#if SONG
// -DSONG=bopeebo
startSong(defineSong(), defineDifficulty());
#elseif LEVEL // -DLEVEL=week1 -DDIFFICULTY=hard
#elseif LEVEL
// -DLEVEL=week1 -DDIFFICULTY=hard
startLevel(defineLevel(), defineDifficulty());
#elseif FREEPLAY // -DFREEPLAY
#elseif FREEPLAY
// -DFREEPLAY
FlxG.switchState(() -> new funkin.ui.freeplay.FreeplayState());
#elseif DIALOGUE // -DDIALOGUE
#elseif DIALOGUE
// -DDIALOGUE
FlxG.switchState(() -> new funkin.ui.debug.dialogue.ConversationDebugState());
#elseif ANIMATE // -DANIMATE
#elseif ANIMATE
// -DANIMATE
FlxG.switchState(() -> new funkin.ui.debug.anim.FlxAnimateTest());
#elseif WAVEFORM // -DWAVEFORM
#elseif WAVEFORM
// -DWAVEFORM
FlxG.switchState(() -> new funkin.ui.debug.WaveformTestState());
#elseif CHARTING // -DCHARTING
#elseif CHARTING
// -DCHARTING
FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState());
#elseif STAGEBUILD // -DSTAGEBUILD
#elseif STAGEBUILD
// -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
#elseif ANIMDEBUG // -DANIMDEBUG
#elseif ANIMDEBUG
// -DANIMDEBUG
FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
#elseif LATENCY // -DLATENCY
#elseif LATENCY
// -DLATENCY
FlxG.switchState(() -> new funkin.LatencyState());
#else
startGameNormally();
@@ -346,6 +298,80 @@ class InitState extends FlxState
});
}
function setupFlixelDebug():Void
{
//
// FLIXEL DEBUG SETUP
//
#if (debug || FORCE_DEBUG_VERSION)
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
// Disable using ~ to open the console (we use that for the Editor menu)
FlxG.debugger.toggleKeys = [F2];
TrackerUtil.initTrackers();
// Adds an additional Close Debugger button.
// This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol!
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
FlxG.debugger.visible = false;
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
});
// Adds a red button to the debugger.
// This pauses the game AND the music! This ensures the Conductor stops.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() {
if (FlxG.vcr.paused)
{
FlxG.vcr.resume();
for (snd in FlxG.sound.list)
{
snd.resume();
}
FlxG.sound.music.resume();
}
else
{
FlxG.vcr.pause();
for (snd in FlxG.sound.list)
{
snd.pause();
}
FlxG.sound.music.pause();
}
});
// Adds a blue button to the debugger.
// This skips forward in the song.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() {
FlxG.game.debugger.vcr.onStep();
for (snd in FlxG.sound.list)
{
snd.pause();
snd.time += FlxG.elapsed * 1000;
}
FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000;
});
#end
}
function defineSong():String
{
return MacroUtil.getDefine('SONG');
+41 -27
View File
@@ -11,130 +11,144 @@ class Paths
{
static var currentLevel:String;
static public function setCurrentLevel(name:String)
public static function setCurrentLevel(name:String):Void
{
currentLevel = name.toLowerCase();
}
static function getPath(file:String, type:AssetType, library:Null<String>)
public static function stripLibrary(path:String):String
{
var parts:Array<String> = path.split(':');
if (parts.length < 2) return path;
return parts[1];
}
public static function getLibrary(path:String):String
{
var parts:Array<String> = path.split(':');
if (parts.length < 2) return 'preload';
return parts[0];
}
static function getPath(file:String, type:AssetType, library:Null<String>):String
{
if (library != null) return getLibraryPath(file, library);
if (currentLevel != null)
{
var levelPath = getLibraryPathForce(file, currentLevel);
var levelPath:String = getLibraryPathForce(file, currentLevel);
if (OpenFlAssets.exists(levelPath, type)) return levelPath;
}
var levelPath = getLibraryPathForce(file, "shared");
var levelPath:String = getLibraryPathForce(file, 'shared');
if (OpenFlAssets.exists(levelPath, type)) return levelPath;
return getPreloadPath(file);
}
static public function getLibraryPath(file:String, library = "preload")
public static function getLibraryPath(file:String, library = 'preload'):String
{
return if (library == "preload" || library == "default") getPreloadPath(file); else getLibraryPathForce(file, library);
return if (library == 'preload' || library == 'default') getPreloadPath(file); else getLibraryPathForce(file, library);
}
inline static function getLibraryPathForce(file:String, library:String)
static inline function getLibraryPathForce(file:String, library:String):String
{
return '$library:assets/$library/$file';
}
inline static function getPreloadPath(file:String)
static inline function getPreloadPath(file:String):String
{
return 'assets/$file';
}
inline static public function file(file:String, type:AssetType = TEXT, ?library:String)
public static function file(file:String, type:AssetType = TEXT, ?library:String):String
{
return getPath(file, type, library);
}
public static inline function animateAtlas(path:String, ?library:String)
public static function animateAtlas(path:String, ?library:String):String
{
return getLibraryPath('images/$path', library);
}
inline static public function txt(key:String, ?library:String)
public static function txt(key:String, ?library:String):String
{
return getPath('data/$key.txt', TEXT, library);
}
inline static public function frag(key:String, ?library:String)
public static function frag(key:String, ?library:String):String
{
return getPath('shaders/$key.frag', TEXT, library);
}
inline static public function vert(key:String, ?library:String)
public static function vert(key:String, ?library:String):String
{
return getPath('shaders/$key.vert', TEXT, library);
}
inline static public function xml(key:String, ?library:String)
public static function xml(key:String, ?library:String):String
{
return getPath('data/$key.xml', TEXT, library);
}
inline static public function json(key:String, ?library:String)
public static function json(key:String, ?library:String):String
{
return getPath('data/$key.json', TEXT, library);
}
static public function sound(key:String, ?library:String)
public static function sound(key:String, ?library:String):String
{
return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library);
}
inline static public function soundRandom(key:String, min:Int, max:Int, ?library:String)
public static function soundRandom(key:String, min:Int, max:Int, ?library:String):String
{
return sound(key + FlxG.random.int(min, max), library);
}
inline static public function music(key:String, ?library:String)
public static function music(key:String, ?library:String):String
{
return getPath('music/$key.${Constants.EXT_SOUND}', MUSIC, library);
}
inline static public function videos(key:String, ?library:String)
public static function videos(key:String, ?library:String):String
{
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library);
}
inline static public function voices(song:String, ?suffix:String = '')
public static function voices(song:String, ?suffix:String = ''):String
{
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
if (suffix == null) suffix = ''; // no suffix, for a sorta backwards compatibility with older-ish voice files
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
}
inline static public function inst(song:String, ?suffix:String = '')
public static function inst(song:String, ?suffix:String = ''):String
{
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}';
}
inline static public function image(key:String, ?library:String)
public static function image(key:String, ?library:String):String
{
return getPath('images/$key.png', IMAGE, library);
}
inline static public function font(key:String)
public static function font(key:String):String
{
return 'assets/fonts/$key';
}
inline static public function ui(key:String, ?library:String)
public static function ui(key:String, ?library:String):String
{
return xml('ui/$key', library);
}
static public function getSparrowAtlas(key:String, ?library:String)
public static function getSparrowAtlas(key:String, ?library:String):FlxAtlasFrames
{
return FlxAtlasFrames.fromSparrow(image(key, library), file('images/$key.xml', library));
}
inline static public function getPackerAtlas(key:String, ?library:String)
public static function getPackerAtlas(key:String, ?library:String):FlxAtlasFrames
{
return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
}
+39 -18
View File
@@ -13,6 +13,7 @@ import flixel.util.FlxSignal;
*/
class PlayerSettings
{
// TODO: Finish implementation of second player.
public static var numPlayers(default, null) = 0;
public static var numAvatars(default, null) = 0;
public static var player1(default, null):PlayerSettings;
@@ -21,12 +22,21 @@ class PlayerSettings
public static var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
public static var onAvatarRemove(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
/**
* The player number associated with this settings object.
*/
public var id(default, null):Int;
/**
* The controls handler for this player.
*/
public var controls(default, null):Controls;
/**
* Return the PlayerSettings for the given player number, or `null` if that player isn't active.
*
* @param id The player number this represents.
* @return The PlayerSettings for the given player number, or `null` if that player isn't active.
*/
public static function get(id:Int):Null<PlayerSettings>
{
@@ -38,6 +48,9 @@ class PlayerSettings
};
}
/**
* Initialize the PlayerSettings singletons for each player.
*/
public static function init():Void
{
if (player1 == null)
@@ -56,22 +69,30 @@ class PlayerSettings
}
}
public static function reset()
/**
* Forcibly destroy the PlayerSettings singletons for each player.
*/
public static function reset():Void
{
player1 = null;
player2 = null;
numPlayers = 0;
}
static function onGamepadAdded(gamepad:FlxGamepad)
/**
* Callback invoked when a gamepad is added.
* @param gamepad The gamepad that was added.
*/
static function onGamepadAdded(gamepad:FlxGamepad):Void
{
// TODO: Make this detect and handle multiple players
player1.addGamepad(gamepad);
}
/**
* @param id The player number this represents. This was refactored to START AT `1`.
*/
private function new(id:Int)
function new(id:Int)
{
trace('loading player settings for id: $id');
@@ -83,11 +104,11 @@ class PlayerSettings
function addKeyboard():Void
{
var useDefault = true;
if (Save.get().hasControls(id, Keys))
var useDefault:Bool = true;
if (Save.instance.hasControls(id, Keys))
{
var keyControlData = Save.get().getControls(id, Keys);
trace("keyControlData: " + haxe.Json.stringify(keyControlData));
var keyControlData = Save.instance.getControls(id, Keys);
trace('Loading keyboard control scheme from user save');
useDefault = false;
controls.fromSaveData(keyControlData, Keys);
}
@@ -98,7 +119,7 @@ class PlayerSettings
if (useDefault)
{
trace("Loading default keyboard control scheme");
trace('Loading default keyboard control scheme');
controls.setKeyboardScheme(Solo);
}
@@ -109,13 +130,13 @@ class PlayerSettings
* Called after an FlxGamepad has been detected.
* @param gamepad The gamepad that was detected.
*/
function addGamepad(gamepad:FlxGamepad)
function addGamepad(gamepad:FlxGamepad):Void
{
var useDefault = true;
if (Save.get().hasControls(id, Gamepad(gamepad.id)))
if (Save.instance.hasControls(id, Gamepad(gamepad.id)))
{
var padControlData = Save.get().getControls(id, Gamepad(gamepad.id));
trace("padControlData: " + haxe.Json.stringify(padControlData));
var padControlData = Save.instance.getControls(id, Gamepad(gamepad.id));
trace('Loading gamepad control scheme from user save');
useDefault = false;
controls.addGamepadWithSaveData(gamepad.id, padControlData);
}
@@ -126,7 +147,7 @@ class PlayerSettings
if (useDefault)
{
trace("Loading gamepad control scheme");
trace('Loading default gamepad control scheme');
controls.addDefaultGamepad(gamepad.id);
}
PreciseInputManager.instance.initializeButtons(controls, gamepad);
@@ -135,13 +156,13 @@ class PlayerSettings
/**
* Save this player's controls to the game's persistent save.
*/
public function saveControls()
public function saveControls():Void
{
var keyData = controls.createSaveData(Keys);
if (keyData != null)
{
trace("saving key data: " + haxe.Json.stringify(keyData));
Save.get().setControls(id, Keys, keyData);
trace('Saving keyboard control scheme to user save');
Save.instance.setControls(id, Keys, keyData);
}
if (controls.gamepadsAdded.length > 0)
@@ -149,8 +170,8 @@ class PlayerSettings
var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0]));
if (padData != null)
{
trace("saving pad data: " + haxe.Json.stringify(padData));
Save.get().setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
trace('Saving gamepad control scheme to user save');
Save.instance.setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
}
}
}
+19 -14
View File
@@ -15,12 +15,12 @@ class Preferences
static function get_naughtyness():Bool
{
return Save.get().options.naughtyness;
return Save.instance.options.naughtyness;
}
static function set_naughtyness(value:Bool):Bool
{
var save = Save.get();
var save:Save = Save.instance;
save.options.naughtyness = value;
save.flush();
return value;
@@ -34,12 +34,12 @@ class Preferences
static function get_downscroll():Bool
{
return Save.get().options.downscroll;
return Save.instance.options.downscroll;
}
static function set_downscroll(value:Bool):Bool
{
var save = Save.get();
var save:Save = Save.instance;
save.options.downscroll = value;
save.flush();
return value;
@@ -53,12 +53,12 @@ class Preferences
static function get_flashingLights():Bool
{
return Save.get().options.flashingLights;
return Save.instance.options.flashingLights;
}
static function set_flashingLights(value:Bool):Bool
{
var save = Save.get();
var save:Save = Save.instance;
save.options.flashingLights = value;
save.flush();
return value;
@@ -72,12 +72,12 @@ class Preferences
static function get_zoomCamera():Bool
{
return Save.get().options.zoomCamera;
return Save.instance.options.zoomCamera;
}
static function set_zoomCamera(value:Bool):Bool
{
var save = Save.get();
var save:Save = Save.instance;
save.options.zoomCamera = value;
save.flush();
return value;
@@ -91,17 +91,17 @@ class Preferences
static function get_debugDisplay():Bool
{
return Save.get().options.debugDisplay;
return Save.instance.options.debugDisplay;
}
static function set_debugDisplay(value:Bool):Bool
{
if (value != Save.get().options.debugDisplay)
if (value != Save.instance.options.debugDisplay)
{
toggleDebugDisplay(value);
}
var save = Save.get();
var save = Save.instance;
save.options.debugDisplay = value;
save.flush();
return value;
@@ -115,22 +115,27 @@ class Preferences
static function get_autoPause():Bool
{
return Save.get().options.autoPause;
return Save.instance.options.autoPause;
}
static function set_autoPause(value:Bool):Bool
{
if (value != Save.get().options.autoPause) FlxG.autoPause = value;
if (value != Save.instance.options.autoPause) FlxG.autoPause = value;
var save = Save.get();
var save:Save = Save.instance;
save.options.autoPause = value;
save.flush();
return value;
}
/**
* Loads the user's preferences from the save data and apply them.
*/
public static function init():Void
{
// Apply the autoPause setting (enables automatic pausing on focus lost).
FlxG.autoPause = Preferences.autoPause;
// Apply the debugDisplay setting (enables the FPS and RAM display).
toggleDebugDisplay(Preferences.debugDisplay);
}
+2 -2
View File
@@ -3,7 +3,6 @@ package funkin;
import flash.Lib;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.BlendMode;
import flash.display.Sprite;
import flixel.system.FlxBasePreloader;
import openfl.display.Sprite;
@@ -12,7 +11,8 @@ import openfl.text.TextField;
import openfl.text.TextFormat;
import flixel.system.FlxAssets;
@:bitmap("art/preloaderArt.png") class LogoImage extends BitmapData {}
@:bitmap('art/preloaderArt.png')
class LogoImage extends BitmapData {}
class Preloader extends FlxBasePreloader
{
+6 -6
View File
@@ -86,10 +86,10 @@ class NGUtil
#end
var onSessionFail:Error->Void = null;
if (sessionId == null && Save.get().ngSessionId != null)
if (sessionId == null && Save.instance.ngSessionId != null)
{
trace("using stored session id");
sessionId = Save.get().ngSessionId;
sessionId = Save.instance.ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
@@ -159,8 +159,8 @@ class NGUtil
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
Save.get().ngSessionId = NG.core.sessionId;
Save.get().flush();
Save.instance.ngSessionId = NG.core.sessionId;
Save.instance.flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
@@ -174,8 +174,8 @@ class NGUtil
{
NG.core.logOut();
Save.get().ngSessionId = null;
Save.get().flush();
Save.instance.ngSessionId = null;
Save.instance.flush();
}
// --- MEDALS
+6 -6
View File
@@ -86,10 +86,10 @@ class NGio
#end
var onSessionFail:Error->Void = null;
if (sessionId == null && Save.get().ngSessionId != null)
if (sessionId == null && Save.instance.ngSessionId != null)
{
trace("using stored session id");
sessionId = Save.get().ngSessionId;
sessionId = Save.instance.ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
@@ -159,8 +159,8 @@ class NGio
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
Save.get().ngSessionId = NG.core.sessionId;
Save.get().flush();
Save.instance.ngSessionId = NG.core.sessionId;
Save.instance.flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
@@ -174,8 +174,8 @@ class NGio
{
NG.core.logOut();
Save.get().ngSessionId = null;
Save.get().flush();
Save.instance.ngSessionId = null;
Save.instance.flush();
}
// --- MEDALS
+108 -35
View File
@@ -1,13 +1,12 @@
package funkin.audio;
#if flash11
import flash.media.Sound;
import flash.utils.ByteArray;
#end
import flixel.sound.FlxSound;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.util.FlxSignal.FlxTypedSignal;
import flixel.system.FlxAssets.FlxSoundAsset;
import funkin.util.tools.ICloneable;
import funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry;
import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
import flixel.math.FlxMath;
@@ -23,9 +22,31 @@ import openfl.utils.AssetType;
@:nullSafety
class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
{
static final MAX_VOLUME:Float = 2.0;
static final MAX_VOLUME:Float = 1.0;
static var cache(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
/**
* An FlxSignal which is dispatched when the volume changes.
*/
public static var onVolumeChanged(get, never):FlxTypedSignal<Float->Void>;
static var _onVolumeChanged:Null<FlxTypedSignal<Float->Void>> = null;
static function get_onVolumeChanged():FlxTypedSignal<Float->Void>
{
if (_onVolumeChanged == null)
{
_onVolumeChanged = new FlxTypedSignal<Float->Void>();
FlxG.sound.volumeHandler = function(volume:Float) {
_onVolumeChanged.dispatch(volume);
}
}
return _onVolumeChanged;
}
/**
* Using `FunkinSound.load` will override a dead instance from here rather than creating a new one, if possible!
*/
static var pool(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
public var muted(default, set):Bool = false;
@@ -40,7 +61,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
override function set_volume(value:Float):Float
{
// Uncap the volume.
fixMaxVolume();
_volume = FlxMath.bound(value, 0.0, MAX_VOLUME);
updateTransform();
return _volume;
@@ -88,6 +108,11 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
*/
var _label:String = "unknown";
/**
* Whether we received a focus lost event.
*/
var _lostFocus:Bool = false;
public function new()
{
super();
@@ -126,17 +151,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
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;
@@ -176,8 +190,18 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
public override function pause():FunkinSound
{
super.pause();
this._shouldPlay = false;
if (_shouldPlay)
{
// This sound will eventually play, but is still at a negative timestamp.
// Manually set the paused flag to ensure proper focus/unfocus behavior.
_shouldPlay = false;
_paused = true;
active = false;
}
else
{
super.pause();
}
return this;
}
@@ -186,10 +210,18 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
*/
override function onFocus():Void
{
if (!_alreadyPaused && this._shouldPlay)
// Flixel can sometimes toss spurious `onFocus` events, e.g. if the Flixel debugger is toggled
// on and off. We only want to resume the sound if we actually lost focus, and if we weren't
// already paused before we lost focus.
if (_lostFocus && !_alreadyPaused)
{
resume();
}
else
{
trace('Not resuming audio on focus!');
}
_lostFocus = false;
}
/**
@@ -197,6 +229,8 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
*/
override function onFocusLost():Void
{
trace('Focus lost, pausing audio!');
_lostFocus = true;
_alreadyPaused = _paused;
pause();
}
@@ -205,7 +239,10 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
{
if (this._time < 0)
{
this._shouldPlay = true;
// Sound with negative timestamp, restart the timer.
_shouldPlay = true;
_paused = false;
active = true;
}
else
{
@@ -246,24 +283,58 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
}
/**
* Creates a new `FunkinSound` object.
* Creates a new `FunkinSound` object and loads it as the current music track.
*
* @param embeddedSound The embedded sound resource you want to play. To stream, use the optional URL parameter instead.
* @param volume How loud to play it (0 to 1).
* @param looped Whether to loop this sound.
* @param group The group to add this sound to.
* @param autoDestroy Whether to destroy this sound when it finishes playing.
* @param key The key of the music you want to play. Music should be at `music/<key>/<key>.ogg`.
* @param overrideExisting Whether to override music if it is already playing.
* @param mapTimeChanges Whether to check for `SongMusicData` to update the Conductor with.
* Data should be at `music/<key>/<key>-metadata.json`.
*/
public static function playMusic(key:String, overrideExisting:Bool = false, mapTimeChanges:Bool = true):Void
{
if (!overrideExisting && FlxG.sound.music?.playing) return;
if (mapTimeChanges)
{
var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key);
// Will fall back and return null if the metadata doesn't exist or can't be parsed.
if (songMusicData != null)
{
Conductor.instance.mapTimeChanges(songMusicData.timeChanges);
}
else
{
FlxG.log.warn('Tried and failed to find music metadata for $key');
}
}
FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'));
// Prevent repeat update() and onFocus() calls.
FlxG.sound.list.remove(FlxG.sound.music);
}
/**
* Creates a new `FunkinSound` object synchronously.
*
* @param embeddedSound The embedded sound resource you want to play. To stream, use the optional URL parameter instead.
* @param volume How loud to play it (0 to 1).
* @param looped Whether to loop this sound.
* @param group The group to add this sound to.
* @param autoDestroy Whether to destroy this sound when it finishes playing.
* Leave this value set to `false` if you want to re-use this `FunkinSound` instance.
* @param autoPlay Whether to play the sound immediately or wait for a `play()` call.
* @param onComplete Called when the sound finished playing.
* @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds.
* @return A `FunkinSound` object.
* @param autoPlay Whether to play the sound immediately or wait for a `play()` call.
* @param onComplete Called when the sound finished playing.
* @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds.
* @return A `FunkinSound` object.
*/
public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false,
?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound
{
var sound:FunkinSound = cache.recycle(construct);
var sound:FunkinSound = pool.recycle(construct);
// Load the sound.
// Sets `exists = true` as a side effect.
sound.loadEmbedded(embeddedSound, looped, autoDestroy, onComplete);
if (embeddedSound is String)
@@ -276,9 +347,11 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
sound.persist = true;
if (autoPlay) sound.play();
// Call OnlLoad() because the sound already loaded
// Call onLoad() because the sound already loaded
if (onLoad != null && sound._sound != null) onLoad();
FlxG.sound.list.remove(FlxG.sound.music);
return sound;
}
@@ -286,7 +359,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
{
var sound:FunkinSound = new FunkinSound();
cache.add(sound);
pool.add(sound);
FlxG.sound.list.add(sound);
return sound;
+3 -2
View File
@@ -80,8 +80,9 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
// We have to play, then pause the sound to set the time,
// else the sound will restart immediately when played.
result.play(true, 0.0);
result.pause();
// TODO: Past me experienced that issue but present me didn't? Investigate.
// result.play(true, 0.0);
// result.pause();
result.time = this.time;
result.onComplete = function() {
@@ -4,13 +4,11 @@ import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.math.FlxVector;
import flixel.sound.FlxSound;
import flixel.util.FlxColor;
import funkin.audio.visualize.PolygonSpectogram.VISTYPE;
import funkin.audio.visualize.VisShit.CurAudioInfo;
import funkin.audio.visualize.dsp.FFT;
import haxe.Timer;
import lime.system.ThreadPool;
import lime.utils.Int16Array;
-1
View File
@@ -3,7 +3,6 @@ package funkin.audio.visualize;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import funkin.audio.visualize.dsp.FFT;
import haxe.Timer;
import lime.system.ThreadPool;
import lime.utils.Int16Array;
import funkin.util.MathUtil;
@@ -187,6 +187,8 @@ class WaveformData
*/
public function merge(that:WaveformData):WaveformData
{
if (that == null) return this.clone();
var result = this.clone([]);
for (channelIndex in 0...this.channels)
@@ -1,5 +1,7 @@
package funkin.audio.waveform;
import funkin.util.TimerUtil;
class WaveformDataParser
{
static final INT16_MAX:Int = 32767;
@@ -71,7 +73,7 @@ class WaveformDataParser
var outputData:Array<Int> = [];
var perfStart = haxe.Timer.stamp();
var perfStart:Float = TimerUtil.start();
for (pointIndex in 0...outputPointCount)
{
@@ -108,8 +110,7 @@ class WaveformDataParser
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.');
trace('[WAVEFORM] Interpreted audio buffer in ${TimerUtil.seconds(perfStart)}.');
return result;
}
+90 -12
View File
@@ -1,6 +1,5 @@
package funkin.data;
import openfl.Assets;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
import haxe.Constraints.Constructible;
@@ -19,12 +18,23 @@ typedef EntryConstructorFunction = String->Void;
@:generic
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J>
{
/**
* The ID of the registry. Used when logging.
*/
public final registryId:String;
final dataFilePath:String;
/**
* A map of entry IDs to entries.
*/
final entries:Map<String, T>;
/**
* A map of entry IDs to scripted class names.
*/
final scriptedEntryIds:Map<String, String>;
/**
* The version rule to use when loading entries.
* If the entry's version does not match this rule, migration is needed.
@@ -37,17 +47,18 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param registryId A readable ID for this registry, used when logging.
* @param dataFilePath The path (relative to `assets/data`) to search for JSON files.
*/
public function new(registryId:String, dataFilePath:String, versionRule:thx.semver.VersionRule = null)
public function new(registryId:String, dataFilePath:String, ?versionRule:thx.semver.VersionRule)
{
this.registryId = registryId;
this.dataFilePath = dataFilePath;
this.versionRule = versionRule == null ? "1.0.x" : versionRule;
this.versionRule = versionRule == null ? '1.0.x' : versionRule;
this.entries = new Map<String, T>();
this.scriptedEntryIds = [];
}
/**
* TODO: Create a `loadEntriesAsync()` function.
* TODO: Create a `loadEntriesAsync(onProgress, onComplete)` function.
*/
public function loadEntries():Void
{
@@ -61,12 +72,22 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
for (entryCls in scriptedEntryClassNames)
{
var entry:T = createScriptedEntry(entryCls);
var entry:Null<T> = null;
try
{
entry = createScriptedEntry(entryCls);
}
catch (e)
{
log('Failed to create scripted entry (${entryCls})');
continue;
}
if (entry != null)
{
log('Successfully created scripted entry (${entryCls} = ${entry.id})');
entries.set(entry.id, entry);
scriptedEntryIds.set(entry.id, entryCls);
}
else
{
@@ -93,7 +114,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
entries.set(entry.id, entry);
}
}
catch (e:Dynamic)
catch (e)
{
// Print the error.
trace(' Failed to load entry data: ${entryId}');
@@ -121,6 +142,36 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
return entries.size();
}
/**
* Return whether the entry ID is known to have an attached script.
* @param id The ID of the entry.
* @return `true` if the entry has an attached script, `false` otherwise.
*/
public function isScriptedEntry(id:String):Bool
{
return scriptedEntryIds.exists(id);
}
/**
* Return the class name of the scripted entry with the given ID, if it exists.
* @param id The ID of the entry.
* @return The class name, or `null` if it does not exist.
*/
public function getScriptedEntryClassName(id:String):String
{
return scriptedEntryIds.get(id);
}
/**
* Return whether the registry has successfully parsed an entry with the given ID.
* @param id The ID of the entry.
* @return `true` if the entry exists, `false` otherwise.
*/
public function hasEntry(id:String):Bool
{
return entries.exists(id);
}
/**
* Fetch an entry by its ID.
* @param id The ID of the entry to fetch.
@@ -136,6 +187,11 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
return 'Registry(' + registryId + ', ${countEntries()} entries)';
}
/**
* Retrieve the data for an entry and parse its Semantic Version.
* @param id The ID of the entry.
* @return The entry's version, or `null` if it does not exist or is invalid.
*/
public function fetchEntryVersion(id:String):Null<thx.semver.Version>
{
var entryStr:String = loadEntryFile(id).contents;
@@ -176,6 +232,8 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* Read, parse, and validate the JSON data and produce the corresponding data object.
*
* NOTE: Must be implemented on the implementation class.
* @param id The ID of the entry.
* @return The created entry.
*/
public abstract function parseEntryData(id:String):Null<J>;
@@ -185,6 +243,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* NOTE: Must be implemented on the implementation class.
* @param contents The JSON as a string.
* @param fileName An optional file name for error reporting.
* @return The created entry.
*/
public abstract function parseEntryDataRaw(contents:String, ?fileName:String):Null<J>;
@@ -193,9 +252,17 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* accounting for old versions of the data.
*
* NOTE: Extend this function to handle migration.
* @param id The ID of the entry.
* @param version The entry's version (use `fetchEntryVersion(id)`).
* @return The created entry.
*/
public function parseEntryDataWithMigration(id:String, version:thx.semver.Version):Null<J>
{
if (version == null)
{
throw '[${registryId}] Entry ${id} could not be JSON-parsed or does not have a parseable version.';
}
// If a version rule is not specified, do not check against it.
if (versionRule == null || VersionUtil.validateVersion(version, versionRule))
{
@@ -206,12 +273,17 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
throw '[${registryId}] Entry ${id} does not support migration to version ${versionRule}.';
}
// Example:
// if (VersionUtil.validateVersion(version, "0.1.x")) {
// return parseEntryData_v0_1_x(id);
// } else {
// super.parseEntryDataWithMigration(id, version);
// }
/*
* An example of what you should override this with:
*
* ```haxe
* if (VersionUtil.validateVersion(version, "0.1.x")) {
* return parseEntryData_v0_1_x(id);
* } else {
* super.parseEntryDataWithMigration(id, version);
* }
* ```
*/
}
/**
@@ -226,6 +298,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
*/
function createEntry(id:String):Null<T>
{
// We enforce that T is Constructible to ensure this is valid.
return new T(id);
}
@@ -240,10 +313,15 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
trace('[${registryId}] Failed to parse entry data: ${id}');
for (error in errors)
{
DataError.printError(error);
}
}
}
/**
* A pair of a file name and its contents.
*/
typedef JsonFile =
{
fileName:String,
+26 -3
View File
@@ -108,8 +108,8 @@ class SongEventRegistry
public static function handleEvent(data:SongEventData):Void
{
var eventType:String = data.event;
var eventHandler:SongEvent = eventCache.get(eventType);
var eventKind:String = data.eventKind;
var eventHandler:SongEvent = eventCache.get(eventKind);
if (eventHandler != null)
{
@@ -117,7 +117,7 @@ class SongEventRegistry
}
else
{
trace('WARNING: No event handler for event with id: ${eventType}');
trace('WARNING: No event handler for event with kind: ${eventKind}');
}
data.activated = true;
@@ -148,6 +148,29 @@ class SongEventRegistry
});
}
/**
* The currentTime has jumped far ahead or back.
* If we moved back in time, we need to reset all the events in that space.
* If we moved forward in time, we need to skip all the events in that space.
*/
public static function handleSkippedEvents(events:Array<SongEventData>, currentTime:Float):Void
{
for (event in events)
{
// Deactivate future events.
if (event.time > currentTime)
{
event.activated = false;
}
// Skip past events.
if (event.time < currentTime)
{
event.activated = true;
}
}
}
/**
* Reset activation of all the provided events.
*/
+36
View File
@@ -0,0 +1,36 @@
package funkin.data.freeplay;
/**
* A type definition for the data for an album of songs.
* It includes things like what graphics to display in Freeplay.
* @see https://lib.haxe.org/p/json2object/
*/
typedef AlbumData =
{
/**
* Semantic version for album data.
*/
public var version:String;
/**
* Readable name of the album.
*/
public var name:String;
/**
* Readable name of the artist(s) of the album.
*/
public var artists:Array<String>;
/**
* Asset key for the album art.
* The album art will be displayed in Freeplay.
*/
public var albumArtAsset:String;
/**
* Asset key for the album title.
* The album title will be displayed below the album art in Freeplay.
*/
public var albumTitleAsset:String;
}
@@ -0,0 +1,84 @@
package funkin.data.freeplay;
import funkin.ui.freeplay.Album;
import funkin.data.freeplay.AlbumData;
import funkin.ui.freeplay.ScriptedAlbum;
class AlbumRegistry extends BaseRegistry<Album, AlbumData>
{
/**
* The current version string for the album data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateAlbumData()` function.
*/
public static final ALBUM_DATA_VERSION:thx.semver.Version = '1.0.0';
public static final ALBUM_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x';
public static final instance:AlbumRegistry = new AlbumRegistry();
public function new()
{
super('ALBUM', 'ui/freeplay/albums', ALBUM_DATA_VERSION_RULE);
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
* @param id The ID of the entry to load.
* @return The parsed data object.
*/
public function parseEntryData(id:String):Null<AlbumData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser:json2object.JsonParser<AlbumData> = new json2object.JsonParser<AlbumData>();
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.
* @return The parsed data object.
*/
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<AlbumData>
{
var parser:json2object.JsonParser<AlbumData> = new json2object.JsonParser<AlbumData>();
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):Album
{
return ScriptedAlbum.init(clsName, 'unknown');
}
function getScriptedClassNames():Array<String>
{
return ScriptedAlbum.listScriptClasses();
}
}
+18 -4
View File
@@ -17,7 +17,7 @@ typedef LevelData =
var version:String;
/**
* The title of the week, as seen in the top corner.
* The title of the level, as seen in the top corner.
*/
var name:String;
@@ -27,21 +27,35 @@ typedef LevelData =
@:jcustomparse(funkin.data.DataParse.stringNotEmpty)
var titleAsset:String;
/**
* The props to display over the colored background.
* In the base game this is usually Boyfriend and the opponent.
*/
@:default([])
var props:Array<LevelPropData>;
@:default(["bopeebo"])
/**
* The list of song IDs included in this level.
*/
@:default(['bopeebo'])
var songs:Array<String>;
@:default("#F9CF51")
/**
* The background for the level behind the props.
*/
@:default('#F9CF51')
@:optional
var background:String;
}
/**
* Data for a single prop for a story mode level.
*/
typedef LevelPropData =
{
/**
* The image to use for the prop. May optionally be a sprite sheet.
*/
// @:jcustomparse(funkin.data.DataParse.stringNotEmpty)
var assetPath:String;
/**
+27 -17
View File
@@ -106,11 +106,14 @@ class SongMetadata implements ICloneable<SongMetadata>
/**
* Serialize this SongMetadata into a JSON string.
* @param pretty Whether the JSON should be big ol string (false),
* or formatted with tabs (true)
* @return The JSON string.
*/
public function serialize(pretty:Bool = true):String
{
var writer = new json2object.JsonWriter<SongMetadata>();
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongMetadata>(ignoreNullOptionals);
// I believe @:jignored should be iggnored by the writer?
// var output = this.clone();
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
@@ -597,7 +600,8 @@ class SongChartData implements ICloneable<SongChartData>
*/
public function serialize(pretty:Bool = true):String
{
var writer = new json2object.JsonWriter<SongChartData>();
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals);
return writer.write(this, pretty ? ' ' : null);
}
@@ -648,7 +652,7 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
* Custom events can be added by scripts with the `ScriptedSongEvent` class.
*/
@:alias("e")
public var event:String;
public var eventKind:String;
/**
* The data for the event.
@@ -668,10 +672,10 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
@:jignored
public var activated:Bool = false;
public function new(time:Float, event:String, value:Dynamic = null)
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this.time = time;
this.event = event;
this.eventKind = eventKind;
this.value = value;
}
@@ -687,22 +691,22 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
public function clone():SongEventDataRaw
{
return new SongEventDataRaw(this.time, this.event, this.value);
return new SongEventDataRaw(this.time, this.eventKind, this.value);
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, event, value, activated, getStepTime, clone)
@:forward(time, eventKind, value, activated, getStepTime, clone)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, event:String, value:Dynamic = null)
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, event, value);
this = new SongEventDataRaw(time, eventKind, value);
}
public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic
public function valueAsStruct(?defaultKey:String = "key"):Dynamic
{
if (this.value == null) return {};
if (Std.isOfType(this.value, Array))
@@ -726,12 +730,12 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
public inline function getHandler():Null<SongEvent>
{
return SongEventRegistry.getEvent(this.event);
return SongEventRegistry.getEvent(this.eventKind);
}
public inline function getSchema():Null<SongEventSchema>
{
return SongEventRegistry.getEventSchema(this.event);
return SongEventRegistry.getEventSchema(this.eventKind);
}
public inline function getDynamic(key:String):Null<Dynamic>
@@ -784,7 +788,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
var eventHandler = getHandler();
var eventSchema = getSchema();
if (eventSchema == null) return 'Unknown Event: ${this.event}';
if (eventSchema == null) return 'Unknown Event: ${this.eventKind}';
var result = '${eventHandler.getTitle()}';
@@ -809,19 +813,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
public function clone():SongEventData
{
return new SongEventData(this.time, this.event, this.value);
return new SongEventData(this.time, this.eventKind, this.value);
}
@:op(A == B)
public function op_equals(other:SongEventData):Bool
{
return this.time == other.time && this.event == other.event && this.value == other.value;
return this.time == other.time && this.eventKind == other.eventKind && this.value == other.value;
}
@:op(A != B)
public function op_notEquals(other:SongEventData):Bool
{
return this.time != other.time || this.event != other.event || this.value != other.value;
return this.time != other.time || this.eventKind != other.eventKind || this.value != other.value;
}
@:op(A > B)
@@ -853,7 +857,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
*/
public function toString():String
{
return 'SongEventData(${this.time}ms, ${this.event}: ${this.value})';
return 'SongEventData(${this.time}ms, ${this.eventKind}: ${this.value})';
}
}
@@ -1022,6 +1026,12 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
}
public function toString():String
{
return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
}
}
/**
+1 -1
View File
@@ -47,7 +47,7 @@ class SongDataUtils
public static function offsetSongEventData(events:Array<SongEventData>, offset:Float):Array<SongEventData>
{
return events.map(function(event:SongEventData):SongEventData {
return new SongEventData(event.time + offset, event.event, event.value);
return new SongEventData(event.time + offset, event.eventKind, event.value);
});
}
+8
View File
@@ -68,6 +68,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
{
log('Successfully created scripted entry (${entryCls} = ${entry.id})');
entries.set(entry.id, entry);
scriptedEntryIds.set(entry.id, entryCls);
}
else
{
@@ -441,6 +442,13 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return {fileName: entryFilePath, contents: rawJson};
}
function hasMusicDataFile(id:String, ?variation:String):Bool
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json');
return openfl.Assets.exists(entryFilePath);
}
function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
+11
View File
@@ -32,18 +32,21 @@ class StageData
bf:
{
zIndex: 0,
scale: 1,
position: [0, 0],
cameraOffsets: [-100, -100]
},
dad:
{
zIndex: 0,
scale: 1,
position: [0, 0],
cameraOffsets: [100, -100]
},
gf:
{
zIndex: 0,
scale: 1,
position: [0, 0],
cameraOffsets: [0, 0]
}
@@ -114,6 +117,7 @@ typedef StageDataProp =
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
@:optional
@:default(Left(1.0))
var scale:haxe.ds.Either<Float, Array<Float>>;
/**
@@ -190,6 +194,13 @@ typedef StageDataCharacter =
@:default([0, 0])
var position:Array<Float>;
/**
* The scale to render the character at.
*/
@:optional
@:default(1)
var scale: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
+229 -3
View File
@@ -3,12 +3,30 @@ package funkin.graphics;
import flixel.FlxSprite;
import flixel.util.FlxColor;
import flixel.graphics.FlxGraphic;
import flixel.tweens.FlxTween;
import openfl.display3D.textures.TextureBase;
import funkin.graphics.framebuffer.FixedBitmapData;
import openfl.display.BitmapData;
/**
* An FlxSprite with additional functionality.
* - A more efficient method for creating solid color sprites.
* - TODO: Better cache handling for textures.
*/
class FunkinSprite extends FlxSprite
{
/**
* An internal list of all the textures cached with `cacheTexture`.
* This excludes any temporary textures like those from `FlxText` or `makeSolidColor`.
*/
static var currentCachedTextures:Map<String, FlxGraphic> = [];
/**
* An internal list of textures that were cached in the previous state.
* We don't know whether we want to keep them cached or not.
*/
static var previousCachedTextures:Map<String, FlxGraphic> = [];
/**
* @param x Starting X position
* @param y Starting Y position
@@ -18,26 +36,225 @@ class FunkinSprite extends FlxSprite
super(x, y);
}
/**
* Create a new FunkinSprite with a static texture.
* @param x The starting X position.
* @param y The starting Y position.
* @param key The key of the texture to load.
* @return The new FunkinSprite.
*/
public static function create(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
{
var sprite:FunkinSprite = new FunkinSprite(x, y);
sprite.loadTexture(key);
return sprite;
}
/**
* Create a new FunkinSprite with a Sparrow atlas animated texture.
* @param x The starting X position.
* @param y The starting Y position.
* @param key The key of the texture to load.
* @return The new FunkinSprite.
*/
public static function createSparrow(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
{
var sprite:FunkinSprite = new FunkinSprite(x, y);
sprite.loadSparrow(key);
return sprite;
}
/**
* Create a new FunkinSprite with a Packer atlas animated texture.
* @param x The starting X position.
* @param y The starting Y position.
* @param key The key of the texture to load.
* @return The new FunkinSprite.
*/
public static function createPacker(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
{
var sprite:FunkinSprite = new FunkinSprite(x, y);
sprite.loadPacker(key);
return sprite;
}
/**
* Load a static image as the sprite's texture.
* @param key The key of the texture to load.
* @return This sprite, for chaining.
*/
public function loadTexture(key:String):FunkinSprite
{
var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
loadGraphic(graphicKey);
return this;
}
/**
* Apply an OpenFL `BitmapData` to this sprite.
* @param input The OpenFL `BitmapData` to apply
* @return This sprite, for chaining
*/
public function loadBitmapData(input:BitmapData):FunkinSprite
{
loadGraphic(input);
return this;
}
/**
* Apply an OpenFL `TextureBase` to this sprite.
* @param input The OpenFL `TextureBase` to apply
* @return This sprite, for chaining
*/
public function loadTextureBase(input:TextureBase):FunkinSprite
{
var inputBitmap:FixedBitmapData = FixedBitmapData.fromTexture(input);
return loadBitmapData(inputBitmap);
}
/**
* Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture.
* @param key The key of the texture to load.
* @return This sprite, for chaining.
*/
public function loadSparrow(key:String):FunkinSprite
{
var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
this.frames = Paths.getSparrowAtlas(key);
return this;
}
/**
* Load an animated texture (Packer atlas spritesheet) as the sprite's texture.
* @param key The key of the texture to load.
* @return This sprite, for chaining.
*/
public function loadPacker(key:String):FunkinSprite
{
var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
this.frames = Paths.getPackerAtlas(key);
return this;
}
/**
* Determine whether the texture with the given key is cached.
* @param key The key of the texture to check.
* @return Whether the texture is cached.
*/
public static function isTextureCached(key:String):Bool
{
return FlxG.bitmap.get(key) != null;
}
/**
* Ensure the texture with the given key is cached.
* @param key The key of the texture to cache.
*/
public static function cacheTexture(key:String):Void
{
// We don't want to cache the same texture twice.
if (currentCachedTextures.exists(key)) return;
if (previousCachedTextures.exists(key))
{
// Move the graphic from the previous cache to the current cache.
var graphic = previousCachedTextures.get(key);
previousCachedTextures.remove(key);
currentCachedTextures.set(key, graphic);
return;
}
// Else, texture is currently uncached.
var graphic:FlxGraphic = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null)
{
FlxG.log.warn('Failed to cache graphic: $key');
}
else
{
trace('Successfully cached graphic: $key');
graphic.persist = true;
currentCachedTextures.set(key, graphic);
}
}
public static function cacheSparrow(key:String):Void
{
cacheTexture(Paths.image(key));
}
public static function cachePacker(key:String):Void
{
cacheTexture(Paths.image(key));
}
/**
* Call this, then `cacheTexture` to keep the textures we still need, then `purgeCache` to remove the textures that we won't be using anymore.
*/
public static function preparePurgeCache():Void
{
previousCachedTextures = currentCachedTextures;
currentCachedTextures = [];
}
public static function purgeCache():Void
{
// Everything that is in previousCachedTextures but not in currentCachedTextures should be destroyed.
for (graphicKey in previousCachedTextures.keys())
{
var graphic = previousCachedTextures.get(graphicKey);
FlxG.bitmap.remove(graphic);
graphic.destroy();
previousCachedTextures.remove(graphicKey);
}
}
static function isGraphicCached(graphic:FlxGraphic):Bool
{
if (graphic == null) return false;
var result = FlxG.bitmap.get(graphic.key);
if (result == null) return false;
if (result != graphic)
{
FlxG.log.warn('Cached graphic does not match original: ${graphic.key}');
return false;
}
return true;
}
/**
* Acts similarly to `makeGraphic`, but with improved memory usage,
* at the expense of not being able to paint onto the sprite.
* at the expense of not being able to paint onto the resulting 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.
* @return This sprite, for chaining.
*/
public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite
{
// Create a tiny solid color graphic and scale it up to the desired size.
var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}');
frames = graphic.imageFrame;
scale.set(width / 2, height / 2);
scale.set(width / 2.0, height / 2.0);
updateHitbox();
return this;
}
/**
* Ensure scale is applied when cloning a sprite.
* Ensure scale is applied when cloning a sprite.R
* The default `clone()` method acts kinda weird TBH.
* @return A clone of this sprite.
*/
@@ -50,4 +267,13 @@ class FunkinSprite extends FlxSprite
return result;
}
public override function destroy():Void
{
frames = null;
// Cancel all tweens so they don't continue to run on a destroyed sprite.
// This prevents crashes.
FlxTween.cancelTweensOf(this);
super.destroy();
}
}
@@ -3,7 +3,9 @@ package funkin.graphics.adobeanimate;
import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings;
import flixel.math.FlxPoint;
import flxanimate.frames.FlxAnimateFrames;
import openfl.display.BitmapData;
import openfl.utils.Assets;
/**
* A sprite which provides convenience functions for rendering a texture atlas with animations.
@@ -18,7 +20,7 @@ class FlxAtlasSprite extends FlxAnimate
// ?OnComplete:Void -> Void,
ShowPivot: #if debug false #else false #end,
Antialiasing: true,
ScrollFactor: new FlxPoint(1, 1),
ScrollFactor: null,
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
};
@@ -31,7 +33,7 @@ class FlxAtlasSprite extends FlxAnimate
var canPlayOtherAnims:Bool = true;
public function new(x:Float, y:Float, path:String, ?settings:Settings)
public function new(x:Float, y:Float, ?path:String, ?settings:Settings)
{
if (settings == null) settings = SETTINGS;
@@ -55,8 +57,9 @@ class FlxAtlasSprite extends FlxAnimate
*/
public function listAnimations():Array<String>
{
// return this.anim.getFrameLabels();
return [""];
if (this.anim == null) return [];
return this.anim.getFrameLabels();
// return [""];
}
/**
@@ -81,9 +84,13 @@ class FlxAtlasSprite extends FlxAnimate
* @param id A string ID of the animation to play.
* @param restart Whether to restart the animation if it is already playing.
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
* @param loop Whether to loop the animation
* NOTE: `loop` and `ignoreOther` are not compatible with each other!
*/
public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false):Void
public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, ?loop:Bool = false):Void
{
if (loop == null) loop = false;
// Skip if not allowed to play animations.
if ((!canPlayOtherAnims && !ignoreOther)) return;
@@ -110,15 +117,21 @@ class FlxAtlasSprite extends FlxAnimate
return;
}
// Stop the current animation if it is playing.
// This includes removing existing frame callbacks.
if (this.currentAnimation != null) this.stopAnimation();
anim.callback = function(_, frame:Int) {
var offset = loop ? 0 : -1;
// Add a callback to ensure `onAnimationFinish` is dispatched.
addFrameCallback(getNextFrameLabel(id), function() {
trace('Animation finished: ' + id);
onAnimationFinish.dispatch(id);
});
if (frame == (anim.getFrameLabel(id).duration + offset) + anim.getFrameLabel(id).index)
{
if (loop)
{
playAnimation(id, true, false, true);
}
else
{
onAnimationFinish.dispatch(id);
}
}
};
// Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false;
@@ -128,6 +141,11 @@ class FlxAtlasSprite extends FlxAnimate
this.currentAnimation = id;
}
override public function update(elapsed:Float)
{
super.update(elapsed);
}
/**
* Stops the current animation.
*/
@@ -146,22 +164,22 @@ class FlxAtlasSprite extends FlxAnimate
frameLabel.add(callback);
}
inline function goToFrameLabel(label:String):Void
function goToFrameLabel(label:String):Void
{
this.anim.goToFrameLabel(label);
}
inline function getNextFrameLabel(label:String):String
function getNextFrameLabel(label:String):String
{
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
}
inline function getLabelIndex(label:String):Int
function getLabelIndex(label:String):Int
{
return listAnimations().indexOf(label);
}
inline function goToFrameIndex(index:Int):Void
function goToFrameIndex(index:Int):Void
{
this.anim.curFrame = index;
}
@@ -169,7 +187,7 @@ class FlxAtlasSprite extends FlxAnimate
public function cleanupAnimation(_:String):Void
{
canPlayOtherAnims = true;
this.currentAnimation = null;
this.anim.stop();
// this.currentAnimation = null;
this.anim.pause();
}
}
@@ -32,11 +32,11 @@ class FixedBitmapData extends BitmapData
public static function fromTexture(texture:TextureBase):FixedBitmapData
{
if (texture == null) return null;
final bitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);
bitmapData.readable = false;
final bitmapData:FixedBitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);
// bitmapData.readable = false;
bitmapData.__texture = texture;
bitmapData.__textureContext = texture.__textureContext;
bitmapData.image = null;
// bitmapData.image = null;
return bitmapData;
}
}
+116 -15
View File
@@ -1,45 +1,113 @@
package funkin.graphics.video;
import flixel.FlxBasic;
import flixel.FlxSprite;
import flixel.util.FlxColor;
import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.audio.FunkinSound;
import openfl.display3D.textures.TextureBase;
import openfl.events.NetStatusEvent;
import openfl.media.SoundTransform;
import openfl.media.Video;
import openfl.net.NetConnection;
import openfl.net.NetStream;
/**
* Plays a video via a NetStream. Only works on HTML5.
* This does NOT replace hxCodec, nor does hxCodec replace this. hxCodec only works on desktop and does not work on HTML5!
* This does NOT replace hxCodec, nor does hxCodec replace this.
* hxCodec only works on desktop and does not work on HTML5!
*/
class FlxVideo extends FlxBasic
class FlxVideo extends FunkinSprite
{
var video:Video;
var netStream:NetStream;
public var finishCallback:Void->Void;
var videoPath:String;
/**
* Doesn't actually interact with Flixel shit, only just a pleasant to use class
* A callback to execute when the video finishes.
*/
public var finishCallback:Void->Void;
public function new(videoPath:String)
{
super();
this.videoPath = videoPath;
makeGraphic(2, 2, FlxColor.TRANSPARENT);
video = new Video();
video.x = 0;
video.y = 0;
video.alpha = 0;
FlxG.addChildBelowMouse(video);
FlxG.game.addChild(video);
var netConnection = new NetConnection();
var netConnection:NetConnection = new NetConnection();
netConnection.connect(null);
netStream = new NetStream(netConnection);
netStream.client = {onMetaData: client_onMetaData};
netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus);
netStream.client = {onMetaData: onClientMetaData};
netConnection.addEventListener(NetStatusEvent.NET_STATUS, onNetConnectionNetStatus);
netStream.play(videoPath);
}
/**
* Tell the FlxVideo to pause playback.
*/
public function pauseVideo():Void
{
if (netStream != null)
{
netStream.pause();
}
}
/**
* Tell the FlxVideo to resume if it is paused.
*/
public function resumeVideo():Void
{
// Resume playing the video.
if (netStream != null)
{
netStream.resume();
}
}
var videoAvailable:Bool = false;
var frameTimer:Float;
static final FRAME_RATE:Float = 60;
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (frameTimer >= (1 / FRAME_RATE))
{
frameTimer = 0;
// TODO: We just draw the video buffer to the sprite 60 times a second.
// Can we copy the video buffer instead somehow?
pixels.draw(video);
}
if (videoAvailable) frameTimer += elapsed;
}
/**
* Tell the FlxVideo to seek to the beginning.
*/
public function restartVideo():Void
{
// Seek to the beginning of the video.
if (netStream != null)
{
netStream.seek(0);
}
}
/**
* Tell the FlxVideo to end.
*/
public function finishVideo():Void
{
netStream.dispose();
@@ -48,15 +116,48 @@ class FlxVideo extends FlxBasic
if (finishCallback != null) finishCallback();
}
public function client_onMetaData(metaData:Dynamic)
public override function destroy():Void
{
if (netStream != null)
{
netStream.dispose();
if (FlxG.game.contains(video)) FlxG.game.removeChild(video);
}
super.destroy();
}
/**
* Callback executed when the video stream loads.
* @param metaData The metadata of the video
*/
public function onClientMetaData(metaData:Dynamic):Void
{
video.attachNetStream(netStream);
video.width = FlxG.width;
video.height = FlxG.height;
onVideoReady();
}
function netConnection_onNetStatus(event:NetStatusEvent):Void
function onVideoReady():Void
{
video.width = FlxG.width;
video.height = FlxG.height;
videoAvailable = true;
FunkinSound.onVolumeChanged.add(onVolumeChanged);
onVolumeChanged(FlxG.sound.muted ? 0 : FlxG.sound.volume);
makeGraphic(Std.int(video.width), Std.int(video.height), FlxColor.TRANSPARENT);
}
function onVolumeChanged(volume:Float):Void
{
netStream.soundTransform = new SoundTransform(volume);
}
function onNetConnectionNetStatus(event:NetStatusEvent):Void
{
if (event.info.code == 'NetStream.Play.Complete') finishVideo();
}
-1
View File
@@ -13,7 +13,6 @@ using Lambda;
using StringTools;
using funkin.util.tools.ArraySortTools;
using funkin.util.tools.ArrayTools;
using funkin.util.tools.DynamicTools;
using funkin.util.tools.FloatTools;
using funkin.util.tools.Int64Tools;
using funkin.util.tools.IntTools;
+31 -49
View File
@@ -58,12 +58,11 @@ class Controls extends FlxActionSet
var _back = new FlxActionDigital(Action.BACK);
var _pause = new FlxActionDigital(Action.PAUSE);
var _reset = new FlxActionDigital(Action.RESET);
var _screenshot = new FlxActionDigital(Action.SCREENSHOT);
var _cutscene_advance = new FlxActionDigital(Action.CUTSCENE_ADVANCE);
var _cutscene_skip = new FlxActionDigital(Action.CUTSCENE_SKIP);
var _debug_menu = new FlxActionDigital(Action.DEBUG_MENU);
var _debug_chart = new FlxActionDigital(Action.DEBUG_CHART);
var _debug_stage = new FlxActionDigital(Action.DEBUG_STAGE);
var _screenshot = new FlxActionDigital(Action.SCREENSHOT);
var _volume_up = new FlxActionDigital(Action.VOLUME_UP);
var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN);
var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE);
@@ -208,16 +207,21 @@ class Controls extends FlxActionSet
inline function get_PAUSE()
return _pause.check();
public var RESET(get, never):Bool;
inline function get_RESET()
return _reset.check();
public var SCREENSHOT(get, never):Bool;
inline function get_SCREENSHOT()
return _screenshot.check();
public var CUTSCENE_ADVANCE(get, never):Bool;
inline function get_CUTSCENE_ADVANCE()
return _cutscene_advance.check();
public var CUTSCENE_SKIP(get, never):Bool;
inline function get_CUTSCENE_SKIP()
return _cutscene_skip.check();
public var DEBUG_MENU(get, never):Bool;
inline function get_DEBUG_MENU()
@@ -233,11 +237,6 @@ class Controls extends FlxActionSet
inline function get_DEBUG_STAGE()
return _debug_stage.check();
public var SCREENSHOT(get, never):Bool;
inline function get_SCREENSHOT()
return _screenshot.check();
public var VOLUME_UP(get, never):Bool;
inline function get_VOLUME_UP()
@@ -253,11 +252,6 @@ class Controls extends FlxActionSet
inline function get_VOLUME_MUTE()
return _volume_mute.check();
public var RESET(get, never):Bool;
inline function get_RESET()
return _reset.check();
public function new(name, scheme:KeyboardScheme = null)
{
super(name);
@@ -289,16 +283,15 @@ class Controls extends FlxActionSet
add(_accept);
add(_back);
add(_pause);
add(_reset);
add(_screenshot);
add(_cutscene_advance);
add(_cutscene_skip);
add(_debug_menu);
add(_debug_chart);
add(_debug_stage);
add(_screenshot);
add(_volume_up);
add(_volume_down);
add(_volume_mute);
add(_reset);
for (action in digitalActions)
byName[action.name] = action;
@@ -383,12 +376,11 @@ class Controls extends FlxActionSet
case BACK: _back;
case PAUSE: _pause;
case RESET: _reset;
case SCREENSHOT: _screenshot;
case CUTSCENE_ADVANCE: _cutscene_advance;
case CUTSCENE_SKIP: _cutscene_skip;
case DEBUG_MENU: _debug_menu;
case DEBUG_CHART: _debug_chart;
case DEBUG_STAGE: _debug_stage;
case SCREENSHOT: _screenshot;
case VOLUME_UP: _volume_up;
case VOLUME_DOWN: _volume_down;
case VOLUME_MUTE: _volume_mute;
@@ -449,26 +441,24 @@ class Controls extends FlxActionSet
func(_back, JUST_PRESSED);
case PAUSE:
func(_pause, JUST_PRESSED);
case RESET:
func(_reset, JUST_PRESSED);
case SCREENSHOT:
func(_screenshot, JUST_PRESSED);
case CUTSCENE_ADVANCE:
func(_cutscene_advance, JUST_PRESSED);
case CUTSCENE_SKIP:
func(_cutscene_skip, PRESSED);
case DEBUG_MENU:
func(_debug_menu, JUST_PRESSED);
case DEBUG_CHART:
func(_debug_chart, JUST_PRESSED);
case DEBUG_STAGE:
func(_debug_stage, JUST_PRESSED);
case SCREENSHOT:
func(_screenshot, JUST_PRESSED);
case VOLUME_UP:
func(_volume_up, JUST_PRESSED);
case VOLUME_DOWN:
func(_volume_down, JUST_PRESSED);
case VOLUME_MUTE:
func(_volume_mute, JUST_PRESSED);
case RESET:
func(_reset, JUST_PRESSED);
}
}
@@ -654,13 +644,12 @@ class Controls extends FlxActionSet
bindKeys(Control.ACCEPT, getDefaultKeybinds(scheme, Control.ACCEPT));
bindKeys(Control.BACK, getDefaultKeybinds(scheme, Control.BACK));
bindKeys(Control.PAUSE, getDefaultKeybinds(scheme, Control.PAUSE));
bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET));
bindKeys(Control.SCREENSHOT, getDefaultKeybinds(scheme, Control.SCREENSHOT));
bindKeys(Control.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE));
bindKeys(Control.CUTSCENE_SKIP, getDefaultKeybinds(scheme, Control.CUTSCENE_SKIP));
bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU));
bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART));
bindKeys(Control.DEBUG_STAGE, getDefaultKeybinds(scheme, Control.DEBUG_STAGE));
bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET));
bindKeys(Control.SCREENSHOT, getDefaultKeybinds(scheme, Control.SCREENSHOT));
bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP));
bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN));
bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE));
@@ -683,16 +672,15 @@ class Controls extends FlxActionSet
case Control.ACCEPT: return [Z, SPACE, ENTER];
case Control.BACK: return [X, BACKSPACE, ESCAPE];
case Control.PAUSE: return [P, ENTER, ESCAPE];
case Control.RESET: return [R];
case Control.SCREENSHOT: return [F3]; // TODO: Change this back to PrintScreen
case Control.CUTSCENE_ADVANCE: return [Z, ENTER];
case Control.CUTSCENE_SKIP: return [P, ESCAPE];
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
case Control.SCREENSHOT: return [F3]; // TODO: Change this back to PrintScreen
case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
case Control.RESET: return [R];
}
case Duo(true):
switch (control) {
@@ -707,16 +695,15 @@ class Controls extends FlxActionSet
case Control.ACCEPT: return [G, Z];
case Control.BACK: return [H, X];
case Control.PAUSE: return [ONE];
case Control.RESET: return [R];
case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.CUTSCENE_ADVANCE: return [G, Z];
case Control.CUTSCENE_SKIP: return [ONE];
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.VOLUME_UP: return [PLUS];
case Control.VOLUME_DOWN: return [MINUS];
case Control.VOLUME_MUTE: return [ZERO];
case Control.RESET: return [R];
}
case Duo(false):
switch (control) {
@@ -731,16 +718,15 @@ class Controls extends FlxActionSet
case Control.ACCEPT: return [ENTER];
case Control.BACK: return [ESCAPE];
case Control.PAUSE: return [ONE];
case Control.RESET: return [R];
case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.CUTSCENE_ADVANCE: return [ENTER];
case Control.CUTSCENE_SKIP: return [ONE];
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.VOLUME_UP: return [NUMPADPLUS];
case Control.VOLUME_DOWN: return [NUMPADMINUS];
case Control.VOLUME_MUTE: return [NUMPADZERO];
case Control.RESET: return [R];
}
default:
// Fallthrough.
@@ -843,15 +829,14 @@ class Controls extends FlxActionSet
Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT),
Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT),
Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE),
Control.RESET => getDefaultGamepadBinds(Control.RESET),
// Control.SCREENSHOT => [],
// Control.VOLUME_UP => [RIGHT_SHOULDER],
// Control.VOLUME_DOWN => [LEFT_SHOULDER],
// Control.VOLUME_MUTE => [RIGHT_TRIGGER],
Control.CUTSCENE_ADVANCE => getDefaultGamepadBinds(Control.CUTSCENE_ADVANCE),
Control.CUTSCENE_SKIP => getDefaultGamepadBinds(Control.CUTSCENE_SKIP),
// Control.DEBUG_MENU
// Control.DEBUG_CHART
Control.RESET => getDefaultGamepadBinds(Control.RESET)
]);
}
@@ -868,15 +853,14 @@ class Controls extends FlxActionSet
case Control.NOTE_LEFT: return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
case Control.NOTE_RIGHT: return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
case Control.PAUSE: return [START];
case Control.RESET: return [RIGHT_SHOULDER];
case Control.SCREENSHOT: return [];
case Control.VOLUME_UP: return [];
case Control.VOLUME_DOWN: return [];
case Control.VOLUME_MUTE: return [];
case Control.CUTSCENE_ADVANCE: return [A];
case Control.CUTSCENE_SKIP: return [START];
case Control.DEBUG_MENU: return [];
case Control.DEBUG_CHART: return [];
case Control.RESET: return [RIGHT_SHOULDER];
default:
// Fallthrough.
}
@@ -1228,14 +1212,13 @@ enum Control
UI_RIGHT;
UI_DOWN;
RESET;
SCREENSHOT;
ACCEPT;
BACK;
PAUSE;
// CUTSCENE
CUTSCENE_ADVANCE;
CUTSCENE_SKIP;
// SCREENSHOT
SCREENSHOT;
// VOLUME
VOLUME_UP;
VOLUME_DOWN;
@@ -1279,15 +1262,14 @@ abstract Action(String) to String from String
var BACK = "back";
var PAUSE = "pause";
var RESET = "reset";
// SCREENSHOT
var SCREENSHOT = "screenshot";
// CUTSCENE
var CUTSCENE_ADVANCE = "cutscene_advance";
var CUTSCENE_SKIP = "cutscene_skip";
// VOLUME
var VOLUME_UP = "volume_up";
var VOLUME_DOWN = "volume_down";
var VOLUME_MUTE = "volume_mute";
// SCREENSHOT
var SCREENSHOT = "screenshot";
// DEBUG
var DEBUG_MENU = "debug_menu";
var DEBUG_CHART = "debug_chart";
+15 -13
View File
@@ -56,7 +56,20 @@ interface IStateStageProp extends IScriptedClass
*/
interface INoteScriptedClass extends IScriptedClass
{
public function onNoteHit(event:NoteScriptEvent):Void;
/**
* Called when a note enters the field of view and approaches the strumline.
*/
public function onNoteIncoming(event:NoteScriptEvent):Void;
/**
* Called when EITHER player hits a note.
* Query the note attached to the event to determine if it was hit by the player or CPU.
*/
public function onNoteHit(event:HitNoteScriptEvent):Void;
/**
* Called when EITHER player (usually the player) misses a note.
*/
public function onNoteMiss(event:NoteScriptEvent):Void;
}
@@ -73,7 +86,7 @@ interface INoteScriptedClass extends IScriptedClass
/**
* Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State.
*/
interface IPlayStateScriptedClass extends IScriptedClass
interface IPlayStateScriptedClass extends INoteScriptedClass
{
/**
* Called when the game is paused.
@@ -113,17 +126,6 @@ interface IPlayStateScriptedClass extends IScriptedClass
*/
public function onSongRetry(event:ScriptEvent):Void;
/**
* Called when EITHER player hits a note.
* Query the note attached to the event to determine if it was hit by the player or CPU.
*/
public function onNoteHit(event:NoteScriptEvent):Void;
/**
* Called when EITHER player (usually the player) misses a note.
*/
public function onNoteMiss(event:NoteScriptEvent):Void;
/**
* Called when the player presses a key when no note is on the strumline.
*/
+92 -42
View File
@@ -1,24 +1,26 @@
package funkin.modding;
import funkin.util.macro.ClassMacro;
import funkin.modding.module.ModuleHandler;
import funkin.data.song.SongData;
import funkin.data.stage.StageData;
import polymod.Polymod;
import polymod.backends.PolymodAssets.PolymodAssetType;
import polymod.format.ParseRules.TextFileFormat;
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.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.freeplay.AlbumRegistry;
import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.save.Save;
import funkin.data.song.SongRegistry;
import funkin.util.FileUtil;
import funkin.util.macro.ClassMacro;
import polymod.backends.PolymodAssets.PolymodAssetType;
import polymod.format.ParseRules.TextFileFormat;
import polymod.Polymod;
/**
* A class for interacting with Polymod, the atomic modding framework for Haxe.
*/
class PolymodHandler
{
/**
@@ -27,16 +29,33 @@ class PolymodHandler
* Bug fixes increment the patch version, new features increment the minor version.
* Changes that break old mods increment the major version.
*/
static final API_VERSION:String = "0.1.0";
static final API_VERSION:String = '0.1.0';
/**
* Where relative to the executable that mods are located.
*/
static final MOD_FOLDER:String = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../example_mods" #elseif REDIRECT_ASSETS_FOLDER "../../../../example_mods" #else "mods" #end;
static final MOD_FOLDER:String =
#if (REDIRECT_ASSETS_FOLDER && macos)
'../../../../../../../example_mods'
#elseif REDIRECT_ASSETS_FOLDER
'../../../../example_mods'
#else
'mods'
#end;
static final CORE_FOLDER:Null<String> = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../assets" #elseif REDIRECT_ASSETS_FOLDER "../../../../assets" #else null #end;
static final CORE_FOLDER:Null<String> =
#if (REDIRECT_ASSETS_FOLDER && macos)
'../../../../../../../assets'
#elseif REDIRECT_ASSETS_FOLDER
'../../../../assets'
#else
null
#end;
public static function createModRoot()
/**
* If the mods folder doesn't exist, create it.
*/
public static function createModRoot():Void
{
FileUtil.createDirIfNotExists(MOD_FOLDER);
}
@@ -44,40 +63,44 @@ class PolymodHandler
/**
* Loads the game with ALL mods enabled with Polymod.
*/
public static function loadAllMods()
public static function loadAllMods():Void
{
// Create the mod root if it doesn't exist.
createModRoot();
trace("Initializing Polymod (using all mods)...");
trace('Initializing Polymod (using all mods)...');
loadModsById(getAllModIds());
}
/**
* Loads the game with configured mods enabled with Polymod.
*/
public static function loadEnabledMods()
public static function loadEnabledMods():Void
{
// Create the mod root if it doesn't exist.
createModRoot();
trace("Initializing Polymod (using configured mods)...");
loadModsById(Save.get().enabledModIds);
trace('Initializing Polymod (using configured mods)...');
loadModsById(Save.instance.enabledModIds);
}
/**
* Loads the game without any mods enabled with Polymod.
*/
public static function loadNoMods()
public static function loadNoMods():Void
{
// Create the mod root if it doesn't exist.
createModRoot();
// We still need to configure the debug print calls etc.
trace("Initializing Polymod (using no mods)...");
trace('Initializing Polymod (using no mods)...');
loadModsById([]);
}
public static function loadModsById(ids:Array<String>)
/**
* Load all the mods with the given ids.
* @param ids The ORDERED list of mod ids to load.
*/
public static function loadModsById(ids:Array<String>):Void
{
if (ids.length == 0)
{
@@ -90,7 +113,7 @@ class PolymodHandler
buildImports();
var loadedModList = polymod.Polymod.init(
var loadedModList:Array<ModMetadata> = polymod.Polymod.init(
{
// Root directory for all mods.
modRoot: MOD_FOLDER,
@@ -142,30 +165,40 @@ class PolymodHandler
}
#if debug
var fileList = Polymod.listModFiles(PolymodAssetType.IMAGE);
var fileList:Array<String> = Polymod.listModFiles(PolymodAssetType.IMAGE);
trace('Installed mods have replaced ${fileList.length} images.');
for (item in fileList)
{
trace(' * $item');
}
fileList = Polymod.listModFiles(PolymodAssetType.TEXT);
trace('Installed mods have added/replaced ${fileList.length} text files.');
for (item in fileList)
{
trace(' * $item');
}
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC);
trace('Installed mods have replaced ${fileList.length} music files.');
for (item in fileList)
{
trace(' * $item');
}
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND);
trace('Installed mods have replaced ${fileList.length} sound files.');
for (item in fileList)
{
trace(' * $item');
}
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC);
trace('Installed mods have replaced ${fileList.length} generic audio files.');
for (item in fileList)
{
trace(' * $item');
}
#end
}
@@ -183,21 +216,21 @@ class PolymodHandler
for (cls in ClassMacro.listClassesInPackage('polymod'))
{
if (cls == null) continue;
var className = Type.getClassName(cls);
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
}
static function buildParseRules():polymod.format.ParseRules
{
var output = polymod.format.ParseRules.getDefault();
var output:polymod.format.ParseRules = polymod.format.ParseRules.getDefault();
// Ensure TXT files have merge support.
output.addType("txt", TextFileFormat.LINES);
output.addType('txt', TextFileFormat.LINES);
// Ensure script files have merge support.
output.addType("hscript", TextFileFormat.PLAINTEXT);
output.addType("hxs", TextFileFormat.PLAINTEXT);
output.addType("hxc", TextFileFormat.PLAINTEXT);
output.addType("hx", TextFileFormat.PLAINTEXT);
output.addType('hscript', TextFileFormat.PLAINTEXT);
output.addType('hxs', TextFileFormat.PLAINTEXT);
output.addType('hxc', TextFileFormat.PLAINTEXT);
output.addType('hx', TextFileFormat.PLAINTEXT);
// You can specify the format of a specific file, with file extension.
// output.addFile("data/introText.txt", TextFileFormat.LINES)
@@ -208,17 +241,21 @@ class PolymodHandler
{
return {
assetLibraryPaths: [
"default" => "preload", "shared" => "shared", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2",
"week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'tutorial' => 'tutorial', 'week1' => 'week1', 'week2' => 'week2',
'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1',
],
coreAssetRedirect: CORE_FOLDER,
}
}
/**
* Retrieve a list of metadata for ALL installed mods, including disabled mods.
* @return An array of mod metadata
*/
public static function getAllMods():Array<ModMetadata>
{
trace('Scanning the mods folder...');
var modMetadata = Polymod.scan(
var modMetadata:Array<ModMetadata> = Polymod.scan(
{
modRoot: MOD_FOLDER,
apiVersionRule: API_VERSION,
@@ -228,17 +265,25 @@ class PolymodHandler
return modMetadata;
}
/**
* Retrieve a list of ALL mod IDs, including disabled mods.
* @return An array of mod IDs
*/
public static function getAllModIds():Array<String>
{
var modIds = [for (i in getAllMods()) i.id];
var modIds:Array<String> = [for (i in getAllMods()) i.id];
return modIds;
}
/**
* Retrieve a list of metadata for all enabled mods.
* @return An array of mod metadata
*/
public static function getEnabledMods():Array<ModMetadata>
{
var modIds = Save.get().enabledModIds;
var modMetadata = getAllMods();
var enabledMods = [];
var modIds:Array<String> = Save.instance.enabledModIds;
var modMetadata:Array<ModMetadata> = getAllMods();
var enabledMods:Array<ModMetadata> = [];
for (item in modMetadata)
{
if (modIds.indexOf(item.id) != -1)
@@ -249,7 +294,11 @@ class PolymodHandler
return enabledMods;
}
public static function forceReloadAssets()
/**
* Clear and reload from disk all data assets.
* Useful for "hot reloading" for fast iteration!
*/
public static function forceReloadAssets():Void
{
// Forcibly clear scripts so that scripts can be edited.
ModuleHandler.clearModuleCache();
@@ -276,6 +325,7 @@ class PolymodHandler
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries();
AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
ModuleHandler.loadModuleCache();
+37 -5
View File
@@ -106,12 +106,19 @@ class NoteScriptEvent extends ScriptEvent
*/
public var playSound(default, default):Bool;
public function new(type:ScriptEventType, note:NoteSprite, comboCount:Int = 0, cancelable:Bool = false):Void
/**
* The health gained or lost from this note.
* This affects both hits and misses. Remember that max health is 2.00.
*/
public var healthChange:Float;
public function new(type:ScriptEventType, note:NoteSprite, healthChange:Float, comboCount:Int = 0, cancelable:Bool = false):Void
{
super(type, cancelable);
this.note = note;
this.comboCount = comboCount;
this.playSound = true;
this.healthChange = healthChange;
}
public override function toString():String
@@ -120,6 +127,31 @@ class NoteScriptEvent extends ScriptEvent
}
}
class HitNoteScriptEvent extends NoteScriptEvent
{
/**
* The judgement the player received for hitting the note.
*/
public var judgement:String;
/**
* The score the player received for hitting the note.
*/
public var score:Int;
public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, comboCount:Int = 0):Void
{
super(NOTE_HIT, note, healthChange, comboCount, true);
this.score = score;
this.judgement = judgement;
}
public override function toString():String
{
return 'HitNoteScriptEvent(note=' + note + ', comboCount=' + comboCount + ', judgement=' + judgement + ', score=' + score + ')';
}
}
/**
* An event that is fired when you press a key with no note present.
*/
@@ -182,17 +214,17 @@ class SongEventScriptEvent extends ScriptEvent
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
public var event(default, null):funkin.data.song.SongData.SongEventData;
public var eventData(default, null):funkin.data.song.SongData.SongEventData;
public function new(event:funkin.data.song.SongData.SongEventData):Void
public function new(eventData:funkin.data.song.SongData.SongEventData):Void
{
super(SONG_EVENT, true);
this.event = event;
this.eventData = eventData;
}
public override function toString():String
{
return 'SongEventScriptEvent(event=' + event + ')';
return 'SongEventScriptEvent(event=' + eventData + ')';
}
}
@@ -71,17 +71,29 @@ class ScriptEventDispatcher
}
}
if (Std.isOfType(target, IPlayStateScriptedClass))
if (Std.isOfType(target, INoteScriptedClass))
{
var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
var t:INoteScriptedClass = cast(target, INoteScriptedClass);
switch (event.type)
{
case NOTE_INCOMING:
t.onNoteIncoming(cast event);
return;
case NOTE_HIT:
t.onNoteHit(cast event);
return;
case NOTE_MISS:
t.onNoteMiss(cast event);
return;
default: // Continue;
}
}
if (Std.isOfType(target, IPlayStateScriptedClass))
{
var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
switch (event.type)
{
case NOTE_GHOST_MISS:
t.onNoteGhostMiss(cast event);
return;
@@ -63,6 +63,13 @@ enum abstract ScriptEventType(String) from String to String
*/
var SONG_STEP_HIT = 'STEP_HIT';
/**
* Called when a note comes on screen and starts approaching the strumline.
*
* This event is not cancelable.
*/
var NOTE_INCOMING = 'NOTE_INCOMING';
/**
* Called when a character hits a note.
* Important information such as judgement/timing, note data, player/opponent, etc. are all provided.
+3 -1
View File
@@ -83,7 +83,9 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
public function onGameOver(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteIncoming(event:NoteScriptEvent) {}
public function onNoteHit(event:HitNoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
+2 -1
View File
@@ -3,6 +3,7 @@ package funkin.play;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.FlxSprite;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent;
@@ -214,7 +215,7 @@ class Countdown
if (spritePath == null) return;
var countdownSprite:FlxSprite = new FlxSprite(0, 0).loadGraphic(Paths.image(spritePath));
var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
countdownSprite.scrollFactor.set(0, 0);
if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
+180 -69
View File
@@ -4,16 +4,19 @@ import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.sound.FlxSound;
import funkin.ui.story.StoryMenuState;
import funkin.audio.FunkinSound;
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;
import funkin.play.PlayState;
import funkin.ui.freeplay.FreeplayState;
import funkin.play.character.BaseCharacter;
import funkin.play.PlayState;
import funkin.util.MathUtil;
import funkin.ui.freeplay.FreeplayState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.story.StoryMenuState;
import openfl.utils.Assets;
/**
* A substate which renders over the PlayState when the player dies.
@@ -63,7 +66,7 @@ class GameOverSubState extends MusicBeatSubState
/**
* The music playing in the background of the state.
*/
var gameOverMusic:FlxSound = new FlxSound();
var gameOverMusic:Null<FunkinSound> = null;
/**
* Whether the player has confirmed and prepared to restart the level.
@@ -71,10 +74,18 @@ class GameOverSubState extends MusicBeatSubState
*/
var isEnding:Bool = false;
/**
* Whether the death music is on its first loop.
*/
var isStarting:Bool = true;
var isChartingMode:Bool = false;
var transparent:Bool;
final CAMERA_ZOOM_DURATION:Float = 0.5;
var targetCameraZoom:Float = 1.0;
public function new(params:GameOverParams)
{
super();
@@ -86,14 +97,14 @@ class GameOverSubState extends MusicBeatSubState
/**
* Reset the game over configuration to the default.
*/
public static function reset()
public static function reset():Void
{
animationSuffix = "";
musicSuffix = "";
blueBallSuffix = "";
animationSuffix = '';
musicSuffix = '';
blueBallSuffix = '';
}
override public function create()
override public function create():Void
{
if (instance != null)
{
@@ -108,6 +119,8 @@ class GameOverSubState extends MusicBeatSubState
// Set up the visuals
//
var playState = PlayState.instance;
// Add a black background to the screen.
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,
@@ -119,55 +132,73 @@ class GameOverSubState extends MusicBeatSubState
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
// We can then play the character's `firstDeath` animation.
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
boyfriend.isDead = true;
add(boyfriend);
boyfriend.resetCharacter();
if (PlayState.instance.isMinimalMode) {}
else
{
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
boyfriend.isDead = true;
add(boyfriend);
boyfriend.resetCharacter();
// Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint);
// Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint);
FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom();
}
//
// Set up the audio
//
// Prepare the game over music.
FlxG.sound.list.add(gameOverMusic);
gameOverMusic.stop();
// The conductor now represents the BPM of the game over music.
Conductor.instance.update(0);
}
public function resetCameraZoom():Void
{
// Apply camera zoom level from stage data.
FlxG.camera.zoom = PlayState?.instance?.currentStage?.camZoom ?? 1.0;
}
var hasStartedAnimation:Bool = false;
override function update(elapsed:Float)
override function update(elapsed:Float):Void
{
if (!hasStartedAnimation)
{
hasStartedAnimation = true;
if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100))
if (PlayState.instance.isMinimalMode)
{
boyfriend.playAnimation('fakeoutDeath', true, false);
}
else
{
boyfriend.playAnimation('firstDeath', true, false); // ignoreOther is set to FALSE since you WANT to be able to mash and confirm game over!
// Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX();
}
else
{
if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100))
{
boyfriend.playAnimation('fakeoutDeath', true, false);
}
else
{
boyfriend.playAnimation('firstDeath', true, false); // ignoreOther is set to FALSE since you WANT to be able to mash and confirm game over!
// Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX();
}
}
}
// Smoothly lerp the camera
FlxG.camera.zoom = MathUtil.smoothLerp(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION);
//
// Handle user inputs.
//
@@ -216,7 +247,7 @@ class GameOverSubState extends MusicBeatSubState
}
}
if (gameOverMusic.playing)
if (gameOverMusic != null && gameOverMusic.playing)
{
// Match the conductor to the music.
// This enables the stepHit and beatHit events.
@@ -224,27 +255,34 @@ class GameOverSubState extends MusicBeatSubState
}
else
{
// Music hasn't started yet.
switch (PlayStatePlaylist.campaignId)
if (PlayState.instance.isMinimalMode)
{
// TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded.
// This will simplify the class and make it easier for mods to add death quotes.
case "week7":
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
{
playingJeffQuote = true;
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.
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
{
startDeathMusic(1.0, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
// startDeathMusic(1.0, false);
}
else
{
// Music hasn't started yet.
switch (PlayStatePlaylist.campaignId)
{
// TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded.
// This will simplify the class and make it easier for mods to add death quotes.
case 'week7':
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
{
playingJeffQuote = true;
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.
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
{
startDeathMusic(1.0, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
}
}
}
@@ -262,7 +300,11 @@ class GameOverSubState extends MusicBeatSubState
isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
if (PlayState.instance.isMinimalMode) {}
else
{
boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
}
// After the animation finishes...
new FlxTimer().start(0.7, function(tmr:FlxTimer) {
@@ -272,10 +314,17 @@ class GameOverSubState extends MusicBeatSubState
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.instance.needsReset = true;
// Readd Boyfriend to the stage.
boyfriend.isDead = false;
remove(boyfriend);
PlayState.instance.currentStage.addCharacter(boyfriend, BF);
if (PlayState.instance.isMinimalMode) {}
else
{
// Readd Boyfriend to the stage.
boyfriend.isDead = false;
remove(boyfriend);
PlayState.instance.currentStage.addCharacter(boyfriend, BF);
}
// Snap reset the camera which may have changed because of the player character data.
resetCameraZoom();
// Close the substate.
close();
@@ -291,24 +340,71 @@ class GameOverSubState extends MusicBeatSubState
ScriptEventDispatcher.callEvent(boyfriend, event);
}
/**
* Rather than hardcoding stuff, we look for the presence of a music file
* with the given suffix, and strip it down until we find one that's valid.
*/
function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null<String>
{
var basePath = 'gameplay/gameover/gameOver';
if (starting) basePath += 'Start';
else if (ending) basePath += 'End';
var musicPath = Paths.music(basePath + suffix);
while (!Assets.exists(musicPath) && suffix.length > 0)
{
suffix = suffix.split('-').slice(0, -1).join('-');
musicPath = Paths.music(basePath + suffix);
}
if (!Assets.exists(musicPath)) return null;
trace('Resolved music path: ' + musicPath);
return musicPath;
}
/**
* Starts the death music at the appropriate volume.
* @param startingVolume
*/
function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void
{
var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix);
if (isEnding)
var musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
var onComplete = null;
if (isStarting)
{
musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix);
if (musicPath == null)
{
isStarting = false;
musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
}
else
{
onComplete = function() {
isStarting = false;
// We need to force to ensure that the non-starting music plays.
startDeathMusic(1.0, true);
};
}
}
if (!gameOverMusic.playing || force)
if (musicPath == null)
{
gameOverMusic.loadEmbedded(musicPath);
trace('Could not find game over music!');
return;
}
else if (gameOverMusic == null || !gameOverMusic.playing || force)
{
if (gameOverMusic != null) gameOverMusic.stop();
gameOverMusic = FunkinSound.load(musicPath);
gameOverMusic.volume = startingVolume;
gameOverMusic.looped = !isEnding;
gameOverMusic.looped = !(isEnding || isStarting);
gameOverMusic.onComplete = onComplete;
gameOverMusic.play();
}
else
{
@:privateAccess
trace('Music already playing! ${gameOverMusic?._label}');
}
}
static var blueballed:Bool = false;
@@ -320,7 +416,14 @@ class GameOverSubState extends MusicBeatSubState
public static function playBlueBalledSFX()
{
blueballed = true;
FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)))
{
FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
}
else
{
FlxG.log.error('Missing blue ball sound effect: ' + Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
}
}
var playingJeffQuote:Bool = false;
@@ -344,6 +447,14 @@ class GameOverSubState extends MusicBeatSubState
});
}
public override function destroy()
{
super.destroy();
if (gameOverMusic != null) gameOverMusic.stop();
gameOverMusic = null;
instance = null;
}
public override function toString():String
{
return "GameOverSubState";
+5 -7
View File
@@ -3,6 +3,7 @@ package funkin.play;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxAtlasFrames;
import funkin.play.PlayState;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import flixel.addons.transition.FlxTransitionableState;
import funkin.ui.mainmenu.MainMenuState;
@@ -27,25 +28,22 @@ class GitarooPause extends MusicBeatState
{
if (FlxG.sound.music != null) FlxG.sound.music.stop();
var bg:FlxSprite = new FlxSprite().loadGraphic(Paths.image('pauseAlt/pauseBG'));
var bg:FunkinSprite = FunkinSprite.create('pauseAlt/pauseBG');
add(bg);
var bf:FlxSprite = new FlxSprite(0, 30);
bf.frames = Paths.getSparrowAtlas('pauseAlt/bfLol');
var bf:FunkinSprite = FunkinSprite.createSparrow(0, 30, 'pauseAlt/bfLol');
bf.animation.addByPrefix('lol', "funnyThing", 13);
bf.animation.play('lol');
add(bf);
bf.screenCenter(X);
replayButton = new FlxSprite(FlxG.width * 0.28, FlxG.height * 0.7);
replayButton.frames = Paths.getSparrowAtlas('pauseAlt/pauseUI');
replayButton = FunkinSprite.createSparrow(FlxG.width * 0.28, FlxG.height * 0.7, 'pauseAlt/pauseUI');
replayButton.animation.addByPrefix('selected', 'bluereplay', 0, false);
replayButton.animation.appendByPrefix('selected', 'yellowreplay');
replayButton.animation.play('selected');
add(replayButton);
cancelButton = new FlxSprite(FlxG.width * 0.58, replayButton.y);
cancelButton.frames = Paths.getSparrowAtlas('pauseAlt/pauseUI');
cancelButton = FunkinSprite.createSparrow(FlxG.width * 0.58, replayButton.y, 'pauseAlt/pauseUI');
cancelButton.animation.addByPrefix('selected', 'bluecancel', 0, false);
cancelButton.animation.appendByPrefix('selected', 'cancelyellow');
cancelButton.animation.play('selected');
+676 -254
View File
@@ -1,316 +1,738 @@
package funkin.play;
import funkin.play.PlayStatePlaylist;
import flixel.FlxSprite;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.ui.MusicBeatSubState;
import funkin.ui.debug.latency.LatencyState;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.play.PlayState;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.data.song.SongRegistry;
import funkin.ui.Alphabet;
import funkin.graphics.FunkinSprite;
import funkin.play.cutscene.VideoCutscene;
import funkin.play.PlayState;
import funkin.ui.AtlasText;
import funkin.ui.debug.latency.LatencyState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.transition.StickerSubState;
/**
* Parameters for initializing the PauseSubState.
*/
typedef PauseSubStateParams =
{
/**
* Which mode to start in. Dictates what entries are displayed.
*/
?mode:PauseMode,
};
/**
* The menu displayed when the Play State is paused.
*/
class PauseSubState extends MusicBeatSubState
{
var grpMenuShit:FlxTypedGroup<Alphabet>;
// ===============
// Constants
// ===============
// todo: maybe make these enums or somethin?
final pauseOptionsBase:Array<String> = [
'Resume',
'Restart Song',
'Change Difficulty',
'Toggle Practice Mode',
'Adjust Input Offsets',
'Exit to Menu'
/**
* Pause menu entries for when the game is paused during a song.
*/
static final PAUSE_MENU_ENTRIES_STANDARD:Array<PauseMenuEntry> = [
{text: 'Resume', callback: resume},
{text: 'Restart Song', callback: restartPlayState},
{text: 'Change Difficulty', callback: switchMode.bind(_, Difficulty)},
{text: 'Enable Practice Mode', callback: enablePracticeMode, filter: () -> !(PlayState.instance?.isPracticeMode ?? false)},
{text: 'Exit to Menu', callback: quitToMenu},
];
final pauseOptionsCharting:Array<String> = ['Resume', 'Restart Song', 'Adjust Input Offsets', 'Exit to Chart Editor'];
final pauseOptionsDifficultyBase:Array<String> = ['BACK'];
/**
* Pause menu entries for when the game is paused in the Chart Editor preview.
*/
static final PAUSE_MENU_ENTRIES_CHARTING:Array<PauseMenuEntry> = [
{text: 'Resume', callback: resume},
{text: 'Restart Song', callback: restartPlayState},
{text: 'Return to Chart Editor', callback: quitToChartEditor},
];
var pauseOptionsDifficulty:Array<String> = []; // AUTO-POPULATED
/**
* Pause menu entries for when the user selects "Change Difficulty".
*/
static final PAUSE_MENU_ENTRIES_DIFFICULTY:Array<PauseMenuEntry> = [
{text: 'Back', callback: switchMode.bind(_, Standard)}
// Other entries are added dynamically.
];
var menuItems:Array<String> = [];
var curSelected:Int = 0;
/**
* Pause menu entries for when the game is paused during a video cutscene.
*/
static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array<PauseMenuEntry> = [
{text: 'Resume', callback: resume},
{text: 'Restart Cutscene', callback: restartVideoCutscene},
{text: 'Skip Cutscene', callback: skipVideoCutscene},
{text: 'Exit to Menu', callback: quitToMenu},
];
var pauseMusic:FlxSound;
/**
* Pause menu entries for when the game is paused during a conversation.
*/
static final PAUSE_MENU_ENTRIES_CONVERSATION:Array<PauseMenuEntry> = [
{text: 'Resume', callback: resume},
{text: 'Restart Dialogue', callback: restartConversation},
{text: 'Skip Dialogue', callback: skipConversation},
{text: 'Exit to Menu', callback: quitToMenu},
];
var practiceText:FlxText;
/**
* Duration for the music to fade in when the pause menu is opened.
*/
static final MUSIC_FADE_IN_TIME:Float = 5;
public var exitingToMenu:Bool = false;
/**
* The final volume for the music when the pause menu is opened.
*/
static final MUSIC_FINAL_VOLUME:Float = 0.75;
var bg:FlxSprite;
var metaDataGrp:FlxTypedGroup<FlxSprite>;
/**
* Defines which pause music to use.
*/
public static var musicSuffix:String = '';
var isChartingMode:Bool;
/**
* Reset the pause configuration to the default.
*/
public static function reset():Void
{
musicSuffix = '';
}
public function new(isChartingMode:Bool = false)
// ===============
// Status Variables
// ===============
/**
* Disallow input until transitions are complete!
* This prevents the pause menu from immediately closing when opened, among other things.
*/
public var allowInput:Bool = false;
/**
* The entries currently displayed in the pause menu.
*/
var currentMenuEntries:Array<PauseMenuEntry>;
/**
* The index of `currentMenuEntries` that is currently selected.
*/
var currentEntry:Int = 0;
/**
* The mode that the pause menu is currently in.
*/
var currentMode:PauseMode;
// ===============
// Graphics Variables
// ===============
/**
* The semi-transparent black background that appears when the game is paused.
*/
var background:FunkinSprite;
/**
* The metadata displayed in the top right.
*/
var metadata:FlxTypedSpriteGroup<FlxText>;
/**
* A text object that displays the current practice mode status.
*/
var metadataPractice:FlxText;
/**
* A text object that displays the current death count.
*/
var metadataDeaths:FlxText;
/**
* The actual text objects for the menu entries.
*/
var menuEntryText:FlxTypedSpriteGroup<AtlasText>;
// ===============
// Audio Variables
// ===============
var pauseMusic:FunkinSound;
// ===============
// Constructor
// ===============
public function new(?params:PauseSubStateParams)
{
super();
this.isChartingMode = isChartingMode;
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation);
trace('DIFFICULTIES: ${difficultiesInVariation}');
pauseOptionsDifficulty = difficultiesInVariation.map(function(item:String):String {
return item.toUpperCase();
}).concat(pauseOptionsDifficultyBase);
if (PlayStatePlaylist.campaignId == 'week6')
{
pauseMusic = new FlxSound().loadEmbedded(Paths.music('breakfast-pixel'), true, true);
}
else
{
pauseMusic = new FlxSound().loadEmbedded(Paths.music('breakfast'), true, true);
}
pauseMusic.volume = 0;
pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2)));
FlxG.sound.list.add(pauseMusic);
bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
bg.alpha = 0;
bg.scrollFactor.set();
add(bg);
metaDataGrp = new FlxTypedGroup<FlxSprite>();
add(metaDataGrp);
var levelInfo:FlxText = new FlxText(20, 15, 0, '', 32);
if (PlayState.instance.currentChart != null)
{
levelInfo.text += '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}';
}
levelInfo.scrollFactor.set();
levelInfo.setFormat(Paths.font('vcr.ttf'), 32);
levelInfo.updateHitbox();
metaDataGrp.add(levelInfo);
var levelDifficulty:FlxText = new FlxText(20, 15 + 32, 0, '', 32);
levelDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase();
levelDifficulty.scrollFactor.set();
levelDifficulty.setFormat(Paths.font('vcr.ttf'), 32);
levelDifficulty.updateHitbox();
metaDataGrp.add(levelDifficulty);
var deathCounter:FlxText = new FlxText(20, 15 + 64, 0, '', 32);
deathCounter.text = 'Blue balled: ${PlayState.instance.deathCounter}';
FlxG.watch.addQuick('totalNotesHit', Highscore.tallies.totalNotesHit);
FlxG.watch.addQuick('totalNotes', Highscore.tallies.totalNotes);
deathCounter.scrollFactor.set();
deathCounter.setFormat(Paths.font('vcr.ttf'), 32);
deathCounter.updateHitbox();
metaDataGrp.add(deathCounter);
practiceText = new FlxText(20, 15 + 64 + 32, 0, 'PRACTICE MODE', 32);
practiceText.scrollFactor.set();
practiceText.setFormat(Paths.font('vcr.ttf'), 32);
practiceText.updateHitbox();
practiceText.x = FlxG.width - (practiceText.width + 20);
practiceText.visible = PlayState.instance.isPracticeMode;
metaDataGrp.add(practiceText);
levelDifficulty.alpha = 0;
levelInfo.alpha = 0;
deathCounter.alpha = 0;
levelInfo.x = FlxG.width - (levelInfo.width + 20);
levelDifficulty.x = FlxG.width - (levelDifficulty.width + 20);
deathCounter.x = FlxG.width - (deathCounter.width + 20);
FlxTween.tween(bg, {alpha: 0.6}, 0.4, {ease: FlxEase.quartInOut});
FlxTween.tween(levelInfo, {alpha: 1, y: 20}, 0.4, {ease: FlxEase.quartInOut, startDelay: 0.3});
FlxTween.tween(levelDifficulty, {alpha: 1, y: levelDifficulty.y + 5}, 0.4, {ease: FlxEase.quartInOut, startDelay: 0.5});
FlxTween.tween(deathCounter, {alpha: 1, y: deathCounter.y + 5}, 0.4, {ease: FlxEase.quartInOut, startDelay: 0.7});
grpMenuShit = new FlxTypedGroup<Alphabet>();
add(grpMenuShit);
regenMenu();
// cameras = [FlxG.cameras.list[FlxG.cameras.list.length - 1]];
this.currentMode = params?.mode ?? Standard;
}
function regenMenu():Void
// ===============
// Lifecycle Functions
// ===============
/**
* Called when the state is first loaded.
*/
public override function create():Void
{
while (grpMenuShit.members.length > 0)
{
grpMenuShit.remove(grpMenuShit.members[0], true);
}
super.create();
for (i in 0...menuItems.length)
{
var songText:Alphabet = new Alphabet(0, (70 * i) + 30, menuItems[i], true, false);
songText.isMenuItem = true;
songText.targetY = i;
grpMenuShit.add(songText);
}
startPauseMusic();
curSelected = 0;
changeSelection();
buildBackground();
buildMetadata();
regenerateMenu();
transitionIn();
}
override function update(elapsed:Float):Void
/**
* Called every frame.
* @param elapsed The time elapsed since the last frame, in seconds.
*/
public override function update(elapsed:Float):Void
{
if (pauseMusic.volume < 0.5) pauseMusic.volume += 0.01 * elapsed;
super.update(elapsed);
handleInputs();
}
/**
* Called when the state is closed.
*/
public override function destroy():Void
{
super.destroy();
pauseMusic.stop();
}
// ===============
// Initialization Functions
// ===============
/**
* Play the pause music.
*/
function startPauseMusic():Void
{
var pauseMusicPath:String = Paths.music('breakfast$musicSuffix');
pauseMusic = FunkinSound.load(pauseMusicPath, true, true);
if (pauseMusic == null)
{
FlxG.log.warn('Could not play pause music: ${pauseMusicPath} does not exist!');
}
// Start playing at a random point in the song.
pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2)));
pauseMusic.fadeIn(MUSIC_FADE_IN_TIME, 0, MUSIC_FINAL_VOLUME);
}
/**
* Render the semi-transparent black background.
*/
function buildBackground():Void
{
// Using state.bgColor causes bugs!
background = new FunkinSprite(0, 0);
background.makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
background.alpha = 0.0;
background.scrollFactor.set(0, 0);
background.updateHitbox();
add(background);
}
/**
* Render the metadata in the top right.
*/
function buildMetadata():Void
{
metadata = new FlxTypedSpriteGroup<FlxText>();
metadata.scrollFactor.set(0, 0);
add(metadata);
var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name - Artist');
metadataSong.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
if (PlayState.instance?.currentChart != null)
{
metadataSong.text = '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}';
}
metadataSong.scrollFactor.set(0, 0);
metadata.add(metadataSong);
var metadataDifficulty:FlxText = new FlxText(20, 15 + 32, FlxG.width - 40, 'Difficulty: ');
metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
if (PlayState.instance?.currentDifficulty != null)
{
metadataDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase();
}
metadataDifficulty.scrollFactor.set(0, 0);
metadata.add(metadataDifficulty);
metadataDeaths = new FlxText(20, 15 + 64, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls');
metadataDeaths.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
metadataDeaths.scrollFactor.set(0, 0);
metadata.add(metadataDeaths);
metadataPractice = new FlxText(20, 15 + 96, FlxG.width - 40, 'PRACTICE MODE');
metadataPractice.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false;
metadataPractice.scrollFactor.set(0, 0);
metadata.add(metadataPractice);
updateMetadataText();
}
/**
* Perform additional animations to transition the pause menu in when it is first displayed.
*/
function transitionIn():Void
{
FlxTween.tween(background, {alpha: 0.6}, 0.8, {ease: FlxEase.quartOut});
// Animate each element a little bit downwards.
var delay:Float = 0.1;
for (child in metadata.members)
{
FlxTween.tween(child, {alpha: 1, y: child.y + 5}, 1.8, {ease: FlxEase.quartOut, startDelay: delay});
delay += 0.1;
}
new FlxTimer().start(0.2, (_) -> {
allowInput = true;
});
}
// ===============
// Input Handling
// ===============
/**
* Process user inputs every frame.
*/
function handleInputs():Void
{
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
if (!allowInput) return;
if (controls.UI_UP_P)
{
changeSelection(-1);
}
if (controls.UI_DOWN_P)
{
changeSelection(1);
}
if (controls.ACCEPT)
{
currentMenuEntries[currentEntry].callback(this);
}
else if (controls.PAUSE)
{
resume(this);
}
#if (debug || FORCE_DEBUG_VERSION)
// to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H)
{
bg.visible = !bg.visible;
grpMenuShit.visible = !grpMenuShit.visible;
metaDataGrp.visible = !metaDataGrp.visible;
var visible = !metadata.visible;
metadata.visible = visible;
menuEntryText.visible = visible;
this.bgColor = visible ? 0x99000000 : 0x00000000; // 60% or fully transparent black
}
#end
if (!exitingToMenu)
{
if (upP)
{
changeSelection(-1);
}
if (downP)
{
changeSelection(1);
}
var androidPause:Bool = false;
#if android
androidPause = FlxG.android.justPressed.BACK;
#end
if (androidPause) close();
if (accepted)
{
var daSelected:String = menuItems[curSelected];
switch (daSelected)
{
case 'Resume':
close();
case 'Change Difficulty':
menuItems = pauseOptionsDifficulty;
regenMenu();
case 'Toggle Practice Mode':
PlayState.instance.isPracticeMode = true;
practiceText.visible = PlayState.instance.isPracticeMode;
case 'Restart Song':
PlayState.instance.needsReset = true;
close();
case 'Exit to Menu':
exitingToMenu = true;
PlayState.instance.deathCounter = 0;
for (item in grpMenuShit.members)
{
item.targetY = -3;
item.alpha = 0.6;
}
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
if (PlayStatePlaylist.isStoryMode)
{
PlayStatePlaylist.reset();
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.story.StoryMenuState(sticker)));
}
else
{
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker)));
}
case 'Adjust Input Offsets':
openSubState(new LatencyState());
case 'Exit to Chart Editor':
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!
case 'BACK':
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
regenMenu();
default:
if (pauseOptionsDifficulty.contains(daSelected))
{
PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
// Reset campaign score when changing difficulty
// So if you switch difficulty on the last song of a week you get a really low overall score.
PlayStatePlaylist.campaignScore = 0;
PlayStatePlaylist.campaignDifficulty = daSelected.toLowerCase();
PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
PlayState.instance.needsReset = true;
close();
}
else
{
trace('[WARN] Unhandled pause menu option: ${daSelected}');
}
}
}
if (FlxG.keys.justPressed.J)
{
// for reference later!
// PlayerSettings.player1.controls.replaceBinding(Control.LEFT, Keys, FlxKey.J, null);
}
}
}
override function destroy():Void
{
pauseMusic.destroy();
super.destroy();
}
/**
* Move the current selection up or down.
* @param change The amount to change the selection by, with sign indicating direction.
*/
function changeSelection(change:Int = 0):Void
{
FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
curSelected += change;
currentEntry += change;
if (curSelected < 0) curSelected = menuItems.length - 1;
if (curSelected >= menuItems.length) curSelected = 0;
if (currentEntry < 0) currentEntry = currentMenuEntries.length - 1;
if (currentEntry >= currentMenuEntries.length) currentEntry = 0;
for (index => item in grpMenuShit.members)
for (entryIndex in 0...currentMenuEntries.length)
{
item.targetY = index - curSelected;
var isCurrent:Bool = entryIndex == currentEntry;
item.alpha = 0.6;
var entry:PauseMenuEntry = currentMenuEntries[entryIndex];
var text:AtlasText = entry.sprite;
if (item.targetY == 0)
{
item.alpha = 1;
}
// Set the transparency.
text.alpha = isCurrent ? 1.0 : 0.6;
// Set the position.
var targetX = FlxMath.remapToRange((entryIndex - currentEntry), 0, 1, 0, 1.3) * 20 + 90;
var targetY = FlxMath.remapToRange((entryIndex - currentEntry), 0, 1, 0, 1.3) * 120 + (FlxG.height * 0.48);
FlxTween.globalManager.cancelTweensOf(text);
FlxTween.tween(text, {x: targetX, y: targetY}, 0.33, {ease: FlxEase.quartOut});
}
}
// ===============
// Menu Functions
// ===============
/**
* Clear the current menu entries and regenerate them based on the current mode.
* @param targetMode Optionally specify a mode to switch to before regenerating the menu.
*/
function regenerateMenu(?targetMode:PauseMode):Void
{
// If targetMode is null, keep the current mode.
if (targetMode == null) targetMode = this.currentMode;
var previousMode:PauseMode = this.currentMode;
this.currentMode = targetMode;
resetSelection();
chooseMenuEntries();
clearAndAddMenuEntries();
updateMetadataText();
changeSelection();
}
/**
* Reset the current selection to the first entry.
*/
function resetSelection():Void
{
this.currentEntry = 0;
}
/**
* Select which menu entries to display based on the current mode.
*/
function chooseMenuEntries():Void
{
// Choose the correct menu entries.
// NOTE: We clone the arrays to prevent modifications to the arrays from affecting the original.
switch (this.currentMode)
{
case PauseMode.Standard:
currentMenuEntries = PAUSE_MENU_ENTRIES_STANDARD.clone();
case PauseMode.Charting:
currentMenuEntries = PAUSE_MENU_ENTRIES_CHARTING.clone();
case PauseMode.Difficulty:
// Prepend the difficulties.
var entries:Array<PauseMenuEntry> = [];
if (PlayState.instance.currentChart != null)
{
var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation);
trace('DIFFICULTIES: ${difficultiesInVariation}');
for (difficulty in difficultiesInVariation)
{
entries.push({text: difficulty.toTitleCase(), callback: (state) -> changeDifficulty(state, difficulty)});
}
}
// Add the back button.
currentMenuEntries = entries.concat(PAUSE_MENU_ENTRIES_DIFFICULTY.clone());
case PauseMode.Conversation:
currentMenuEntries = PAUSE_MENU_ENTRIES_CONVERSATION.clone();
case PauseMode.Cutscene:
currentMenuEntries = PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE.clone();
}
}
/**
* Clear the `menuEntryText` group and render the current menu entries to it.
* We first create the `menuEntryText` group if it doesn't already exist.
*/
function clearAndAddMenuEntries():Void
{
if (menuEntryText == null)
{
menuEntryText = new FlxTypedSpriteGroup<AtlasText>();
menuEntryText.scrollFactor.set(0, 0);
add(menuEntryText);
}
menuEntryText.clear();
// Render out the entries depending on the mode.
var entryIndex:Int = 0;
var toRemove = [];
for (entry in currentMenuEntries)
{
if (entry == null || (entry.filter != null && !entry.filter()))
{
// Remove entries that should be hidden.
toRemove.push(entry);
}
else
{
// Handle visible entries.
var yPos:Float = 70 * entryIndex + 30;
var text:AtlasText = new AtlasText(0, yPos, entry.text, AtlasFont.BOLD);
text.scrollFactor.set(0, 0);
text.alpha = 0;
menuEntryText.add(text);
entry.sprite = text;
entryIndex++;
}
}
for (entry in toRemove)
{
currentMenuEntries.remove(entry);
}
}
// ===============
// Metadata Functions
// ===============
/**
* Update the values for the metadata text in the top right.
*/
function updateMetadataText():Void
{
metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false;
switch (this.currentMode)
{
case Standard | Difficulty:
metadataDeaths.text = '${PlayState.instance?.deathCounter} Blue Balls';
case Charting:
metadataDeaths.text = 'Chart Editor Preview';
case Conversation:
metadataDeaths.text = 'Dialogue Paused';
case Cutscene:
metadataDeaths.text = 'Video Paused';
}
}
// ===============
// Menu Callbacks
// ===============
/**
* Close the pause menu and resume the game.
* @param state The current PauseSubState.
*/
static function resume(state:PauseSubState):Void
{
// Resume a paused video if it exists.
VideoCutscene.resumeVideo();
state.close();
}
/**
* Switch the pause menu to the indicated mode.
* Create a callback from this using `.bind(_, targetMode)`.
* @param state The current PauseSubState.
* @param targetMode The mode to switch to.
*/
static function switchMode(state:PauseSubState, targetMode:PauseMode):Void
{
state.regenerateMenu(targetMode);
}
/**
* Switch the game's difficulty to the indicated difficulty, then resume the game.
* @param state The current PauseSubState.
* @param difficulty The difficulty to switch to.
*/
static function changeDifficulty(state:PauseSubState, difficulty:String):Void
{
PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
// Reset campaign score when changing difficulty
// So if you switch difficulty on the last song of a week you get a really low overall score.
PlayStatePlaylist.campaignScore = 0;
PlayStatePlaylist.campaignDifficulty = difficulty;
PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
PlayState.instance.needsReset = true;
state.close();
}
/**
* Restart the current level, then resume the game.
* @param state The current PauseSubState.
*/
static function restartPlayState(state:PauseSubState):Void
{
PlayState.instance.needsReset = true;
state.close();
}
/**
* Force the game into practice mode, then update the pause menu.
* @param state The current PauseSubState.
*/
static function enablePracticeMode(state:PauseSubState):Void
{
if (PlayState.instance == null) return;
PlayState.instance.isPracticeMode = true;
state.regenerateMenu();
}
/**
* Restart the paused video cutscene, then resume the game.
* @param state The current PauseSubState.
*/
static function restartVideoCutscene(state:PauseSubState):Void
{
VideoCutscene.restartVideo();
state.close();
}
/**
* Skip the paused video cutscene, then resume the game.
* @param state The current PauseSubState.
*/
static function skipVideoCutscene(state:PauseSubState):Void
{
VideoCutscene.finishVideo();
state.close();
}
/**
* Restart the paused conversation, then resume the game.
* @param state The current PauseSubState.
*/
static function restartConversation(state:PauseSubState):Void
{
if (PlayState.instance?.currentConversation == null) return;
PlayState.instance.currentConversation.resetConversation();
state.close();
}
/**
* Skip the paused conversation, then resume the game.
* @param state The current PauseSubState.
*/
static function skipConversation(state:PauseSubState):Void
{
if (PlayState.instance?.currentConversation == null) return;
PlayState.instance.currentConversation.skipConversation();
state.close();
}
/**
* Quit the game and return to the main menu.
* @param state The current PauseSubState.
*/
static function quitToMenu(state:PauseSubState):Void
{
state.allowInput = false;
PlayState.instance.deathCounter = 0;
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
if (PlayStatePlaylist.isStoryMode)
{
PlayStatePlaylist.reset();
state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.story.StoryMenuState(sticker)));
}
else
{
state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker)));
}
}
/**
* Quit the game and return to the chart editor.
* @param state The current PauseSubState.
*/
static function quitToChartEditor(state:PauseSubState):Void
{
state.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!
}
}
/**
* Which set of options the pause menu should display.
*/
enum PauseMode
{
/**
* The menu displayed when the player pauses the game during a song.
*/
Standard;
/**
* The menu displayed when the player pauses the game during a song while in charting mode.
*/
Charting;
/**
* The menu displayed when the player moves to change the game's difficulty.
*/
Difficulty;
/**
* The menu displayed when the player pauses the game during a conversation.
*/
Conversation;
/**
* The menu displayed when the player pauses the game during a video cutscene.
*/
Cutscene;
}
/**
* Represents a single entry in the pause menu.
*/
typedef PauseMenuEntry =
{
/**
* The text to display for this entry.
* TODO: Implement localization.
*/
var text:String;
/**
* The callback to execute when the user selects this entry.
*/
var callback:PauseSubState->Void;
/**
* If this returns true, the entry will be displayed. If it returns false, the entry will be hidden.
*/
var ?filter:Void->Bool;
// Instance-specific properties
/**
* The text object currently displaying this entry.
*/
var ?sprite:AtlasText;
};
File diff suppressed because it is too large Load Diff
+28 -27
View File
@@ -2,26 +2,25 @@ package funkin.play;
import funkin.ui.story.StoryMenuState;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxBasic;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxAtlasFrames;
import funkin.graphics.FunkinSprite;
import flixel.graphics.frames.FlxBitmapFont;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxPoint;
import funkin.ui.MusicBeatSubState;
import flixel.math.FlxRect;
import flixel.text.FlxBitmapText;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import funkin.ui.freeplay.FreeplayState;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxGradient;
import flixel.util.FlxTimer;
import funkin.graphics.shaders.LeftMaskShader;
import funkin.play.components.TallyCounter;
import flxanimate.FlxAnimate.Settings;
/**
* The state for the results screen after a song or week is finished.
*/
class ResultState extends MusicBeatSubState
{
final params:ResultsStateParams;
@@ -30,8 +29,8 @@ class ResultState extends MusicBeatSubState
var songName:FlxBitmapText;
var difficulty:FlxSprite;
var maskShaderSongName = new LeftMaskShader();
var maskShaderDifficulty = new LeftMaskShader();
var maskShaderSongName:LeftMaskShader = new LeftMaskShader();
var maskShaderDifficulty:LeftMaskShader = new LeftMaskShader();
public function new(params:ResultsStateParams)
{
@@ -49,22 +48,22 @@ class ResultState extends MusicBeatSubState
else
resultsVariation = NORMAL;
var loops = resultsVariation != SHIT;
var loops:Bool = resultsVariation != SHIT;
FlxG.sound.playMusic(Paths.music("results" + resultsVariation), 1, loops);
// TEMP-ish, just used to sorta "cache" the 3000x3000 image!
var cacheBullShit = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem"));
var cacheBullShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem"));
add(cacheBullShit);
var dumb = new FlxSprite().loadGraphic(Paths.image("resultScreen/scorePopin"));
var dumb:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/scorePopin"));
add(dumb);
var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90);
bg.scrollFactor.set();
add(bg);
var bgFlash:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFffeb69, 0xFFffe66a], 90);
var bgFlash:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90);
bgFlash.scrollFactor.set();
bgFlash.visible = false;
add(bgFlash);
@@ -96,8 +95,7 @@ class ResultState extends MusicBeatSubState
bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
var gf:FlxSprite = new FlxSprite(500, 300);
gf.frames = Paths.getSparrowAtlas('resultScreen/resultGirlfriendGOOD');
var gf:FlxSprite = FunkinSprite.createSparrow(500, 300, 'resultScreen/resultGirlfriendGOOD');
gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
gf.visible = false;
gf.animation.finishCallback = _ -> {
@@ -105,8 +103,7 @@ class ResultState extends MusicBeatSubState
};
add(gf);
var boyfriend:FlxSprite = new FlxSprite(640, -200);
boyfriend.frames = Paths.getSparrowAtlas('resultScreen/resultBoyfriendGOOD');
var boyfriend:FlxSprite = FunkinSprite.createSparrow(640, -200, 'resultScreen/resultBoyfriendGOOD');
boyfriend.animation.addByPrefix("fall", "Boyfriend Good", 24, false);
boyfriend.visible = false;
boyfriend.animation.finishCallback = function(_) {
@@ -115,8 +112,7 @@ class ResultState extends MusicBeatSubState
add(boyfriend);
var soundSystem:FlxSprite = new FlxSprite(-15, -180);
soundSystem.frames = Paths.getSparrowAtlas("resultScreen/soundSystem");
var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
soundSystem.visible = false;
new FlxTimer().start(0.4, _ -> {
@@ -162,20 +158,17 @@ class ResultState extends MusicBeatSubState
FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut, startDelay: 0.5});
add(blackTopBar);
var resultsAnim:FlxSprite = new FlxSprite(-200, -10);
resultsAnim.frames = Paths.getSparrowAtlas("resultScreen/results");
var resultsAnim:FunkinSprite = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
resultsAnim.animation.addByPrefix("result", "results", 24, false);
resultsAnim.animation.play("result");
add(resultsAnim);
var ratingsPopin:FlxSprite = new FlxSprite(-150, 120);
ratingsPopin.frames = Paths.getSparrowAtlas("resultScreen/ratingsPopin");
var ratingsPopin:FunkinSprite = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false);
ratingsPopin.visible = false;
add(ratingsPopin);
var scorePopin:FlxSprite = new FlxSprite(-180, 520);
scorePopin.frames = Paths.getSparrowAtlas("resultScreen/scorePopin");
var scorePopin:FunkinSprite = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
scorePopin.animation.addByPrefix("score", "tally score", 24, false);
scorePopin.visible = false;
add(scorePopin);
@@ -193,6 +186,10 @@ class ResultState extends MusicBeatSubState
var ratingGrp:FlxTypedGroup<TallyCounter> = new FlxTypedGroup<TallyCounter>();
add(ratingGrp);
/**
* NOTE: We display how many notes were HIT, not how many notes there were in total.
*
*/
var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit);
ratingGrp.add(totalHit);
@@ -207,7 +204,7 @@ class ResultState extends MusicBeatSubState
var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5);
ratingGrp.add(tallyGood);
var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xffE6CF8A);
var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xFFE6CF8A);
ratingGrp.add(tallyBad);
var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A);
@@ -279,13 +276,13 @@ class ResultState extends MusicBeatSubState
super.create();
}
function timerThenSongName()
function timerThenSongName():Void
{
movingSongStuff = false;
difficulty.x = 555;
var diffYTween = 122;
var diffYTween:Float = 122;
difficulty.y = -difficulty.height;
FlxTween.tween(difficulty, {y: diffYTween}, 0.5, {ease: FlxEase.quartOut, startDelay: 0.8});
@@ -315,7 +312,7 @@ class ResultState extends MusicBeatSubState
// maskShaderSongName.frameUV = songName.frame.uv;
}
override function update(elapsed:Float)
override function update(elapsed:Float):Void
{
// maskShaderSongName.swagSprX = songName.x;
maskShaderDifficulty.swagSprX = difficulty.x;
@@ -350,6 +347,10 @@ class ResultState extends MusicBeatSubState
if (controls.PAUSE)
{
FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8);
FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1, {onComplete: _ -> {
FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4);
}});
if (params.storyMode)
{
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
@@ -46,7 +46,8 @@ class AnimateAtlasCharacter extends BaseCharacter
var _skipTransformChildren:Bool = false;
var animations:Map<String, AnimateAtlasAnimation> = new Map<String, AnimateAtlasAnimation>();
var currentAnimation:String;
var currentAnimName:Null<String> = null;
var animFinished:Bool = false;
public function new(id:String)
{
@@ -77,6 +78,7 @@ class AnimateAtlasCharacter extends BaseCharacter
var atlasSprite:FlxAtlasSprite = loadAtlasSprite();
setSprite(atlasSprite);
loadAnimations();
super.onCreate(event);
@@ -86,10 +88,36 @@ class AnimateAtlasCharacter extends BaseCharacter
{
if ((!canPlayOtherAnims && !ignoreOther)) return;
currentAnimation = name;
var prefix:String = getAnimationData(name).prefix;
if (prefix == null) prefix = name;
this.mainSprite.playAnimation(prefix, restart, ignoreOther);
var correctName = correctAnimationName(name);
if (correctName == null)
{
trace('Could not find Atlas animation: ' + name);
return;
}
var animData = getAnimationData(correctName);
currentAnimName = correctName;
var prefix:String = animData.prefix;
if (prefix == null) prefix = correctName;
var loop:Bool = animData.looped;
this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop);
animFinished = false;
}
public override function hasAnimation(name:String):Bool
{
return getAnimationData(name) != null;
}
/**
* Returns true if the animation has finished playing.
* Never true if animation is configured to loop.
*/
public override function isAnimationFinished():Bool
{
return animFinished;
}
function loadAtlasSprite():FlxAtlasSprite
@@ -114,7 +142,13 @@ class AnimateAtlasCharacter extends BaseCharacter
}
else
{
// Make the game hold on the last frame.
this.mainSprite.cleanupAnimation(prefix);
// currentAnimName = null;
animFinished = true;
// Fallback to idle!
// playAnimation('idle', true, false);
}
}
@@ -140,19 +174,30 @@ class AnimateAtlasCharacter extends BaseCharacter
function loadAnimations():Void
{
trace('[ATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}');
trace('[ATLASCHAR] Attempting to load ${_data.animations.length} animations for ${characterId}');
var animData:Array<AnimateAtlasAnimation> = cast _data.animations;
for (anim in animData)
{
// Validate the animation before adding.
var prefix = anim.prefix;
if (!this.mainSprite.hasAnimation(prefix))
{
FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}');
continue;
}
animations.set(anim.name, anim);
trace('[ATLASCHAR] - Successfully loaded animation ${anim.name} to ${characterId}');
}
trace('[ATLASCHAR] Loaded ${animations.size()} animations for ${characterId}');
}
public override function getCurrentAnimation():String
{
return this.mainSprite.getCurrentAnimation();
// return this.mainSprite.getCurrentAnimation();
return currentAnimName;
}
function getAnimationData(name:String = null):AnimateAtlasAnimation
@@ -191,7 +236,11 @@ class AnimateAtlasCharacter extends BaseCharacter
_skipTransformChildren = true;
super.kill();
_skipTransformChildren = false;
this.mainSprite.kill();
if (this.mainSprite != null)
{
this.mainSprite.kill();
this.mainSprite = null;
}
}
/**
+23 -8
View File
@@ -60,7 +60,7 @@ class BaseCharacter extends Bopper
@:allow(funkin.ui.debug.anim.DebugBoundingState)
final _data:CharacterData;
final singTimeSec:Float;
final singTimeSteps:Float;
/**
* The offset between the corner of the sprite and the origin of the sprite (at the character's feet).
@@ -180,7 +180,7 @@ class BaseCharacter extends Bopper
{
this.characterName = _data.name;
this.name = _data.name;
this.singTimeSec = _data.singTime;
this.singTimeSteps = _data.singTime;
this.globalOffsets = _data.offsets;
this.flipX = _data.flipX;
}
@@ -193,6 +193,21 @@ class BaseCharacter extends Bopper
return _data.death?.cameraOffsets ?? [0.0, 0.0];
}
public function getBaseScale():Float
{
return _data.scale;
}
public function getDeathCameraZoom():Float
{
return _data.death?.cameraZoom ?? 1.0;
}
public function getDeathPreTransitionDelay():Float
{
return _data.death?.preTransitionDelay ?? 0.0;
}
/**
* Gets the value of flipX from the character data.
* `!getFlipX()` is the direction Boyfriend should face.
@@ -250,8 +265,8 @@ class BaseCharacter extends Bopper
}
/**
* Set the sprite scale to the appropriate value.
* @param scale
* Set the character's sprite scale to the appropriate value.
* @param scale The desired scale.
*/
public function setScale(scale:Null<Float>):Void
{
@@ -367,9 +382,9 @@ 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.instance.beatLengthMs * 0.001); // x beats, to ms.
var singTimeSec:Float = singTimeSteps * (Conductor.instance.stepLengthMs / Constants.MS_PER_SEC); // x beats, to ms.
if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss
if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss???
// Without this check here, the player character would only play the `sing` animation
// for one beat, as opposed to holding it as long as the player is holding the button.
@@ -378,7 +393,7 @@ class BaseCharacter extends Bopper
FlxG.watch.addQuick('singTimeSec-${characterId}', singTimeSec);
if (holdTimer > singTimeSec && shouldStopSinging)
{
// trace('holdTimer reached ${holdTimer}sec (> ${singTimeSec}), stopping sing animation');
trace('holdTimer reached ${holdTimer}sec (> ${singTimeSec}), stopping sing animation');
holdTimer = 0;
dance(true);
}
@@ -475,7 +490,7 @@ class BaseCharacter extends Bopper
* Every time a note is hit, check if the note is from the same strumline.
* If it is, then play the sing animation.
*/
public override function onNoteHit(event:NoteScriptEvent)
public override function onNoteHit(event:HitNoteScriptEvent)
{
super.onNoteHit(event);
+24 -5
View File
@@ -305,9 +305,15 @@ class CharacterDataParser
icon = "darnell";
case "senpai-angry":
icon = "senpai";
case "tankman" | "tankman-atlas":
icon = "tankmen";
}
return Paths.image("freeplay/icons/" + icon + "pixel");
var path = Paths.image("freeplay/icons/" + icon + "pixel");
if (Assets.exists(path)) return path;
// TODO: Hardcode some additional behavior or a fallback.
return null;
}
/**
@@ -372,12 +378,12 @@ class CharacterDataParser
}
/**
* The default time the character should sing for, in beats.
* The default time the character should sing for, in steps.
* Values that are too low will cause the character to stop singing between notes.
* Originally, this value was set to 1, but it was changed to 2 because that became
* too low after some other code changes.
* Values that are too high will cause the character to hold their singing pose for too long after they're done.
* @default `8 steps`
*/
static final DEFAULT_SINGTIME:Float = 2.0;
static final DEFAULT_SINGTIME:Float = 8.0;
static final DEFAULT_DANCEEVERY:Int = 1;
static final DEFAULT_FLIPX:Bool = false;
@@ -738,4 +744,17 @@ typedef DeathData =
* @default [0, 0]
*/
var ?cameraOffsets:Array<Float>;
/**
* The amount to zoom the camera by while focusing on this character as they die.
* Value is a multiplier of the default camera zoom for the stage.
* @default 1.0
*/
var ?cameraZoom:Float;
/**
* Impose a delay between when the character reaches `0` health and when the death animation plays.
* @default 0.0
*/
var ?preTransitionDelay:Float;
}
+3 -2
View File
@@ -6,6 +6,7 @@ import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import funkin.play.character.CharacterData.CharacterDataParser;
import openfl.utils.Assets;
import funkin.graphics.FunkinSprite;
import funkin.util.MathUtil;
/**
@@ -26,7 +27,7 @@ import funkin.util.MathUtil;
* @author MasterEric
*/
@:nullSafety
class HealthIcon extends FlxSprite
class HealthIcon extends FunkinSprite
{
/**
* The character this icon is representing.
@@ -408,7 +409,7 @@ class HealthIcon extends FlxSprite
if (!isLegacyStyle)
{
frames = Paths.getSparrowAtlas('icons/icon-$charId');
loadSparrow('icons/icon-$charId');
loadAnimationNew();
}
+19 -23
View File
@@ -3,7 +3,10 @@ package funkin.play.components;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.tweens.FlxTween;
import flixel.util.FlxDirection;
import funkin.graphics.FunkinSprite;
import funkin.play.PlayState;
import funkin.util.TimerUtil;
class PopUpStuff extends FlxTypedGroup<FlxSprite>
{
@@ -14,26 +17,20 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
public function displayRating(daRating:String)
{
var perfStart:Float = TimerUtil.start();
if (daRating == null) daRating = "good";
var rating:FlxSprite = new FlxSprite(0, 0);
rating.scrollFactor.set(0.2, 0.2);
rating.zIndex = 1000;
var ratingPath:String = daRating;
if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
rating.loadGraphic(Paths.image(ratingPath));
rating.x = FlxG.width * 0.50;
rating.x -= FlxG.camera.scroll.x * 0.2;
// make sure rating is visible lol!
// if (rating.x < FlxG.camera.scroll.x)
// rating.x = FlxG.camera.scroll.x;
// else if (rating.x > FlxG.camera.scroll.x + FlxG.camera.width - rating.width)
// rating.x = FlxG.camera.scroll.x + FlxG.camera.width - rating.width;
var rating:FunkinSprite = FunkinSprite.create(0, 0, ratingPath);
rating.scrollFactor.set(0.2, 0.2);
// FlxG.camera.scroll.y +
rating.zIndex = 1000;
rating.x = FlxG.width * 0.50;
// rating.x -= FlxG.camera.scroll.x * 0.2;
rating.y = FlxG.camera.height * 0.4 - 60;
rating.acceleration.y = 550;
rating.velocity.y -= FlxG.random.int(140, 175);
@@ -61,10 +58,14 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
},
startDelay: Conductor.instance.beatLengthMs * 0.001
});
trace('displayRating took: ${TimerUtil.seconds(perfStart)}');
}
public function displayCombo(?combo:Int = 0):Int
{
var perfStart:Float = TimerUtil.start();
if (combo == null) combo = 0;
var pixelShitPart1:String = "";
@@ -75,16 +76,10 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
pixelShitPart1 = 'weeb/pixelUI/';
pixelShitPart2 = '-pixel';
}
var comboSpr:FlxSprite = new FlxSprite().loadGraphic(Paths.image(pixelShitPart1 + 'combo' + pixelShitPart2));
var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2);
comboSpr.y = FlxG.camera.height * 0.4 + 80;
comboSpr.x = FlxG.width * 0.50;
comboSpr.x -= FlxG.camera.scroll.x * 0.2;
// make sure combo is visible lol!
// 194 fits 4 combo digits
// if (comboSpr.x < FlxG.camera.scroll.x + 194)
// comboSpr.x = FlxG.camera.scroll.x + 194;
// else if (comboSpr.x > FlxG.camera.scroll.x + FlxG.camera.width - comboSpr.width)
// comboSpr.x = FlxG.camera.scroll.x + FlxG.camera.width - comboSpr.width;
// comboSpr.x -= FlxG.camera.scroll.x * 0.2;
comboSpr.acceleration.y = 600;
comboSpr.velocity.y -= 150;
@@ -129,8 +124,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
var daLoop:Int = 1;
for (i in seperatedScore)
{
var numScore:FlxSprite = new FlxSprite().loadGraphic(Paths.image(pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2));
numScore.y = comboSpr.y;
var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2);
if (PlayState.instance.currentStageId.startsWith('school'))
{
@@ -163,6 +157,8 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
daLoop++;
}
trace('displayCombo took: ${TimerUtil.seconds(perfStart)}');
return combo;
}
}
+118 -11
View File
@@ -19,13 +19,22 @@ import hxcodec.flixel.FlxVideoSprite;
class VideoCutscene
{
static var blackScreen:FlxSprite;
static var cutsceneType:CutsceneType;
#if html5
static var vid:FlxVideo;
#end
#if hxCodec
static var vid:FlxVideoSprite;
#end
/**
* Play a video cutscene.
* TODO: Currently this is hardcoded to start the countdown after the video is done.
* @param path The path to the video file. Use Paths.file(path) to get the correct path.
* @param cutseneType The type of cutscene to play, determines what the game does after. Defaults to `CutsceneType.STARTING`.
*/
public static function play(filePath:String):Void
public static function play(filePath:String, ?cutsceneType:CutsceneType = STARTING):Void
{
if (PlayState.instance == null) return;
@@ -36,6 +45,8 @@ class VideoCutscene
return;
}
var rawFilePath = Paths.stripLibrary(filePath);
// Trigger the cutscene. Don't play the song in the background.
PlayState.instance.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
@@ -47,12 +58,14 @@ class VideoCutscene
blackScreen.cameras = [PlayState.instance.camCutscene];
PlayState.instance.add(blackScreen);
#if html5
playVideoHTML5(filePath);
#end
VideoCutscene.cutsceneType = cutsceneType;
#if hxCodec
playVideoNative(filePath);
#if html5
playVideoHTML5(rawFilePath);
#elseif hxCodec
playVideoNative(rawFilePath);
#else
throw "No video support for this platform!";
#end
}
@@ -66,8 +79,6 @@ class VideoCutscene
}
#if html5
static var vid:FlxVideo;
static function playVideoHTML5(filePath:String):Void
{
// Video displays OVER the FlxState.
@@ -92,8 +103,6 @@ class VideoCutscene
#end
#if hxCodec
static var vid:FlxVideoSprite;
static function playVideoNative(filePath:String):Void
{
// Video displays OVER the FlxState.
@@ -103,6 +112,7 @@ class VideoCutscene
{
vid.zIndex = 0;
vid.bitmap.onEndReached.add(finishVideo.bind(0.5));
vid.autoPause = false;
vid.cameras = [PlayState.instance.camCutscene];
@@ -110,6 +120,15 @@ class VideoCutscene
PlayState.instance.refresh();
vid.play(filePath, false);
// Resize videos bigger or smaller than the screen.
vid.bitmap.onTextureSetup.add(() -> {
vid.setGraphicSize(FlxG.width, FlxG.height);
vid.updateHitbox();
vid.x = 0;
vid.y = 0;
// vid.scale.set(0.5, 0.5);
});
}
else
{
@@ -118,10 +137,74 @@ class VideoCutscene
}
#end
public static function restartVideo(resume:Bool = true):Void
{
#if html5
if (vid != null)
{
vid.restartVideo();
}
#end
#if hxCodec
if (vid != null)
{
// Seek to the start of the video.
vid.bitmap.time = 0;
if (resume)
{
// Resume the video if it was paused.
vid.resume();
}
}
#end
}
public static function pauseVideo():Void
{
#if html5
if (vid != null)
{
vid.pauseVideo();
}
#end
#if hxCodec
if (vid != null)
{
vid.pause();
}
#end
}
public static function resumeVideo():Void
{
#if html5
if (vid != null)
{
vid.resumeVideo();
}
#end
#if hxCodec
if (vid != null)
{
vid.resume();
}
#end
}
/**
* Finish the active video cutscene. Done when the video is finished or when the player skips the cutscene.
* @param transitionTime The duration of the transition to the next state. Defaults to 0.5 seconds (this time is always used when cancelling the video).
* @param finishCutscene The callback to call when the transition is finished.
*/
public static function finishVideo(?transitionTime:Float = 0.5):Void
{
trace('ALERT: Finish video cutscene called!');
var cutsceneType:CutsceneType = VideoCutscene.cutsceneType;
#if html5
if (vid != null)
{
@@ -157,8 +240,32 @@ class VideoCutscene
{
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween) {
PlayState.instance.startCountdown();
onCutsceneFinish(cutsceneType);
}
});
}
/**
* The default callback used when a cutscene is finished.
* You can specify your own callback when calling `VideoCutscene#play()`.
*/
static function onCutsceneFinish(cutsceneType:CutsceneType):Void
{
switch (cutsceneType)
{
case CutsceneType.STARTING:
PlayState.instance.startCountdown();
case CutsceneType.ENDING:
PlayState.instance.endSong(true); // true = right goddamn now
case CutsceneType.MIDSONG:
throw "Not implemented!";
}
}
}
enum CutsceneType
{
STARTING; // The default cutscene type. Starts the countdown after the video is done.
MIDSONG; // TODO: Implement this!
ENDING; // Ends the song after the video is done.
}
@@ -31,10 +31,6 @@ import funkin.data.dialogue.DialogueBoxRegistry;
*/
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<ConversationData>
{
static final CONVERSATION_SKIP_TIMER:Float = 1.5;
var skipHeldTimer:Float = 0.0;
/**
* The ID of the conversation.
*/
@@ -105,8 +101,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
var currentDialogueBox:DialogueBox;
var skipTimer:FlxPieDial;
public function new(id:String)
{
super();
@@ -124,8 +118,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
{
// Reset the progress in the dialogue.
currentDialogueEntry = 0;
currentDialogueLine = 0;
this.state = ConversationState.Start;
this.alpha = 1.0;
// Start the dialogue.
dispatchEvent(new DialogueScriptEvent(DIALOGUE_START, this, false));
@@ -151,8 +145,31 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
music.play();
}
public function pauseMusic():Void
{
if (music != null)
{
music.pause();
}
}
public function resumeMusic():Void
{
if (music != null)
{
music.resume();
}
}
function setupBackdrop():Void
{
if (backdrop != null)
{
backdrop.destroy();
remove(backdrop);
backdrop = null;
}
backdrop = new FunkinSprite(0, 0);
if (_data.backdrop == null) return;
@@ -181,12 +198,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
refresh();
}
function setupSkipTimer():Void
{
add(skipTimer = new FlxPieDial(16, 16, 32, FlxColor.WHITE, 36, CIRCLE, true, 24));
skipTimer.amount = 0;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
@@ -199,9 +210,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
var nextSpeakerId:String = currentDialogueEntryData.speaker;
// Skip the next steps if the current speaker is already displayed.
if (currentSpeaker != null && nextSpeakerId == currentSpeaker.id) return;
var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
if ((currentSpeaker != null && currentSpeaker.alive) && nextSpeakerId == currentSpeaker.id) return;
if (currentSpeaker != null)
{
@@ -210,6 +219,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
currentSpeaker = null;
}
var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
if (nextSpeaker == null)
{
if (nextSpeakerId == null)
@@ -222,6 +233,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
}
return;
}
if (!nextSpeaker.alive) nextSpeaker.revive();
ScriptEventDispatcher.callEvent(nextSpeaker, new ScriptEvent(CREATE, true));
@@ -249,8 +261,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
{
var nextDialogueBoxId:String = currentDialogueEntryData?.box;
// Skip the next steps if the current speaker is already displayed.
if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.id) return;
// Skip the next steps if the current dialogue box is already displayed.
if ((currentDialogueBox != null && currentDialogueBox.alive) && nextDialogueBoxId == currentDialogueBox.id) return;
if (currentDialogueBox != null)
{
@@ -266,6 +278,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
trace('Dialogue box could not be retrieved.');
return;
}
if (!nextDialogueBox.alive) nextDialogueBox.revive();
ScriptEventDispatcher.callEvent(nextDialogueBox, new ScriptEvent(CREATE, true));
@@ -347,29 +360,28 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
currentDialogueEntry = 0;
this.state = ConversationState.Start;
advanceConversation();
}
public function trySkipConversation(elapsed:Float):Void
{
if (skipTimer == null || skipTimer.animation == null) return;
if (elapsed < 0)
if (outroTween != null)
{
skipHeldTimer = 0.0;
}
else
{
skipHeldTimer += elapsed;
outroTween.cancel();
}
outroTween = null;
skipTimer.visible = skipHeldTimer >= 0.05;
skipTimer.amount = Math.min(skipHeldTimer / CONVERSATION_SKIP_TIMER, 1.0);
if (this.music != null) this.music.stop();
this.music = null;
if (skipHeldTimer >= CONVERSATION_SKIP_TIMER)
{
skipConversation();
}
if (currentSpeaker != null) currentSpeaker.kill();
remove(currentSpeaker);
currentSpeaker = null;
if (currentDialogueBox != null) currentDialogueBox.kill();
remove(currentDialogueBox);
currentDialogueBox = null;
if (backdrop != null) backdrop.destroy();
remove(backdrop);
backdrop = null;
startConversation();
}
/**
@@ -383,7 +395,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
dispatchEvent(new DialogueScriptEvent(DIALOGUE_SKIP, this, true));
}
static var outroTween:FlxTween;
var outroTween:FlxTween;
public function startOutro():Void
{
@@ -411,7 +423,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
public function endOutro():Void
{
outroTween = null;
ScriptEventDispatcher.callEvent(this, new ScriptEvent(DESTROY, false));
}
@@ -425,7 +436,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
// Fade in the music and backdrop.
setupMusic();
setupBackdrop();
setupSkipTimer();
// Advance the conversation.
state = ConversationState.Opening;
@@ -547,19 +557,25 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
{
propagateEvent(event);
if (outroTween != null) outroTween.cancel(); // Canc
if (outroTween != null)
{
outroTween.cancel();
}
outroTween = null;
this.alpha = 0.0;
if (this.music != null) this.music.stop();
this.music = null;
this.skipTimer = null;
if (currentSpeaker != null) currentSpeaker.kill();
remove(currentSpeaker);
currentSpeaker = null;
if (currentDialogueBox != null) currentDialogueBox.kill();
remove(currentDialogueBox);
currentDialogueBox = null;
if (backdrop != null) backdrop.kill();
if (backdrop != null) backdrop.destroy();
remove(backdrop);
backdrop = null;
this.clear();
@@ -578,16 +594,27 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
*/
function propagateEvent(event:ScriptEvent):Void
{
if (this.currentDialogueBox != null)
if (this.currentDialogueBox != null && this.currentDialogueBox.exists)
{
ScriptEventDispatcher.callEvent(this.currentDialogueBox, event);
}
if (this.currentSpeaker != null)
if (this.currentSpeaker != null && this.currentSpeaker.exists)
{
ScriptEventDispatcher.callEvent(this.currentSpeaker, event);
}
}
/**
* Calls `kill()` on the group's members and then on the group itself.
* You can revive this group later via `revive()` after this.
*/
public override function revive():Void
{
super.revive();
this.alpha = 1;
this.visible = true;
}
/**
* Calls `kill()` on the group's members and then on the group itself.
* You can revive this group later via `revive()` after this.
@@ -599,6 +626,12 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
exists = false;
_skipTransformChildren = false;
if (group != null) group.kill();
if (outroTween != null)
{
outroTween.cancel();
outroTween = null;
}
}
public override function toString():String
@@ -4,10 +4,11 @@ import flixel.FlxSprite;
import funkin.data.IRegistryEntry;
import flixel.group.FlxSpriteGroup;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.text.FlxText;
import funkin.graphics.FunkinSprite;
import flixel.addons.text.FlxTypeText;
import funkin.util.assets.FlxAnimationUtil;
import funkin.modding.events.ScriptEvent;
import funkin.audio.FunkinSound;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import flixel.util.FlxColor;
import funkin.data.dialogue.DialogueBoxData;
@@ -111,9 +112,6 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
this.y = 0;
this.alpha = 1;
this.boxSprite = new FlxSprite(0, 0);
add(this.boxSprite);
loadSpritesheet();
loadAnimations();
@@ -122,6 +120,14 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
function loadSpritesheet():Void
{
if (this.boxSprite != null)
{
remove(this.boxSprite);
this.boxSprite = null;
}
this.boxSprite = new FunkinSprite(0, 0);
trace('[DIALOGUE BOX] Loading spritesheet ${_data.assetPath} for ${id}');
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
@@ -146,6 +152,8 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
this.flipY = _data.flipY;
this.globalOffsets = _data.offsets;
this.setScale(_data.scale);
add(this.boxSprite);
}
public function setText(newText:String):Void
@@ -190,6 +198,34 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
this.boxSprite.updateHitbox();
}
/**
* Calls `kill()` on the group's members and then on the group itself.
* You can revive this group later via `revive()` after this.
*/
public override function kill():Void
{
super.kill();
if (this.boxSprite != null)
{
this.boxSprite.kill();
this.boxSprite = null;
}
if (this.textDisplay != null)
{
this.textDisplay.kill();
this.textDisplay = null;
}
this.clear();
}
public override function revive():Void
{
super.revive();
this.visible = true;
this.alpha = 1.0;
}
function loadAnimations():Void
{
trace('[DIALOGUE BOX] Loading ${_data.animations.length} animations for ${id}');
@@ -246,7 +282,8 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
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)];
// TODO: Add an option to configure this.
textDisplay.sounds = [FunkinSound.load(Paths.sound('pixelText'), 0.6)];
textDisplay.completeCallback = onTypingComplete;
@@ -106,6 +106,26 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
loadAnimations();
}
/**
* Calls `kill()` on the group's members and then on the group itself.
* You can revive this group later via `revive()` after this.
*/
public override function kill():Void
{
super.kill();
}
public override function revive():Void
{
super.revive();
this.visible = true;
this.alpha = 1.0;
loadSpritesheet();
loadAnimations();
}
function loadSpritesheet():Void
{
trace('[SPEAKER] Loading spritesheet ${_data.assetPath} for ${id}');
@@ -198,25 +218,25 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
// If the animation exists, we're good.
if (hasAnimation(name)) return name;
trace('[BOPPER] Animation "$name" does not exist!');
FlxG.log.notice('Speaker tried to play animation "$name" that does not exist, stripping suffixes...');
// Attempt to strip a `-alt` suffix, if it exists.
if (name.lastIndexOf('-') != -1)
{
var correctName = name.substring(0, name.lastIndexOf('-'));
trace('[BOPPER] Attempting to fallback to "$correctName"');
FlxG.log.notice('Speaker tried to play animation "$name" that does not exist, stripping suffixes...');
return correctAnimationName(correctName);
}
else
{
if (name != 'idle')
{
trace('[BOPPER] Attempting to fallback to "idle"');
FlxG.log.warn('Speaker tried to play animation "$name" that does not exist, fallback to idle...');
return correctAnimationName('idle');
}
else
{
trace('[BOPPER] Failing animation playback.');
FlxG.log.error('Speaker tried to play animation "idle" that does not exist! This is bad!');
return null;
}
}
@@ -1,5 +1,6 @@
package funkin.play.event;
import flixel.tweens.FlxEase;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
@@ -57,6 +58,9 @@ class FocusCameraSongEvent extends SongEvent
// Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null || PlayState.instance.currentStage == null) return;
// Does nothing if we are minimal mode.
if (PlayState.instance.isMinimalMode) return;
var posX:Null<Float> = data.getFloat('x');
if (posX == null) posX = 0.0;
var posY:Null<Float> = data.getFloat('y');
@@ -66,6 +70,13 @@ class FocusCameraSongEvent extends SongEvent
if (char == null) char = cast data.value;
var useTween:Null<Bool> = data.getBool('useTween');
if (useTween == null) useTween = false;
var duration:Null<Float> = data.getFloat('duration');
if (duration == null) duration = 4.0;
var ease:Null<String> = data.getString('ease');
if (ease == null) ease = 'linear';
switch (char)
{
case -1: // Position
@@ -114,6 +125,26 @@ class FocusCameraSongEvent extends SongEvent
default:
trace('Unknown camera focus: ' + data);
}
if (useTween)
{
switch (ease)
{
case 'INSTANT':
PlayState.instance.tweenCameraToFollowPoint(0);
default:
var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
var easeFunction:Null<Float->Float> = Reflect.field(FlxEase, ease);
if (easeFunction == null)
{
trace('Invalid ease function: $ease');
return;
}
PlayState.instance.tweenCameraToFollowPoint(durSeconds, easeFunction);
}
}
}
public override function getTitle():String
@@ -155,6 +186,51 @@ class FocusCameraSongEvent extends SongEvent
step: 10.0,
type: SongEventFieldType.FLOAT,
units: "px"
},
{
name: 'useTween',
title: 'Use Tween',
type: SongEventFieldType.BOOL,
defaultValue: false
},
{
name: 'duration',
title: 'Duration',
defaultValue: 4.0,
step: 0.5,
type: SongEventFieldType.FLOAT,
units: 'steps'
},
{
name: 'ease',
title: 'Easing Type',
defaultValue: 'linear',
type: SongEventFieldType.ENUM,
keys: [
'Linear' => 'linear',
'Instant' => 'INSTANT',
'Quad In' => 'quadIn',
'Quad Out' => 'quadOut',
'Quad In/Out' => 'quadInOut',
'Cube In' => 'cubeIn',
'Cube Out' => 'cubeOut',
'Cube In/Out' => 'cubeInOut',
'Quart In' => 'quartIn',
'Quart Out' => 'quartOut',
'Quart In/Out' => 'quartInOut',
'Quint In' => 'quintIn',
'Quint Out' => 'quintOut',
'Quint In/Out' => 'quintInOut',
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
'Sine In' => 'sineIn',
'Sine Out' => 'sineOut',
'Sine In/Out' => 'sineInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
'Elastic In/Out' => 'elasticInOut',
]
}
]);
}
+30 -13
View File
@@ -52,26 +52,36 @@ class ZoomCameraSongEvent extends SongEvent
super('ZoomCamera');
}
static final DEFAULT_ZOOM:Float = 1.0;
static final DEFAULT_DURATION:Float = 4.0;
static final DEFAULT_MODE:String = 'direct';
static final DEFAULT_EASE:String = 'linear';
public override function handleEvent(data:SongEventData):Void
{
// Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null) return;
if (PlayState.instance == null || PlayState.instance.currentStage == null) return;
var zoom:Null<Float> = data.getFloat('zoom');
if (zoom == null) zoom = 1.0;
var duration:Null<Float> = data.getFloat('duration');
if (duration == null) duration = 4.0;
// Does nothing if we are minimal mode.
if (PlayState.instance.isMinimalMode) return;
var ease:Null<String> = data.getString('ease');
if (ease == null) ease = 'linear';
var zoom:Float = data.getFloat('zoom') ?? DEFAULT_ZOOM;
var duration:Float = data.getFloat('duration') ?? DEFAULT_DURATION;
var mode:String = data.getString('mode') ?? DEFAULT_MODE;
var isDirectMode:Bool = mode == 'direct';
var ease:String = data.getString('ease') ?? DEFAULT_EASE;
// If it's a string, check the value.
switch (ease)
{
case 'INSTANT':
// Set the zoom. Use defaultCameraZoom to prevent breaking camera bops.
PlayState.instance.defaultCameraZoom = zoom * FlxCamera.defaultZoom;
PlayState.instance.tweenCameraZoom(zoom, 0, isDirectMode);
default:
var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
var easeFunction:Null<Float->Float> = Reflect.field(FlxEase, ease);
if (easeFunction == null)
{
@@ -79,8 +89,7 @@ class ZoomCameraSongEvent extends SongEvent
return;
}
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.instance.stepLengthMs * duration / 1000),
{ease: easeFunction});
PlayState.instance.tweenCameraZoom(zoom, durSeconds, isDirectMode, easeFunction);
}
}
@@ -93,8 +102,9 @@ class ZoomCameraSongEvent extends SongEvent
* ```
* {
* 'zoom': FLOAT, // Target zoom level.
* 'duration': FLOAT, // Optional duration in steps
* 'ease': ENUM, // Optional easing function
* 'duration': FLOAT, // Optional duration in steps.
* 'mode': ENUM, // Whether to set additive zoom or direct zoom.
* 'ease': ENUM, // Optional easing function.
* }
* @return SongEventSchema
*/
@@ -117,6 +127,13 @@ class ZoomCameraSongEvent extends SongEvent
type: SongEventFieldType.FLOAT,
units: 'steps'
},
{
name: 'mode',
title: 'Mode',
defaultValue: 'direct',
type: SongEventFieldType.ENUM,
keys: ['Additive' => 'additive', 'Direct' => 'direct']
},
{
name: 'ease',
title: 'Easing Type',
+3
View File
@@ -35,6 +35,7 @@ class NoteSplash extends FlxSprite
*/
function setup():Void
{
if (frameCollection?.parent?.isDestroyed ?? false) frameCollection = null;
if (frameCollection == null) preloadFrames();
this.frames = frameCollection;
@@ -75,6 +76,8 @@ class NoteSplash extends FlxSprite
this.playAnimation('splash${variant}Right');
}
if (animation.curAnim == null) return;
// Vary the speed of the animation a bit.
animation.curAnim.frameRate = FRAMERATE_DEFAULT + FlxG.random.int(-FRAMERATE_VARIANCE, FRAMERATE_VARIANCE);
+37 -12
View File
@@ -4,9 +4,10 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.HSVShader;
class NoteSprite extends FlxSprite
class NoteSprite extends FunkinSprite
{
static final DIRECTION_COLORS:Array<String> = ['purple', 'blue', 'green', 'red'];
@@ -15,26 +16,53 @@ class NoteSprite extends FlxSprite
var hsvShader:HSVShader;
/**
* The time at which the note should be hit, in milliseconds.
* The strum time at which the note should be hit, in milliseconds.
*/
public var strumTime(default, set):Float;
public var strumTime(get, set):Float;
function get_strumTime():Float
{
return this.noteData?.time ?? 0.0;
}
function set_strumTime(value:Float):Float
{
this.strumTime = value;
return this.strumTime;
if (this.noteData == null) return value;
return this.noteData.time = value;
}
/**
* The length for which the note should be held, in milliseconds.
* Defaults to 0 for single notes.
*/
public var length(get, set):Float;
function get_length():Float
{
return this.noteData?.length ?? 0.0;
}
function set_length(value:Float):Float
{
if (this.noteData == null) return value;
return this.noteData.length = value;
}
/**
* An extra attribute for the note.
* For example, whether the note is an "alt" note, or whether it has custom behavior on hit.
*/
public var kind(default, set):String;
public var kind(get, set):Null<String>;
function get_kind():Null<String>
{
return this.noteData?.kind;
}
function set_kind(value:String):String
{
this.kind = value;
return this.kind;
if (this.noteData == null) return value;
return this.noteData.kind = value;
}
/**
@@ -99,16 +127,13 @@ class NoteSprite extends FlxSprite
*/
public var handledMiss:Bool;
public function new(noteStyle:NoteStyle, strumTime:Float = 0, direction:Int = 0)
public function new(noteStyle:NoteStyle, direction:Int = 0)
{
super(0, -9999);
this.strumTime = strumTime;
this.direction = direction;
this.hsvShader = new HSVShader();
if (this.strumTime < 0) this.strumTime = 0;
setupNoteGraphic(noteStyle);
// Disables the update() function for performance.
+11 -1
View File
@@ -1,5 +1,6 @@
package funkin.play.notes;
import flixel.util.FlxSignal.FlxTypedSignal;
import flixel.FlxG;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.group.FlxSpriteGroup;
@@ -39,6 +40,10 @@ class Strumline extends FlxSpriteGroup
return FlxG.height / 0.45;
}
/**
* Whether this strumline is controlled by the player's inputs.
* False means it's controlled by the opponent or Bot Play.
*/
public var isPlayer:Bool;
/**
@@ -56,6 +61,8 @@ class Strumline extends FlxSpriteGroup
public var holdNotes:FlxTypedSpriteGroup<SustainTrail>;
public var onNoteIncoming:FlxTypedSignal<NoteSprite->Void>;
var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>;
var noteSplashes:FlxTypedSpriteGroup<NoteSplash>;
var noteHoldCovers:FlxTypedSpriteGroup<NoteHoldCover>;
@@ -113,6 +120,8 @@ class Strumline extends FlxSpriteGroup
this.refresh();
this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>();
for (i in 0...KEY_COUNT)
{
var child:StrumlineNote = new StrumlineNote(noteStyle, isPlayer, DIRECTIONS[i]);
@@ -398,6 +407,8 @@ class Strumline extends FlxSpriteGroup
}
nextNoteIndex = noteIndex + 1; // Increment the nextNoteIndex rather than splicing the array, because splicing is slow.
onNoteIncoming.dispatch(noteSprite);
}
// Update rendering of notes.
@@ -746,7 +757,6 @@ class Strumline extends FlxSpriteGroup
if (noteSprite != null)
{
noteSprite.strumTime = note.time;
noteSprite.direction = note.getDirection();
noteSprite.noteData = note;
@@ -4,6 +4,7 @@ import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.data.animation.AnimationData;
import funkin.data.IRegistryEntry;
import funkin.graphics.FunkinSprite;
import funkin.data.notestyle.NoteStyleData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
@@ -100,6 +101,14 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
function buildNoteFrames(force:Bool = false):FlxAtlasFrames
{
if (!FunkinSprite.isTextureCached(Paths.image(getNoteAssetPath())))
{
FlxG.log.warn('Note texture is not cached: ${getNoteAssetPath()}');
}
// Purge the note frames if the cached atlas is invalid.
if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null;
if (noteFrames != null && !force) return noteFrames;
noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary());
@@ -109,8 +118,6 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
throw 'Could not load note frames for note style: $id';
}
noteFrames.parent.persist = true;
return noteFrames;
}
+130 -16
View File
@@ -1,6 +1,5 @@
package funkin.play.song;
import flixel.sound.FlxSound;
import funkin.audio.VoicesGroup;
import funkin.audio.FunkinSound;
import funkin.data.IRegistryEntry;
@@ -13,9 +12,8 @@ import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongData.SongTimeFormat;
import funkin.data.song.SongRegistry;
import funkin.data.song.SongRegistry;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.events.ScriptEvent;
import funkin.modding.IScriptedClass;
import funkin.util.SortUtil;
import openfl.utils.Assets;
@@ -31,14 +29,44 @@ import openfl.utils.Assets;
@:nullSafety
class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMetadata>
{
public static final DEFAULT_SONGNAME:String = "Unknown";
public static final DEFAULT_ARTIST:String = "Unknown";
/**
* The default value for the song's name
*/
public static final DEFAULT_SONGNAME:String = 'Unknown';
/**
* The default value for the song's artist
*/
public static final DEFAULT_ARTIST:String = 'Unknown';
/**
* The default value for the song's time format
*/
public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
/**
* The default value for the song's divisions
*/
public static final DEFAULT_DIVISIONS:Null<Int> = null;
/**
* The default value for whether the song loops.
*/
public static final DEFAULT_LOOPED:Bool = false;
public static final DEFAULT_STAGE:String = "mainStage";
/**
* The default value for the song's playable stage.
*/
public static final DEFAULT_STAGE:String = 'mainStage';
/**
* The default value for the song's scroll speed.
*/
public static final DEFAULT_SCROLLSPEED:Float = 1.0;
/**
* The internal ID of the song.
*/
public final id:String;
/**
@@ -53,6 +81,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
final _metadata:Map<String, SongMetadata>;
final difficulties:Map<String, SongDifficulty>;
/**
* The list of variations a song has.
*/
public var variations(get, never):Array<String>;
function get_variations():Array<String>
@@ -65,6 +96,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/
public var validScore:Bool = true;
/**
* The readable name of the song.
*/
public var songName(get, never):String;
function get_songName():String
@@ -74,6 +108,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return DEFAULT_SONGNAME;
}
/**
* The artist of the song.
*/
public var songArtist(get, never):String;
function get_songArtist():String
@@ -101,7 +138,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
{
for (vari in _data.playData.songVariations)
{
var variMeta = fetchVariationMetadata(id, vari);
var variMeta:Null<SongMetadata> = fetchVariationMetadata(id, vari);
if (variMeta != null) _metadata.set(variMeta.variation, variMeta);
}
}
@@ -115,32 +152,87 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
populateDifficulties();
}
@:allow(funkin.play.song.Song)
/**
* Build a song from existing metadata rather than loading it from the `assets` folder.
* Used by the Chart Editor.
*
* @param songId The ID of the song.
* @param metadata The metadata of the song.
* @param variations The list of variations this song has.
* @param charts The chart data for each variation.
* @param includeScript Whether to initialize the scripted class tied to the song, if it exists.
* @param validScore Whether the song is elegible for highscores.
* @return The constructed song object.
*/
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>,
validScore:Bool = false):Song
includeScript:Bool = true, validScore:Bool = false):Song
{
var result:Song = new Song(songId);
@:privateAccess
var result:Null<Song>;
if (includeScript && SongRegistry.instance.isScriptedEntry(songId))
{
var songClassName:String = SongRegistry.instance.getScriptedEntryClassName(songId);
@:privateAccess
result = SongRegistry.instance.createScriptedEntry(songClassName);
}
else
{
@:privateAccess
result = SongRegistry.instance.createEntry(songId);
}
if (result == null) throw 'ERROR: Could not build Song instance ($songId), is the attached script bad?';
result._metadata.clear();
for (meta in metadata)
{
result._metadata.set(meta.variation, meta);
}
result.difficulties.clear();
result.populateDifficulties();
for (variation => chartData in charts)
{
result.applyChartData(chartData, variation);
}
result.validScore = validScore;
return result;
}
/**
* Retrieve a list of the raw metadata for the song.
* @return The metadata JSON objects for the song's variations.
*/
public function getRawMetadata():Array<SongMetadata>
{
return _metadata.values();
}
/**
* List the album IDs for each variation of the song.
* @return A map of variation IDs to album IDs.
*/
public function listAlbums():Map<String, String>
{
var result:Map<String, String> = new Map<String, String>();
for (difficultyId in difficulties.keys())
{
var meta:Null<SongDifficulty> = difficulties.get(difficultyId);
if (meta != null && meta.album != null)
{
result.set(difficultyId, meta.album);
}
}
return result;
}
/**
* Populate the difficulty data from the provided metadata.
* Does not load chart data (that is triggered later when we want to play the song).
@@ -192,6 +284,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
/**
* Parse and cache the chart for all difficulties of this song.
* @param force Whether to forcibly clear the list of charts first.
*/
public function cacheCharts(force:Bool = false):Void
{
@@ -294,11 +387,14 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
/**
* List all the difficulties in this song.
*
* @param variationId Optionally filter by a single variation.
* @param variationIds Optionally filter by multiple variations.
* @param showHidden Include charts which are not accessible to the player.
*
* @return The list of difficulties.
*/
public function listDifficulties(?variationId:String, ?variationIds:Array<String>):Array<String>
public function listDifficulties(?variationId:String, ?variationIds:Array<String>, showHidden:Bool = false):Array<String>
{
if (variationIds == null) variationIds = [];
if (variationId != null) variationIds.push(variationId);
@@ -314,6 +410,15 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return difficulty.difficulty;
}).nonNull().unique();
diffFiltered = diffFiltered.filter(function(diffId:String):Bool {
if (showHidden) return true;
for (targetVariation in variationIds)
{
if (isDifficultyVisible(diffId, targetVariation)) return true;
}
return false;
});
diffFiltered.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST));
return diffFiltered;
@@ -332,6 +437,13 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return false;
}
public function isDifficultyVisible(diffId:String, variationId:String):Bool
{
var variation = _metadata.get(variationId);
if (variation == null) return false;
return variation.playData.difficulties.contains(diffId);
}
/**
* Purge the cached chart data for each difficulty of this song.
*/
@@ -364,7 +476,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
public function onSongRetry(event:ScriptEvent):Void {};
public function onNoteHit(event:NoteScriptEvent):Void {};
public function onNoteIncoming(event:NoteScriptEvent) {}
public function onNoteHit(event:HitNoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent):Void {};
@@ -502,11 +616,11 @@ class SongDifficulty
}
}
public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void
public function playInst(volume:Float = 1.0, looped:Bool = false):Void
{
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
FlxG.sound.music = FunkinSound.load(Paths.inst(this.song.id, suffix), volume, looped);
FlxG.sound.music = FunkinSound.load(Paths.inst(this.song.id, suffix), volume, looped, false, true);
// Workaround for a bug where FlxG.sound.music.update() was being called twice.
FlxG.sound.list.remove(FlxG.sound.music);
@@ -614,9 +728,9 @@ class SongDifficulty
}
// Add player vocals.
if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(Assets.getSound(voiceList[0])));
if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(voiceList[0]));
// Add opponent vocals.
if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(Assets.getSound(voiceList[1])));
if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(voiceList[1]));
// Add additional vocals.
if (voiceList.length > 2)
+22 -14
View File
@@ -167,10 +167,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
function update_shouldAlternate():Void
{
if (hasAnimation('danceLeft'))
{
this.shouldAlternate = true;
}
this.shouldAlternate = hasAnimation('danceLeft');
}
/**
@@ -228,33 +225,42 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
/**
* Ensure that a given animation exists before playing it.
* Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
* @param name
* Will gracefully check for name, then name with stripped suffixes, then fail to play.
* @param name The animation name to attempt to correct.
* @param fallback Instead of failing to play, try to play this animation instead.
*/
function correctAnimationName(name:String):String
function correctAnimationName(name:String, ?fallback:String):String
{
// If the animation exists, we're good.
if (hasAnimation(name)) return name;
trace('[BOPPER] Animation "$name" does not exist!');
FlxG.log.notice('Bopper tried to play animation "$name" that does not exist, stripping suffixes...');
// Attempt to strip a `-alt` suffix, if it exists.
if (name.lastIndexOf('-') != -1)
{
var correctName = name.substring(0, name.lastIndexOf('-'));
trace('[BOPPER] Attempting to fallback to "$correctName"');
FlxG.log.notice('Bopper tried to play animation "$name" that does not exist, stripping suffixes...');
return correctAnimationName(correctName);
}
else
{
if (name != 'idle')
if (fallback != null)
{
trace('[BOPPER] Attempting to fallback to "idle"');
return correctAnimationName('idle');
if (fallback == name)
{
FlxG.log.error('Bopper tried to play animation "$name" that does not exist! This is bad!');
return null;
}
else
{
FlxG.log.warn('Bopper tried to play animation "$name" that does not exist, fallback to idle...');
return correctAnimationName('idle');
}
}
else
{
trace('[BOPPER] Failing animation playback.');
FlxG.log.error('Bopper tried to play animation "$name" that does not exist! This is bad!');
return null;
}
}
@@ -353,7 +359,9 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
public function onGameOver(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteIncoming(event:NoteScriptEvent) {}
public function onNoteHit(event:HitNoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
+63 -27
View File
@@ -109,9 +109,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
{
getBoyfriend().resetCharacter(true);
// Reapply the camera offsets.
var charData = _data.characters.bf;
getBoyfriend().cameraFocusPoint.x += charData.cameraOffsets[0];
getBoyfriend().cameraFocusPoint.y += charData.cameraOffsets[1];
var stageCharData:StageDataCharacter = _data.characters.bf;
var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale;
getBoyfriend().setScale(finalScale);
getBoyfriend().cameraFocusPoint.x += stageCharData.cameraOffsets[0];
getBoyfriend().cameraFocusPoint.y += stageCharData.cameraOffsets[1];
}
else
{
@@ -121,17 +123,21 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
{
getGirlfriend().resetCharacter(true);
// Reapply the camera offsets.
var charData = _data.characters.gf;
getGirlfriend().cameraFocusPoint.x += charData.cameraOffsets[0];
getGirlfriend().cameraFocusPoint.y += charData.cameraOffsets[1];
var stageCharData:StageDataCharacter = _data.characters.gf;
var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale;
getGirlfriend().setScale(finalScale);
getGirlfriend().cameraFocusPoint.x += stageCharData.cameraOffsets[0];
getGirlfriend().cameraFocusPoint.y += stageCharData.cameraOffsets[1];
}
if (getDad() != null)
{
getDad().resetCharacter(true);
// Reapply the camera offsets.
var charData = _data.characters.dad;
getDad().cameraFocusPoint.x += charData.cameraOffsets[0];
getDad().cameraFocusPoint.y += charData.cameraOffsets[1];
var stageCharData:StageDataCharacter = _data.characters.dad;
var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale;
getDad().setScale(finalScale);
getDad().cameraFocusPoint.x += stageCharData.cameraOffsets[0];
getDad().cameraFocusPoint.y += stageCharData.cameraOffsets[1];
}
// Reset positions of named props.
@@ -185,9 +191,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
switch (dataProp.animType)
{
case 'packer':
propSprite.frames = Paths.getPackerAtlas(dataProp.assetPath);
propSprite.loadPacker(dataProp.assetPath);
default: // 'sparrow'
propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath);
propSprite.loadSparrow(dataProp.assetPath);
}
}
else if (isSolidColor)
@@ -209,7 +215,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
else
{
// Initalize static sprite.
propSprite.loadGraphic(Paths.image(dataProp.assetPath));
propSprite.loadTexture(dataProp.assetPath);
// Disables calls to update() for a performance boost.
propSprite.active = false;
@@ -226,7 +232,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
switch (dataProp.scale)
{
case Left(value):
propSprite.scale.set(value);
propSprite.scale.set(value, value);
case Right(values):
propSprite.scale.set(values[0], values[1]);
@@ -390,22 +396,25 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
#end
// Apply position and z-index.
var charData:StageDataCharacter = null;
var stageCharData:StageDataCharacter = null;
switch (charType)
{
case BF:
this.characters.set('bf', character);
charData = _data.characters.bf;
stageCharData = _data.characters.bf;
character.flipX = !character.getDataFlipX();
character.name = 'bf';
character.initHealthIcon(false);
case GF:
this.characters.set('gf', character);
charData = _data.characters.gf;
stageCharData = _data.characters.gf;
character.flipX = character.getDataFlipX();
character.name = 'gf';
case DAD:
this.characters.set('dad', character);
charData = _data.characters.dad;
stageCharData = _data.characters.dad;
character.flipX = character.getDataFlipX();
character.name = 'dad';
character.initHealthIcon(true);
default:
this.characters.set(character.characterId, character);
@@ -415,15 +424,15 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
// This ensures positioning is based on the idle animation.
character.resetCharacter(true);
if (charData != null)
if (stageCharData != null)
{
character.zIndex = charData.zIndex;
character.zIndex = stageCharData.zIndex;
// Start with the per-stage character position.
// Subtracting the origin ensures characters are positioned relative to their feet.
// Subtracting the global offset allows positioning on a per-character basis.
character.x = charData.position[0] - character.characterOrigin.x + character.globalOffsets[0];
character.y = charData.position[1] - character.characterOrigin.y + character.globalOffsets[1];
character.x = stageCharData.position[0] - character.characterOrigin.x + character.globalOffsets[0];
character.y = stageCharData.position[1] - character.characterOrigin.y + character.globalOffsets[1];
@:privateAccess(funkin.play.stage.Bopper)
{
@@ -432,15 +441,17 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
character.originalPosition.y = character.y + character.animOffsets[1];
}
character.cameraFocusPoint.x += charData.cameraOffsets[0];
character.cameraFocusPoint.y += charData.cameraOffsets[1];
var finalScale = character.getBaseScale() * stageCharData.scale;
character.setScale(finalScale); // Don't use scale.set for characters!
character.cameraFocusPoint.x += stageCharData.cameraOffsets[0];
character.cameraFocusPoint.y += stageCharData.cameraOffsets[1];
#if debug
// Draw the debug icon at the character's feet.
if (charType == BF || charType == DAD)
{
debugIcon.x = charData.position[0];
debugIcon.y = charData.position[1];
debugIcon.x = stageCharData.position[0];
debugIcon.y = stageCharData.position[1];
debugIcon2.x = character.x;
debugIcon2.y = character.y;
}
@@ -634,7 +645,30 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
*/
public function dispatchToCharacters(event:ScriptEvent):Void
{
for (characterId in characters.keys())
var charList = this.characters.keys().array();
// Dad, then BF, then GF, in that order.
if (charList.contains('dad'))
{
dispatchToCharacter('dad', event);
charList.remove('dad');
}
if (charList.contains('bf'))
{
dispatchToCharacter('bf', event);
charList.remove('bf');
}
if (charList.contains('gf'))
{
dispatchToCharacter('gf', event);
charList.remove('gf');
}
// Then the rest of the characters, if any.
for (characterId in charList)
{
dispatchToCharacter(characterId, event);
}
@@ -840,7 +874,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteIncoming(event:NoteScriptEvent) {}
public function onNoteHit(event:HitNoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
+215 -195
View File
@@ -11,8 +11,7 @@ import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import thx.semver.Version;
@:nullSafety
@:forward(volume, mute)
abstract Save(RawSaveData)
class Save
{
// Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2";
@@ -25,6 +24,20 @@ abstract Save(RawSaveData)
static final SAVE_PATH_LEGACY:String = 'ninjamuffin99';
static final SAVE_NAME_LEGACY:String = 'funkin';
public static var instance(get, never):Save;
static var _instance:Null<Save> = null;
static function get_instance():Save
{
if (_instance == null)
{
_instance = new Save(FlxG.save.data);
}
return _instance;
}
var data:RawSaveData;
public static function load():Void
{
trace("[SAVE] Loading save...");
@@ -33,86 +46,87 @@ abstract Save(RawSaveData)
loadFromSlot(1);
}
public static function get():Save
{
return FlxG.save.data;
}
/**
* Constructing a new Save will load the default values.
*/
public function new()
public function new(data:RawSaveData)
{
this =
{
version: Save.SAVE_DATA_VERSION,
this.data = data;
volume: 1.0,
mute: false,
if (this.data == null) data = Save.getDefault();
}
api:
{
newgrounds:
{
sessionId: null,
}
},
scores:
{
// No saved scores.
levels: [],
songs: [],
},
options:
{
// Reasonable defaults.
naughtyness: true,
downscroll: false,
flashingLights: true,
zoomCamera: true,
debugDisplay: false,
autoPause: true,
inputOffset: 0,
audioVisualOffset: 0,
public static function getDefault():RawSaveData
{
return {
version: Save.SAVE_DATA_VERSION,
controls:
{
// Leave controls blank so defaults are loaded.
p1:
{
keyboard: {},
gamepad: {},
},
p2:
{
keyboard: {},
gamepad: {},
},
},
},
volume: 1.0,
mute: false,
mods:
{
// No mods enabled.
enabledMods: [],
modOptions: [],
},
api:
{
newgrounds:
{
sessionId: null,
}
},
scores:
{
// No saved scores.
levels: [],
songs: [],
},
options:
{
// Reasonable defaults.
naughtyness: true,
downscroll: false,
flashingLights: true,
zoomCamera: true,
debugDisplay: false,
autoPause: true,
inputOffset: 0,
audioVisualOffset: 0,
optionsChartEditor:
{
// Reasonable defaults.
previousFiles: [],
noteQuant: 3,
chartEditorLiveInputStyle: ChartEditorLiveInputStyle.None,
theme: ChartEditorTheme.Light,
playtestStartTime: false,
downscroll: false,
metronomeVolume: 1.0,
hitsoundsEnabledPlayer: true,
hitsoundsEnabledOpponent: true,
themeMusic: true
},
};
controls:
{
// Leave controls blank so defaults are loaded.
p1:
{
keyboard: {},
gamepad: {},
},
p2:
{
keyboard: {},
gamepad: {},
},
},
},
mods:
{
// No mods enabled.
enabledMods: [],
modOptions: [],
},
optionsChartEditor:
{
// Reasonable defaults.
previousFiles: [],
noteQuant: 3,
chartEditorLiveInputStyle: ChartEditorLiveInputStyle.None,
theme: ChartEditorTheme.Light,
playtestStartTime: false,
downscroll: false,
metronomeVolume: 1.0,
hitsoundVolumePlayer: 1.0,
hitsoundVolumeOpponent: 1.0,
themeMusic: true
},
};
}
/**
@@ -122,7 +136,7 @@ abstract Save(RawSaveData)
function get_options():SaveDataOptions
{
return this.options;
return data.options;
}
/**
@@ -132,7 +146,7 @@ abstract Save(RawSaveData)
function get_modOptions():Map<String, Dynamic>
{
return this.mods.modOptions;
return data.mods.modOptions;
}
/**
@@ -142,249 +156,232 @@ abstract Save(RawSaveData)
function get_ngSessionId():Null<String>
{
return this.api.newgrounds.sessionId;
return data.api.newgrounds.sessionId;
}
function set_ngSessionId(value:Null<String>):Null<String>
{
this.api.newgrounds.sessionId = value;
data.api.newgrounds.sessionId = value;
flush();
return this.api.newgrounds.sessionId;
return data.api.newgrounds.sessionId;
}
public var enabledModIds(get, set):Array<String>;
function get_enabledModIds():Array<String>
{
return this.mods.enabledMods;
return data.mods.enabledMods;
}
function set_enabledModIds(value:Array<String>):Array<String>
{
this.mods.enabledMods = value;
data.mods.enabledMods = value;
flush();
return this.mods.enabledMods;
return data.mods.enabledMods;
}
public var chartEditorPreviousFiles(get, set):Array<String>;
function get_chartEditorPreviousFiles():Array<String>
{
if (this.optionsChartEditor.previousFiles == null) this.optionsChartEditor.previousFiles = [];
if (data.optionsChartEditor.previousFiles == null) data.optionsChartEditor.previousFiles = [];
return this.optionsChartEditor.previousFiles;
return data.optionsChartEditor.previousFiles;
}
function set_chartEditorPreviousFiles(value:Array<String>):Array<String>
{
// Set and apply.
this.optionsChartEditor.previousFiles = value;
data.optionsChartEditor.previousFiles = value;
flush();
return this.optionsChartEditor.previousFiles;
return data.optionsChartEditor.previousFiles;
}
public var chartEditorHasBackup(get, set):Bool;
function get_chartEditorHasBackup():Bool
{
if (this.optionsChartEditor.hasBackup == null) this.optionsChartEditor.hasBackup = false;
if (data.optionsChartEditor.hasBackup == null) data.optionsChartEditor.hasBackup = false;
return this.optionsChartEditor.hasBackup;
return data.optionsChartEditor.hasBackup;
}
function set_chartEditorHasBackup(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.hasBackup = value;
data.optionsChartEditor.hasBackup = value;
flush();
return this.optionsChartEditor.hasBackup;
return data.optionsChartEditor.hasBackup;
}
public var chartEditorNoteQuant(get, set):Int;
function get_chartEditorNoteQuant():Int
{
if (this.optionsChartEditor.noteQuant == null) this.optionsChartEditor.noteQuant = 3;
if (data.optionsChartEditor.noteQuant == null) data.optionsChartEditor.noteQuant = 3;
return this.optionsChartEditor.noteQuant;
return data.optionsChartEditor.noteQuant;
}
function set_chartEditorNoteQuant(value:Int):Int
{
// Set and apply.
this.optionsChartEditor.noteQuant = value;
data.optionsChartEditor.noteQuant = value;
flush();
return this.optionsChartEditor.noteQuant;
return data.optionsChartEditor.noteQuant;
}
public var chartEditorLiveInputStyle(get, set):ChartEditorLiveInputStyle;
function get_chartEditorLiveInputStyle():ChartEditorLiveInputStyle
{
if (this.optionsChartEditor.chartEditorLiveInputStyle == null) this.optionsChartEditor.chartEditorLiveInputStyle = ChartEditorLiveInputStyle.None;
if (data.optionsChartEditor.chartEditorLiveInputStyle == null) data.optionsChartEditor.chartEditorLiveInputStyle = ChartEditorLiveInputStyle.None;
return this.optionsChartEditor.chartEditorLiveInputStyle;
return data.optionsChartEditor.chartEditorLiveInputStyle;
}
function set_chartEditorLiveInputStyle(value:ChartEditorLiveInputStyle):ChartEditorLiveInputStyle
{
// Set and apply.
this.optionsChartEditor.chartEditorLiveInputStyle = value;
data.optionsChartEditor.chartEditorLiveInputStyle = value;
flush();
return this.optionsChartEditor.chartEditorLiveInputStyle;
return data.optionsChartEditor.chartEditorLiveInputStyle;
}
public var chartEditorDownscroll(get, set):Bool;
function get_chartEditorDownscroll():Bool
{
if (this.optionsChartEditor.downscroll == null) this.optionsChartEditor.downscroll = false;
if (data.optionsChartEditor.downscroll == null) data.optionsChartEditor.downscroll = false;
return this.optionsChartEditor.downscroll;
return data.optionsChartEditor.downscroll;
}
function set_chartEditorDownscroll(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.downscroll = value;
data.optionsChartEditor.downscroll = value;
flush();
return this.optionsChartEditor.downscroll;
return data.optionsChartEditor.downscroll;
}
public var chartEditorPlaytestStartTime(get, set):Bool;
function get_chartEditorPlaytestStartTime():Bool
{
if (this.optionsChartEditor.playtestStartTime == null) this.optionsChartEditor.playtestStartTime = false;
if (data.optionsChartEditor.playtestStartTime == null) data.optionsChartEditor.playtestStartTime = false;
return this.optionsChartEditor.playtestStartTime;
return data.optionsChartEditor.playtestStartTime;
}
function set_chartEditorPlaytestStartTime(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.playtestStartTime = value;
data.optionsChartEditor.playtestStartTime = value;
flush();
return this.optionsChartEditor.playtestStartTime;
return data.optionsChartEditor.playtestStartTime;
}
public var chartEditorTheme(get, set):ChartEditorTheme;
function get_chartEditorTheme():ChartEditorTheme
{
if (this.optionsChartEditor.theme == null) this.optionsChartEditor.theme = ChartEditorTheme.Light;
if (data.optionsChartEditor.theme == null) data.optionsChartEditor.theme = ChartEditorTheme.Light;
return this.optionsChartEditor.theme;
return data.optionsChartEditor.theme;
}
function set_chartEditorTheme(value:ChartEditorTheme):ChartEditorTheme
{
// Set and apply.
this.optionsChartEditor.theme = value;
data.optionsChartEditor.theme = value;
flush();
return this.optionsChartEditor.theme;
return data.optionsChartEditor.theme;
}
public var chartEditorMetronomeVolume(get, set):Float;
function get_chartEditorMetronomeVolume():Float
{
if (this.optionsChartEditor.metronomeVolume == null) this.optionsChartEditor.metronomeVolume = 1.0;
if (data.optionsChartEditor.metronomeVolume == null) data.optionsChartEditor.metronomeVolume = 1.0;
return this.optionsChartEditor.metronomeVolume;
return data.optionsChartEditor.metronomeVolume;
}
function set_chartEditorMetronomeVolume(value:Float):Float
{
// Set and apply.
this.optionsChartEditor.metronomeVolume = value;
data.optionsChartEditor.metronomeVolume = value;
flush();
return this.optionsChartEditor.metronomeVolume;
return data.optionsChartEditor.metronomeVolume;
}
public var chartEditorHitsoundVolume(get, set):Float;
public var chartEditorHitsoundVolumePlayer(get, set):Float;
function get_chartEditorHitsoundVolume():Float
function get_chartEditorHitsoundVolumePlayer():Float
{
if (this.optionsChartEditor.hitsoundVolume == null) this.optionsChartEditor.hitsoundVolume = 1.0;
if (data.optionsChartEditor.hitsoundVolumePlayer == null) data.optionsChartEditor.hitsoundVolumePlayer = 1.0;
return this.optionsChartEditor.hitsoundVolume;
return data.optionsChartEditor.hitsoundVolumePlayer;
}
function set_chartEditorHitsoundVolume(value:Float):Float
function set_chartEditorHitsoundVolumePlayer(value:Float):Float
{
// Set and apply.
this.optionsChartEditor.hitsoundVolume = value;
data.optionsChartEditor.hitsoundVolumePlayer = value;
flush();
return this.optionsChartEditor.hitsoundVolume;
return data.optionsChartEditor.hitsoundVolumePlayer;
}
public var chartEditorHitsoundsEnabledPlayer(get, set):Bool;
public var chartEditorHitsoundVolumeOpponent(get, set):Float;
function get_chartEditorHitsoundsEnabledPlayer():Bool
function get_chartEditorHitsoundVolumeOpponent():Float
{
if (this.optionsChartEditor.hitsoundsEnabledPlayer == null) this.optionsChartEditor.hitsoundsEnabledPlayer = true;
if (data.optionsChartEditor.hitsoundVolumeOpponent == null) data.optionsChartEditor.hitsoundVolumeOpponent = 1.0;
return this.optionsChartEditor.hitsoundsEnabledPlayer;
return data.optionsChartEditor.hitsoundVolumeOpponent;
}
function set_chartEditorHitsoundsEnabledPlayer(value:Bool):Bool
function set_chartEditorHitsoundVolumeOpponent(value:Float):Float
{
// Set and apply.
this.optionsChartEditor.hitsoundsEnabledPlayer = value;
data.optionsChartEditor.hitsoundVolumeOpponent = value;
flush();
return this.optionsChartEditor.hitsoundsEnabledPlayer;
}
public var chartEditorHitsoundsEnabledOpponent(get, set):Bool;
function get_chartEditorHitsoundsEnabledOpponent():Bool
{
if (this.optionsChartEditor.hitsoundsEnabledOpponent == null) this.optionsChartEditor.hitsoundsEnabledOpponent = true;
return this.optionsChartEditor.hitsoundsEnabledOpponent;
}
function set_chartEditorHitsoundsEnabledOpponent(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.hitsoundsEnabledOpponent = value;
flush();
return this.optionsChartEditor.hitsoundsEnabledOpponent;
return data.optionsChartEditor.hitsoundVolumeOpponent;
}
public var chartEditorThemeMusic(get, set):Bool;
function get_chartEditorThemeMusic():Bool
{
if (this.optionsChartEditor.themeMusic == null) this.optionsChartEditor.themeMusic = true;
if (data.optionsChartEditor.themeMusic == null) data.optionsChartEditor.themeMusic = true;
return this.optionsChartEditor.themeMusic;
return data.optionsChartEditor.themeMusic;
}
function set_chartEditorThemeMusic(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.themeMusic = value;
data.optionsChartEditor.themeMusic = value;
flush();
return this.optionsChartEditor.themeMusic;
return data.optionsChartEditor.themeMusic;
}
public var chartEditorPlaybackSpeed(get, set):Float;
function get_chartEditorPlaybackSpeed():Float
{
if (this.optionsChartEditor.playbackSpeed == null) this.optionsChartEditor.playbackSpeed = 1.0;
if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 1.0;
return this.optionsChartEditor.playbackSpeed;
return data.optionsChartEditor.playbackSpeed;
}
function set_chartEditorPlaybackSpeed(value:Float):Float
{
// Set and apply.
this.optionsChartEditor.playbackSpeed = value;
data.optionsChartEditor.playbackSpeed = value;
flush();
return this.optionsChartEditor.playbackSpeed;
return data.optionsChartEditor.playbackSpeed;
}
/**
@@ -396,11 +393,11 @@ abstract Save(RawSaveData)
*/
public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null<SaveScoreData>
{
var level = this.scores.levels.get(levelId);
var level = data.scores.levels.get(levelId);
if (level == null)
{
level = [];
this.scores.levels.set(levelId, level);
data.scores.levels.set(levelId, level);
}
return level.get(difficultyId);
@@ -411,11 +408,11 @@ abstract Save(RawSaveData)
*/
public function setLevelScore(levelId:String, difficultyId:String, score:SaveScoreData):Void
{
var level = this.scores.levels.get(levelId);
var level = data.scores.levels.get(levelId);
if (level == null)
{
level = [];
this.scores.levels.set(levelId, level);
data.scores.levels.set(levelId, level);
}
level.set(difficultyId, score);
@@ -424,11 +421,11 @@ abstract Save(RawSaveData)
public function isLevelHighScore(levelId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
{
var level = this.scores.levels.get(levelId);
var level = data.scores.levels.get(levelId);
if (level == null)
{
level = [];
this.scores.levels.set(levelId, level);
data.scores.levels.set(levelId, level);
}
var currentScore = level.get(difficultyId);
@@ -467,11 +464,11 @@ abstract Save(RawSaveData)
*/
public function getSongScore(songId:String, difficultyId:String = 'normal'):Null<SaveScoreData>
{
var song = this.scores.songs.get(songId);
var song = data.scores.songs.get(songId);
if (song == null)
{
song = [];
this.scores.songs.set(songId, song);
data.scores.songs.set(songId, song);
}
return song.get(difficultyId);
}
@@ -481,11 +478,11 @@ abstract Save(RawSaveData)
*/
public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void
{
var song = this.scores.songs.get(songId);
var song = data.scores.songs.get(songId);
if (song == null)
{
song = [];
this.scores.songs.set(songId, song);
data.scores.songs.set(songId, song);
}
song.set(difficultyId, score);
@@ -501,11 +498,11 @@ abstract Save(RawSaveData)
*/
public function isSongHighScore(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
{
var song = this.scores.songs.get(songId);
var song = data.scores.songs.get(songId);
if (song == null)
{
song = [];
this.scores.songs.set(songId, song);
data.scores.songs.set(songId, song);
}
var currentScore = song.get(difficultyId);
@@ -546,9 +543,9 @@ abstract Save(RawSaveData)
switch (inputType)
{
case Keys:
return (playerId == 0) ? this.options.controls.p1.keyboard : this.options.controls.p2.keyboard;
return (playerId == 0) ? data.options.controls.p1.keyboard : data.options.controls.p2.keyboard;
case Gamepad(_):
return (playerId == 0) ? this.options.controls.p1.gamepad : this.options.controls.p2.gamepad;
return (playerId == 0) ? data.options.controls.p1.gamepad : data.options.controls.p2.gamepad;
}
}
@@ -566,20 +563,20 @@ abstract Save(RawSaveData)
case Keys:
if (playerId == 0)
{
this.options.controls.p1.keyboard = controls;
data.options.controls.p1.keyboard = controls;
}
else
{
this.options.controls.p2.keyboard = controls;
data.options.controls.p2.keyboard = controls;
}
case Gamepad(_):
if (playerId == 0)
{
this.options.controls.p1.gamepad = controls;
data.options.controls.p1.gamepad = controls;
}
else
{
this.options.controls.p2.gamepad = controls;
data.options.controls.p2.gamepad = controls;
}
}
@@ -600,6 +597,36 @@ abstract Save(RawSaveData)
}
}
/**
* The user's current volume setting.
*/
public var volume(get, set):Float;
function get_volume():Float
{
return data.volume;
}
function set_volume(value:Float):Float
{
return data.volume = value;
}
/**
* Whether the user's volume is currently muted.
*/
public var mute(get, set):Bool;
function get_mute():Bool
{
return data.mute;
}
function set_mute(value:Bool):Bool
{
return data.mute = value;
}
/**
* Call this to make sure the save data is written to disk.
*/
@@ -625,17 +652,22 @@ abstract Save(RawSaveData)
if (legacySaveData != null)
{
trace('[SAVE] Found legacy save data, converting...');
FlxG.save.mergeData(SaveDataMigrator.migrateFromLegacy(legacySaveData));
var gameSave = SaveDataMigrator.migrate(legacySaveData);
@:privateAccess
FlxG.save.mergeData(gameSave.data);
}
else
{
trace('[SAVE] No legacy save data found.');
}
}
else
{
trace('[SAVE] Loaded save data.');
FlxG.save.mergeData(SaveDataMigrator.migrate(FlxG.save.data));
@:privateAccess
var gameSave = SaveDataMigrator.migrate(FlxG.save.data);
FlxG.save.mergeData(gameSave.data);
}
trace('[SAVE] Done loading save data.');
trace(FlxG.save.data);
}
static function fetchLegacySaveData():Null<RawSaveData_v1_0_0>
@@ -924,12 +956,6 @@ typedef SaveControlsData =
*/
var ?CUTSCENE_ADVANCE:Array<Int>;
/**
* Keybind for skipping a cutscene.
* @default `Escape`
*/
var ?CUTSCENE_SKIP:Array<Int>;
/**
* Keybind for increasing volume.
* @default `Plus`
@@ -1004,10 +1030,16 @@ typedef SaveDataChartEditorOptions =
var ?metronomeVolume:Float;
/**
* Hitsound volume in the Chart Editor.
* Hitsound volume (player) in the Chart Editor.
* @default `1.0`
*/
var ?hitsoundVolume:Float;
var ?hitsoundVolumePlayer:Float;
/**
* Hitsound volume (opponent) in the Chart Editor.
* @default `1.0`
*/
var ?hitsoundVolumeOpponent:Float;
/**
* If true, playtest songs from the current position in the Chart Editor.
@@ -1015,18 +1047,6 @@ typedef SaveDataChartEditorOptions =
*/
var ?playtestStartTime:Bool;
/**
* Player note hit sounds in the Chart Editor.
* @default `true`
*/
var ?hitsoundsEnabledPlayer:Bool;
/**
* Opponent note hit sounds in the Chart Editor.
* @default `true`
*/
var ?hitsoundsEnabledOpponent:Bool;
/**
* Theme music in the Chart Editor.
* @default `true`
@@ -35,7 +35,6 @@ typedef SaveControlsData_v1_0_0 =
var ?ACCEPT:Array<Int>;
var ?BACK:Array<Int>;
var ?CUTSCENE_ADVANCE:Array<Int>;
var ?CUTSCENE_SKIP:Array<Int>;
var ?NOTE_DOWN:Array<Int>;
var ?NOTE_LEFT:Array<Int>;
var ?NOTE_RIGHT:Array<Int>;
@@ -19,21 +19,21 @@ class SaveDataMigrator
{
trace('[SAVE] No version found in save data! Returning blank data.');
trace(inputData);
return new Save();
return new Save(Save.getDefault());
}
else
{
if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
{
// Simply cast the structured data.
var save:Save = inputData;
// Simply import the structured data.
var save:Save = new Save(inputData);
return save;
}
else
{
trace('[SAVE] Invalid save data version! Returning blank data.');
trace(inputData);
return new Save();
return new Save(Save.getDefault());
}
}
}
@@ -45,7 +45,7 @@ class SaveDataMigrator
{
var inputSaveData:RawSaveData_v1_0_0 = cast inputData;
var result:Save = new Save();
var result:Save = new Save(Save.getDefault());
result.volume = inputSaveData.volume;
result.mute = inputSaveData.mute;
@@ -272,7 +272,6 @@ class SaveDataMigrator
ACCEPT: controlsData?.keys?.ACCEPT ?? null,
BACK: controlsData?.keys?.BACK ?? null,
CUTSCENE_ADVANCE: controlsData?.keys?.CUTSCENE_ADVANCE ?? null,
CUTSCENE_SKIP: controlsData?.keys?.CUTSCENE_SKIP ?? null,
NOTE_DOWN: controlsData?.keys?.NOTE_DOWN ?? null,
NOTE_LEFT: controlsData?.keys?.NOTE_LEFT ?? null,
NOTE_RIGHT: controlsData?.keys?.NOTE_RIGHT ?? null,
@@ -293,7 +292,6 @@ class SaveDataMigrator
ACCEPT: controlsData?.pad?.ACCEPT ?? null,
BACK: controlsData?.pad?.BACK ?? null,
CUTSCENE_ADVANCE: controlsData?.pad?.CUTSCENE_ADVANCE ?? null,
CUTSCENE_SKIP: controlsData?.pad?.CUTSCENE_SKIP ?? null,
NOTE_DOWN: controlsData?.pad?.NOTE_DOWN ?? null,
NOTE_LEFT: controlsData?.pad?.NOTE_LEFT ?? null,
NOTE_RIGHT: controlsData?.pad?.NOTE_RIGHT ?? null,
+11 -2
View File
@@ -1,6 +1,6 @@
package funkin.ui;
import flixel.addons.transition.FlxTransitionableSubState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxSubState;
import flixel.text.FlxText;
import funkin.ui.mainmenu.MainMenuState;
@@ -20,7 +20,16 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
{
public var leftWatermarkText:FlxText = null;
public var rightWatermarkText:FlxText = null;
public var conductorInUse:Conductor = Conductor.instance;
public var conductorInUse(get, default):Conductor;
var _conductorInUse:Null<Conductor>;
function get_conductorInUse():Conductor
{
if (_conductorInUse == null) return Conductor.instance;
return _conductorInUse;
}
public function new(bgColor:FlxColor = FlxColor.TRANSPARENT)
{
+2 -1
View File
@@ -1,5 +1,6 @@
package funkin.ui.debug;
import funkin.util.MemoryUtil;
import openfl.text.TextFormat;
import openfl.system.System;
import openfl.text.TextField;
@@ -35,7 +36,7 @@ class MemoryCounter extends TextField
@:noCompletion
#if !flash override #end function __enterFrame(deltaTime:Float):Void
{
var mem:Float = Math.round(System.totalMemory / BYTES_PER_MEG / ROUND_TO) * ROUND_TO;
var mem:Float = Math.round(MemoryUtil.getMemoryUsed() / BYTES_PER_MEG / ROUND_TO) * ROUND_TO;
if (mem > memPeak) memPeak = mem;
@@ -40,6 +40,7 @@ import openfl.utils.ByteArray;
import funkin.input.Cursor;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.util.SortUtil;
import haxe.ui.core.Screen;
using flixel.util.FlxSpriteUtil;
@@ -75,21 +76,19 @@ class DebugBoundingState extends FlxState
var uiStuff:Component;
var haxeUIFocused(get, default):Bool = false;
function get_haxeUIFocused():Bool
{
// get the screen position, according to the HUD camera, temp default to FlxG.camera juuust in case?
var hudMousePos:FlxPoint = FlxG.mouse.getScreenPosition(hudCam ?? FlxG.camera);
return Screen.instance.hasSolidComponentUnderPoint(hudMousePos.x, hudMousePos.y);
}
override function create()
{
Paths.setCurrentLevel('week1');
var str = Paths.xml('ui/animation-editor/offset-editor-view');
uiStuff = RuntimeComponentBuilder.fromAsset(str);
// uiStuff.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET;
var dropdown:DropDown = cast uiStuff.findComponent("swapper");
dropdown.onChange = function(e:UIEvent) {
trace(e.type);
curView = cast e.data.curView;
trace(e.data);
// trace(e.data);
};
// lv.
// lv.onChange = function(e:UIEvent)
// {
@@ -106,23 +105,40 @@ class DebugBoundingState extends FlxState
hudCam = new FlxCamera();
hudCam.bgColor.alpha = 0;
FlxG.cameras.add(hudCam, false);
bg = FlxGridOverlay.create(10, 10);
// bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.GREEN);
bg.scrollFactor.set();
add(bg);
initSpritesheetView();
initOffsetView();
// we are setting this as the default draw camera only temporarily, to trick haxeui
FlxG.cameras.add(hudCam);
Cursor.show();
var str = Paths.xml('ui/animation-editor/offset-editor-view');
uiStuff = RuntimeComponentBuilder.fromAsset(str);
// uiStuff.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET;
var dropdown:DropDown = cast uiStuff.findComponent("swapper");
dropdown.onChange = function(e:UIEvent) {
trace(e.type);
curView = cast e.data.curView;
trace(e.data);
// trace(e.data);
};
uiStuff.cameras = [hudCam];
add(uiStuff);
// sets the default camera back to FlxG.camera, since we set it to hudCamera for haxeui stuf
FlxG.cameras.setDefaultDrawTarget(FlxG.camera, true);
FlxG.cameras.setDefaultDrawTarget(hudCam, false);
initSpritesheetView();
initOffsetView();
Cursor.show();
super.create();
}
@@ -359,7 +375,7 @@ class DebugBoundingState extends FlxState
offsetView.visible = true;
offsetView.active = true;
offsetControls();
mouseOffsetMovement();
if (!haxeUIFocused) mouseOffsetMovement();
}
if (FlxG.keys.justPressed.H) hudCam.visible = !hudCam.visible;
@@ -367,7 +383,7 @@ class DebugBoundingState extends FlxState
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
MouseUtil.mouseCamDrag();
MouseUtil.mouseWheelZoom();
if (!haxeUIFocused) MouseUtil.mouseWheelZoom();
// bg.scale.x = FlxG.camera.zoom;
// bg.scale.y = FlxG.camera.zoom;
@@ -310,10 +310,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
this.songLengthInMs = value;
// Make sure playhead doesn't go outside the song.
if (playheadPositionInMs > songLengthInMs) playheadPositionInMs = songLengthInMs;
onSongLengthChanged();
updateGridHeight();
return this.songLengthInMs;
}
@@ -595,11 +592,22 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var playtestPracticeMode:Bool = false;
/**
* If true, playtesting a chart will make the computer do it for you!
*/
var playtestBotPlayMode:Bool = false;
/**
* Enables or disables the "debugger" popup that appears when you run into a flixel error.
*/
var enabledDebuggerPopup:Bool = true;
/**
* Whether song scripts should be enabled during playtesting.
* You should probably check the box if the song has custom mechanics.
*/
var playtestSongScripts:Bool = true;
// Visuals
/**
@@ -704,19 +712,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var metronomeVolume:Float = 1.0;
/**
* The volume to play hitsounds at.
* The volume to play the player's hitsounds at.
*/
var hitsoundVolume:Float = 1.0;
var hitsoundVolumePlayer:Float = 1.0;
/**
* Whether hitsounds are enabled for the player.
* The volume to play the opponent's hitsounds at.
*/
var hitsoundsEnabledPlayer:Bool = true;
/**
* Whether hitsounds are enabled for the opponent.
*/
var hitsoundsEnabledOpponent:Bool = true;
var hitsoundVolumeOpponent:Float = 1.0;
/**
* Whether hitsounds are enabled for at least one character.
@@ -725,7 +728,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_hitsoundsEnabled():Bool
{
return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent;
return hitsoundVolumePlayer + hitsoundVolumeOpponent > 0;
}
// Auto-save
@@ -875,6 +878,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var noteDisplayDirty:Bool = true;
var noteTooltipsDirty:Bool = true;
/**
* Whether the selected charactesr have been modified and the health icons need to be updated.
*/
@@ -928,12 +933,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_shouldShowBackupAvailableDialog():Bool
{
return Save.get().chartEditorHasBackup;
return Save.instance.chartEditorHasBackup;
}
function set_shouldShowBackupAvailableDialog(value:Bool):Bool
{
return Save.get().chartEditorHasBackup = value;
return Save.instance.chartEditorHasBackup = value;
}
/**
@@ -1399,7 +1404,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_currentSongId():String
{
return currentSongName.toLowerKebabCase().replace('.', '').replace(' ', '-');
return currentSongName.toLowerKebabCase().replace(' ', '-').sanitize();
}
var currentSongArtist(get, set):String;
@@ -1538,6 +1543,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Make sure view is updated when the variation changes.
noteDisplayDirty = true;
notePreviewDirty = true;
noteTooltipsDirty = true;
notePreviewViewportBoundsDirty = true;
switchToCurrentInstrumental();
@@ -1559,6 +1565,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Make sure view is updated when the difficulty changes.
noteDisplayDirty = true;
notePreviewDirty = true;
noteTooltipsDirty = true;
notePreviewViewportBoundsDirty = true;
// Make sure the difficulty we selected is in the list of difficulties.
@@ -1757,30 +1764,30 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var menubarItemVolumeMetronome:Slider;
/**
* The `Audio -> Enable Player Hitsounds` menu checkbox.
*/
var menubarItemPlayerHitsounds:MenuCheckBox;
/**
* The `Audio -> Enable Opponent Hitsounds` menu checkbox.
*/
var menubarItemOpponentHitsounds:MenuCheckBox;
/**
* The `Audio -> Play Theme Music` menu checkbox.
*/
var menubarItemThemeMusic:MenuCheckBox;
/**
* The `Audio -> Hitsound Volume` label.
* The `Audio -> Player Hitsound Volume` label.
*/
var menubarLabelVolumeHitsounds:Label;
var menubarLabelVolumeHitsoundPlayer:Label;
/**
* The `Audio -> Hitsound Volume` slider.
* The `Audio -> Enemy Hitsound Volume` label.
*/
var menubarItemVolumeHitsounds:Slider;
var menubarLabelVolumeHitsoundOpponent:Label;
/**
* The `Audio -> Player Hitsound Volume` slider.
*/
var menubarItemVolumeHitsoundPlayer:Slider;
/**
* The `Audio -> Enemy Hitsound Volume` slider.
*/
var menubarItemVolumeHitsoundOpponent:Slider;
/**
* The `Audio -> Instrumental Volume` label.
@@ -2171,7 +2178,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
public function loadPreferences():Void
{
var save:Save = Save.get();
var save:Save = Save.instance;
if (previousWorkingFilePaths[0] == null)
{
@@ -2187,9 +2194,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
playtestStartTime = save.chartEditorPlaytestStartTime;
currentTheme = save.chartEditorTheme;
metronomeVolume = save.chartEditorMetronomeVolume;
hitsoundVolume = save.chartEditorHitsoundVolume;
hitsoundsEnabledPlayer = save.chartEditorHitsoundsEnabledPlayer;
hitsoundsEnabledOpponent = save.chartEditorHitsoundsEnabledOpponent;
hitsoundVolumePlayer = save.chartEditorHitsoundVolumePlayer;
hitsoundVolumePlayer = save.chartEditorHitsoundVolumeOpponent;
this.welcomeMusic.active = save.chartEditorThemeMusic;
// audioInstTrack.volume = save.chartEditorInstVolume;
@@ -2200,7 +2206,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
public function writePreferences(hasBackup:Bool):Void
{
var save:Save = Save.get();
var save:Save = Save.instance;
// Can't use filter() because of null safety checking!
var filteredWorkingFilePaths:Array<String> = [];
@@ -2217,9 +2223,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
save.chartEditorPlaytestStartTime = playtestStartTime;
save.chartEditorTheme = currentTheme;
save.chartEditorMetronomeVolume = metronomeVolume;
save.chartEditorHitsoundVolume = hitsoundVolume;
save.chartEditorHitsoundsEnabledPlayer = hitsoundsEnabledPlayer;
save.chartEditorHitsoundsEnabledOpponent = hitsoundsEnabledOpponent;
save.chartEditorHitsoundVolumePlayer = hitsoundVolumePlayer;
save.chartEditorHitsoundVolumeOpponent = hitsoundVolumeOpponent;
save.chartEditorThemeMusic = this.welcomeMusic.active;
// save.chartEditorInstVolume = audioInstTrack.volume;
@@ -2848,12 +2853,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menuBarItemInputStyleNone.onClick = function(event:UIEvent) {
currentLiveInputStyle = None;
};
menuBarItemInputStyleNone.selected = currentLiveInputStyle == None;
menuBarItemInputStyleNumberKeys.onClick = function(event:UIEvent) {
currentLiveInputStyle = NumberKeys;
};
menuBarItemInputStyleNumberKeys.selected = currentLiveInputStyle == NumberKeys;
menuBarItemInputStyleWASD.onClick = function(event:UIEvent) {
currentLiveInputStyle = WASD;
currentLiveInputStyle = WASDKeys;
};
menuBarItemInputStyleWASD.selected = currentLiveInputStyle == WASDKeys;
menubarItemAbout.onClick = _ -> this.openAboutDialog();
menubarItemWelcomeDialog.onClick = _ -> this.openWelcomeDialog(true);
@@ -2912,24 +2920,25 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
};
menubarItemVolumeMetronome.value = Std.int(metronomeVolume * 100);
menubarItemPlayerHitsounds.onChange = event -> hitsoundsEnabledPlayer = event.value;
menubarItemPlayerHitsounds.selected = hitsoundsEnabledPlayer;
menubarItemOpponentHitsounds.onChange = event -> hitsoundsEnabledOpponent = event.value;
menubarItemOpponentHitsounds.selected = hitsoundsEnabledOpponent;
menubarItemThemeMusic.onChange = event -> {
this.welcomeMusic.active = event.value;
fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION);
};
menubarItemThemeMusic.selected = this.welcomeMusic.active;
menubarItemVolumeHitsound.onChange = event -> {
menubarItemVolumeHitsoundPlayer.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0;
hitsoundVolume = volume;
menubarLabelVolumeHitsound.text = 'Hitsound - ${Std.int(event.value)}%';
hitsoundVolumePlayer = volume;
menubarLabelVolumeHitsoundPlayer.text = 'Player - ${Std.int(event.value)}%';
};
menubarItemVolumeHitsound.value = Std.int(hitsoundVolume * 100);
menubarItemVolumeHitsoundPlayer.value = Std.int(hitsoundVolumePlayer * 100);
menubarItemVolumeHitsoundOpponent.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0;
hitsoundVolumeOpponent = volume;
menubarLabelVolumeHitsoundOpponent.text = 'Enemy - ${Std.int(event.value)}%';
};
menubarItemVolumeHitsoundOpponent.value = Std.int(hitsoundVolumeOpponent * 100);
menubarItemVolumeInstrumental.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0;
@@ -2951,7 +2960,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemPlaybackSpeed.onChange = event -> {
var pitch:Float = (event.value.toFloat() * 2.0) / 100.0;
pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25.
pitch = Math.floor(pitch / 0.05) * 0.05; // Round to nearest 5%
pitch = Math.max(0.05, Math.min(2.0, pitch)); // Clamp to 5% to 200%
#if FLX_PITCH
if (audioInstTrack != null) audioInstTrack.pitch = pitch;
audioVocalTrackGroup.pitch = pitch;
@@ -3417,7 +3427,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Update the event sprite's position.
eventSprite.updateEventPosition(renderedEvents);
// Update the sprite's graphic. TODO: Is this inefficient?
eventSprite.playAnimation(eventSprite.eventData.event);
eventSprite.playAnimation(eventSprite.eventData.eventKind);
}
else
{
@@ -3657,8 +3667,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
selectionSquare.width = eventSprite.width;
selectionSquare.height = eventSprite.height;
}
// Additional cleanup on notes.
if (noteTooltipsDirty) eventSprite.updateTooltipText();
}
noteTooltipsDirty = false;
// Sort the notes DESCENDING. This keeps the sustain behind the associated note.
renderedNotes.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort()
@@ -4530,14 +4545,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// Create an event and place it in the chart.
// TODO: Figure out configuring event data.
var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.clone());
var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.copy());
performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL));
}
else
{
// Create a note and place it in the chart.
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace.clone());
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace);
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
@@ -4678,9 +4693,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, eventKindToPlace, null);
if (eventKindToPlace != eventData.event)
if (eventKindToPlace != eventData.eventKind)
{
eventData.event = eventKindToPlace;
eventData.eventKind = eventKindToPlace;
}
eventData.time = cursorSnappedMs;
@@ -4942,7 +4957,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Place notes at the playhead.
switch (currentLiveInputStyle)
{
case ChartEditorLiveInputStyle.WASD:
case ChartEditorLiveInputStyle.WASDKeys:
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
@@ -5245,6 +5260,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
// Would bind Ctrl+A and Ctrl+D here, but they are already bound to Select All and Select None.
}
else
{
trace('Ignoring keybinds for View menu items because we are in live input mode (${currentLiveInputStyle}).');
}
}
/**
@@ -5267,14 +5286,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// F1 = Open Help
if (FlxG.keys.justPressed.F1) this.openUserGuideDialog();
// DEBUG KEYBIND: Ctrl + Alt + Shift + L = Crash the game.
#if debug
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.L)
{
throw "DEBUG: Crashing the chart editor!";
}
#end
}
function handleQuickWatch():Void
@@ -5317,10 +5328,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var startTimestamp:Float = 0;
if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs;
var playbackRate:Float = ((menubarItemPlaybackSpeed.value ?? 1.0) * 2.0) / 100.0;
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
var targetSong:Song;
try
{
targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false);
targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, playtestSongScripts, false);
}
catch (e)
{
@@ -5328,9 +5343,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return;
}
LogStyle.WARNING.openConsole = enabledDebuggerPopup;
LogStyle.ERROR.openConsole = enabledDebuggerPopup;
// TODO: Rework asset system so we can remove this.
switch (currentSongStage)
{
@@ -5348,7 +5360,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
Paths.setCurrentLevel('week6');
case 'tankmanBattlefield':
Paths.setCurrentLevel('week7');
case 'phillyStreets' | 'phillyBlazin':
case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2':
Paths.setCurrentLevel('weekend1');
}
@@ -5364,8 +5376,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
targetDifficulty: selectedDifficulty,
targetVariation: selectedVariation,
practiceMode: playtestPracticeMode,
botPlayMode: playtestBotPlayMode,
minimalMode: minimal,
startTimestamp: startTimestamp,
playbackRate: playbackRate,
overrideMusic: true,
});
@@ -5511,8 +5525,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
this.switchToInstrumental(currentInstrumentalId, currentSongMetadata.playData.characters.player, currentSongMetadata.playData.characters.opponent);
}
function onSongLengthChanged():Void
public function updateGridHeight():Void
{
// Make sure playhead doesn't go outside the song after we update the grid height.
if (playheadPositionInMs > songLengthInMs) playheadPositionInMs = songLengthInMs;
if (gridTiledSprite != null)
{
gridTiledSprite.height = songLengthInPixels;
@@ -5929,7 +5946,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault());
tempNote.noteData = noteData;
tempNote.scrollFactor.set(0, 0);
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, tempNote, 1, true);
var event:NoteScriptEvent = new HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', 0);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@@ -5939,9 +5956,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
switch (noteData.getStrumlineIndex())
{
case 0: // Player
if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume);
if (hitsoundVolumePlayer > 0) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolumePlayer);
case 1: // Opponent
if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume);
if (hitsoundVolumeOpponent > 0) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolumeOpponent);
}
}
}
@@ -6139,7 +6156,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
/**
* Available input modes for the chart editor state.
* Available input modes for the chart editor state. Numbers/arrows/WASD available for other keybinds.
*/
enum ChartEditorLiveInputStyle
{
@@ -6154,9 +6171,9 @@ enum ChartEditorLiveInputStyle
NumberKeys;
/**
* WASD to place notes on opponent's side, arrow keys to place notes on player's side.
* WASD to place notes on opponent's side, Arrow keys to place notes on player's side.
*/
WASD;
WASDKeys;
}
typedef ChartEditorParams =
@@ -40,6 +40,8 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.updateGridHeight();
}
public function undo(state:ChartEditorState):Void
@@ -62,6 +64,8 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.updateGridHeight();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
@@ -34,11 +34,11 @@ class SelectItemsCommand implements ChartEditorCommand
}
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
if (this.notes.length == 0 && this.events.length >= 1)
if (this.notes.length == 0 && this.events.length == 1)
{
var eventSelected = this.events[0];
state.eventKindToPlace = eventSelected.event;
state.eventKindToPlace = eventSelected.eventKind;
// This code is here to parse event data that's not built as a struct for some reason.
// TODO: Clean this up or get rid of it.
@@ -46,7 +46,7 @@ class SelectItemsCommand implements ChartEditorCommand
var defaultKey = null;
if (eventSchema == null)
{
trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
trace('[WARNING] Event schema not found for event ${eventSelected.eventKind}.');
}
else
{
@@ -60,7 +60,7 @@ class SelectItemsCommand implements ChartEditorCommand
}
// If we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
if (this.events.length == 0 && this.notes.length >= 1)
if (this.events.length == 0 && this.notes.length == 1)
{
var noteSelected = this.notes[0];
@@ -31,11 +31,11 @@ class SetItemSelectionCommand implements ChartEditorCommand
state.currentEventSelection = events;
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
if (this.notes.length == 0 && this.events.length >= 1)
if (this.notes.length == 0 && this.events.length == 1)
{
var eventSelected = this.events[0];
state.eventKindToPlace = eventSelected.event;
state.eventKindToPlace = eventSelected.eventKind;
// This code is here to parse event data that's not built as a struct for some reason.
// TODO: Clean this up or get rid of it.
@@ -43,7 +43,7 @@ class SetItemSelectionCommand implements ChartEditorCommand
var defaultKey = null;
if (eventSchema == null)
{
trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
trace('[WARNING] Event schema not found for event ${eventSelected.eventKind}.');
}
else
{
@@ -51,13 +51,18 @@ class SetItemSelectionCommand implements ChartEditorCommand
}
var eventData = eventSelected.valueAsStruct(defaultKey);
state.eventDataToPlace = eventData;
var eventDataClone = Reflect.copy(eventData);
if (eventDataClone != null)
{
state.eventDataToPlace = eventDataClone;
}
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
}
// IF we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
if (this.events.length == 0 && this.notes.length >= 1)
if (this.events.length == 0 && this.notes.length == 1)
{
var noteSelected = this.notes[0];
@@ -133,7 +133,7 @@ class ChartEditorEventSprite extends FlxSprite
public function playAnimation(?name:String):Void
{
if (name == null) name = eventData?.event ?? DEFAULT_EVENT;
if (name == null) name = eventData?.eventKind ?? DEFAULT_EVENT;
var correctedName = correctAnimationName(name);
this.animation.play(correctedName);
@@ -160,12 +160,11 @@ class ChartEditorEventSprite extends FlxSprite
else
{
this.visible = true;
playAnimation(value.event);
playAnimation(value.eventKind);
this.eventData = value;
// Update the position to match the note data.
updateEventPosition();
// Update the tooltip text.
this.tooltip.tipData = {text: this.eventData.buildTooltip()};
updateTooltipText();
return this.eventData;
}
}
@@ -188,6 +187,13 @@ class ChartEditorEventSprite extends FlxSprite
this.updateTooltipPosition();
}
public function updateTooltipText():Void
{
if (this.eventData == null) return;
if (this.isGhost) return;
this.tooltip.tipData = {text: this.eventData.buildTooltip()};
}
public function updateTooltipPosition():Void
{
// No tooltip for ghost sprites.
@@ -58,9 +58,15 @@ class ChartEditorMeasureTicks extends FlxTypedSpriteGroup<FlxSprite>
var measureNumberInViewport = Math.floor(viewTopPosition / ChartEditorState.GRID_SIZE / Conductor.instance.stepsPerMeasure) + 1;
var measureNumberPosition = measureNumberInViewport * ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure;
measureNumber.text = '${measureNumberInViewport + 1}';
measureNumber.y = measureNumberPosition + this.y;
// Show the measure number only if it isn't beneath the end of the note grid.
// Using measureNumber + 1 because the cut-off bar at the bottom is technically a bar, but it looks bad if a measure number shows up there.
if ((measureNumberInViewport + 1) < chartEditorState.songLengthInSteps / Conductor.instance.stepsPerMeasure)
measureNumber.text = '${measureNumberInViewport + 1}';
else
measureNumber.text = '';
// trace(measureNumber.text + ' at ' + measureNumber.y);
}
@@ -7,6 +7,7 @@ import funkin.audio.FunkinSound;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.util.FileUtil;
import funkin.util.assets.SoundUtil;
import funkin.util.TimerUtil;
import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
import funkin.audio.waveform.WaveformSprite;
@@ -128,41 +129,41 @@ class ChartEditorAudioHandler
public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool
{
var perfA = haxe.Timer.stamp();
var perfA:Float = TimerUtil.start();
var result:Bool = playInstrumental(state, instId);
if (!result) return false;
var perfB = haxe.Timer.stamp();
var perfB:Float = TimerUtil.start();
stopExistingVocals(state);
var perfC = haxe.Timer.stamp();
var perfC:Float = TimerUtil.start();
result = playVocals(state, BF, playerId, instId);
var perfD = haxe.Timer.stamp();
var perfD:Float = TimerUtil.start();
// if (!result) return false;
result = playVocals(state, DAD, opponentId, instId);
// if (!result) return false;
var perfE = haxe.Timer.stamp();
var perfE:Float = TimerUtil.start();
state.hardRefreshOffsetsToolbox();
var perfF = haxe.Timer.stamp();
var perfF:Float = TimerUtil.start();
state.hardRefreshFreeplayToolbox();
var perfG = haxe.Timer.stamp();
var perfG:Float = TimerUtil.start();
trace('Switched to instrumental in ${perfB - perfA} seconds.');
trace('Stopped existing vocals in ${perfC - perfB} seconds.');
trace('Played BF vocals in ${perfD - perfC} seconds.');
trace('Played DAD vocals in ${perfE - perfD} seconds.');
trace('Hard refreshed offsets toolbox in ${perfF - perfE} seconds.');
trace('Hard refreshed freeplay toolbox in ${perfG - perfF} seconds.');
trace('Switched to instrumental in ${TimerUtil.seconds(perfA, perfB)}.');
trace('Stopped existing vocals in ${TimerUtil.seconds(perfB, perfC)}.');
trace('Played BF vocals in ${TimerUtil.seconds(perfC, perfD)}.');
trace('Played DAD vocals in ${TimerUtil.seconds(perfD, perfE)}.');
trace('Hard refreshed offsets toolbox in ${TimerUtil.seconds(perfE, perfF)}.');
trace('Hard refreshed freeplay toolbox in ${TimerUtil.seconds(perfF, perfG)}.');
return true;
}
@@ -174,10 +175,9 @@ class ChartEditorAudioHandler
{
if (instId == '') instId = 'default';
var instTrackData:Null<Bytes> = state.audioInstTrackData.get(instId);
var perfA = haxe.Timer.stamp();
var perfStart:Float = TimerUtil.start();
var instTrack:Null<FunkinSound> = SoundUtil.buildSoundFromBytes(instTrackData);
var perfB = haxe.Timer.stamp();
trace('Built instrumental track in ${perfB - perfA} seconds.');
trace('Built instrumental track in ${TimerUtil.seconds(perfStart)} seconds.');
if (instTrack == null) return false;
stopExistingInstrumental(state);
@@ -205,10 +205,9 @@ class ChartEditorAudioHandler
{
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
var vocalTrackData:Null<Bytes> = state.audioVocalTrackData.get(trackId);
var perfStart = haxe.Timer.stamp();
var perfStart:Float = TimerUtil.start();
var vocalTrack:Null<FunkinSound> = SoundUtil.buildSoundFromBytes(vocalTrackData);
var perfEnd = haxe.Timer.stamp();
trace('Built vocal track in ${perfEnd - perfStart} seconds.');
trace('Built vocal track in ${TimerUtil.seconds(perfStart)}.');
if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
@@ -219,10 +218,9 @@ class ChartEditorAudioHandler
case BF:
state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
var perfStart = haxe.Timer.stamp();
var perfStart:Float = TimerUtil.start();
var waveformData:Null<WaveformData> = vocalTrack.waveformData;
var perfEnd = haxe.Timer.stamp();
trace('Interpreted waveform data in ${perfEnd - perfStart} seconds.');
trace('Interpreted waveform data in ${TimerUtil.seconds(perfStart)}.');
if (waveformData != null)
{
@@ -246,10 +244,9 @@ class ChartEditorAudioHandler
case DAD:
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
var perfStart = haxe.Timer.stamp();
var perfStart:Float = TimerUtil.start();
var waveformData:Null<WaveformData> = vocalTrack.waveformData;
var perfEnd = haxe.Timer.stamp();
trace('Interpreted waveform data in ${perfEnd - perfStart} seconds.');
trace('Interpreted waveform data in ${TimerUtil.seconds(perfStart)}.');
if (waveformData != null)
{
@@ -299,16 +296,14 @@ class ChartEditorAudioHandler
*/
public static function playSound(_state:ChartEditorState, path:String, volume:Float = 1.0):Void
{
var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path);
if (asset == null)
{
trace('WARN: Failed to play sound $path, asset not found.');
return;
}
snd.loadEmbedded(asset);
var snd:FunkinSound = FunkinSound.load(asset);
snd.autoDestroy = true;
FlxG.sound.list.add(snd);
snd.play(true);
snd.volume = volume;
}
@@ -808,7 +808,8 @@ class ChartEditorDialogHandler
}
songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel);
#if FILE_DROP_SUPPORTED
addDropHandler(songVariationMetadataEntry, onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel));
state.addDropHandler({component: songVariationMetadataEntry, handler: onDropFileMetadataVariation.bind(variation)
.bind(songVariationMetadataEntryLabel)});
#end
chartContainerB.addComponent(songVariationMetadataEntry);
@@ -832,7 +833,11 @@ class ChartEditorDialogHandler
}
songVariationChartDataEntry.onClick = onClickChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel);
#if FILE_DROP_SUPPORTED
addDropHandler(songVariationChartDataEntry, onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel));
state.addDropHandler(
{
component: songVariationChartDataEntry,
handler: onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel)
});
#end
chartContainerB.addComponent(songVariationChartDataEntry);
}
@@ -299,6 +299,15 @@ class ChartEditorToolboxHandler
state.playtestStartTime = checkboxStartTime.selected;
};
var checkboxBotPlay:Null<CheckBox> = toolbox.findComponent('playtestBotPlayCheckbox', CheckBox);
if (checkboxBotPlay == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestBotPlayCheckbox component.';
checkboxBotPlay.selected = state.playtestBotPlayMode;
checkboxBotPlay.onClick = _ -> {
state.playtestBotPlayMode = checkboxBotPlay.selected;
};
var checkboxDebugger:Null<CheckBox> = toolbox.findComponent('playtestDebuggerCheckbox', CheckBox);
if (checkboxDebugger == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestDebuggerCheckbox component.';
@@ -309,6 +318,17 @@ class ChartEditorToolboxHandler
state.enabledDebuggerPopup = checkboxDebugger.selected;
};
var checkboxSongScripts:Null<CheckBox> = toolbox.findComponent('playtestSongScriptsCheckbox', CheckBox);
if (checkboxSongScripts == null)
throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestSongScriptsCheckbox component.';
state.playtestSongScripts = checkboxSongScripts.selected;
checkboxSongScripts.onClick = _ -> {
state.playtestSongScripts = checkboxSongScripts.selected;
};
return toolbox;
}
@@ -90,7 +90,7 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
// Edit the event data of any selected events.
for (event in chartEditorState.currentEventSelection)
{
event.event = chartEditorState.eventKindToPlace;
event.eventKind = chartEditorState.eventKindToPlace;
event.value = chartEditorState.eventDataToPlace;
}
chartEditorState.saveDataDirty = true;
@@ -237,6 +237,11 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
{
value = event.target.value.value;
}
else if (field.type == BOOL)
{
var chk:CheckBox = cast event.target;
value = cast(chk.selected, Null<Bool>); // Need to cast to nullable bool or the compiler will get mad.
}
trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}');
@@ -253,14 +258,15 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
// Edit the event data of any existing events.
if (!_initializing && chartEditorState.currentEventSelection.length > 0)
{
for (event in chartEditorState.currentEventSelection)
for (songEvent in chartEditorState.currentEventSelection)
{
event.event = chartEditorState.eventKindToPlace;
event.value = chartEditorState.eventDataToPlace;
songEvent.eventKind = chartEditorState.eventKindToPlace;
songEvent.value = Reflect.copy(chartEditorState.eventDataToPlace);
}
chartEditorState.saveDataDirty = true;
chartEditorState.noteDisplayDirty = true;
chartEditorState.notePreviewDirty = true;
chartEditorState.noteTooltipsDirty = true;
}
}
}
@@ -1,21 +1,22 @@
package funkin.ui.debug.charting.toolboxes;
import flixel.addons.display.FlxTiledSprite;
import flixel.math.FlxMath;
import funkin.audio.SoundGroup;
import funkin.audio.waveform.WaveformDataParser;
import funkin.ui.debug.charting.commands.SetFreeplayPreviewCommand;
import funkin.ui.haxeui.components.WaveformPlayer;
import funkin.ui.freeplay.FreeplayState;
import funkin.util.TimerUtil;
import haxe.ui.backend.flixel.components.SpriteWrapper;
import haxe.ui.components.Button;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.components.Label;
import flixel.addons.display.FlxTiledSprite;
import flixel.math.FlxMath;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
import haxe.ui.backend.flixel.components.SpriteWrapper;
import funkin.ui.debug.charting.commands.SetFreeplayPreviewCommand;
import funkin.ui.haxeui.components.WaveformPlayer;
import funkin.audio.waveform.WaveformDataParser;
import haxe.ui.containers.VBox;
import haxe.ui.containers.Absolute;
import haxe.ui.containers.ScrollView;
import funkin.ui.freeplay.FreeplayState;
import haxe.ui.containers.Frame;
import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent;
@@ -288,12 +289,12 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox
// Build player waveform.
// waveformMusic.waveform.forceUpdate = true;
var perfStart = haxe.Timer.stamp();
var waveformData1 = playerVoice.waveformData;
var waveformData2 = opponentVoice?.waveformData ?? playerVoice.waveformData; // this null check is for songs that only have 1 vocals file!
var perfStart:Float = TimerUtil.start();
var waveformData1 = playerVoice?.waveformData;
var waveformData2 = opponentVoice?.waveformData ?? playerVoice?.waveformData; // this null check is for songs that only have 1 vocals file!
var waveformData3 = chartEditorState.audioInstTrack.waveformData;
var waveformData = waveformData1.merge(waveformData2).merge(waveformData3);
trace('Waveform data merging took: ${haxe.Timer.stamp() - perfStart} seconds');
var waveformData = waveformData3.merge(waveformData1).merge(waveformData2);
trace('Waveform data merging took: ${TimerUtil.seconds(perfStart)}');
waveformMusic.waveform.waveformData = waveformData;
// Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
@@ -270,24 +270,21 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
// Build player waveform.
// waveformPlayer.waveform.forceUpdate = true;
waveformPlayer.waveform.waveformData = playerVoice.waveformData;
waveformPlayer.waveform.waveformData = playerVoice?.waveformData;
// Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
waveformPlayer.waveform.duration = playerVoice.length / Constants.MS_PER_SEC;
waveformPlayer.waveform.duration = (playerVoice?.length ?? 1000.0) / Constants.MS_PER_SEC;
// Build opponent waveform.
// waveformOpponent.waveform.forceUpdate = true;
// note: if song only has one set of vocals (Vocals.ogg/mp3) then this is null and crashes charting editor
// so we null check
if (opponentVoice != null)
{
waveformOpponent.waveform.waveformData = opponentVoice.waveformData;
waveformOpponent.waveform.duration = opponentVoice.length / Constants.MS_PER_SEC;
}
waveformOpponent.waveform.waveformData = opponentVoice?.waveformData;
waveformOpponent.waveform.duration = (opponentVoice?.length ?? 1000.0) / Constants.MS_PER_SEC;
// Build instrumental waveform.
// waveformInstrumental.waveform.forceUpdate = true;
waveformInstrumental.waveform.waveformData = chartEditorState.audioInstTrack.waveformData;
waveformInstrumental.waveform.duration = instTrack.length / Constants.MS_PER_SEC;
waveformInstrumental.waveform.duration = (instTrack?.length ?? 1000.0) / Constants.MS_PER_SEC;
addOffsetsToAudioPreview();
}
@@ -117,18 +117,32 @@ class ChartEditorDropdowns
"ugh" => "Ugh (Week 7)",
"hehPrettyGood" => "Heh, Pretty Good (Week 7)",
// Weekend 1
"weekend-1-lightcan" => "Light Can (2hot)",
"weekend-1-kickcan" => "Kick Can (2hot)",
"weekend-1-kneecan" => "Knee Can (2hot)",
"weekend-1-cockgun" => "Cock Gun (2hot)",
"weekend-1-firegun" => "Fire Gun (2hot)",
"weekend-1-punchlow" => "Punch Low (Blazin)",
"weekend-1-punchhigh" => "Punch High (Blazin)",
"weekend-1-punchlowblocked" => "Punch Low Blocked (Blazin)",
"weekend-1-punchhighblocked" => "Punch High Blocked (Blazin)",
"weekend-1-dodgelow" => "Dodge Low (Blazin)",
"weekend-1-blockhigh" => "Block High (Blazin)",
"weekend-1-fakeout" => "Fakeout (Blazin)",
"weekend-1-punchhigh" => "Punch High (Blazin')",
"weekend-1-punchhighdodged" => "Punch High (Dodge) (Blazin')",
"weekend-1-punchhighblocked" => "Punch High (Block) (Blazin')",
"weekend-1-punchhighspin" => "Punch High (Spin) (Blazin')",
"weekend-1-punchlow" => "Punch Low (Blazin')",
"weekend-1-punchlowdodged" => "Punch Low (Dodge) (Blazin')",
"weekend-1-punchlowblocked" => "Punch Low (Block) (Blazin')",
"weekend-1-punchlowspin" => "Punch High (Spin) (Blazin')",
"weekend-1-picouppercutprep" => "Pico Uppercut (Prep) (Blazin')",
"weekend-1-picouppercut" => "Pico Uppercut (Blazin')",
"weekend-1-blockhigh" => "Block High (Blazin')",
"weekend-1-blocklow" => "Block Low (Blazin')",
"weekend-1-blockspin" => "Block High (Spin) (Blazin')",
"weekend-1-dodgehigh" => "Dodge High (Blazin')",
"weekend-1-dodgelow" => "Dodge Low (Blazin')",
"weekend-1-dodgespin" => "Dodge High (Spin) (Blazin')",
"weekend-1-hithigh" => "Hit High (Blazin')",
"weekend-1-hitlow" => "Hit Low (Blazin')",
"weekend-1-hitspin" => "Hit High (Spin) (Blazin')",
"weekend-1-darnelluppercutprep" => "Darnell Uppercut (Prep) (Blazin')",
"weekend-1-darnelluppercut" => "Darnell Uppercut (Blazin')",
"weekend-1-idle" => "Idle (Blazin')",
"weekend-1-fakeout" => "Fakeout (Blazin')",
"weekend-1-taunt" => "Taunt (If Fakeout) (Blazin')",
"weekend-1-tauntforce" => "Taunt (Forced) (Blazin')",
"weekend-1-reversefakeout" => "Fakeout (Reverse) (Blazin')",
];
public static function populateDropdownWithNoteKinds(dropDown:DropDown, startingKindId:String):DropDownEntry
@@ -147,6 +161,8 @@ class ChartEditorDropdowns
dropDown.dataSource.add(value);
}
dropDown.dataSource.sort('id', ASCENDING);
return returnValue;
}
@@ -11,6 +11,7 @@ import funkin.data.dialogue.DialogueBoxData;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.freeplay.AlbumRegistry;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.play.cutscene.dialogue.Speaker;
@@ -36,11 +37,24 @@ class ConversationDebugState extends MusicBeatState
public override function create():Void
{
conversation = ConversationRegistry.instance.fetchEntry(conversationId);
conversation.completeCallback = onConversationComplete;
add(conversation);
super.create();
startConversation();
}
ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false));
function startConversation():Void
{
if (conversation != null) return;
conversation = ConversationRegistry.instance.fetchEntry(conversationId);
if (conversation == null) return;
if (!conversation.alive) conversation.revive();
conversation.zIndex = 1000;
add(conversation);
refresh();
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(conversation, event);
}
function onConversationComplete():Void
@@ -61,15 +75,17 @@ class ConversationDebugState extends MusicBeatState
if (conversation != null)
{
if (controls.CUTSCENE_ADVANCE) conversation.advanceConversation();
if (controls.CUTSCENE_SKIP)
if (controls.CUTSCENE_ADVANCE)
{
conversation.trySkipConversation(elapsed);
conversation.advanceConversation();
}
else
else if (controls.PAUSE)
{
conversation.trySkipConversation(-1);
conversation.kill();
remove(conversation);
conversation = null;
FlxG.switchState(() -> new ConversationDebugState());
}
}
}
@@ -49,8 +49,11 @@ class StageOffsetSubState extends HaxeUISubState
{
super.create();
var playState = PlayState.instance;
FlxG.mouse.visible = true;
PlayState.instance.pauseMusic();
playState.pauseMusic();
playState.cancelAllCameraTweens();
FlxG.camera.target = null;
setupUIListeners();
@@ -63,8 +66,8 @@ class StageOffsetSubState extends HaxeUISubState
// add(uiStuff);
PlayState.instance.persistentUpdate = true;
component.cameras = [PlayState.instance.camHUD];
playState.persistentUpdate = true;
component.cameras = [playState.camHUD];
// uiStuff.cameras = [PlayState.instance.camHUD];
// btn.cameras = [PlayState.instance.camHUD];
@@ -72,7 +75,7 @@ class StageOffsetSubState extends HaxeUISubState
var layerList:ListView = findComponent("prop-layers");
for (thing in PlayState.instance.currentStage)
for (thing in playState.currentStage)
{
var prop:StageProp = cast thing;
if (prop != null && prop.name != null)
+89
View File
@@ -0,0 +1,89 @@
package funkin.ui.freeplay;
import funkin.data.freeplay.AlbumData;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.IRegistryEntry;
import flixel.graphics.FlxGraphic;
/**
* A class representing the data for an album as displayed in Freeplay.
*/
class Album implements IRegistryEntry<AlbumData>
{
/**
* The internal ID for this album.
*/
public final id:String;
/**
* The full data for an album.
*/
public final _data:AlbumData;
public function new(id:String)
{
this.id = id;
this._data = _fetchData(id);
if (_data == null)
{
throw 'Could not parse album data for id: $id';
}
}
/**
* Return the name of the album.
* @
*/
public function getAlbumName():String
{
return _data.name;
}
/**
* Return the artists of the album.
* @return The list of artists
*/
public function getAlbumArtists():Array<String>
{
return _data.artists;
}
/**
* Get the asset key for the album art.
* @return The asset key
*/
public function getAlbumArtAssetKey():String
{
return _data.albumArtAsset;
}
/**
* Get the album art as a graphic, ready to apply to a sprite.
* @return The built graphic
*/
public function getAlbumArtGraphic():FlxGraphic
{
return FlxG.bitmap.add(Paths.image(getAlbumArtAssetKey()));
}
/**
* Get the asset key for the album title.
*/
public function getAlbumTitleAssetKey():String
{
return _data.albumTitleAsset;
}
public function toString():String
{
return 'Album($id)';
}
public function destroy():Void {}
static function _fetchData(id:String):Null<AlbumData>
{
return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id));
}
}
+192
View File
@@ -0,0 +1,192 @@
package funkin.ui.freeplay;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.util.FlxSort;
import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;
import flixel.tweens.FlxEase;
import funkin.data.freeplay.AlbumRegistry;
import funkin.graphics.FunkinSprite;
import funkin.util.SortUtil;
import openfl.utils.Assets;
/**
* The graphic for the album roll in the FreeplayState.
* Simply set `albumID` to fetch the required data and update the textures.
*/
class AlbumRoll extends FlxSpriteGroup
{
/**
* The ID of the album to display.
* Modify this value to automatically update the album art and title.
*/
public var albumId(default, set):String;
function set_albumId(value:String):String
{
if (this.albumId != value)
{
this.albumId = value;
updateAlbum();
}
return value;
}
var albumArt:FunkinSprite;
var albumTitle:FunkinSprite;
var difficultyStars:DifficultyStars;
var _exitMovers:Null<FreeplayState.ExitMoverData>;
var albumData:Album;
public function new()
{
super();
albumTitle = new FunkinSprite(947, 491);
albumTitle.visible = true;
albumTitle.zIndex = 200;
add(albumTitle);
difficultyStars = new DifficultyStars(140, 39);
difficultyStars.stars.visible = true;
albumTitle.visible = false;
// albumArtist.visible = false;
// var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
}
/**
* Load the album data by ID and update the textures.
*/
function updateAlbum():Void
{
albumData = AlbumRegistry.instance.fetchEntry(albumId);
if (albumData == null)
{
FlxG.log.warn('Could not find album data for album ID: ${albumId}');
return;
};
if (albumArt != null)
{
FlxTween.cancelTweensOf(albumArt);
albumArt.visible = false;
albumArt.destroy();
remove(albumArt);
}
// Paths.animateAtlas('freeplay/albumRoll'),
albumArt = FunkinSprite.create(1500, 360, albumData.getAlbumArtAssetKey());
albumArt.setGraphicSize(262, 262); // Magic number for size IG
albumArt.zIndex = 100;
// playIntro();
add(albumArt);
applyExitMovers();
if (Assets.exists(Paths.image(albumData.getAlbumTitleAssetKey())))
{
albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey()));
}
else
{
albumTitle.visible = false;
}
refresh();
}
public function refresh():Void
{
sort(SortUtil.byZIndex, FlxSort.ASCENDING);
}
/**
* Apply exit movers for the album roll.
* @param exitMovers The exit movers to apply.
*/
public function applyExitMovers(?exitMovers:FreeplayState.ExitMoverData):Void
{
if (exitMovers == null)
{
exitMovers = _exitMovers;
}
else
{
_exitMovers = exitMovers;
}
if (exitMovers == null) return;
exitMovers.set([albumArt],
{
x: FlxG.width,
speed: 0.4,
wait: 0
});
exitMovers.set([albumTitle],
{
x: FlxG.width,
speed: 0.2,
wait: 0.1
});
/*
exitMovers.set([albumArtist],
{
x: FlxG.width * 1.1,
speed: 0.2,
wait: 0.2
});
*/
exitMovers.set([difficultyStars],
{
x: FlxG.width * 1.2,
speed: 0.2,
wait: 0.3
});
}
/**
* Play the intro animation on the album art.
*/
public function playIntro():Void
{
albumArt.visible = true;
FlxTween.tween(albumArt, {x: 950, y: 320, angle: -340}, 0.5, {ease: FlxEase.elasticOut});
albumTitle.visible = false;
new FlxTimer().start(0.75, function(_) {
showTitle();
});
}
public function setDifficultyStars(?difficulty:Int):Void
{
if (difficulty == null) return;
difficultyStars.difficulty = difficulty;
}
public function showTitle():Void
{
albumTitle.visible = true;
}
/**
* Make the album stars visible.
*/
public function showStars():Void
{
// albumArtist.visible = false;
difficultyStars.stars.visible = false;
}
}
+1 -3
View File
@@ -4,7 +4,7 @@ import flixel.FlxSprite;
import flixel.util.FlxSignal;
import funkin.util.assets.FlxAnimationUtil;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.system.FlxSound;
import flixel.sound.FlxSound;
import flixel.util.FlxTimer;
import funkin.audio.FlxStreamSound;
@@ -156,8 +156,6 @@ class DJBoyfriend extends FlxAtlasSprite
function setupAnimations():Void
{
// frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk'));
// animation.addByPrefix('intro', "boyfriend dj intro", 24, false);
addOffset('boyfriend dj intro', 8, 3);
+1 -1
View File
@@ -23,7 +23,7 @@ class FreeplayFlames extends FlxSpriteGroup
{
var flame:FlxSprite = new FlxSprite(flameX + (flameSpreadX * i), flameY + (flameSpreadY * i));
flame.frames = Paths.getSparrowAtlas("freeplay/freeplayFlame");
flame.animation.addByPrefix("flame", "fire loop", FlxG.random.int(23, 25), false);
flame.animation.addByPrefix("flame", "fire loop full instance 1", FlxG.random.int(23, 25), false);
flame.animation.play("flame");
flame.visible = false;
flameCount = 0;

Some files were not shown because too many files have changed in this diff Show More