Merge branch 'rewrite/master' into feature/charSelect

This commit is contained in:
Cameron Taylor 2024-08-29 15:13:58 -04:00
commit 825327e1c8
54 changed files with 2253 additions and 937 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 --always --global git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
haxelib --debug --global --always git hmm https://github.com/FunkinCrew/hmm funkin-patches
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

@ -111,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
assets

@ -1 +1 @@
Subproject commit f986e52819c5fd2e47f4d8d3297f326351682675
Subproject commit 8558bc906ecd48ba5d7b1c213982f15b5d033227

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,7 +18,7 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "10c2a203c43a78ff1ff26b8368fd736576829d8d",
"ref": "599f38eeb502a8ba6439784036c2cfdc7b485260",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@ -32,7 +39,7 @@
"name": "flixel-ui",
"type": "git",
"dir": null,
"ref": "d0afed7293c71ffdb1184751317fc709b44c9056",
"ref": "27f1ba626f80a6282fa8a187115e79a4a2133dc2",
"url": "https://github.com/HaxeFlixel/flixel-ui"
},
{
@ -42,13 +49,6 @@
"ref": "c3340af27d8e5bc11b42362d0ba33504bd2f3d35",
"url": "https://github.com/Dot-Stuff/flxanimate"
},
{
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "a1eab7b9bf507b87200a3341719054fe427f3b15",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
},
{
"name": "format",
"type": "haxelib",
@ -58,7 +58,7 @@
"name": "funkin.vis",
"type": "git",
"dir": null,
"ref": "38261833590773cb1de34ac5d11e0825696fc340",
"ref": "22b1ce089dd924f15cdc4632397ef3504d464e90",
"url": "https://github.com/FunkinCrew/funkVis"
},
{
@ -89,8 +89,10 @@
},
{
"name": "hscript",
"type": "haxelib",
"version": "2.5.0"
"type": "git",
"dir": null,
"ref": "12785398e2f07082f05034cb580682e5671442a2",
"url": "https://github.com/FunkinCrew/hscript"
},
{
"name": "hxCodec",
@ -103,8 +105,8 @@
"name": "hxcpp",
"type": "git",
"dir": null,
"url": "https://github.com/HaxeFoundation/hxcpp",
"ref": "8dc8020f8465027de6c2aaaed90718bc693651ed"
"ref": "904ea40643b050a5a154c5e4c33a83fd2aec18b1",
"url": "https://github.com/HaxeFoundation/hxcpp"
},
{
"name": "hxcpp-debug-server",
@ -143,7 +145,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "f6153ffcb1ffcf733f91d531eac5fda4189e07f7",
"ref": "e0b2339e02fff91168789dbd1a0dd019ea3dda39",
"url": "https://github.com/FunkinCrew/lime"
},
{
@ -185,7 +187,7 @@
"name": "polymod",
"type": "git",
"dir": null,
"ref": "98945c6c7f5ecde01a32c4623d3515bf012a023a",
"ref": "96cfc5fa693b017e47f7cb13b765cc68698fa6b6",
"url": "https://github.com/larsiusprime/polymod"
},
{

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

@ -34,7 +34,7 @@ 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
@ -123,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) {
@ -144,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();
@ -374,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

@ -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,6 +54,41 @@ 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.

View file

@ -93,8 +93,8 @@ class StageRegistry extends BaseRegistry<Stage, StageData>
public function listBaseGameStageIds():Array<String>
{
return [
"mainStage", "spookyMansion", "phillyTrain", "phillyTrainErect", "limoRide", "limoRideErect", "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

@ -22,7 +22,7 @@ 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

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

@ -28,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.
@ -178,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)
@ -258,8 +257,29 @@ class PolymodHandler
// 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;
@ -268,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;

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

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

@ -67,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
@ -445,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 = '';
@ -698,7 +698,7 @@ class PlayState extends MusicBeatSubState
initStrumlines();
initPopups();
#if discord_rpc
#if FEATURE_DISCORD_RPC
// Initialize Discord Rich Presence.
initDiscord();
#end
@ -738,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;
@ -977,7 +977,7 @@ class PlayState extends MusicBeatSubState
// boyfriendPos.put(); // TODO: Why is this here?
}
#if discord_rpc
#if FEATURE_DISCORD_RPC
DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
#end
}
@ -1036,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?
@ -1047,7 +1047,7 @@ class PlayState extends MusicBeatSubState
{
#end
persistentDraw = false;
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
}
#end
@ -1066,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
@ -1179,9 +1179,9 @@ class PlayState extends MusicBeatSubState
}
/**
* 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,
@ -1237,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))
@ -1278,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,
@ -1301,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();
@ -1311,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
@ -1329,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
@ -1346,8 +1346,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Call this by pressing F5 on a debug build.
*/
* Call this by pressing F5 on a debug build.
*/
override function reloadAssets():Void
{
funkin.modding.PolymodHandler.forceReloadAssets();
@ -1459,8 +1459,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Initializes the game and HUD cameras.
*/
* Initializes the game and HUD cameras.
*/
function initCameras():Void
{
camGame = new FunkinCamera('playStateCamGame');
@ -1484,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;
@ -1516,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);
@ -1537,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);
@ -1558,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
}
@ -1581,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)
@ -1661,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
}
@ -1670,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
}
@ -1681,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
}
@ -1692,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;
@ -1725,8 +1725,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Configures the judgement and combo popups.
*/
* Configures the judgement and combo popups.
*/
function initPopups():Void
{
var noteStyleId:String = currentChart.noteStyle;
@ -1740,11 +1740,11 @@ class PlayState extends MusicBeatSubState
}
/**
* Initializes the Discord Rich Presence.
*/
* Initializes the Discord Rich Presence.
*/
function initDiscord():Void
{
#if discord_rpc
#if FEATURE_DISCORD_RPC
storyDifficultyText = difficultyString();
iconRPC = currentSong.player2;
@ -1775,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)
@ -1808,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;
@ -1862,10 +1862,10 @@ 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.
@ -1879,9 +1879,9 @@ class PlayState extends MusicBeatSubState
}
/**
* 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;
@ -1901,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;
@ -1921,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;
@ -1957,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
@ -1972,14 +1972,14 @@ 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();
@ -1992,8 +1992,8 @@ class PlayState extends MusicBeatSubState
}
/**
* 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.
@ -2010,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)
@ -2025,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;
@ -2036,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;
@ -2047,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;
@ -2221,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;
}
@ -2266,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
@ -2291,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;
@ -2321,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).
@ -2333,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.
@ -2435,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!
@ -2493,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.
@ -2548,11 +2549,11 @@ class PlayState extends MusicBeatSubState
}
/**
* Debug keys. Disabled while in cutscenes.
*/
* Debug keys. Disabled while in cutscenes.
*/
function debugKeyShit():Void
{
#if CHART_EDITOR_SUPPORTED
#if FEATURE_CHART_EDITOR
// Open the stage editor overlaying the current state.
if (controls.DEBUG_STAGE)
{
@ -2582,7 +2583,7 @@ 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;
@ -2599,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);
@ -2612,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)
@ -2645,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')
@ -2703,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;
@ -2750,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;
@ -2918,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(),
});
});
@ -2930,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(),
});
}
@ -2968,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.
@ -3027,8 +3038,8 @@ class PlayState extends MusicBeatSubState
}
/**
* 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!');
@ -3092,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;
@ -3142,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();
@ -3151,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.
@ -3174,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);
@ -3183,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.
@ -3221,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.
@ -3253,8 +3264,8 @@ class PlayState extends MusicBeatSubState
}
/**
* Cancel all active camera tweens simultaneously.
*/
* Cancel all active camera tweens simultaneously.
*/
public function cancelAllCameraTweens()
{
cancelCameraFollowTween();
@ -3264,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.
@ -3315,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

@ -129,7 +129,7 @@ class AnimateAtlasCharacter extends BaseCharacter
{
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.onAnimationComplete.removeAll();
sprite.onAnimationComplete.add(this.onAnimationFinished);

View file

@ -366,12 +366,19 @@ class BaseCharacter extends Bopper
// and Darnell (this keeps the flame on his lighter flickering).
// Works for idle, singLEFT/RIGHT/UP/DOWN, alt singing animations, and anything else really.
if (!getCurrentAnimation().endsWith(Constants.ANIMATION_HOLD_SUFFIX)
&& hasAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX)
&& isAnimationFinished())
if (isAnimationFinished()
&& !getCurrentAnimation().endsWith(Constants.ANIMATION_HOLD_SUFFIX)
&& hasAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX))
{
playAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX);
}
else
{
if (isAnimationFinished())
{
trace('Not playing hold (${getCurrentAnimation()}) (${isAnimationFinished()}, ${getCurrentAnimation().endsWith(Constants.ANIMATION_HOLD_SUFFIX)}, ${hasAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX)})');
}
}
// Handle character note hold time.
if (isSinging())
@ -411,7 +418,6 @@ class BaseCharacter extends Bopper
else
{
// Play the idle animation.
trace('${characterId}: attempting dance');
dance(true);
}
}
@ -594,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);
@ -628,12 +634,12 @@ class BaseCharacter extends Bopper
var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != '' ? '-${suffix}' : ''}';
// restart even if already playing, because the character might sing the same note twice.
trace('Playing ${anim}...');
playAnimation(anim, true);
}
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

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

