mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2025-04-21 19:31:52 -04:00
Merge branch 'rewrite/master' into a-bot-bars
This commit is contained in:
commit
1808f08554
67 changed files with 1338 additions and 665 deletions
.github
.gitignore.prettierignore.vscode
assetscheckstyle.jsonhmm.jsonhxformat.jsonsource
Postbuild.hxPrebuild.hx
funkin
Highscore.hxInitState.hxPlayerSettings.hxPreferences.hx
api/newgrounds
audio
data
graphics/adobeanimate
import.hxmodding
play
GameOverSubState.hxPauseSubState.hxPlayState.hxResultState.hx
character
components
cutscene/dialogue
notes
song
stage
save
ui
util
39
.github/actions/setup-haxeshit/action.yml
vendored
39
.github/actions/setup-haxeshit/action.yml
vendored
|
@ -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:
|
||||
|
|
113
.github/workflows/build-shit.yml
vendored
113
.github/workflows/build-shit.yml
vendored
|
@ -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: [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 -DNO_REDIRECT_ASSETS_FOLDER -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"
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,3 +6,4 @@ dump/
|
|||
export/
|
||||
RECOVER_*.fla
|
||||
shitAudio/
|
||||
.build_time
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# Ignore artifacts
|
||||
export
|
||||
|
||||
# Ignore all asset files (including FlxAnimate JSONs)
|
||||
assets
|
||||
# Ignore all JSONS in the images folder (including FlxAnimate JSONs)
|
||||
assets/preload/images
|
||||
assets/shared/images
|
||||
|
||||
# Don't ignore data files
|
||||
!assets/preload/data
|
||||
# TODO: These don't work.
|
||||
!assets/preload/data/
|
||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -71,6 +71,7 @@
|
|||
|
||||
"haxe.displayPort": "auto",
|
||||
"haxe.enableCompilationServer": false,
|
||||
"haxe.enableServerView": true,
|
||||
"haxe.displayServer": {
|
||||
"arguments": ["-v"]
|
||||
},
|
||||
|
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit 0b211e60f17e195041e58ed142e8040729982b28
|
||||
Subproject commit f550436bfed37f2e6efb1b97edd66a5c840bd262
|
|
@ -23,10 +23,10 @@
|
|||
},
|
||||
{
|
||||
"props": {
|
||||
"allowSingleArgParens": false,
|
||||
"allowReturn": false,
|
||||
"allowFunction": false,
|
||||
"allowCurlyBody": false
|
||||
"allowSingleArgParens": true,
|
||||
"allowReturn": true,
|
||||
"allowFunction": true,
|
||||
"allowCurlyBody": true
|
||||
},
|
||||
"type": "ArrowFunction"
|
||||
},
|
||||
|
@ -80,7 +80,7 @@
|
|||
"props": {
|
||||
"ignoreExtern": true,
|
||||
"format": "^[A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$",
|
||||
"tokens": []
|
||||
"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",
|
||||
|
|
2
hmm.json
2
hmm.json
|
@ -153,7 +153,7 @@
|
|||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d5a3b89",
|
||||
"ref": "be712450e5d3ba446008884921bb56873b299a64",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,37 @@
|
|||
package source; // Yeah, I know...
|
||||
|
||||
import sys.FileSystem;
|
||||
import sys.io.File;
|
||||
|
||||
class Postbuild
|
||||
{
|
||||
static inline final buildTimeFile = '.build_time';
|
||||
|
||||
static function main()
|
||||
{
|
||||
trace('Postbuild');
|
||||
printBuildTime();
|
||||
}
|
||||
|
||||
// TODO: Maybe put a 'Build took X seconds' message here?
|
||||
static function printBuildTime()
|
||||
{
|
||||
// get buildEnd before fs operations since they are blocking
|
||||
var end:Float = Sys.time();
|
||||
if (FileSystem.exists(buildTimeFile))
|
||||
{
|
||||
var fi = File.read(buildTimeFile);
|
||||
var start:Float = fi.readDouble();
|
||||
fi.close();
|
||||
|
||||
sys.FileSystem.deleteFile(buildTimeFile);
|
||||
|
||||
var buildTime = roundToTwoDecimals(end - start);
|
||||
|
||||
trace('Build took: ${buildTime} seconds');
|
||||
}
|
||||
}
|
||||
|
||||
private static function roundToTwoDecimals(value:Float):Float
|
||||
{
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,22 @@
|
|||
package source; // Yeah, I know...
|
||||
|
||||
import sys.io.File;
|
||||
|
||||
class Prebuild
|
||||
{
|
||||
static inline final buildTimeFile = '.build_time';
|
||||
|
||||
static function main()
|
||||
{
|
||||
trace('Prebuild');
|
||||
saveBuildTime();
|
||||
trace('Building...');
|
||||
}
|
||||
|
||||
static function saveBuildTime()
|
||||
{
|
||||
var fo = File.write(buildTimeFile);
|
||||
var now = Sys.time();
|
||||
fo.writeDouble(now);
|
||||
fo.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,9 @@ import funkin.modding.module.ModuleHandler;
|
|||
import funkin.ui.title.TitleState;
|
||||
import funkin.util.CLIUtil;
|
||||
import funkin.util.CLIUtil.CLIParams;
|
||||
import funkin.util.tools.TimerTools;
|
||||
import funkin.ui.transition.LoadingState;
|
||||
import funkin.util.TrackerUtil;
|
||||
#if discord_rpc
|
||||
import Discord.DiscordClient;
|
||||
#end
|
||||
|
@ -66,7 +68,7 @@ class InitState extends FlxState
|
|||
/**
|
||||
* Setup a bunch of important Flixel stuff.
|
||||
*/
|
||||
function setupShit()
|
||||
function setupShit():Void
|
||||
{
|
||||
//
|
||||
// GAME SETUP
|
||||
|
@ -94,7 +96,7 @@ class InitState extends FlxState
|
|||
#if (debug || FORCE_DEBUG_VERSION)
|
||||
// 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!
|
||||
|
@ -219,7 +221,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 = TimerTools.start();
|
||||
SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
|
||||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
|
@ -236,9 +238,7 @@ class InitState extends FlxState
|
|||
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: ${TimerTools.ms(perfStart)}');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -84,9 +84,9 @@ class PlayerSettings
|
|||
function addKeyboard():Void
|
||||
{
|
||||
var useDefault = true;
|
||||
if (Save.get().hasControls(id, Keys))
|
||||
if (Save.instance.hasControls(id, Keys))
|
||||
{
|
||||
var keyControlData = Save.get().getControls(id, Keys);
|
||||
var keyControlData = Save.instance.getControls(id, Keys);
|
||||
trace("keyControlData: " + haxe.Json.stringify(keyControlData));
|
||||
useDefault = false;
|
||||
controls.fromSaveData(keyControlData, Keys);
|
||||
|
@ -112,9 +112,9 @@ class PlayerSettings
|
|||
function addGamepad(gamepad:FlxGamepad)
|
||||
{
|
||||
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));
|
||||
var padControlData = Save.instance.getControls(id, Gamepad(gamepad.id));
|
||||
trace("padControlData: " + haxe.Json.stringify(padControlData));
|
||||
useDefault = false;
|
||||
controls.addGamepadWithSaveData(gamepad.id, padControlData);
|
||||
|
@ -141,7 +141,7 @@ class PlayerSettings
|
|||
if (keyData != null)
|
||||
{
|
||||
trace("saving key data: " + haxe.Json.stringify(keyData));
|
||||
Save.get().setControls(id, Keys, keyData);
|
||||
Save.instance.setControls(id, Keys, keyData);
|
||||
}
|
||||
|
||||
if (controls.gamepadsAdded.length > 0)
|
||||
|
@ -150,7 +150,7 @@ class PlayerSettings
|
|||
if (padData != null)
|
||||
{
|
||||
trace("saving pad data: " + haxe.Json.stringify(padData));
|
||||
Save.get().setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
|
||||
Save.instance.setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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.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.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.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,14 +115,14 @@ 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.instance;
|
||||
save.options.autoPause = value;
|
||||
save.flush();
|
||||
return value;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -90,6 +90,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();
|
||||
|
@ -167,8 +172,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;
|
||||
}
|
||||
|
||||
|
@ -177,7 +192,10 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
|||
*/
|
||||
override function onFocus():Void
|
||||
{
|
||||
if (!_alreadyPaused)
|
||||
// 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();
|
||||
}
|
||||
|
@ -185,6 +203,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
|||
{
|
||||
trace('Not resuming audio on focus!');
|
||||
}
|
||||
_lostFocus = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -193,6 +212,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
|||
override function onFocusLost():Void
|
||||
{
|
||||
trace('Focus lost, pausing audio!');
|
||||
_lostFocus = true;
|
||||
_alreadyPaused = _paused;
|
||||
pause();
|
||||
}
|
||||
|
@ -201,7 +221,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
|
||||
{
|
||||
|
|
|
@ -10,7 +10,6 @@ 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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package funkin.audio.waveform;
|
||||
|
||||
import funkin.util.tools.TimerTools;
|
||||
|
||||
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 = TimerTools.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 ${TimerTools.seconds(perfStart)}.');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -240,6 +240,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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -106,6 +106,8 @@ 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -82,6 +82,8 @@ 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, ?loop:Bool = false):Void
|
||||
{
|
||||
|
@ -114,11 +116,18 @@ class FlxAtlasSprite extends FlxAnimate
|
|||
}
|
||||
|
||||
anim.callback = function(_, frame:Int) {
|
||||
if (frame == (anim.getFrameLabel(id).duration - 1) + anim.getFrameLabel(id).index)
|
||||
var offset = loop ? 0 : -1;
|
||||
|
||||
if (frame == (anim.getFrameLabel(id).duration + offset) + anim.getFrameLabel(id).index)
|
||||
{
|
||||
if (loop) playAnimation(id, true, false, true);
|
||||
if (loop)
|
||||
{
|
||||
playAnimation(id, true, false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
onAnimationFinish.dispatch(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -176,7 +185,7 @@ class FlxAtlasSprite extends FlxAnimate
|
|||
public function cleanupAnimation(_:String):Void
|
||||
{
|
||||
canPlayOtherAnims = true;
|
||||
this.currentAnimation = null;
|
||||
this.anim.stop();
|
||||
// this.currentAnimation = null;
|
||||
this.anim.pause();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
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.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 +28,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 +62,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 +112,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 +164,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 +215,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 +240,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 +264,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 +293,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();
|
||||
|
|
|
@ -107,18 +107,18 @@ class NoteScriptEvent extends ScriptEvent
|
|||
public var playSound(default, default):Bool;
|
||||
|
||||
/**
|
||||
* A multiplier to the health gained or lost from this note.
|
||||
* The health gained or lost from this note.
|
||||
* This affects both hits and misses. Remember that max health is 2.00.
|
||||
*/
|
||||
public var healthMulti:Float;
|
||||
public var healthChange:Float;
|
||||
|
||||
public function new(type:ScriptEventType, note:NoteSprite, comboCount:Int = 0, cancelable:Bool = false):Void
|
||||
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.healthMulti = 1.0;
|
||||
this.healthChange = healthChange;
|
||||
}
|
||||
|
||||
public override function toString():String
|
||||
|
@ -127,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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import funkin.modding.events.ScriptEvent;
|
|||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.PlayState;
|
||||
import funkin.util.MathUtil;
|
||||
import funkin.ui.freeplay.FreeplayState;
|
||||
import funkin.ui.MusicBeatSubState;
|
||||
import funkin.ui.story.StoryMenuState;
|
||||
|
@ -82,6 +83,9 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
var transparent:Bool;
|
||||
|
||||
final CAMERA_ZOOM_DURATION:Float = 0.5;
|
||||
var targetCameraZoom:Float = 1.0;
|
||||
|
||||
public function new(params:GameOverParams)
|
||||
{
|
||||
super();
|
||||
|
@ -142,6 +146,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
FlxG.camera.target = null;
|
||||
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
|
||||
targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom();
|
||||
|
||||
//
|
||||
// Set up the audio
|
||||
|
@ -177,6 +182,9 @@ class GameOverSubState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
// Smoothly lerp the camera
|
||||
FlxG.camera.zoom = MathUtil.smoothLerp(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION);
|
||||
|
||||
//
|
||||
// Handle user inputs.
|
||||
//
|
||||
|
@ -286,6 +294,9 @@ class GameOverSubState extends MusicBeatSubState
|
|||
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();
|
||||
});
|
||||
|
@ -338,8 +349,8 @@ class GameOverSubState extends MusicBeatSubState
|
|||
}
|
||||
else
|
||||
{
|
||||
isStarting = false;
|
||||
onComplete = function() {
|
||||
isStarting = false;
|
||||
// We need to force to ensure that the non-starting music plays.
|
||||
startDeathMusic(1.0, true);
|
||||
};
|
||||
|
|
|
@ -386,7 +386,6 @@ class PauseSubState extends MusicBeatSubState
|
|||
// 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);
|
||||
trace(targetY);
|
||||
FlxTween.globalManager.cancelTweensOf(text);
|
||||
FlxTween.tween(text, {x: targetX, y: targetY}, 0.33, {ease: FlxEase.quartOut});
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ typedef PlayStateParams =
|
|||
?targetDifficulty:String,
|
||||
/**
|
||||
* The variation to play on.
|
||||
* @default `Constants.DEFAULT_VARIATION` .
|
||||
* @default `Constants.DEFAULT_VARIATION`
|
||||
*/
|
||||
?targetVariation:String,
|
||||
/**
|
||||
|
@ -111,6 +111,11 @@ typedef PlayStateParams =
|
|||
* @default `false`
|
||||
*/
|
||||
?practiceMode:Bool,
|
||||
/**
|
||||
* Whether the song should start in Bot Play Mode.
|
||||
* @default `false`
|
||||
*/
|
||||
?botPlayMode:Bool,
|
||||
/**
|
||||
* Whether the song should be in minimal mode.
|
||||
* @default `false`
|
||||
|
@ -118,8 +123,14 @@ typedef PlayStateParams =
|
|||
?minimalMode:Bool,
|
||||
/**
|
||||
* If specified, the game will jump to the specified timestamp after the countdown ends.
|
||||
* @default `0.0`
|
||||
*/
|
||||
?startTimestamp:Float,
|
||||
/**
|
||||
* If specified, the game will play the song with the given speed.
|
||||
* @default `1.0` for 100% speed.
|
||||
*/
|
||||
?playbackRate:Float,
|
||||
/**
|
||||
* If specified, the game will not load the instrumental or vocal tracks,
|
||||
* and must be loaded externally.
|
||||
|
@ -210,6 +221,12 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
public var startTimestamp:Float = 0.0;
|
||||
|
||||
/**
|
||||
* Play back the song at this speed.
|
||||
* @default `1.0` for normal speed.
|
||||
*/
|
||||
public var playbackRate:Float = 1.0;
|
||||
|
||||
/**
|
||||
* An empty FlxObject contained in the scene.
|
||||
* The current gameplay camera will always follow this object. Tween its position to move the camera smoothly.
|
||||
|
@ -270,6 +287,18 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
public var isPracticeMode:Bool = false;
|
||||
|
||||
/**
|
||||
* Whether the game is currently in Bot Play Mode.
|
||||
* If true, player will not lose gain or lose score from notes.
|
||||
*/
|
||||
public var isBotPlayMode:Bool = false;
|
||||
|
||||
/**
|
||||
* Whether the player has dropped below zero health,
|
||||
* and we are just waiting for an animation to play out before transitioning.
|
||||
*/
|
||||
public var isPlayerDying:Bool = false;
|
||||
|
||||
/**
|
||||
* In Minimal Mode, the stage and characters are not loaded and a standard background is used.
|
||||
*/
|
||||
|
@ -548,8 +577,10 @@ class PlayState extends MusicBeatSubState
|
|||
if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty;
|
||||
if (params.targetVariation != null) currentVariation = params.targetVariation;
|
||||
isPracticeMode = params.practiceMode ?? false;
|
||||
isBotPlayMode = params.botPlayMode ?? false;
|
||||
isMinimalMode = params.minimalMode ?? false;
|
||||
startTimestamp = params.startTimestamp ?? 0.0;
|
||||
playbackRate = params.playbackRate ?? 1.0;
|
||||
overrideMusic = params.overrideMusic ?? false;
|
||||
previousCameraFollowPoint = params.cameraFollowPoint;
|
||||
|
||||
|
@ -772,11 +803,13 @@ class PlayState extends MusicBeatSubState
|
|||
persistentDraw = true;
|
||||
|
||||
startingSong = true;
|
||||
isPlayerDying = false;
|
||||
|
||||
inputSpitter = [];
|
||||
|
||||
// Reset music properly.
|
||||
FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
|
||||
FlxG.sound.music.pitch = playbackRate;
|
||||
FlxG.sound.music.pause();
|
||||
|
||||
if (!overrideMusic)
|
||||
|
@ -913,7 +946,7 @@ class PlayState extends MusicBeatSubState
|
|||
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
|
||||
}
|
||||
|
||||
if (currentStage != null)
|
||||
if (currentStage != null && currentStage.getBoyfriend() != null)
|
||||
{
|
||||
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
|
||||
}
|
||||
|
@ -939,7 +972,7 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
#end
|
||||
|
||||
if (health <= Constants.HEALTH_MIN && !isPracticeMode)
|
||||
if (health <= Constants.HEALTH_MIN && !isPracticeMode && !isPlayerDying)
|
||||
{
|
||||
vocals.pause();
|
||||
FlxG.sound.music.pause();
|
||||
|
@ -965,20 +998,30 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
#end
|
||||
|
||||
var gameOverSubState = new GameOverSubState(
|
||||
{
|
||||
isChartingMode: isChartingMode,
|
||||
transparent: persistentDraw
|
||||
isPlayerDying = true;
|
||||
|
||||
var deathPreTransitionDelay = currentStage?.getBoyfriend()?.getDeathPreTransitionDelay() ?? 0.0;
|
||||
if (deathPreTransitionDelay > 0)
|
||||
{
|
||||
new FlxTimer().start(deathPreTransitionDelay, function(_) {
|
||||
moveToGameOver();
|
||||
});
|
||||
FlxTransitionableSubState.skipNextTransIn = true;
|
||||
FlxTransitionableSubState.skipNextTransOut = true;
|
||||
openSubState(gameOverSubState);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Transition immediately.
|
||||
moveToGameOver();
|
||||
}
|
||||
|
||||
#if discord_rpc
|
||||
// Game Over doesn't get his own variable because it's only used here
|
||||
DiscordClient.changePresence('Game Over - ' + detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
|
||||
#end
|
||||
}
|
||||
else if (isPlayerDying)
|
||||
{
|
||||
// Wait up.
|
||||
}
|
||||
}
|
||||
|
||||
processSongEvents();
|
||||
|
@ -994,6 +1037,18 @@ class PlayState extends MusicBeatSubState
|
|||
justUnpaused = false;
|
||||
}
|
||||
|
||||
function moveToGameOver():Void
|
||||
{
|
||||
var gameOverSubState = new GameOverSubState(
|
||||
{
|
||||
isChartingMode: isChartingMode,
|
||||
transparent: persistentDraw
|
||||
});
|
||||
FlxTransitionableSubState.skipNextTransIn = true;
|
||||
FlxTransitionableSubState.skipNextTransOut = true;
|
||||
openSubState(gameOverSubState);
|
||||
}
|
||||
|
||||
function processSongEvents():Void
|
||||
{
|
||||
// Query and activate song events.
|
||||
|
@ -1484,17 +1539,17 @@ class PlayState extends MusicBeatSubState
|
|||
if (dad != null)
|
||||
{
|
||||
dad.characterType = CharacterType.DAD;
|
||||
}
|
||||
|
||||
//
|
||||
// OPPONENT HEALTH ICON
|
||||
//
|
||||
iconP2 = new HealthIcon('dad', 1);
|
||||
iconP2.y = healthBar.y - (iconP2.height / 2);
|
||||
dad.initHealthIcon(true); // Apply the character ID here
|
||||
iconP2.zIndex = 850;
|
||||
add(iconP2);
|
||||
iconP2.cameras = [camHUD];
|
||||
//
|
||||
// OPPONENT HEALTH ICON
|
||||
//
|
||||
iconP2 = new HealthIcon('dad', 1);
|
||||
iconP2.y = healthBar.y - (iconP2.height / 2);
|
||||
dad.initHealthIcon(true); // Apply the character ID here
|
||||
iconP2.zIndex = 850;
|
||||
add(iconP2);
|
||||
iconP2.cameras = [camHUD];
|
||||
}
|
||||
|
||||
//
|
||||
// BOYFRIEND
|
||||
|
@ -1504,17 +1559,17 @@ class PlayState extends MusicBeatSubState
|
|||
if (boyfriend != null)
|
||||
{
|
||||
boyfriend.characterType = CharacterType.BF;
|
||||
}
|
||||
|
||||
//
|
||||
// PLAYER HEALTH ICON
|
||||
//
|
||||
iconP1 = new HealthIcon('bf', 0);
|
||||
iconP1.y = healthBar.y - (iconP1.height / 2);
|
||||
boyfriend.initHealthIcon(false); // Apply the character ID here
|
||||
iconP1.zIndex = 850;
|
||||
add(iconP1);
|
||||
iconP1.cameras = [camHUD];
|
||||
//
|
||||
// PLAYER HEALTH ICON
|
||||
//
|
||||
iconP1 = new HealthIcon('bf', 0);
|
||||
iconP1.y = healthBar.y - (iconP1.height / 2);
|
||||
boyfriend.initHealthIcon(false); // Apply the character ID here
|
||||
iconP1.zIndex = 850;
|
||||
add(iconP1);
|
||||
iconP1.cameras = [camHUD];
|
||||
}
|
||||
|
||||
//
|
||||
// ADD CHARACTERS TO SCENE
|
||||
|
@ -1571,8 +1626,10 @@ class PlayState extends MusicBeatSubState
|
|||
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
|
||||
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
|
||||
|
||||
playerStrumline = new Strumline(noteStyle, true);
|
||||
playerStrumline = new Strumline(noteStyle, !isBotPlayMode);
|
||||
playerStrumline.onNoteIncoming.add(onStrumlineNoteIncoming);
|
||||
opponentStrumline = new Strumline(noteStyle, false);
|
||||
opponentStrumline.onNoteIncoming.add(onStrumlineNoteIncoming);
|
||||
add(playerStrumline);
|
||||
add(opponentStrumline);
|
||||
|
||||
|
@ -1689,7 +1746,6 @@ class PlayState extends MusicBeatSubState
|
|||
if (strumTime < startTime) continue; // Skip notes that are before the start time.
|
||||
|
||||
var noteData:Int = songNote.getDirection();
|
||||
|
||||
var playerNote:Bool = true;
|
||||
|
||||
if (noteData > 3) playerNote = false;
|
||||
|
@ -1698,6 +1754,8 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
case 0:
|
||||
playerNoteData.push(songNote);
|
||||
// increment totalNotes for total possible notes able to be hit by the player
|
||||
Highscore.tallies.totalNotes++;
|
||||
case 1:
|
||||
opponentNoteData.push(songNote);
|
||||
}
|
||||
|
@ -1707,6 +1765,13 @@ class PlayState extends MusicBeatSubState
|
|||
opponentStrumline.applyNoteData(opponentNoteData);
|
||||
}
|
||||
|
||||
function onStrumlineNoteIncoming(noteSprite:NoteSprite):Void
|
||||
{
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_INCOMING, noteSprite, 0, false);
|
||||
|
||||
dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares to start the countdown.
|
||||
* Ends any running cutscenes, creates the strumlines, and starts the countdown.
|
||||
|
@ -1781,14 +1846,16 @@ class PlayState extends MusicBeatSubState
|
|||
// A negative instrumental offset means the song skips the first few milliseconds of the track.
|
||||
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
|
||||
FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset);
|
||||
FlxG.sound.music.pitch = playbackRate;
|
||||
|
||||
// I am going insane.
|
||||
FlxG.sound.music.volume = 1.0;
|
||||
FlxG.sound.music.fadeTween?.cancel();
|
||||
if (FlxG.sound.music.fadeTween != null) FlxG.sound.music.fadeTween.cancel();
|
||||
|
||||
trace('Playing vocals...');
|
||||
add(vocals);
|
||||
vocals.play();
|
||||
vocals.pitch = playbackRate;
|
||||
resyncVocals();
|
||||
|
||||
#if discord_rpc
|
||||
|
@ -1829,7 +1896,14 @@ class PlayState extends MusicBeatSubState
|
|||
function updateScoreText():Void
|
||||
{
|
||||
// TODO: Add functionality for modules to update the score text.
|
||||
scoreText.text = 'Score:' + songScore;
|
||||
if (isBotPlayMode)
|
||||
{
|
||||
scoreText.text = 'Bot Play Enabled';
|
||||
}
|
||||
else
|
||||
{
|
||||
scoreText.text = 'Score:' + songScore;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1837,7 +1911,14 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
function updateHealthBar():Void
|
||||
{
|
||||
healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
|
||||
if (isBotPlayMode)
|
||||
{
|
||||
healthLerp = Constants.HEALTH_MAX;
|
||||
}
|
||||
else
|
||||
{
|
||||
healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1881,13 +1962,16 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (Conductor.instance.songPosition > hitWindowEnd)
|
||||
{
|
||||
if (note.hasMissed) continue;
|
||||
if (note.hasMissed || note.hasBeenHit) continue;
|
||||
|
||||
note.tooEarly = false;
|
||||
note.mayHit = false;
|
||||
note.hasMissed = true;
|
||||
|
||||
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
|
||||
if (note.holdNoteSprite != null)
|
||||
{
|
||||
note.holdNoteSprite.missedNote = true;
|
||||
}
|
||||
}
|
||||
else if (Conductor.instance.songPosition > hitWindowCenter)
|
||||
{
|
||||
|
@ -1895,7 +1979,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
// Call an event to allow canceling the note hit.
|
||||
// NOTE: This is what handles the character animations!
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, 0, true);
|
||||
var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
|
||||
dispatchEvent(event);
|
||||
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
|
@ -1974,10 +2058,38 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (Conductor.instance.songPosition > hitWindowEnd)
|
||||
{
|
||||
if (note.hasMissed || note.hasBeenHit) continue;
|
||||
note.tooEarly = false;
|
||||
note.mayHit = false;
|
||||
note.hasMissed = true;
|
||||
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
|
||||
if (note.holdNoteSprite != null)
|
||||
{
|
||||
note.holdNoteSprite.missedNote = true;
|
||||
}
|
||||
}
|
||||
else if (isBotPlayMode && Conductor.instance.songPosition > hitWindowCenter)
|
||||
{
|
||||
if (note.hasBeenHit) continue;
|
||||
|
||||
// We call onHitNote to play the proper animations,
|
||||
// but not goodNoteHit! This means zero score and zero notes hit for the results screen!
|
||||
|
||||
// Call an event to allow canceling the note hit.
|
||||
// NOTE: This is what handles the character animations!
|
||||
var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
|
||||
dispatchEvent(event);
|
||||
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
if (event.eventCanceled) continue;
|
||||
|
||||
// Command the bot to hit the note on time.
|
||||
// NOTE: This is what handles the strumline and cleaning up the note itself!
|
||||
playerStrumline.hitNote(note);
|
||||
|
||||
if (note.holdNoteSprite != null)
|
||||
{
|
||||
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
|
||||
}
|
||||
}
|
||||
else if (Conductor.instance.songPosition > hitWindowStart)
|
||||
{
|
||||
|
@ -2000,7 +2112,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
// Call an event to allow canceling the note miss.
|
||||
// NOTE: This is what handles the character animations!
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, 0, true);
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, -Constants.HEALTH_MISS_PENALTY, 0, true);
|
||||
dispatchEvent(event);
|
||||
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
|
@ -2009,7 +2121,7 @@ class PlayState extends MusicBeatSubState
|
|||
// Judge the miss.
|
||||
// NOTE: This is what handles the scoring.
|
||||
trace('Missed note! ${note.noteData}');
|
||||
onNoteMiss(note, event.playSound, event.healthMulti);
|
||||
onNoteMiss(note, event.playSound, event.healthChange);
|
||||
|
||||
note.handledMiss = true;
|
||||
}
|
||||
|
@ -2022,7 +2134,7 @@ class PlayState extends MusicBeatSubState
|
|||
if (holdNote == null || !holdNote.alive) continue;
|
||||
|
||||
// While the hold note is being hit, and there is length on the hold note...
|
||||
if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
|
||||
if (!isBotPlayMode && holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
|
||||
{
|
||||
// Grant the player health.
|
||||
health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
|
||||
|
@ -2155,13 +2267,41 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void
|
||||
{
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, Highscore.tallies.combo + 1, true);
|
||||
// Calculate the input latency (do this as late as possible).
|
||||
// trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
|
||||
var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
|
||||
var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
|
||||
// trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
|
||||
|
||||
// Get the offset and compensate for input latency.
|
||||
// Round inward (trim remainder) for consistency.
|
||||
var noteDiff:Int = Std.int(Conductor.instance.songPosition - note.noteData.time - inputLatencyMs);
|
||||
|
||||
var score = Scoring.scoreNote(noteDiff, PBOT1);
|
||||
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
|
||||
|
||||
var healthChange = 0.0;
|
||||
switch (daRating)
|
||||
{
|
||||
case 'sick':
|
||||
healthChange = Constants.HEALTH_SICK_BONUS;
|
||||
case 'good':
|
||||
healthChange = Constants.HEALTH_GOOD_BONUS;
|
||||
case 'bad':
|
||||
healthChange = Constants.HEALTH_BAD_BONUS;
|
||||
case 'shit':
|
||||
healthChange = Constants.HEALTH_SHIT_BONUS;
|
||||
}
|
||||
|
||||
// Send the note hit event.
|
||||
var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, Highscore.tallies.combo + 1);
|
||||
dispatchEvent(event);
|
||||
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
if (event.eventCanceled) return;
|
||||
|
||||
popUpScore(note, input, event.healthMulti);
|
||||
// Display the combo meter and add the calculation to the score.
|
||||
popUpScore(note, event.score, event.judgement, event.healthChange);
|
||||
|
||||
if (note.isHoldNote && note.holdNoteSprite != null)
|
||||
{
|
||||
|
@ -2175,11 +2315,11 @@ class PlayState extends MusicBeatSubState
|
|||
* Called when a note leaves the screen and is considered missed by the player.
|
||||
* @param note
|
||||
*/
|
||||
function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthLossMulti:Float = 1.0):Void
|
||||
function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthChange:Float):Void
|
||||
{
|
||||
// If we are here, we already CALLED the onNoteMiss script hook!
|
||||
|
||||
health -= Constants.HEALTH_MISS_PENALTY * healthLossMulti;
|
||||
health += healthChange;
|
||||
songScore -= 10;
|
||||
|
||||
if (!isPracticeMode)
|
||||
|
@ -2351,23 +2491,10 @@ class PlayState extends MusicBeatSubState
|
|||
/**
|
||||
* Handles health, score, and rating popups when a note is hit.
|
||||
*/
|
||||
function popUpScore(daNote:NoteSprite, input:PreciseInputEvent, healthGainMulti:Float = 1.0):Void
|
||||
function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void
|
||||
{
|
||||
vocals.playerVolume = 1;
|
||||
|
||||
// Calculate the input latency (do this as late as possible).
|
||||
// trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
|
||||
var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
|
||||
var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
|
||||
// trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
|
||||
|
||||
// Get the offset and compensate for input latency.
|
||||
// Round inward (trim remainder) for consistency.
|
||||
var noteDiff:Int = Std.int(Conductor.instance.songPosition - daNote.noteData.time - inputLatencyMs);
|
||||
|
||||
var score = Scoring.scoreNote(noteDiff, PBOT1);
|
||||
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
|
||||
|
||||
if (daRating == 'miss')
|
||||
{
|
||||
// If daRating is 'miss', that means we made a mistake and should not continue.
|
||||
|
@ -2382,22 +2509,20 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
case 'sick':
|
||||
Highscore.tallies.sick += 1;
|
||||
health += Constants.HEALTH_SICK_BONUS * healthGainMulti;
|
||||
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
|
||||
case 'good':
|
||||
Highscore.tallies.good += 1;
|
||||
health += Constants.HEALTH_GOOD_BONUS * healthGainMulti;
|
||||
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
|
||||
case 'bad':
|
||||
Highscore.tallies.bad += 1;
|
||||
health += Constants.HEALTH_BAD_BONUS * healthGainMulti;
|
||||
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
|
||||
case 'shit':
|
||||
Highscore.tallies.shit += 1;
|
||||
health += Constants.HEALTH_SHIT_BONUS * healthGainMulti;
|
||||
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
|
||||
}
|
||||
|
||||
health += healthChange;
|
||||
|
||||
if (isComboBreak)
|
||||
{
|
||||
// Break the combo, but don't increment tallies.misses.
|
||||
|
@ -2563,9 +2688,12 @@ class PlayState extends MusicBeatSubState
|
|||
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
|
||||
};
|
||||
|
||||
if (Save.get().isSongHighScore(currentSong.id, currentDifficulty, data))
|
||||
// adds current song data into the tallies for the level (story levels)
|
||||
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
|
||||
|
||||
if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data))
|
||||
{
|
||||
Save.get().setSongScore(currentSong.id, currentDifficulty, data);
|
||||
Save.instance.setSongScore(currentSong.id, currentDifficulty, data);
|
||||
#if newgrounds
|
||||
NGio.postScore(score, currentSong.id);
|
||||
#end
|
||||
|
@ -2613,9 +2741,9 @@ class PlayState extends MusicBeatSubState
|
|||
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
|
||||
};
|
||||
|
||||
if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
|
||||
if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
|
||||
{
|
||||
Save.get().setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
|
||||
Save.instance.setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
|
||||
#if newgrounds
|
||||
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
|
||||
#end
|
||||
|
@ -2728,14 +2856,19 @@ class PlayState extends MusicBeatSubState
|
|||
// TODO: Uncache the song.
|
||||
}
|
||||
|
||||
if (!overrideMusic)
|
||||
if (overrideMusic)
|
||||
{
|
||||
// Stop the music.
|
||||
// Stop the music. Do NOT destroy it, something still references it!
|
||||
FlxG.sound.music.pause();
|
||||
if (vocals != null) vocals.stop();
|
||||
if (vocals != null)
|
||||
{
|
||||
vocals.pause();
|
||||
remove(vocals);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop and destroy the music.
|
||||
FlxG.sound.music.pause();
|
||||
if (vocals != null)
|
||||
{
|
||||
|
@ -2831,11 +2964,14 @@ class PlayState extends MusicBeatSubState
|
|||
persistentUpdate = false;
|
||||
vocals.stop();
|
||||
camHUD.alpha = 1;
|
||||
|
||||
var talliesToUse:Tallies = PlayStatePlaylist.isStoryMode ? Highscore.talliesLevel : Highscore.tallies;
|
||||
|
||||
var res:ResultState = new ResultState(
|
||||
{
|
||||
storyMode: PlayStatePlaylist.isStoryMode,
|
||||
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
|
||||
tallies: Highscore.tallies,
|
||||
tallies: talliesToUse,
|
||||
});
|
||||
res.camera = camHUD;
|
||||
openSubState(res);
|
||||
|
|
|
@ -2,27 +2,25 @@ package funkin.play;
|
|||
|
||||
import funkin.ui.story.StoryMenuState;
|
||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||
import flixel.FlxBasic;
|
||||
import flixel.FlxSprite;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
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;
|
||||
|
@ -31,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)
|
||||
{
|
||||
|
@ -50,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);
|
||||
|
@ -188,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);
|
||||
|
||||
|
@ -202,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);
|
||||
|
@ -274,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});
|
||||
|
@ -310,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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,16 @@ class BaseCharacter extends Bopper
|
|||
return _data.death?.cameraOffsets ?? [0.0, 0.0];
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -367,9 +377,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 +388,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 +485,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);
|
||||
|
||||
|
|
|
@ -744,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,9 +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 flixel.util.FlxDirection;
|
||||
import funkin.util.tools.TimerTools;
|
||||
|
||||
class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
||||
{
|
||||
|
@ -16,9 +17,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
|||
|
||||
public function displayRating(daRating:String)
|
||||
{
|
||||
#if sys
|
||||
var perfStart:Float = Sys.time();
|
||||
#end
|
||||
var perfStart:Float = TimerTools.start();
|
||||
|
||||
if (daRating == null) daRating = "good";
|
||||
|
||||
|
@ -60,17 +59,12 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
|||
startDelay: Conductor.instance.beatLengthMs * 0.001
|
||||
});
|
||||
|
||||
#if sys
|
||||
var perfEnd:Float = Sys.time();
|
||||
trace("displayRating took: " + (perfEnd - perfStart));
|
||||
#end
|
||||
trace('displayRating took: ${TimerTools.seconds(perfStart)}');
|
||||
}
|
||||
|
||||
public function displayCombo(?combo:Int = 0):Int
|
||||
{
|
||||
#if sys
|
||||
var perfStart:Float = Sys.time();
|
||||
#end
|
||||
var perfStart:Float = TimerTools.start();
|
||||
|
||||
if (combo == null) combo = 0;
|
||||
|
||||
|
@ -163,10 +157,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
|||
daLoop++;
|
||||
}
|
||||
|
||||
#if sys
|
||||
var perfEnd:Float = Sys.time();
|
||||
trace("displayCombo took: " + (perfEnd - perfStart));
|
||||
#end
|
||||
trace('displayCombo took: ${TimerTools.seconds(perfStart)}');
|
||||
|
||||
return combo;
|
||||
}
|
||||
|
|
|
@ -218,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,26 +16,53 @@ class NoteSprite extends FunkinSprite
|
|||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,16 +127,13 @@ class NoteSprite extends FunkinSprite
|
|||
*/
|
||||
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.
|
||||
|
|
|
@ -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;
|
||||
|
@ -38,6 +39,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;
|
||||
|
||||
/**
|
||||
|
@ -49,6 +54,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>;
|
||||
|
@ -106,6 +113,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]);
|
||||
|
@ -311,6 +320,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.
|
||||
|
@ -659,7 +670,6 @@ class Strumline extends FlxSpriteGroup
|
|||
|
||||
if (noteSprite != null)
|
||||
{
|
||||
noteSprite.strumTime = note.time;
|
||||
noteSprite.direction = note.getDirection();
|
||||
noteSprite.noteData = note;
|
||||
|
||||
|
|
|
@ -364,7 +364,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 {};
|
||||
|
||||
|
|
|
@ -236,25 +236,25 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
|
|||
// 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')
|
||||
{
|
||||
trace('[BOPPER] Attempting to fallback to "idle"');
|
||||
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 "idle" that does not exist! This is bad!');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -353,7 +353,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) {}
|
||||
|
||||
|
|
|
@ -110,6 +110,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
getBoyfriend().resetCharacter(true);
|
||||
// Reapply the camera offsets.
|
||||
var charData = _data.characters.bf;
|
||||
getBoyfriend().scale.set(charData.scale, charData.scale);
|
||||
getBoyfriend().cameraFocusPoint.x += charData.cameraOffsets[0];
|
||||
getBoyfriend().cameraFocusPoint.y += charData.cameraOffsets[1];
|
||||
}
|
||||
|
@ -122,6 +123,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
getGirlfriend().resetCharacter(true);
|
||||
// Reapply the camera offsets.
|
||||
var charData = _data.characters.gf;
|
||||
getGirlfriend().scale.set(charData.scale, charData.scale);
|
||||
getGirlfriend().cameraFocusPoint.x += charData.cameraOffsets[0];
|
||||
getGirlfriend().cameraFocusPoint.y += charData.cameraOffsets[1];
|
||||
}
|
||||
|
@ -130,6 +132,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
getDad().resetCharacter(true);
|
||||
// Reapply the camera offsets.
|
||||
var charData = _data.characters.dad;
|
||||
getDad().scale.set(charData.scale, charData.scale);
|
||||
getDad().cameraFocusPoint.x += charData.cameraOffsets[0];
|
||||
getDad().cameraFocusPoint.y += charData.cameraOffsets[1];
|
||||
}
|
||||
|
@ -226,7 +229,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]);
|
||||
|
@ -435,6 +438,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
character.originalPosition.y = character.y + character.animOffsets[1];
|
||||
}
|
||||
|
||||
character.scale.set(charData.scale, charData.scale);
|
||||
character.cameraFocusPoint.x += charData.cameraOffsets[0];
|
||||
character.cameraFocusPoint.y += charData.cameraOffsets[1];
|
||||
|
||||
|
@ -637,7 +641,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);
|
||||
}
|
||||
|
@ -843,7 +870,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) {}
|
||||
|
||||
|
|
|
@ -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,84 +46,85 @@ 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,
|
||||
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,
|
||||
|
||||
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
|
||||
},
|
||||
};
|
||||
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
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,7 +134,7 @@ abstract Save(RawSaveData)
|
|||
|
||||
function get_options():SaveDataOptions
|
||||
{
|
||||
return this.options;
|
||||
return data.options;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -130,7 +144,7 @@ abstract Save(RawSaveData)
|
|||
|
||||
function get_modOptions():Map<String, Dynamic>
|
||||
{
|
||||
return this.mods.modOptions;
|
||||
return data.mods.modOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -140,232 +154,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 chartEditorHitsoundVolumePlayer(get, set):Float;
|
||||
|
||||
function get_chartEditorHitsoundVolumePlayer():Float
|
||||
{
|
||||
if (this.optionsChartEditor.hitsoundVolumePlayer == null) this.optionsChartEditor.hitsoundVolumePlayer = 1.0;
|
||||
if (data.optionsChartEditor.hitsoundVolumePlayer == null) data.optionsChartEditor.hitsoundVolumePlayer = 1.0;
|
||||
|
||||
return this.optionsChartEditor.hitsoundVolumePlayer;
|
||||
return data.optionsChartEditor.hitsoundVolumePlayer;
|
||||
}
|
||||
|
||||
function set_chartEditorHitsoundVolumePlayer(value:Float):Float
|
||||
{
|
||||
// Set and apply.
|
||||
this.optionsChartEditor.hitsoundVolumePlayer = value;
|
||||
data.optionsChartEditor.hitsoundVolumePlayer = value;
|
||||
flush();
|
||||
return this.optionsChartEditor.hitsoundVolumePlayer;
|
||||
return data.optionsChartEditor.hitsoundVolumePlayer;
|
||||
}
|
||||
|
||||
public var chartEditorHitsoundVolumeOpponent(get, set):Float;
|
||||
|
||||
function get_chartEditorHitsoundVolumeOpponent():Float
|
||||
{
|
||||
if (this.optionsChartEditor.hitsoundVolumeOpponent == null) this.optionsChartEditor.hitsoundVolumeOpponent = 1.0;
|
||||
if (data.optionsChartEditor.hitsoundVolumeOpponent == null) data.optionsChartEditor.hitsoundVolumeOpponent = 1.0;
|
||||
|
||||
return this.optionsChartEditor.hitsoundVolumeOpponent;
|
||||
return data.optionsChartEditor.hitsoundVolumeOpponent;
|
||||
}
|
||||
|
||||
function set_chartEditorHitsoundVolumeOpponent(value:Float):Float
|
||||
{
|
||||
// Set and apply.
|
||||
this.optionsChartEditor.hitsoundVolumeOpponent = value;
|
||||
data.optionsChartEditor.hitsoundVolumeOpponent = value;
|
||||
flush();
|
||||
return this.optionsChartEditor.hitsoundVolumeOpponent;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -377,11 +391,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);
|
||||
|
@ -392,11 +406,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);
|
||||
|
||||
|
@ -405,11 +419,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);
|
||||
|
@ -448,11 +462,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);
|
||||
}
|
||||
|
@ -462,11 +476,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);
|
||||
|
||||
|
@ -482,11 +496,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);
|
||||
|
@ -527,9 +541,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -547,20 +561,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -581,6 +595,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.
|
||||
*/
|
||||
|
@ -606,17 +650,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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -592,6 +592,11 @@ 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.
|
||||
*/
|
||||
|
@ -920,12 +925,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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2163,7 +2168,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)
|
||||
{
|
||||
|
@ -2191,7 +2196,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> = [];
|
||||
|
@ -4525,14 +4530,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, Reflect.copy(eventDataToPlace));
|
||||
|
||||
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, Reflect.copy(noteKindToPlace));
|
||||
|
||||
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
|
||||
|
||||
|
@ -5308,6 +5313,10 @@ 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
|
||||
{
|
||||
|
@ -5355,8 +5364,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,
|
||||
});
|
||||
|
||||
|
@ -5923,7 +5934,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!
|
||||
|
|
|
@ -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.tools.TimerTools;
|
||||
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 = TimerTools.start();
|
||||
|
||||
var result:Bool = playInstrumental(state, instId);
|
||||
if (!result) return false;
|
||||
|
||||
var perfB = haxe.Timer.stamp();
|
||||
var perfB:Float = TimerTools.start();
|
||||
|
||||
stopExistingVocals(state);
|
||||
|
||||
var perfC = haxe.Timer.stamp();
|
||||
var perfC:Float = TimerTools.start();
|
||||
|
||||
result = playVocals(state, BF, playerId, instId);
|
||||
|
||||
var perfD = haxe.Timer.stamp();
|
||||
var perfD:Float = TimerTools.start();
|
||||
|
||||
// if (!result) return false;
|
||||
result = playVocals(state, DAD, opponentId, instId);
|
||||
// if (!result) return false;
|
||||
|
||||
var perfE = haxe.Timer.stamp();
|
||||
var perfE:Float = TimerTools.start();
|
||||
|
||||
state.hardRefreshOffsetsToolbox();
|
||||
|
||||
var perfF = haxe.Timer.stamp();
|
||||
var perfF:Float = TimerTools.start();
|
||||
|
||||
state.hardRefreshFreeplayToolbox();
|
||||
|
||||
var perfG = haxe.Timer.stamp();
|
||||
var perfG:Float = TimerTools.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 ${TimerTools.seconds(perfA, perfB)}.');
|
||||
trace('Stopped existing vocals in ${TimerTools.seconds(perfB, perfC)}.');
|
||||
trace('Played BF vocals in ${TimerTools.seconds(perfC, perfD)}.');
|
||||
trace('Played DAD vocals in ${TimerTools.seconds(perfD, perfE)}.');
|
||||
trace('Hard refreshed offsets toolbox in ${TimerTools.seconds(perfE, perfF)}.');
|
||||
trace('Hard refreshed freeplay toolbox in ${TimerTools.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 = TimerTools.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 ${TimerTools.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 = TimerTools.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 ${TimerTools.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 = TimerTools.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 ${TimerTools.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 = TimerTools.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 ${TimerTools.seconds(perfStart)}.');
|
||||
|
||||
if (waveformData != null)
|
||||
{
|
||||
|
|
|
@ -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.';
|
||||
|
|
|
@ -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.tools.TimerTools;
|
||||
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 perfStart:Float = TimerTools.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 = waveformData3.merge(waveformData1).merge(waveformData2);
|
||||
trace('Waveform data merging took: ${haxe.Timer.stamp() - perfStart} seconds');
|
||||
trace('Waveform data merging took: ${TimerTools.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.
|
||||
|
|
|
@ -272,19 +272,19 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
|
|||
// waveformPlayer.waveform.forceUpdate = true;
|
||||
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 ?? 1000) / 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
|
||||
waveformOpponent.waveform.waveformData = opponentVoice?.waveformData;
|
||||
waveformOpponent.waveform.duration = (opponentVoice?.length ?? 1000) / Constants.MS_PER_SEC;
|
||||
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 ?? 1000) / 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" => "Dodge High (Blazin')",
|
||||
"weekend-1-blockspin" => "Block High (Spin) (Blazin')",
|
||||
"weekend-1-dodgehigh" => "Block Low (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
|
||||
|
|
|
@ -130,7 +130,7 @@ class LatencyState extends MusicBeatSubState
|
|||
|
||||
for (i in 0...32)
|
||||
{
|
||||
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.instance.beatLengthMs * i);
|
||||
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault());
|
||||
noteGrp.add(note);
|
||||
}
|
||||
|
||||
|
|
|
@ -1001,7 +1001,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
var daSong = songs[curSelected];
|
||||
if (daSong != null)
|
||||
{
|
||||
var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, currentDifficulty);
|
||||
var songScore:SaveScoreData = Save.instance.getSongScore(songs[curSelected].songId, currentDifficulty);
|
||||
intendedScore = songScore?.score ?? 0;
|
||||
intendedCompletion = songScore?.accuracy ?? 0.0;
|
||||
rememberedDifficulty = currentDifficulty;
|
||||
|
@ -1143,6 +1143,12 @@ class FreeplayState extends MusicBeatSubState
|
|||
targetSong: targetSong,
|
||||
targetDifficulty: targetDifficulty,
|
||||
targetVariation: targetVariation,
|
||||
practiceMode: false,
|
||||
minimalMode: false,
|
||||
// TODO: Make these an option! It's currently only accessible via chart editor.
|
||||
// startTimestamp: 0.0,
|
||||
// playbackRate: 0.5,
|
||||
// botPlayMode: true,
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
@ -1183,7 +1189,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
var daSongCapsule = grpCapsules.members[curSelected];
|
||||
if (daSongCapsule.songData != null)
|
||||
{
|
||||
var songScore:SaveScoreData = Save.get().getSongScore(daSongCapsule.songData.songId, currentDifficulty);
|
||||
var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty);
|
||||
intendedScore = songScore?.score ?? 0;
|
||||
intendedCompletion = songScore?.accuracy ?? 0.0;
|
||||
diffIdsCurrent = daSongCapsule.songData.songDifficulties;
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.ui.haxeui.components;
|
|||
|
||||
import funkin.modding.events.ScriptEvent.GhostMissNoteScriptEvent;
|
||||
import funkin.modding.events.ScriptEvent.NoteScriptEvent;
|
||||
import funkin.modding.events.ScriptEvent.HitNoteScriptEvent;
|
||||
import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
|
||||
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
|
||||
import haxe.ui.core.IDataComponent;
|
||||
|
@ -216,12 +217,17 @@ class CharacterPlayer extends Box
|
|||
if (character != null) character.onStepHit(event);
|
||||
}
|
||||
|
||||
public function onNoteIncoming(event:NoteScriptEvent)
|
||||
{
|
||||
if (character != null) character.onNoteIncoming(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a note is hit in the song
|
||||
* Used to play character animations.
|
||||
* @param event The event.
|
||||
*/
|
||||
public function onNoteHit(event:NoteScriptEvent):Void
|
||||
public function onNoteHit(event:HitNoteScriptEvent):Void
|
||||
{
|
||||
if (character != null) character.onNoteHit(event);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class ColorsMenu extends Page
|
|||
|
||||
for (i in 0...4)
|
||||
{
|
||||
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), 0, i);
|
||||
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), i);
|
||||
|
||||
note.x = (100 * i) + i;
|
||||
note.screenCenter(Y);
|
||||
|
|
|
@ -51,6 +51,7 @@ class Level implements IRegistryEntry<LevelData>
|
|||
|
||||
/**
|
||||
* Retrieve the title of the level for display on the menu.
|
||||
* @return Title of the level as a string
|
||||
*/
|
||||
public function getTitle():String
|
||||
{
|
||||
|
@ -58,16 +59,21 @@ class Level implements IRegistryEntry<LevelData>
|
|||
return _data.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the title graphic for the level.
|
||||
* @return The constructed graphic as a sprite.
|
||||
*/
|
||||
public function buildTitleGraphic():FlxSprite
|
||||
{
|
||||
var result = new FlxSprite().loadGraphic(Paths.image(_data.titleAsset));
|
||||
var result:FlxSprite = new FlxSprite().loadGraphic(Paths.image(_data.titleAsset));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of songs in this level, as an array of names, for display on the menu.
|
||||
* @return Array<String>
|
||||
* @param difficulty The difficulty of the level being displayed
|
||||
* @return The display names of the songs in this level
|
||||
*/
|
||||
public function getSongDisplayNames(difficulty:String):Array<String>
|
||||
{
|
||||
|
@ -88,7 +94,9 @@ class Level implements IRegistryEntry<LevelData>
|
|||
|
||||
/**
|
||||
* Whether this level is unlocked. If not, it will be greyed out on the menu and have a lock icon.
|
||||
* TODO: Change this behavior in a later release.
|
||||
* Override this in a script.
|
||||
* @default `true`
|
||||
* @return Whether this level is unlocked
|
||||
*/
|
||||
public function isUnlocked():Bool
|
||||
{
|
||||
|
@ -97,6 +105,9 @@ class Level implements IRegistryEntry<LevelData>
|
|||
|
||||
/**
|
||||
* Whether this level is visible. If not, it will not be shown on the menu at all.
|
||||
* Override this in a script.
|
||||
* @default `true`
|
||||
* @return Whether this level is visible in the menu
|
||||
*/
|
||||
public function isVisible():Bool
|
||||
{
|
||||
|
@ -106,6 +117,7 @@ class Level implements IRegistryEntry<LevelData>
|
|||
/**
|
||||
* Build a sprite for the background of the level.
|
||||
* Can be overriden by ScriptedLevel. Not used if `isBackgroundSimple` returns true.
|
||||
* @return The constructed sprite
|
||||
*/
|
||||
public function buildBackground():FlxSprite
|
||||
{
|
||||
|
@ -124,6 +136,7 @@ class Level implements IRegistryEntry<LevelData>
|
|||
/**
|
||||
* Returns true if the background is a solid color.
|
||||
* If you have a ScriptedLevel with a fancy background, you may want to override this to false.
|
||||
* @return Whether the background is a simple color
|
||||
*/
|
||||
public function isBackgroundSimple():Bool
|
||||
{
|
||||
|
@ -133,30 +146,36 @@ class Level implements IRegistryEntry<LevelData>
|
|||
/**
|
||||
* Returns true if the background is a solid color.
|
||||
* If you have a ScriptedLevel with a fancy background, you may want to override this to false.
|
||||
* @return The background as a simple color. May not be valid if `isBackgroundSimple` returns false.
|
||||
*/
|
||||
public function getBackgroundColor():FlxColor
|
||||
{
|
||||
return FlxColor.fromString(_data.background);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of difficulties the player can select from for this level.
|
||||
* @return The difficulty IDs.
|
||||
*/
|
||||
public function getDifficulties():Array<String>
|
||||
{
|
||||
var difficulties:Array<String> = [];
|
||||
|
||||
var songList = getSongs();
|
||||
var songList:Array<String> = getSongs();
|
||||
|
||||
var firstSongId:String = songList[0];
|
||||
var firstSong:Song = SongRegistry.instance.fetchEntry(firstSongId);
|
||||
|
||||
if (firstSong != null)
|
||||
{
|
||||
// Don't display alternate characters in Story Mode.
|
||||
for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, "erect"]))
|
||||
// Don't display alternate characters in Story Mode. Only show `default` and `erect` variations.
|
||||
for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect']))
|
||||
{
|
||||
difficulties.push(difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort in a specific order! Fall back to alphabetical.
|
||||
difficulties.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST));
|
||||
|
||||
// Filter to only include difficulties that are present in all songs
|
||||
|
@ -169,7 +188,7 @@ class Level implements IRegistryEntry<LevelData>
|
|||
|
||||
for (difficulty in difficulties)
|
||||
{
|
||||
if (!song.hasDifficulty(difficulty, [Constants.DEFAULT_VARIATION, "erect"]))
|
||||
if (!song.hasDifficulty(difficulty, [Constants.DEFAULT_VARIATION, 'erect']))
|
||||
{
|
||||
difficulties.remove(difficulty);
|
||||
}
|
||||
|
@ -181,6 +200,11 @@ class Level implements IRegistryEntry<LevelData>
|
|||
return difficulties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the props for display over the colored background.
|
||||
* @param existingProps The existing prop sprites, if any.
|
||||
* @return The constructed prop sprites
|
||||
*/
|
||||
public function buildProps(?existingProps:Array<LevelProp>):Array<LevelProp>
|
||||
{
|
||||
var props:Array<LevelProp> = existingProps == null ? [] : [for (x in existingProps) x];
|
||||
|
@ -189,11 +213,13 @@ class Level implements IRegistryEntry<LevelData>
|
|||
|
||||
var hiddenProps:Array<LevelProp> = props.splice(_data.props.length - 1, props.length - 1);
|
||||
for (hiddenProp in hiddenProps)
|
||||
{
|
||||
hiddenProp.visible = false;
|
||||
}
|
||||
|
||||
for (propIndex in 0..._data.props.length)
|
||||
{
|
||||
var propData = _data.props[propIndex];
|
||||
var propData:LevelPropData = _data.props[propIndex];
|
||||
|
||||
// Attempt to reuse the `LevelProp` object.
|
||||
// This prevents animations from resetting.
|
||||
|
@ -201,8 +227,15 @@ class Level implements IRegistryEntry<LevelData>
|
|||
if (existingProp != null)
|
||||
{
|
||||
existingProp.propData = propData;
|
||||
existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
|
||||
existingProp.visible = true;
|
||||
if (existingProp.propData == null)
|
||||
{
|
||||
existingProp.visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
existingProp.visible = true;
|
||||
existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -217,6 +250,10 @@ class Level implements IRegistryEntry<LevelData>
|
|||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the level is destroyed.
|
||||
* TODO: Document when this gets called
|
||||
*/
|
||||
public function destroy():Void {}
|
||||
|
||||
public function toString():String
|
||||
|
@ -224,6 +261,11 @@ class Level implements IRegistryEntry<LevelData>
|
|||
return 'Level($id)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve and parse the JSON data for a level by ID.
|
||||
* @param id The ID of the level
|
||||
* @return The parsed level data, or null if not found or invalid
|
||||
*/
|
||||
static function _fetchData(id:String):Null<LevelData>
|
||||
{
|
||||
return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id));
|
||||
|
|
|
@ -11,11 +11,11 @@ class LevelProp extends Bopper
|
|||
function set_propData(value:LevelPropData):LevelPropData
|
||||
{
|
||||
// Only reset the prop if the asset path has changed.
|
||||
if (propData == null || value.assetPath != this.propData.assetPath)
|
||||
if (propData == null || value?.assetPath != propData?.assetPath)
|
||||
{
|
||||
this.visible = (value != null);
|
||||
this.propData = value;
|
||||
danceEvery = this.propData.danceEvery;
|
||||
danceEvery = this.propData?.danceEvery ?? 0;
|
||||
applyData();
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,16 @@ class LevelProp extends Bopper
|
|||
|
||||
function applyData():Void
|
||||
{
|
||||
if (propData == null)
|
||||
{
|
||||
this.visible = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
var isAnimated:Bool = propData.animations.length > 0;
|
||||
if (isAnimated)
|
||||
{
|
||||
|
|
|
@ -141,10 +141,10 @@ class StoryMenuState extends MusicBeatState
|
|||
|
||||
persistentUpdate = persistentDraw = true;
|
||||
|
||||
updateData();
|
||||
|
||||
rememberSelection();
|
||||
|
||||
updateData();
|
||||
|
||||
// Explicitly define the background color.
|
||||
this.bgColor = FlxColor.BLACK;
|
||||
|
||||
|
@ -403,7 +403,8 @@ class StoryMenuState extends MusicBeatState
|
|||
|
||||
function hasModdedLevels():Bool
|
||||
{
|
||||
return LevelRegistry.instance.listModdedLevelIds().length > 0;
|
||||
return false;
|
||||
// return LevelRegistry.instance.listModdedLevelIds().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -519,7 +520,7 @@ class StoryMenuState extends MusicBeatState
|
|||
}
|
||||
}
|
||||
|
||||
function selectLevel()
|
||||
function selectLevel():Void
|
||||
{
|
||||
if (!currentLevel.isUnlocked())
|
||||
{
|
||||
|
@ -554,6 +555,8 @@ class StoryMenuState extends MusicBeatState
|
|||
PlayStatePlaylist.campaignTitle = currentLevel.getTitle();
|
||||
PlayStatePlaylist.campaignDifficulty = currentDifficultyId;
|
||||
|
||||
Highscore.talliesLevel = new funkin.Highscore.Tallies();
|
||||
|
||||
new FlxTimer().start(1, function(tmr:FlxTimer) {
|
||||
FlxTransitionableState.skipNextTransIn = false;
|
||||
FlxTransitionableState.skipNextTransOut = false;
|
||||
|
@ -649,7 +652,7 @@ class StoryMenuState extends MusicBeatState
|
|||
tracklistText.screenCenter(X);
|
||||
tracklistText.x -= FlxG.width * 0.35;
|
||||
|
||||
var levelScore:Null<SaveScoreData> = Save.get().getLevelScore(currentLevelId, currentDifficultyId);
|
||||
var levelScore:Null<SaveScoreData> = Save.instance.getLevelScore(currentLevelId, currentDifficultyId);
|
||||
highScore = levelScore?.score ?? 0;
|
||||
// levelScore.accuracy
|
||||
}
|
||||
|
|
23
source/funkin/util/TrackerUtil.hx
Normal file
23
source/funkin/util/TrackerUtil.hx
Normal file
|
@ -0,0 +1,23 @@
|
|||
package funkin.util;
|
||||
|
||||
// This may or may not already be imported via imports.hx...
|
||||
import flixel.system.debug.watch.Tracker;
|
||||
import flixel.FlxG;
|
||||
|
||||
/**
|
||||
* Utility class that helps manage adding profiles to the flixel debugger tracker
|
||||
*/
|
||||
class TrackerUtil
|
||||
{
|
||||
/**
|
||||
* Adds profiles to the debugger to help track certain game objects.
|
||||
* NOTE: This isn't the full list of profiles made, as sometimes they're made
|
||||
* on in various places in code for random debugging purposes!
|
||||
* Might be good to put them all here though!
|
||||
*/
|
||||
public static function initTrackers():Void
|
||||
{
|
||||
Tracker.addProfile(new TrackerProfile(Highscore, ["tallies"]));
|
||||
FlxG.console.registerClass(Highscore);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package funkin.util.plugins;
|
||||
|
||||
import flixel.FlxBasic;
|
||||
import funkin.util.tools.TimerTools;
|
||||
|
||||
/**
|
||||
* A plugin which adds functionality to press `Ins` to immediately perform memory garbage collection.
|
||||
|
@ -23,10 +24,9 @@ class MemoryGCPlugin extends FlxBasic
|
|||
|
||||
if (FlxG.keys.justPressed.INSERT)
|
||||
{
|
||||
var perfStart:Float = Sys.time();
|
||||
var perfStart:Float = TimerTools.start();
|
||||
funkin.util.MemoryUtil.collect(true);
|
||||
var perfEnd:Float = Sys.time();
|
||||
trace("Memory GC took " + (perfEnd - perfStart) + " seconds");
|
||||
trace('Memory GC took: ${TimerTools.seconds(perfStart)}');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
package funkin.util.tools;
|
||||
|
||||
class DynamicTools
|
||||
{
|
||||
/**
|
||||
* Creates a full clone of the input `Dynamic`. Only guaranteed to work on anonymous structures.
|
||||
* @param input The `Dynamic` to clone.
|
||||
* @return A clone of the input `Dynamic`.
|
||||
*/
|
||||
public static function clone(input:Dynamic):Dynamic
|
||||
{
|
||||
return Reflect.copy(input);
|
||||
}
|
||||
}
|
|
@ -12,4 +12,13 @@ class FloatTools
|
|||
{
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
/**
|
||||
Round a float to a certain number of decimal places.
|
||||
**/
|
||||
public static function round(number:Float, ?precision = 2):Float
|
||||
{
|
||||
number *= Math.pow(10, precision);
|
||||
return Math.round(number) / Math.pow(10, precision);
|
||||
}
|
||||
}
|
||||
|
|
30
source/funkin/util/tools/TimerTools.hx
Normal file
30
source/funkin/util/tools/TimerTools.hx
Normal file
|
@ -0,0 +1,30 @@
|
|||
package funkin.util.tools;
|
||||
|
||||
import funkin.util.tools.FloatTools;
|
||||
import haxe.Timer;
|
||||
|
||||
class TimerTools
|
||||
{
|
||||
public static function start():Float
|
||||
{
|
||||
return Timer.stamp();
|
||||
}
|
||||
|
||||
private static function took(start:Float, ?end:Float):Float
|
||||
{
|
||||
var endOrNow:Float = end != null ? end : Timer.stamp();
|
||||
return endOrNow - start;
|
||||
}
|
||||
|
||||
public static function seconds(start:Float, ?end:Float, ?precision = 2):String
|
||||
{
|
||||
var seconds:Float = FloatTools.round(took(start, end), precision);
|
||||
return '${seconds} seconds';
|
||||
}
|
||||
|
||||
public static function ms(start:Float, ?end:Float):String
|
||||
{
|
||||
var seconds:Float = took(start, end);
|
||||
return '${seconds * 1000} ms';
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue