1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-21 01:19:26 +00:00

Merge branch 'rewrite/master' into feature/pico-playable-christmas

This commit is contained in:
Cameron Taylor 2024-08-29 13:40:56 -04:00
commit b27aa4c94d
96 changed files with 4981 additions and 1758 deletions

4
.gitattributes vendored
View file

@ -1 +1,3 @@
* text=auto eol=lf
* text=auto eol=lf
*.hxc linguist-language=Haxe
*.hxp linguist-language=Haxe

View file

@ -44,7 +44,7 @@ runs:
g++ \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
libasound2-dev
libasound2-dev libpulse-dev
ln -s /usr/lib/x86_64-linux-gnu/libffi.so.8 /usr/lib/x86_64-linux-gnu/libffi.so.6 || true
- name: Install linux-specific dependencies
if: ${{ runner.os == 'Linux' && contains(inputs.targets, 'linux') }}
@ -56,12 +56,17 @@ runs:
shell: bash
run: |
echo "TIMER_HAXELIB=$(date +%s)" >> "$GITHUB_ENV"
haxelib --debug --never install haxelib 4.1.0 --global
haxelib --debug --never deleterepo || true
haxelib fixrepo --global || true
haxelib --debug --never --global install haxelib 4.1.0
haxelib --debug --global set haxelib 4.1.0
haxelib --global remove haxelib git || true
haxelib --global remove hmm || true
rm -rf .haxelib
haxelib --debug --never --global git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
haxelib --debug --never --global git hmm https://github.com/FunkinCrew/hmm funkin-patches
haxelib --debug --never newrepo
haxelib version
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
haxelib --debug --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
haxelib --debug --global install hmm
echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV"
- name: Restore cached dependencies
@ -75,7 +80,11 @@ runs:
name: Prep git for dependency install
uses: gacts/run-and-post-run@v1
with:
run: git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/
run: |
git config --global --name-only --get-regexp 'url\.https\:\/\/x-access-token:.+@github\.com\/\.insteadOf' \
| xargs git config --global --unset
git config -l --show-scope --show-origin
git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/
post: git config --global --unset 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf'
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}

1
.github/labeler.yml vendored
View file

@ -1,7 +1,6 @@
# Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder
Documentation:
- changed-files:
- any-glob-to-any-file:
- any-glob-to-any-file:
- docs/*
- '**/*.md'

View file

@ -45,7 +45,11 @@ jobs:
uses: ./.github/actions/setup-haxe
with:
gh-token: ${{ steps.app_token.outputs.token }}
- name: Setup HXCPP dev commit
run: |
cd .haxelib/hxcpp/git/tools/hxcpp
haxe compile.hxml
cd ../../../../..
- name: Build game
if: ${{ matrix.target == 'windows' }}
run: |
@ -107,7 +111,9 @@ jobs:
name: Install dependencies
run: |
git config --global 'url.https://x-access-token:${{ steps.app_token.outputs.token }}@github.com/.insteadOf' https://github.com/
git config --global advice.detachedHead false
haxelib --global run hmm install -q
cd .haxelib/hxcpp/git/tools/hxcpp && haxe compile.hxml
- if: ${{ matrix.target != 'html5' }}
name: Restore hxcpp cache

30
.vscode/settings.json vendored
View file

@ -94,12 +94,12 @@
{
"label": "Windows / Debug",
"target": "windows",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Linux / Debug",
"target": "linux",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug",
@ -109,7 +109,7 @@
{
"label": "Windows / Debug (FlxAnimate Test)",
"target": "windows",
"args": ["-debug", "-DANIMATE", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
@ -119,7 +119,7 @@
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
"args": ["-debug", "-DFREEPLAY", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
@ -132,13 +132,13 @@
"args": [
"-debug",
"-DSONG=bopeebo -DDIFFICULTY=normal",
"-DFORCE_DEBUG_VERSION"
"-DFEATURE_DEBUG_FUNCTIONS"
]
},
{
"label": "Windows / Debug (Straight to Play - 2hot)",
"target": "windows",
"args": ["-debug", "-DSONG=2hot", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
@ -148,7 +148,7 @@
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
"args": ["-debug", "-DDIALOGUE", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Conversation Test)",
@ -163,7 +163,7 @@
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
"args": ["-debug", "-DCHARTING", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Chart Editor)",
@ -173,12 +173,12 @@
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
"args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DANIMDEBUG", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Debug hxCodec)",
"target": "windows",
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
@ -188,7 +188,7 @@
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
"args": ["-debug", "-DLATENCY", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Latency Test)",
@ -198,7 +198,7 @@
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
"args": ["-debug", "-DWAVEFORM", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Release",
@ -218,17 +218,17 @@
{
"label": "HTML5 / Debug",
"target": "html5",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
"args": ["-debug", "-watch", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-watch", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "macOS / Debug",
"target": "mac",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "macOS / Release",

View file

@ -4,6 +4,62 @@ All notable changes will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.0] - 2024-08-??
### Added
- Added a new Character Select screen to switch between playable characters in Freeplay
- Modding isn't 100% there but we're working on it!
- Added Pico as a playable character! Unlock him by completing Weekend 1 (if you haven't already done that)
- The songs from Weekend 1 have moved; you must now switch to Pico in Freeplay to access them
- Added ## new Pico remixes! Access them by selecting Pico from in the Character Select screen
- Added 2 new Erect remixes! Access them by switching difficulty on the song
- Implemented support for a new Instrumental Selector in Freeplay
- Beating a Pico remix lets you use that instrumental when playing as Boyfriend
- Added the first batch of Erect Stages! These graphical overhauls of the original stages will be used when playing Erect remixes and Pico remixes
- Implemented support for scripted Note Kinds. You can use HScript define a different note style to display for these notes as well as custom behavior. (community feature by lemz1)
- Implemented a new Strumline Background option, to display a darkened background behind the strumline with your choice of opacity.
- Implemented support for Numeric and Selector options in the Options menu. (community feature by FlooferLand)
## Changed
- Girlfriend and Nene now perform previously unused animations when you achieve a large combo, or drop a large combo.
- The pixel character icons in the Freeplay menu now display an animation!
- Altered how Week 6 displays sprites to make things look more retro.
- Character offsets are now independent of the character's scale.
- This should resolve issues with offsets when porting characters from older mods.
- Pixel character offsets have been modified to compensate.
- Note style data can now specify custom combo count graphics, judgement graphics, countdown graphics, and countdown audio. (community feature by anysad)
- These were previously using hardcoded values based on whether the stage was `school` or `schoolEvil`.
- The `danceEvery` property of characters and stage props can now use values with a precision of `0.25`, to play their idle animation up to four times per beat.
- Reworked the JSON merging system in Polymod; you can now include JSONPatch files under `_merge` in your mod folder to add, modify, or remove values in a JSON without replacing it entirely!
- Cutscenes now automatically pause when tabbing out (community fix by AbnormalPoof)
- Characters will now respect the `danceEvery` property (community fix by gamerbross)
- The F5 function now reloads the current song's chart data from disc (community feature by gamerbross)
- Refactored the compilation guide and added common troubleshooting steps (community fix by Hundrec)
- Made several layout improvements and fixes to the Animation Offsets editor in the Debug menu (community fix by gamerbross)
- Fixed a bug where the Back sound would be not played when leaving the Story menu and Options menu (community fix by AppleHair)
- Animation offsets no longer directly modify the `x` and `y` position of props, which makes props work better with tweens (community fix by Sword352)
- The YEAH! events in Tutorial now use chart events rather than being hard-coded (community fix by anysad)
- The player's Score now displays commas in it (community fix by loggo)
## Fixed
- Fixed an issue where songs with no notes would crash on the Results screen.
- Fixed an issue where the old icon easter egg would not work properly on pixel levels.
- Fixed an issue where you could play notes during the Thorns cutscene.
- Fixed an issue where the Heart icon when favoriting a song in Freeplay would be malformed.
- Fixed an issue where Pico's death animation displays a faint blue background (community fix by doggogit)
- Fixed an issue where mod songs would not play a preview in the Freeplay menu (community fix by KarimAkra)
- Fixed an issue where the Memory Usage counter could overflow and display a negative number (community fix by KarimAkra)
- Fixed an issue where pressing the Chart Editor keybind while playtesting a chart would reset the chart editor (community fix by gamerbross)
- Fixed a crash bug when pressing F5 after seeing the sticker transition (community fix by gamerbross)
- Fixed an issue where the Story Mode menu couldn't be scrolled with a mouse (community fix by JVNpixels)
- Fixed an issue causing the song to majorly desync sometimes (community fix by Burgerballs)
- Fixed an issue where the Freeplay song preview would not respect the instrumental ID specified in the song metadata (community fix by AppleHair)
- Fixed an issue where Tankman's icon wouldn't display in the Chart Editor (community fix by hundrec)
- Fixed an issue where pausing the game during a camera zoom would zoom the pause menu. (community fix by gamerbros)
- Fixed an issue where certain UI elements would not flash at a consistent rate (community fix by cyn0x8)
- Fixed an issue where the game would not use the placeholder health icon as a fallback (community fix by gamerbross)
- Fixed an issue where the chart editor could get stuck creating a hold note when using Live Inputs (community fix by gamerbross)
- Fixed an issue where character graphics could not be placed in week folders (community fix by 7oltan)
- Fixed a crash issue when a Freeplay song has no `Normal` difficulty (community fix by Applehair and gamerbross)
- Fixed an issue in Story Mode where a song that isn't valid for the current variation could be selected (community fix by Applehair)
## [0.4.1] - 2024-06-12
### Added
- Pressing ESCAPE on the title screen on desktop now exits the game, allowing you to exit the game while in fullscreen on desktop

View file

@ -1,269 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://lime.openfl.org/project/1.0.4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://lime.openfl.org/project/1.0.4 http://lime.openfl.org/xsd/project-1.0.4.xsd">
<!-- _________________________ Application Settings _________________________ -->
<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.5.0" company="ninjamuffin99" />
<!--Switch Export with Unique ApplicationID and Icon-->
<set name="APP_ID" value="0x0100f6c013bbc000" />
<!--
Define the OpenFL sprite which displays the preloader.
You can't replace the preloader's logic here, sadly, but you can extend it.
Basic preloading logic is done by `openfl.display.Preloader`.
-->
<app preloader="funkin.ui.transition.preload.FunkinPreloader" />
<!--Minimum without FLX_NO_GAMEPAD: 11.8, without FLX_NO_NATIVE_CURSOR: 11.2-->
<set name="SWF_VERSION" value="11.8" />
<!-- ____________________________ Window Settings ___________________________ -->
<!--These window settings apply to all targets-->
<window width="1280" height="720" fps="60" background="#000000" hardware="true" vsync="false" />
<!--HTML5-specific-->
<window if="html5" resizable="true" />
<!--Desktop-specific-->
<window if="desktop" orientation="landscape" fullscreen="false" resizable="true" vsync="false" />
<!--Mobile-specific-->
<window if="mobile" orientation="landscape" fullscreen="true" width="0" height="0" resizable="false" />
<!-- _____________________________ Path Settings ____________________________ -->
<set name="BUILD_DIR" value="export/debug" if="debug" />
<set name="BUILD_DIR" value="export/release" unless="debug" />
<set name="BUILD_DIR" value="export/32bit" if="32bit" />
<source path="source" />
<assets path="assets/preload" rename="assets" exclude="*.ogg|*.wav" if="web" />
<assets path="assets/preload" rename="assets" exclude="*.mp3|*.wav" unless="web" />
<define name="PRELOAD_ALL" unless="web" />
<define name="NO_PRELOAD_ALL" unless="PRELOAD_ALL" />
<section if="PRELOAD_ALL">
<library name="songs" preload="true" />
<library name="shared" preload="true" />
<library name="tutorial" preload="true" />
<library name="week1" preload="true" />
<library name="week2" preload="true" />
<library name="week3" preload="true" />
<library name="week4" preload="true" />
<library name="week5" preload="true" />
<library name="week6" preload="true" />
<library name="week7" preload="true" />
<library name="weekend1" preload="true" />
<library name="videos" preload="true" />
</section>
<section if="NO_PRELOAD_ALL">
<library name="songs" preload="false" />
<library name="shared" preload="false" />
<library name="tutorial" preload="false" />
<library name="week1" preload="false" />
<library name="week2" preload="false" />
<library name="week3" preload="false" />
<library name="week4" preload="false" />
<library name="week5" preload="false" />
<library name="week6" preload="false" />
<library name="week7" preload="false" />
<library name="weekend1" preload="false" />
<library name="videos" preload="false" />
</section>
<library name="art" preload="false" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" />
<!-- Videos go in their own library because web never needs to preload them, they can just be streamed. -->
<assets path="assets/videos" library="videos" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.mp3|*.wav" unless="web" />
<!-- <assets path='example_mods' rename='mods' embed='false'/> -->
<!--
AUTOMATICALLY MOVING EXAMPLE MODS INTO THE BUILD CAUSES ISSUES
Currently, this line will add the mod files to the library manifest,
which causes issues if the mod is not enabled.
If we can exclude the `mods` folder from the manifest, we can re-enable this line.
<assets path='example_mods' rename='mods' embed='false' exclude="*.md" />
-->
<assets path="art/readme.txt" rename="do NOT readme.txt" library="art"/>
<assets path="CHANGELOG.md" rename="changelog.txt" library="art"/>
<!-- NOTE FOR FUTURE SELF SINCE FONTS ARE ALWAYS FUCKY
TO FIX ONE OF THEM, I CONVERTED IT TO OTF. DUNNO IF YOU NEED TO
THEN UHHH I USED THE NAME OF THE FONT WITH SETFORMAT() ON THE TEXT!!!
NOT USING A DIRECT THING TO THE ASSET!!!
-->
<assets path="assets/fonts" embed="true" />
<!-- If compiled via github actions, show debug version number. -->
<define name="FORCE_DEBUG_VERSION" if="GITHUB_BUILD" />
<define name="NO_REDIRECT_ASSETS_FOLDER" if="GITHUB_BUILD" />
<define name="TOUCH_HERE_TO_PLAY" if="web" />
<!-- _______________________________ Libraries ______________________________ -->
<haxelib name="lime" /> <!-- Game engine backend -->
<haxelib name="openfl" /> <!-- Game engine backend -->
<haxelib name="flixel" /> <!-- Game engine -->
<haxedev set="webgl" />
<haxelib name="flixel-addons" /> <!-- Additional utilities for Flixel -->
<haxelib name="hscript" /> <!-- Scripting -->
<haxelib name="flixel-ui" /> <!-- UI framework (DEPRECATED) -->
<haxelib name="haxeui-core" /> <!-- UI framework -->
<haxelib name="haxeui-flixel" /> <!-- Integrate HaxeUI with Flixel -->
<haxelib name="flixel-text-input" /> <!-- Improved text field rendering for HaxeUI -->
<haxelib name="polymod" /> <!-- Modding framework -->
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
<haxelib name="hxCodec" if="desktop" unless="hl" /> <!-- Video playback -->
<haxelib name="funkin.vis"/>
<haxelib name="grig.audio" />
<haxelib name="FlxPartialSound" /> <!-- Loading partial sound data -->
<haxelib name="json2object" /> <!-- JSON parsing -->
<haxelib name="thx.core" /> <!-- General utility library, "the lodash of Haxe" -->
<haxelib name="thx.semver" /> <!-- Version string handling -->
<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
<!--Disable the Flixel core focus lost screen-->
<haxedef name="FLX_NO_FOCUS_LOST_SCREEN" />
<!--Disable the Flixel core debugger. Automatically gets set whenever you compile in release mode!-->
<haxedef name="FLX_NO_DEBUG" unless="debug || FORCE_DEBUG_VERSION" />
<!--Enable this for Nape release builds for a serious peformance improvement-->
<haxedef name="NAPE_RELEASE_BUILD" unless="debug" />
<!--
Hide deprecation warnings until they're fixed.
TODO: REMOVE THIS!!!!
<haxeflag name="-w" value="-WDeprecated" />
-->
<!-- Haxe 4.3.0+: Enable pretty syntax errors and stuff. -->
<haxedef name="message.reporting" value="pretty" />
<!-- _________________________________ Custom _______________________________ -->
<!-- Disable trace() calls in release builds to bump up performance.
<haxeflag name="- -no-traces" unless="debug" />-->
<!-- HScript relies heavily on Reflection, which means we can't use DCE. -->
<haxeflag name="-dce no" />
<!-- Ensure all Funkin' classes are available at runtime. -->
<haxeflag name="--macro" value="include('funkin')" />
<!-- Ensure all UI components are available at runtime. -->
<haxeflag name="--macro" value="include('haxe.ui.backend.flixel.components')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.dialogs')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.menus')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.properties')" />
<haxeflag name="--macro" value="include('haxe.ui.core')" />
<haxeflag name="--macro" value="include('haxe.ui.components')" />
<haxeflag name="--macro" value="include('haxe.ui.containers')" />
<!--
Ensure additional class packages are available at runtime (some only really used by scripts).
Ignore packages we can't include.
-->
<haxeflag name="--macro" value="include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*' ])" />
<!-- Necessary to provide stack traces for HScript. -->
<haxedef name="hscriptPos" />
<haxedef name="safeMode"/>
<haxedef name="HXCPP_CHECK_POINTER" />
<haxedef name="HXCPP_STACK_LINE" />
<haxedef name="HXCPP_STACK_TRACE" />
<!-- This macro allows addition of new functionality to existing Flixel. -->
<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')" />
<!--Place custom nodes like icons here (higher priority to override the HaxeFlixel icon)-->
<icon path="art/icon16.png" size="16" />
<icon path="art/icon32.png" size="32" />
<icon path="art/icon64.png" size="64" />
<icon path="art/iconOG.png" />
<haxedef name="CAN_OPEN_LINKS" unless="switch" />
<haxedef name="CAN_CHEAT" if="switch debug" />
<!-- I don't remember what this is for. -->
<haxedef name="haxeui_no_mouse_reset" />
<!-- Clicking outside a dialog should deselect the current focused component. -->
<haxedef name="haxeui_focus_out_on_click" />
<!-- Required to use haxe.ui.backend.flixel.UIState with build macros. -->
<haxedef name="haxeui_dont_impose_base_class" />
<haxedef name="HARDCODED_CREDITS" />
<!-- Skip the Intro -->
<section if="debug">
<!-- Starts the game at the specified week, at the first song -->
<!-- <haxedef name="week" value="1" if="debug"/> -->
<!-- Starts the game at the specified song -->
<!-- <haxedef name="song" value="bopeebo" if="debug"/> -->
<!-- Difficulty, only used for week or song, defaults to 1 -->
<!-- <haxedef name="dif" value="2" if="debug"/> -->
</section>
<section if="newgrounds">
<!-- Enables Ng.core.verbose -->
<!-- <haxedef name="NG_VERBOSE" /> -->
<!-- Enables a NG debug session, so medals don't permently unlock -->
<!-- <haxedef name="NG_DEBUG" /> -->
<!-- pretends that the saved session Id was expired, forcing the reconnect prompt -->
<!-- <haxedef name="NG_FORCE_EXPIRED_SESSION" if="debug" /> -->
</section>
<!-- Uncomment this to wipe your input settings. -->
<!-- <haxedef name="CLEAR_INPUT_SAVE"/> -->
<section if="debug" unless="NO_REDIRECT_ASSETS_FOLDER || html5 || GITHUB_BUILD">
<!--
Use the parent assets folder rather than the exported one
No more will we accidentally undo our changes!
-->
<haxedef name="REDIRECT_ASSETS_FOLDER" />
</section>
<section>
<!--
This flag enables the popup/crashlog error handler.
However, it also messes with breakpoints on some platforms.
-->
<haxedef name="openfl-enable-handle-error" />
</section>
<!-- Run a script before and after building. -->
<prebuild haxe="source/Prebuild.hx"/> -->
<postbuild haxe="source/Postbuild.hx"/> -->
<!-- Enable this on platforms which do not support dropping files onto the window. -->
<haxedef name="FILE_DROP_UNSUPPORTED" if="mac" />
<section unless="FILE_DROP_UNSUPPORTED">
<haxedef name="FILE_DROP_SUPPORTED" />
</section>
<!-- Enable this on platforms which do not support the edsior views. -->
<haxedef name="CHART_EDITOR_UNSUPPORTED" if="web" />
<haxedef name="CHART_EDITOR_SUPPORTED" unless="web"/>
<!-- Options for Polymod -->
<section if="polymod">
<!-- Turns on additional debug logging. -->
<haxedef name="POLYMOD_DEBUG" value="true" if="debug" />
<!-- The file extension to use for script files. -->
<haxedef name="POLYMOD_SCRIPT_EXT" value=".hscript" />
<!-- Which asset library to use for scripts. -->
<haxedef name="POLYMOD_SCRIPT_LIBRARY" value="scripts" />
<!-- The base path from which scripts should be accessed. -->
<haxedef name="POLYMOD_ROOT_PATH" value="scripts/" />
<!-- Determines the subdirectory of the mod folder used for file appending. -->
<haxedef name="POLYMOD_APPEND_FOLDER" value="_append" />
<!-- Determines the subdirectory of the mod folder used for file merges. -->
<haxedef name="POLYMOD_MERGE_FOLDER" value="_merge" />
<!-- Determines the file in the mod folder used for metadata. -->
<haxedef name="POLYMOD_MOD_METADATA_FILE" value="_polymod_meta.json" />
<!-- Determines the file in the mod folder used for the icon. -->
<haxedef name="POLYMOD_MOD_ICON_FILE" value="_polymod_icon.png" />
</section>
</project>

2
art

@ -1 +1 @@
Subproject commit faeba700c5526bd4fd57ccc927d875c82b9d3553
Subproject commit 0bb988c49788fd25a230b56dd9e4448838bc79c9

2
assets

@ -1 +1 @@
Subproject commit c7589a95af2709d240e1b1a2994e68a04565b00a
Subproject commit c559b25e9a294837e6ba98397dad0ef32f325fd6

View file

@ -83,7 +83,7 @@ apt-fast install -y --no-install-recommends \
libc6-dev libffi-dev \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
libasound2-dev \
libasound2-dev libpulse-dev \
libvlc-dev libvlccore-dev
EOF
@ -137,8 +137,8 @@ ENV PATH="$HAXEPATH:$PATH"
RUN <<EOF
HOME=/etc haxelib setup "$HAXEPATH/lib"
haxelib --global --never install haxelib $haxelib_version
haxelib --global --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
haxelib --global --never install hmm
haxelib --global --never git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
haxelib --global --never git hmm https://github.com/FunkinCrew/hmm funkin-patches
EOF
# hxcpp

View file

@ -1,5 +1,12 @@
{
"dependencies": [
{
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "a1eab7b9bf507b87200a3341719054fe427f3b15",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
},
{
"name": "discord_rpc",
"type": "git",
@ -11,41 +18,36 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "a7d8e3bad89a0a3506a4714121f73d8e34522c49",
"ref": "599f38eeb502a8ba6439784036c2cfdc7b485260",
"url": "https://github.com/FunkinCrew/flixel"
},
{
"name": "flixel-addons",
"type": "git",
"dir": null,
"ref": "a523c3b56622f0640933944171efed46929e360e",
"ref": "9c6fb47968e894eb36bf10e94725cd7640c49281",
"url": "https://github.com/FunkinCrew/flixel-addons"
},
{
"name": "flixel-text-input",
"type": "haxelib",
"version": "1.1.0"
"type": "git",
"dir": null,
"ref": "951a0103a17bfa55eed86703ce50b4fb0d7590bc",
"url": "https://github.com/FunkinCrew/flixel-text-input"
},
{
"name": "flixel-ui",
"type": "git",
"dir": null,
"ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15",
"ref": "27f1ba626f80a6282fa8a187115e79a4a2133dc2",
"url": "https://github.com/HaxeFlixel/flixel-ui"
},
{
"name": "flxanimate",
"type": "git",
"dir": null,
"ref": "17e0d59fdbc2b6283a5c0e4df41f1c7f27b71c49",
"url": "https://github.com/FunkinCrew/flxanimate"
},
{
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "a1eab7b9bf507b87200a3341719054fe427f3b15",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
"ref": "768740a56b26aa0c072720e0d1236b94afe68e3e",
"url": "https://github.com/Dot-Stuff/flxanimate"
},
{
"name": "format",
@ -56,7 +58,7 @@
"name": "funkin.vis",
"type": "git",
"dir": null,
"ref": "38261833590773cb1de34ac5d11e0825696fc340",
"ref": "22b1ce089dd924f15cdc4632397ef3504d464e90",
"url": "https://github.com/FunkinCrew/funkVis"
},
{
@ -75,20 +77,22 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "5dc4c933bdc029f6139a47962e3b8c754060f210",
"ref": "22f7c5a8ffca90d4677cffd6e570f53761709fbc",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "57c1604d6b5174839d7e0e012a4dd5dcbfc129da",
"ref": "28bb710d0ae5d94b5108787593052165be43b980",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
"name": "hscript",
"type": "haxelib",
"version": "2.5.0"
"type": "git",
"dir": null,
"ref": "12785398e2f07082f05034cb580682e5671442a2",
"url": "https://github.com/FunkinCrew/hscript"
},
{
"name": "hxCodec",
@ -99,8 +103,10 @@
},
{
"name": "hxcpp",
"type": "haxelib",
"version": "4.3.2"
"type": "git",
"dir": null,
"ref": "904ea40643b050a5a154c5e4c33a83fd2aec18b1",
"url": "https://github.com/HaxeFoundation/hxcpp"
},
{
"name": "hxcpp-debug-server",
@ -121,11 +127,25 @@
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
"url": "https://github.com/FunkinCrew/json2object"
},
{
"name": "jsonpatch",
"type": "git",
"dir": null,
"ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
"url": "https://github.com/EliteMasterEric/jsonpatch"
},
{
"name": "jsonpath",
"type": "git",
"dir": null,
"ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
"url": "https://github.com/EliteMasterEric/jsonpath"
},
{
"name": "lime",
"type": "git",
"dir": null,
"ref": "872ff6db2f2d27c0243d4ff76802121ded550dd7",
"ref": "e0b2339e02fff91168789dbd1a0dd019ea3dda39",
"url": "https://github.com/FunkinCrew/lime"
},
{
@ -160,21 +180,21 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134",
"ref": "8306425c497766739510ab29e876059c96f77bd2",
"url": "https://github.com/FunkinCrew/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
"ref": "bfbe30d81601b3543d80dce580108ad6b7e182c7",
"ref": "96cfc5fa693b017e47f7cb13b765cc68698fa6b6",
"url": "https://github.com/larsiusprime/polymod"
},
{
"name": "thx.core",
"type": "git",
"dir": null,
"ref": "6240b6e136f7490d9298edbe8c1891374bd7cdf2",
"ref": "76d87418fadd92eb8e1b61f004cff27d656e53dd",
"url": "https://github.com/fponticelli/thx.core"
},
{

1109
project.hxp Normal file

File diff suppressed because it is too large Load diff

View file

@ -113,7 +113,7 @@ class Main extends Sprite
addChild(game);
#if debug
#if FEATURE_DEBUG_FUNCTIONS
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
#end

View file

@ -27,13 +27,14 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState;
import funkin.util.CLIUtil;
import funkin.util.CLIUtil.CLIParams;
import funkin.util.TimerUtil;
import funkin.util.TrackerUtil;
#if discord_rpc
#if FEATURE_DISCORD_RPC
import Discord.DiscordClient;
#end
@ -122,7 +123,7 @@ class InitState extends FlxState
//
// DISCORD API SETUP
//
#if discord_rpc
#if FEATURE_DISCORD_RPC
DiscordClient.initialize();
Application.current.onExit.add(function(exitCode) {
@ -143,7 +144,7 @@ class InitState extends FlxState
// Plugins provide a useful interface for globally active Flixel objects,
// that receive update events regardless of the current state.
// TODO: Move scripted Module behavior to a Flixel plugin.
#if debug
#if FEATURE_DEBUG_FUNCTIONS
funkin.util.plugins.MemoryGCPlugin.initialize();
#end
funkin.util.plugins.EvacuateDebugPlugin.initialize();
@ -176,6 +177,8 @@ class InitState extends FlxState
// Move it to use a BaseRegistry.
CharacterDataParser.loadCharacterCache();
NoteKindManager.loadScripts();
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
ModuleHandler.callOnCreate();
@ -241,11 +244,11 @@ class InitState extends FlxState
totalNotesHit: 140,
totalNotes: 190
}
// 2000 = loss
// 240 = good
// 230 = great
// 210 = excellent
// 190 = perfect
// 2400 total notes = 7% = LOSS
// 240 total notes = 79% = GOOD
// 230 total notes = 82% = GREAT
// 210 total notes = 91% = EXCELLENT
// 190 total notes = PERFECT
},
}));
#elseif ANIMDEBUG
@ -371,11 +374,16 @@ class InitState extends FlxState
//
// FLIXEL DEBUG SETUP
//
#if (debug || FORCE_DEBUG_VERSION)
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
#if FEATURE_DEBUG_FUNCTIONS
trace('Initializing Flixel debugger...');
#if !debug
// Make errors less annoying on release builds.
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
#end
// Make errors and warnings less annoying.
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;

View file

@ -1,13 +1,13 @@
package funkin.api.discord;
import Sys.sleep;
#if discord_rpc
#if FEATURE_DISCORD_RPC
import discord_rpc.DiscordRpc;
#end
class DiscordClient
{
#if discord_rpc
#if FEATURE_DISCORD_RPC
public function new()
{
trace("Discord Client starting...");

View file

@ -24,7 +24,7 @@ class NGUnsafe
NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event);
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib');
#end
#end
@ -39,7 +39,7 @@ class NGUnsafe
if (!medal.unlocked) medal.sendUnlock();
}
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib');
#end
#end
@ -63,7 +63,7 @@ class NGUnsafe
}
}
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end
#end

View file

@ -239,7 +239,7 @@ class NGio
NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event);
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib');
#end
#end
@ -254,7 +254,7 @@ class NGio
if (!medal.unlocked) medal.sendUnlock();
}
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib');
#end
#end
@ -278,7 +278,7 @@ class NGio
}
}
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end
#end

View file

@ -54,7 +54,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
public function initAnalyzer()
{
@:privateAccess
analyzer = new SpectralAnalyzer(snd._channel.__source, 7, 0.1, 40);
analyzer = new SpectralAnalyzer(snd._channel.__audioSource, 7, 0.1, 40);
#if desktop
// On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5

View file

@ -117,7 +117,7 @@ class VisShit
{
// Math.pow3
@:privateAccess
var buf = snd._channel.__source.buffer;
var buf = snd._channel.__audioSource.buffer;
// @:privateAccess
audioData = cast buf.data; // jank and hacky lol! kinda busted on HTML5 also!!

View file

@ -16,7 +16,7 @@ class WaveformDataParser
// Method 1. This only works if the sound has been played before.
@:privateAccess
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__source?.buffer;
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__audioSource?.buffer;
if (soundBuffer == null)
{

View file

@ -263,7 +263,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param version The entry's version (use `fetchEntryVersion(id)`).
* @return The created entry.
*/
public function parseEntryDataWithMigration(id:String, version:thx.semver.Version):Null<J>
public function parseEntryDataWithMigration(id:String, version:Null<thx.semver.Version>):Null<J>
{
if (version == null)
{

View file

@ -46,7 +46,7 @@ class SongEventRegistry
if (event != null)
{
trace(' Loaded built-in song event: (${event.id})');
trace(' Loaded built-in song event: ${event.id}');
eventCache.set(event.id, event);
}
else
@ -59,9 +59,9 @@ class SongEventRegistry
static function registerScriptedEvents()
{
var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses();
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return;
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
for (eventCls in scriptedEventClassNames)
{
var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");

View file

@ -38,6 +38,17 @@ class PlayerData
@:optional
public var freeplayDJ:Null<PlayerFreeplayDJData> = null;
/**
* Data for displaying this character in the Character Select menu.
* If null, exclude from Character Select.
*/
@:optional
public var charSelect:Null<PlayerCharSelectData> = null;
/**
* Data for displaying this character in the results screen.
*/
@:optional
public var results:Null<PlayerResultsData> = null;
/**
@ -97,6 +108,9 @@ class PlayerFreeplayDJData
@:optional
var cartoon:Null<PlayerFreeplayDJCartoonData>;
@:optional
var fistPump:Null<PlayerFreeplayDJFistPumpData>;
public function new()
{
animationMap = new Map();
@ -183,6 +197,58 @@ class PlayerFreeplayDJData
{
return cartoon?.channelChangeFrame ?? 60;
}
public function getFistPumpIntroStartFrame():Int
{
return fistPump?.introStartFrame ?? 0;
}
public function getFistPumpIntroEndFrame():Int
{
return fistPump?.introEndFrame ?? 0;
}
public function getFistPumpLoopStartFrame():Int
{
return fistPump?.loopStartFrame ?? 0;
}
public function getFistPumpLoopEndFrame():Int
{
return fistPump?.loopEndFrame ?? 0;
}
public function getFistPumpIntroBadStartFrame():Int
{
return fistPump?.introBadStartFrame ?? 0;
}
public function getFistPumpIntroBadEndFrame():Int
{
return fistPump?.introBadEndFrame ?? 0;
}
public function getFistPumpLoopBadStartFrame():Int
{
return fistPump?.loopBadStartFrame ?? 0;
}
public function getFistPumpLoopBadEndFrame():Int
{
return fistPump?.loopBadEndFrame ?? 0;
}
}
class PlayerCharSelectData
{
/**
* A zero-indexed number for the character's preferred position in the grid.
* 0 = top left, 4 = center, 8 = bottom right
* In the event of a conflict, the first character alphabetically gets it,
* and others get shifted over.
*/
@:optional
public var position:Null<Int>;
}
typedef PlayerResultsData =
@ -242,3 +308,30 @@ typedef PlayerFreeplayDJCartoonData =
var loopFrame:Int;
var channelChangeFrame:Int;
}
typedef PlayerFreeplayDJFistPumpData =
{
@:default(0)
var introStartFrame:Int;
@:default(4)
var introEndFrame:Int;
@:default(4)
var loopStartFrame:Int;
@:default(-1)
var loopEndFrame:Int;
@:default(0)
var introBadStartFrame:Int;
@:default(4)
var introBadEndFrame:Int;
@:default(4)
var loopBadStartFrame:Int;
@:default(-1)
var loopBadEndFrame:Int;
};

View file

@ -3,6 +3,7 @@ package funkin.data.freeplay.player;
import funkin.data.freeplay.player.PlayerData;
import funkin.ui.freeplay.charselect.PlayableCharacter;
import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter;
import funkin.save.Save;
class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
{
@ -53,13 +54,49 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
log('Loaded ${countEntries()} playable characters with ${ownedCharacterIds.size()} associations.');
}
public function countUnlockedCharacters():Int
{
var count = 0;
for (charId in listEntryIds())
{
var player = fetchEntry(charId);
if (player == null) continue;
if (player.isUnlocked()) count++;
}
return count;
}
public function hasNewCharacter():Bool
{
var characters = Save.instance.charactersSeen.clone();
for (charId in listEntryIds())
{
var player = fetchEntry(charId);
if (player == null) continue;
if (!player.isUnlocked()) continue;
if (characters.contains(charId)) continue;
// This character is unlocked but we haven't seen them in Freeplay yet.
return true;
}
// Fallthrough case.
return false;
}
/**
* Get the playable character associated with a given stage character.
* @param characterId The stage character ID.
* @return The playable character.
*/
public function getCharacterOwnerId(characterId:String):Null<String>
public function getCharacterOwnerId(characterId:Null<String>):Null<String>
{
if (characterId == null) return null;
return ownedCharacterIds[characterId];
}

View file

@ -0,0 +1,31 @@
# Note Style Data Schema Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0]
### Added
- Added several new `assets`:
- `countdownThree`
- `countdownTwo`
- `countdownOne`
- `countdownGo`
- `judgementSick`
- `judgementGood`
- `judgementBad`
- `judgementShit`
- `comboNumber0`
- `comboNumber1`
- `comboNumber2`
- `comboNumber3`
- `comboNumber4`
- `comboNumber5`
- `comboNumber6`
- `comboNumber7`
- `comboNumber8`
- `comboNumber9`
## [1.0.0]
Initial version.

View file

@ -74,6 +74,84 @@ typedef NoteStyleAssetsData =
*/
@:optional
var holdNoteCover:NoteStyleAssetData<NoteStyleData_HoldNoteCover>;
/**
* The THREE sound (and an optional pre-READY graphic).
*/
@:optional
var countdownThree:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The TWO sound and READY graphic.
*/
@:optional
var countdownTwo:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The ONE sound and SET graphic.
*/
@:optional
var countdownOne:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The GO sound and GO! graphic.
*/
@:optional
var countdownGo:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The SICK! judgement.
*/
@:optional
var judgementSick:NoteStyleAssetData<NoteStyleData_Judgement>;
/**
* The GOOD! judgement.
*/
@:optional
var judgementGood:NoteStyleAssetData<NoteStyleData_Judgement>;
/**
* The BAD! judgement.
*/
@:optional
var judgementBad:NoteStyleAssetData<NoteStyleData_Judgement>;
/**
* The SHIT! judgement.
*/
@:optional
var judgementShit:NoteStyleAssetData<NoteStyleData_Judgement>;
@:optional
var comboNumber0:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber1:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber2:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber3:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber4:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber5:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber6:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber7:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber8:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber9:NoteStyleAssetData<NoteStyleData_ComboNum>;
}
/**
@ -109,10 +187,19 @@ typedef NoteStyleAssetData<T> =
@:optional
var isPixel:Bool;
/**
* If true, animations will be played on the graphic.
* @default `false` to save performance.
*/
@:default(false)
@:optional
var animated:Bool;
/**
* The structure of this data depends on the asset.
*/
var data:T;
@:optional
var data:Null<T>;
}
typedef NoteStyleData_Note =
@ -123,7 +210,14 @@ typedef NoteStyleData_Note =
var right:UnnamedAnimationData;
}
typedef NoteStyleData_Countdown =
{
var audioPath:String;
}
typedef NoteStyleData_HoldNote = {}
typedef NoteStyleData_Judgement = {}
typedef NoteStyleData_ComboNum = {}
/**
* Data on animations for each direction of the strumline.

View file

@ -11,9 +11,9 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateNoteStyleData()` function.
*/
public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.0.0";
public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.1.0";
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
public static var instance(get, never):NoteStyleRegistry;
static var _instance:Null<NoteStyleRegistry> = null;

View file

@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.2.4]
### Added
- Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent.
- If the value isn't present, it will use the `playData.characters.opponent`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the opponent)
- Added `playData.characters.playerVocals` to specify which vocal track(s) to play for the player.
- If the value isn't present, it will use the `playData.characters.player`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the player)
## [2.2.3]
### Added
- Added `charter` field to denote authorship of a chart.

View file

@ -529,12 +529,26 @@ class SongCharacterData implements ICloneable<SongCharacterData>
@:default([])
public var altInstrumentals:Array<String> = [];
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '')
@:optional
public var opponentVocals:Null<Array<String>> = null;
@:optional
public var playerVocals:Null<Array<String>> = null;
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '', ?altInstrumentals:Array<String>,
?opponentVocals:Array<String>, ?playerVocals:Array<String>)
{
this.player = player;
this.girlfriend = girlfriend;
this.opponent = opponent;
this.instrumental = instrumental;
this.altInstrumentals = altInstrumentals;
this.opponentVocals = opponentVocals;
this.playerVocals = playerVocals;
if (opponentVocals == null) this.opponentVocals = [opponent];
if (playerVocals == null) this.playerVocals = [player];
}
public function clone():SongCharacterData
@ -722,18 +736,6 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
{
return new SongEventDataRaw(this.time, this.eventKind, this.value);
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, eventKind, value, activated, getStepTime, clone)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, eventKind, value);
}
public function valueAsStruct(?defaultKey:String = "key"):Dynamic
{
@ -757,27 +759,27 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
}
}
public inline function getHandler():Null<SongEvent>
public function getHandler():Null<SongEvent>
{
return SongEventRegistry.getEvent(this.eventKind);
}
public inline function getSchema():Null<SongEventSchema>
public function getSchema():Null<SongEventSchema>
{
return SongEventRegistry.getEventSchema(this.eventKind);
}
public inline function getDynamic(key:String):Null<Dynamic>
public function getDynamic(key:String):Null<Dynamic>
{
return this.value == null ? null : Reflect.field(this.value, key);
}
public inline function getBool(key:String):Null<Bool>
public function getBool(key:String):Null<Bool>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getInt(key:String):Null<Int>
public function getInt(key:String):Null<Int>
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
@ -787,7 +789,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result;
}
public inline function getFloat(key:String):Null<Float>
public function getFloat(key:String):Null<Float>
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
@ -797,17 +799,17 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result;
}
public inline function getString(key:String):String
public function getString(key:String):String
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getArray(key:String):Array<Dynamic>
public function getArray(key:String):Array<Dynamic>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getBoolArray(key:String):Array<Bool>
public function getBoolArray(key:String):Array<Bool>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
@ -839,6 +841,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return result;
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, eventKind, value, activated, getStepTime, clone, getHandler, getSchema, getDynamic, getBool, getInt, getFloat, getString, getArray,
getBoolArray, buildTooltip, valueAsStruct)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, eventKind, value);
}
public function clone():SongEventData
{
@ -951,12 +966,18 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return this.kind = value;
}
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
@:alias("p")
@:default([])
@:optional
public var params:Array<NoteParamData>;
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
{
this.time = time;
this.data = data;
this.length = length;
this.kind = kind;
this.params = params ?? [];
}
/**
@ -1051,9 +1072,19 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
_stepLength = null;
}
public function cloneParams():Array<NoteParamData>
{
var params:Array<NoteParamData> = [];
for (param in this.params)
{
params.push(param.clone());
}
return params;
}
public function clone():SongNoteDataRaw
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams());
}
public function toString():String
@ -1069,9 +1100,9 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
@:forward
abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
{
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
{
this = new SongNoteDataRaw(time, data, length, kind);
this = new SongNoteDataRaw(time, data, length, kind, params);
}
public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String
@ -1115,7 +1146,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (other.kind == '' || this.kind == null) return false;
}
return this.time == other.time && this.data == other.data && this.length == other.length;
return this.time == other.time && this.data == other.data && this.length == other.length && this.params == other.params;
}
@:op(A != B)
@ -1134,7 +1165,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (other.kind == '') return true;
}
return this.time != other.time || this.data != other.data || this.length != other.length;
return this.time != other.time || this.data != other.data || this.length != other.length || this.params != other.params;
}
@:op(A > B)
@ -1171,7 +1202,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
public function clone():SongNoteData
{
return new SongNoteData(this.time, this.data, this.length, this.kind);
return new SongNoteData(this.time, this.data, this.length, this.kind, this.params);
}
/**
@ -1183,3 +1214,30 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
}
}
class NoteParamData implements ICloneable<NoteParamData>
{
@:alias("n")
public var name:String;
@:alias("v")
@:jcustomparse(funkin.data.DataParse.dynamicValue)
@:jcustomwrite(funkin.data.DataWrite.dynamicValue)
public var value:Dynamic;
public function new(name:String, value:Dynamic)
{
this.name = name;
this.value = value;
}
public function clone():NoteParamData
{
return new NoteParamData(this.name, this.value);
}
public function toString():String
{
return 'NoteParamData(${this.name}, ${this.value})';
}
}

View file

@ -199,6 +199,8 @@ class FNFLegacyImporter
{
// Handle the dumb logic for mustHitSection.
var noteData = note.data;
if (noteData < 0) continue; // Exclude Psych event notes.
if (noteData > (STRUMLINE_SIZE * 2)) noteData = noteData % (2 * STRUMLINE_SIZE); // Handle other engine event notes.
// Flip notes if mustHitSection is FALSE (not true lol).
if (!mustHitSection)

View file

@ -93,8 +93,8 @@ class StageRegistry extends BaseRegistry<Stage, StageData>
public function listBaseGameStageIds():Array<String>
{
return [
"mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets",
"phillyBlazin",
"mainStage", "mainStageErect", "spookyMansion", "phillyTrain", "phillyTrainErect", "limoRide", "limoRideErect", "mallXmas", "mallEvil", "school",
"schoolEvil", "tankmanBattlefield", "phillyStreets", "phillyBlazin",
];
}

View file

@ -93,9 +93,9 @@ typedef LevelPropData =
* The frequency to bop at, in beats.
* 1 = every beat, 2 = every other beat, etc.
* Supports up to 0.25 precision.
* @default 0.0
* @default 1.0
*/
@:default(0.0)
@:default(1.0)
@:optional
var danceEvery:Float;

View file

@ -4,8 +4,11 @@ import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings;
import flxanimate.frames.FlxAnimateFrames;
import flixel.graphics.frames.FlxFrame;
import flixel.system.FlxAssets.FlxGraphicAsset;
import openfl.display.BitmapData;
import openfl.utils.Assets;
import flixel.math.FlxPoint;
/**
* A sprite which provides convenience functions for rendering a texture atlas with animations.
@ -18,16 +21,26 @@ class FlxAtlasSprite extends FlxAnimate
FrameRate: 24.0,
Reversed: false,
// ?OnComplete:Void -> Void,
ShowPivot: #if debug false #else false #end,
ShowPivot: false,
Antialiasing: true,
ScrollFactor: null,
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
};
/**
* Signal dispatched when an animation finishes playing.
* Signal dispatched when an animation advances to the next frame.
*/
public var onAnimationFinish:FlxTypedSignal<String->Void> = new FlxTypedSignal<String->Void>();
public var onAnimationFrame:FlxTypedSignal<String->Int->Void> = new FlxTypedSignal();
/**
* Signal dispatched when a non-looping animation finishes playing.
*/
public var onAnimationComplete:FlxTypedSignal<String->Void> = new FlxTypedSignal();
/**
* Signal dispatched when a looping animation finishes playing
*/
public var onAnimationLoopComplete:FlxTypedSignal<String->Void> = new FlxTypedSignal();
var currentAnimation:String;
@ -44,17 +57,20 @@ class FlxAtlasSprite extends FlxAnimate
super(x, y, path, settings);
if (this.anim.curInstance == null)
if (this.anim.stageInstance == null)
{
throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
}
onAnimationFinish.add(cleanupAnimation);
onAnimationComplete.add(cleanupAnimation);
// This defaults the sprite to play the first animation in the atlas,
// then pauses it. This ensures symbols are intialized properly.
this.anim.play('');
this.anim.pause();
this.anim.onComplete.add(_onAnimationComplete);
this.anim.onFrame.add(_onAnimationFrame);
}
/**
@ -62,9 +78,13 @@ class FlxAtlasSprite extends FlxAnimate
*/
public function listAnimations():Array<String>
{
if (this.anim == null) return [];
return this.anim.getFrameLabels();
// return [""];
var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name];
if (mainSymbol == null)
{
FlxG.log.error('FlxAtlasSprite does not have its main symbol!');
return [];
}
return mainSymbol.getFrameLabels().map(keyFrame -> keyFrame.name).filterNull();
}
/**
@ -107,12 +127,11 @@ class FlxAtlasSprite extends FlxAnimate
* @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
* @param startFrame The frame to start the animation on
* 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
public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, loop:Bool = false, startFrame:Int = 0):Void
{
if (loop == null) loop = false;
// Skip if not allowed to play animations.
if ((!canPlayOtherAnims && !ignoreOther)) return;
@ -128,7 +147,7 @@ class FlxAtlasSprite extends FlxAnimate
else
{
// Resume animation if it's paused.
anim.play('', false, false);
anim.play('', restart, false, startFrame);
}
}
else
@ -141,31 +160,27 @@ class FlxAtlasSprite extends FlxAnimate
}
}
anim.callback = function(_, frame:Int) {
var offset = loop ? 0 : -1;
var frameLabel = anim.getFrameLabel(id);
if (frame == (frameLabel.duration + offset) + frameLabel.index)
anim.onComplete.removeAll();
anim.onComplete.add(function() {
if (loop)
{
if (loop)
{
playAnimation(id, true, false, true);
}
else
{
onAnimationFinish.dispatch(id);
}
onAnimationLoopComplete.dispatch(id);
this.anim.play(id, restart, false, startFrame);
this.currentAnimation = id;
}
};
anim.onComplete = function() {
onAnimationFinish.dispatch(id);
};
else
{
onAnimationComplete.dispatch(id);
}
});
// Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false;
// Move to the first frame of the animation.
// goToFrameLabel(id);
trace('Playing animation $id');
this.anim.play(id, restart, false, startFrame);
goToFrameLabel(id);
this.currentAnimation = id;
}
@ -175,6 +190,24 @@ class FlxAtlasSprite extends FlxAnimate
super.update(elapsed);
}
/**
* Returns true if the animation has finished playing.
* Never true if animation is configured to loop.
*/
public function isAnimationFinished():Bool
{
return this.anim.finished;
}
/**
* Returns true if the animation has reached the last frame.
* Can be true even if animation is configured to loop.
*/
public function isLoopComplete():Bool
{
return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1));
}
/**
* Stops the current animation.
*/
@ -219,4 +252,76 @@ class FlxAtlasSprite extends FlxAnimate
// this.currentAnimation = null;
this.anim.pause();
}
function _onAnimationFrame(frame:Int):Void
{
if (currentAnimation != null)
{
onAnimationFrame.dispatch(currentAnimation, frame);
if (isLoopComplete()) onAnimationLoopComplete.dispatch(currentAnimation);
}
}
function _onAnimationComplete():Void
{
if (currentAnimation != null)
{
onAnimationComplete.dispatch(currentAnimation);
}
}
var prevFrames:Map<Int, FlxFrame> = [];
public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void
{
if (graphic == null || !Assets.exists(graphic))
{
var prevFrame:Null<FlxFrame> = prevFrames.get(index);
if (prevFrame == null) return;
prevFrame.copyTo(frames.getByIndex(index));
return;
}
var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo();
prevFrames.set(index, prevFrame);
var frame = FlxG.bitmap.add(graphic).imageFrame.frame;
frame.copyTo(frames.getByIndex(index));
// Additional sizing fix.
@:privateAccess
if (true)
{
var frame = frames.getByIndex(index);
frame.tileMatrix[0] = prevFrame.frame.width / frame.frame.width;
frame.tileMatrix[3] = prevFrame.frame.height / frame.frame.height;
}
}
public function getBasePosition():Null<FlxPoint>
{
var stagePos = new FlxPoint(anim.stageInstance.matrix.tx, anim.stageInstance.matrix.ty);
var instancePos = new FlxPoint(anim.curInstance.matrix.tx, anim.curInstance.matrix.ty);
var firstElement = anim.curSymbol.timeline?.get(0)?.get(0)?.get(0);
if (firstElement == null) return instancePos;
var firstElementPos = new FlxPoint(firstElement.matrix.tx, firstElement.matrix.ty);
return instancePos + firstElementPos;
}
public function getPivotPosition():Null<FlxPoint>
{
return anim.curInstance.symbol.transformationPoint;
}
public override function destroy():Void
{
for (prevFrameId in prevFrames.keys())
{
replaceFrameGraphic(prevFrameId, null);
}
super.destroy();
}
}

View file

@ -0,0 +1,55 @@
package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
class AdjustColorShader extends FlxRuntimeShader
{
public var hue(default, set):Float;
public var saturation(default, set):Float;
public var brightness(default, set):Float;
public var contrast(default, set):Float;
public function new()
{
super(Assets.getText(Paths.frag('adjustColor')));
// FlxG.debugger.addTrackerProfile(new TrackerProfile(HSVShader, ['hue', 'saturation', 'brightness', 'contrast']));
hue = 0;
saturation = 0;
brightness = 0;
contrast = 0;
}
function set_hue(value:Float):Float
{
this.setFloat('hue', value);
this.hue = value;
return this.hue;
}
function set_saturation(value:Float):Float
{
this.setFloat('saturation', value);
this.saturation = value;
return this.saturation;
}
function set_brightness(value:Float):Float
{
this.setFloat('brightness', value);
this.brightness = value;
return this.brightness;
}
function set_contrast(value:Float):Float
{
this.setFloat('contrast', value);
this.contrast = value;
return this.contrast;
}
}

View file

@ -2,6 +2,7 @@ package funkin.graphics.shaders;
import flixel.FlxCamera;
import flixel.FlxG;
import flixel.graphics.frames.FlxFrame;
import flixel.addons.display.FlxRuntimeShader;
import lime.graphics.opengl.GLProgram;
import lime.utils.Log;
@ -32,6 +33,9 @@ class RuntimePostEffectShader extends FlxRuntimeShader
// equals (camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom)
uniform vec4 uCameraBounds;
// equals (frame.left, frame.top, frame.right, frame.bottom)
uniform vec4 uFrameBounds;
// screen coord -> world coord conversion
// returns world coord in px
vec2 screenToWorld(vec2 screenCoord) {
@ -56,6 +60,25 @@ class RuntimePostEffectShader extends FlxRuntimeShader
return (worldCoord - offset) / scale;
}
// screen coord -> frame coord conversion
// returns normalized frame coord
vec2 screenToFrame(vec2 screenCoord) {
float left = uFrameBounds.x;
float top = uFrameBounds.y;
float right = uFrameBounds.z;
float bottom = uFrameBounds.w;
float width = right - left;
float height = bottom - top;
float clampedX = clamp(screenCoord.x, left, right);
float clampedY = clamp(screenCoord.y, top, bottom);
return vec2(
(clampedX - left) / (width),
(clampedY - top) / (height)
);
}
// internally used to get the maximum `openfl_TextureCoordv`
vec2 bitmapCoordScale() {
return openfl_TextureCoordv / screenCoord;
@ -80,6 +103,8 @@ class RuntimePostEffectShader extends FlxRuntimeShader
{
super(fragmentSource, null, glVersion);
uScreenResolution.value = [FlxG.width, FlxG.height];
uCameraBounds.value = [0, 0, FlxG.width, FlxG.height];
uFrameBounds.value = [0, 0, FlxG.width, FlxG.height];
}
// basically `updateViewInfo(FlxG.width, FlxG.height, FlxG.camera)` is good
@ -89,6 +114,12 @@ class RuntimePostEffectShader extends FlxRuntimeShader
uCameraBounds.value = [camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom];
}
public function updateFrameInfo(frame:FlxFrame)
{
// NOTE: uv.width is actually the right pos and uv.height is the bottom pos
uFrameBounds.value = [frame.uv.x, frame.uv.y, frame.uv.width, frame.uv.height];
}
override function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram
{
try

View file

@ -32,6 +32,14 @@ class RuntimeRainShader extends RuntimePostEffectShader
return time = value;
}
public var spriteMode(default, set):Bool = false;
function set_spriteMode(value:Bool):Bool
{
this.setBool('uSpriteMode', value);
return spriteMode = value;
}
// The scale of the rain depends on the world coordinate system, so higher resolution makes
// the raindrops smaller. This parameter can be used to adjust the total scale of the scene.
// The size of the raindrops is proportional to the value of this parameter.

View file

@ -356,9 +356,10 @@ class Controls extends FlxActionSet
public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool
{
#if debug
#if FEATURE_DEBUG_FUNCTIONS
if (!byName.exists(name)) throw 'Invalid name: $name';
#end
var action = byName[name];
if (gamepadOnly) return action.checkFiltered(trigger, GAMEPAD);
else
@ -367,7 +368,7 @@ class Controls extends FlxActionSet
public function getKeysForAction(name:Action):Array<FlxKey>
{
#if debug
#if FEATURE_DEBUG_FUNCTIONS
if (!byName.exists(name)) throw 'Invalid name: $name';
#end
@ -382,7 +383,7 @@ class Controls extends FlxActionSet
public function getButtonsForAction(name:Action):Array<FlxGamepadInputID>
{
#if debug
#if FEATURE_DEBUG_FUNCTIONS
if (!byName.exists(name)) throw 'Invalid name: $name';
#end

View file

@ -7,6 +7,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.story.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.data.song.SongRegistry;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.data.stage.StageRegistry;
@ -27,11 +28,10 @@ class PolymodHandler
{
/**
* The API version that mods should comply with.
* Format this with Semantic Versioning; <MAJOR>.<MINOR>.<PATCH>.
* Bug fixes increment the patch version, new features increment the minor version.
* Changes that break old mods increment the major version.
* Indicates which mods are compatible with this version of the game.
* Minor updates rarely impact mods but major versions often do.
*/
static final API_VERSION:String = '0.1.0';
static final API_VERSION:String = "0.5.0"; // Constants.VERSION;
/**
* Where relative to the executable that mods are located.
@ -177,7 +177,7 @@ class PolymodHandler
loadedModIds.push(mod.id);
}
#if debug
#if FEATURE_DEBUG_FUNCTIONS
var fileList:Array<String> = Polymod.listModFiles(PolymodAssetType.IMAGE);
trace('Installed mods have replaced ${fileList.length} images.');
for (item in fileList)
@ -233,6 +233,8 @@ class PolymodHandler
// NOTE: Scripted classes are automatically aliased to their parent class.
Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
// Add blacklisting for prohibited classes and packages.
// `Sys`
@ -251,8 +253,33 @@ class PolymodHandler
// Lib.load() can load malicious DLLs
Polymod.blacklistImport('cpp.Lib');
// `Unserializer`
// Unserializerr.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
Polymod.blacklistImport('Unserializer');
// `lime.system.CFFI`
// Can load and execute compiled binaries.
Polymod.blacklistImport('lime.system.CFFI');
// `lime.system.JNI`
// Can load and execute compiled binaries.
Polymod.blacklistImport('lime.system.JNI');
// `lime.system.System`
// System.load() can load malicious DLLs
Polymod.blacklistImport('lime.system.System');
// `lime.utils.Assets`
// Literally just has a private `resolveClass` function for some reason?
Polymod.blacklistImport('lime.utils.Assets');
Polymod.blacklistImport('openfl.utils.Assets');
// `openfl.desktop.NativeProcess`
// Can load native processes on the host operating system.
Polymod.blacklistImport('openfl.desktop.NativeProcess');
// `polymod.*`
// You can probably unblacklist a module
// Contains functions which may allow for un-blacklisting other modules.
for (cls in ClassMacro.listClassesInPackage('polymod'))
{
if (cls == null) continue;
@ -261,6 +288,7 @@ class PolymodHandler
}
// `sys.*`
// Access to system utilities such as the file system.
for (cls in ClassMacro.listClassesInPackage('sys'))
{
if (cls == null) continue;
@ -383,6 +411,7 @@ class PolymodHandler
StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
NoteKindManager.loadScripts();
ModuleHandler.loadModuleCache();
}
}

View file

@ -1,8 +0,0 @@
package funkin.modding.base;
/**
* A script that can be tied to an FlxUIState.
* Create a scripted class that extends FlxUIState to use this.
*/
@:hscriptClass
class ScriptedFlxUIState extends flixel.addons.ui.FlxUIState implements HScriptedClass {}

View file

@ -11,6 +11,9 @@ import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer;
import funkin.util.EaseUtil;
import funkin.audio.FunkinSound;
import openfl.utils.Assets;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
class Countdown
{
@ -19,6 +22,24 @@ class Countdown
*/
public static var countdownStep(default, null):CountdownStep = BEFORE;
/**
* Which alternate graphic/sound on countdown to use.
* This is set via the current notestyle.
* For example, in Week 6 it is `pixel`.
*/
public static var soundSuffix:String = '';
/**
* Which alternate graphic on countdown to use.
* You can set this via script.
* For example, in Week 6 it is `-pixel`.
*/
public static var graphicSuffix:String = '';
static var noteStyle:NoteStyle;
static var fallbackNoteStyle:Null<NoteStyle>;
/**
* The currently running countdown. This will be null if there is no countdown running.
*/
@ -30,7 +51,7 @@ class Countdown
* This will automatically stop and restart the countdown if it is already running.
* @returns `false` if the countdown was cancelled by a script.
*/
public static function performCountdown(isPixelStyle:Bool):Bool
public static function performCountdown():Bool
{
countdownStep = BEFORE;
var cancelled:Bool = propagateCountdownEvent(countdownStep);
@ -65,10 +86,10 @@ class Countdown
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
// Countdown graphic.
showCountdownGraphic(countdownStep, isPixelStyle);
showCountdownGraphic(countdownStep);
// Countdown sound.
playCountdownSound(countdownStep, isPixelStyle);
playCountdownSound(countdownStep);
// Event handling bullshit.
var cancelled:Bool = propagateCountdownEvent(countdownStep);
@ -177,122 +198,69 @@ class Countdown
}
/**
* Retrieves the graphic to use for this step of the countdown.
* TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
*
* This is public so modules can do lol funny shit.
* Reset the countdown configuration to the default.
*/
public static function showCountdownGraphic(index:CountdownStep, isPixelStyle:Bool):Void
public static function reset()
{
var spritePath:String = null;
noteStyle = null;
}
/**
* Retrieve the note style data (if we haven't already)
* @param noteStyleId The id of the note style to fetch. Defaults to the one used by the current PlayState.
* @param force Fetch the note style from the registry even if we've already fetched it.
*/
static function fetchNoteStyle(?noteStyleId:String, force:Bool = false):Void
{
if (noteStyle != null && !force) return;
if (noteStyleId == null) noteStyleId = PlayState.instance?.currentChart?.noteStyle;
noteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
}
/**
* Retrieves the graphic to use for this step of the countdown.
*/
public static function showCountdownGraphic(index:CountdownStep):Void
{
fetchNoteStyle();
var countdownSprite = noteStyle.buildCountdownSprite(index);
if (countdownSprite == null) return;
var fadeEase = FlxEase.cubeInOut;
if (isPixelStyle)
{
fadeEase = EaseUtil.stepped(8);
switch (index)
{
case TWO:
spritePath = 'weeb/pixelUI/ready-pixel';
case ONE:
spritePath = 'weeb/pixelUI/set-pixel';
case GO:
spritePath = 'weeb/pixelUI/date-pixel';
default:
// null
}
}
else
{
switch (index)
{
case TWO:
spritePath = 'ready';
case ONE:
spritePath = 'set';
case GO:
spritePath = 'go';
default:
// null
}
}
if (spritePath == null) return;
var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
countdownSprite.scrollFactor.set(0, 0);
if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
countdownSprite.antialiasing = !isPixelStyle;
countdownSprite.updateHitbox();
countdownSprite.screenCenter();
if (noteStyle.isCountdownSpritePixel(index)) fadeEase = EaseUtil.stepped(8);
// Fade sprite in, then out, then destroy it.
FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100}, Conductor.instance.beatLengthMs / 1000,
FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000,
{
ease: FlxEase.cubeInOut,
ease: fadeEase,
onComplete: function(twn:FlxTween) {
countdownSprite.destroy();
}
});
FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000,
{
ease: fadeEase
});
countdownSprite.cameras = [PlayState.instance.camHUD];
PlayState.instance.add(countdownSprite);
countdownSprite.screenCenter();
var offsets = noteStyle.getCountdownSpriteOffsets(index);
countdownSprite.x += offsets[0];
countdownSprite.y += offsets[1];
}
/**
* Retrieves the sound file to use for this step of the countdown.
* TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
*
* This is public so modules can do lol funny shit.
*/
public static function playCountdownSound(index:CountdownStep, isPixelStyle:Bool):Void
public static function playCountdownSound(step:CountdownStep):FunkinSound
{
var soundPath:String = null;
fetchNoteStyle();
var path = noteStyle.getCountdownSoundPath(step);
if (path == null) return null;
if (isPixelStyle)
{
switch (index)
{
case THREE:
soundPath = 'intro3-pixel';
case TWO:
soundPath = 'intro2-pixel';
case ONE:
soundPath = 'intro1-pixel';
case GO:
soundPath = 'introGo-pixel';
default:
// null
}
}
else
{
switch (index)
{
case THREE:
soundPath = 'intro3';
case TWO:
soundPath = 'intro2';
case ONE:
soundPath = 'intro1';
case GO:
soundPath = 'introGo';
default:
// null
}
}
if (soundPath == null) return;
FunkinSound.playOnce(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME);
return FunkinSound.playOnce(path, Constants.COUNTDOWN_VOLUME);
}
public static function decrement(step:CountdownStep):CountdownStep

View file

@ -306,7 +306,7 @@ class PauseSubState extends MusicBeatSubState
metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
if (PlayState.instance?.currentDifficulty != null)
{
metadataDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase();
metadataDifficulty.text += PlayState.instance.currentDifficulty.replace('-', ' ').toTitleCase();
}
metadataDifficulty.scrollFactor.set(0, 0);
metadata.add(metadataDifficulty);
@ -430,7 +430,7 @@ class PauseSubState extends MusicBeatSubState
resume(this);
}
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
// to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H)
{

View file

@ -49,6 +49,7 @@ import funkin.play.notes.NoteSprite;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.Strumline;
import funkin.play.notes.SustainTrail;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
import funkin.play.stage.Stage;
@ -66,7 +67,7 @@ import lime.ui.Haptic;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
import openfl.Lib;
#if discord_rpc
#if FEATURE_DISCORD_RPC
import Discord.DiscordClient;
#end
@ -444,7 +445,7 @@ class PlayState extends MusicBeatSubState
*/
public var vocals:VoicesGroup;
#if discord_rpc
#if FEATURE_DISCORD_RPC
// Discord RPC variables
var storyDifficultyText:String = '';
var iconRPC:String = '';
@ -503,7 +504,7 @@ class PlayState extends MusicBeatSubState
public var camGame:FlxCamera;
/**
* The camera which contains, and controls visibility of, a video cutscene.
* The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition.
*/
public var camCutscene:FlxCamera;
@ -578,7 +579,8 @@ class PlayState extends MusicBeatSubState
// TODO: Refactor or document
var generatedMusic:Bool = false;
var perfectMode:Bool = false;
var skipEndingTransition:Bool = false;
static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK;
@ -694,14 +696,9 @@ class PlayState extends MusicBeatSubState
initMinimalMode();
}
initStrumlines();
initPopups();
// Initialize the judgements and combo meter.
comboPopUps = new PopUpStuff();
comboPopUps.zIndex = 900;
add(comboPopUps);
comboPopUps.cameras = [camHUD];
#if discord_rpc
#if FEATURE_DISCORD_RPC
// Initialize Discord Rich Presence.
initDiscord();
#end
@ -741,7 +738,7 @@ class PlayState extends MusicBeatSubState
rightWatermarkText.cameras = [camHUD];
// Initialize some debug stuff.
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
// Display the version number (and git commit hash) in the bottom right corner.
this.rightWatermarkText.text = Constants.VERSION;
@ -900,7 +897,7 @@ class PlayState extends MusicBeatSubState
health = Constants.HEALTH_STARTING;
songScore = 0;
Highscore.tallies.combo = 0;
Countdown.performCountdown(currentStageId.startsWith('school'));
Countdown.performCountdown();
needsReset = false;
}
@ -975,12 +972,12 @@ class PlayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
pauseSubState.camera = camHUD;
pauseSubState.camera = camCutscene;
openSubState(pauseSubState);
// boyfriendPos.put(); // TODO: Why is this here?
}
#if discord_rpc
#if FEATURE_DISCORD_RPC
DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
#end
}
@ -1039,7 +1036,7 @@ class PlayState extends MusicBeatSubState
// Disable updates, preventing animations in the background from playing.
persistentUpdate = false;
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
if (FlxG.keys.pressed.THREE)
{
// TODO: Change the key or delete this?
@ -1050,7 +1047,7 @@ class PlayState extends MusicBeatSubState
{
#end
persistentDraw = false;
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
}
#end
@ -1069,7 +1066,7 @@ class PlayState extends MusicBeatSubState
moveToGameOver();
}
#if discord_rpc
#if FEATURE_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
@ -1165,6 +1162,9 @@ class PlayState extends MusicBeatSubState
// super.dispatchEvent(event) dispatches event to module scripts.
super.dispatchEvent(event);
// Dispatch event to note kind scripts
NoteKindManager.callEvent(event);
// Dispatch event to stage script.
ScriptEventDispatcher.callEvent(currentStage, event);
@ -1176,14 +1176,12 @@ class PlayState extends MusicBeatSubState
// Dispatch event to conversation script.
ScriptEventDispatcher.callEvent(currentConversation, event);
// TODO: Dispatch event to note scripts
}
/**
* Function called before opening a new substate.
* @param subState The substate to open.
*/
* Function called before opening a new substate.
* @param subState The substate to open.
*/
public override function openSubState(subState:FlxSubState):Void
{
// If there is a substate which requires the game to continue,
@ -1239,9 +1237,9 @@ class PlayState extends MusicBeatSubState
}
/**
* Function called before closing the current substate.
* @param subState
*/
* Function called before closing the current substate.
* @param subState
*/
public override function closeSubState():Void
{
if (Std.isOfType(subState, PauseSubState))
@ -1280,7 +1278,7 @@ class PlayState extends MusicBeatSubState
// Resume the countdown.
Countdown.resumeCountdown();
#if discord_rpc
#if FEATURE_DISCORD_RPC
if (startTimer.finished)
{
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true,
@ -1303,8 +1301,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Function called when the game window gains focus.
*/
* Function called when the game window gains focus.
*/
public override function onFocus():Void
{
if (VideoCutscene.isPlaying() && FlxG.autoPause && isGamePaused) VideoCutscene.pauseVideo();
@ -1313,7 +1311,7 @@ class PlayState extends MusicBeatSubState
VideoCutscene.resumeVideo();
#end
#if discord_rpc
#if FEATURE_DISCORD_RPC
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause)
{
if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
@ -1331,15 +1329,15 @@ class PlayState extends MusicBeatSubState
}
/**
* Function called when the game window loses focus.
*/
* Function called when the game window loses focus.
*/
public override function onFocusLost():Void
{
#if html5
if (FlxG.autoPause) VideoCutscene.pauseVideo();
#end
#if discord_rpc
#if FEATURE_DISCORD_RPC
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText,
currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
#end
@ -1348,64 +1346,13 @@ class PlayState extends MusicBeatSubState
}
/**
* Removes any references to the current stage, then clears the stage cache,
* then reloads all the stages.
*
* This is useful for when you want to edit a stage without reloading the whole game.
* Reloading works on both the JSON and the HXC, if applicable.
*
* Call this by pressing F5 on a debug build.
*/
override function debug_refreshModules():Void
* Call this by pressing F5 on a debug build.
*/
override function reloadAssets():Void
{
// Prevent further gameplay updates, which will try to reference dead objects.
criticalFailure = true;
// Remove the current stage. If the stage gets deleted while it's still in use,
// it'll probably crash the game or something.
if (this.currentStage != null)
{
remove(currentStage);
var event:ScriptEvent = new ScriptEvent(DESTROY, false);
ScriptEventDispatcher.callEvent(currentStage, event);
currentStage = null;
}
if (!overrideMusic)
{
// Stop the instrumental.
if (FlxG.sound.music != null)
{
FlxG.sound.music.destroy();
FlxG.sound.music = null;
}
// Stop the vocals.
if (vocals != null && vocals.exists)
{
vocals.destroy();
vocals = null;
}
}
else
{
// Stop the instrumental.
if (FlxG.sound.music != null)
{
FlxG.sound.music.stop();
}
// Stop the vocals.
if (vocals != null && vocals.exists)
{
vocals.stop();
}
}
super.debug_refreshModules();
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentSong, event);
funkin.modding.PolymodHandler.forceReloadAssets();
lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
LoadingState.loadPlayState(lastParams);
}
override function stepHit():Bool
@ -1417,17 +1364,6 @@ class PlayState extends MusicBeatSubState
if (isGamePaused) return false;
if (!startingSong
&& FlxG.sound.music != null
&& (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200
|| Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200))
{
trace("VOCALS NEED RESYNC");
if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
resyncVocals();
}
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
@ -1449,6 +1385,17 @@ class PlayState extends MusicBeatSubState
// activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
}
if (!startingSong
&& FlxG.sound.music != null
&& (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100
|| Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100))
{
trace("VOCALS NEED RESYNC");
if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
resyncVocals();
}
// Only bop camera if zoom level is below 135%
if (Preferences.zoomCamera
&& FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom)
@ -1501,9 +1448,6 @@ class PlayState extends MusicBeatSubState
if (playerStrumline != null) playerStrumline.onBeatHit();
if (opponentStrumline != null) opponentStrumline.onBeatHit();
// Make the characters dance on the beat
danceOnBeat();
return true;
}
@ -1515,28 +1459,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Handles characters dancing to the beat of the current song.
*
* TODO: Move some of this logic into `Bopper.hx`, or individual character scripts.
*/
function danceOnBeat():Void
{
if (currentStage == null) return;
// TODO: Add HEY! song events to Tutorial.
if (Conductor.instance.currentBeat % 16 == 15
&& currentStage.getDad().characterId == 'gf'
&& Conductor.instance.currentBeat > 16
&& Conductor.instance.currentBeat < 48)
{
currentStage.getBoyfriend().playAnimation('hey', true);
currentStage.getDad().playAnimation('cheer', true);
}
}
/**
* Initializes the game and HUD cameras.
*/
* Initializes the game and HUD cameras.
*/
function initCameras():Void
{
camGame = new FunkinCamera('playStateCamGame');
@ -1560,8 +1484,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Initializes the health bar on the HUD.
*/
* Initializes the health bar on the HUD.
*/
function initHealthBar():Void
{
var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9;
@ -1592,8 +1516,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Generates the stage and all its props.
*/
* Generates the stage and all its props.
*/
function initStage():Void
{
loadStage(currentStageId);
@ -1613,10 +1537,10 @@ class PlayState extends MusicBeatSubState
}
/**
* Loads stage data from cache, assembles the props,
* and adds it to the state.
* @param id
*/
* Loads stage data from cache, assembles the props,
* and adds it to the state.
* @param id
*/
function loadStage(id:String):Void
{
currentStage = StageRegistry.instance.fetchEntry(id);
@ -1634,7 +1558,7 @@ class PlayState extends MusicBeatSubState
// Add the stage to the scene.
this.add(currentStage);
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
FlxG.console.registerObject('stage', currentStage);
#end
}
@ -1657,8 +1581,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Generates the character sprites and adds them to the stage.
*/
* Generates the character sprites and adds them to the stage.
*/
function initCharacters():Void
{
if (currentSong == null || currentChart == null)
@ -1737,7 +1661,7 @@ class PlayState extends MusicBeatSubState
{
currentStage.addCharacter(girlfriend, GF);
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
FlxG.console.registerObject('gf', girlfriend);
#end
}
@ -1746,7 +1670,7 @@ class PlayState extends MusicBeatSubState
{
currentStage.addCharacter(boyfriend, BF);
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
FlxG.console.registerObject('bf', boyfriend);
#end
}
@ -1757,7 +1681,7 @@ class PlayState extends MusicBeatSubState
// Camera starts at dad.
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
FlxG.console.registerObject('dad', dad);
#end
}
@ -1768,8 +1692,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Constructs the strumlines for each player.
*/
* Constructs the strumlines for each player.
*/
function initStrumlines():Void
{
var noteStyleId:String = currentChart.noteStyle;
@ -1801,11 +1725,26 @@ class PlayState extends MusicBeatSubState
}
/**
* Initializes the Discord Rich Presence.
*/
* Configures the judgement and combo popups.
*/
function initPopups():Void
{
var noteStyleId:String = currentChart.noteStyle;
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
// Initialize the judgements and combo meter.
comboPopUps = new PopUpStuff(noteStyle);
comboPopUps.zIndex = 900;
add(comboPopUps);
comboPopUps.cameras = [camHUD];
}
/**
* Initializes the Discord Rich Presence.
*/
function initDiscord():Void
{
#if discord_rpc
#if FEATURE_DISCORD_RPC
storyDifficultyText = difficultyString();
iconRPC = currentSong.player2;
@ -1836,9 +1775,9 @@ class PlayState extends MusicBeatSubState
}
/**
* Initializes the song (applying the chart, generating the notes, etc.)
* Should be done before the countdown starts.
*/
* Initializes the song (applying the chart, generating the notes, etc.)
* Should be done before the countdown starts.
*/
function generateSong():Void
{
if (currentChart == null)
@ -1869,8 +1808,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Read note data from the chart and generate the notes.
*/
* Read note data from the chart and generate the notes.
*/
function regenNoteData(startTime:Float = 0):Void
{
Highscore.tallies.combo = 0;
@ -1923,27 +1862,26 @@ class PlayState extends MusicBeatSubState
}
/**
* Prepares to start the countdown.
* Ends any running cutscenes, creates the strumlines, and starts the countdown.
* This is public so that scripts can call it.
*/
* Prepares to start the countdown.
* Ends any running cutscenes, creates the strumlines, and starts the countdown.
* This is public so that scripts can call it.
*/
public function startCountdown():Void
{
// If Countdown.performCountdown returns false, then the countdown was canceled by a script.
var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school'));
var result:Bool = Countdown.performCountdown();
if (!result) return;
isInCutscene = false;
camCutscene.visible = false;
// TODO: Maybe tween in the camera after any cutscenes.
camHUD.visible = true;
}
/**
* Displays a dialogue cutscene with the given ID.
* This is used by song scripts to display dialogue.
*/
* Displays a dialogue cutscene with the given ID.
* This is used by song scripts to display dialogue.
*/
public function startConversation(conversationId:String):Void
{
isInCutscene = true;
@ -1963,8 +1901,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Handler function called when a conversation ends.
*/
* Handler function called when a conversation ends.
*/
function onConversationComplete():Void
{
isInCutscene = false;
@ -1983,8 +1921,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Starts playing the song after the countdown has completed.
*/
* Starts playing the song after the countdown has completed.
*/
function startSong():Void
{
startingSong = false;
@ -2000,7 +1938,9 @@ class PlayState extends MusicBeatSubState
return;
}
FlxG.sound.music.onComplete = endSong.bind(false);
FlxG.sound.music.onComplete = function() {
endSong(skipEndingTransition);
};
// 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);
@ -2017,7 +1957,7 @@ class PlayState extends MusicBeatSubState
vocals.pitch = playbackRate;
resyncVocals();
#if discord_rpc
#if FEATURE_DISCORD_RPC
// Updating Discord Rich Presence (with Time Left)
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, currentSongLengthMs);
#end
@ -2032,26 +1972,28 @@ class PlayState extends MusicBeatSubState
}
/**
* Resyncronize the vocal tracks if they have become offset from the instrumental.
*/
* Resyncronize the vocal tracks if they have become offset from the instrumental.
*/
function resyncVocals():Void
{
if (vocals == null) return;
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
if (!FlxG.sound.music.playing) return;
if (!(FlxG.sound.music?.playing ?? false)) return;
var timeToPlayAt:Float = Conductor.instance.songPosition - Conductor.instance.instrumentalOffset;
FlxG.sound.music.pause();
vocals.pause();
FlxG.sound.music.play(FlxG.sound.music.time);
FlxG.sound.music.time = timeToPlayAt;
FlxG.sound.music.play(false, timeToPlayAt);
vocals.time = FlxG.sound.music.time;
vocals.play(false, FlxG.sound.music.time);
vocals.time = timeToPlayAt;
vocals.play(false, timeToPlayAt);
}
/**
* Updates the position and contents of the score display.
*/
* Updates the position and contents of the score display.
*/
function updateScoreText():Void
{
// TODO: Add functionality for modules to update the score text.
@ -2068,8 +2010,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Updates the values of the health bar.
*/
* Updates the values of the health bar.
*/
function updateHealthBar():Void
{
if (isBotPlayMode)
@ -2083,8 +2025,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Callback executed when one of the note keys is pressed.
*/
* Callback executed when one of the note keys is pressed.
*/
function onKeyPress(event:PreciseInputEvent):Void
{
if (isGamePaused) return;
@ -2094,8 +2036,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Callback executed when one of the note keys is released.
*/
* Callback executed when one of the note keys is released.
*/
function onKeyRelease(event:PreciseInputEvent):Void
{
if (isGamePaused) return;
@ -2105,8 +2047,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Handles opponent note hits and player note misses.
*/
* Handles opponent note hits and player note misses.
*/
function processNotes(elapsed:Float):Void
{
if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return;
@ -2279,10 +2221,14 @@ class PlayState extends MusicBeatSubState
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) continue;
// Skip handling the miss in botplay!
if (!isBotPlayMode)
{
// Judge the miss.
// NOTE: This is what handles the scoring.
trace('Missed note! ${note.noteData}');
onNoteMiss(note, event.playSound, event.healthChange);
}
note.handledMiss = true;
}
@ -2324,8 +2270,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Spitting out the input for ravy 🙇!!
*/
* Spitting out the input for ravy 🙇!!
*/
var inputSpitter:Array<ScoreInput> = [];
function handleSkippedNotes():Void
@ -2349,9 +2295,9 @@ class PlayState extends MusicBeatSubState
}
/**
* PreciseInputEvents are put into a queue between update() calls,
* and then processed here.
*/
* PreciseInputEvents are put into a queue between update() calls,
* and then processed here.
*/
function processInputQueue():Void
{
if (inputPressQueue.length + inputReleaseQueue.length == 0) return;
@ -2379,9 +2325,16 @@ class PlayState extends MusicBeatSubState
playerStrumline.pressKey(input.noteDirection);
// Don't credit or penalize inputs in Bot Play.
if (isBotPlayMode) continue;
var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection];
if (!Constants.GHOST_TAPPING && notesInDirection.length == 0)
#if FEATURE_GHOST_TAPPING
if ((!playerStrumline.mayGhostTap()) && notesInDirection.length == 0)
#else
if (notesInDirection.length == 0)
#end
{
// Pressed a wrong key with no notes nearby.
// Perform a ghost miss (anti-spam).
@ -2391,16 +2344,6 @@ class PlayState extends MusicBeatSubState
playerStrumline.playPress(input.noteDirection);
trace('PENALTY Score: ${songScore}');
}
else if (Constants.GHOST_TAPPING && (!playerStrumline.mayGhostTap()) && notesInDirection.length == 0)
{
// Pressed a wrong key with notes visible on-screen.
// Perform a ghost miss (anti-spam).
ghostNoteMiss(input.noteDirection, notesInRange.length > 0);
// Play the strumline animation.
playerStrumline.playPress(input.noteDirection);
trace('PENALTY Score: ${songScore}');
}
else if (notesInDirection.length == 0)
{
// Press a key with no penalty.
@ -2493,9 +2436,9 @@ class PlayState extends MusicBeatSubState
}
/**
* Called when a note leaves the screen and is considered missed by the player.
* @param note
*/
* Called when a note leaves the screen and is considered missed by the player.
* @param note
*/
function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthChange:Float):Void
{
// If we are here, we already CALLED the onNoteMiss script hook!
@ -2551,13 +2494,13 @@ class PlayState extends MusicBeatSubState
}
/**
* Called when a player presses a key with no note present.
* Scripts can modify the amount of health/score lost, whether player animations or sounds are used,
* or even cancel the event entirely.
*
* @param direction
* @param hasPossibleNotes
*/
* Called when a player presses a key with no note present.
* Scripts can modify the amount of health/score lost, whether player animations or sounds are used,
* or even cancel the event entirely.
*
* @param direction
* @param hasPossibleNotes
*/
function ghostNoteMiss(direction:NoteDirection, hasPossibleNotes:Bool = true):Void
{
var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in.
@ -2606,17 +2549,11 @@ class PlayState extends MusicBeatSubState
}
/**
* Debug keys. Disabled while in cutscenes.
*/
* Debug keys. Disabled while in cutscenes.
*/
function debugKeyShit():Void
{
#if !debug
perfectMode = false;
#else
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
#end
#if CHART_EDITOR_SUPPORTED
#if FEATURE_CHART_EDITOR
// Open the stage editor overlaying the current state.
if (controls.DEBUG_STAGE)
{
@ -2646,7 +2583,10 @@ class PlayState extends MusicBeatSubState
}
#end
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
// H: Hide the HUD.
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
// 1: End the song immediately.
if (FlxG.keys.justPressed.ONE) endSong(true);
@ -2660,7 +2600,7 @@ class PlayState extends MusicBeatSubState
// 9: Toggle the old icon.
if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon();
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
// PAGEUP: Skip forward two sections.
// SHIFT+PAGEUP: Skip forward twenty sections.
if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 20 : 2);
@ -2673,8 +2613,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Handles applying health, score, and ratings.
*/
* Handles applying health, score, and ratings.
*/
function applyScore(score:Int, daRating:String, healthChange:Float, isComboBreak:Bool)
{
switch (daRating)
@ -2706,8 +2646,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Handles rating popups when a note is hit.
*/
* Handles rating popups when a note is hit.
*/
function popUpScore(daRating:String, ?combo:Int):Void
{
if (daRating == 'miss')
@ -2764,10 +2704,10 @@ class PlayState extends MusicBeatSubState
}
/**
* Handle keyboard inputs during cutscenes.
* This includes advancing conversations and skipping videos.
* @param elapsed Time elapsed since last game update.
*/
* Handle keyboard inputs during cutscenes.
* This includes advancing conversations and skipping videos.
* @param elapsed Time elapsed since last game update.
*/
function handleCutsceneKeys(elapsed:Float):Void
{
if (isGamePaused) return;
@ -2811,20 +2751,20 @@ class PlayState extends MusicBeatSubState
}
/**
* Handle logic for actually skipping a video cutscene after it has been held.
*/
* Handle logic for actually skipping a video cutscene after it has been held.
*/
function skipVideoCutscene():Void
{
VideoCutscene.finishVideo();
}
/**
* End the song. Handle saving high scores and transitioning to the results screen.
*
* Broadcasts an `onSongEnd` event, which can be cancelled to prevent the song from ending (for a cutscene or something).
* Remember to call `endSong` again when the song should actually end!
* @param rightGoddamnNow If true, don't play the fancy animation where you zoom onto Girlfriend. Used after a cutscene.
*/
* End the song. Handle saving high scores and transitioning to the results screen.
*
* Broadcasts an `onSongEnd` event, which can be cancelled to prevent the song from ending (for a cutscene or something).
* Remember to call `endSong` again when the song should actually end!
* @param rightGoddamnNow If true, don't play the fancy animation where you zoom onto Girlfriend. Used after a cutscene.
*/
public function endSong(rightGoddamnNow:Bool = false):Void
{
if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
@ -2979,11 +2919,16 @@ class PlayState extends MusicBeatSubState
FunkinSound.playOnce(Paths.sound('Lights_Shut_off'), function() {
// no camFollow so it centers on horror tree
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
var targetVariation:String = currentVariation;
if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation))
{
targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION;
}
LoadingState.loadPlayState(
{
targetSong: targetSong,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetVariation: currentVariation,
targetVariation: targetVariation,
cameraFollowPoint: cameraFollowPoint.getPosition(),
});
});
@ -2991,11 +2936,16 @@ class PlayState extends MusicBeatSubState
else
{
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
var targetVariation:String = currentVariation;
if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation))
{
targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION;
}
LoadingState.loadPlayState(
{
targetSong: targetSong,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetVariation: currentVariation,
targetVariation: targetVariation,
cameraFollowPoint: cameraFollowPoint.getPosition(),
});
}
@ -3029,8 +2979,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Perform necessary cleanup before leaving the PlayState.
*/
* Perform necessary cleanup before leaving the PlayState.
*/
function performCleanup():Void
{
// If the camera is being tweened, stop it.
@ -3081,14 +3031,15 @@ class PlayState extends MusicBeatSubState
GameOverSubState.reset();
PauseSubState.reset();
Countdown.reset();
// Clear the static reference to this state.
instance = null;
}
/**
* Play the camera zoom animation and then move to the results screen once it's done.
*/
* Play the camera zoom animation and then move to the results screen once it's done.
*/
function zoomIntoResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
{
trace('WENT TO RESULTS SCREEN!');
@ -3152,17 +3103,17 @@ class PlayState extends MusicBeatSubState
// Zoom over to the Results screen.
// TODO: Re-enable this.
/*
FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1,
{
ease: FlxEase.expoIn,
});
*/
FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1,
{
ease: FlxEase.expoIn,
});
*/
});
}
/**
* Move to the results screen right goddamn now.
*/
* Move to the results screen right goddamn now.
*/
function moveToResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
{
persistentUpdate = false;
@ -3202,8 +3153,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Pauses music and vocals easily.
*/
* Pauses music and vocals easily.
*/
public function pauseMusic():Void
{
if (FlxG.sound.music != null) FlxG.sound.music.pause();
@ -3211,8 +3162,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Resets the camera's zoom level and focus point.
*/
* Resets the camera's zoom level and focus point.
*/
public function resetCamera(?resetZoom:Bool = true, ?cancelTweens:Bool = true):Void
{
// Cancel camera tweens if any are active.
@ -3234,8 +3185,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Sets the camera follow point's position and tweens the camera there.
*/
* Sets the camera follow point's position and tweens the camera there.
*/
public function tweenCameraToPosition(?x:Float, ?y:Float, ?duration:Float, ?ease:Null<Float->Float>):Void
{
cameraFollowPoint.setPosition(x, y);
@ -3243,8 +3194,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Disables camera following and tweens the camera to the follow point manually.
*/
* Disables camera following and tweens the camera to the follow point manually.
*/
public function tweenCameraToFollowPoint(?duration:Float, ?ease:Null<Float->Float>):Void
{
// Cancel the current tween if it's active.
@ -3281,8 +3232,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Tweens the camera zoom to the desired amount.
*/
* Tweens the camera zoom to the desired amount.
*/
public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?direct:Bool, ?ease:Null<Float->Float>):Void
{
// Cancel the current tween if it's active.
@ -3313,8 +3264,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Cancel all active camera tweens simultaneously.
*/
* Cancel all active camera tweens simultaneously.
*/
public function cancelAllCameraTweens()
{
cancelCameraFollowTween();
@ -3324,8 +3275,8 @@ class PlayState extends MusicBeatSubState
var prevScrollTargets:Array<Dynamic> = []; // used to snap scroll speed when things go unruely
/**
* The magical function that shall tween the scroll speed.
*/
* The magical function that shall tween the scroll speed.
*/
public function tweenScrollSpeed(?speed:Float, ?duration:Float, ?ease:Null<Float->Float>, strumlines:Array<String>):Void
{
// Cancel the current tween if it's active.
@ -3375,12 +3326,12 @@ class PlayState extends MusicBeatSubState
scrollSpeedTweens = [];
}
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
/**
* Jumps forward or backward a number of sections in the song.
* Accounts for BPM changes, does not prevent death from skipped notes.
* @param sections The number of sections to jump, negative to go backwards.
*/
* Jumps forward or backward a number of sections in the song.
* Accounts for BPM changes, does not prevent death from skipped notes.
* @param sections The number of sections to jump, negative to go backwards.
*/
function changeSection(sections:Int):Void
{
// FlxG.sound.music.pause();

View file

@ -70,6 +70,8 @@ class ResultState extends MusicBeatSubState
delay:Float
}> = [];
var playerCharacterId:Null<String>;
var rankBg:FunkinSprite;
final cameraBG:FunkinCamera;
final cameraScroll:FunkinCamera;
@ -164,7 +166,7 @@ class ResultState extends MusicBeatSubState
add(soundSystem);
// Fetch playable character data. Default to BF on the results screen if we can't find it.
var playerCharacterId:Null<String> = PlayerRegistry.instance.getCharacterOwnerId(params.characterId);
playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId);
var playerCharacter:Null<PlayableCharacter> = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf');
trace('Got playable character: ${playerCharacter?.getName()}');
@ -189,7 +191,7 @@ class ResultState extends MusicBeatSubState
if (!(animData.looped ?? true))
{
// Animation is not looped.
animation.onAnimationFinish.add((_name:String) -> {
animation.onAnimationComplete.add((_name:String) -> {
if (animation != null)
{
animation.anim.pause();
@ -198,7 +200,7 @@ class ResultState extends MusicBeatSubState
}
else if (animData.loopFrameLabel != null)
{
animation.onAnimationFinish.add((_name:String) -> {
animation.onAnimationComplete.add((_name:String) -> {
if (animation != null)
{
animation.playAnimation(animData.loopFrameLabel ?? ''); // unpauses this anim, since it's on PlayOnce!
@ -207,7 +209,7 @@ class ResultState extends MusicBeatSubState
}
else if (animData.loopFrame != null)
{
animation.onAnimationFinish.add((_name:String) -> {
animation.onAnimationComplete.add((_name:String) -> {
if (animation != null)
{
animation.anim.curFrame = animData.loopFrame ?? 0;
@ -742,6 +744,7 @@ class ResultState extends MusicBeatSubState
FlxG.switchState(FreeplayState.build(
{
{
character: playerCharacterId ?? "bf",
fromResults:
{
oldRank: Scoring.calculateRank(params?.prevScoreData),
@ -799,8 +802,9 @@ typedef ResultsStateParams =
/**
* The character ID for the song we just played.
* @default `bf`
*/
var characterId:String;
var ?characterId:String;
/**
* Whether the displayed score is a new highscore

View file

@ -109,8 +109,6 @@ class AnimateAtlasCharacter extends BaseCharacter
var loop:Bool = animData.looped;
this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop);
animFinished = false;
}
public override function hasAnimation(name:String):Bool
@ -124,17 +122,17 @@ class AnimateAtlasCharacter extends BaseCharacter
*/
public override function isAnimationFinished():Bool
{
return animFinished;
return mainSprite.isAnimationFinished();
}
function loadAtlasSprite():FlxAtlasSprite
{
trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.');
var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas(_data.assetPath, 'shared'));
var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas(_data.assetPath));
sprite.onAnimationFinish.removeAll();
sprite.onAnimationFinish.add(this.onAnimationFinished);
// sprite.onAnimationComplete.removeAll();
sprite.onAnimationComplete.add(this.onAnimationFinished);
return sprite;
}
@ -152,7 +150,6 @@ class AnimateAtlasCharacter extends BaseCharacter
// Make the game hold on the last frame.
this.mainSprite.cleanupAnimation(prefix);
// currentAnimName = null;
animFinished = true;
// Fallback to idle!
// playAnimation('idle', true, false);
@ -165,6 +162,11 @@ class AnimateAtlasCharacter extends BaseCharacter
this.mainSprite = sprite;
// This forces the atlas to recalcuate its width and height
this.mainSprite.alpha = 0.0001;
this.mainSprite.draw();
this.mainSprite.alpha = 1.0;
var feetPos:FlxPoint = feetPosition;
this.updateHitbox();

View file

@ -118,22 +118,6 @@ class BaseCharacter extends Bopper
*/
public var cameraFocusPoint(default, null):FlxPoint = new FlxPoint(0, 0);
override function set_animOffsets(value:Array<Float>):Array<Float>
{
if (animOffsets == null) value = [0, 0];
if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value;
// Make sure animOffets are halved when scale is 0.5.
var xDiff = (animOffsets[0] * this.scale.x / (this.isPixel ? 6 : 1)) - value[0];
var yDiff = (animOffsets[1] * this.scale.y / (this.isPixel ? 6 : 1)) - value[1];
// Call the super function so that camera focus point is not affected.
super.set_x(this.x + xDiff);
super.set_y(this.y + yDiff);
return animOffsets = value;
}
/**
* If the x position changes, other than via changing the animation offset,
* then we need to update the camera focus point.
@ -434,7 +418,6 @@ class BaseCharacter extends Bopper
else
{
// Play the idle animation.
// trace('${characterId}: attempting dance');
dance(true);
}
}
@ -528,6 +511,9 @@ class BaseCharacter extends Bopper
{
super.onNoteHit(event);
// If another script cancelled the event, don't do anything.
if (event.eventCanceled) return;
if (event.note.noteData.getMustHitNote() && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.
@ -560,6 +546,9 @@ class BaseCharacter extends Bopper
{
super.onNoteMiss(event);
// If another script cancelled the event, don't do anything.
if (event.eventCanceled) return;
if (event.note.noteData.getMustHitNote() && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.
@ -611,7 +600,7 @@ class BaseCharacter extends Bopper
/**
* Every time a wrong key is pressed, play the miss animation if we are Boyfriend.
*/
public override function onNoteGhostMiss(event:GhostMissNoteScriptEvent)
public override function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void
{
super.onNoteGhostMiss(event);
@ -651,7 +640,6 @@ class BaseCharacter extends Bopper
public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void
{
// FlxG.watch.addQuick('playAnim(${characterName})', name);
super.playAnimation(name, restart, ignoreOther, reversed);
}
}

View file

@ -305,6 +305,8 @@ class CharacterDataParser
icon = "darnell";
case "senpai-angry":
icon = "senpai";
case "spooky-dark":
icon = "spooky";
case "tankman-atlas":
icon = "tankman";
}

View file

@ -62,11 +62,13 @@ class MultiSparrowCharacter extends BaseCharacter
}
}
var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath, 'shared');
var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath);
if (texture == null)
{
trace('Multi-Sparrow atlas could not load PRIMARY texture: ${_data.assetPath}');
FlxG.log.error('Multi-Sparrow atlas could not load PRIMARY texture: ${_data.assetPath}');
return;
}
else
{
@ -76,7 +78,7 @@ class MultiSparrowCharacter extends BaseCharacter
for (asset in assetList)
{
var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset, 'shared');
var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset);
// If we don't do this, the unused textures will be removed as soon as they're loaded.
if (subTexture == null)

View file

@ -30,7 +30,7 @@ class PackerCharacter extends BaseCharacter
{
trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath, 'shared');
var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath);
if (tex == null)
{
trace('Could not load Packer sprite: ${_data.assetPath}');

View file

@ -33,7 +33,7 @@ class SparrowCharacter extends BaseCharacter
{
trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath, 'shared');
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
if (tex == null)
{
trace('Could not load Sparrow sprite: ${_data.assetPath}');

View file

@ -150,13 +150,17 @@ class HealthIcon extends FunkinSprite
{
if (characterId == 'bf-old')
{
isPixel = PlayState.instance.currentStage.getBoyfriend().isPixel;
PlayState.instance.currentStage.getBoyfriend().initHealthIcon(false);
}
else
{
characterId = 'bf-old';
isPixel = false;
loadCharacter(characterId);
}
lerpIconSize(true);
}
/**
@ -200,31 +204,45 @@ class HealthIcon extends FunkinSprite
if (bopEvery != 0)
{
// Lerp the health icon back to its normal size,
// while maintaining aspect ratio.
if (this.width > this.height)
{
// Apply linear interpolation while accounting for frame rate.
var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15));
setGraphicSize(targetSize, 0);
}
else
{
var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15));
setGraphicSize(0, targetSize);
}
lerpIconSize();
// Lerp the health icon back to its normal angle.
this.angle = MathUtil.coolLerp(this.angle, 0, 0.15);
this.updateHitbox();
}
this.updatePosition();
}
/**
* Does the calculation to lerp the icon size. Usually called every frame, but can be forced to the target size.
* Mainly forced when changing to old icon to not have a weird lerp related to changing from pixel icon to non-pixel old icon
* @param force Force the icon immedialtely to be the target size. Defaults to false.
*/
function lerpIconSize(force:Bool = false):Void
{
// Lerp the health icon back to its normal size,
// while maintaining aspect ratio.
if (this.width > this.height)
{
// Apply linear interpolation while accounting for frame rate.
var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15));
if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.x);
setGraphicSize(targetSize, 0);
}
else
{
var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15));
if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.y);
setGraphicSize(0, targetSize);
}
this.updateHitbox();
}
/**
* Update the position (and status) of the health icon.
*/
@ -412,6 +430,8 @@ class HealthIcon extends FunkinSprite
isLegacyStyle = !isNewSpritesheet(charId);
trace(' Loading health icon for character: $charId (legacy: $isLegacyStyle)');
if (!isLegacyStyle)
{
loadSparrow('icons/icon-$charId');

View file

@ -8,58 +8,51 @@ import funkin.graphics.FunkinSprite;
import funkin.play.PlayState;
import funkin.util.TimerUtil;
import funkin.util.EaseUtil;
import openfl.utils.Assets;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
@:nullSafety
class PopUpStuff extends FlxTypedGroup<FunkinSprite>
{
public var offsets:Array<Int> = [0, 0];
/**
* The current note style to use. This determines which graphics to display.
* For example, Week 6 uses the `pixel` note style, and mods can create their own.
*/
var noteStyle:NoteStyle;
override public function new()
override public function new(noteStyle:NoteStyle)
{
super();
this.noteStyle = noteStyle;
}
public function displayRating(daRating:String):Void
public function displayRating(daRating:Null<String>)
{
var perfStart:Float = TimerUtil.start();
if (daRating == null) daRating = "good";
var ratingPath:String = daRating;
if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
var rating:FunkinSprite = FunkinSprite.create(0, 0, ratingPath);
rating.scrollFactor.set(0.2, 0.2);
var rating:Null<FunkinSprite> = noteStyle.buildJudgementSprite(daRating);
if (rating == null) return;
rating.zIndex = 1000;
rating.x = (FlxG.width * 0.474) + offsets[0];
// rating.x -= FlxG.camera.scroll.x * 0.2;
rating.y = (FlxG.camera.height * 0.45 - 60) + offsets[1];
rating.x = (FlxG.width * 0.474);
rating.x -= rating.width / 2;
rating.y = (FlxG.camera.height * 0.45 - 60);
rating.y -= rating.height / 2;
var offsets = noteStyle.getJudgementSpriteOffsets(daRating);
rating.x += offsets[0];
rating.y += offsets[1];
rating.acceleration.y = 550;
rating.velocity.y -= FlxG.random.int(140, 175);
rating.velocity.x -= FlxG.random.int(0, 10);
add(rating);
var fadeEase = null;
if (PlayState.instance.currentStageId.startsWith('school'))
{
rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7));
rating.antialiasing = false;
rating.pixelPerfectRender = true;
rating.pixelPerfectPosition = true;
fadeEase = EaseUtil.stepped(2);
}
else
{
rating.setGraphicSize(Std.int(rating.width * 0.65));
rating.antialiasing = true;
}
rating.updateHitbox();
rating.x -= rating.width / 2;
rating.y -= rating.height / 2;
var fadeEase = noteStyle.isJudgementSpritePixel(daRating) ? EaseUtil.stepped(2) : null;
FlxTween.tween(rating, {alpha: 0}, 0.2,
{
@ -70,62 +63,10 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
startDelay: Conductor.instance.beatLengthMs * 0.001,
ease: fadeEase
});
trace('displayRating took: ${TimerUtil.seconds(perfStart)}');
}
public function displayCombo(?combo:Int = 0):Int
public function displayCombo(combo:Int = 0):Void
{
var perfStart:Float = TimerUtil.start();
if (combo == null) combo = 0;
var pixelShitPart1:String = "";
var pixelShitPart2:String = '';
if (PlayState.instance.currentStageId.startsWith('school'))
{
pixelShitPart1 = 'weeb/pixelUI/';
pixelShitPart2 = '-pixel';
}
var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2);
comboSpr.y = (FlxG.camera.height * 0.44) + offsets[1];
comboSpr.x = (FlxG.width * 0.507) + offsets[0];
// comboSpr.x -= FlxG.camera.scroll.x * 0.2;
comboSpr.acceleration.y = 600;
comboSpr.velocity.y -= 150;
comboSpr.velocity.x += FlxG.random.int(1, 10);
// add(comboSpr);
var fadeEase = null;
if (PlayState.instance.currentStageId.startsWith('school'))
{
comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 1));
comboSpr.antialiasing = false;
comboSpr.pixelPerfectRender = true;
comboSpr.pixelPerfectPosition = true;
fadeEase = EaseUtil.stepped(2);
}
else
{
comboSpr.setGraphicSize(Std.int(comboSpr.width * 0.7));
comboSpr.antialiasing = true;
}
comboSpr.updateHitbox();
FlxTween.tween(comboSpr, {alpha: 0}, 0.2,
{
onComplete: function(tween:FlxTween) {
remove(comboSpr, true);
comboSpr.destroy();
},
startDelay: Conductor.instance.beatLengthMs * 0.001,
ease: fadeEase
});
var seperatedScore:Array<Int> = [];
var tempCombo:Int = combo;
@ -140,31 +81,27 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
// seperatedScore.reverse();
var daLoop:Int = 1;
for (i in seperatedScore)
for (digit in seperatedScore)
{
var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2);
var numScore:Null<FunkinSprite> = noteStyle.buildComboNumSprite(digit);
if (numScore == null) continue;
if (PlayState.instance.currentStageId.startsWith('school'))
{
numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 1));
numScore.antialiasing = false;
numScore.pixelPerfectRender = true;
numScore.pixelPerfectPosition = true;
}
else
{
numScore.setGraphicSize(Std.int(numScore.width * 0.45));
numScore.antialiasing = true;
}
numScore.updateHitbox();
numScore.x = (FlxG.width * 0.507) - (36 * daLoop) - 65;
trace('numScore($daLoop) = ${numScore.x}');
numScore.y = (FlxG.camera.height * 0.44);
var offsets = noteStyle.getComboNumSpriteOffsets(digit);
numScore.x += offsets[0];
numScore.y += offsets[1];
numScore.x = comboSpr.x - (36 * daLoop) - 65; //- 90;
numScore.acceleration.y = FlxG.random.int(250, 300);
numScore.velocity.y -= FlxG.random.int(130, 150);
numScore.velocity.x = FlxG.random.float(-5, 5);
add(numScore);
var fadeEase = noteStyle.isComboNumSpritePixel(digit) ? EaseUtil.stepped(2) : null;
FlxTween.tween(numScore, {alpha: 0}, 0.2,
{
onComplete: function(tween:FlxTween) {
@ -177,9 +114,5 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
daLoop++;
}
trace('displayCombo took: ${TimerUtil.seconds(perfStart)}');
return combo;
}
}

View file

@ -81,7 +81,6 @@ class VideoCutscene
// Trigger the cutscene. Don't play the song in the background.
PlayState.instance.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
PlayState.instance.camCutscene.visible = true;
// Display a black screen to hide the game while the video is playing.
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
@ -305,7 +304,6 @@ class VideoCutscene
vid = null;
#end
PlayState.instance.camCutscene.visible = true;
PlayState.instance.camHUD.visible = true;
FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,

View file

@ -114,7 +114,7 @@ class ZoomCameraSongEvent extends SongEvent
name: 'zoom',
title: 'Zoom Level',
defaultValue: 1.0,
step: 0.1,
step: 0.05,
type: SongEventFieldType.FLOAT,
units: 'x'
},

View file

@ -1,6 +1,7 @@
package funkin.play.notes;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.NoteParamData;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
@ -65,6 +66,22 @@ class NoteSprite extends FunkinSprite
return this.noteData.kind = value;
}
/**
* An array of custom parameters for this note
*/
public var params(get, set):Array<NoteParamData>;
function get_params():Array<NoteParamData>
{
return this.noteData?.params ?? [];
}
function set_params(value:Array<NoteParamData>):Array<NoteParamData>
{
if (this.noteData == null) return value;
return this.noteData.params = value;
}
/**
* The data of the note (i.e. the direction.)
*/
@ -74,7 +91,7 @@ class NoteSprite extends FunkinSprite
{
if (frames == null) return value;
animation.play(DIRECTION_COLORS[value] + 'Scroll');
playNoteAnimation(value);
this.direction = value;
return this.direction;
@ -135,19 +152,37 @@ class NoteSprite extends FunkinSprite
this.hsvShader = new HSVShader();
setupNoteGraphic(noteStyle);
// Disables the update() function for performance.
this.active = false;
}
function setupNoteGraphic(noteStyle:NoteStyle):Void
/**
* Creates frames and animations
* @param noteStyle The `NoteStyle` instance
*/
public function setupNoteGraphic(noteStyle:NoteStyle):Void
{
noteStyle.buildNoteSprite(this);
setGraphicSize(Strumline.STRUMLINE_SIZE);
updateHitbox();
this.shader = hsvShader;
// `false` disables the update() function for performance.
this.active = noteStyle.isNoteAnimated();
}
/**
* Retrieve the value of the param with the given name
* @param name Name of the param
* @return Null<Dynamic>
*/
public function getParam(name:String):Null<Dynamic>
{
for (param in params)
{
if (param.name == name)
{
return param.value;
}
}
return null;
}
#if FLX_DEBUG
@ -173,6 +208,11 @@ class NoteSprite extends FunkinSprite
}
#end
function playNoteAnimation(value:Int):Void
{
animation.play(DIRECTION_COLORS[value] + 'Scroll');
}
public function desaturate():Void
{
this.hsvShader.saturation = 0.2;

View file

@ -16,6 +16,7 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.ui.options.PreferencesMenu;
import funkin.util.SortUtil;
import funkin.modding.events.ScriptEvent;
import funkin.play.notes.notekind.NoteKindManager;
/**
* A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player.
@ -93,6 +94,10 @@ class Strumline extends FlxSpriteGroup
final noteStyle:NoteStyle;
#if FEATURE_GHOST_TAPPING
var ghostTapTimer:Float = 0.0;
#end
/**
* The note data for the song. Should NOT be altered after the song starts,
* so we can easily rewind.
@ -178,21 +183,36 @@ class Strumline extends FlxSpriteGroup
super.update(elapsed);
updateNotes();
#if FEATURE_GHOST_TAPPING
updateGhostTapTimer(elapsed);
#end
}
#if FEATURE_GHOST_TAPPING
/**
* Returns `true` if no notes are in range of the strumline and the player can spam without penalty.
*/
public function mayGhostTap():Bool
{
// TODO: Refine this. Only querying "can be hit" is too tight but "is being rendered" is too loose.
// Also, if you just hit a note, there should be a (short) period where this is off so you can't spam.
// Any notes in range of the strumline.
if (getNotesMayHit().length > 0)
{
return false;
}
// Any hold notes in range of the strumline.
if (getHoldNotesHitOrMissed().length > 0)
{
return false;
}
// If there are any notes on screen, we can't ghost tap.
return notes.members.filter(function(note:NoteSprite) {
return note != null && note.alive && !note.hasBeenHit;
}).length == 0;
// Note has been hit recently.
if (ghostTapTimer > 0.0) return false;
// **yippee**
return true;
}
#end
/**
* Return notes that are within `Constants.HIT_WINDOW` ms of the strumline.
@ -491,6 +511,32 @@ class Strumline extends FlxSpriteGroup
}
}
/**
* Return notes that are within, or way after, `Constants.HIT_WINDOW` ms of the strumline.
* @return An array of `NoteSprite` objects.
*/
public function getNotesOnScreen():Array<NoteSprite>
{
return notes.members.filter(function(note:NoteSprite) {
return note != null && note.alive && !note.hasBeenHit;
});
}
#if FEATURE_GHOST_TAPPING
function updateGhostTapTimer(elapsed:Float):Void
{
// If it's still our turn, don't update the ghost tap timer.
if (getNotesOnScreen().length > 0) return;
ghostTapTimer -= elapsed;
if (ghostTapTimer <= 0)
{
ghostTapTimer = 0;
}
}
#end
/**
* Called when the PlayState skips a large amount of time forward or backward.
*/
@ -562,6 +608,10 @@ class Strumline extends FlxSpriteGroup
playStatic(dir);
}
resetScrollSpeed();
#if FEATURE_GHOST_TAPPING
ghostTapTimer = 0;
#end
}
public function applyNoteData(data:Array<SongNoteData>):Void
@ -601,6 +651,10 @@ class Strumline extends FlxSpriteGroup
note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition;
}
#if FEATURE_GHOST_TAPPING
ghostTapTimer = Constants.GHOST_TAP_DELAY;
#end
}
public function killNote(note:NoteSprite):Void
@ -708,11 +762,15 @@ class Strumline extends FlxSpriteGroup
if (noteSprite != null)
{
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
noteSprite.setupNoteGraphic(noteKindStyle);
noteSprite.direction = note.getDirection();
noteSprite.noteData = note;
noteSprite.x = this.x;
noteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
noteSprite.x -= (noteSprite.width - Strumline.STRUMLINE_SIZE) / 2; // Center it
noteSprite.x -= NUDGE;
// noteSprite.x += INITIAL_OFFSET;
noteSprite.y = -9999;
@ -727,6 +785,9 @@ class Strumline extends FlxSpriteGroup
if (holdNoteSprite != null)
{
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
holdNoteSprite.setupHoldNoteGraphic(noteKindStyle);
holdNoteSprite.parentStrumline = this;
holdNoteSprite.noteData = note;
holdNoteSprite.strumTime = note.time;

View file

@ -75,6 +75,13 @@ class StrumlineNote extends FlxSprite
function setup(noteStyle:NoteStyle):Void
{
if (noteStyle == null)
{
// If you get an exception on this line, check the debug console.
// You probably have a parsing error in your note style's JSON file.
throw "FATAL ERROR: Attempted to initialize PlayState with an invalid NoteStyle.";
}
noteStyle.applyStrumlineFrames(this);
noteStyle.applyStrumlineAnimations(this, this.direction);

View file

@ -99,7 +99,27 @@ class SustainTrail extends FlxSprite
*/
public function new(noteDirection:NoteDirection, sustainLength:Float, noteStyle:NoteStyle)
{
super(0, 0, noteStyle.getHoldNoteAssetPath());
super(0, 0);
// BASIC SETUP
this.sustainLength = sustainLength;
this.fullSustainLength = sustainLength;
this.noteDirection = noteDirection;
setupHoldNoteGraphic(noteStyle);
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
this.active = true; // This NEEDS to be true for the note to be drawn!
}
/**
* Creates hold note graphic and applies correct zooming
* @param noteStyle The note style
*/
public function setupHoldNoteGraphic(noteStyle:NoteStyle):Void
{
loadGraphic(noteStyle.getHoldNoteAssetPath());
antialiasing = true;
@ -109,13 +129,14 @@ class SustainTrail extends FlxSprite
endOffset = bottomClip = 1;
antialiasing = false;
}
else
{
endOffset = 0.5;
bottomClip = 0.9;
}
zoom = 1.0;
zoom *= noteStyle.fetchHoldNoteScale();
// BASIC SETUP
this.sustainLength = sustainLength;
this.fullSustainLength = sustainLength;
this.noteDirection = noteDirection;
zoom *= 0.7;
// CALCULATE SIZE
@ -131,9 +152,6 @@ class SustainTrail extends FlxSprite
updateColorTransform();
updateClipping();
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
this.active = true; // This NEEDS to be true for the note to be drawn!
}
function getBaseScrollSpeed()
@ -195,6 +213,11 @@ class SustainTrail extends FlxSprite
*/
public function updateClipping(songTime:Float = 0):Void
{
if (graphic == null)
{
return;
}
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0), 0, graphicHeight);
if (clipHeight <= 0.1)
{

View file

@ -0,0 +1,119 @@
package funkin.play.notes.notekind;
import funkin.modding.IScriptedClass.INoteScriptedClass;
import funkin.modding.events.ScriptEvent;
import flixel.math.FlxMath;
/**
* Class for note scripts
*/
class NoteKind implements INoteScriptedClass
{
/**
* The name of the note kind
*/
public var noteKind:String;
/**
* Description used in chart editor
*/
public var description:String;
/**
* Custom note style
*/
public var noteStyleId:Null<String>;
/**
* Custom parameters for the chart editor
*/
public var params:Array<NoteKindParam>;
public function new(noteKind:String, description:String = "", ?noteStyleId:String, ?params:Array<NoteKindParam>)
{
this.noteKind = noteKind;
this.description = description;
this.noteStyleId = noteStyleId;
this.params = params ?? [];
}
public function toString():String
{
return noteKind;
}
/**
* Retrieve all notes of this kind
* @return Array<NoteSprite>
*/
function getNotes():Array<NoteSprite>
{
var allNotes:Array<NoteSprite> = PlayState.instance.playerStrumline.notes.members.concat(PlayState.instance.opponentStrumline.notes.members);
return allNotes.filter(function(note:NoteSprite) {
return note != null && note.noteData.kind == this.noteKind;
});
}
public function onScriptEvent(event:ScriptEvent):Void {}
public function onCreate(event:ScriptEvent):Void {}
public function onDestroy(event:ScriptEvent):Void {}
public function onUpdate(event:UpdateScriptEvent):Void {}
public function onNoteIncoming(event:NoteScriptEvent):Void {}
public function onNoteHit(event:HitNoteScriptEvent):Void {}
public function onNoteMiss(event:NoteScriptEvent):Void {}
}
/**
* Abstract for setting the type of the `NoteKindParam`
* This was supposed to be an enum but polymod kept being annoying
*/
abstract NoteKindParamType(String) from String to String
{
public static final STRING:String = 'String';
public static final INT:String = 'Int';
public static final FLOAT:String = 'Float';
}
typedef NoteKindParamData =
{
/**
* If `min` is null, there is no minimum
*/
?min:Null<Float>,
/**
* If `max` is null, there is no maximum
*/
?max:Null<Float>,
/**
* If `step` is null, it will use 1.0
*/
?step:Null<Float>,
/**
* If `precision` is null, there will be 0 decimal places
*/
?precision:Null<Int>,
?defaultValue:Dynamic
}
/**
* Typedef for creating custom parameters in the chart editor
*/
typedef NoteKindParam =
{
name:String,
description:String,
type:NoteKindParamType,
?data:NoteKindParamData
}

View file

@ -0,0 +1,121 @@
package funkin.play.notes.notekind;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.notekind.ScriptedNoteKind;
import funkin.play.notes.notekind.NoteKind.NoteKindParam;
class NoteKindManager
{
static var noteKinds:Map<String, NoteKind> = [];
public static function loadScripts():Void
{
var scriptedClassName:Array<String> = ScriptedNoteKind.listScriptClasses();
if (scriptedClassName.length > 0)
{
trace('Instantiating ${scriptedClassName.length} scripted note kind(s)...');
for (scriptedClass in scriptedClassName)
{
try
{
var script:NoteKind = ScriptedNoteKind.init(scriptedClass, "unknown");
trace(' Initialized scripted note kind: ${script.noteKind}');
noteKinds.set(script.noteKind, script);
ChartEditorDropdowns.NOTE_KINDS.set(script.noteKind, script.description);
}
catch (e)
{
trace(' FAILED to instantiate scripted note kind: ${scriptedClass}');
trace(e);
}
}
}
}
/**
* Calls the given event for note kind scripts
* @param event The event
*/
public static function callEvent(event:ScriptEvent):Void
{
// if it is a note script event,
// then only call the event for the specific note kind script
if (Std.isOfType(event, NoteScriptEvent))
{
var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
var noteKind:NoteKind = noteKinds.get(noteEvent.note.kind);
if (noteKind != null)
{
ScriptEventDispatcher.callEvent(noteKind, event);
}
}
else // call the event for all note kind scripts
{
for (noteKind in noteKinds.iterator())
{
ScriptEventDispatcher.callEvent(noteKind, event);
}
}
}
/**
* Retrieve the note style from the given note kind
* @param noteKind note kind name
* @param suffix Used for song note styles
* @return NoteStyle
*/
public static function getNoteStyle(noteKind:String, ?suffix:String):Null<NoteStyle>
{
var noteStyleId:Null<String> = getNoteStyleId(noteKind, suffix);
if (noteStyleId == null)
{
return null;
}
return NoteStyleRegistry.instance.fetchEntry(noteStyleId);
}
/**
* Retrieve the note style id from the given note kind
* @param noteKind Note kind name
* @param suffix Used for song note styles
* @return Null<String>
*/
public static function getNoteStyleId(noteKind:String, ?suffix:String):Null<String>
{
if (suffix == '')
{
suffix = null;
}
var noteStyleId:Null<String> = noteKinds.get(noteKind)?.noteStyleId;
if (noteStyleId != null && suffix != null)
{
noteStyleId = NoteStyleRegistry.instance.hasEntry('$noteStyleId-$suffix') ? '$noteStyleId-$suffix' : noteStyleId;
}
return noteStyleId;
}
/**
* Retrive custom params of the given note kind
* @param noteKind Name of the note kind
* @return Array<NoteKindParam>
*/
public static function getParams(noteKind:Null<String>):Array<NoteKindParam>
{
if (noteKind == null)
{
return [];
}
return noteKinds.get(noteKind)?.params ?? [];
}
}

View file

@ -0,0 +1,9 @@
package funkin.play.notes.notekind;
/**
* A script that can be tied to a NoteKind.
* Create a scripted class that extends NoteKind,
* then call `super('noteKind')` in the constructor to use this.
*/
@:hscriptClass
class ScriptedNoteKind extends NoteKind implements polymod.hscript.HScriptedClass {}

View file

@ -1,5 +1,6 @@
package funkin.play.notes.notestyle;
import funkin.play.Countdown;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.data.animation.AnimationData;
@ -16,6 +17,7 @@ using funkin.data.animation.AnimationData.AnimationDataUtil;
* Holds the data for what assets to use for a note style,
* and provides convenience methods for building sprites based on them.
*/
@:nullSafety
class NoteStyle implements IRegistryEntry<NoteStyleData>
{
/**
@ -42,12 +44,8 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
this.id = id;
_data = _fetchData(id);
if (_data == null)
{
throw 'Could not parse note style data for id: $id';
}
this.fallback = NoteStyleRegistry.instance.fetchEntry(getFallbackID());
var fallbackID = _data.fallback;
if (fallbackID != null) this.fallback = NoteStyleRegistry.instance.fetchEntry(fallbackID);
}
/**
@ -72,7 +70,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
* Get the note style ID of the parent note style.
* @return The string ID, or `null` if there is no parent.
*/
function getFallbackID():Null<String>
public function getFallbackID():Null<String>
{
return _data.fallback;
}
@ -80,7 +78,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
public function buildNoteSprite(target:NoteSprite):Void
{
// Apply the note sprite frames.
var atlas:FlxAtlasFrames = buildNoteFrames(false);
var atlas:Null<FlxAtlasFrames> = buildNoteFrames(false);
if (atlas == null)
{
@ -89,29 +87,40 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
target.frames = atlas;
target.scale.x = _data.assets.note.scale;
target.scale.y = _data.assets.note.scale;
target.antialiasing = !_data.assets.note.isPixel;
target.antialiasing = !(_data.assets?.note?.isPixel ?? false);
// Apply the animations.
buildNoteAnimations(target);
// Set the scale.
target.setGraphicSize(Strumline.STRUMLINE_SIZE * getNoteScale());
target.updateHitbox();
}
var noteFrames:FlxAtlasFrames = null;
var noteFrames:Null<FlxAtlasFrames> = null;
function buildNoteFrames(force:Bool = false):FlxAtlasFrames
function buildNoteFrames(force:Bool = false):Null<FlxAtlasFrames>
{
if (!FunkinSprite.isTextureCached(Paths.image(getNoteAssetPath())))
var noteAssetPath = getNoteAssetPath();
if (noteAssetPath == null) return null;
if (!FunkinSprite.isTextureCached(Paths.image(noteAssetPath)))
{
FlxG.log.warn('Note texture is not cached: ${getNoteAssetPath()}');
FlxG.log.warn('Note texture is not cached: ${noteAssetPath}');
}
// Purge the note frames if the cached atlas is invalid.
if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null;
@:nullSafety(Off)
{
if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null;
}
if (noteFrames != null && !force) return noteFrames;
noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary());
var noteAssetPath = getNoteAssetPath();
if (noteAssetPath == null) return null;
noteFrames = Paths.getSparrowAtlas(noteAssetPath, getNoteAssetLibrary());
if (noteFrames == null)
{
@ -121,17 +130,18 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
return noteFrames;
}
function getNoteAssetPath(raw:Bool = false):String
function getNoteAssetPath(raw:Bool = false):Null<String>
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.note?.assetPath;
if (rawPath == null) return fallback.getNoteAssetPath(true);
if (rawPath == null && fallback != null) return fallback.getNoteAssetPath(true);
return rawPath;
}
// library:path
var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return getNoteAssetPath(true);
return parts[1];
}
@ -139,47 +149,63 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
function getNoteAssetLibrary():Null<String>
{
// library:path
var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return null;
return parts[0];
}
function buildNoteAnimations(target:NoteSprite):Void
{
var leftData:AnimationData = fetchNoteAnimationData(LEFT);
target.animation.addByPrefix('purpleScroll', leftData.prefix, leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY);
var downData:AnimationData = fetchNoteAnimationData(DOWN);
target.animation.addByPrefix('blueScroll', downData.prefix, downData.frameRate, downData.looped, downData.flipX, downData.flipY);
var upData:AnimationData = fetchNoteAnimationData(UP);
target.animation.addByPrefix('greenScroll', upData.prefix, upData.frameRate, upData.looped, upData.flipX, upData.flipY);
var rightData:AnimationData = fetchNoteAnimationData(RIGHT);
target.animation.addByPrefix('redScroll', rightData.prefix, rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
var leftData:Null<AnimationData> = fetchNoteAnimationData(LEFT);
if (leftData != null) target.animation.addByPrefix('purpleScroll', leftData.prefix ?? '', leftData.frameRate ?? 24, leftData.looped ?? false,
leftData.flipX, leftData.flipY);
var downData:Null<AnimationData> = fetchNoteAnimationData(DOWN);
if (downData != null) target.animation.addByPrefix('blueScroll', downData.prefix ?? '', downData.frameRate ?? 24, downData.looped ?? false,
downData.flipX, downData.flipY);
var upData:Null<AnimationData> = fetchNoteAnimationData(UP);
if (upData != null) target.animation.addByPrefix('greenScroll', upData.prefix ?? '', upData.frameRate ?? 24, upData.looped ?? false, upData.flipX,
upData.flipY);
var rightData:Null<AnimationData> = fetchNoteAnimationData(RIGHT);
if (rightData != null) target.animation.addByPrefix('redScroll', rightData.prefix ?? '', rightData.frameRate ?? 24, rightData.looped ?? false,
rightData.flipX, rightData.flipY);
}
function fetchNoteAnimationData(dir:NoteDirection):AnimationData
public function isNoteAnimated():Bool
{
return _data.assets?.note?.animated ?? false;
}
public function getNoteScale():Float
{
return _data.assets?.note?.scale ?? 1.0;
}
function fetchNoteAnimationData(dir:NoteDirection):Null<AnimationData>
{
var result:Null<AnimationData> = switch (dir)
{
case LEFT: _data.assets.note.data.left.toNamed();
case DOWN: _data.assets.note.data.down.toNamed();
case UP: _data.assets.note.data.up.toNamed();
case RIGHT: _data.assets.note.data.right.toNamed();
case LEFT: _data.assets?.note?.data?.left?.toNamed();
case DOWN: _data.assets?.note?.data?.down?.toNamed();
case UP: _data.assets?.note?.data?.up?.toNamed();
case RIGHT: _data.assets?.note?.data?.right?.toNamed();
};
return (result == null) ? fallback.fetchNoteAnimationData(dir) : result;
return (result == null && fallback != null) ? fallback.fetchNoteAnimationData(dir) : result;
}
public function getHoldNoteAssetPath(raw:Bool = false):String
public function getHoldNoteAssetPath(raw:Bool = false):Null<String>
{
if (raw)
{
// TODO: figure out why ?. didn't work here
var rawPath:Null<String> = (_data?.assets?.holdNote == null) ? null : _data?.assets?.holdNote?.assetPath;
return (rawPath == null) ? fallback.getHoldNoteAssetPath(true) : rawPath;
return (rawPath == null && fallback != null) ? fallback.getHoldNoteAssetPath(true) : rawPath;
}
// library:path
var parts = getHoldNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
var parts = getHoldNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return Paths.image(parts[0]);
return Paths.image(parts[1], parts[0]);
}
@ -187,15 +213,15 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
public function isHoldNotePixel():Bool
{
var data = _data?.assets?.holdNote;
if (data == null) return fallback.isHoldNotePixel();
return data.isPixel;
if (data == null && fallback != null) return fallback.isHoldNotePixel();
return data?.isPixel ?? false;
}
public function fetchHoldNoteScale():Float
{
var data = _data?.assets?.holdNote;
if (data == null) return fallback.fetchHoldNoteScale();
return data.scale;
if (data == null && fallback != null) return fallback.fetchHoldNoteScale();
return data?.scale ?? 1.0;
}
public function applyStrumlineFrames(target:StrumlineNote):Void
@ -203,7 +229,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
// TODO: Add support for multi-Sparrow.
// Will be less annoying after this is merged: https://github.com/HaxeFlixel/flixel/pull/2772
var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath(), getStrumlineAssetLibrary());
var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath() ?? '', getStrumlineAssetLibrary());
if (atlas == null)
{
@ -212,31 +238,30 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
target.frames = atlas;
target.scale.x = _data.assets.noteStrumline.scale;
target.scale.y = _data.assets.noteStrumline.scale;
target.antialiasing = !_data.assets.noteStrumline.isPixel;
target.scale.set(_data.assets.noteStrumline?.scale ?? 1.0);
target.antialiasing = !(_data.assets.noteStrumline?.isPixel ?? false);
}
function getStrumlineAssetPath(raw:Bool = false):String
function getStrumlineAssetPath(raw:Bool = false):Null<String>
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.noteStrumline?.assetPath;
if (rawPath == null) return fallback.getStrumlineAssetPath(true);
if (rawPath == null && fallback != null) return fallback.getStrumlineAssetPath(true);
return rawPath;
}
// library:path
var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
if (parts.length == 1) return getStrumlineAssetPath(true);
var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length <= 1) return getStrumlineAssetPath(true);
return parts[1];
}
function getStrumlineAssetLibrary():Null<String>
{
// library:path
var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
if (parts.length == 1) return null;
var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length <= 1) return null;
return parts[0];
}
@ -247,60 +272,592 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
function getStrumlineAnimationData(dir:NoteDirection):Array<AnimationData>
{
var result:Array<AnimationData> = switch (dir)
var result:Array<Null<AnimationData>> = switch (dir)
{
case NoteDirection.LEFT: [
_data.assets.noteStrumline.data.leftStatic.toNamed('static'),
_data.assets.noteStrumline.data.leftPress.toNamed('press'),
_data.assets.noteStrumline.data.leftConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.leftConfirmHold.toNamed('confirm-hold'),
_data.assets.noteStrumline?.data?.leftStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.leftPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.leftConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.leftConfirmHold?.toNamed('confirm-hold'),
];
case NoteDirection.DOWN: [
_data.assets.noteStrumline.data.downStatic.toNamed('static'),
_data.assets.noteStrumline.data.downPress.toNamed('press'),
_data.assets.noteStrumline.data.downConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.downConfirmHold.toNamed('confirm-hold'),
_data.assets.noteStrumline?.data?.downStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.downPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.downConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.downConfirmHold?.toNamed('confirm-hold'),
];
case NoteDirection.UP: [
_data.assets.noteStrumline.data.upStatic.toNamed('static'),
_data.assets.noteStrumline.data.upPress.toNamed('press'),
_data.assets.noteStrumline.data.upConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.upConfirmHold.toNamed('confirm-hold'),
_data.assets.noteStrumline?.data?.upStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.upPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.upConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.upConfirmHold?.toNamed('confirm-hold'),
];
case NoteDirection.RIGHT: [
_data.assets.noteStrumline.data.rightStatic.toNamed('static'),
_data.assets.noteStrumline.data.rightPress.toNamed('press'),
_data.assets.noteStrumline.data.rightConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.rightConfirmHold.toNamed('confirm-hold'),
_data.assets.noteStrumline?.data?.rightStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.rightPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.rightConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.rightConfirmHold?.toNamed('confirm-hold'),
];
default: [];
};
return result;
return thx.Arrays.filterNull(result);
}
public function applyStrumlineOffsets(target:StrumlineNote)
public function applyStrumlineOffsets(target:StrumlineNote):Void
{
target.x += _data.assets.noteStrumline.offsets[0];
target.y += _data.assets.noteStrumline.offsets[1];
var offsets = _data?.assets?.noteStrumline?.offsets ?? [0.0, 0.0];
target.x += offsets[0];
target.y += offsets[1];
}
public function getStrumlineScale():Float
{
return _data.assets.noteStrumline.scale;
return _data?.assets?.noteStrumline?.scale ?? 1.0;
}
public function isNoteSplashEnabled():Bool
{
var data = _data?.assets?.noteSplash?.data;
if (data == null) return fallback.isNoteSplashEnabled();
return data.enabled;
if (data == null) return fallback?.isNoteSplashEnabled() ?? false;
return data.enabled ?? false;
}
public function isHoldNoteCoverEnabled():Bool
{
var data = _data?.assets?.holdNoteCover?.data;
if (data == null) return fallback.isHoldNoteCoverEnabled();
return data.enabled;
if (data == null) return fallback?.isHoldNoteCoverEnabled() ?? false;
return data.enabled ?? false;
}
/**
* Build a sprite for the given step of the countdown.
* @param step
* @return A `FunkinSprite`, or `null` if no graphic is available for this step.
*/
public function buildCountdownSprite(step:Countdown.CountdownStep):Null<FunkinSprite>
{
var result = new FunkinSprite();
switch (step)
{
case THREE:
if (_data.assets.countdownThree == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownThree?.scale ?? 1.0;
result.scale.y = _data.assets.countdownThree?.scale ?? 1.0;
case TWO:
if (_data.assets.countdownTwo == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownTwo?.scale ?? 1.0;
result.scale.y = _data.assets.countdownTwo?.scale ?? 1.0;
case ONE:
if (_data.assets.countdownOne == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownOne?.scale ?? 1.0;
result.scale.y = _data.assets.countdownOne?.scale ?? 1.0;
case GO:
if (_data.assets.countdownGo == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownGo?.scale ?? 1.0;
result.scale.y = _data.assets.countdownGo?.scale ?? 1.0;
default:
// TODO: Do something here?
return null;
}
result.scrollFactor.set(0, 0);
result.antialiasing = !isCountdownSpritePixel(step);
result.updateHitbox();
return result;
}
function buildCountdownSpritePath(step:Countdown.CountdownStep):Null<String>
{
var basePath:Null<String> = null;
switch (step)
{
case THREE:
basePath = _data.assets.countdownThree?.assetPath;
case TWO:
basePath = _data.assets.countdownTwo?.assetPath;
case ONE:
basePath = _data.assets.countdownOne?.assetPath;
case GO:
basePath = _data.assets.countdownGo?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildCountdownSpritePath(step);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length < 1) return null;
if (parts.length == 1) return parts[0];
return parts[1];
}
function buildCountdownSpriteLibrary(step:Countdown.CountdownStep):Null<String>
{
var basePath:Null<String> = null;
switch (step)
{
case THREE:
basePath = _data.assets.countdownThree?.assetPath;
case TWO:
basePath = _data.assets.countdownTwo?.assetPath;
case ONE:
basePath = _data.assets.countdownOne?.assetPath;
case GO:
basePath = _data.assets.countdownGo?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildCountdownSpriteLibrary(step);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length <= 1) return null;
return parts[0];
}
public function isCountdownSpritePixel(step:Countdown.CountdownStep):Bool
{
switch (step)
{
case THREE:
var result = _data.assets.countdownThree?.isPixel;
if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step);
return result ?? false;
case TWO:
var result = _data.assets.countdownTwo?.isPixel;
if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step);
return result ?? false;
case ONE:
var result = _data.assets.countdownOne?.isPixel;
if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step);
return result ?? false;
case GO:
var result = _data.assets.countdownGo?.isPixel;
if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step);
return result ?? false;
default:
return false;
}
}
public function getCountdownSpriteOffsets(step:Countdown.CountdownStep):Array<Float>
{
switch (step)
{
case THREE:
var result = _data.assets.countdownThree?.offsets;
if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step);
return result ?? [0, 0];
case TWO:
var result = _data.assets.countdownTwo?.offsets;
if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step);
return result ?? [0, 0];
case ONE:
var result = _data.assets.countdownOne?.offsets;
if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step);
return result ?? [0, 0];
case GO:
var result = _data.assets.countdownGo?.offsets;
if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step);
return result ?? [0, 0];
default:
return [0, 0];
}
}
public function getCountdownSoundPath(step:Countdown.CountdownStep, raw:Bool = false):Null<String>
{
if (raw)
{
// TODO: figure out why ?. didn't work here
var rawPath:Null<String> = switch (step)
{
case Countdown.CountdownStep.THREE:
_data.assets.countdownThree?.data?.audioPath;
case Countdown.CountdownStep.TWO:
_data.assets.countdownTwo?.data?.audioPath;
case Countdown.CountdownStep.ONE:
_data.assets.countdownOne?.data?.audioPath;
case Countdown.CountdownStep.GO:
_data.assets.countdownGo?.data?.audioPath;
default:
null;
}
return (rawPath == null && fallback != null) ? fallback.getCountdownSoundPath(step, true) : rawPath;
}
// library:path
var parts = getCountdownSoundPath(step, true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return Paths.image(parts[0]);
return Paths.sound(parts[1], parts[0]);
}
public function buildJudgementSprite(rating:String):Null<FunkinSprite>
{
var result = new FunkinSprite();
switch (rating)
{
case "sick":
if (_data.assets.judgementSick == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementSick?.scale ?? 1.0;
result.scale.y = _data.assets.judgementSick?.scale ?? 1.0;
case "good":
if (_data.assets.judgementGood == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementGood?.scale ?? 1.0;
result.scale.y = _data.assets.judgementGood?.scale ?? 1.0;
case "bad":
if (_data.assets.judgementBad == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementBad?.scale ?? 1.0;
result.scale.y = _data.assets.judgementBad?.scale ?? 1.0;
case "shit":
if (_data.assets.judgementShit == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementShit?.scale ?? 1.0;
result.scale.y = _data.assets.judgementShit?.scale ?? 1.0;
default:
return null;
}
result.scrollFactor.set(0.2, 0.2);
var isPixel = isJudgementSpritePixel(rating);
result.antialiasing = !isPixel;
result.pixelPerfectRender = isPixel;
result.pixelPerfectPosition = isPixel;
result.updateHitbox();
return result;
}
public function isJudgementSpritePixel(rating:String):Bool
{
switch (rating)
{
case "sick":
var result = _data.assets.judgementSick?.isPixel;
if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating);
return result ?? false;
case "good":
var result = _data.assets.judgementGood?.isPixel;
if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating);
return result ?? false;
case "bad":
var result = _data.assets.judgementBad?.isPixel;
if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating);
return result ?? false;
case "GO":
var result = _data.assets.judgementShit?.isPixel;
if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating);
return result ?? false;
default:
return false;
}
}
function buildJudgementSpritePath(rating:String):Null<String>
{
var basePath:Null<String> = null;
switch (rating)
{
case "sick":
basePath = _data.assets.judgementSick?.assetPath;
case "good":
basePath = _data.assets.judgementGood?.assetPath;
case "bad":
basePath = _data.assets.judgementBad?.assetPath;
case "shit":
basePath = _data.assets.judgementShit?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildJudgementSpritePath(rating);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length < 1) return null;
if (parts.length == 1) return parts[0];
return parts[1];
}
public function getJudgementSpriteOffsets(rating:String):Array<Float>
{
switch (rating)
{
case "sick":
var result = _data.assets.judgementSick?.offsets;
if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating);
return result ?? [0, 0];
case "good":
var result = _data.assets.judgementGood?.offsets;
if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating);
return result ?? [0, 0];
case "bad":
var result = _data.assets.judgementBad?.offsets;
if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating);
return result ?? [0, 0];
case "shit":
var result = _data.assets.judgementShit?.offsets;
if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating);
return result ?? [0, 0];
default:
return [0, 0];
}
}
public function buildComboNumSprite(digit:Int):Null<FunkinSprite>
{
var result = new FunkinSprite();
switch (digit)
{
case 0:
if (_data.assets.comboNumber0 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber0?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber0?.scale ?? 1.0;
case 1:
if (_data.assets.comboNumber1 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber1?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber1?.scale ?? 1.0;
case 2:
if (_data.assets.comboNumber2 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber2?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber2?.scale ?? 1.0;
case 3:
if (_data.assets.comboNumber3 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber3?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber3?.scale ?? 1.0;
case 4:
if (_data.assets.comboNumber4 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber4?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber4?.scale ?? 1.0;
case 5:
if (_data.assets.comboNumber5 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber5?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber5?.scale ?? 1.0;
case 6:
if (_data.assets.comboNumber6 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber6?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber6?.scale ?? 1.0;
case 7:
if (_data.assets.comboNumber7 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber7?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber7?.scale ?? 1.0;
case 8:
if (_data.assets.comboNumber8 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber8?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber8?.scale ?? 1.0;
case 9:
if (_data.assets.comboNumber9 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber9?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber9?.scale ?? 1.0;
default:
return null;
}
var isPixel = isComboNumSpritePixel(digit);
result.antialiasing = !isPixel;
result.pixelPerfectRender = isPixel;
result.pixelPerfectPosition = isPixel;
result.updateHitbox();
return result;
}
public function isComboNumSpritePixel(digit:Int):Bool
{
switch (digit)
{
case 0:
var result = _data.assets.comboNumber0?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 1:
var result = _data.assets.comboNumber1?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 2:
var result = _data.assets.comboNumber2?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 3:
var result = _data.assets.comboNumber3?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 4:
var result = _data.assets.comboNumber4?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 5:
var result = _data.assets.comboNumber5?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 6:
var result = _data.assets.comboNumber6?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 7:
var result = _data.assets.comboNumber7?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 8:
var result = _data.assets.comboNumber8?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 9:
var result = _data.assets.comboNumber9?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
default:
return false;
}
}
function buildComboNumSpritePath(digit:Int):Null<String>
{
var basePath:Null<String> = null;
switch (digit)
{
case 0:
basePath = _data.assets.comboNumber0?.assetPath;
case 1:
basePath = _data.assets.comboNumber1?.assetPath;
case 2:
basePath = _data.assets.comboNumber2?.assetPath;
case 3:
basePath = _data.assets.comboNumber3?.assetPath;
case 4:
basePath = _data.assets.comboNumber4?.assetPath;
case 5:
basePath = _data.assets.comboNumber5?.assetPath;
case 6:
basePath = _data.assets.comboNumber6?.assetPath;
case 7:
basePath = _data.assets.comboNumber7?.assetPath;
case 8:
basePath = _data.assets.comboNumber8?.assetPath;
case 9:
basePath = _data.assets.comboNumber9?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildComboNumSpritePath(digit);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length < 1) return null;
if (parts.length == 1) return parts[0];
return parts[1];
}
public function getComboNumSpriteOffsets(digit:Int):Array<Float>
{
switch (digit)
{
case 0:
var result = _data.assets.comboNumber0?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 1:
var result = _data.assets.comboNumber1?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 2:
var result = _data.assets.comboNumber2?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 3:
var result = _data.assets.comboNumber3?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 4:
var result = _data.assets.comboNumber4?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 5:
var result = _data.assets.comboNumber5?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 6:
var result = _data.assets.comboNumber6?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 7:
var result = _data.assets.comboNumber7?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 8:
var result = _data.assets.comboNumber8?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 9:
var result = _data.assets.comboNumber9?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
default:
return [0, 0];
}
}
public function destroy():Void {}
@ -310,8 +867,17 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
return 'NoteStyle($id)';
}
static function _fetchData(id:String):Null<NoteStyleData>
static function _fetchData(id:String):NoteStyleData
{
return NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id));
var result = NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id));
if (result == null)
{
throw 'Could not parse note style data for id: $id';
}
else
{
return result;
}
}
}

View file

@ -277,7 +277,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
// If there are no difficulties in the metadata, there's a problem.
if (metadata.playData.difficulties.length == 0)
{
throw 'Song $id has no difficulties listed in metadata!';
trace('[SONG] Warning: Song $id (variation ${metadata.variation}) has no difficulties listed in metadata!');
continue;
}
// There may be more difficulties in the chart file than in the metadata,
@ -494,6 +495,24 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return diffFiltered;
}
public function listSuffixedDifficulties(variationIds:Array<String>, ?showLocked:Bool, ?showHidden:Bool):Array<String>
{
var result = [];
for (variation in variationIds)
{
var difficulties = listDifficulties(variation, null, showLocked, showHidden);
for (difficulty in difficulties)
{
var suffixedDifficulty = (variation != Constants.DEFAULT_VARIATION
&& variation != 'erect') ? '$difficulty-${variation}' : difficulty;
result.push(suffixedDifficulty);
}
}
return result;
}
public function hasDifficulty(diffId:String, ?variationId:String, ?variationIds:Array<String>):Bool
{
if (variationIds == null) variationIds = [];
@ -706,10 +725,11 @@ class SongDifficulty
* Cache the vocals for a given character.
* @param id The character we are about to play.
*/
public inline function cacheVocals():Void
public function cacheVocals():Void
{
for (voice in buildVoiceList())
{
trace('Caching vocal track: $voice');
FlxG.sound.cache(voice);
}
}
@ -721,6 +741,20 @@ class SongDifficulty
* @param id The character we are about to play.
*/
public function buildVoiceList():Array<String>
{
var result:Array<String> = [];
result = result.concat(buildPlayerVoiceList());
result = result.concat(buildOpponentVoiceList());
if (result.length == 0)
{
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
// Try to use `Voices.ogg` if no other voices are found.
if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
}
return result;
}
public function buildPlayerVoiceList():Array<String>
{
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
@ -728,62 +762,88 @@ class SongDifficulty
// For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
// Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
var playerId:String = characters.player;
var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix');
while (voicePlayer != null && !Assets.exists(voicePlayer))
if (characters.playerVocals == null)
{
// Remove the last suffix.
// For example, bf-car becomes bf.
playerId = playerId.split('-').slice(0, -1).join('-');
// Try again.
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
if (voicePlayer == null)
{
// Try again without $suffix.
playerId = characters.player;
voicePlayer = Paths.voices(this.song.id, '-${playerId}');
while (voicePlayer != null && !Assets.exists(voicePlayer))
var playerId:String = characters.player;
var playerVoice:String = Paths.voices(this.song.id, '-${playerId}$suffix');
while (playerVoice != null && !Assets.exists(playerVoice))
{
// Remove the last suffix.
// For example, bf-car becomes bf.
playerId = playerId.split('-').slice(0, -1).join('-');
// Try again.
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
if (playerVoice == null)
{
// Try again without $suffix.
playerId = characters.player;
playerVoice = Paths.voices(this.song.id, '-${playerId}');
while (playerVoice != null && !Assets.exists(playerVoice))
{
// Remove the last suffix.
playerId = playerId.split('-').slice(0, -1).join('-');
// Try again.
playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
}
}
var opponentId:String = characters.opponent;
var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
{
// Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again.
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
return playerVoice != null ? [playerVoice] : [];
}
if (voiceOpponent == null)
else
{
// Try again without $suffix.
opponentId = characters.opponent;
voiceOpponent = Paths.voices(this.song.id, '-${opponentId}');
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
// The metadata explicitly defines the list of voices.
var playerIds:Array<String> = characters?.playerVocals ?? [characters.player];
var playerVoices:Array<String> = playerIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
return playerVoices;
}
}
public function buildOpponentVoiceList():Array<String>
{
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
// Automatically resolve voices by removing suffixes.
// For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
// Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
if (characters.opponentVocals == null)
{
var opponentId:String = characters.opponent;
var opponentVoice:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
while (opponentVoice != null && !Assets.exists(opponentVoice))
{
// Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again.
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
}
if (opponentVoice == null)
{
// Try again without $suffix.
opponentId = characters.opponent;
opponentVoice = Paths.voices(this.song.id, '-${opponentId}');
while (opponentVoice != null && !Assets.exists(opponentVoice))
{
// Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again.
opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
}
}
}
var result:Array<String> = [];
if (voicePlayer != null) result.push(voicePlayer);
if (voiceOpponent != null) result.push(voiceOpponent);
if (voicePlayer == null && voiceOpponent == null)
{
// Try to use `Voices.ogg` if no other voices are found.
if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
return opponentVoice != null ? [opponentVoice] : [];
}
else
{
// The metadata explicitly defines the list of voices.
var opponentIds:Array<String> = characters?.opponentVocals ?? [characters.opponent];
var opponentVoices:Array<String> = opponentIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
return opponentVoices;
}
return result;
}
/**
@ -795,26 +855,19 @@ class SongDifficulty
{
var result:VoicesGroup = new VoicesGroup();
var voiceList:Array<String> = buildVoiceList();
if (voiceList.length == 0)
{
trace('Could not find any voices for song ${this.song.id}');
return result;
}
var playerVoiceList:Array<String> = this.buildPlayerVoiceList();
var opponentVoiceList:Array<String> = this.buildOpponentVoiceList();
// Add player vocals.
if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(voiceList[0]));
// Add opponent vocals.
if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(voiceList[1]));
// Add additional vocals.
if (voiceList.length > 2)
for (playerVoice in playerVoiceList)
{
for (i in 2...voiceList.length)
{
result.add(FunkinSound.load(Assets.getSound(voiceList[i])));
}
result.addPlayerVoice(FunkinSound.load(playerVoice));
}
// Add opponent vocals.
for (opponentVoice in opponentVoiceList)
{
result.addOpponentVoice(FunkinSound.load(opponentVoice));
}
result.playerVoicesOffset = offsets.getVocalOffset(characters.player);

View file

@ -1,6 +1,7 @@
package funkin.play.stage;
import flixel.FlxSprite;
import flixel.FlxCamera;
import flixel.math.FlxPoint;
import flixel.util.FlxTimer;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
@ -45,8 +46,8 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
public var idleSuffix(default, set):String = '';
/**
* If this bopper is rendered with pixel art,
* disable anti-aliasing and render at 6x scale.
* If this bopper is rendered with pixel art, disable anti-aliasing.
* @default `false`
*/
public var isPixel(default, set):Bool = false;
@ -79,11 +80,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
if (globalOffsets == null) globalOffsets = [0, 0];
if (globalOffsets == value) return value;
var xDiff = globalOffsets[0] - value[0];
var yDiff = globalOffsets[1] - value[1];
this.x += xDiff;
this.y += yDiff;
return globalOffsets = value;
}
@ -97,12 +93,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
if (animOffsets == null) animOffsets = [0, 0];
if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value;
var xDiff = animOffsets[0] - value[0];
var yDiff = animOffsets[1] - value[1];
this.x += xDiff;
this.y += yDiff;
return animOffsets = value;
}
@ -320,19 +310,12 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
function applyAnimationOffsets(name:String):Void
{
var offsets = animationOffsets.get(name);
if (offsets != null && !(offsets[0] == 0 && offsets[1] == 0))
{
this.animOffsets = [offsets[0] + globalOffsets[0], offsets[1] + globalOffsets[1]];
}
else
{
this.animOffsets = globalOffsets;
}
this.animOffsets = offsets;
}
public function isAnimationFinished():Bool
{
return this.animation.finished;
return this.animation?.finished ?? false;
}
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
@ -351,6 +334,15 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
return this.animation.curAnim.name;
}
// override getScreenPosition (used by FlxSprite's draw method) to account for animation offsets.
override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
{
var output:FlxPoint = super.getScreenPosition(result, camera);
output.x -= (animOffsets[0] - globalOffsets[0]) * this.scale.x;
output.y -= (animOffsets[1] - globalOffsets[1]) * this.scale.y;
return output;
}
public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {}

View file

@ -386,7 +386,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
{
if (character == null) return;
#if debug
#if FEATURE_DEBUG_FUNCTIONS
// Temporary marker that shows where the character's location is relative to.
// Should display at the stage position of the character (before any offsets).
// TODO: Make this a toggle? It's useful to turn on from time to time.
@ -436,8 +436,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
// Start with the per-stage character position.
// Subtracting the origin ensures characters are positioned relative to their feet.
// Subtracting the global offset allows positioning on a per-character basis.
character.x = stageCharData.position[0] - character.characterOrigin.x + character.globalOffsets[0];
character.y = stageCharData.position[1] - character.characterOrigin.y + character.globalOffsets[1];
// We previously applied the global offset here but that is now done elsewhere.
character.x = stageCharData.position[0] - character.characterOrigin.x;
character.y = stageCharData.position[1] - character.characterOrigin.y;
@:privateAccess(funkin.play.stage.Bopper)
{
@ -451,7 +452,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
character.cameraFocusPoint.x += stageCharData.cameraOffsets[0];
character.cameraFocusPoint.y += stageCharData.cameraOffsets[1];
#if debug
#if FEATURE_DEBUG_FUNCTIONS
// Draw the debug icon at the character's feet.
if (charType == BF || charType == DAD)
{
@ -468,7 +469,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
ScriptEventDispatcher.callEvent(character, new ScriptEvent(ADDED, false));
#if debug
#if FEATURE_DEBUG_FUNCTIONS
debugIconGroup.add(debugIcon);
debugIconGroup.add(debugIcon2);
#end
@ -769,39 +770,15 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
* A function that gets called once per step in the song.
* @param curStep The current step number.
*/
public function onStepHit(event:SongTimeScriptEvent):Void
{
// Override me in your scripted stage to perform custom behavior!
// Make sure to call super.onStepHit(event) if you want to keep the boppers dancing.
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onStepHit(event:SongTimeScriptEvent):Void {}
/**
* A function that gets called once per beat in the song (once every four steps).
* @param curStep The current beat number.
*/
public function onBeatHit(event:SongTimeScriptEvent):Void
{
// Override me in your scripted stage to perform custom behavior!
// Make sure to call super.onBeatHit(event) if you want to keep the boppers dancing.
public function onBeatHit(event:SongTimeScriptEvent):Void {}
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onUpdate(event:UpdateScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onUpdate(event:UpdateScriptEvent) {}
public override function kill()
{
@ -883,129 +860,41 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
public function onScriptEvent(event:ScriptEvent)
{
// Ensure all custom events get broadcast to the elements of the stage.
// If we do it here, we don't have to add a handler to EACH script event function.
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onPause(event:PauseScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onResume(event:ScriptEvent) {}
public function onSongStart(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongStart(event:ScriptEvent) {}
public function onSongEnd(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongEnd(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onGameOver(event:ScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onNoteIncoming(event:NoteScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onNoteIncoming(event:NoteScriptEvent) {}
public function onNoteHit(event:HitNoteScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onNoteHit(event:HitNoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onNoteMiss(event:NoteScriptEvent) {}
public function onSongEvent(event:SongEventScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongEvent(event:SongEventScriptEvent) {}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onSongLoaded(event:SongLoadScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongLoaded(event:SongLoadScriptEvent) {}
public function onSongRetry(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongRetry(event:ScriptEvent) {}
}

View file

@ -121,6 +121,12 @@ class Save
modOptions: [],
},
unlocks:
{
// Default to having seen the default character.
charactersSeen: ["bf"],
},
optionsChartEditor:
{
// Reasonable defaults.
@ -393,6 +399,22 @@ class Save
return data.optionsChartEditor.playbackSpeed;
}
public var charactersSeen(get, never):Array<String>;
function get_charactersSeen():Array<String>
{
return data.unlocks.charactersSeen;
}
/**
* When we've seen a character unlock, add it to the list of characters seen.
* @param character
*/
public function addCharacterSeen(character:String):Void
{
data.unlocks.charactersSeen.push(character);
}
/**
* Return the score the user achieved for a given level on a given difficulty.
*
@ -471,10 +493,18 @@ class Save
for (difficulty in difficultyList)
{
var score:Null<SaveScoreData> = getLevelScore(levelId, difficulty);
// TODO: Do we need to check accuracy/score here?
if (score != null)
{
return true;
if (score.score > 0)
{
// Level has score data, which means we cleared it!
return true;
}
else
{
// Level has score data, but the score is 0.
return false;
}
}
}
return false;
@ -630,10 +660,18 @@ class Save
for (difficulty in difficultyList)
{
var score:Null<SaveScoreData> = getSongScore(songId, difficulty);
// TODO: Do we need to check accuracy/score here?
if (score != null)
{
return true;
if (score.score > 0)
{
// Level has score data, which means we cleared it!
return true;
}
else
{
// Level has score data, but the score is 0.
return false;
}
}
}
return false;
@ -956,6 +994,8 @@ typedef RawSaveData =
*/
var options:SaveDataOptions;
var unlocks:SaveDataUnlocks;
/**
* The user's favorited songs in the Freeplay menu,
* as a list of song IDs.
@ -980,6 +1020,15 @@ typedef SaveApiNewgroundsData =
var sessionId:Null<String>;
}
typedef SaveDataUnlocks =
{
/**
* Every time we see the unlock animation for a character,
* add it to this list so that we don't show it again.
*/
var charactersSeen:Array<String>;
}
/**
* An anoymous structure containing options about the user's high scores.
*/

View file

@ -152,6 +152,32 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
}
}
public function getWidth():Int
{
var width = 0;
for (char in this.text.split(""))
{
switch (char)
{
case " ":
{
width += 40;
}
case "\n":
{}
case char:
{
var sprite = new AtlasChar(atlas, char);
sprite.revive();
sprite.char = char;
sprite.alpha = 1;
width += Std.int(sprite.width);
}
}
}
return width;
}
override function toString()
{
return "InputItem, " + FlxStringUtil.getDebugString([

View file

@ -78,9 +78,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
{
// Emergency exit button.
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5) debug_refreshModules();
}
override function update(elapsed:Float)
@ -114,12 +111,10 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
ModuleHandler.callEvent(event);
}
function debug_refreshModules()
function reloadAssets()
{
PolymodHandler.forceReloadAssets();
this.destroy();
// Create a new instance of the current state, so old data is cleared.
FlxG.resetState();
}

View file

@ -72,9 +72,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
// Emergency exit button.
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5) debug_refreshModules();
// Display Conductor info in the watch window.
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
Conductor.watchQuick(conductorInUse);
@ -82,7 +79,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
dispatchEvent(new UpdateScriptEvent(elapsed));
}
function debug_refreshModules()
function reloadAssets()
{
PolymodHandler.forceReloadAssets();

View file

@ -22,14 +22,26 @@ class PixelatedIcon extends FlxSprite
switch (char)
{
case 'monster-christmas':
charPath += 'monsterpixel';
case 'mom-car':
charPath += 'mommypixel';
case 'darnell-blazin':
charPath += 'darnellpixel';
case 'senpai-angry':
charPath += 'senpaipixel';
case "bf-christmas" | "bf-car" | "bf-pixel" | "bf-holding-gf":
charPath += "bfpixel";
case "monster-christmas":
charPath += "monsterpixel";
case "mom" | "mom-car":
charPath += "mommypixel";
case "pico-blazin" | "pico-playable" | "pico-speaker":
charPath += "picopixel";
case "gf-christmas" | "gf-car" | "gf-pixel" | "gf-tankmen":
charPath += "gfpixel";
case "dad":
charPath += "dadpixel";
case "darnell-blazin":
charPath += "darnellpixel";
case "senpai-angry":
charPath += "senpaipixel";
case "spooky-dark":
charPath += "spookypixel";
case "tankman-atlas":
charPath += "tankmanpixel";
default:
charPath += '${char}pixel';
}

View file

@ -9,7 +9,7 @@ class CharSelectPlayer extends FlxAtlasSprite
{
super(x, y, Paths.animateAtlas("charSelect/bfChill"));
onAnimationFinish.add(function(animLabel:String) {
onAnimationComplete.add(function(animLabel:String) {
switch (animLabel)
{
case "slidein":

View file

@ -1,27 +1,31 @@
package funkin.ui.charSelect;
import funkin.ui.freeplay.FreeplayState;
import flixel.text.FlxText;
import funkin.ui.PixelatedIcon;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.math.FlxPoint;
import flixel.tweens.FlxTween;
import openfl.display.BlendMode;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup;
import funkin.play.stage.Stage;
import flixel.math.FlxPoint;
import flixel.sound.FlxSound;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.data.freeplay.player.PlayerData;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.graphics.FunkinCamera;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxObject;
import openfl.display.BlendMode;
import flixel.group.FlxGroup;
import funkin.play.stage.Stage;
import funkin.ui.freeplay.charselect.PlayableCharacter;
import funkin.ui.freeplay.FreeplayState;
import funkin.ui.PixelatedIcon;
import funkin.util.MathUtil;
import flixel.util.FlxTimer;
import flixel.tweens.FlxEase;
import flixel.sound.FlxSound;
import funkin.audio.FunkinSound;
import funkin.vis.dsp.SpectralAnalyzer;
import openfl.display.BlendMode;
class CharSelectSubState extends MusicBeatSubState
{
@ -67,8 +71,29 @@ class CharSelectSubState extends MusicBeatSubState
{
super();
availableChars.set(4, "bf");
availableChars.set(3, "pico");
loadAvailableCharacters();
}
function loadAvailableCharacters():Void
{
var playerIds:Array<String> = PlayerRegistry.instance.listEntryIds();
for (playerId in playerIds)
{
var player:Null<PlayableCharacter> = PlayerRegistry.instance.fetchEntry(playerId);
if (player == null) continue;
var playerData = player.getCharSelectData();
if (playerData == null) continue;
var targetPosition:Int = playerData.position ?? 0;
while (availableChars.exists(targetPosition))
{
targetPosition += 1;
}
trace('Placing player ${playerId} at position ${targetPosition}');
availableChars.set(targetPosition, playerId);
}
}
override public function create():Void
@ -245,7 +270,7 @@ class CharSelectSubState extends MusicBeatSubState
cursorBlue.scrollFactor.set();
cursorDarkBlue.scrollFactor.set();
FlxTween.color(cursor, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: FlxTween.PINGPONG});
FlxTween.color(cursor, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: PINGPONG});
// FlxG.debugger.track(cursor);
@ -269,7 +294,6 @@ class CharSelectSubState extends MusicBeatSubState
}
var grpIcons:FlxSpriteGroup;
var grpXSpread(default, set):Float = 107;
var grpYSpread(default, set):Float = 127;
@ -600,7 +624,7 @@ class CharSelectSubState extends MusicBeatSubState
playerChill.visible = false;
playerChillOut.visible = true;
playerChillOut.anim.goToFrameLabel("slideout");
playerChillOut.anim.callback = (_, frame:Int) -> {
playerChillOut.onAnimationFrame.add((_, frame:Int) -> {
if (frame == playerChillOut.anim.getFrameLabel("slideout").index + 1)
{
playerChill.visible = true;
@ -612,7 +636,7 @@ class CharSelectSubState extends MusicBeatSubState
playerChillOut.switchChar(value);
playerChillOut.visible = false;
}
};
});
return value;
}

View file

@ -34,7 +34,13 @@ class CreditsState extends MusicBeatState
* To use a font from the `assets` folder, use `Paths.font(...)`.
* Choose something that will render Unicode properly.
*/
#if windows
static final CREDITS_FONT = 'Consolas';
#elseif mac
static final CREDITS_FONT = 'Menlo';
#else
static final CREDITS_FONT = "Courier New";
#end
/**
* The size of the font.

View file

@ -54,7 +54,7 @@ class DebugMenuSubState extends MusicBeatSubState
// Create each menu item.
// Call onMenuChange when the first item is created to move the camera .
#if CHART_EDITOR_SUPPORTED
#if FEATURE_CHART_EDITOR
onMenuChange(createItem("CHART EDITOR", openChartEditor));
#end
// createItem("Input Offset Testing", openInputOffsetTesting);

View file

@ -1,33 +1,26 @@
package funkin.ui.debug.anim;
import flixel.addons.display.FlxBackdrop;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.ui.FlxInputText;
import flixel.addons.ui.FlxUIDropDownMenu;
import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFrame;
import flixel.group.FlxGroup;
import flixel.math.FlxPoint;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.input.Cursor;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.character.SparrowCharacter;
import funkin.ui.mainmenu.MainMenuState;
import funkin.util.MouseUtil;
import funkin.util.SerializerUtil;
import funkin.util.SortUtil;
import haxe.ui.components.DropDown;
import haxe.ui.core.Component;
import haxe.ui.containers.dialogs.CollapsibleDialog;
import haxe.ui.core.Screen;
import haxe.ui.events.ItemEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.RuntimeComponentBuilder;
import lime.utils.Assets as LimeAssets;
@ -36,9 +29,6 @@ import openfl.events.Event;
import openfl.events.IOErrorEvent;
import openfl.geom.Rectangle;
import openfl.net.FileReference;
import openfl.net.URLLoader;
import openfl.net.URLRequest;
import openfl.utils.ByteArray;
using flixel.util.FlxSpriteUtil;
@ -55,10 +45,10 @@ class DebugBoundingState extends FlxState
TODAY'S TO-DO
- Cleaner UI
*/
var bg:FlxSprite;
var bg:FlxBackdrop;
var fileInfo:FlxText;
var txtGrp:FlxGroup;
var txtGrp:FlxTypedGroup<FlxText>;
var hudCam:FlxCamera;
@ -66,16 +56,23 @@ class DebugBoundingState extends FlxState
var spriteSheetView:FlxGroup;
var offsetView:FlxGroup;
var animDropDownMenu:FlxUIDropDownMenu;
var dropDownSetup:Bool = false;
var onionSkinChar:FlxSprite;
var txtOffsetShit:FlxText;
var uiStuff:Component;
var offsetEditorDialog:CollapsibleDialog;
var offsetAnimationDropdown:DropDown;
var haxeUIFocused(get, default):Bool = false;
var currentAnimationName(get, never):String;
function get_currentAnimationName():String
{
return offsetAnimationDropdown?.value?.id ?? "idle";
}
function get_haxeUIFocused():Bool
{
// get the screen position, according to the HUD camera, temp default to FlxG.camera juuust in case?
@ -87,46 +84,35 @@ class DebugBoundingState extends FlxState
{
Paths.setCurrentLevel('week1');
// lv.
// lv.onChange = function(e:UIEvent)
// {
// trace(e.type);
// // trace(e.data.curView);
// // var item:haxe.ui.core.ItemRenderer = cast e.target;
// trace(e.target);
// // if (e.type == "change")
// // {
// // curView = cast e.data;
// // }
// };
hudCam = new FlxCamera();
hudCam.bgColor.alpha = 0;
bg = FlxGridOverlay.create(10, 10);
// bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.GREEN);
bg.scrollFactor.set();
bg = new FlxBackdrop(FlxGridOverlay.createGrid(10, 10, FlxG.width, FlxG.height, true, 0xffe7e6e6, 0xffd9d5d5));
add(bg);
// we are setting this as the default draw camera only temporarily, to trick haxeui
FlxG.cameras.add(hudCam);
var str = Paths.xml('ui/animation-editor/offset-editor-view');
uiStuff = RuntimeComponentBuilder.fromAsset(str);
offsetEditorDialog = cast RuntimeComponentBuilder.fromAsset(str);
// uiStuff.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET;
var dropdown:DropDown = cast uiStuff.findComponent("swapper");
dropdown.onChange = function(e:UIEvent) {
// offsetEditorDialog.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET;
var viewDropdown:DropDown = offsetEditorDialog.findComponent("swapper", DropDown);
viewDropdown.onChange = function(e:UIEvent) {
trace(e.type);
curView = cast e.data.curView;
trace(e.data);
// trace(e.data);
};
uiStuff.cameras = [hudCam];
offsetAnimationDropdown = offsetEditorDialog.findComponent("animationDropdown", DropDown);
add(uiStuff);
offsetEditorDialog.cameras = [hudCam];
add(offsetEditorDialog);
// Anchor to the right side by default
// offsetEditorDialog.x = FlxG.width - offsetEditorDialog.width;
// sets the default camera back to FlxG.camera, since we set it to hudCamera for haxeui stuf
FlxG.cameras.setDefaultDrawTarget(FlxG.camera, true);
@ -159,7 +145,7 @@ class DebugBoundingState extends FlxState
generateOutlines(tex.frames);
txtGrp = new FlxGroup();
txtGrp = new FlxTypedGroup<FlxText>();
txtGrp.cameras = [hudCam];
spriteSheetView.add(txtGrp);
@ -168,64 +154,6 @@ class DebugBoundingState extends FlxState
addInfo('Height', bf.height);
spriteSheetView.add(swagOutlines);
FlxG.stage.window.onDropFile.add(function(path:String) {
// WACKY ASS TESTING SHIT FOR WEB FILE LOADING??
#if web
var swagList:FileList = cast path;
var objShit = js.html.URL.createObjectURL(swagList.item(0));
trace(objShit);
var funnysound = new FunkinSound().loadStream('https://cdn.discordapp.com/attachments/767500676166451231/817821618251759666/Flutter.mp3', false, false,
null, function() {
trace('LOADED SHIT??');
});
funnysound.volume = 1;
funnysound.play();
var urlShit = new URLLoader(new URLRequest(objShit));
new FlxTimer().start(3, function(tmr:FlxTimer) {
// music lol!
if (urlShit.dataFormat == BINARY)
{
// var daSwagBytes:ByteArray = urlShit.data;
// FlxG.sound.playMusic();
// trace('is binary!!');
}
trace(urlShit.dataFormat);
});
// remove(bf);
// FlxG.bitmap.removeByKey(Paths.image('characters/temp'));
// Assets.cache.clear();
// bf.loadGraphic(objShit);
// add(bf);
// trace(swagList.item(0).name);
// var urlShit = js.html.URL.createObjectURL(path);
#end
#if sys
trace("DROPPED FILE FROM: " + Std.string(path));
var newPath = "./" + Paths.image('characters/temp');
File.copy(path, newPath);
var swag = Paths.image('characters/temp');
if (bf != null) remove(bf);
FlxG.bitmap.removeByKey(Paths.image('characters/temp'));
Assets.cache.clear();
bf.loadGraphic(Paths.image('characters/temp'));
add(bf);
#end
});
}
function generateOutlines(frameShit:Array<FlxFrame>):Void
@ -260,15 +188,9 @@ class DebugBoundingState extends FlxState
txtOffsetShit = new FlxText(20, 20, 0, "", 20);
txtOffsetShit.setFormat(Paths.font("vcr.ttf"), 26, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
txtOffsetShit.cameras = [hudCam];
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
offsetView.add(txtOffsetShit);
animDropDownMenu = new FlxUIDropDownMenu(0, 0, FlxUIDropDownMenu.makeStrIdLabelArray(['weed'], true));
animDropDownMenu.cameras = [hudCam];
// Move to bottom right corner
animDropDownMenu.x = FlxG.width - animDropDownMenu.width - 20;
animDropDownMenu.y = FlxG.height - animDropDownMenu.height - 20;
offsetView.add(animDropDownMenu);
var characters:Array<String> = CharacterDataParser.listCharacterIds();
characters = characters.filter(function(charId:String) {
var char = CharacterDataParser.fetchCharacterData(charId);
@ -276,7 +198,7 @@ class DebugBoundingState extends FlxState
});
characters.sort(SortUtil.alphabetically);
var charDropdown:DropDown = cast uiStuff.findComponent('characterDropdown');
var charDropdown:DropDown = offsetEditorDialog.findComponent('characterDropdown', DropDown);
for (char in characters)
{
charDropdown.dataSource.add({text: char});
@ -289,32 +211,47 @@ class DebugBoundingState extends FlxState
public var mouseOffset:FlxPoint = FlxPoint.get(0, 0);
public var oldPos:FlxPoint = FlxPoint.get(0, 0);
public var movingCharacter:Bool = false;
function mouseOffsetMovement()
{
if (swagChar != null)
{
if (FlxG.mouse.justPressed)
if (FlxG.mouse.justPressed && !haxeUIFocused)
{
movingCharacter = true;
mouseOffset.set(FlxG.mouse.x - -swagChar.animOffsets[0], FlxG.mouse.y - -swagChar.animOffsets[1]);
}
if (!movingCharacter) return;
if (FlxG.mouse.pressed)
{
swagChar.animOffsets = [(FlxG.mouse.x - mouseOffset.x) * -1, (FlxG.mouse.y - mouseOffset.y) * -1];
swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, swagChar.animOffsets);
swagChar.animationOffsets.set(offsetAnimationDropdown.value.id, swagChar.animOffsets);
txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
}
if (FlxG.mouse.justReleased)
{
movingCharacter = false;
}
}
}
function addInfo(str:String, value:Dynamic)
{
var swagText:FlxText = new FlxText(10, 10 + (28 * txtGrp.length));
var swagText:FlxText = new FlxText(10, FlxG.height - 32);
swagText.setFormat(Paths.font("vcr.ttf"), 26, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
swagText.scrollFactor.set();
for (text in txtGrp.members)
{
text.y -= swagText.height;
}
txtGrp.add(swagText);
swagText.text = str + ": " + Std.string(value);
@ -345,14 +282,14 @@ class DebugBoundingState extends FlxState
{
if (FlxG.keys.justPressed.ONE)
{
var lv:DropDown = cast uiStuff.findComponent("swapper");
var lv:DropDown = offsetEditorDialog.findComponent("swapper", DropDown);
lv.selectedIndex = 0;
curView = SPRITESHEET;
}
if (FlxG.keys.justReleased.TWO)
{
var lv:DropDown = cast uiStuff.findComponent("swapper");
var lv:DropDown = offsetEditorDialog.findComponent("swapper", DropDown);
lv.selectedIndex = 1;
curView = ANIMATIONS;
if (swagChar != null)
@ -368,12 +305,14 @@ class DebugBoundingState extends FlxState
spriteSheetView.visible = true;
offsetView.visible = false;
offsetView.active = false;
offsetAnimationDropdown.visible = false;
case ANIMATIONS:
spriteSheetView.visible = false;
offsetView.visible = true;
offsetView.active = true;
offsetAnimationDropdown.visible = true;
offsetControls();
if (!haxeUIFocused) mouseOffsetMovement();
mouseOffsetMovement();
}
if (FlxG.keys.justPressed.H) hudCam.visible = !hudCam.visible;
@ -395,24 +334,36 @@ class DebugBoundingState extends FlxState
{
if (FlxG.keys.justPressed.RBRACKET || FlxG.keys.justPressed.E)
{
if (Std.parseInt(animDropDownMenu.selectedId) + 1 <= animDropDownMenu.length)
animDropDownMenu.selectedId = Std.string(Std.parseInt(animDropDownMenu.selectedId)
+ 1);
if (offsetAnimationDropdown.selectedIndex + 1 <= offsetAnimationDropdown.dataSource.size)
{
offsetAnimationDropdown.selectedIndex += 1;
}
else
animDropDownMenu.selectedId = Std.string(0);
playCharacterAnimation(animDropDownMenu.selectedId, true);
{
offsetAnimationDropdown.selectedIndex = 0;
}
trace(offsetAnimationDropdown.selectedIndex);
trace(offsetAnimationDropdown.dataSource.size);
trace(offsetAnimationDropdown.value);
trace(currentAnimationName);
playCharacterAnimation(currentAnimationName, true);
}
if (FlxG.keys.justPressed.LBRACKET || FlxG.keys.justPressed.Q)
{
if (Std.parseInt(animDropDownMenu.selectedId) - 1 >= 0) animDropDownMenu.selectedId = Std.string(Std.parseInt(animDropDownMenu.selectedId) - 1);
if (offsetAnimationDropdown.selectedIndex - 1 >= 0)
{
offsetAnimationDropdown.selectedIndex -= 1;
}
else
animDropDownMenu.selectedId = Std.string(animDropDownMenu.length - 1);
playCharacterAnimation(animDropDownMenu.selectedId, true);
{
offsetAnimationDropdown.selectedIndex = offsetAnimationDropdown.dataSource.size - 1;
}
playCharacterAnimation(currentAnimationName, true);
}
// Keyboards controls for general WASD "movement"
// modifies the animDropDownMenu so that it's properly updated and shit
// and then it's just played and updated from the animDropDownMenu callback, which is set in the loadAnimShit() function probabbly
// modifies the animDrooffsetAnimationDropdownpDownMenu so that it's properly updated and shit
// and then it's just played and updated from the offsetAnimationDropdown callback, which is set in the loadAnimShit() function probabbly
if (FlxG.keys.justPressed.W || FlxG.keys.justPressed.S || FlxG.keys.justPressed.D || FlxG.keys.justPressed.A)
{
var suffix:String = '';
@ -425,18 +376,19 @@ class DebugBoundingState extends FlxState
if (FlxG.keys.justPressed.A) targetLabel = 'singLEFT$suffix';
if (FlxG.keys.justPressed.D) targetLabel = 'singRIGHT$suffix';
if (targetLabel != animDropDownMenu.selectedLabel)
if (targetLabel != currentAnimationName)
{
offsetAnimationDropdown.value = {id: targetLabel, text: targetLabel};
// Play the new animation if the IDs are the different.
// Override the onion skin.
animDropDownMenu.selectedLabel = targetLabel;
playCharacterAnimation(animDropDownMenu.selectedId, true);
playCharacterAnimation(currentAnimationName, true);
}
else
{
// Replay the current animation if the IDs are the same.
// Don't override the onion skin.
playCharacterAnimation(animDropDownMenu.selectedId, false);
playCharacterAnimation(currentAnimationName, false);
}
}
@ -448,16 +400,20 @@ class DebugBoundingState extends FlxState
// Plays the idle animation
if (FlxG.keys.justPressed.SPACE)
{
animDropDownMenu.selectedLabel = 'idle';
playCharacterAnimation(animDropDownMenu.selectedId, true);
offsetAnimationDropdown.value = {id: 'idle', text: 'idle'};
playCharacterAnimation(currentAnimationName, true);
}
// Playback the animation
if (FlxG.keys.justPressed.ENTER) playCharacterAnimation(animDropDownMenu.selectedId, false);
if (FlxG.keys.justPressed.ENTER)
{
playCharacterAnimation(currentAnimationName, false);
}
if (FlxG.keys.justPressed.RIGHT || FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.UP || FlxG.keys.justPressed.DOWN)
{
var animName = animDropDownMenu.selectedLabel;
var animName = currentAnimationName;
var coolValues:Array<Float> = swagChar.animationOffsets.get(animName).copy();
var multiplier:Int = 5;
@ -471,10 +427,11 @@ class DebugBoundingState extends FlxState
else if (FlxG.keys.justPressed.UP) coolValues[1] += 1 * multiplier;
else if (FlxG.keys.justPressed.DOWN) coolValues[1] -= 1 * multiplier;
swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, coolValues);
swagChar.animationOffsets.set(currentAnimationName, coolValues);
swagChar.playAnimation(animName);
txtOffsetShit.text = 'Offset: ' + coolValues;
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
trace(animName);
}
@ -529,7 +486,7 @@ class DebugBoundingState extends FlxState
swagChar = CharacterDataParser.fetchCharacter(char);
swagChar.x = 100;
swagChar.y = 100;
// swagChar.debugMode = true;
swagChar.debug = true;
offsetView.add(swagChar);
if (swagChar == null || swagChar.frames == null)
@ -554,11 +511,25 @@ class DebugBoundingState extends FlxState
trace(swagChar.animationOffsets[i]);
}
animDropDownMenu.setData(FlxUIDropDownMenu.makeStrIdLabelArray(characterAnimNames, true));
animDropDownMenu.callback = function(str:String) {
playCharacterAnimation(str, true);
};
offsetAnimationDropdown.dataSource.clear();
for (charAnim in characterAnimNames)
{
trace('Adding ${charAnim} to HaxeUI dropdown');
offsetAnimationDropdown.dataSource.add({id: charAnim, text: charAnim});
}
offsetAnimationDropdown.selectedIndex = 0;
trace('Added ${offsetAnimationDropdown.dataSource.size} to HaxeUI dropdown');
offsetAnimationDropdown.onChange = function(event:UIEvent) {
trace('Selected animation ${event?.data?.id}');
playCharacterAnimation(event.data.id, true);
}
txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
dropDownSetup = true;
}
@ -575,11 +546,13 @@ class DebugBoundingState extends FlxState
onionSkinChar.alpha = 0.6;
}
var animName = characterAnimNames[Std.parseInt(str)];
// var animName = characterAnimNames[Std.parseInt(str)];
var animName = str;
swagChar.playAnimation(animName, true); // trace();
trace(swagChar.animationOffsets.get(animName));
txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
}
var _file:FileReference;

View file

@ -35,6 +35,7 @@ import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongData.NoteParamData;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageData;
@ -45,6 +46,7 @@ import funkin.input.TurboActionHandler;
import funkin.input.TurboButtonHandler;
import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
@ -282,6 +284,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0;
/**
* A map of the keys for every live input style.
*/
public static final LIVE_INPUT_KEYS:Map<ChartEditorLiveInputStyle, Array<FlxKey>> = [
NumberKeys => [
FIVE, SIX, SEVEN, EIGHT,
ONE, TWO, THREE, FOUR
],
WASDKeys => [
LEFT, DOWN, UP, RIGHT,
A, S, W, D
],
None => []
];
/**
* INSTANCE DATA
*/
@ -538,6 +555,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var noteKindToPlace:Null<String> = null;
/**
* The note params to use for notes being placed in the chart. Defaults to `[]`.
*/
var noteParamsToPlace:Array<NoteParamData> = [];
/**
* The event type to use for events being placed in the chart. Defaults to `''`.
*/
@ -1401,7 +1423,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_currentSongNoteStyle():String
{
if (currentSongMetadata.playData.noteStyle == null)
if (currentSongMetadata.playData.noteStyle == null
|| currentSongMetadata.playData.noteStyle == ''
|| currentSongMetadata.playData.noteStyle == 'item')
{
// Initialize to the default value if not set.
currentSongMetadata.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE;
@ -2436,7 +2460,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
gridGhostNote = new ChartEditorNoteSprite(this);
gridGhostNote.alpha = 0.6;
gridGhostNote.noteData = new SongNoteData(0, 0, 0, "");
gridGhostNote.noteData = new SongNoteData(0, 0, 0, "", []);
gridGhostNote.visible = false;
add(gridGhostNote);
gridGhostNote.zIndex = 11;
@ -3303,7 +3327,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
handleTestKeybinds();
handleHelpKeybinds();
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
handleQuickWatch();
#end
@ -3584,6 +3608,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// The note sprite handles animation playback and positioning.
noteSprite.noteData = noteData;
noteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
noteSprite.overrideStepTime = null;
noteSprite.overrideData = null;
@ -3607,6 +3632,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
holdNoteSprite.setHeightDirectly(noteLengthPixels);
holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
trace(holdNoteSprite.x + ', ' + holdNoteSprite.y + ', ' + holdNoteSprite.width + ', ' + holdNoteSprite.height);
@ -3669,9 +3696,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
holdNoteSprite.noteData = noteData;
holdNoteSprite.noteDirection = noteData.getDirection();
holdNoteSprite.setHeightDirectly(noteLengthPixels);
holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
displayedHoldNoteData.push(noteData);
@ -4569,7 +4597,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
gridGhostHoldNote.noteData = currentPlaceNoteData;
gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
}
else
@ -4726,7 +4754,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
else
{
// Create a note and place it in the chart.
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace);
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace,
ChartEditorState.cloneNoteParams(noteParamsToPlace));
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
@ -4885,12 +4914,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace);
var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace,
ChartEditorState.cloneNoteParams(noteParamsToPlace));
if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind)
if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind || noteParamsToPlace != noteData.params)
{
noteData.kind = noteKindToPlace;
noteData.params = noteParamsToPlace;
noteData.data = cursorColumn;
gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
gridGhostNote.playNoteAnimation();
}
noteData.time = cursorSnappedMs;
@ -5129,46 +5161,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handlePlayhead():Void
{
// Place notes at the playhead with the keyboard.
switch (currentLiveInputStyle)
for (note => key in LIVE_INPUT_KEYS[currentLiveInputStyle])
{
case ChartEditorLiveInputStyle.WASDKeys:
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.NumberKeys:
// Flipped because Dad is on the left but represents data 0-3.
if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.None:
// Do nothing.
if (FlxG.keys.checkStatus(key, JUST_PRESSED)) placeNoteAtPlayhead(note)
else if (FlxG.keys.checkStatus(key, JUST_RELEASED)) finishPlaceNoteAtPlayhead(note);
}
// Place events at playhead.
@ -5196,7 +5192,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (notesAtPos.length == 0 && !removeNoteInstead)
{
trace('Placing note. ${column}');
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace, ChartEditorState.cloneNoteParams(noteParamsToPlace));
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
currentLiveInputPlaceNoteData[column] = newNoteData;
}
@ -5282,6 +5278,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
ghostHold.visible = true;
ghostHold.alpha = 0.6;
ghostHold.setHeightDirectly(0);
ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
ghostHold.updateHoldNotePosition(renderedHoldNotes);
}
@ -5648,6 +5645,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0);
FlxG.watch.addQuick('noteKindToPlace', noteKindToPlace);
FlxG.watch.addQuick('noteParamsToPlace', noteParamsToPlace);
FlxG.watch.addQuick('eventKindToPlace', eventKindToPlace);
FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
@ -5701,13 +5699,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// TODO: Rework asset system so we can remove this jank.
switch (currentSongStage)
{
case 'mainStage':
case 'mainStage' | 'mainStageErect':
PlayStatePlaylist.campaignId = 'week1';
case 'spookyMansion':
case 'spookyMansion' | 'spookyMansionErect':
PlayStatePlaylist.campaignId = 'week2';
case 'phillyTrain':
case 'phillyTrain' | 'phillyTrainErect':
PlayStatePlaylist.campaignId = 'week3';
case 'limoRide':
case 'limoRide' | 'limoRideErect':
PlayStatePlaylist.campaignId = 'week4';
case 'mallXmas' | 'mallEvil':
PlayStatePlaylist.campaignId = 'week5';
@ -6511,6 +6509,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
return input;
}
public static function cloneNoteParams(paramsToClone:Array<NoteParamData>):Array<NoteParamData>
{
var params:Array<NoteParamData> = [];
for (param in paramsToClone)
{
params.push(param.clone());
}
return params;
}
}
/**

View file

@ -2,6 +2,7 @@ package funkin.ui.debug.charting.components;
import funkin.play.notes.Strumline;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection;
@ -15,6 +16,7 @@ import flixel.math.FlxMath;
* A sprite that can be used to display the trail of a hold note in a chart.
* Designed to be used and reused efficiently. Has no gameplay functionality.
*/
@:access(funkin.ui.debug.charting.ChartEditorState)
@:nullSafety
class ChartEditorHoldNoteSprite extends SustainTrail
{
@ -23,6 +25,22 @@ class ChartEditorHoldNoteSprite extends SustainTrail
*/
public var parentState:ChartEditorState;
@:isVar
public var noteStyle(get, set):Null<String>;
function get_noteStyle():Null<String>
{
return this.noteStyle ?? this.parentState.currentSongNoteStyle;
}
@:nullSafety(Off)
function set_noteStyle(value:Null<String>):Null<String>
{
this.noteStyle = value;
this.updateHoldNoteGraphic();
return value;
}
public function new(parent:ChartEditorState)
{
var noteStyle = NoteStyleRegistry.instance.fetchDefault();
@ -30,14 +48,52 @@ class ChartEditorHoldNoteSprite extends SustainTrail
super(0, 100, noteStyle);
this.parentState = parent;
}
@:nullSafety(Off)
function updateHoldNoteGraphic():Void
{
var bruhStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyle);
if (bruhStyle == null) bruhStyle = NoteStyleRegistry.instance.fetchDefault();
setupHoldNoteGraphic(bruhStyle);
}
override function setupHoldNoteGraphic(noteStyle:NoteStyle):Void
{
var graphicPath = noteStyle.getHoldNoteAssetPath();
if (graphicPath == null) return;
loadGraphic(graphicPath);
antialiasing = true;
this.isPixel = noteStyle.isHoldNotePixel();
if (isPixel)
{
endOffset = bottomClip = 1;
antialiasing = false;
}
else
{
endOffset = 0.5;
bottomClip = 0.9;
}
zoom = 1.0;
zoom *= noteStyle.fetchHoldNoteScale();
zoom *= 0.7;
zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
graphicHeight = sustainLength * 0.45; // sustainHeight
flipY = false;
alpha = 1.0;
updateColorTransform();
updateClipping();
setup();
}

View file

@ -7,7 +7,11 @@ import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFrame;
import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint;
import funkin.data.animation.AnimationData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.NoteDirection;
/**
* A sprite that can be used to display a note in a chart.
@ -36,7 +40,8 @@ class ChartEditorNoteSprite extends FlxSprite
/**
* The name of the note style currently in use.
*/
public var noteStyle(get, never):String;
@:isVar
public var noteStyle(get, set):Null<String>;
public var overrideStepTime(default, set):Null<Float> = null;
@ -66,72 +71,80 @@ class ChartEditorNoteSprite extends FlxSprite
this.parentState = parent;
var entries:Array<String> = NoteStyleRegistry.instance.listEntryIds();
if (noteFrameCollection == null)
{
initFrameCollection();
buildEmptyFrameCollection();
for (entry in entries)
{
addNoteStyleFrames(fetchNoteStyle(entry));
}
}
if (noteFrameCollection == null) throw 'ERROR: Could not initialize note sprite animations.';
this.frames = noteFrameCollection;
// Initialize all the animations, not just the one we're going to use immediately,
// so that later we can reuse the sprite without having to initialize more animations during scrolling.
this.animation.addByPrefix('tapLeftFunkin', 'purple instance');
this.animation.addByPrefix('tapDownFunkin', 'blue instance');
this.animation.addByPrefix('tapUpFunkin', 'green instance');
this.animation.addByPrefix('tapRightFunkin', 'red instance');
this.animation.addByPrefix('holdLeftFunkin', 'LeftHoldPiece');
this.animation.addByPrefix('holdDownFunkin', 'DownHoldPiece');
this.animation.addByPrefix('holdUpFunkin', 'UpHoldPiece');
this.animation.addByPrefix('holdRightFunkin', 'RightHoldPiece');
this.animation.addByPrefix('holdEndLeftFunkin', 'LeftHoldEnd');
this.animation.addByPrefix('holdEndDownFunkin', 'DownHoldEnd');
this.animation.addByPrefix('holdEndUpFunkin', 'UpHoldEnd');
this.animation.addByPrefix('holdEndRightFunkin', 'RightHoldEnd');
this.animation.addByPrefix('tapLeftPixel', 'pixel4');
this.animation.addByPrefix('tapDownPixel', 'pixel5');
this.animation.addByPrefix('tapUpPixel', 'pixel6');
this.animation.addByPrefix('tapRightPixel', 'pixel7');
for (entry in entries)
{
addNoteStyleAnimations(fetchNoteStyle(entry));
}
}
static var noteFrameCollection:Null<FlxFramesCollection> = null;
/**
* We load all the note frames once, then reuse them.
*/
static function initFrameCollection():Void
function fetchNoteStyle(noteStyleId:String):NoteStyle
{
buildEmptyFrameCollection();
if (noteFrameCollection == null) return;
var result = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (result != null) return result;
return NoteStyleRegistry.instance.fetchDefault();
}
// TODO: Automatically iterate over the list of note skins.
@:access(funkin.play.notes.notestyle.NoteStyle)
@:nullSafety(Off)
static function addNoteStyleFrames(noteStyle:NoteStyle):Void
{
var prefix:String = noteStyle.id.toTitleCase();
// Normal notes
var frameCollectionNormal:FlxAtlasFrames = Paths.getSparrowAtlas('NOTE_assets');
for (frame in frameCollectionNormal.frames)
var frameCollection:FlxAtlasFrames = Paths.getSparrowAtlas(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary());
if (frameCollection == null)
{
noteFrameCollection.pushFrame(frame);
trace('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}');
FlxG.log.error('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}');
return;
}
// Pixel notes
var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null);
if (graphicPixel == null) trace('ERROR: Could not load graphic: ' + Paths.image('weeb/pixelUI/arrows-pixels', 'week6'));
var frameCollectionPixel = FlxTileFrames.fromGraphic(graphicPixel, new FlxPoint(17, 17));
for (i in 0...frameCollectionPixel.frames.length)
for (frame in frameCollection.frames)
{
var frame:Null<FlxFrame> = frameCollectionPixel.frames[i];
if (frame == null) continue;
frame.name = 'pixel' + i;
noteFrameCollection.pushFrame(frame);
// cloning the frame because else
// we will fuck up the frame data used in game
var clonedFrame:FlxFrame = frame.copyTo();
clonedFrame.name = '$prefix${clonedFrame.name}';
noteFrameCollection.pushFrame(clonedFrame);
}
}
@:access(funkin.play.notes.notestyle.NoteStyle)
@:nullSafety(Off)
function addNoteStyleAnimations(noteStyle:NoteStyle):Void
{
var prefix:String = noteStyle.id.toTitleCase();
var suffix:String = noteStyle.id.toTitleCase();
var leftData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.LEFT);
this.animation.addByPrefix('tapLeft$suffix', '$prefix${leftData.prefix}', leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY);
var downData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.DOWN);
this.animation.addByPrefix('tapDown$suffix', '$prefix${downData.prefix}', downData.frameRate, downData.looped, downData.flipX, downData.flipY);
var upData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.UP);
this.animation.addByPrefix('tapUp$suffix', '$prefix${upData.prefix}', upData.frameRate, upData.looped, upData.flipX, upData.flipY);
var rightData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.RIGHT);
this.animation.addByPrefix('tapRight$suffix', '$prefix${rightData.prefix}', rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
}
@:nullSafety(Off)
static function buildEmptyFrameCollection():Void
{
@ -185,12 +198,24 @@ class ChartEditorNoteSprite extends FlxSprite
}
}
function get_noteStyle():String
function get_noteStyle():Null<String>
{
// Fall back to Funkin' if it's not a valid note style.
return if (NOTE_STYLES.contains(this.parentState.currentSongNoteStyle)) this.parentState.currentSongNoteStyle else 'funkin';
if (this.noteStyle == null)
{
var result = this.parentState.currentSongNoteStyle;
return result;
}
return this.noteStyle;
}
function set_noteStyle(value:Null<String>):Null<String>
{
this.noteStyle = value;
this.playNoteAnimation();
return value;
}
@:nullSafety(Off)
public function playNoteAnimation():Void
{
if (this.noteData == null) return;
@ -200,6 +225,7 @@ class ChartEditorNoteSprite extends FlxSprite
// Play the appropriate animation for the type, direction, and skin.
var dirName:String = overrideData != null ? SongNoteData.buildDirectionName(overrideData) : this.noteData.getDirectionName();
var noteStyleSuffix:String = this.noteStyle?.toTitleCase() ?? Constants.DEFAULT_NOTE_STYLE.toTitleCase();
var animationName:String = '${baseAnimationName}${dirName}${this.noteStyle.toTitleCase()}';
this.animation.play(animationName);
@ -209,12 +235,12 @@ class ChartEditorNoteSprite extends FlxSprite
switch (baseAnimationName)
{
case 'tap':
this.setGraphicSize(0, ChartEditorState.GRID_SIZE);
this.setGraphicSize(ChartEditorState.GRID_SIZE, 0);
this.updateHitbox();
}
this.updateHitbox();
// TODO: Make this an attribute of the note skin.
this.antialiasing = (this.parentState.currentSongNoteStyle != 'Pixel');
var bruhStyle:NoteStyle = fetchNoteStyle(this.noteStyle);
this.antialiasing = !bruhStyle._data?.assets?.note?.isPixel ?? true;
}
/**

View file

@ -190,8 +190,8 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
var numberStepper:NumberStepper = new NumberStepper();
numberStepper.id = field.name;
numberStepper.step = field.step ?? 1.0;
numberStepper.min = field.min ?? 0.0;
numberStepper.max = field.max ?? 10.0;
if (field.min != null) numberStepper.min = field.min;
if (field.min != null) numberStepper.max = field.max;
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
input = numberStepper;
case FLOAT:

View file

@ -2,8 +2,16 @@ package funkin.ui.debug.charting.toolboxes;
import haxe.ui.components.DropDown;
import haxe.ui.components.TextField;
import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper;
import haxe.ui.containers.Grid;
import haxe.ui.core.Component;
import haxe.ui.events.UIEvent;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.notes.notekind.NoteKind.NoteKindParam;
import funkin.play.notes.notekind.NoteKind.NoteKindParamType;
import funkin.data.song.SongData.NoteParamData;
/**
* The toolbox which allows modifying information like Note Kind.
@ -12,8 +20,22 @@ import funkin.ui.debug.charting.util.ChartEditorDropdowns;
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/note-data.xml"))
class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
{
// 100 is the height used in note-data.xml
static final DIALOG_HEIGHT:Int = 100;
// toolboxNotesGrid.height + 45
// this is what i found out by printing this.height and grid.height
// and then seeing that this.height is 100 and grid.height is 55
static final HEIGHT_OFFSET:Int = 45;
// minimizing creates a gray bar the bottom, which would obscure the components,
// which is why we use an extra offset of 20
static final MINIMIZE_FIX:Int = 20;
var toolboxNotesGrid:Grid;
var toolboxNotesNoteKind:DropDown;
var toolboxNotesCustomKind:TextField;
var toolboxNotesParams:Array<ToolboxNoteKindParam> = [];
var _initializing:Bool = true;
@ -54,12 +76,35 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
}
createNoteKindParams(noteKind);
if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
{
// Edit the note data of any selected notes.
for (note in chartEditorState.currentNoteSelection)
{
// Edit the note data of any selected notes.
note.kind = chartEditorState.noteKindToPlace;
note.params = ChartEditorState.cloneNoteParams(chartEditorState.noteParamsToPlace);
// update note sprites
for (noteSprite in chartEditorState.renderedNotes.members)
{
if (noteSprite.noteData == note)
{
noteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle;
break;
}
}
// update hold note sprites
for (holdNoteSprite in chartEditorState.renderedHoldNotes.members)
{
if (holdNoteSprite.noteData == note)
{
holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle;
break;
}
}
}
chartEditorState.saveDataDirty = true;
chartEditorState.noteDisplayDirty = true;
@ -94,6 +139,8 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
toolboxNotesNoteKind.value = ChartEditorDropdowns.lookupNoteKind(chartEditorState.noteKindToPlace);
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
createNoteKindParams(chartEditorState.noteKindToPlace);
}
function showCustom():Void
@ -108,8 +155,149 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
toolboxNotesCustomKind.hidden = true;
}
function createNoteKindParams(noteKind:Null<String>):Void
{
clearNoteKindParams();
var setParamsToPlace:Bool = false;
if (!_initializing)
{
for (note in chartEditorState.currentNoteSelection)
{
if (note.kind == chartEditorState.noteKindToPlace)
{
chartEditorState.noteParamsToPlace = ChartEditorState.cloneNoteParams(note.params);
setParamsToPlace = true;
break;
}
}
}
var noteKindParams:Array<NoteKindParam> = NoteKindManager.getParams(noteKind);
for (i in 0...noteKindParams.length)
{
var param:NoteKindParam = noteKindParams[i];
var paramLabel:Label = new Label();
paramLabel.value = param.description;
paramLabel.verticalAlign = "center";
paramLabel.horizontalAlign = "right";
var paramComponent:Component = null;
switch (param.type)
{
case NoteKindParamType.INT | NoteKindParamType.FLOAT:
var paramStepper:NumberStepper = new NumberStepper();
paramStepper.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? 0.0;
paramStepper.percentWidth = 100;
paramStepper.step = param.data?.step ?? 1.0;
// this check should be unnecessary but for some reason
// even when these are null it will set it to 0
if (param.data?.min != null)
{
paramStepper.min = param.data.min;
}
if (param.data?.max != null)
{
paramStepper.max = param.data.max;
}
if (param.data?.precision != null)
{
paramStepper.precision = param.data.precision;
}
paramComponent = paramStepper;
case NoteKindParamType.STRING:
var paramTextField:TextField = new TextField();
paramTextField.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? '';
paramTextField.percentWidth = 100;
paramComponent = paramTextField;
}
if (paramComponent == null)
{
continue;
}
paramComponent.onChange = function(event:UIEvent) {
chartEditorState.noteParamsToPlace[i].value = paramComponent.value;
for (note in chartEditorState.currentNoteSelection)
{
if (note.params.length != noteKindParams.length)
{
break;
}
if (note.params[i].name == param.name)
{
note.params[i].value = paramComponent.value;
}
}
}
addNoteKindParam(paramLabel, paramComponent);
}
if (!setParamsToPlace)
{
var noteParamData:Array<NoteParamData> = [];
for (i in 0...noteKindParams.length)
{
noteParamData.push(new NoteParamData(noteKindParams[i].name, toolboxNotesParams[i].component.value));
}
chartEditorState.noteParamsToPlace = noteParamData;
}
}
function addNoteKindParam(label:Label, component:Component):Void
{
toolboxNotesParams.push({label: label, component: component});
toolboxNotesGrid.addComponent(label);
toolboxNotesGrid.addComponent(component);
this.height = Math.max(DIALOG_HEIGHT, DIALOG_HEIGHT - 30 + toolboxNotesParams.length * 30);
}
function clearNoteKindParams():Void
{
for (param in toolboxNotesParams)
{
toolboxNotesGrid.removeComponent(param.component);
toolboxNotesGrid.removeComponent(param.label);
}
toolboxNotesParams = [];
this.height = DIALOG_HEIGHT;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// current dialog is minimized, dont change the height
if (this.minimized)
{
return;
}
var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, (toolboxNotesGrid?.height ?? 50.0) + HEIGHT_OFFSET)) + MINIMIZE_FIX;
if (this.height != heightToSet)
{
this.height = heightToSet;
}
}
public static function build(chartEditorState:ChartEditorState):ChartEditorNoteDataToolbox
{
return new ChartEditorNoteDataToolbox(chartEditorState);
}
}
typedef ToolboxNoteKindParam =
{
var label:Label;
var component:Component;
}

View file

@ -135,6 +135,14 @@ class ChartEditorDropdowns
var noteStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) continue;
// check if the note style has all necessary assets (strums, notes, holdNotes)
if (noteStyle._data?.assets?.noteStrumline == null
|| noteStyle._data?.assets?.note == null
|| noteStyle._data?.assets?.holdNote == null)
{
continue;
}
var value = {id: noteStyleId, text: noteStyle.getName()};
if (startingStyleId == noteStyleId) returnValue = value;
@ -146,7 +154,7 @@ class ChartEditorDropdowns
return returnValue;
}
static final NOTE_KINDS:Map<String, String> = [
public static final NOTE_KINDS:Map<String, String> = [
// Base
"" => "Default",
"~CUSTOM~" => "Custom",
@ -187,11 +195,11 @@ class ChartEditorDropdowns
{
dropDown.dataSource.clear();
var returnValue:DropDownEntry = lookupNoteKind('~CUSTOM');
var returnValue:DropDownEntry = lookupNoteKind('');
for (noteKindId in NOTE_KINDS.keys())
{
var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Default';
var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Unknown';
var value:DropDownEntry = {id: noteKindId, text: noteKind};
if (startingKindId == noteKindId) returnValue = value;
@ -208,7 +216,7 @@ class ChartEditorDropdowns
{
if (noteKindId == null) return lookupNoteKind('');
if (!NOTE_KINDS.exists(noteKindId)) return {id: '~CUSTOM~', text: 'Custom'};
return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Default'};
return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Unknown'};
}
/**

View file

@ -37,6 +37,7 @@ class AlbumRoll extends FlxSpriteGroup
}
var newAlbumArt:FlxAtlasSprite;
var albumTitle:FunkinSprite;
var difficultyStars:DifficultyStars;
var _exitMovers:Null<FreeplayState.ExitMoverData>;
@ -59,24 +60,27 @@ class AlbumRoll extends FlxSpriteGroup
{
super();
newAlbumArt = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum"));
newAlbumArt = new FlxAtlasSprite(640, 350, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum"));
newAlbumArt.visible = false;
newAlbumArt.onAnimationFinish.add(onAlbumFinish);
newAlbumArt.onAnimationComplete.add(onAlbumFinish);
add(newAlbumArt);
difficultyStars = new DifficultyStars(140, 39);
difficultyStars.visible = false;
add(difficultyStars);
buildAlbumTitle("freeplay/albumRoll/volume1-text");
albumTitle.visible = false;
}
function onAlbumFinish(animName:String):Void
{
// Play the idle animation for the current album.
newAlbumArt.playAnimation(animNames.get('$albumId-idle'), false, false, true);
// End on the last frame and don't continue until playAnimation is called again.
// newAlbumArt.anim.pause();
if (animName != "idle")
{
// newAlbumArt.playAnimation('idle', true);
}
}
/**
@ -104,6 +108,12 @@ class AlbumRoll extends FlxSpriteGroup
return;
};
// Update the album art.
var albumGraphic = Paths.image(albumData.getAlbumArtAssetKey());
newAlbumArt.replaceFrameGraphic(0, albumGraphic);
buildAlbumTitle(albumData.getAlbumTitleAssetKey());
applyExitMovers();
refresh();
@ -146,19 +156,57 @@ class AlbumRoll extends FlxSpriteGroup
*/
public function playIntro():Void
{
albumTitle.visible = false;
newAlbumArt.visible = true;
newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false);
newAlbumArt.playAnimation('intro', true);
difficultyStars.visible = false;
new FlxTimer().start(0.75, function(_) {
// showTitle();
showTitle();
showStars();
albumTitle.animation.play('switch');
});
}
public function skipIntro():Void
{
newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false);
// Weird workaround
newAlbumArt.playAnimation('switch', true);
albumTitle.animation.play('switch');
}
public function showTitle():Void
{
albumTitle.visible = true;
}
public function buildAlbumTitle(assetKey:String):Void
{
if (albumTitle != null)
{
remove(albumTitle);
albumTitle = null;
}
albumTitle = FunkinSprite.createSparrow(925, 500, assetKey);
albumTitle.visible = albumTitle.frames != null && newAlbumArt.visible;
albumTitle.animation.addByPrefix('idle', 'idle0', 24, true);
albumTitle.animation.addByPrefix('switch', 'switch0', 24, false);
add(albumTitle);
albumTitle.animation.finishCallback = (function(name) {
if (name == 'switch') albumTitle.animation.play('idle');
});
albumTitle.animation.play('idle');
albumTitle.zIndex = 1000;
if (_exitMovers != null) _exitMovers.set([albumTitle],
{
x: FlxG.width,
speed: 0.4,
wait: 0
});
}
public function setDifficultyStars(?difficulty:Int):Void

View file

@ -15,7 +15,7 @@ class FreeplayDJ extends FlxAtlasSprite
{
// Represents the sprite's current status.
// Without state machines I would have driven myself crazy years ago.
public var currentState:DJBoyfriendState = Intro;
public var currentState:FreeplayDJState = Intro;
// A callback activated when the intro animation finishes.
public var onIntroDone:FlxSignal = new FlxSignal();
@ -43,7 +43,7 @@ class FreeplayDJ extends FlxAtlasSprite
super(x, y, playableCharData.getAtlasPath());
anim.callback = function(name, number) {
onAnimationFrame.add(function(name, number) {
if (name == playableCharData.getAnimationPrefix('cartoon'))
{
if (number == playableCharData.getCartoonSoundClickFrame())
@ -55,12 +55,12 @@ class FreeplayDJ extends FlxAtlasSprite
runTvLogic();
}
}
};
});
FlxG.debugger.track(this);
FlxG.console.registerObject("dj", this);
anim.onComplete = onFinishAnim;
onAnimationComplete.add(onFinishAnim);
FlxG.console.registerFunction("freeplayCartoon", function() {
currentState = Cartoon;
@ -96,10 +96,10 @@ class FreeplayDJ extends FlxAtlasSprite
var animPrefix = playableCharData.getAnimationPrefix('idle');
if (getCurrentAnimation() != animPrefix)
{
playFlashAnimation(animPrefix, true);
playFlashAnimation(animPrefix, true, false, true);
}
if (getCurrentAnimation() == animPrefix && this.isLoopFinished())
if (getCurrentAnimation() == animPrefix && this.isLoopComplete())
{
if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg)
{
@ -111,18 +111,69 @@ class FreeplayDJ extends FlxAtlasSprite
}
}
timeIdling += elapsed;
case NewUnlock:
var animPrefix = playableCharData.getAnimationPrefix('newUnlock');
if (!hasAnimation(animPrefix))
{
currentState = Idle;
}
if (getCurrentAnimation() != animPrefix)
{
playFlashAnimation(animPrefix, true, false, true);
}
case Confirm:
var animPrefix = playableCharData.getAnimationPrefix('confirm');
if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false);
timeIdling = 0;
case FistPumpIntro:
var animPrefix = playableCharData.getAnimationPrefix('fistPump');
if (getCurrentAnimation() != animPrefix) playFlashAnimation('Boyfriend DJ fist pump', false);
if (getCurrentAnimation() == animPrefix && anim.curFrame >= 4)
var animPrefixA = playableCharData.getAnimationPrefix('fistPump');
var animPrefixB = playableCharData.getAnimationPrefix('loss');
if (getCurrentAnimation() == animPrefixA)
{
anim.play("Boyfriend DJ fist pump", true, false, 0);
var endFrame = playableCharData.getFistPumpIntroEndFrame();
if (endFrame > -1 && anim.curFrame >= endFrame)
{
playFlashAnimation(animPrefixA, true, false, false, playableCharData.getFistPumpIntroStartFrame());
}
}
else if (getCurrentAnimation() == animPrefixB)
{
var endFrame = playableCharData.getFistPumpIntroBadEndFrame();
if (endFrame > -1 && anim.curFrame >= endFrame)
{
playFlashAnimation(animPrefixB, true, false, false, playableCharData.getFistPumpIntroBadStartFrame());
}
}
else
{
FlxG.log.warn("Unrecognized animation in FistPumpIntro: " + getCurrentAnimation());
}
case FistPump:
var animPrefixA = playableCharData.getAnimationPrefix('fistPump');
var animPrefixB = playableCharData.getAnimationPrefix('loss');
if (getCurrentAnimation() == animPrefixA)
{
var endFrame = playableCharData.getFistPumpLoopEndFrame();
if (endFrame > -1 && anim.curFrame >= endFrame)
{
playFlashAnimation(animPrefixA, true, false, false, playableCharData.getFistPumpLoopStartFrame());
}
}
else if (getCurrentAnimation() == animPrefixB)
{
var endFrame = playableCharData.getFistPumpLoopBadEndFrame();
if (endFrame > -1 && anim.curFrame >= endFrame)
{
playFlashAnimation(animPrefixB, true, false, false, playableCharData.getFistPumpLoopBadStartFrame());
}
}
else
{
FlxG.log.warn("Unrecognized animation in FistPump: " + getCurrentAnimation());
}
case IdleEasterEgg:
var animPrefix = playableCharData.getAnimationPrefix('idleEasterEgg');
@ -135,9 +186,12 @@ class FreeplayDJ extends FlxAtlasSprite
timeIdling = 0;
case Cartoon:
var animPrefix = playableCharData.getAnimationPrefix('cartoon');
if (animPrefix == null) {
if (animPrefix == null)
{
currentState = IdleEasterEgg;
} else {
}
else
{
if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true);
timeIdling = 0;
}
@ -145,6 +199,7 @@ class FreeplayDJ extends FlxAtlasSprite
// I shit myself.
}
#if FEATURE_DEBUG_FUNCTIONS
if (FlxG.keys.pressed.CONTROL)
{
if (FlxG.keys.justPressed.LEFT)
@ -167,20 +222,28 @@ class FreeplayDJ extends FlxAtlasSprite
this.offsetY += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
}
if (FlxG.keys.justPressed.SPACE)
if (FlxG.keys.justPressed.C)
{
currentState = (currentState == Idle ? Cartoon : Idle);
}
}
#end
}
function onFinishAnim():Void
function onFinishAnim(name:String):Void
{
var name = anim.curSymbol.name;
// var name = anim.curSymbol.name;
if (name == playableCharData.getAnimationPrefix('intro'))
{
currentState = Idle;
if (PlayerRegistry.instance.hasNewCharacter())
{
currentState = NewUnlock;
}
else
{
currentState = Idle;
}
onIntroDone.dispatch();
}
else if (name == playableCharData.getAnimationPrefix('idle'))
@ -220,9 +283,17 @@ class FreeplayDJ extends FlxAtlasSprite
// runTvLogic();
}
trace('Replay idle: ${frame}');
anim.play(playableCharData.getAnimationPrefix('cartoon'), true, false, frame);
playFlashAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, frame);
// trace('Finished confirm');
}
else if (name == playableCharData.getAnimationPrefix('newUnlock'))
{
// Animation should loop.
}
else if (name == playableCharData.getAnimationPrefix('charSelect'))
{
onCharSelectComplete();
}
else
{
trace('Finished ${name}');
@ -235,6 +306,15 @@ class FreeplayDJ extends FlxAtlasSprite
seenIdleEasterEgg = false;
}
/**
* Dynamic function, it's actually a variable you can reassign!
* `dj.onCharSelectComplete = function() {};`
*/
public dynamic function onCharSelectComplete():Void
{
trace('onCharSelectComplete()');
}
var offsetX:Float = 0.0;
var offsetY:Float = 0.0;
@ -266,7 +346,7 @@ class FreeplayDJ extends FlxAtlasSprite
function loadCartoon()
{
cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() {
anim.play("Boyfriend DJ watchin tv OG", true, false, 60);
playFlashAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, 60);
});
// Fade out music to 40% volume over 1 second.
@ -296,21 +376,48 @@ class FreeplayDJ extends FlxAtlasSprite
currentState = Confirm;
}
public function fistPump():Void
public function toCharSelect():Void
{
if (hasAnimation('charSelect'))
{
currentState = CharSelect;
var animPrefix = playableCharData.getAnimationPrefix('charSelect');
playFlashAnimation(animPrefix, true, false, false, 0);
}
else
{
currentState = Confirm;
// Call this immediately; otherwise, we get locked out of Character Select.
onCharSelectComplete();
}
}
public function fistPumpIntro():Void
{
currentState = FistPumpIntro;
var animPrefix = playableCharData.getAnimationPrefix('fistPump');
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpIntroStartFrame());
}
public function pumpFist():Void
public function fistPump():Void
{
currentState = FistPump;
anim.play("Boyfriend DJ fist pump", true, false, 4);
var animPrefix = playableCharData.getAnimationPrefix('fistPump');
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpLoopStartFrame());
}
public function pumpFistBad():Void
public function fistPumpLossIntro():Void
{
currentState = FistPumpIntro;
var animPrefix = playableCharData.getAnimationPrefix('loss');
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpIntroBadStartFrame());
}
public function fistPumpLoss():Void
{
currentState = FistPump;
anim.play("Boyfriend DJ loss reaction 1", true, false, 4);
var animPrefix = playableCharData.getAnimationPrefix('loss');
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpLoopBadStartFrame());
}
override public function getCurrentAnimation():String
@ -319,9 +426,9 @@ class FreeplayDJ extends FlxAtlasSprite
return this.anim.curSymbol.name;
}
public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void
public function playFlashAnimation(id:String, Force:Bool = false, Reverse:Bool = false, Loop:Bool = false, Frame:Int = 0):Void
{
anim.play(id, Force, Reverse, Frame);
playAnimation(id, Force, Reverse, Loop, Frame);
applyAnimOffset();
}
@ -361,13 +468,53 @@ class FreeplayDJ extends FlxAtlasSprite
}
}
enum DJBoyfriendState
enum FreeplayDJState
{
/**
* Character enters the frame and transitions to Idle.
*/
Intro;
/**
* Character loops in idle.
*/
Idle;
Confirm;
FistPumpIntro;
FistPump;
/**
* Plays an easter egg animation after a period in Idle, then reverts to Idle.
*/
IdleEasterEgg;
/**
* Plays an elaborate easter egg animation. Does not revert until another animation is triggered.
*/
Cartoon;
/**
* Player has selected a song.
*/
Confirm;
/**
* Character preps to play the fist pump animation; plays after the Results screen.
* The actual frame label that gets played may vary based on the player's success.
*/
FistPumpIntro;
/**
* Character plays the fist pump animation.
* The actual frame label that gets played may vary based on the player's success.
*/
FistPump;
/**
* Plays an animation to indicate that the player has a new unlock in Character Select.
* Overrides all idle animations as well as the fist pump. Only Confirm and CharSelect will override this.
*/
NewUnlock;
/**
* Plays an animation to transition to the Character Select screen.
*/
CharSelect;
}

