assets submod

This commit is contained in:
Cameron Taylor 2024-03-04 18:15:01 -05:00
commit 078dc8e9ee
46 changed files with 831 additions and 457 deletions

View file

@ -3,21 +3,46 @@ description: "sets up haxe shit, using HMM!"
runs:
using: "composite"
steps:
- uses: funkincrew/ci-haxe@v3
- name: Install Haxe lol
uses: funkincrew/ci-haxe@v3.1.0
with:
haxe-version: 4.3.3
- name: Config haxelib
run: |
haxelib config
haxelib --never install haxelib 4.1.0 --global
haxelib --never deleterepo || true
haxelib --never newrepo
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
haxelib --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
shell: bash
- name: Installing Haxe lol
- name: Gather debug info
run: |
cat << EOF >> "$GITHUB_STEP_SUMMARY"
## haxe
- version: \`$(haxe -version)\`
- exe: \`$(which haxe)\`
## haxelib
- version: \`$(haxelib version)\`
- exe: \`$(which haxelib)\`
- path: \`$HAXEPATH\`
### local
- config: \`$(haxelib config)\`
- path: \`$(haxelib path haxelib || true)\`
### global
- config: \`$(haxelib config --global)\`
- path: \`$(haxelib path haxelib --global || true)\`
### system
- version: \`$(haxelib --system version)\`
- local: \`$(haxelib --system config)\`
- global: \`$(haxelib --system config --global)\`
EOF
shell: bash
- name: Install hmm
# hmm only supports global installs
run: |
haxe -version
haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git master
haxelib version
haxelib --global install hmm
shell: bash
- name: dependency install cache
- name: Restore cached dependencies
id: cache-hmm
uses: actions/cache@v4
with:

View file

@ -8,71 +8,80 @@ jobs:
runs-on: [self-hosted, linux]
container: ubuntu:23.10
steps:
- name: prepare container
- name: Install tools missing in container
run: |
apt update
apt install sudo git curl unzip -y
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: get token from gh app
apt install -y sudo git curl unzip
- name: Fix git config on posix runner
run: |
git config --global --add safe.directory ${{ github.workspace }}
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
- name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- uses: ./.github/actions/setup-haxeshit
- name: gather game dependencies
- name: Install Haxe, dependencies
uses: ./.github/actions/setup-haxeshit
- name: Install native dependencies
run: |
sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
- name: build game
apt install -y \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
libasound2-dev
- name: Build game
run: |
haxelib run lime build html5 -release --times -DGITHUB_BUILD
ls
- uses: ./.github/actions/upload-itch
- name: Upload build artifacts
uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/html5/bin
target: html5
create-nightly-win:
runs-on: [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
View file

@ -6,3 +6,4 @@ dump/
export/
RECOVER_*.fla
shitAudio/
.build_time

View file

@ -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/

View file

@ -71,6 +71,7 @@
"haxe.displayPort": "auto",
"haxe.enableCompilationServer": false,
"haxe.enableServerView": true,
"haxe.displayServer": {
"arguments": ["-v"]
},

2
assets

@ -1 +1 @@
Subproject commit 6eeb023a2273de712f44ff569995ac9b5774e9ef
Subproject commit 55c602f2adbbd84de541ea86e5e646c4d2a1df0b

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -30,6 +30,7 @@ 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;
#if discord_rpc
import Discord.DiscordClient;
@ -219,7 +220,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 +237,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)}');
}
/**

View file

@ -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);
}
}
}

View file

@ -15,12 +15,12 @@ class Preferences
static function get_naughtyness():Bool
{
return Save.get().options.naughtyness;
return Save.instance.options.naughtyness;
}
static function set_naughtyness(value:Bool):Bool
{
var save = Save.get();
var save = Save.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;

View file

@ -86,10 +86,10 @@ class NGUtil
#end
var onSessionFail:Error->Void = null;
if (sessionId == null && Save.get().ngSessionId != null)
if (sessionId == null && Save.instance.ngSessionId != null)
{
trace("using stored session id");
sessionId = Save.get().ngSessionId;
sessionId = Save.instance.ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
@ -159,8 +159,8 @@ class NGUtil
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
Save.get().ngSessionId = NG.core.sessionId;
Save.get().flush();
Save.instance.ngSessionId = NG.core.sessionId;
Save.instance.flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
@ -174,8 +174,8 @@ class NGUtil
{
NG.core.logOut();
Save.get().ngSessionId = null;
Save.get().flush();
Save.instance.ngSessionId = null;
Save.instance.flush();
}
// --- MEDALS

View file

@ -86,10 +86,10 @@ class NGio
#end
var onSessionFail:Error->Void = null;
if (sessionId == null && Save.get().ngSessionId != null)
if (sessionId == null && Save.instance.ngSessionId != null)
{
trace("using stored session id");
sessionId = Save.get().ngSessionId;
sessionId = Save.instance.ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
@ -159,8 +159,8 @@ class NGio
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
Save.get().ngSessionId = NG.core.sessionId;
Save.get().flush();
Save.instance.ngSessionId = NG.core.sessionId;
Save.instance.flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
@ -174,8 +174,8 @@ class NGio
{
NG.core.logOut();
Save.get().ngSessionId = null;
Save.get().flush();
Save.instance.ngSessionId = null;
Save.instance.flush();
}
// --- MEDALS

View file

@ -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;

View file

@ -3,7 +3,6 @@ package funkin.audio.visualize;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import funkin.audio.visualize.dsp.FFT;
import haxe.Timer;
import lime.system.ThreadPool;
import lime.utils.Int16Array;
import funkin.util.MathUtil;

View file

@ -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;
}

View file

@ -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);
}

View file

@ -32,18 +32,21 @@ class StageData
bf:
{
zIndex: 0,
scale: 1,
position: [0, 0],
cameraOffsets: [-100, -100]
},
dad:
{
zIndex: 0,
scale: 1,
position: [0, 0],
cameraOffsets: [100, -100]
},
gf:
{
zIndex: 0,
scale: 1,
position: [0, 0],
cameraOffsets: [0, 0]
}
@ -114,6 +117,7 @@ typedef StageDataProp =
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
@:optional
@:default(Left(1.0))
var scale:haxe.ds.Either<Float, Array<Float>>;
/**
@ -190,6 +194,13 @@ typedef StageDataCharacter =
@:default([0, 0])
var position:Array<Float>;
/**
* The scale to render the character at.
*/
@:optional
@:default(1)
var scale:Float;
/**
* The camera offsets to apply when focusing on the character on this stage.
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF

View file

@ -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();
}
}

View file

@ -61,7 +61,7 @@ class PolymodHandler
createModRoot();
trace("Initializing Polymod (using configured mods)...");
loadModsById(Save.get().enabledModIds);
loadModsById(Save.instance.enabledModIds);
}
/**
@ -236,7 +236,7 @@ class PolymodHandler
public static function getEnabledMods():Array<ModMetadata>
{
var modIds = Save.get().enabledModIds;
var modIds = Save.instance.enabledModIds;
var modMetadata = getAllMods();
var enabledMods = [];
for (item in modMetadata)

View file

@ -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.
*/

View file

@ -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();
});

View file

@ -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});
}

View file

@ -97,7 +97,7 @@ typedef PlayStateParams =
?targetDifficulty:String,
/**
* The variation to play on.
* @default `Constants.DEFAULT_VARIATION` .
* @default `Constants.DEFAULT_VARIATION`
*/
?targetVariation:String,
/**
@ -118,8 +118,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 +216,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 +282,12 @@ class PlayState extends MusicBeatSubState
*/
public var isPracticeMode: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.
*/
@ -550,6 +568,7 @@ class PlayState extends MusicBeatSubState
isPracticeMode = params.practiceMode ?? false;
isMinimalMode = params.minimalMode ?? false;
startTimestamp = params.startTimestamp ?? 0.0;
playbackRate = params.playbackRate ?? 1.0;
overrideMusic = params.overrideMusic ?? false;
previousCameraFollowPoint = params.cameraFollowPoint;
@ -772,11 +791,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 +934,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 +960,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 +986,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 +1025,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 +1527,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 +1547,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
@ -1783,14 +1826,17 @@ 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();
FlxG.sound.music.fadeTween?.cancel();
trace('Playing vocals...');
add(vocals);
vocals.play();
vocals.pitch = playbackRate;
resyncVocals();
#if discord_rpc
@ -2000,7 +2046,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 +2055,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;
}
@ -2155,13 +2201,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 +2249,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 +2425,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 +2443,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 +2622,9 @@ class PlayState extends MusicBeatSubState
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
};
if (Save.get().isSongHighScore(currentSong.id, currentDifficulty, data))
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 +2672,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

View file

@ -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

View file

@ -60,7 +60,7 @@ class BaseCharacter extends Bopper
@:allow(funkin.ui.debug.anim.DebugBoundingState)
final _data:CharacterData;
final singTimeSec:Float;
final singTimeSteps:Float;
/**
* The offset between the corner of the sprite and the origin of the sprite (at the character's feet).
@ -180,7 +180,7 @@ class BaseCharacter extends Bopper
{
this.characterName = _data.name;
this.name = _data.name;
this.singTimeSec = _data.singTime;
this.singTimeSteps = _data.singTime;
this.globalOffsets = _data.offsets;
this.flipX = _data.flipX;
}
@ -193,6 +193,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);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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.

View file

@ -659,7 +659,6 @@ class Strumline extends FlxSpriteGroup
if (noteSprite != null)
{
noteSprite.strumTime = note.time;
noteSprite.direction = note.getDirection();
noteSprite.noteData = note;

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -11,8 +11,7 @@ import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import thx.semver.Version;
@:nullSafety
@:forward(volume, mute)
abstract Save(RawSaveData)
class Save
{
// Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2";
@ -25,6 +24,20 @@ abstract Save(RawSaveData)
static final SAVE_PATH_LEGACY:String = 'ninjamuffin99';
static final SAVE_NAME_LEGACY:String = 'funkin';
public static var instance(get, never):Save;
static var _instance:Null<Save> = null;
static function get_instance():Save
{
if (_instance == null)
{
_instance = new Save(FlxG.save.data);
}
return _instance;
}
var data:RawSaveData;
public static function load():Void
{
trace("[SAVE] Loading save...");
@ -33,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>

View file

@ -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;

View file

@ -920,12 +920,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 +2163,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 +2191,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> = [];
@ -5308,6 +5308,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
{
@ -5357,6 +5361,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
practiceMode: playtestPracticeMode,
minimalMode: minimal,
startTimestamp: startTimestamp,
playbackRate: playbackRate,
overrideMusic: true,
});

View file

@ -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)
{

View file

@ -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.

View file

@ -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

View file

@ -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);
}

View file

@ -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,
// TODO: Make this an option!
// startTimestamp: 0.0,
// TODO: Make this an option!
// playbackRate: 0.5,
practiceMode: false,
minimalMode: false,
}, 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;

View file

@ -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);

View file

@ -649,7 +649,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
}

View file

@ -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)}');
}
}

View file

@ -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);
}
}

View 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';
}
}