@ -94,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.
@ -179,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.
@ -492,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.
*/
@ -563,6 +608,10 @@ class Strumline extends FlxSpriteGroup
playStatic(dir);
}
resetScrollSpeed();
#if FEATURE_GHOST_TAPPING
ghostTapTimer = 0;
#end
}
public function applyNoteData(data:Array<SongNoteData>):Void
@ -602,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

View file

@ -315,7 +315,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
public function isAnimationFinished():Bool
{
return this.animation.finished;
return this.animation?.finished ?? false;
}
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void

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

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

@ -26,6 +26,8 @@ class CharSelectGF extends FlxAtlasSprite
var analyzer:SpectralAnalyzer;
var curGF:GFChar = GF;
public function new()
{
super(0, 0, Paths.animateAtlas("charSelect/gfChill"));
@ -35,7 +37,7 @@ class CharSelectGF extends FlxAtlasSprite
switchGF("bf");
}
override public function update(elapsed:Float)
override public function update(elapsed:Float):Void
{
super.update(elapsed);
@ -103,7 +105,7 @@ class CharSelectGF extends FlxAtlasSprite
* @param animInfo Should not be confused with animInInfo!
* This is merely a local var for the function!
*/
function doFade(animInfo:FramesJSFLInfo)
function doFade(animInfo:FramesJSFLInfo):Void
{
fadeTimer += FlxG.elapsed;
if (fadeTimer >= 1 / 24)
@ -141,27 +143,30 @@ class CharSelectGF extends FlxAtlasSprite
fadeAnimIndex = 0;
}
public function switchGF(str:String)
/**
* For switching between "GFs" such as gf, nene, etc
* @param bf Which BF we are selecting, so that we know the accompyaning GF
*/
public function switchGF(bf:String):Void
{
str = switch (str)
var prevGF:GFChar = curGF;
switch (bf)
{
case "pico":
"nene";
curGF = NENE;
case "bf":
"gf";
curGF = GF;
default:
"gf";
curGF = GF;
}
char = str;
switch str
{
default:
loadAtlas(Paths.animateAtlas("charSelect/" + str + "Chill"));
}
// We don't need to update any anims if we didn't change GF
if (prevGF == curGF) return;
animInInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + str + "AnimInfo/" + str + "In.txt"));
animOutInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + str + "AnimInfo/" + str + "Out.txt"));
loadAtlas(Paths.animateAtlas("charSelect/" + curGF + "Chill"));
animInInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "In.txt"));
animOutInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "Out.txt"));
playAnimation("idle", true, false, true);
// addFrameCallback(getNextFrameLabel("idle"), () -> playAnimation("idle", true, false, false));
@ -176,3 +181,9 @@ enum FadeStatus
FADE_OUT;
FADE_IN;
}
enum abstract GFChar(String) from String to String
{
var GF = "gf";
var NENE = "nene";
}