View file

@ -1,7 +1,6 @@
package funkin.ui.freeplay;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.ui.FlxInputText;
import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.group.FlxGroup;
@ -307,14 +306,14 @@ class FreeplayState extends MusicBeatSubState
stickerSubState.degenStickers();
}
#if discord_rpc
#if FEATURE_DISCORD_RPC
// Updating Discord Rich Presence
DiscordClient.changePresence('In the Menus', null);
#end
var isDebug:Bool = false;
#if debug
#if FEATURE_DEBUG_FUNCTIONS
isDebug = true;
#end
@ -354,7 +353,7 @@ class FreeplayState extends MusicBeatSubState
// Only display songs which actually have available difficulties for the current character.
var displayedVariations = song.getVariationsByCharacter(currentCharacter);
trace('Displayed Variations (${songId}): $displayedVariations');
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
var availableDifficultiesForSong:Array<String> = song.listSuffixedDifficulties(displayedVariations, false, false);
trace('Available Difficulties: $availableDifficultiesForSong');
if (availableDifficultiesForSong.length == 0) continue;
@ -645,8 +644,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.3
});
var diffSelLeft:DifficultySelector = new DifficultySelector(20, grpDifficulties.y - 10, false, controls);
var diffSelRight:DifficultySelector = new DifficultySelector(325, grpDifficulties.y - 10, true, controls);
var diffSelLeft:DifficultySelector = new DifficultySelector(this, 20, grpDifficulties.y - 10, false, controls);
var diffSelRight:DifficultySelector = new DifficultySelector(this, 325, grpDifficulties.y - 10, true, controls);
diffSelLeft.visible = false;
diffSelRight.visible = false;
add(diffSelLeft);
@ -884,7 +883,7 @@ class FreeplayState extends MusicBeatSubState
return str.songName.toLowerCase().startsWith(songFilter.filterData ?? '');
});
case ALL:
// no filter!
// no filter!
case FAVORITE:
songsToFilter = songsToFilter.filter(str -> {
if (str == null) return true; // Random
@ -914,7 +913,15 @@ class FreeplayState extends MusicBeatSubState
changeSelection();
changeDiff();
if (dj != null) dj.fistPump();
if (fromResultsParams?.newRank == SHIT)
{
if (dj != null) dj.fistPumpLossIntro();
}
else
{
if (dj != null) dj.fistPumpIntro();
}
// rankCamera.fade(FlxColor.BLACK, 0.5, true);
rankCamera.fade(0xFF000000, 0.5, true, null, true);
if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
@ -1096,11 +1103,11 @@ class FreeplayState extends MusicBeatSubState
if (fromResultsParams?.newRank == SHIT)
{
if (dj != null) dj.pumpFistBad();
if (dj != null) dj.fistPumpLoss();
}
else
{
if (dj != null) dj.pumpFist();
if (dj != null) dj.fistPump();
}
rankCamera.zoom = 0.8;
@ -1132,7 +1139,7 @@ class FreeplayState extends MusicBeatSubState
// NOW we can interact with the menu
busy = false;
grpCapsules.members[curSelected].sparkle.alpha = 0.7;
capsule.sparkle.alpha = 0.7;
playCurSongPreview(capsule);
}, null);
@ -1203,7 +1210,7 @@ class FreeplayState extends MusicBeatSubState
/**
* If true, disable interaction with the interface.
*/
var busy:Bool = false;
public var busy:Bool = false;
var originalPos:FlxPoint = new FlxPoint();
@ -1211,7 +1218,7 @@ class FreeplayState extends MusicBeatSubState
{
super.update(elapsed);
#if debug
#if FEATURE_DEBUG_FUNCTIONS
if (FlxG.keys.justPressed.T)
{
rankAnimStart(fromResultsParams ??
@ -1246,7 +1253,32 @@ class FreeplayState extends MusicBeatSubState
if (controls.FREEPLAY_CHAR_SELECT && !busy)
{
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
// Check if we have ACCESS to character select!
trace('Is Pico unlocked? ${PlayerRegistry.instance.fetchEntry('pico')?.isUnlocked()}');
trace('Number of characters: ${PlayerRegistry.instance.countUnlockedCharacters()}');
if (PlayerRegistry.instance.countUnlockedCharacters() > 1)
{
if (dj != null)
{
busy = true;
// Transition to character select after animation
dj.onCharSelectComplete = function() {
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
}
dj.toCharSelect();
}
else
{
// Transition to character select immediately
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
}
}
else
{
trace('Not enough characters unlocked to open character select!');
FunkinSound.playOnce(Paths.sound('cancelMenu'));
}
}
if (controls.FREEPLAY_FAVORITE && !busy)
@ -1339,6 +1371,8 @@ class FreeplayState extends MusicBeatSubState
}
handleInputs(elapsed);
if (dj != null) FlxG.watch.addQuick('dj-anim', dj.getCurrentAnimation());
}
function handleInputs(elapsed:Float):Void
@ -1496,7 +1530,7 @@ class FreeplayState extends MusicBeatSubState
generateSongList(currentFilter, true);
}
if (controls.BACK)
if (controls.BACK && !busy)
{
busy = true;
FlxTween.globalManager.clear();
@ -1539,7 +1573,7 @@ class FreeplayState extends MusicBeatSubState
var moveDataX = funnyMoveShit.x ?? spr.x;
var moveDataY = funnyMoveShit.y ?? spr.y;
var moveDataSpeed = funnyMoveShit.speed ?? 0.2;
var moveDataWait = funnyMoveShit.wait ?? 0;
var moveDataWait = funnyMoveShit.wait ?? 0.0;
FlxTween.tween(spr, {x: moveDataX, y: moveDataY}, moveDataSpeed, {ease: FlxEase.expoIn});
@ -1686,6 +1720,9 @@ class FreeplayState extends MusicBeatSubState
songCapsule.init(null, null, null);
}
}
// Reset the song preview in case we changed variations (normal->erect etc)
playCurSongPreview();
}
// Set the album graphic and play the animation if relevant.
@ -1782,7 +1819,7 @@ class FreeplayState extends MusicBeatSubState
var targetInstId:String = baseInstrumentalId;
// TODO: Make this a UI element.
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL)
{
targetInstId = altInstrumentalIds[0];
@ -1804,7 +1841,7 @@ class FreeplayState extends MusicBeatSubState
confirmGlow.visible = true;
confirmGlow2.visible = true;
backingTextYeah.anim.play("BF back card confirm raw", false, false, 0);
backingTextYeah.playAnimation("BF back card confirm raw", false, false, false, 0);
confirmGlow2.alpha = 0;
confirmGlow.alpha = 0;
@ -1843,7 +1880,7 @@ class FreeplayState extends MusicBeatSubState
practiceMode: false,
minimalMode: false,
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
botPlayMode: FlxG.keys.pressed.SHIFT,
#else
botPlayMode: false,
@ -1924,8 +1961,10 @@ class FreeplayState extends MusicBeatSubState
}
}
public function playCurSongPreview(daSongCapsule:SongMenuItem):Void
public function playCurSongPreview(?daSongCapsule:SongMenuItem):Void
{
if (daSongCapsule == null) daSongCapsule = grpCapsules.members[curSelected];
if (curSelected == 0)
{
FunkinSound.playMusic('freeplayRandom',
@ -1950,7 +1989,7 @@ class FreeplayState extends MusicBeatSubState
var instSuffix:String = baseInstrumentalId;
// TODO: Make this a UI element.
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL)
{
instSuffix = altInstrumentalIds[0];
@ -2008,10 +2047,13 @@ class DifficultySelector extends FlxSprite
var controls:Controls;
var whiteShader:PureColor;
public function new(x:Float, y:Float, flipped:Bool, controls:Controls)
var parent:FreeplayState;
public function new(parent:FreeplayState, x:Float, y:Float, flipped:Bool, controls:Controls)
{
super(x, y);
this.parent = parent;
this.controls = controls;
frames = Paths.getSparrowAtlas('freeplay/freeplaySelector');
@ -2027,8 +2069,8 @@ class DifficultySelector extends FlxSprite
override function update(elapsed:Float):Void
{
if (flipX && controls.UI_RIGHT_P) moveShitDown();
if (!flipX && controls.UI_LEFT_P) moveShitDown();
if (flipX && controls.UI_RIGHT_P && !parent.busy) moveShitDown();
if (!flipX && controls.UI_LEFT_P && !parent.busy) moveShitDown();
super.update(elapsed);
}
@ -2158,7 +2200,13 @@ class FreeplaySongData
function updateValues(variations:Array<String>):Void
{
this.songDifficulties = song.listDifficulties(null, variations, false, false);
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
if (!this.songDifficulties.contains(currentDifficulty))
{
currentDifficulty = Constants.DEFAULT_DIFFICULTY;
// This method gets called again by the setter-method
// or the difficulty didn't change, so there's no need to continue.
return;
}
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, null, variations);
if (songDifficulty == null) return;
@ -2219,15 +2267,26 @@ class DifficultySprite extends FlxSprite
difficultyId = diffId;
if (Assets.exists(Paths.file('images/freeplay/freeplay${diffId}.xml')))
var assetDiffId:String = diffId;
while (!Assets.exists(Paths.image('freeplay/freeplay${assetDiffId}')))
{
this.frames = Paths.getSparrowAtlas('freeplay/freeplay${diffId}');
// Remove the last suffix of the difficulty id until we find an asset or there are no more suffixes.
var assetDiffIdParts:Array<String> = assetDiffId.split('-');
assetDiffIdParts.pop();
if (assetDiffIdParts.length == 0) break;
assetDiffId = assetDiffIdParts.join('-');
}
// Check for an XML to use an animation instead of an image.
if (Assets.exists(Paths.file('images/freeplay/freeplay${assetDiffId}.xml')))
{
this.frames = Paths.getSparrowAtlas('freeplay/freeplay${assetDiffId}');
this.animation.addByPrefix('idle', 'idle0', 24, true);
if (Preferences.flashingLights) this.animation.play('idle');
}
else
{
this.loadGraphic(Paths.image('freeplay/freeplay' + diffId));
this.loadGraphic(Paths.image('freeplay/freeplay' + assetDiffId));
}
}
}

View file

@ -88,6 +88,11 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
return _data.freeplayDJ.getFreeplayDJText(index);
}
public function getCharSelectData():PlayerCharSelectData
{
return _data.charSelect;
}
/**
* @param rank Which rank to get info for
* @return An array of animations. For example, BF Great has two animations, one for BF and one for GF

View file

@ -27,7 +27,7 @@ import funkin.ui.title.TitleState;
import funkin.ui.story.StoryMenuState;
import funkin.ui.Prompt;
import funkin.util.WindowUtil;
#if discord_rpc
#if FEATURE_DISCORD_RPC
import Discord.DiscordClient;
#end
#if newgrounds
@ -54,7 +54,7 @@ class MainMenuState extends MusicBeatState
override function create():Void
{
#if discord_rpc
#if FEATURE_DISCORD_RPC
// Updating Discord Rich Presence
DiscordClient.changePresence("In the Menus", null);
#end
@ -98,14 +98,7 @@ class MainMenuState extends MusicBeatState
add(menuItems);
menuItems.onChange.add(onMenuItemChange);
menuItems.onAcceptPress.add(function(_) {
if (_.name == 'freeplay')
{
magenta.visible = true;
}
else
{
FlxFlicker.flicker(magenta, 1.1, 0.15, false, true);
}
FlxFlicker.flicker(magenta, 1.1, 0.15, false, true);
});
menuItems.enabled = true; // can move on intro
@ -117,13 +110,7 @@ class MainMenuState extends MusicBeatState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
openSubState(new FreeplayState(
{
#if debug
// If SHIFT is held, toggle the selected character, else use the remembered character
character: (FlxG.keys.pressed.SHIFT) ? (FreeplayState.rememberedCharacterId == Constants.DEFAULT_CHARACTER ? 'pico' : 'bf') : null,
#end
}));
openSubState(new FreeplayState());
});
#if CAN_OPEN_LINKS
@ -347,7 +334,7 @@ class MainMenuState extends MusicBeatState
}
}
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
// Open the debug menu, defaults to ` / ~
if (controls.DEBUG_MENU)
{
@ -358,6 +345,7 @@ class MainMenuState extends MusicBeatState
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.W)
{
FunkinSound.playOnce(Paths.sound('confirmMenu'));
// Give the user a score of 1 point on Weekend 1 story mode.
// This makes the level count as cleared and displays the songs in Freeplay.
funkin.save.Save.instance.setLevelScore('weekend1', 'easy',
@ -378,6 +366,29 @@ class MainMenuState extends MusicBeatState
});
}
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.L)
{
FunkinSound.playOnce(Paths.sound('confirmMenu'));
// Give the user a score of 0 points on Weekend 1 story mode.
// This makes the level count as uncleared and no longer displays the songs in Freeplay.
funkin.save.Save.instance.setLevelScore('weekend1', 'easy',
{
score: 1,
tallies:
{
sick: 0,
good: 0,
bad: 0,
shit: 0,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 0,
totalNotes: 0,
}
});
}
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.R)
{
// Give the user a hypothetical overridden score,

View file

@ -0,0 +1,10 @@
package funkin.ui.options;
// Add enums for use with `EnumPreferenceItem` here!
/* Example:
class MyOptionEnum
{
public static inline var YuhUh = "true"; // "true" is the value's ID
public static inline var NuhUh = "false";
}
*/

View file

@ -8,6 +8,11 @@ import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.options.OptionsState.Page;
import funkin.graphics.FunkinCamera;
import funkin.ui.TextMenuList.TextMenuItem;
import funkin.audio.FunkinSound;
import funkin.ui.options.MenuItemEnums;
import funkin.ui.options.items.CheckboxPreferenceItem;
import funkin.ui.options.items.NumberPreferenceItem;
import funkin.ui.options.items.EnumPreferenceItem;
class PreferencesMenu extends Page
{
@ -69,11 +74,51 @@ class PreferencesMenu extends Page
}, Preferences.autoPause);
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// Indent the selected item.
items.forEach(function(daItem:TextMenuItem) {
var thyOffset:Int = 0;
// Initializing thy text width (if thou text present)
var thyTextWidth:Int = 0;
if (Std.isOfType(daItem, EnumPreferenceItem)) thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth();
else if (Std.isOfType(daItem, NumberPreferenceItem)) thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth();
if (thyTextWidth != 0)
{
// Magic number because of the weird offset thats being added by default
thyOffset += thyTextWidth - 75;
}
if (items.selectedItem == daItem)
{
thyOffset += 150;
}
else
{
thyOffset += 120;
}
daItem.x = thyOffset;
});
}
// - Preference item creation methods -
// Should be moved into a separate PreferenceItems class but you can't access PreferencesMenu.items and PreferencesMenu.preferenceItems from outside.
/**
* Creates a pref item that works with booleans
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
*/
function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
{
var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
items.createItem(0, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
var value = !checkbox.currentValue;
onChange(value);
checkbox.currentValue = value;
@ -82,62 +127,54 @@ class PreferencesMenu extends Page
preferenceItems.add(checkbox);
}
override function update(elapsed:Float)
/**
* Creates a pref item that works with general numbers
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed value looks
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
* @param min Minimum value (example: 0)
* @param max Maximum value (example: 10)
* @param step The value to increment/decrement by (default = 0.1)
* @param precision Rounds decimals up to a `precision` amount of digits (ex: 4 -> 0.1234, 2 -> 0.12)
*/
function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Int, min:Int, max:Int,
step:Float = 0.1, precision:Int):Void
{
super.update(elapsed);
var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, step, precision, onChange, valueFormatter);
items.addItem(prefName, item);
preferenceItems.add(item.lefthandText);
}
// Indent the selected item.
// TODO: Only do this on menu change?
items.forEach(function(daItem:TextMenuItem) {
if (items.selectedItem == daItem) daItem.x = 150;
else
daItem.x = 120;
});
}
}
class CheckboxPreferenceItem extends FlxSprite
{
public var currentValue(default, set):Bool;
public function new(x:Float, y:Float, defaultValue:Bool = false)
{
super(x, y);
frames = Paths.getSparrowAtlas('checkboxThingie');
animation.addByPrefix('static', 'Check Box unselected', 24, false);
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
this.currentValue = defaultValue;
}
override function update(elapsed:Float)
{
super.update(elapsed);
switch (animation.curAnim.name)
{
case 'static':
offset.set();
case 'checked':
offset.set(17, 70);
}
}
function set_currentValue(value:Bool):Bool
{
if (value)
{
animation.play('checked', true);
}
else
{
animation.play('static');
}
return currentValue = value;
/**
* Creates a pref item that works with number percentages
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
* @param min Minimum value (default = 0)
* @param max Maximum value (default = 100)
*/
function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):Void
{
var newCallback = function(value:Float) {
onChange(Std.int(value));
};
var formatter = function(value:Float) {
return '${value}%';
};
var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, 10, 0, newCallback, formatter);
items.addItem(prefName, item);
preferenceItems.add(item.lefthandText);
}
/**
* Creates a pref item that works with enums
* @param values Maps enum values to display strings _(ex: `NoteHitSoundType.PingPong => "Ping pong"`)_
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
*/
function createPrefItemEnum(prefName:String, prefDesc:String, values:Map<String, String>, onChange:String->Void, defaultValue:String):Void
{
var item = new EnumPreferenceItem(0, (120 * items.length) + 30, prefName, values, defaultValue, onChange);
items.addItem(prefName, item);
preferenceItems.add(item.lefthandText);
}
}

View file

@ -0,0 +1,49 @@
package funkin.ui.options.items;
import flixel.FlxSprite.FlxSprite;
class CheckboxPreferenceItem extends FlxSprite
{
public var currentValue(default, set):Bool;
public function new(x:Float, y:Float, defaultValue:Bool = false)
{
super(x, y);
frames = Paths.getSparrowAtlas('checkboxThingie');
animation.addByPrefix('static', 'Check Box unselected', 24, false);
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
this.currentValue = defaultValue;
}
override function update(elapsed:Float)
{
super.update(elapsed);
switch (animation.curAnim.name)
{
case 'static':
offset.set();
case 'checked':
offset.set(17, 70);
}
}
function set_currentValue(value:Bool):Bool
{
if (value)
{
animation.play('checked', true);
}
else
{
animation.play('static');
}
return currentValue = value;
}
}

View file

@ -0,0 +1,84 @@
package funkin.ui.options.items;
import funkin.ui.TextMenuList;
import funkin.ui.AtlasText;
import funkin.input.Controls;
import funkin.ui.options.MenuItemEnums;
import haxe.EnumTools;
/**
* Preference item that allows the player to pick a value from an enum (list of values)
*/
class EnumPreferenceItem extends TextMenuItem
{
function controls():Controls
{
return PlayerSettings.player1.controls;
}
public var lefthandText:AtlasText;
public var currentValue:String;
public var onChangeCallback:Null<String->Void>;
public var map:Map<String, String>;
public var keys:Array<String> = [];
var index = 0;
public function new(x:Float, y:Float, name:String, map:Map<String, String>, defaultValue:String, ?callback:String->Void)
{
super(x, y, name, function() {
callback(this.currentValue);
});
updateHitbox();
this.map = map;
this.currentValue = defaultValue;
this.onChangeCallback = callback;
var i:Int = 0;
for (key in map.keys())
{
this.keys.push(key);
if (this.currentValue == key) index = i;
i += 1;
}
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// var fancyTextFancyColor:Color;
if (selected)
{
var shouldDecrease:Bool = controls().UI_LEFT_P;
var shouldIncrease:Bool = controls().UI_RIGHT_P;
if (shouldDecrease) index -= 1;
if (shouldIncrease) index += 1;
if (index > keys.length - 1) index = 0;
if (index < 0) index = keys.length - 1;
currentValue = keys[index];
if (onChangeCallback != null && (shouldIncrease || shouldDecrease))
{
onChangeCallback(currentValue);
}
}
lefthandText.text = formatted(currentValue);
}
function formatted(value:String):String
{
// FIXME: Can't add arrows around the text because the font doesn't support < >
// var leftArrow:String = selected ? '<' : '';
// var rightArrow:String = selected ? '>' : '';
return '${map.get(value) ?? value}';
}
}