View file

@ -1,29 +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.graphics.FunkinCamera;
import funkin.vis.dsp.SpectralAnalyzer;
import openfl.display.BlendMode;
class CharSelectSubState extends MusicBeatSubState
{
@ -73,8 +75,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
@ -259,7 +282,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);
@ -283,7 +306,6 @@ class CharSelectSubState extends MusicBeatSubState
}
var grpIcons:FlxSpriteGroup;
var grpXSpread(default, set):Float = 107;
var grpYSpread(default, set):Float = 127;

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

@ -3327,7 +3327,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
handleTestKeybinds();
handleHelpKeybinds();
#if (debug || FORCE_DEBUG_VERSION)
#if FEATURE_DEBUG_FUNCTIONS
handleQuickWatch();
#end
@ -5699,7 +5699,7 @@ 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' | 'spookyMansionErect':
PlayStatePlaylist.campaignId = 'week2';

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

@ -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();
@ -99,7 +99,7 @@ class FreeplayDJ extends FlxAtlasSprite
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)
{
playAnimation("Boyfriend DJ fist pump", true, false, 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');
@ -148,7 +199,7 @@ class FreeplayDJ extends FlxAtlasSprite
// I shit myself.
}
#if debug
#if FEATURE_DEBUG_FUNCTIONS
if (FlxG.keys.pressed.CONTROL)
{
if (FlxG.keys.justPressed.LEFT)
@ -185,7 +236,14 @@ class FreeplayDJ extends FlxAtlasSprite
if (name == playableCharData.getAnimationPrefix('intro'))
{
currentState = Idle;
if (PlayerRegistry.instance.hasNewCharacter())
{
currentState = NewUnlock;
}
else
{
currentState = Idle;
}
onIntroDone.dispatch();
}
else if (name == playableCharData.getAnimationPrefix('idle'))
@ -225,9 +283,17 @@ class FreeplayDJ extends FlxAtlasSprite
// runTvLogic();
}
trace('Replay idle: ${frame}');
playAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, 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}');
@ -240,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;
@ -271,7 +346,7 @@ class FreeplayDJ extends FlxAtlasSprite
function loadCartoon()
{
cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() {
playAnimation("Boyfriend DJ watchin tv OG", true, false, false, 60);
playFlashAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, 60);
});
// Fade out music to 40% volume over 1 second.
@ -301,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;
playAnimation("Boyfriend DJ fist pump", true, false, 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;
playAnimation("Boyfriend DJ loss reaction 1", true, false, false, 4);
var animPrefix = playableCharData.getAnimationPrefix('loss');
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpLoopBadStartFrame());
}
override public function getCurrentAnimation():String
@ -366,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;
@ -178,9 +177,22 @@ class FreeplayState extends MusicBeatSubState
var stickerSubState:Null<StickerSubState> = null;
public static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
/**
* The difficulty we were on when this menu was last accessed.
*/
public static var rememberedDifficulty:String = Constants.DEFAULT_DIFFICULTY;
/**
* The song we were on when this menu was last accessed.
* NOTE: `null` if the last song was `Random`.
*/
public static var rememberedSongId:Null<String> = 'tutorial';
/**
* The character we were on when this menu was last accessed.
*/
public static var rememberedCharacterId:String = Constants.DEFAULT_CHARACTER;
var funnyCam:FunkinCamera;
var rankCamera:FunkinCamera;
var rankBg:FunkinSprite;
@ -210,14 +222,16 @@ class FreeplayState extends MusicBeatSubState
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
{
currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER;
currentCharacterId = params?.character ?? rememberedCharacterId;
var fetchPlayableCharacter = function():PlayableCharacter {
var result = PlayerRegistry.instance.fetchEntry(params?.character ?? Constants.DEFAULT_CHARACTER);
var result = PlayerRegistry.instance.fetchEntry(params?.character ?? rememberedCharacterId);
if (result == null) throw 'No valid playable character with id ${params?.character}';
return result;
};
currentCharacter = fetchPlayableCharacter();
rememberedCharacterId = currentCharacter?.id ?? Constants.DEFAULT_CHARACTER;
fromResultsParams = params?.fromResults;
if (fromResultsParams?.playRankAnim == true)
@ -292,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
@ -630,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);
@ -747,10 +761,7 @@ class FreeplayState extends MusicBeatSubState
var tempSongs:Array<Null<FreeplaySongData>> = songs;
// Remember just the difficulty because it's important for song sorting.
if (rememberedDifficulty != null)
{
currentDifficulty = rememberedDifficulty;
}
currentDifficulty = rememberedDifficulty;
if (filterStuff != null) tempSongs = sortSongs(tempSongs, filterStuff);
@ -875,7 +886,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
@ -905,7 +916,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;
@ -1087,11 +1106,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;
@ -1194,7 +1213,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();
@ -1202,7 +1221,7 @@ class FreeplayState extends MusicBeatSubState
{
super.update(elapsed);
#if debug
#if FEATURE_DEBUG_FUNCTIONS
if (FlxG.keys.justPressed.T)
{
rankAnimStart(fromResultsParams ??
@ -1219,7 +1238,7 @@ class FreeplayState extends MusicBeatSubState
FlxG.switchState(FreeplayState.build(
{
{
character: currentCharacterId == "pico" ? "bf" : "pico",
character: currentCharacterId == "pico" ? Constants.DEFAULT_CHARACTER : "pico",
}
}));
}
@ -1237,7 +1256,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)
@ -1330,6 +1374,8 @@ class FreeplayState extends MusicBeatSubState
}
handleInputs(elapsed);
if (dj != null) FlxG.watch.addQuick('dj-anim', dj.getCurrentAnimation());
}
function handleInputs(elapsed:Float):Void
@ -1487,7 +1533,7 @@ class FreeplayState extends MusicBeatSubState
generateSongList(currentFilter, true);
}
if (controls.BACK)
if (controls.BACK && !busy)
{
busy = true;
FlxTween.globalManager.clear();
@ -1776,7 +1822,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];
@ -1837,7 +1883,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,
@ -1895,7 +1941,7 @@ class FreeplayState extends MusicBeatSubState
intendedCompletion = 0.0;
diffIdsCurrent = diffIdsTotal;
rememberedSongId = null;
rememberedDifficulty = null;
rememberedDifficulty = Constants.DEFAULT_DIFFICULTY;
albumRoll.albumId = null;
}
@ -1946,7 +1992,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];
@ -2004,10 +2050,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');
@ -2023,8 +2072,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);
}
@ -2153,8 +2202,14 @@ class FreeplaySongData
function updateValues(variations:Array<String>):Void
{
this.songDifficulties = song.listSuffixedDifficulties(variations, false, false);
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
this.songDifficulties = song.listDifficulties(null, variations, false, false);
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;

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,10 +110,7 @@ class MainMenuState extends MusicBeatState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
openSubState(new FreeplayState(
{
character: FlxG.keys.pressed.SHIFT ? 'pico' : 'bf',
}));
openSubState(new FreeplayState());
});
#if CAN_OPEN_LINKS
@ -344,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)
{
@ -355,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',
@ -375,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

@ -16,7 +16,7 @@ class LevelProp extends Bopper
this.propData = value;
this.visible = this.propData != null;
danceEvery = this.propData?.danceEvery ?? 0.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
}
@ -314,25 +314,28 @@ class LoadingState extends MusicBeatSubState
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('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/normal/sick'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/good'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/bad'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/shit'));
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'));
FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this
// 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;
@ -384,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
@ -528,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;
}
}