View file

@ -0,0 +1,136 @@
package funkin.ui.options.items;
import funkin.ui.TextMenuList;
import funkin.ui.AtlasText;
import funkin.input.Controls;
/**
* Preference item that allows the player to pick a value between min and max
*/
class NumberPreferenceItem extends TextMenuItem
{
function controls():Controls
{
return PlayerSettings.player1.controls;
}
// Widgets
public var lefthandText:AtlasText;
// Constants
static final HOLD_DELAY:Float = 0.3; // seconds
static final CHANGE_RATE:Float = 0.08; // seconds
// Constructor-initialized variables
public var currentValue:Float;
public var min:Float;
public var max:Float;
public var step:Float;
public var precision:Int;
public var onChangeCallback:Null<Float->Void>;
public var valueFormatter:Null<Float->String>;
// Variables
var holdDelayTimer:Float = HOLD_DELAY; // seconds
var changeRateTimer:Float = 0.0; // seconds
/**
* @param min Minimum value (example: 0)
* @param max Maximum value (example: 100)
* @param step The value to increment/decrement by (example: 10)
* @param callback Will get called every time the user changes the setting; use this to apply/save the setting.
* @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed string looks
*/
public function new(x:Float, y:Float, name:String, defaultValue:Float, min:Float, max:Float, step:Float, precision:Int, ?callback:Float->Void,
?valueFormatter:Float->String):Void
{
super(x, y, name, function() {
callback(this.currentValue);
});
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
updateHitbox();
this.currentValue = defaultValue;
this.min = min;
this.max = max;
this.step = step;
this.precision = precision;
this.onChangeCallback = callback;
this.valueFormatter = valueFormatter;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// var fancyTextFancyColor:Color;
if (selected)
{
holdDelayTimer -= elapsed;
if (holdDelayTimer <= 0.0)
{
changeRateTimer -= elapsed;
}
var jpLeft:Bool = controls().UI_LEFT_P;
var jpRight:Bool = controls().UI_RIGHT_P;
if (jpLeft || jpRight)
{
holdDelayTimer = HOLD_DELAY;
changeRateTimer = 0.0;
}
var shouldDecrease:Bool = jpLeft;
var shouldIncrease:Bool = jpRight;
if (controls().UI_LEFT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
{
shouldDecrease = true;
changeRateTimer = CHANGE_RATE;
}
else if (controls().UI_RIGHT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
{
shouldIncrease = true;
changeRateTimer = CHANGE_RATE;
}
// Actually increasing/decreasing the value
if (shouldDecrease)
{
var isBelowMin:Bool = currentValue - step < min;
currentValue = (currentValue - step).clamp(min, max);
if (onChangeCallback != null && !isBelowMin) onChangeCallback(currentValue);
}
else if (shouldIncrease)
{
var isAboveMax:Bool = currentValue + step > max;
currentValue = (currentValue + step).clamp(min, max);
if (onChangeCallback != null && !isAboveMax) onChangeCallback(currentValue);
}
}
lefthandText.text = formatted(currentValue);
}
/** Turns the float into a string */
function formatted(value:Float):String
{
var float:Float = toFixed(value);
if (valueFormatter != null)
{
return valueFormatter(float);
}
else
{
return '${float}';
}
}
function toFixed(value:Float):Float
{
var multiplier:Float = Math.pow(10, precision);
return Math.floor(value * multiplier) / multiplier;
}
}

View file

@ -16,7 +16,7 @@ class LevelProp extends Bopper
this.propData = value;
this.visible = this.propData != null;
danceEvery = this.propData?.danceEvery ?? 0;
danceEvery = this.propData?.danceEvery ?? 1.0;
applyData();
}
@ -32,7 +32,7 @@ class LevelProp extends Bopper
public function playConfirm():Void
{
playAnimation('confirm', true, true);
if (hasAnimation('confirm')) playAnimation('confirm', true, true);
}
function applyData():Void

View file

@ -216,7 +216,7 @@ class StoryMenuState extends MusicBeatState
changeLevel();
refresh();
#if discord_rpc
#if FEATURE_DISCORD_RPC
// Updating Discord Rich Presence
DiscordClient.changePresence('In the Menus', null);
#end

View file

@ -174,7 +174,7 @@ class LoadingState extends MusicBeatSubState
FlxG.watch.addQuick('percentage?', callbacks.numRemaining / callbacks.length);
}
#if debug
#if FEATURE_DEBUG_FUNCTIONS
if (FlxG.keys.justPressed.SPACE) trace('fired: ' + callbacks.getFired() + ' unfired:' + callbacks.getUnfired());
#end
}
@ -291,29 +291,51 @@ class LoadingState extends MusicBeatSubState
FunkinSprite.preparePurgeCache();
FunkinSprite.cacheTexture(Paths.image('healthBar'));
FunkinSprite.cacheTexture(Paths.image('menuDesat'));
FunkinSprite.cacheTexture(Paths.image('combo'));
FunkinSprite.cacheTexture(Paths.image('num0'));
FunkinSprite.cacheTexture(Paths.image('num1'));
FunkinSprite.cacheTexture(Paths.image('num2'));
FunkinSprite.cacheTexture(Paths.image('num3'));
FunkinSprite.cacheTexture(Paths.image('num4'));
FunkinSprite.cacheTexture(Paths.image('num5'));
FunkinSprite.cacheTexture(Paths.image('num6'));
FunkinSprite.cacheTexture(Paths.image('num7'));
FunkinSprite.cacheTexture(Paths.image('num8'));
FunkinSprite.cacheTexture(Paths.image('num9'));
// Lord have mercy on me and this caching -anysad
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/combo'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num0'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num1'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num2'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num3'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num4'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num5'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num6'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num7'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num8'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num9'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/combo'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num0'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num1'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num2'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num3'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num4'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num5'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num6'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num7'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num8'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num9'));
FunkinSprite.cacheTexture(Paths.image('notes', 'shared'));
FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared'));
FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared'));
FunkinSprite.cacheTexture(Paths.image('NOTE_hold_assets'));
FunkinSprite.cacheTexture(Paths.image('ready', 'shared'));
FunkinSprite.cacheTexture(Paths.image('set', 'shared'));
FunkinSprite.cacheTexture(Paths.image('go', 'shared'));
FunkinSprite.cacheTexture(Paths.image('sick', 'shared'));
FunkinSprite.cacheTexture(Paths.image('good', 'shared'));
FunkinSprite.cacheTexture(Paths.image('bad', 'shared'));
FunkinSprite.cacheTexture(Paths.image('shit', 'shared'));
FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/ready', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/set', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/go', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/ready', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/set', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/go', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/sick'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/good'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/bad'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/shit'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/sick'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/good'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/bad'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/shit'));
// List all image assets in the level's library.
// This is crude and I want to remove it when we have a proper asset caching system.

View file

@ -41,9 +41,9 @@ class Constants
* A suffix to add to the game version.
* Add a suffix to prototype builds and remove it for releases.
*/
public static final VERSION_SUFFIX:String = #if (DEBUG || FORCE_DEBUG_VERSION) ' PROTOTYPE' #else '' #end;
public static final VERSION_SUFFIX:String = #if FEATURE_DEBUG_FUNCTIONS ' PROTOTYPE' #else '' #end;
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
static function get_VERSION():String
{
return 'v${Application.current.meta.get('version')} (${GIT_BRANCH} : ${GIT_HASH}${GIT_HAS_LOCAL_CHANGES ? ' : MODIFIED' : ''})' + VERSION_SUFFIX;
@ -258,6 +258,11 @@ class Constants
*/
public static final DEFAULT_NOTE_STYLE:String = 'funkin';
/**
* The default pixel note style for songs.
*/
public static final DEFAULT_PIXEL_NOTE_STYLE:String = 'pixel';
/**
* The default album for songs in Freeplay.
*/
@ -379,11 +384,7 @@ class Constants
* 1 = The preloader waits for 1 second before moving to the next step.
* The progress bare is automatically rescaled to match.
*/
#if debug
public static final PRELOADER_MIN_STAGE_TIME:Float = 0.0;
#else
public static final PRELOADER_MIN_STAGE_TIME:Float = 0.1;
#end
/**
* HEALTH VALUES
@ -523,12 +524,16 @@ class Constants
* OTHER
*/
// ==============================
#if FEATURE_GHOST_TAPPING
// Hey there, Eric here.
// This feature is currently still in development. You can test it out by creating a special debug build!
// lime build windows -DFEATURE_GHOST_TAPPING
/**
* If true, the player will not receive the ghost miss penalty if there are no notes within the hit window.
* This is the thing people have been begging for forever lolol.
* Duration, in seconds, after the player's section ends before the player can spam without penalty.
*/
public static final GHOST_TAPPING:Bool = false;
public static final GHOST_TAP_DELAY:Float = 3 / 8;
#end
/**
* The maximum number of previous file paths for the Chart Editor to remember.

View file

@ -265,9 +265,10 @@ class CrashHandler
static function renderMethod():String
{
try
var outputStr:String = 'UNKNOWN';
outputStr = try
{
return switch (FlxG.renderMethod)
switch (FlxG.renderMethod)
{
case FlxRenderMethod.DRAW_TILES: 'DRAW_TILES';
case FlxRenderMethod.BLITTING: 'BLITTING';
@ -276,7 +277,9 @@ class CrashHandler
}
catch (e)
{
return 'ERROR ON QUERY RENDER METHOD: ${e}';
'ERROR ON QUERY RENDER METHOD: ${e}';
}
return outputStr;
}
}

View file

@ -1,6 +1,9 @@
package funkin.util.plugins;
import flixel.FlxG;
import flixel.FlxBasic;
import funkin.ui.MusicBeatState;
import funkin.ui.MusicBeatSubState;
/**
* A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state.
@ -28,10 +31,15 @@ class ReloadAssetsDebugPlugin extends FlxBasic
if (FlxG.keys.justPressed.F5)
#end
{
funkin.modding.PolymodHandler.forceReloadAssets();
var state:Dynamic = FlxG.state;
if (state is MusicBeatState || state is MusicBeatSubState) state.reloadAssets();
else
{
funkin.modding.PolymodHandler.forceReloadAssets();
// Create a new instance of the current state, so old data is cleared.
FlxG.resetState();
// Create a new instance of the current state, so old data is cleared.
FlxG.resetState();
}
}
}