Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3746545452 | ||
|
|
b4c48893bc | ||
|
|
a42240e6a5 | ||
|
|
91b4544f7e | ||
|
|
9c8ba2c053 | ||
|
|
19abcb5009 | ||
|
|
1189c226ee | ||
|
|
097dbf5bb4 | ||
|
|
8890cbf270 | ||
|
|
623985a2cc | ||
|
|
8cd7f2a5b1 | ||
|
|
0a5419d7fc | ||
|
|
b2647fe09f | ||
|
|
e66290c55f | ||
|
|
31d1b3e246 | ||
|
|
880615e707 | ||
|
|
7d5d80d515 | ||
|
|
6848815e80 | ||
|
|
5d10bb9f54 | ||
|
|
f6a5a9d8ce | ||
|
|
9695e79134 | ||
|
|
3267c61258 | ||
|
|
903b3fc599 | ||
|
|
0d60929f18 | ||
|
|
bc546e86aa | ||
|
|
de5cca1120 | ||
|
|
3aa4591bab | ||
|
|
bc8f506193 | ||
|
|
f52472a476 | ||
|
|
ab5bda3ee5 | ||
|
|
777978f5a5 | ||
|
|
711e0a6b75 | ||
|
|
1fde59f999 | ||
|
|
165ad60155 | ||
|
|
d2e29879fe | ||
|
|
4fa9a0daaa | ||
|
|
c0314c85ec | ||
|
|
f862fb2c3e | ||
|
|
d7f5070317 | ||
|
|
1d2bd61119 | ||
|
|
a4ed55721f | ||
|
|
f1b6e6c4e4 | ||
|
|
29b6763290 | ||
|
|
410cfe972d | ||
|
|
39b1a42cfe | ||
|
|
b0b73c8399 | ||
|
|
5141ac13df | ||
|
|
45a5160b06 | ||
|
|
89ee33e9b9 | ||
|
|
4b54118731 | ||
|
|
dbf66ac250 | ||
|
|
79eb7d633c | ||
|
|
f61789e453 | ||
|
|
1da0da249d | ||
|
|
6cd1dec3d4 | ||
|
|
2175beaa65 | ||
|
|
6df80ba69d | ||
|
|
67f7b638fb | ||
|
|
399869cdcc | ||
|
|
c5a76cf1d5 | ||
|
|
30a9887136 | ||
|
|
9324359d2f | ||
|
|
4a9ad53695 | ||
|
|
5d013a1aa7 |
@@ -6,41 +6,45 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: Be sure to complete these steps to increase the chances of your issue being addressed!
|
||||
options:
|
||||
- label: I have properly named the issue
|
||||
- label: I have checked the issues/discussions pages to see if the issue has been previously reported
|
||||
- label: I have properly named my issue
|
||||
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What platform are you using?
|
||||
label: Platform
|
||||
description: Which platform are you playing on?
|
||||
options:
|
||||
- Newgrounds (Web)
|
||||
- Itch.io (Web)
|
||||
- Newgrounds (Web/HTML5)
|
||||
- Itch.io (Web/HTML5)
|
||||
- Itch.io (Downloadable Build) - Windows
|
||||
- Itch.io (Downloadable Build) - MacOS
|
||||
- Itch.io (Downloadable Build) - Linux
|
||||
- Compiled from GitHub Source Code
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: If you are playing on a browser, which one are you using?
|
||||
label: Browser
|
||||
description: (Web/HTML5 users only) Which browser are you playing on?
|
||||
options:
|
||||
- Google Chrome
|
||||
- Microsoft Edge
|
||||
- Firefox
|
||||
- Opera
|
||||
- Safari
|
||||
- Other (Specify below)
|
||||
- Other (Specify in Description field)
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version are you using?
|
||||
placeholder: ex. 0.4.1
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.5.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Describe your bug."
|
||||
@@ -49,14 +53,16 @@ body:
|
||||
attributes:
|
||||
value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "#### Provide as many details as you can."
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Context (Provide images, videos, etc.)
|
||||
|
||||
label: Description (include any images, videos, errors, or crash logs)
|
||||
description: Provide as much detail as you can. The better others understand your issue, the more they can help you!
|
||||
placeholder: Describe your issue here...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce (or crash logs, errors, etc.)
|
||||
label: Steps to Reproduce
|
||||
description: What steps can other people take to reliably encounter this issue?
|
||||
placeholder: 1. Open the game...
|
||||
|
||||
@@ -6,65 +6,73 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: Be sure to complete these steps to increase the chances of your issue being addressed!
|
||||
options:
|
||||
- label: I have properly named the issue
|
||||
- label: I have checked the issues/discussions pages to see if the issue has been previously reported
|
||||
- label: I have properly named my issue
|
||||
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What platform are you using?
|
||||
label: Platform
|
||||
description: Which platform are you playing on?
|
||||
options:
|
||||
- Newgrounds (Web)
|
||||
- Itch.io (Web)
|
||||
- Newgrounds (Web/HTML5)
|
||||
- Itch.io (Web/HTML5)
|
||||
- Itch.io (Downloadable Build) - Windows
|
||||
- Itch.io (Downloadable Build) - MacOS
|
||||
- Itch.io (Downloadable Build) - Linux
|
||||
- Compiled from GitHub Source Code
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: If you are playing on a browser, which one are you using?
|
||||
label: Browser
|
||||
description: (Web/HTML5 users only) Which browser are you playing on?
|
||||
options:
|
||||
- Google Chrome
|
||||
- Microsoft Edge
|
||||
- Firefox
|
||||
- Opera
|
||||
- Safari
|
||||
- Other (Specify below)
|
||||
- Other (Specify in Description field)
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version are you using?
|
||||
placeholder: ex. 0.4.1
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.5.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Describe your issue."
|
||||
value: "## Describe the crash."
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "#### Provide as many details as you can."
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Context (Provide screenshots or videos of the crash happening)
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
label: Description
|
||||
description: Include screenshots or videos of the crash happening. Provide as much detail as you can.
|
||||
placeholder: Describe the crash here...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Crash logs (can be found in the logs folder where Funkin.exe is)
|
||||
label: Steps to Reproduce
|
||||
description: What steps can other people take to reliably trigger this crash?
|
||||
placeholder: 1. Open the game...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Crash logs
|
||||
description: These can be found in the logs folder where Funkin.exe is.
|
||||
placeholder: Upload your logs here...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -6,10 +6,13 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: Be sure to complete these steps to increase the chances of your suggestion being considered!
|
||||
options:
|
||||
- label: I have properly named the enhancement
|
||||
- label: I have checked the issues/discussions pages to see if the enhancement has been previously suggested
|
||||
- label: I have properly named my enhancement
|
||||
- label: I have checked the Issues/Discussions pages to see if my enhancement has already been suggested
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What is your suggestion, and why should it be implemented?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -7,6 +7,6 @@ medium:
|
||||
min: 10
|
||||
max: 99
|
||||
|
||||
# Add 'large' to any changes for more than 100 lines
|
||||
# Add 'large' to any changes of at least 100 lines
|
||||
large:
|
||||
min: 100
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
# Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder
|
||||
# Add Documentation tag to PR's changing markdown files, or anything in the docs folder
|
||||
Documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- docs/*
|
||||
- '**/*.md'
|
||||
|
||||
# Adds Haxe tag to PR's changing haxe code files
|
||||
# Add Haxe tag to PR's changing haxe code files
|
||||
Haxe:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.hx'
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[submodule "assets"]
|
||||
path = assets
|
||||
url = https://github.com/FunkinCrew/Funkin-assets-secret
|
||||
url = https://github.com/FunkinCrew/Funkin.assets
|
||||
[submodule "art"]
|
||||
path = art
|
||||
url = https://github.com/FunkinCrew/Funkin-art-secret
|
||||
url = https://github.com/FunkinCrew/Funkin.art
|
||||
|
||||
+121
@@ -4,6 +4,127 @@ 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.2] - 2024-10-11
|
||||
|
||||
### Changed
|
||||
- (docs) Delete Modding.md since we have a separate modding documentation ([a42240e](https://github.com/FunkinCrew/Funkin/commit/a42240e6a595d33034f2c887bf38a350d1fa0f15)) - by @AbnormalPoof in [#3651](https://github.com/FunkinCrew/Funkin/pull/3651)
|
||||
- (docs) Create a git cliff template for easier changelog stuff ([91b4544](https://github.com/FunkinCrew/Funkin/commit/91b4544f7ebc51485e3e28c3d716ba6ee69ad885)) - by @ninjamuffin99 in [#3652](https://github.com/FunkinCrew/Funkin/pull/3652)
|
||||
- Added InverseDotsShader that emulates flash selections ([097dbf5](https://github.com/FunkinCrew/Funkin/commit/097dbf5bb4346d431d8ca9f0ec4bc5b5e6f4523f)) - by @ninjamuffin99
|
||||
- (docs) Add additional `variation` input parameter to `Save.hasBeatenSong()` to allow usage of the function by inputting a variation id ([4fa9a0d](https://github.com/FunkinCrew/Funkin/commit/4fa9a0daaa67e0977460b147bd1f74a118e3e2a5)) - by @ninjamuffin99
|
||||
- (docs) Added modding docs link in readme ([4b54118](https://github.com/FunkinCrew/Funkin/commit/4b54118731e26118111e06558ae4853c577fe4bb)) - by @Cartridge-Man
|
||||
- Fix some misspellings and grammar in code documentation ([2175bea](https://github.com/FunkinCrew/Funkin/commit/2175beaa651e009332202985be4b7eb4ed36e5a4)) - by @Hundrec
|
||||
- (docs) Improvements to Github Issues templates ([399869c](https://github.com/FunkinCrew/Funkin/commit/399869cdccc9c5ac27cecfbcdc33c3d7eb4b348c)) - by @Hundrec in [#3458](https://github.com/FunkinCrew/Funkin/pull/3458)
|
||||
- Fix some misspellings and grammar in code documentation ([6df80ba](https://github.com/FunkinCrew/Funkin/commit/6df80ba69d0e24269f40471f83462cab7d5e13cf)) - by @Hundrec in [#3477](https://github.com/FunkinCrew/Funkin/pull/3477)
|
||||
- (docs) Improvements to Github Issues templates ([67f7b63](https://github.com/FunkinCrew/Funkin/commit/67f7b638fb76840b868cbfa70a1c6063577984c5)) - by @Hundrec
|
||||
|
||||
### Fixed
|
||||
- Disable flickering when changing FPS in the options menu ([b2647fe](https://github.com/FunkinCrew/Funkin/commit/b2647fe09f5281ce7074b26d47bc1524764168ee)) - by @lemz1 in [#3629](https://github.com/FunkinCrew/Funkin/pull/3629)
|
||||
- Anti alias / smooth the volume sound tray ([e66290c](https://github.com/FunkinCrew/Funkin/commit/e66290c55f7141402223644f06ec8a69edeee089)) - by @Kn1ghtNight in [#2853](https://github.com/FunkinCrew/Funkin/pull/2853)
|
||||
- Don't restart the FreeplayState song preview when changing the difficulty within the same variation ([903b3fc](https://github.com/FunkinCrew/Funkin/commit/903b3fc59905a70802618a1cd67407722ea956ed)) - by @JustKolosaki in [#3587](https://github.com/FunkinCrew/Funkin/pull/3587)
|
||||
- Exiting the chart editor doesn't crash the game anymore ([f52472a](https://github.com/FunkinCrew/Funkin/commit/f52472a4767388b22cfbab0f5f7860f6e6762856)) - by @EliteMasterEric
|
||||
- Character Select cursor moves properly at lower framerates ([ab5bda3](https://github.com/FunkinCrew/Funkin/commit/ab5bda3ee573a6e03595ec6941e6de38df851889)) - by @ninjamuffin99 in [#3507](https://github.com/FunkinCrew/Funkin/pull/3507)
|
||||
- Stopped allowing F1 to create more than one help dialog window in the Charting Editor ([777978f](https://github.com/FunkinCrew/Funkin/commit/777978f5a544e1b7c89b47dcc365f734eb6d0df1)) - by @amyspark-ng
|
||||
- Main menu music doesn't cut out when switching states anymore. ([711e0a6](https://github.com/FunkinCrew/Funkin/commit/711e0a6b7547eb04113e9318dab900f01ad576a5)) - by @EliteMasterEric in [#3530](https://github.com/FunkinCrew/Funkin/pull/3530)
|
||||
- The dialog now shows up on the animation debugger view ([1fde59f](https://github.com/FunkinCrew/Funkin/commit/1fde59f999eac94eb10fc22094885de2f5310705)) - by @EliteMasterEric
|
||||
- Center preloader 'fnf' and 'dsp' text so it doesn't clip anymore ([165ad60](https://github.com/FunkinCrew/Funkin/commit/165ad6015539a295e9eefdaef291c312e9566b26)) - by @Burgerballs in [#3567](https://github.com/FunkinCrew/Funkin/pull/3567)
|
||||
- `Song.getFirstValidVariation()` now properly takes into account multiple variations/difficulty input ([d2e2987](https://github.com/FunkinCrew/Funkin/commit/d2e29879fe2acc6febfe0f335f655b741d630c34)) - by @ninjamuffin99 in [#3506](https://github.com/FunkinCrew/Funkin/pull/3506)
|
||||
- (freeplay) Proper variation / difficulty loading for Freeplay Menu ([c0314c8](https://github.com/FunkinCrew/Funkin/commit/c0314c85ecd5116641aff3de8e9153f7fe48e79c)) - by @ninjamuffin99
|
||||
- Picos songs properly load on freeplay ([1d2bd61](https://github.com/FunkinCrew/Funkin/commit/1d2bd61119e5f418df7f11d7ef2a0fdedee17d3d)) - by @ninjamuffin99
|
||||
- (debug) No more fullscreening when typing "F" in the flixel debugger console ([29b6763](https://github.com/FunkinCrew/Funkin/commit/29b6763290df05d42039806f3d142740568c80f0)) - by @ninjamuffin99
|
||||
- Fix the user song offsets being applied incorrectly ([410cfe9](https://github.com/FunkinCrew/Funkin/commit/410cfe972d6df9de4d4d128375cf8380c4f06d92)) - by @JustKolosaki
|
||||
- Fix crash in LatencyState when exiting / cleaning up state data ([39b1a42](https://github.com/FunkinCrew/Funkin/commit/39b1a42cfeafe2b7be8b66e2fe529e853d9ae197)) - by @lemz1
|
||||
- Add additional classes to Polymod Blacklist ([b0b73c8](https://github.com/FunkinCrew/Funkin/commit/b0b73c83994f33118c6a69550da9ec8ec1c07adc)) - by @EliteMasterEric
|
||||
- Stop allowing inputs after selecting a character ([dbf66ac](https://github.com/FunkinCrew/Funkin/commit/dbf66ac250137262866d75f7c1387645b35d88d0)) - by @ACrazyTown
|
||||
- Fixed an issue where the player and girlfriend would disappear or overlap themselves in Character Select (community fix by gamerbross)
|
||||
- Fixed an issue where the game would show the wrong girlfriend in Character Select (community fix by gamerbross)
|
||||
- Fixed an issue where the cursor wouldn't update properly in Character Select (community fix by gamerbross)
|
||||
- Fixed an issue where the player would display double after entering character select or when spamming buttons (community fix by gamerbross)
|
||||
|
||||
## New Contributors for 0.5.2
|
||||
* @Kn1ghtNight made their first contribution in [#2853](https://github.com/FunkinCrew/Funkin/pull/2853)
|
||||
* @DaWaterMalone made their first contribution
|
||||
* @amyspark-ng made their first contribution
|
||||
* @Cartridge-Man made their first contribution
|
||||
* @afreetoplaynoob made their first contribution
|
||||
|
||||
|
||||
## [0.5.1] - 2024-09-30
|
||||
### Added
|
||||
- Readded the Merch button to the main menu.
|
||||
- Click it to check out our Makeship campaign!
|
||||
- Added Discord Rich Presence support. People can now see what song you are playing from Discord!
|
||||
- We'll get mod support working for this eventually.
|
||||
- Added an FPS limit option to the Preferences menu.
|
||||
- You can now change how high the game tries to push your frame rate, from as little as 30 to as high as 300.
|
||||
- Added support for the Tracy instrumenation-based profiling tool in development environments. Enable it with the `-DFEATURE_DEBUG_TRACY` compilation flag.
|
||||
- For the people who aren't nerds, this is a tool for tracking down performance issues!
|
||||
- Playable Character data now defines an asset location for an Animate Atlas to display Girlfriend.
|
||||
- This includes the option to display a visualizer, if configured correctly.
|
||||
- Separated the Perfect and Perfect (Gold) animations in the Playable Character data.
|
||||
- Base game just uses the same animation for both, but modders can split the animations up on their custom characters now.
|
||||
- Added a bunch of Flash project files from the Weekend 1 and Playable Pico updates to the `Funkin.art` repository.
|
||||
- Added the `flipX` and `flipY` parameters to props in the Stage data. (community feature by abnormalpoof)
|
||||
### Changed
|
||||
- The game's mod API version check is now more dynamic.
|
||||
- The update accepts mods with API version `0.5.0` as well as `0.5.1`.
|
||||
- Pico is no longer unlocked for all players automatically.
|
||||
- You need to beat Weekend 1 in Story Mode in order to unlock him in Character Select.
|
||||
- Removed some of the more spammy `trace()` calls to improve debugging a bit.
|
||||
- Improved some of the compilation and modding documentation.
|
||||
- The game now complains if you create a song variation with symbols in the name.
|
||||
- Switched the force crash keybind from Ctrl-Shift-L to Ctrl-Alt-Shift-L.
|
||||
- Added some additional functions to `funkin.Assets` after `openfl.utils.Assets` had to get blacklisted from scripts.
|
||||
### Fixed
|
||||
- Fixed an issue where DadBattle (Pico Mix) was not properly credited to `TeraVex (ft. Saruky)`.
|
||||
- Fixed an issue where DadBattle (Pico Mix) was missing charts on the Normal and Easy difficulty.
|
||||
- Fixed an issue where Spookeez (Pico Mix) was not properly credited to `Six Impala (ft. Saster)`.
|
||||
- Fixed an issue where the "Shit!" judgement would display with anti-aliasing in Week 6.
|
||||
- Fixed an issue where Pico Erect could be played with different instrumentals.
|
||||
- Fixed an issue where Pico would not play his shooting animations in Stress.
|
||||
- Fixed an issue where Freeplay would display no custom songs when switching characters.
|
||||
- Fixed an issue where Freeplay would sometimes display the wrong text on the capsules.
|
||||
- Fixed an issue where duplicate difficulties from custom variations wouldn't display properly in Freeplay.
|
||||
- Fixed an issue where custom note styles would sometimes just use default values rather than using the fallback note style.
|
||||
- Fixed an issue where custom note styles would randomly fail to fetch information about their fallback note style.
|
||||
- Fixed an issue where the Screenshots and Chart Editor binds displayed in the controls menu on Web builds (where they are disabled).
|
||||
- Fixed an issue where the Stage Editor bind displayed in the controls menu even when the feature was disabled.
|
||||
- Fixed an issue where the Freeplay Character Select keybind displayed weird in the keybinds menu.
|
||||
- Fixed an issue where setting the input offset or visual offset too high would cause the song to skip.
|
||||
- Fixed an issue where video cutscenes would not scale their volume properly.
|
||||
- Fixed an issue where Cocoa Erect (Erect difficulty) had a tap note stacked on top of a hold note,
|
||||
- Fixed an issue where the game would stutter when playing on the Week 5 Remix stage.
|
||||
- Fixed an issue where the save data version number wouldn't get written to save data properly.
|
||||
- Fixed an issue where the example mod could not be loaded.
|
||||
- Fixed an issue where a script error would display when entering Blazin'.
|
||||
- Fixed an issue where pressing F5 to force reload a song would sometimes cause the game to crash.
|
||||
- Fixed an issue where animations on Animate Atlas characters would throw a bunch of warnings in the console.
|
||||
- Fixed an issue where characters with high offsets would shift over after the player dies or restarts.
|
||||
- Fixed an issue where Pico wouldn't play out his full burp animation in South (Pico Mix).
|
||||
- Fixed an issue where Results screen audio could continue into Freeplay or even gameplay.
|
||||
- Fixed an issue where some audio tracks would get destroyed even if they were flagged as persistent.
|
||||
- Fixed an issue where the audio track would stay muted if you missed a note just before Pico burps.
|
||||
- Fixed an issue where the curtains in Week 1 would display in front of larger characters.
|
||||
- Fixed an issue where Boyfriend wouldn't play his death animation properly on Week 2's Remix stage.
|
||||
- Fixed an issue in Freeplay where the clear % would not display after switching characters.
|
||||
- Fixed an issue in Freeplay where character remixes would display the base song's highscore.
|
||||
- Fixed an issue where Pico would become locked every time the game starts, and you would have to watch the unlock animation each game boot.
|
||||
- The animation should now play only once per save file.
|
||||
- Fixed an issue where Spirit's trail in Week 6 would not display correctly.
|
||||
- Fixed an issue where the Input Offsets menu would crash when entering it before playing a song on web builds.
|
||||
- Fixed an issue where the Results screen would spam the percentage tick noise instead of playing when the value changes.
|
||||
- Fixed an issue where parts of the Chart Editor could not be interacted with. (community fix by KadeDeveloper)
|
||||
- Fixed an issue where classic FocusCamera song events could cause the camera to snap in place. (community fix by NebulaZorua)
|
||||
- Fixed an issue where achieving the same rank on a song (but a different clear %) would override your clear %, even if it was lower. (community fix by lemz1)
|
||||
- Fixed an issue where the FPS counter would display even if Debug Display was turned off. (community fix by Lethrial)
|
||||
- Fixed an issue where selecting the area to the left of the Chart Editor would select some of the player's notes (community fix by NotHyper474)
|
||||
- Fixed an issue where pixel icons in the Chart Editor would not display correctly. (community fix by Techniktil)
|
||||
- Fixed an issue where `Stage.addCharacter` would not properly assign the `characterType`. (community fix by KadeDeveloper)
|
||||
- Fixed an issue where players should interact with Character Select during the unlock sequence, causing a crash. (community fix by actualmandm)
|
||||
- Fixed an issue where hold notes in Week 6 were not scaled/positioned correctly. (community fix by dombomb64)
|
||||
- Fixed an issue where audio offets would not interact with the Chart Editor properly. (community fix by KadeDev)
|
||||
- Fixed an issue where fetching Modules during the `onDestroy` event would fail at random. (community fix by cyn0x8)
|
||||
- Fixed an issue where `onSubStateOpenEnd` and `onSubStateCloseEnd` script events would not always get called. (community fix by lemz1)
|
||||
|
||||
## [0.5.0] - 2024-09-12
|
||||
### Added
|
||||
- Added a new Character Select screen to switch between playable characters in Freeplay
|
||||
|
||||
+3
-3
@@ -4,14 +4,14 @@ Code style is enforced using Visual Studio Code extensions.
|
||||
|
||||
## .hx
|
||||
Formatting is handled by the `nadako.vshaxe` extension, which includes Haxe Formatter.
|
||||
Haxe Formatter automatically resolves issues such as intentation style and line breaks, and can be configured in `hxformat.json`.
|
||||
Haxe Formatter automatically resolves issues such as indentation style and line breaks, and can be configured in `hxformat.json`.
|
||||
|
||||
Code Quality is handled by the `vshaxe.haxe-checkstyle` extension, which includes Haxe Checkstyle.
|
||||
|
||||
### Haxe Checkstyle Notes
|
||||
* Checks can be escalated to display as different serverities in the Problems window.
|
||||
* Checks can be escalated to display as different severities in the Problems window.
|
||||
* Checks can be disabled by setting the severity to `IGNORE`.
|
||||
* `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the intentation is.
|
||||
* `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the indentation is.
|
||||
* `CommentedOutCode` check is in place because old code should be retrieved via Git history.
|
||||
* TODO items: Enable these one-by-one and fix them to improve the overall code quality.
|
||||
- Reconfigure `MethodLength`
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
# RIGHT NOW THE MODS FOLDER DOES NOT WORK ENTIRELY JUST YET!!!
|
||||
## THIS IS WORK IN PROGRESS!!!
|
||||
|
||||
# QUICK AND DIRTY MOD GUIDE
|
||||
|
||||
With the 0.2.6 update, I added a bit of a slightly nicer mod support backend.
|
||||
|
||||
It's POLYMOD, which is made by Lars Doucet: https://github.com/larsiusprime/polymod
|
||||
|
||||
You may have noticed that there's a new folder in the assets. MODS. Within it you will see 2 files. modList.txt, and a folder called introMod.
|
||||
modList.txt will load any folder into the game. Put the folder you want to load into a new line in modList.txt, and reboot the game.
|
||||
|
||||
Now you may be wondering, what do I put in the folder? Well later down it'll get a bit more complicated, especially as I'll make the IN-GAME mod loader nicer.
|
||||
@@ -17,6 +17,10 @@ To learn how to install the necessary dependencies and compile the game from sou
|
||||
|
||||
Please check out our [Contributor's guide](./CONTRIBUTORS.md) on how you can actively participate in the development of Friday Night Funkin'.
|
||||
|
||||
# Modding
|
||||
|
||||
Feel free to start learning to mod the game by reading our [documentation](https://funkincrew.github.io/funkin-modding-docs/) and guide to modding.
|
||||
|
||||
# Credits and Special Thanks
|
||||
|
||||
Full credits can be found in-game, or in the `credits.json` file which is located [here](https://github.com/FunkinCrew/funkin.assets/blob/main/exclude/data/credits.json).
|
||||
|
||||
+1
-1
Submodule assets updated: b2404b6b1c...4abd6cc06e
+90
@@ -0,0 +1,90 @@
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "FunkinCrew"
|
||||
repo = "Funkin"
|
||||
|
||||
# To bypass "you have reached your rate limit!", you can input a github token either as an environment variable
|
||||
# or alongside the `git cliff` command like so `git cliff --github-token TOKEN_HERE`
|
||||
# Personally I like to use the github cli `gh` tool to get a token
|
||||
# `git cliff --github-token $(gh auth token)`
|
||||
|
||||
[changelog]
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||
{%- endmacro -%}
|
||||
|
||||
{% if version -%}
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else -%}
|
||||
## [Unreleased]
|
||||
{% endif -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{%- for commit in commits %}
|
||||
- {% if commit.scope %}({{commit.scope}}) {% endif %}{{ commit.message | split(pat="\n") | first | upper_first | trim }} \
|
||||
([{{ commit.id | truncate(length=7, end="") }}]({{self::remote_url()}}/commit/{{ commit.id }})) -\
|
||||
{% if commit.remote.username %} by @{{ commit.remote.username }}
|
||||
{%- elif commit.author.name %} by {{ commit.author.name }}
|
||||
{%- endif -%}
|
||||
{% if commit.remote.pr_number %} in \
|
||||
[#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
|
||||
{%- endif -%}
|
||||
{% if commit.links | length != 0 %}\
|
||||
[#{{ commit.links }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
## New Contributors
|
||||
{%- endif -%}
|
||||
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor %}\n
|
||||
"""
|
||||
|
||||
# remove the leading and trailing whitespace from the templates
|
||||
trim = true
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
# remove issue numbers from commits
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^docs", group = "Changed", scope="docs" },
|
||||
{ message = "^[a|A]dd", group = "Added" },
|
||||
{ message = "^[s|S]upport", group = "Added" },
|
||||
{ message = "^[r|R]emove", group = "Removed" },
|
||||
{ message = "^.*: add", group = "Added" },
|
||||
{ message = "^.*: support", group = "Added" },
|
||||
{ message = "^.*: remove", group = "Removed" },
|
||||
{ message = "^.*: delete", group = "Removed" },
|
||||
{ message = "^test", group = "Fixed" },
|
||||
{ message = "^fix", group = "Fixed" },
|
||||
{ message = "^.*: fix", group = "Fixed" },
|
||||
{ message = "^.*", group = "Changed" },
|
||||
]
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
||||
|
||||
+2
-1
@@ -34,10 +34,11 @@ There are several useful build flags you can add to a build to affect how it wor
|
||||
- `-DGITHUB_BUILD` will enable in-game debug functions (such as the ability to time travel in a song by pressing `PgUp`/`PgDn`), without enabling the other stuff
|
||||
- `-DFEATURE_POLYMOD_MODS` or `-DNO_FEATURE_POLYMOD_MODS` to forcibly enable or disable modding support.
|
||||
- `-DREDIRECT_ASSETS_FOLDER` or `-DNO_REDIRECT_ASSETS_FOLDER` to forcibly enable or disable asset redirection.
|
||||
- This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game
|
||||
- This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game will break if you try to zip it up and send it to someone, so it's disabled for release builds.
|
||||
- `-DFEATURE_DISCORD_RPC` or `-DNO_FEATURE_DISCORD_RPC` to forcibly enable or disable support for Discord Rich Presence.
|
||||
- `-DFEATURE_VIDEO_PLAYBACK` or `-DNO_FEATURE_VIDEO_PLAYBACK` to forcibly enable or disable video cutscene support.
|
||||
- `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu.
|
||||
- `-DFEATURE_SCREENSHOTS` or `-DNO_FEATURE_SCREENSHOTS` to forcibly enable or disable the screenshots feature.
|
||||
- `-DFEATURE_STAGE_EDITOR` to forcibly enable the experimental stage editor.
|
||||
- `-DFEATURE_GHOST_TAPPING` to forcibly enable an experimental gameplay change to the anti-mash system.
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ Most of this functionality is only available on debug builds of the game!
|
||||
- `2`: ***GAIN HEALTH***: Debug function, add 10% to the player's health.
|
||||
- `3`: ***LOSE HEALTH***: Debug function, subtract 5% to the player's health.
|
||||
- `9`: NEATO!
|
||||
- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL****: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead.
|
||||
- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL****: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead.
|
||||
- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL***: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead.
|
||||
- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL***: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead.
|
||||
|
||||
## **Freeplay State**
|
||||
- `F` (Freeplay Menu) - Move to Favorites
|
||||
@@ -27,5 +27,5 @@ Most of this functionality is only available on debug builds of the game!
|
||||
- `Y` - WOAH
|
||||
|
||||
## **Main Menu**
|
||||
- `~`: ***DEBUG****: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu.
|
||||
- `~`: ***DEBUG***: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu.
|
||||
- `CTRL-ALT-SHIFT-W`: ***ALL ACCESS***: Unlocks all songs in Freeplay. Only available on debug builds.
|
||||
|
||||
@@ -63,14 +63,14 @@
|
||||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "c9d96b168ea2a19274ad7c766ab1a34b57baa793",
|
||||
"ref": "51c23588614397089a5ce182cddea729f0be6fa0",
|
||||
"url": "https://github.com/haxeui/haxeui-core"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "013b9d4e56bfe9a034e028a8d685f0b274cb73c4",
|
||||
"ref": "da27e833947f32ef007ed11f523aa5524f5a5d54",
|
||||
"url": "https://github.com/haxeui/haxeui-flixel"
|
||||
},
|
||||
{
|
||||
@@ -205,4 +205,4 @@
|
||||
"url": "https://github.com/fponticelli/thx.semver"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ class Project extends HXProject {
|
||||
* REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES!
|
||||
* You only have to change it here, the rest of the game will query this value.
|
||||
*/
|
||||
static final VERSION:String = "0.5.1";
|
||||
static final VERSION:String = "0.5.2";
|
||||
|
||||
/**
|
||||
* The game's name. Used as the default window title.
|
||||
|
||||
@@ -413,7 +413,7 @@ class Conductor
|
||||
}
|
||||
|
||||
// Take into account instrumental and file format song offsets.
|
||||
songPos += applyOffsets ? (instrumentalOffset + formatOffset + audioVisualOffset) : 0;
|
||||
songPos += applyOffsets ? (combinedOffset) : 0;
|
||||
|
||||
var oldMeasure:Float = this.currentMeasure;
|
||||
var oldBeat:Float = this.currentBeat;
|
||||
|
||||
@@ -37,7 +37,7 @@ class Preferences
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether some particularly fowl language is displayed.
|
||||
* Whether some particularly foul language is displayed.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var naughtyness(get, set):Bool;
|
||||
|
||||
@@ -20,6 +20,10 @@ class StageData
|
||||
@:optional
|
||||
public var cameraZoom:Null<Float>;
|
||||
|
||||
@:default("shared")
|
||||
@:optional
|
||||
public var directory:Null<String>;
|
||||
|
||||
public function new()
|
||||
{
|
||||
this.version = StageRegistry.STAGE_DATA_VERSION;
|
||||
@@ -198,6 +202,32 @@ typedef StageDataProp =
|
||||
@:default("sparrow")
|
||||
@:optional
|
||||
var animType:String;
|
||||
|
||||
/**
|
||||
* The angle of the prop, as a float.
|
||||
* @default 1.0
|
||||
*/
|
||||
@:optional
|
||||
@:default(0.0)
|
||||
var angle:Float;
|
||||
|
||||
/**
|
||||
* The blend mode of the prop, as a string.
|
||||
* Just like in photoshop.
|
||||
* @default Nothing.
|
||||
*/
|
||||
@:default("")
|
||||
@:optional
|
||||
var blend:String;
|
||||
|
||||
/**
|
||||
* The color of the prop overlay, as a hex string.
|
||||
* White overlays, or the ones with the value #FFFFFF, do not appear.
|
||||
* @default `#FFFFFF`
|
||||
*/
|
||||
@:default("#FFFFFF")
|
||||
@:optional
|
||||
var color:String;
|
||||
};
|
||||
|
||||
typedef StageDataCharacter =
|
||||
|
||||
@@ -238,6 +238,18 @@ class FunkinSprite extends FlxSprite
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The animation ID to check.
|
||||
* @return Whether the animation is dynamic (has multiple frames). `false` for static, one-frame animations.
|
||||
*/
|
||||
public function isAnimationDynamic(id:String):Bool
|
||||
{
|
||||
if (this.animation == null) return false;
|
||||
var animData = this.animation.getByName(id);
|
||||
if (animData == null) return false;
|
||||
return animData.numFrames > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts similarly to `makeGraphic`, but with improved memory usage,
|
||||
* at the expense of not being able to paint onto the resulting sprite.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package funkin.graphics.shaders;
|
||||
|
||||
import flixel.addons.display.FlxRuntimeShader;
|
||||
|
||||
/**
|
||||
* Create a little dotting effect.
|
||||
*/
|
||||
class InverseDotsShader extends FlxRuntimeShader
|
||||
{
|
||||
public var amount:Float;
|
||||
|
||||
public function new(amount:Float = 1.0)
|
||||
{
|
||||
super(Assets.getText(Paths.frag("InverseDots")));
|
||||
setAmount(amount);
|
||||
}
|
||||
|
||||
public function setAmount(value:Float):Void
|
||||
{
|
||||
this.amount = value;
|
||||
this.setFloat("_amount", amount);
|
||||
}
|
||||
}
|
||||
@@ -282,6 +282,14 @@ class PolymodHandler
|
||||
// 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');
|
||||
Polymod.blacklistImport('openfl.Lib');
|
||||
Polymod.blacklistImport('openfl.system.ApplicationDomain');
|
||||
Polymod.blacklistImport('funkin.util.FunkinTypeResolver');
|
||||
|
||||
// `openfl.desktop.NativeProcess`
|
||||
// Can load native processes on the host operating system.
|
||||
Polymod.blacklistImport('openfl.desktop.NativeProcess');
|
||||
|
||||
@@ -1457,11 +1457,34 @@ class PlayState extends MusicBeatSubState
|
||||
if (FlxG.sound.music != null)
|
||||
{
|
||||
var correctSync:Float = Math.min(FlxG.sound.music.length, Math.max(0, Conductor.instance.songPosition - Conductor.instance.combinedOffset));
|
||||
var playerVoicesError:Float = 0;
|
||||
var opponentVoicesError:Float = 0;
|
||||
|
||||
if (!startingSong && (Math.abs(FlxG.sound.music.time - correctSync) > 5 || Math.abs(vocals.checkSyncError(correctSync)) > 5))
|
||||
if (vocals != null)
|
||||
{
|
||||
@:privateAccess // todo: maybe make the groups public :thinking:
|
||||
{
|
||||
vocals.playerVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
var currentRawVoiceTime:Float = voice.time + vocals.playerVoicesOffset;
|
||||
if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(playerVoicesError)) playerVoicesError = currentRawVoiceTime - correctSync;
|
||||
});
|
||||
|
||||
vocals.opponentVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
var currentRawVoiceTime:Float = voice.time + vocals.opponentVoicesOffset;
|
||||
if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(opponentVoicesError)) opponentVoicesError = currentRawVoiceTime - correctSync;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!startingSong
|
||||
&& (Math.abs(FlxG.sound.music.time - correctSync) > 5 || Math.abs(playerVoicesError) > 5 || Math.abs(opponentVoicesError) > 5))
|
||||
{
|
||||
trace("VOCALS NEED RESYNC");
|
||||
if (vocals != null) trace(vocals.checkSyncError(correctSync));
|
||||
if (vocals != null)
|
||||
{
|
||||
trace(playerVoicesError);
|
||||
trace(opponentVoicesError);
|
||||
}
|
||||
trace(FlxG.sound.music.time);
|
||||
trace(correctSync);
|
||||
resyncVocals();
|
||||
@@ -1837,6 +1860,17 @@ class PlayState extends MusicBeatSubState
|
||||
smallImageKey: discordRPCIcon
|
||||
});
|
||||
#end
|
||||
|
||||
#if FEATURE_DISCORD_RPC
|
||||
// Updating Discord Rich Presence.
|
||||
DiscordClient.instance.setPresence(
|
||||
{
|
||||
state: buildDiscordRPCState(),
|
||||
details: buildDiscordRPCDetails(),
|
||||
largeImageKey: discordRPCAlbum,
|
||||
smallImageKey: discordRPCIcon
|
||||
});
|
||||
#end
|
||||
}
|
||||
|
||||
function buildDiscordRPCDetails():String
|
||||
@@ -2528,8 +2562,8 @@ class PlayState extends MusicBeatSubState
|
||||
healthChange = Constants.HEALTH_BAD_BONUS;
|
||||
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
|
||||
case 'shit':
|
||||
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
|
||||
healthChange = Constants.HEALTH_SHIT_BONUS;
|
||||
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
|
||||
}
|
||||
|
||||
// Send the note hit event.
|
||||
@@ -2599,8 +2633,6 @@ class PlayState extends MusicBeatSubState
|
||||
}
|
||||
vocals.playerVolume = 0;
|
||||
|
||||
if (Highscore.tallies.combo != 0) if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
|
||||
|
||||
applyScore(-10, 'miss', healthChange, true);
|
||||
|
||||
if (playSound)
|
||||
@@ -2819,7 +2851,7 @@ class PlayState extends MusicBeatSubState
|
||||
}
|
||||
}
|
||||
comboPopUps.displayRating(daRating);
|
||||
if (combo >= 10 || combo == 0) comboPopUps.displayCombo(combo);
|
||||
if (combo >= 10) comboPopUps.displayCombo(combo);
|
||||
|
||||
vocals.playerVolume = 1;
|
||||
}
|
||||
|
||||
@@ -2,28 +2,46 @@ package funkin.play.notes;
|
||||
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.FlxSprite;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.play.notes.NoteSprite;
|
||||
|
||||
/**
|
||||
* The actual receptor that you see on screen.
|
||||
*/
|
||||
class StrumlineNote extends FlxSprite
|
||||
class StrumlineNote extends FunkinSprite
|
||||
{
|
||||
/**
|
||||
* Whether this strumline note is on the player's side or the opponent's side.
|
||||
*/
|
||||
public var isPlayer(default, null):Bool;
|
||||
|
||||
/**
|
||||
* The direction which this strumline note is facing.
|
||||
*/
|
||||
public var direction(default, set):NoteDirection;
|
||||
|
||||
var confirmHoldTimer:Float = -1;
|
||||
|
||||
static final CONFIRM_HOLD_TIME:Float = 0.1;
|
||||
|
||||
function set_direction(value:NoteDirection):NoteDirection
|
||||
{
|
||||
this.direction = value;
|
||||
return this.direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this flag to `true` to disable performance optimizations that cause
|
||||
* the Strumline note sprite to ignore `velocity` and `acceleration`.
|
||||
*/
|
||||
public var forceActive:Bool = false;
|
||||
|
||||
/**
|
||||
* How long to continue the hold note animation after a note is pressed.
|
||||
*/
|
||||
static final CONFIRM_HOLD_TIME:Float = 0.1;
|
||||
|
||||
/**
|
||||
* How long the hold note animation has been playing after a note is pressed.
|
||||
*/
|
||||
var confirmHoldTimer:Float = -1;
|
||||
|
||||
public function new(noteStyle:NoteStyle, isPlayer:Bool, direction:NoteDirection)
|
||||
{
|
||||
super(0, 0);
|
||||
@@ -41,7 +59,10 @@ class StrumlineNote extends FlxSprite
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
function onAnimationFrame(name:String, frameNumber:Int, frameIndex:Int):Void {}
|
||||
function onAnimationFrame(name:String, frameNumber:Int, frameIndex:Int):Void
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
function onAnimationFinished(name:String):Void
|
||||
{
|
||||
@@ -103,19 +124,19 @@ class StrumlineNote extends FlxSprite
|
||||
|
||||
public function playStatic():Void
|
||||
{
|
||||
this.active = false;
|
||||
this.active = (forceActive || isAnimationDynamic('static'));
|
||||
this.playAnimation('static', true);
|
||||
}
|
||||
|
||||
public function playPress():Void
|
||||
{
|
||||
this.active = true;
|
||||
this.active = (forceActive || isAnimationDynamic('press'));
|
||||
this.playAnimation('press', true);
|
||||
}
|
||||
|
||||
public function playConfirm():Void
|
||||
{
|
||||
this.active = true;
|
||||
this.active = (forceActive || isAnimationDynamic('confirm'));
|
||||
this.playAnimation('confirm', true);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import funkin.data.song.SongRegistry;
|
||||
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import funkin.util.SortUtil;
|
||||
|
||||
/**
|
||||
@@ -79,7 +80,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
|
||||
// key = variation id, value = metadata
|
||||
final _metadata:Map<String, SongMetadata>;
|
||||
final difficulties:Map<String, SongDifficulty>;
|
||||
|
||||
/**
|
||||
* holds the difficulties (as in SongDifficulty) for each variation
|
||||
* difficulties.get('default').get('easy') would return the easy difficulty for the default variation
|
||||
*/
|
||||
final difficulties:Map<String, Map<String, SongDifficulty>>;
|
||||
|
||||
/**
|
||||
* The list of variations a song has.
|
||||
@@ -92,7 +98,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
}
|
||||
|
||||
// this returns false so that any new song can override this and return true when needed
|
||||
public function isSongNew(currentDifficulty:String):Bool
|
||||
public function isSongNew(currentDifficulty:String, currentVariation:String):Bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -146,7 +152,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
{
|
||||
this.id = id;
|
||||
|
||||
difficulties = new Map<String, SongDifficulty>();
|
||||
difficulties = new Map<String, Map<String, SongDifficulty>>();
|
||||
|
||||
_data = _fetchData(id);
|
||||
|
||||
@@ -156,7 +162,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
{
|
||||
for (vari in _data.playData.songVariations)
|
||||
{
|
||||
if (!validateVariationId(vari)) {
|
||||
if (!validateVariationId(vari))
|
||||
{
|
||||
trace(' [WARN] Variation id "$vari" is invalid, skipping...');
|
||||
continue;
|
||||
}
|
||||
@@ -249,22 +256,39 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
* List the album IDs for each variation of the song.
|
||||
* @return A map of variation IDs to album IDs.
|
||||
*/
|
||||
public function listAlbums():Map<String, String>
|
||||
public function listAlbums(variation:String):Map<String, String>
|
||||
{
|
||||
var result:Map<String, String> = new Map<String, String>();
|
||||
|
||||
for (difficultyId in difficulties.keys())
|
||||
for (variationMap in difficulties)
|
||||
{
|
||||
var meta:Null<SongDifficulty> = difficulties.get(difficultyId);
|
||||
if (meta != null && meta.album != null)
|
||||
for (difficultyId in variationMap.keys())
|
||||
{
|
||||
result.set(difficultyId, meta.album);
|
||||
var meta:Null<SongDifficulty> = variationMap.get(difficultyId);
|
||||
if (meta != null && meta.album != null)
|
||||
{
|
||||
result.set(difficultyId, meta.album);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input a difficulty ID and a variation ID, and get the album ID.
|
||||
* @param diffId
|
||||
* @param variation
|
||||
* @return String
|
||||
*/
|
||||
public function getAlbumId(diffId:String, variation:String):String
|
||||
{
|
||||
var diff:Null<SongDifficulty> = getDifficulty(diffId, variation);
|
||||
if (diff == null) return '';
|
||||
|
||||
return diff.album ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the difficulty data from the provided metadata.
|
||||
* Does not load chart data (that is triggered later when we want to play the song).
|
||||
@@ -285,6 +309,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
continue;
|
||||
}
|
||||
|
||||
// This resides within difficulties
|
||||
var difficultyMap:Map<String, SongDifficulty> = new Map<String, SongDifficulty>();
|
||||
|
||||
// There may be more difficulties in the chart file than in the metadata,
|
||||
// (i.e. non-playable charts like the one used for Pico on the speaker in Stress)
|
||||
// but all the difficulties in the metadata must be in the chart file.
|
||||
@@ -309,10 +336,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
difficulty.noteStyle = metadata.playData.noteStyle;
|
||||
|
||||
difficulty.characters = metadata.playData.characters;
|
||||
|
||||
var variationSuffix = (metadata.variation != Constants.DEFAULT_VARIATION) ? '-${metadata.variation}' : '';
|
||||
difficulties.set('$diffId$variationSuffix', difficulty);
|
||||
difficultyMap.set(diffId, difficulty);
|
||||
}
|
||||
difficulties.set(metadata.variation, difficultyMap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,15 +371,18 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
|
||||
for (diffId in chartNotes.keys())
|
||||
{
|
||||
// Retrieve the cached difficulty data.
|
||||
var variationSuffix = (variation != Constants.DEFAULT_VARIATION) ? '-$variation' : '';
|
||||
var difficulty:Null<SongDifficulty> = difficulties.get('$diffId$variationSuffix');
|
||||
if (difficulty == null)
|
||||
// Retrieve the cached difficulty data. This one could potentially be null.
|
||||
var nullDiff:Null<SongDifficulty> = getDifficulty(diffId, variation);
|
||||
|
||||
// if the difficulty doesn't exist, create a new one, and then proceed to fill it with data.
|
||||
// I mostly do this since I don't wanna throw around ? everywhere for null check lol?
|
||||
var difficulty:SongDifficulty = nullDiff ?? new SongDifficulty(this, diffId, variation);
|
||||
|
||||
if (nullDiff == null)
|
||||
{
|
||||
trace('Fabricated new difficulty for $diffId.');
|
||||
difficulty = new SongDifficulty(this, diffId, variation);
|
||||
var metadata = _metadata.get(variation);
|
||||
difficulties.set('$diffId$variationSuffix', difficulty);
|
||||
difficulties.get(variation)?.set(diffId, difficulty);
|
||||
|
||||
if (metadata != null)
|
||||
{
|
||||
@@ -396,29 +425,34 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
|
||||
for (currentVariation in variations)
|
||||
{
|
||||
var variationSuffix = (currentVariation != Constants.DEFAULT_VARIATION) ? '-$currentVariation' : '';
|
||||
|
||||
if (difficulties.exists('$diffId$variationSuffix'))
|
||||
if (difficulties.get(currentVariation)?.exists(diffId) ?? false)
|
||||
{
|
||||
return difficulties.get('$diffId$variationSuffix');
|
||||
return difficulties.get(currentVariation)?.get(diffId);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first valid variation that matches both the difficulty id, and the current character / possible input variations
|
||||
* @param diffId
|
||||
* @param currentCharacter
|
||||
* @param possibleVariations
|
||||
* @return Null<String>
|
||||
*/
|
||||
public function getFirstValidVariation(?diffId:String, ?currentCharacter:PlayableCharacter, ?possibleVariations:Array<String>):Null<String>
|
||||
{
|
||||
if (possibleVariations == null)
|
||||
{
|
||||
possibleVariations = getVariationsByCharacter(currentCharacter);
|
||||
}
|
||||
|
||||
if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0];
|
||||
|
||||
for (variationId in possibleVariations)
|
||||
{
|
||||
var variationSuffix = (variationId != Constants.DEFAULT_VARIATION) ? '-$variationId' : '';
|
||||
if (difficulties.exists('$diffId$variationSuffix')) return variationId;
|
||||
if (difficulties.get('$variationId')?.exists(diffId) ?? false) return variationId;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -440,7 +474,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
}
|
||||
|
||||
var result = [];
|
||||
trace('Evaluating variations for ${this.id} ${char.id}: ${this.variations}');
|
||||
for (variation in variations)
|
||||
{
|
||||
var metadata = _metadata.get(variation);
|
||||
@@ -459,6 +492,19 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nearly the same thing as getVariationsByCharacter, but takes a character ID instead.
|
||||
* @param charId
|
||||
* @return Array<String>
|
||||
* @see getVariationsByCharacter
|
||||
*/
|
||||
public function getVariationsByCharacterId(?charId:String):Array<String>
|
||||
{
|
||||
var charPlayer = PlayerRegistry.instance.fetchEntry(charId ?? '');
|
||||
|
||||
return getVariationsByCharacter(charPlayer);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all the difficulties in this song.
|
||||
*
|
||||
@@ -501,6 +547,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
/**
|
||||
* TODO: This line of code makes me sad, but you can't really fix it without a breaking migration.
|
||||
* @return `easy`, `erect`, `normal-pico`, etc.
|
||||
* @deprecated This function is deprecated, Funkin no longer uses suffixed difficulties.
|
||||
*/
|
||||
public function listSuffixedDifficulties(variationIds:Array<String>, ?showLocked:Bool, ?showHidden:Bool):Array<String>
|
||||
{
|
||||
@@ -529,8 +576,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
|
||||
for (targetVariation in variationIds)
|
||||
{
|
||||
var variationSuffix = (targetVariation != Constants.DEFAULT_VARIATION) ? '-$targetVariation' : '';
|
||||
if (difficulties.exists('$diffId$variationSuffix')) return true;
|
||||
if (difficulties.get(targetVariation)?.exists(diffId) ?? false) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -565,13 +611,16 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge the cached chart data for each difficulty of this song.
|
||||
* Purge the cached chart data for each difficulty/variation of this song.
|
||||
*/
|
||||
public function clearCharts():Void
|
||||
{
|
||||
for (diff in difficulties)
|
||||
for (variationMap in difficulties)
|
||||
{
|
||||
diff.clearChart();
|
||||
for (diff in variationMap)
|
||||
{
|
||||
diff.clearChart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +696,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||
* Auto-accept if it's one of the base game default variations.
|
||||
* Reject if the ID starts with a number, or contains invalid characters.
|
||||
*/
|
||||
static function validateVariationId(variation:String):Bool {
|
||||
static function validateVariationId(variation:String):Bool
|
||||
{
|
||||
if (Constants.DEFAULT_VARIATION_LIST.contains(variation)) return true;
|
||||
|
||||
return VARIATION_REGEX.match(variation);
|
||||
|
||||
@@ -256,6 +256,10 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
||||
propSprite.scrollFactor.x = dataProp.scroll[0];
|
||||
propSprite.scrollFactor.y = dataProp.scroll[1];
|
||||
|
||||
propSprite.angle = dataProp.angle;
|
||||
propSprite.color = FlxColor.fromString(dataProp.color);
|
||||
@:privateAccess if (!isSolidColor) propSprite.blend = BlendMode.fromString(dataProp.blend);
|
||||
|
||||
propSprite.zIndex = dataProp.zIndex;
|
||||
|
||||
propSprite.flipX = dataProp.flipX;
|
||||
|
||||
+168
-8
@@ -10,6 +10,7 @@ import funkin.save.migrator.SaveDataMigrator;
|
||||
import funkin.save.migrator.SaveDataMigrator;
|
||||
import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
|
||||
import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
|
||||
import funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme;
|
||||
import funkin.util.SerializerUtil;
|
||||
import thx.semver.Version;
|
||||
import thx.semver.Version;
|
||||
@@ -146,6 +147,14 @@ class Save
|
||||
hitsoundVolumeOpponent: 1.0,
|
||||
themeMusic: true
|
||||
},
|
||||
|
||||
optionsStageEditor:
|
||||
{
|
||||
previousFiles: [],
|
||||
moveStep: "1px",
|
||||
angleStep: 5,
|
||||
theme: StageEditorTheme.Light
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -428,6 +437,91 @@ class Save
|
||||
return data.unlocks.oldChar;
|
||||
}
|
||||
|
||||
public var stageEditorPreviousFiles(get, set):Array<String>;
|
||||
|
||||
function get_stageEditorPreviousFiles():Array<String>
|
||||
{
|
||||
if (data.optionsStageEditor.previousFiles == null) data.optionsStageEditor.previousFiles = [];
|
||||
|
||||
return data.optionsStageEditor.previousFiles;
|
||||
}
|
||||
|
||||
function set_stageEditorPreviousFiles(value:Array<String>):Array<String>
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsStageEditor.previousFiles = value;
|
||||
flush();
|
||||
return data.optionsStageEditor.previousFiles;
|
||||
}
|
||||
|
||||
public var stageEditorHasBackup(get, set):Bool;
|
||||
|
||||
function get_stageEditorHasBackup():Bool
|
||||
{
|
||||
if (data.optionsStageEditor.hasBackup == null) data.optionsStageEditor.hasBackup = false;
|
||||
|
||||
return data.optionsStageEditor.hasBackup;
|
||||
}
|
||||
|
||||
function set_stageEditorHasBackup(value:Bool):Bool
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsStageEditor.hasBackup = value;
|
||||
flush();
|
||||
return data.optionsStageEditor.hasBackup;
|
||||
}
|
||||
|
||||
public var stageEditorMoveStep(get, set):String;
|
||||
|
||||
function get_stageEditorMoveStep():String
|
||||
{
|
||||
if (data.optionsStageEditor.moveStep == null) data.optionsStageEditor.moveStep = "1px";
|
||||
|
||||
return data.optionsStageEditor.moveStep;
|
||||
}
|
||||
|
||||
function set_stageEditorMoveStep(value:String):String
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsStageEditor.moveStep = value;
|
||||
flush();
|
||||
return data.optionsStageEditor.moveStep;
|
||||
}
|
||||
|
||||
public var stageEditorAngleStep(get, set):Float;
|
||||
|
||||
function get_stageEditorAngleStep():Float
|
||||
{
|
||||
if (data.optionsStageEditor.angleStep == null) data.optionsStageEditor.angleStep = 5;
|
||||
|
||||
return data.optionsStageEditor.angleStep;
|
||||
}
|
||||
|
||||
function set_stageEditorAngleStep(value:Float):Float
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsStageEditor.angleStep = value;
|
||||
flush();
|
||||
return data.optionsStageEditor.angleStep;
|
||||
}
|
||||
|
||||
public var stageEditorTheme(get, set):StageEditorTheme;
|
||||
|
||||
function get_stageEditorTheme():StageEditorTheme
|
||||
{
|
||||
if (data.optionsStageEditor.theme == null) data.optionsStageEditor.theme = StageEditorTheme.Light;
|
||||
|
||||
return data.optionsStageEditor.theme;
|
||||
}
|
||||
|
||||
function set_stageEditorTheme(value:StageEditorTheme):StageEditorTheme
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsStageEditor.theme = value;
|
||||
flush();
|
||||
return data.optionsStageEditor.theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* When we've seen a character unlock, add it to the list of characters seen.
|
||||
* @param character
|
||||
@@ -543,22 +637,34 @@ class Save
|
||||
*
|
||||
* @param songId The ID of the song.
|
||||
* @param difficultyId The difficulty to check.
|
||||
* @param variation The variation to check. Defaults to empty string. Appended to difficulty with `-`, e.g. `easy-pico`.
|
||||
* @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved.
|
||||
*/
|
||||
public function getSongScore(songId:String, difficultyId:String = 'normal'):Null<SaveScoreData>
|
||||
public function getSongScore(songId:String, difficultyId:String = 'normal', ?variation:String):Null<SaveScoreData>
|
||||
{
|
||||
var song = data.scores.songs.get(songId);
|
||||
trace('Getting song score for $songId $difficultyId $variation');
|
||||
if (song == null)
|
||||
{
|
||||
trace('Could not find song data for $songId $difficultyId $variation');
|
||||
song = [];
|
||||
data.scores.songs.set(songId, song);
|
||||
}
|
||||
|
||||
// 'default' variations are left with no suffix ('easy', 'normal', 'hard'),
|
||||
// along with 'erect' variations ('erect', 'nightmare')
|
||||
// otherwise, we want to add a suffix of our current variation to get the save data.
|
||||
if (variation != null && variation != '' && variation != 'default' && variation != 'erect')
|
||||
{
|
||||
difficultyId = '${difficultyId}-${variation}';
|
||||
}
|
||||
|
||||
return song.get(difficultyId);
|
||||
}
|
||||
|
||||
public function getSongRank(songId:String, difficultyId:String = 'normal'):Null<ScoringRank>
|
||||
public function getSongRank(songId:String, difficultyId:String = 'normal', ?variation:String):Null<ScoringRank>
|
||||
{
|
||||
return Scoring.calculateRank(getSongScore(songId, difficultyId));
|
||||
return Scoring.calculateRank(getSongScore(songId, difficultyId, variation));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,18 +784,31 @@ class Save
|
||||
|
||||
/**
|
||||
* Has the provided song been beaten on one of the listed difficulties?
|
||||
* Note: This function can still take in the 'difficulty-variation' format for the difficultyList parameter
|
||||
* as it is used in the old save data format. However inputting a variation will append it to the difficulty
|
||||
* so you can do `hasBeatenSong('dadbattle', ['easy-pico'])` to check if you've beaten the Pico mix on easy.
|
||||
* or you can do `hasBeatenSong('dadbattle', ['easy'], 'pico')` to check if you've beaten the Pico mix on easy.
|
||||
* however you should not mix the two as it will append '-pico' to the 'easy-pico' if it's inputted into the array.
|
||||
* @param songId The song ID to check.
|
||||
* @param difficultyList The difficulties to check. Defaults to `easy`, `normal`, and `hard`.
|
||||
* @param variation The variation to check. Defaults to empty string. Appended to difficulty list with `-`, e.g. `easy-pico`.
|
||||
* This is our old format for getting difficulty/variation information, however we don't want to mess around with
|
||||
* save migration just yet.
|
||||
* @return Whether the song has been beaten on any of the listed difficulties.
|
||||
*/
|
||||
public function hasBeatenSong(songId:String, ?difficultyList:Array<String>):Bool
|
||||
public function hasBeatenSong(songId:String, ?difficultyList:Array<String>, ?variation:String):Bool
|
||||
{
|
||||
if (difficultyList == null)
|
||||
{
|
||||
difficultyList = ['easy', 'normal', 'hard'];
|
||||
}
|
||||
|
||||
if (variation == null) variation = '';
|
||||
|
||||
for (difficulty in difficultyList)
|
||||
{
|
||||
if (variation != '') difficulty = '${difficulty}-${variation}';
|
||||
|
||||
var score:Null<SaveScoreData> = getSongScore(songId, difficulty);
|
||||
if (score != null)
|
||||
{
|
||||
@@ -1043,6 +1162,11 @@ typedef RawSaveData =
|
||||
* The user's preferences specific to the Chart Editor.
|
||||
*/
|
||||
var optionsChartEditor:SaveDataChartEditorOptions;
|
||||
|
||||
/**
|
||||
* The user's preferences specific to the Stage Editor.
|
||||
*/
|
||||
var optionsStageEditor:SaveDataStageEditorOptions;
|
||||
};
|
||||
|
||||
typedef SaveApiData =
|
||||
@@ -1152,7 +1276,7 @@ typedef SaveDataOptions =
|
||||
var framerate:Int;
|
||||
|
||||
/**
|
||||
* Whether some particularly fowl language is displayed.
|
||||
* Whether some particularly foul language is displayed.
|
||||
* @default `true`
|
||||
*/
|
||||
var naughtyness:Bool;
|
||||
@@ -1188,20 +1312,20 @@ typedef SaveDataOptions =
|
||||
var autoPause:Bool;
|
||||
|
||||
/**
|
||||
* Offset the users inputs by this many ms.
|
||||
* Offset the user's inputs by this many ms.
|
||||
* @default `0`
|
||||
*/
|
||||
var inputOffset:Int;
|
||||
|
||||
/**
|
||||
* Affects the delay between the audio and the visuals during gameplay
|
||||
* Affects the delay between the audio and the visuals during gameplay.
|
||||
* @default `0`
|
||||
*/
|
||||
var audioVisualOffset:Int;
|
||||
|
||||
/**
|
||||
* If we want the framerate to be unlocked on HTML5.
|
||||
* @default `false
|
||||
* @default `false`
|
||||
*/
|
||||
var unlockedFramerate:Bool;
|
||||
|
||||
@@ -1416,3 +1540,39 @@ typedef SaveDataChartEditorOptions =
|
||||
*/
|
||||
var ?playbackSpeed:Float;
|
||||
};
|
||||
|
||||
typedef SaveDataStageEditorOptions =
|
||||
{
|
||||
// a lot of these things were copied from savedatacharteditoroptions
|
||||
|
||||
/**
|
||||
* Whether the Stage Editor created a backup the last time it closed.
|
||||
* Prompt the user to load it, then set this back to `false`.
|
||||
* @default `false`
|
||||
*/
|
||||
var ?hasBackup:Bool;
|
||||
|
||||
/**
|
||||
* Previous files opened in the Stage Editor.
|
||||
* @default `[]`
|
||||
*/
|
||||
var ?previousFiles:Array<String>;
|
||||
|
||||
/**
|
||||
* The Step at which an Object or Character is moved.
|
||||
* @default `1px`
|
||||
*/
|
||||
var ?moveStep:String;
|
||||
|
||||
/**
|
||||
* The Step at which an Object is rotated.
|
||||
* @default `5`
|
||||
*/
|
||||
var ?angleStep:Float;
|
||||
|
||||
/**
|
||||
* Theme in the Stage Editor.
|
||||
* @default `StageEditorTheme.Light`
|
||||
*/
|
||||
var ?theme:StageEditorTheme;
|
||||
};
|
||||
|
||||
@@ -175,6 +175,7 @@ class CharSelectSubState extends MusicBeatSubState
|
||||
|
||||
playerChillOut = new CharSelectPlayer(0, 0);
|
||||
playerChillOut.switchChar("bf");
|
||||
playerChillOut.visible = false;
|
||||
add(playerChillOut);
|
||||
|
||||
playerChill = new CharSelectPlayer(0, 0);
|
||||
@@ -543,6 +544,7 @@ class CharSelectSubState extends MusicBeatSubState
|
||||
|
||||
nametag.switchChar(char);
|
||||
gfChill.switchGF(char);
|
||||
gfChill.visible = true;
|
||||
|
||||
var icon = new PixelatedIcon(0, 0);
|
||||
icon.setCharacter(char);
|
||||
@@ -788,17 +790,18 @@ class CharSelectSubState extends MusicBeatSubState
|
||||
cursorY = -1;
|
||||
}
|
||||
|
||||
if (autoFollow
|
||||
&& availableChars.exists(getCurrentSelected())
|
||||
&& Save.instance.charactersSeen.contains(availableChars[getCurrentSelected()]))
|
||||
if (availableChars.exists(getCurrentSelected()) && Save.instance.charactersSeen.contains(availableChars[getCurrentSelected()]))
|
||||
{
|
||||
curChar = availableChars.get(getCurrentSelected());
|
||||
|
||||
if (allowInput && !pressedSelect && controls.ACCEPT)
|
||||
{
|
||||
spamUp = false;
|
||||
spamDown = false;
|
||||
spamLeft = false;
|
||||
spamRight = false;
|
||||
|
||||
cursorConfirmed.visible = true;
|
||||
cursorConfirmed.x = cursor.x - 2;
|
||||
cursorConfirmed.y = cursor.y - 4;
|
||||
cursorConfirmed.animation.play("idle", true);
|
||||
|
||||
grpCursors.visible = false;
|
||||
@@ -846,7 +849,7 @@ class CharSelectSubState extends MusicBeatSubState
|
||||
selectTimer.cancel();
|
||||
}
|
||||
}
|
||||
else if (autoFollow)
|
||||
else
|
||||
{
|
||||
curChar = "locked";
|
||||
|
||||
@@ -855,8 +858,6 @@ class CharSelectSubState extends MusicBeatSubState
|
||||
if (allowInput && controls.ACCEPT)
|
||||
{
|
||||
cursorDenied.visible = true;
|
||||
cursorDenied.x = cursor.x - 2;
|
||||
cursorDenied.y = cursor.y - 4;
|
||||
|
||||
playerChill.playAnimation("cannot select Label", true);
|
||||
|
||||
@@ -884,14 +885,20 @@ class CharSelectSubState extends MusicBeatSubState
|
||||
cursorLocIntended.x += cursorOffsetX;
|
||||
cursorLocIntended.y += cursorOffsetY;
|
||||
|
||||
cursor.x = MathUtil.coolLerp(cursor.x, cursorLocIntended.x, lerpAmnt);
|
||||
cursor.y = MathUtil.coolLerp(cursor.y, cursorLocIntended.y, lerpAmnt);
|
||||
cursor.x = MathUtil.smoothLerp(cursor.x, cursorLocIntended.x, elapsed, 0.1);
|
||||
cursor.y = MathUtil.smoothLerp(cursor.y, cursorLocIntended.y, elapsed, 0.1);
|
||||
|
||||
cursorBlue.x = MathUtil.coolLerp(cursorBlue.x, cursor.x, lerpAmnt * 0.4);
|
||||
cursorBlue.y = MathUtil.coolLerp(cursorBlue.y, cursor.y, lerpAmnt * 0.4);
|
||||
|
||||
cursorDarkBlue.x = MathUtil.coolLerp(cursorDarkBlue.x, cursorLocIntended.x, lerpAmnt * 0.2);
|
||||
cursorDarkBlue.y = MathUtil.coolLerp(cursorDarkBlue.y, cursorLocIntended.y, lerpAmnt * 0.2);
|
||||
|
||||
cursorConfirmed.x = cursor.x - 2;
|
||||
cursorConfirmed.y = cursor.y - 4;
|
||||
|
||||
cursorDenied.x = cursor.x - 2;
|
||||
cursorDenied.y = cursor.y - 4;
|
||||
}
|
||||
|
||||
var bopTimer:Float = 0;
|
||||
@@ -1056,18 +1063,21 @@ class CharSelectSubState extends MusicBeatSubState
|
||||
staticSound.stop();
|
||||
|
||||
nametag.switchChar(value);
|
||||
gfChill.visible = false;
|
||||
playerChill.visible = false;
|
||||
playerChillOut.visible = true;
|
||||
playerChillOut.playAnimation("slideout");
|
||||
var index = playerChillOut.anim.getFrameLabel("slideout").index;
|
||||
playerChillOut.onAnimationFrame.removeAll();
|
||||
playerChillOut.onAnimationFrame.add((_, frame:Int) -> {
|
||||
if (frame == index + 1)
|
||||
if (frame >= index + 1)
|
||||
{
|
||||
playerChill.visible = true;
|
||||
playerChill.switchChar(value);
|
||||
gfChill.switchGF(value);
|
||||
gfChill.visible = true;
|
||||
}
|
||||
if (frame == index + 2)
|
||||
if (frame >= index + 2)
|
||||
{
|
||||
playerChillOut.switchChar(value);
|
||||
playerChillOut.visible = false;
|
||||
|
||||
@@ -60,7 +60,7 @@ class DebugMenuSubState extends MusicBeatSubState
|
||||
// createItem("Input Offset Testing", openInputOffsetTesting);
|
||||
createItem("CHARACTER SELECT", openCharSelect, true);
|
||||
createItem("ANIMATION EDITOR", openAnimationEditor);
|
||||
// createItem("STAGE EDITOR", openStageEditor);
|
||||
createItem("STAGE EDITOR", openStageEditor);
|
||||
// createItem("TEST STICKERS", testStickers);
|
||||
#if sys
|
||||
createItem("OPEN CRASH LOG FOLDER", openLogFolder);
|
||||
@@ -125,6 +125,7 @@ class DebugMenuSubState extends MusicBeatSubState
|
||||
function openStageEditor()
|
||||
{
|
||||
trace('Stage Editor');
|
||||
FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState());
|
||||
}
|
||||
|
||||
#if sys
|
||||
|
||||
@@ -109,9 +109,11 @@ class DebugBoundingState extends FlxState
|
||||
offsetEditorDialog.cameras = [hudCam];
|
||||
|
||||
add(offsetEditorDialog);
|
||||
offsetEditorDialog.showDialog(false);
|
||||
|
||||
// Anchor to the right side by default
|
||||
// offsetEditorDialog.x = FlxG.width - offsetEditorDialog.width;
|
||||
// Anchor to the left side by default
|
||||
offsetEditorDialog.x = 16;
|
||||
offsetEditorDialog.y = 16;
|
||||
|
||||
// sets the default camera back to FlxG.camera, since we set it to hudCamera for haxeui stuf
|
||||
FlxG.cameras.setDefaultDrawTarget(FlxG.camera, true);
|
||||
|
||||
@@ -79,7 +79,6 @@ import funkin.ui.debug.charting.commands.SetItemSelectionCommand;
|
||||
import funkin.ui.debug.charting.components.ChartEditorEventSprite;
|
||||
import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
|
||||
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
|
||||
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
|
||||
import funkin.ui.debug.charting.components.ChartEditorNotePreview;
|
||||
import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
|
||||
import funkin.ui.debug.charting.components.ChartEditorPlaybarHead;
|
||||
@@ -5160,7 +5159,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2);
|
||||
var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2);
|
||||
if (songPos < 0) songPosMinutes = '-' + songPosMinutes;
|
||||
var songPosString:String = '${songPosMinutes}:${songPosSeconds}:${songPosMilliseconds}';
|
||||
var songPosString:String = '${songPosMinutes}:${songPosSeconds}.${songPosMilliseconds}';
|
||||
|
||||
if (playbarSongPos.value != songPosString) playbarSongPos.value = songPosString;
|
||||
|
||||
@@ -5655,7 +5654,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
function handleHelpKeybinds():Void
|
||||
{
|
||||
// F1 = Open Help
|
||||
if (FlxG.keys.justPressed.F1) this.openUserGuideDialog();
|
||||
if (FlxG.keys.justPressed.F1 && !isHaxeUIDialogOpen) {
|
||||
this.openUserGuideDialog();
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuickWatch():Void
|
||||
|
||||
@@ -199,7 +199,11 @@ class LatencyState extends MusicBeatSubState
|
||||
PreciseInputManager.instance.onInputPressed.remove(preciseInputPressed);
|
||||
PreciseInputManager.instance.onInputReleased.remove(preciseInputReleased);
|
||||
|
||||
FlxG.sound.music.volume = previousVolume;
|
||||
if (FlxG.sound.music != null)
|
||||
{
|
||||
FlxG.sound.music.volume = previousVolume;
|
||||
}
|
||||
|
||||
swagSong.stop();
|
||||
FlxG.sound.list.remove(swagSong);
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package funkin.ui.debug.stageeditor;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.graphics.shaders.InverseDotsShader;
|
||||
|
||||
/**
|
||||
* Contains all the Logic needed for Stage Editor. Only for Stage Editor, as in the gameplay StageProps and Boppers will be used.
|
||||
*/
|
||||
class StageEditorObject extends FunkinSprite
|
||||
{
|
||||
/**
|
||||
* The internal Name of the Object.
|
||||
*/
|
||||
public var name:String = "Unnamed";
|
||||
|
||||
public var selectedShader:InverseDotsShader;
|
||||
|
||||
/**
|
||||
* What animation to play upon starting.
|
||||
*/
|
||||
public var startingAnimation:String = "";
|
||||
|
||||
public var animDatas:Map<String, AnimationData> = [];
|
||||
|
||||
override public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
selectedShader = new InverseDotsShader(0);
|
||||
shader = selectedShader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Object is currently being modified in the Stage Editor.
|
||||
*/
|
||||
public var isDebugged(default, set):Bool = true;
|
||||
|
||||
function set_isDebugged(value:Bool):Bool
|
||||
{
|
||||
this.isDebugged = value;
|
||||
|
||||
if (value == false) // plays upon starting yippee!!!
|
||||
playAnim(startingAnimation, true);
|
||||
else
|
||||
{
|
||||
if (animation.curAnim != null)
|
||||
{
|
||||
animation.stop();
|
||||
offset.set();
|
||||
updateHitbox();
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public function playAnim(name:String, restart:Bool = false, reversed:Bool = false):Void
|
||||
{
|
||||
if (!animation.getNameList().contains(name)) return;
|
||||
|
||||
animation.play(name, restart, reversed, 0);
|
||||
|
||||
if (animDatas.exists(name)) offset.set(animDatas[name].offsets[0], animDatas[name].offsets[1]);
|
||||
else
|
||||
offset.set();
|
||||
}
|
||||
|
||||
/**
|
||||
* On which beat should it dance?
|
||||
*/
|
||||
public var danceEvery:Float = 0;
|
||||
|
||||
/**
|
||||
* Internal, handles danceLeft and danceRight.
|
||||
*/
|
||||
var _danced:Bool = true;
|
||||
|
||||
public function dance(restart:Bool = false):Void
|
||||
{
|
||||
if (isDebugged) return;
|
||||
|
||||
var idle = animation.getNameList().contains("idle");
|
||||
var dancing = animation.getNameList().contains("danceLeft") && animation.getNameList().contains("danceRight");
|
||||
|
||||
if (!idle && !dancing) return;
|
||||
|
||||
if (dancing)
|
||||
{
|
||||
if (_danced) playAnim("danceRight", restart);
|
||||
else
|
||||
playAnim("danceLeft", restart);
|
||||
|
||||
_danced = !_danced;
|
||||
}
|
||||
else if (idle)
|
||||
{
|
||||
playAnim("idle", restart);
|
||||
}
|
||||
}
|
||||
|
||||
public function addAnim(name:String, prefix:String, offsets:Array<Float>, indices:Array<Int>, frameRate:Int = 24, looped:Bool = true, flipX:Bool = false,
|
||||
flipY:Bool = false)
|
||||
{
|
||||
if (indices.length > 0) animation.addByIndices(name, prefix, indices, "", frameRate, looped, flipX, flipY);
|
||||
else
|
||||
animation.addByPrefix(name, prefix, frameRate, looped, flipX, flipY);
|
||||
|
||||
if (animation.getNameList().contains(name)) // sometimes the animation doesnt add
|
||||
{
|
||||
animDatas.set(name,
|
||||
{
|
||||
name: name,
|
||||
prefix: prefix,
|
||||
offsets: offsets,
|
||||
looped: looped,
|
||||
frameRate: frameRate,
|
||||
flipX: flipX,
|
||||
flipY: flipY,
|
||||
frameIndices: indices
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
package funkin.ui.debug.stageeditor.components;
|
||||
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/about.xml"))
|
||||
class AboutDialog extends Dialog {}
|
||||
@@ -0,0 +1,80 @@
|
||||
package funkin.ui.debug.stageeditor.components;
|
||||
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||
import funkin.util.FileUtil;
|
||||
import haxe.io.Path;
|
||||
import funkin.util.DateUtil;
|
||||
import funkin.util.WindowUtil;
|
||||
|
||||
using StringTools;
|
||||
|
||||
@:xml('
|
||||
<dialog id="backupAvailableDialog" width="475" height="150" title="Hey! Listen!">
|
||||
<vbox width="100%" height="100%">
|
||||
<label text="There is a chart backup available, would you like to open it?\n" width="100%" textAlign="center" />
|
||||
<spacer height="6" />
|
||||
<label id="backupTimeLabel" text="Jan 1, 1970 0:00" width="100%" textAlign="center" />
|
||||
<spacer height="100%" />
|
||||
<hbox width="100%">
|
||||
<button text="No Thanks" id="dialogCancel" />
|
||||
<spacer width="100%" />
|
||||
<button text="Take Me There" id="buttonGoToFolder" />
|
||||
<spacer width="100%" />
|
||||
<button text="Open It" id="buttonOpenBackup" />
|
||||
</hbox>
|
||||
</vbox>
|
||||
</dialog>
|
||||
')
|
||||
class BackupAvailableDialog extends Dialog
|
||||
{
|
||||
override public function new(state:StageEditorState, filePath:String)
|
||||
{
|
||||
super();
|
||||
|
||||
if (!FileUtil.doesFileExist(filePath)) return;
|
||||
|
||||
// time text
|
||||
var fileDate = Path.withoutExtension(Path.withoutDirectory(filePath));
|
||||
var dateParts = fileDate.split("-");
|
||||
|
||||
while (dateParts.length < 8)
|
||||
dateParts.push("0");
|
||||
|
||||
var year:Int = Std.parseInt(dateParts[2]) ?? 0; // copied parts from ChartEditorImportExportHandler.hx
|
||||
var month:Int = Std.parseInt(dateParts[3]) ?? 1;
|
||||
var day:Int = Std.parseInt(dateParts[4]) ?? 0;
|
||||
var hour:Int = Std.parseInt(dateParts[5]) ?? 0;
|
||||
var minute:Int = Std.parseInt(dateParts[6]) ?? 0;
|
||||
var second:Int = Std.parseInt(dateParts[7]) ?? 0;
|
||||
|
||||
backupTimeLabel.text = DateUtil.generateCleanTimestamp(new Date(year, month - 1, day, hour, minute, second));
|
||||
|
||||
// button callbacks
|
||||
dialogCancel.onClick = function(_) hideDialog(DialogButton.CANCEL);
|
||||
|
||||
buttonGoToFolder.onClick = function(_) {
|
||||
// :[
|
||||
#if sys
|
||||
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), StageEditorState.BACKUPS_PATH]);
|
||||
WindowUtil.openFolder(absoluteBackupsPath);
|
||||
#end
|
||||
}
|
||||
|
||||
buttonOpenBackup.onClick = function(_) {
|
||||
if (FileUtil.doesFileExist(filePath) && state.welcomeDialog != null) // doing a check in case a sleezy FUCK decides to delete the backup file AFTER dialog opens
|
||||
{
|
||||
state.welcomeDialog.loadFromFilePath(filePath);
|
||||
}
|
||||
hideDialog(DialogButton.APPLY);
|
||||
}
|
||||
|
||||
// uhhh
|
||||
onDialogClosed = function(event) {
|
||||
if (event.button == DialogButton.APPLY)
|
||||
{
|
||||
if (state.welcomeDialog != null) state.welcomeDialog.hideDialog(DialogButton.APPLY);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package funkin.ui.debug.stageeditor.components;
|
||||
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/exit-confirm.xml"))
|
||||
class ExitConfirmDialog extends Dialog
|
||||
{
|
||||
var onComplete:Void->Void = null;
|
||||
|
||||
override public function new(onComp:Void->Void)
|
||||
{
|
||||
super();
|
||||
|
||||
onComplete = onComp;
|
||||
|
||||
buttons = DialogButton.CANCEL | "{{Proceed}}";
|
||||
defaultButton = "{{Proceed}}";
|
||||
|
||||
destroyOnClose = true;
|
||||
}
|
||||
|
||||
public override function validateDialog(button:DialogButton, fn:Bool->Void)
|
||||
{
|
||||
if (button == "{{Proceed}}" && onComplete != null)
|
||||
{
|
||||
onComplete();
|
||||
}
|
||||
|
||||
fn(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package funkin.ui.debug.stageeditor.components;
|
||||
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
import funkin.play.stage.StageProp;
|
||||
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.components.CheckBox;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/find-object.xml"))
|
||||
class FindObjDialog extends Dialog
|
||||
{
|
||||
var stageEditorState:StageEditorState;
|
||||
|
||||
var assets:Array<StageEditorObject> = [];
|
||||
var curSelected:Int = 0;
|
||||
var field:TextField;
|
||||
|
||||
var checkWord:CheckBox;
|
||||
var checkCaps:CheckBox;
|
||||
|
||||
override public function new(state:StageEditorState, searchFor:String = "")
|
||||
{
|
||||
super();
|
||||
|
||||
stageEditorState = state;
|
||||
nameField.text = searchFor;
|
||||
|
||||
this.field = nameField;
|
||||
this.checkWord = wordCheck;
|
||||
this.checkCaps = capsCheck;
|
||||
|
||||
field.onChange = function(_) updateIndicator();
|
||||
indicator.hide();
|
||||
|
||||
top = 20;
|
||||
left = FlxG.width - width - 20;
|
||||
|
||||
buttons = DialogButton.CANCEL | "{{Find Next}}";
|
||||
defaultButton = "{{Find Next}}";
|
||||
}
|
||||
|
||||
public function updateIndicator()
|
||||
{
|
||||
var prevObjCheck = assets[curSelected];
|
||||
|
||||
assets = [];
|
||||
|
||||
for (ass in stageEditorState.spriteArray)
|
||||
{
|
||||
var name = ass.name;
|
||||
var checkFor = field.text;
|
||||
|
||||
if (!checkCaps.selected)
|
||||
{
|
||||
name = name.toLowerCase();
|
||||
checkFor = checkFor.toLowerCase();
|
||||
}
|
||||
|
||||
if (((name.contains(checkFor) && !checkWord.selected) || (name == checkFor && checkWord.selected)) && ass.visible) assets.push(ass);
|
||||
}
|
||||
|
||||
if (assets.length > 0 && prevObjCheck == null)
|
||||
{
|
||||
stageEditorState.selectedSprite = assets[0];
|
||||
}
|
||||
|
||||
if (assets.length > 0)
|
||||
{
|
||||
indicator.text = "Selected: " + (assets.indexOf(stageEditorState.selectedSprite) + 1) + " / " + assets.length;
|
||||
}
|
||||
else
|
||||
{
|
||||
indicator.text = "No Matches Found";
|
||||
}
|
||||
|
||||
if (field.text != "" && field.text != null) indicator.show();
|
||||
else
|
||||
indicator.hide();
|
||||
}
|
||||
|
||||
public override function validateDialog(button:DialogButton, fn:Bool->Void)
|
||||
{
|
||||
var done = true;
|
||||
|
||||
if (button == "{{Find Next}}")
|
||||
{
|
||||
done = false;
|
||||
|
||||
if (assets.length > 0)
|
||||
{
|
||||
curSelected = assets.indexOf(stageEditorState.selectedSprite);
|
||||
curSelected++;
|
||||
|
||||
if (curSelected >= assets.length) curSelected = 0;
|
||||
|
||||
stageEditorState.selectedSprite = assets[curSelected];
|
||||
indicator.text = "Selected: " + (assets.indexOf(stageEditorState.selectedSprite) + 1) + " / " + assets.length;
|
||||
|
||||
stageEditorState.camFollow.x = assets[curSelected].getMidpoint().x;
|
||||
stageEditorState.camFollow.y = assets[curSelected].getMidpoint().y;
|
||||
}
|
||||
}
|
||||
fn(done);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package funkin.ui.debug.stageeditor.components;
|
||||
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
import lime.utils.Bytes;
|
||||
import haxe.ui.components.TextField;
|
||||
import openfl.net.URLLoader;
|
||||
import openfl.net.URLRequest;
|
||||
import openfl.events.Event;
|
||||
import openfl.events.IOErrorEvent;
|
||||
import openfl.events.ProgressEvent;
|
||||
import openfl.events.SecurityErrorEvent;
|
||||
import openfl.utils.ByteArray;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/load-url.xml"))
|
||||
class LoadFromUrlDialog extends Dialog
|
||||
{
|
||||
var urlField:TextField;
|
||||
var loader:URLLoader;
|
||||
|
||||
override public function new(successCallback:Bytes->Void = null, failCallback:String->Void = null)
|
||||
{
|
||||
super();
|
||||
destroyOnClose = true;
|
||||
|
||||
loader = new URLLoader();
|
||||
loader.dataFormat = BINARY;
|
||||
|
||||
urlField.text = "";
|
||||
|
||||
loader.addEventListener(Event.COMPLETE, function(event:Event) {
|
||||
var bytes:Bytes = cast(loader.data, ByteArray);
|
||||
|
||||
if (successCallback != null) successCallback(bytes);
|
||||
|
||||
trace("loaded the image and did success callback");
|
||||
|
||||
@:privateAccess
|
||||
loader.__removeAllListeners();
|
||||
|
||||
hideDialog(DialogButton.CANCEL);
|
||||
});
|
||||
|
||||
loader.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent) {
|
||||
if (failCallback != null) failCallback(urlField.text);
|
||||
|
||||
trace("error with this shit");
|
||||
});
|
||||
|
||||
loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function(event:SecurityErrorEvent) {
|
||||
if (failCallback != null) failCallback(urlField.text);
|
||||
|
||||
trace("error with this shit");
|
||||
});
|
||||
|
||||
buttons = DialogButton.CANCEL | "{{Load}}";
|
||||
defaultButton = "{{Load}}";
|
||||
}
|
||||
|
||||
override public function validateDialog(button:DialogButton, fn:Bool->Void)
|
||||
{
|
||||
if (button == DialogButton.CANCEL)
|
||||
{
|
||||
fn(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
loader.load(new URLRequest(urlField.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package funkin.ui.debug.stageeditor.components;
|
||||
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
import haxe.ui.components.Link;
|
||||
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
|
||||
import funkin.save.Save;
|
||||
import funkin.util.FileUtil;
|
||||
import lime.ui.FileDialog;
|
||||
import flixel.FlxG;
|
||||
import openfl.display.BitmapData;
|
||||
import haxe.ui.notifications.NotificationType;
|
||||
import haxe.ui.notifications.NotificationManager;
|
||||
import funkin.play.stage.StageProp;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/new-object.xml"))
|
||||
class NewObjDialog extends Dialog
|
||||
{
|
||||
var stageEditorState:StageEditorState;
|
||||
var bitmap:BitmapData;
|
||||
|
||||
override public function new(state:StageEditorState, img:BitmapData = null)
|
||||
{
|
||||
super();
|
||||
|
||||
stageEditorState = state;
|
||||
bitmap = img;
|
||||
|
||||
field.onChange = function(_) {
|
||||
field.removeClasses(["invalid-value", "valid-value"]);
|
||||
}
|
||||
|
||||
buttons = DialogButton.CANCEL | "{{Create}}";
|
||||
defaultButton = "{{Create}}";
|
||||
|
||||
destroyOnClose = true;
|
||||
}
|
||||
|
||||
public override function validateDialog(button:DialogButton, fn:Bool->Void)
|
||||
{
|
||||
var done = true;
|
||||
|
||||
if (button == "{{Create}}")
|
||||
{
|
||||
var objNames = [for (obj in StageEditorState.instance.spriteArray) obj.name];
|
||||
|
||||
if (field.text == "" || field.text == null || objNames.contains(field.text))
|
||||
{
|
||||
field.swapClass("invalid-value", "valid-value");
|
||||
done = false;
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: "Problem Creating an Object",
|
||||
body: objNames.contains(field.text) ? "Object with the Name " + field.text + " already exists!" : "Invalid Object Name!",
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var spr = new StageEditorObject();
|
||||
|
||||
if (bitmap != null)
|
||||
{
|
||||
var bitToLoad = stageEditorState.addBitmap(bitmap);
|
||||
spr.loadGraphic(stageEditorState.bitmaps[bitToLoad]);
|
||||
}
|
||||
else
|
||||
spr.loadGraphic(AssetDataHandler.getDefaultGraphic());
|
||||
|
||||
spr.name = field.text;
|
||||
spr.screenCenter();
|
||||
spr.zIndex = 0;
|
||||
|
||||
stageEditorState.selectedSprite = spr;
|
||||
stageEditorState.createAndPushAction(OBJECT_CREATED);
|
||||
|
||||
stageEditorState.add(spr);
|
||||
stageEditorState.updateArray();
|
||||
stageEditorState.saved = false;
|
||||
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: "Object Creating Successful",
|
||||
body: "Successfully created an Object with the Name " + field.text + "!",
|
||||
type: NotificationType.Success
|
||||
});
|
||||
}
|
||||
}
|
||||
fn(done);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package funkin.ui.debug.stageeditor.components;
|
||||
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/user-guide.xml"))
|
||||
class UserGuideDialog extends Dialog {}
|
||||
@@ -0,0 +1,146 @@
|
||||
package funkin.ui.debug.stageeditor.components;
|
||||
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
|
||||
import haxe.ui.components.Link;
|
||||
import funkin.ui.debug.stageeditor.handlers.StageDataHandler;
|
||||
import funkin.save.Save;
|
||||
import funkin.util.FileUtil;
|
||||
import lime.ui.FileDialog;
|
||||
import flixel.FlxG;
|
||||
import funkin.input.Cursor;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.ui.debug.stageeditor.StageEditorState.StageEditorDialogType;
|
||||
|
||||
using funkin.util.tools.FloatTools;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/welcome.xml"))
|
||||
class WelcomeDialog extends Dialog
|
||||
{
|
||||
var stageEditorState:StageEditorState;
|
||||
|
||||
override public function new(state:StageEditorState)
|
||||
{
|
||||
super();
|
||||
|
||||
stageEditorState = state;
|
||||
|
||||
buttonNew.onClick = function(_) {
|
||||
stageEditorState.clearAssets();
|
||||
stageEditorState.loadDummyData();
|
||||
stageEditorState.currentFile = "";
|
||||
killDaDialog();
|
||||
}
|
||||
|
||||
for (file in Save.instance.stageEditorPreviousFiles)
|
||||
{
|
||||
trace(file);
|
||||
|
||||
if (!FileUtil.doesFileExist(file)) continue; // whats the point of loading something that doesnt exist
|
||||
|
||||
var patj = new haxe.io.Path(file);
|
||||
|
||||
var fileText = new Link();
|
||||
fileText.percentWidth = 100;
|
||||
fileText.text = patj.file + "." + patj.ext;
|
||||
fileText.onClick = function(_) loadFromFilePath(file);
|
||||
|
||||
#if sys
|
||||
var stat = sys.FileSystem.stat(file);
|
||||
var sizeInMB = (stat.size / 1000000).round(2);
|
||||
|
||||
fileText.tooltip = "Full Name: " + file + "\nLast Modified: " + stat.mtime.toString() + "\nSize: " + sizeInMB + "MB";
|
||||
#end
|
||||
|
||||
contentRecent.addComponent(fileText);
|
||||
}
|
||||
|
||||
boxDrag.onClick = function(_) FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], loadFromFilePath, null, null, "Open Stage Data");
|
||||
|
||||
var defaultStages = StageRegistry.instance.listBaseGameStageIds();
|
||||
defaultStages.sort(funkin.util.SortUtil.alphabetically);
|
||||
|
||||
for (stage in defaultStages)
|
||||
{
|
||||
var baseStage = StageRegistry.instance.parseEntryDataWithMigration(stage, StageRegistry.instance.fetchEntryVersion(stage));
|
||||
if (baseStage == null) continue;
|
||||
|
||||
var link = new Link(); // this is how the legend of zelda started btw
|
||||
link.percentWidth = 100;
|
||||
link.text = baseStage.name;
|
||||
|
||||
link.onClick = function(_) loadFromPreset(baseStage);
|
||||
|
||||
contentPresets.addComponent(link);
|
||||
}
|
||||
|
||||
FlxG.stage.window.onDropFile.add(loadFromFilePath);
|
||||
}
|
||||
|
||||
public function loadFromPreset(data:StageData)
|
||||
{
|
||||
if (data == null) return;
|
||||
|
||||
if (!stageEditorState.saved)
|
||||
{
|
||||
Dialogs.messageBox("This will destroy all of your Unsaved Work.\n\nAre you sure? This cannot be undone.", "Load Stage", MessageBoxType.TYPE_YESNO, true,
|
||||
function(btn:DialogButton) {
|
||||
if (btn == DialogButton.YES)
|
||||
{
|
||||
stageEditorState.saved = true;
|
||||
loadFromPreset(data);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
stageEditorState.clearAssets();
|
||||
stageEditorState.currentFile = "";
|
||||
stageEditorState.loadFromDataRaw(data);
|
||||
killDaDialog();
|
||||
}
|
||||
|
||||
public function loadFromFilePath(file:String)
|
||||
{
|
||||
if (!stageEditorState.saved)
|
||||
{
|
||||
Dialogs.messageBox("This will destroy all of your Unsaved Work.\n\nAre you sure? This cannot be undone.", "Load Stage", MessageBoxType.TYPE_YESNO, true,
|
||||
function(btn:DialogButton) {
|
||||
if (btn == DialogButton.YES)
|
||||
{
|
||||
stageEditorState.saved = true;
|
||||
loadFromFilePath(file);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = FileUtil.readBytesFromPath(file);
|
||||
|
||||
if (bytes == null)
|
||||
{
|
||||
stageEditorState.notifyChange("Problem Loading the Stage", "The Stage File could not be loaded.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
stageEditorState.clearAssets();
|
||||
stageEditorState.currentFile = file;
|
||||
stageEditorState.unpackShitFromZip(bytes);
|
||||
killDaDialog();
|
||||
}
|
||||
|
||||
function killDaDialog()
|
||||
{
|
||||
stageEditorState.updateDialog(StageEditorDialogType.OBJECT);
|
||||
stageEditorState.updateDialog(StageEditorDialogType.CHARACTER);
|
||||
stageEditorState.updateDialog(StageEditorDialogType.STAGE);
|
||||
|
||||
FlxG.stage.window.onDropFile.remove(loadFromFilePath);
|
||||
hide();
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package funkin.ui.debug.stageeditor.handlers;
|
||||
|
||||
import flixel.FlxG;
|
||||
import openfl.display.BitmapData;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.math.FlxRect;
|
||||
import openfl.display.BlendMode;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.data.stage.StageData.StageDataProp;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* Handles the Stage Props and Datas - being able to convert one to the other.
|
||||
*/
|
||||
class AssetDataHandler
|
||||
{
|
||||
static var state:StageEditorState;
|
||||
|
||||
public static function init(state:StageEditorState)
|
||||
{
|
||||
AssetDataHandler.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns an Object into Data.
|
||||
* @param obj the Object whose data to read.
|
||||
* @param useBitmaps Whether to Save object's BitmapData directly.
|
||||
* @return Data of the Object
|
||||
*/
|
||||
public static function toData(obj:StageEditorObject, useBitmaps:Bool = false):StageEditorObjectData
|
||||
{
|
||||
var outputData:StageEditorObjectData =
|
||||
{
|
||||
name: obj.name,
|
||||
assetPath: "",
|
||||
position: [obj.x, obj.y],
|
||||
zIndex: obj.zIndex,
|
||||
isPixel: !obj.antialiasing,
|
||||
scale: obj.scale.x == obj.scale.y ? Left(obj.scale.x) : Right([obj.scale.x, obj.scale.y]),
|
||||
alpha: obj.alpha,
|
||||
danceEvery: obj.animation.getNameList().length > 0 ? obj.danceEvery : 0,
|
||||
scroll: [obj.scrollFactor.x, obj.scrollFactor.y],
|
||||
animations: [for (n => d in obj.animDatas) d],
|
||||
startingAnimation: obj.startingAnimation,
|
||||
animType: "sparrow", // automatically making sparrow atlases yeah
|
||||
angle: obj.angle,
|
||||
blend: obj.blend == null ? "" : Std.string(obj.blend),
|
||||
color: obj.color.toWebString(),
|
||||
xmlData: obj.generateXML()
|
||||
}
|
||||
|
||||
if (useBitmaps)
|
||||
{
|
||||
outputData.bitmap = obj.pixels.clone();
|
||||
return outputData;
|
||||
}
|
||||
|
||||
for (name => bit in state.bitmaps)
|
||||
{
|
||||
if (areTheseBitmapsEqual(bit, obj.pixels))
|
||||
{
|
||||
outputData.assetPath = name;
|
||||
return outputData;
|
||||
}
|
||||
}
|
||||
|
||||
outputData.assetPath = "#FFFFFF";
|
||||
|
||||
return outputData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies an Object based on the Data.
|
||||
* @param object Object to modify. Set to null to create a new one.
|
||||
* @param data The Data used for the Object.
|
||||
*/
|
||||
public static function fromData(object:StageEditorObject, data:StageEditorObjectData)
|
||||
{
|
||||
if (data.bitmap != null)
|
||||
{
|
||||
var bitToLoad = state.addBitmap(data.bitmap.clone());
|
||||
object.loadGraphic(state.bitmaps[bitToLoad]);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (data.animations != null && data.animations.length > 0) // considering we're unpacking we might as well just do this instead of switch
|
||||
{
|
||||
object.frames = flixel.graphics.frames.FlxAtlasFrames.fromSparrow(state.bitmaps[data.assetPath].clone(), data.xmlData);
|
||||
}
|
||||
else if (data.assetPath.startsWith("#"))
|
||||
{
|
||||
object.loadGraphic(getDefaultGraphic());
|
||||
object.color = FlxColor.fromString(data.assetPath);
|
||||
}
|
||||
else
|
||||
object.loadGraphic(state.bitmaps[data.assetPath].clone());
|
||||
}
|
||||
|
||||
object.name = data.name;
|
||||
object.setPosition(data.position[0], data.position[1]);
|
||||
object.zIndex = data.zIndex;
|
||||
object.antialiasing = !data.isPixel;
|
||||
object.alpha = data.alpha;
|
||||
object.danceEvery = data.danceEvery;
|
||||
object.scrollFactor.set(data.scroll[0], data.scroll[1]);
|
||||
object.startingAnimation = data.startingAnimation;
|
||||
object.angle = data.angle;
|
||||
object.blend = blendFromString(data.blend);
|
||||
if (!data.assetPath.startsWith("#")) object.color = FlxColor.fromString(data.color);
|
||||
|
||||
// yeah
|
||||
object.pixelPerfectRender = data.isPixel;
|
||||
object.pixelPerfectPosition = data.isPixel;
|
||||
|
||||
for (anim in data.animations)
|
||||
{
|
||||
object.addAnim(anim.name, anim.prefix, anim.offsets ?? [0, 0], anim.frameIndices ?? [], anim.frameRate ?? 24, anim.looped ?? false, anim.flipX ?? false,
|
||||
anim.flipY ?? false);
|
||||
}
|
||||
|
||||
if (object.animation.getNameList().contains(data.startingAnimation)) object.startingAnimation = data.startingAnimation;
|
||||
|
||||
switch (data.scale)
|
||||
{
|
||||
case Left(value):
|
||||
object.scale.set(value, value);
|
||||
|
||||
case Right(values):
|
||||
object.scale.set(values[0], values[1]);
|
||||
}
|
||||
object.updateHitbox();
|
||||
|
||||
object.playAnim(object.startingAnimation);
|
||||
|
||||
flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
|
||||
if (object != null && object.animation.curAnim != null) object.animation.stop();
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a default BitmapData to be used for all the props.
|
||||
* @return BitmapData
|
||||
*/
|
||||
public static function getDefaultGraphic():BitmapData
|
||||
{
|
||||
return new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE).pixels.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns OpenFL's BlendMode based on the Name.
|
||||
* @param blend the BlendMode Name.
|
||||
* @return BlendMode
|
||||
*/
|
||||
public static function blendFromString(blend:String):BlendMode
|
||||
{
|
||||
// originally this was a MASSIVE and I do mean MASSIVE switch case, though then I found out that blendmode already has one implemented
|
||||
@:privateAccess
|
||||
return BlendMode.fromString(blend.toLowerCase().trim());
|
||||
}
|
||||
|
||||
public static function generateXML(obj:StageEditorObject)
|
||||
{
|
||||
// the last check is for if the only frame is the standard graphic frame
|
||||
if (obj == null || obj.frames.frames.length == 0 || obj.frames.frames[0].name == null) return "";
|
||||
|
||||
var xml = [
|
||||
"<!--This XML File was automatically generated by StageEditorEngine, in order to make Funkin' be able to load it.-->",
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<TextureAtlas imagePath="${obj.toData(false).assetPath}.png" width="${obj.pixels.width}" height="${obj.pixels.height}">'
|
||||
].join("\n");
|
||||
|
||||
for (daFrame in obj.frames.frames)
|
||||
{
|
||||
xml += ' <SubTexture name="${daFrame.name}" x="${daFrame.frame.x}" y="${daFrame.frame.y}" width="${daFrame.frame.width}" height="${daFrame.frame.height}" frameX="${- daFrame.offset.x}" frameY="${- daFrame.offset.y}" frameWidth="${daFrame.sourceSize.x}" frameHeight="${daFrame.sourceSize.y}" flipX="${daFrame.flipX}" flipY="${daFrame.flipY}"/>\n';
|
||||
}
|
||||
|
||||
xml += "</TextureAtlas>";
|
||||
return xml;
|
||||
}
|
||||
|
||||
// I am aware OpenFL has it's own compare bitmap function, though I find this to be better ngl
|
||||
static function areTheseBitmapsEqual(bitmap1:BitmapData, bitmap2:BitmapData)
|
||||
{
|
||||
if (bitmap1.width != bitmap2.width || bitmap1.height != bitmap2.height) return false;
|
||||
|
||||
for (px in 0...bitmap1.width)
|
||||
{
|
||||
for (py in 0...bitmap1.height)
|
||||
{
|
||||
if (bitmap1.getPixel32(px, py) != bitmap2.getPixel32(px, py)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
typedef StageEditorObjectData =
|
||||
{
|
||||
> StageDataProp,
|
||||
var xmlData:String;
|
||||
var ?bitmap:BitmapData;
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package funkin.ui.debug.stageeditor.handlers;
|
||||
|
||||
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
|
||||
import haxe.io.Bytes;
|
||||
import funkin.util.FileUtil;
|
||||
import openfl.display.BitmapData;
|
||||
import haxe.Json;
|
||||
import haxe.zip.Entry;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.data.stage.StageData.StageDataCharacter;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import openfl.utils.Assets as OpenFLAssets;
|
||||
import lime.utils.Assets as LimeAssets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class StageDataHandler
|
||||
{
|
||||
public static function checkForCharacter(char:BaseCharacter)
|
||||
return char != null;
|
||||
|
||||
public static function packShitToZip(state:StageEditorState)
|
||||
{
|
||||
// step 1: data
|
||||
var endData:StageData = new StageData();
|
||||
endData.name = state.stageName;
|
||||
endData.cameraZoom = state.stageZoom;
|
||||
endData.directory = state.stageFolder;
|
||||
|
||||
// step 1 phase 1: object data
|
||||
var xmlMap:Map<String, String> = [];
|
||||
|
||||
for (obj in state.spriteArray)
|
||||
{
|
||||
var data = obj.toData(false);
|
||||
endData.props.push(
|
||||
{
|
||||
name: data.name,
|
||||
assetPath: data.assetPath.startsWith("#") ? data.color : data.assetPath,
|
||||
position: data.position.copy(),
|
||||
zIndex: data.zIndex,
|
||||
isPixel: data.isPixel,
|
||||
scale: data.scale,
|
||||
alpha: data.alpha,
|
||||
danceEvery: data.danceEvery,
|
||||
scroll: data.scroll.copy(),
|
||||
animations: data.animations,
|
||||
startingAnimation: data.startingAnimation,
|
||||
animType: data.animType,
|
||||
angle: data.angle,
|
||||
blend: data.blend,
|
||||
color: data.assetPath.startsWith("#") ? "#FFFFFF" : data.color
|
||||
});
|
||||
|
||||
if (!xmlMap.exists(data.assetPath) && data.xmlData != "") xmlMap.set(data.assetPath, data.xmlData);
|
||||
}
|
||||
|
||||
// step 1 phase 2: character data
|
||||
endData.characters.bf.zIndex = state.charGroups[CharacterType.BF].zIndex;
|
||||
endData.characters.dad.zIndex = state.charGroups[CharacterType.DAD].zIndex;
|
||||
endData.characters.gf.zIndex = state.charGroups[CharacterType.GF].zIndex;
|
||||
|
||||
endData.characters.bf.scale = state.bf.scale.x / state.bf.getBaseScale();
|
||||
endData.characters.dad.scale = state.dad.scale.x / state.dad.getBaseScale();
|
||||
endData.characters.gf.scale = state.gf.scale.x / state.gf.getBaseScale();
|
||||
|
||||
endData.characters.bf.cameraOffsets = state.charCamOffsets[CharacterType.BF].copy();
|
||||
endData.characters.gf.cameraOffsets = state.charCamOffsets[CharacterType.GF].copy();
|
||||
endData.characters.dad.cameraOffsets = state.charCamOffsets[CharacterType.DAD].copy();
|
||||
|
||||
endData.characters.bf.position = [
|
||||
state.bf.feetPosition.x - state.bf.globalOffsets[0],
|
||||
state.bf.feetPosition.y - state.bf.globalOffsets[1]
|
||||
];
|
||||
|
||||
endData.characters.gf.position = [
|
||||
state.gf.feetPosition.x - state.gf.globalOffsets[0],
|
||||
state.gf.feetPosition.y - state.gf.globalOffsets[1]
|
||||
];
|
||||
|
||||
endData.characters.dad.position = [
|
||||
state.dad.feetPosition.x - state.dad.globalOffsets[0],
|
||||
state.dad.feetPosition.y - state.dad.globalOffsets[1]
|
||||
];
|
||||
|
||||
// step 2: saving everything to entryList
|
||||
var entryList = new Array<Entry>();
|
||||
|
||||
// step 2 phase 1: images
|
||||
state.removeUnusedBitmaps();
|
||||
for (name => img in state.bitmaps)
|
||||
{
|
||||
var bytes = img?.image?.encode(PNG);
|
||||
if (bytes == null) continue;
|
||||
|
||||
var entry:Entry =
|
||||
{
|
||||
fileName: name + ".png",
|
||||
fileSize: bytes.length,
|
||||
fileTime: Date.now(),
|
||||
compressed: false,
|
||||
dataSize: bytes.length,
|
||||
data: bytes,
|
||||
crc32: null // apparently fileutil.hx does not like crc32, idk why but i dont even know what crc32 is
|
||||
}
|
||||
|
||||
entryList.push(entry);
|
||||
}
|
||||
|
||||
// step 2 phase 2: xmls
|
||||
for (obj in endData.props)
|
||||
{
|
||||
if (!xmlMap.exists(obj.assetPath)) continue; // damn
|
||||
|
||||
var bytes = Bytes.ofString(xmlMap[obj.assetPath]);
|
||||
|
||||
var entry:Entry =
|
||||
{
|
||||
fileName: obj.assetPath + ".xml",
|
||||
fileSize: bytes.length,
|
||||
fileTime: Date.now(),
|
||||
compressed: false,
|
||||
dataSize: bytes.length,
|
||||
data: bytes,
|
||||
crc32: null
|
||||
}
|
||||
|
||||
entryList.push(entry);
|
||||
}
|
||||
|
||||
// step 2 phase 3: the main data
|
||||
var stageBytes = Bytes.ofString(endData.serialize());
|
||||
entryList.push(
|
||||
{
|
||||
fileName: "yourstagename.json",
|
||||
fileSize: stageBytes.length,
|
||||
fileTime: Date.now(),
|
||||
compressed: false,
|
||||
dataSize: stageBytes.length,
|
||||
data: stageBytes,
|
||||
crc32: null
|
||||
});
|
||||
|
||||
var zipFileBytes = FileUtil.createZIPFromEntries(entryList);
|
||||
return zipFileBytes;
|
||||
}
|
||||
|
||||
public static function unpackShitFromZip(state:StageEditorState, zip:Bytes)
|
||||
{
|
||||
state.clearAssets();
|
||||
state.bitmaps.clear();
|
||||
|
||||
var entries = FileUtil.readZIPFromBytes(zip);
|
||||
var stageData:StageData = new StageData();
|
||||
|
||||
var xmls:Map<String, String> = [];
|
||||
|
||||
for (stuff in entries)
|
||||
{
|
||||
var ext = stuff.fileName.split(".")[1];
|
||||
|
||||
switch (ext)
|
||||
{
|
||||
case "png":
|
||||
var data = BitmapData.fromBytes(stuff.data);
|
||||
state.bitmaps.set(stuff.fileName.replace(".png", ""), data);
|
||||
|
||||
case "xml":
|
||||
xmls.set(stuff.fileName.replace(".xml", ""), stuff.data.toString());
|
||||
|
||||
case "json":
|
||||
stageData = StageRegistry.instance.parseEntryDataRaw(stuff.data.toString(), stuff.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
if (stageData == null)
|
||||
{
|
||||
// TODO: throw an error, then load a dummy data
|
||||
loadDummyData(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// actual data unpacking
|
||||
state.stageName = stageData.name;
|
||||
state.stageZoom = stageData.cameraZoom;
|
||||
state.stageFolder = stageData.directory ?? "shared";
|
||||
|
||||
// chars
|
||||
state.loadCharDatas(stageData);
|
||||
|
||||
// objects
|
||||
for (objData in stageData.props)
|
||||
{
|
||||
// make the data and roll with it
|
||||
var spr = new StageEditorObject();
|
||||
spr.fromData(
|
||||
{
|
||||
name: objData.name ?? "Unnamed",
|
||||
assetPath: objData.assetPath,
|
||||
animations: objData.animations.copy(),
|
||||
scale: objData.scale,
|
||||
position: objData.position,
|
||||
alpha: objData.alpha,
|
||||
angle: objData.angle,
|
||||
zIndex: objData.zIndex,
|
||||
danceEvery: objData.danceEvery,
|
||||
isPixel: objData.isPixel,
|
||||
scroll: objData.scroll.copy(),
|
||||
color: objData.color,
|
||||
blend: objData.blend,
|
||||
startingAnimation: objData.startingAnimation,
|
||||
xmlData: xmls[objData.assetPath] ?? ""
|
||||
});
|
||||
|
||||
state.add(spr);
|
||||
}
|
||||
|
||||
state.updateArray();
|
||||
state.sortAssets();
|
||||
state.updateMarkerPos();
|
||||
}
|
||||
|
||||
static function loadCharDatas(state:StageEditorState, data:StageData)
|
||||
{
|
||||
var chars = state.getCharacters();
|
||||
for (char in chars)
|
||||
{
|
||||
var charData:StageDataCharacter = null;
|
||||
|
||||
switch (char.characterType)
|
||||
{
|
||||
case CharacterType.BF:
|
||||
charData = data.characters.bf;
|
||||
case CharacterType.GF:
|
||||
charData = data.characters.gf;
|
||||
case CharacterType.DAD:
|
||||
charData = data.characters.dad;
|
||||
default: // nothing rip
|
||||
}
|
||||
|
||||
char.resetCharacter(true);
|
||||
|
||||
if (charData == null) continue;
|
||||
|
||||
char.x = charData.position[0] - char.characterOrigin.x + char.globalOffsets[0];
|
||||
char.y = charData.position[1] - char.characterOrigin.y + char.globalOffsets[1];
|
||||
state.charGroups[char.characterType].zIndex = charData.zIndex;
|
||||
|
||||
char.setScale(char.getBaseScale() * charData.scale);
|
||||
char.cameraFocusPoint.x += charData.cameraOffsets[0];
|
||||
char.cameraFocusPoint.y += charData.cameraOffsets[1];
|
||||
|
||||
state.charCamOffsets[char.characterType] = charData.cameraOffsets.copy();
|
||||
}
|
||||
}
|
||||
|
||||
public static function loadFromDataRaw(state:StageEditorState, data:StageData)
|
||||
{
|
||||
state.clearAssets();
|
||||
state.bitmaps.clear();
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
loadDummyData(state);
|
||||
return;
|
||||
}
|
||||
@:privateAccess
|
||||
if (!LimeAssets.libraryPaths.exists(data.directory))
|
||||
{
|
||||
loadDummyData(state);
|
||||
return;
|
||||
}
|
||||
|
||||
Paths.setCurrentLevel(data.directory);
|
||||
|
||||
if (OpenFLAssets.getLibrary(data.directory) == null)
|
||||
{
|
||||
OpenFLAssets.loadLibrary(data.directory).onComplete(function(_) {
|
||||
loadFromDataRaw(state, data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
state.stageName = data.name;
|
||||
state.stageZoom = data.cameraZoom;
|
||||
state.stageFolder = data.directory ?? "shared";
|
||||
|
||||
state.loadCharDatas(data);
|
||||
|
||||
for (objData in data.props)
|
||||
{
|
||||
var spr = new StageEditorObject();
|
||||
if (!objData.assetPath.startsWith("#")) state.bitmaps.set(objData.assetPath, Assets.getBitmapData(Paths.image(objData.assetPath)));
|
||||
|
||||
spr.fromData(
|
||||
{
|
||||
name: objData.name ?? "Unnamed",
|
||||
assetPath: objData.assetPath,
|
||||
animations: objData.animations.copy(),
|
||||
scale: objData.scale,
|
||||
position: objData.position,
|
||||
alpha: objData.alpha,
|
||||
angle: objData.angle,
|
||||
zIndex: objData.zIndex,
|
||||
danceEvery: objData.danceEvery,
|
||||
isPixel: objData.isPixel,
|
||||
scroll: objData.scroll.copy(),
|
||||
color: objData.color,
|
||||
blend: objData.blend,
|
||||
startingAnimation: objData.startingAnimation,
|
||||
xmlData: Assets.exists(Paths.file("images/" + objData.assetPath + ".xml")) ? Assets.getText(Paths.file("images/" + objData.assetPath + ".xml")) : ""
|
||||
});
|
||||
|
||||
state.add(spr);
|
||||
}
|
||||
|
||||
state.updateArray();
|
||||
state.sortAssets();
|
||||
state.updateMarkerPos();
|
||||
}
|
||||
|
||||
public static function loadDummyData(state:StageEditorState)
|
||||
{
|
||||
state.clearAssets();
|
||||
|
||||
state.stageName = "Unnamed";
|
||||
state.stageZoom = 1.0;
|
||||
state.stageFolder = "shared";
|
||||
|
||||
state.charCamOffsets = StageEditorState.DEFAULT_CAMERA_OFFSETS.copy();
|
||||
state.charPos = StageEditorState.DEFAULT_POSITIONS.copy();
|
||||
|
||||
state.gf.resetCharacter(true);
|
||||
state.dad.resetCharacter(true);
|
||||
state.bf.resetCharacter(true);
|
||||
|
||||
state.charGroups[CharacterType.BF].zIndex = 300;
|
||||
state.charGroups[CharacterType.DAD].zIndex = 200;
|
||||
state.charGroups[CharacterType.GF].zIndex = 100;
|
||||
|
||||
state.gf.x = state.charPos[CharacterType.GF][0] - state.gf.characterOrigin.x + state.gf.globalOffsets[0];
|
||||
state.gf.y = state.charPos[CharacterType.GF][1] - state.gf.characterOrigin.y + state.gf.globalOffsets[1];
|
||||
state.dad.x = state.charPos[CharacterType.DAD][0] - state.dad.characterOrigin.x + state.dad.globalOffsets[0];
|
||||
state.dad.y = state.charPos[CharacterType.DAD][1] - state.dad.characterOrigin.y + state.dad.globalOffsets[1];
|
||||
state.bf.x = state.charPos[CharacterType.BF][0] - state.bf.characterOrigin.x + state.bf.globalOffsets[0];
|
||||
state.bf.y = state.charPos[CharacterType.BF][1] - state.bf.characterOrigin.y + state.bf.globalOffsets[1];
|
||||
|
||||
state.gf.setScale(state.gf.getBaseScale());
|
||||
state.dad.setScale(state.dad.getBaseScale());
|
||||
state.bf.setScale(state.bf.getBaseScale());
|
||||
|
||||
state.gf.cameraFocusPoint.x += state.charCamOffsets[CharacterType.GF][0];
|
||||
state.gf.cameraFocusPoint.y += state.charCamOffsets[CharacterType.GF][1];
|
||||
state.dad.cameraFocusPoint.x += state.charCamOffsets[CharacterType.DAD][0];
|
||||
state.dad.cameraFocusPoint.y += state.charCamOffsets[CharacterType.DAD][1];
|
||||
state.bf.cameraFocusPoint.x += state.charCamOffsets[CharacterType.BF][0];
|
||||
state.bf.cameraFocusPoint.y += state.charCamOffsets[CharacterType.BF][1];
|
||||
|
||||
// no props :p
|
||||
|
||||
state.updateMarkerPos();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package funkin.ui.debug.stageeditor.handlers;
|
||||
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler.StageEditorObjectData;
|
||||
import funkin.ui.debug.stageeditor.StageEditorState.StageEditorDialogType;
|
||||
|
||||
class UndoRedoHandler
|
||||
{
|
||||
public static function performLastAction(state:StageEditorState, redo:Bool = false):Void
|
||||
{
|
||||
if (state == null || (state.undoArray.length <= 0 && !redo) || (state.redoArray.length <= 0 && redo)) return;
|
||||
var actionToDo = redo ? state.redoArray.pop() : state.undoArray.pop();
|
||||
|
||||
switch (actionToDo.type)
|
||||
{
|
||||
case CHARACTER_MOVED:
|
||||
createAndPushAction(state, actionToDo.type, !redo);
|
||||
|
||||
var type = actionToDo.data.type == null ? CharacterType.BF : actionToDo.data.type;
|
||||
var pos = actionToDo.data.pos == null ? [0, 0] : actionToDo.data.pos;
|
||||
|
||||
for (char in state.getCharacters())
|
||||
{
|
||||
if (char.characterType == type) state.selectedChar = char;
|
||||
}
|
||||
|
||||
state.selectedChar.x = pos[0] - state.selectedChar.characterOrigin.x + state.selectedChar.globalOffsets[0];
|
||||
state.selectedChar.y = pos[1] - state.selectedChar.characterOrigin.y + state.selectedChar.globalOffsets[1];
|
||||
|
||||
state.updateMarkerPos();
|
||||
state.updateDialog(StageEditorDialogType.CHARACTER);
|
||||
|
||||
case OBJECT_MOVED:
|
||||
var id = actionToDo.data.ID ?? -1;
|
||||
var pos = actionToDo.data.pos ?? [0, 0];
|
||||
|
||||
for (obj in state.spriteArray)
|
||||
{
|
||||
if (obj.ID == id) state.selectedSprite = obj;
|
||||
}
|
||||
|
||||
if (state.selectedSprite != null)
|
||||
{
|
||||
createAndPushAction(state, actionToDo.type, !redo);
|
||||
|
||||
state.selectedSprite.x = pos[0];
|
||||
state.selectedSprite.y = pos[1];
|
||||
|
||||
state.updateDialog(StageEditorDialogType.OBJECT);
|
||||
}
|
||||
|
||||
case OBJECT_CREATED: // this removes the object
|
||||
var id = actionToDo.data.ID ?? -1;
|
||||
|
||||
for (obj in state.spriteArray)
|
||||
{
|
||||
if (obj.ID == id)
|
||||
{
|
||||
state.selectedSprite = obj;
|
||||
createAndPushAction(state, OBJECT_DELETED, !redo);
|
||||
|
||||
state.selectedSprite = null;
|
||||
|
||||
obj.kill();
|
||||
state.remove(obj, true);
|
||||
obj.destroy();
|
||||
|
||||
state.updateArray();
|
||||
state.updateDialog(StageEditorDialogType.OBJECT);
|
||||
trace("found object");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
case OBJECT_DELETED: // this creates the object
|
||||
if (actionToDo.data.data == null) return;
|
||||
|
||||
var id = actionToDo.data.ID ?? -1;
|
||||
var data:StageEditorObjectData = cast actionToDo.data.data;
|
||||
|
||||
var obj = new StageEditorObject().fromData(data);
|
||||
obj.ID = id;
|
||||
state.selectedSprite = obj;
|
||||
|
||||
createAndPushAction(state, OBJECT_CREATED, !redo);
|
||||
state.add(obj);
|
||||
|
||||
state.updateDialog(StageEditorDialogType.OBJECT);
|
||||
state.updateArray();
|
||||
|
||||
case OBJECT_ROTATED: // primarily copied from OBJECT_MOVED
|
||||
var id = actionToDo.data.ID ?? -1;
|
||||
var angle = actionToDo.data.angle ?? 0;
|
||||
|
||||
for (obj in state.spriteArray)
|
||||
{
|
||||
if (obj.ID == id) state.selectedSprite = obj;
|
||||
}
|
||||
|
||||
if (state.selectedSprite != null)
|
||||
{
|
||||
createAndPushAction(state, actionToDo.type, !redo);
|
||||
state.selectedSprite.angle = angle;
|
||||
state.updateDialog(StageEditorDialogType.OBJECT);
|
||||
}
|
||||
|
||||
default: // do nothing dumbass
|
||||
}
|
||||
}
|
||||
|
||||
public static function createAndPushAction(state:StageEditorState, action:UndoActionType, redo:Bool = false)
|
||||
{
|
||||
if (state == null) return;
|
||||
|
||||
var finalAction:UndoAction = {type: action, data: null};
|
||||
|
||||
if (!redo && state.redoArray.length > 0) state.redoArray = []; // incorporate resetting as well
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case CHARACTER_MOVED:
|
||||
var char = state.selectedChar.characterType;
|
||||
finalAction.data = {type: char, pos: state.charPos[char].copy()};
|
||||
|
||||
case OBJECT_MOVED:
|
||||
finalAction.data = {ID: state.selectedSprite.ID, pos: [state.selectedSprite.x, state.selectedSprite.y]}
|
||||
|
||||
case OBJECT_CREATED:
|
||||
finalAction.data = {ID: state.selectedSprite.ID}
|
||||
|
||||
case OBJECT_DELETED:
|
||||
finalAction.data =
|
||||
{
|
||||
ID: state.selectedSprite.ID,
|
||||
data: state.selectedSprite.toData(true)
|
||||
}
|
||||
|
||||
case OBJECT_ROTATED:
|
||||
finalAction.data = {ID: state.selectedSprite.ID, angle: state.selectedSprite.angle}
|
||||
|
||||
default: // nop
|
||||
}
|
||||
|
||||
if (finalAction.data == null) return;
|
||||
|
||||
if (redo) state.redoArray.push(finalAction);
|
||||
else if (!redo) state.undoArray.push(finalAction);
|
||||
}
|
||||
}
|
||||
|
||||
typedef UndoAction =
|
||||
{
|
||||
/**
|
||||
* The Type of Undo Action to store.
|
||||
*/
|
||||
var type:UndoActionType;
|
||||
|
||||
/**
|
||||
* The added Data of the Action.
|
||||
*/
|
||||
var data:Dynamic;
|
||||
}
|
||||
|
||||
enum abstract UndoActionType(String) from String
|
||||
{
|
||||
/**
|
||||
* Triggerred when an Object is deleted.
|
||||
*/
|
||||
var OBJECT_DELETED = "object_deleted";
|
||||
|
||||
/**
|
||||
* Triggerred when an Object is created.
|
||||
*/
|
||||
var OBJECT_CREATED = "object_created";
|
||||
|
||||
/**
|
||||
* Triggerred when an Object is moved.
|
||||
*/
|
||||
var OBJECT_MOVED = "object_moved";
|
||||
|
||||
/**
|
||||
* Triggerred when a Character is moved.
|
||||
*/
|
||||
var CHARACTER_MOVED = "character_moved";
|
||||
|
||||
/**
|
||||
* Triggerred when an Object is rotated.
|
||||
*/
|
||||
var OBJECT_ROTATED = "object_rotated";
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package funkin.ui.debug.stageeditor;
|
||||
|
||||
#if !macro
|
||||
using funkin.ui.debug.stageeditor.handlers.StageDataHandler;
|
||||
using funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
|
||||
using funkin.ui.debug.stageeditor.handlers.UndoRedoHandler;
|
||||
#end
|
||||
@@ -0,0 +1,242 @@
|
||||
package funkin.ui.debug.stageeditor.toolboxes;
|
||||
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.util.SortUtil;
|
||||
import haxe.ui.data.ArrayDataSource;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.components.Label;
|
||||
import funkin.ui.debug.stageeditor.handlers.StageDataHandler;
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
import haxe.ui.containers.ScrollView;
|
||||
import haxe.ui.core.Screen;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.tweens.FlxEase;
|
||||
import haxe.ui.containers.Grid;
|
||||
import funkin.play.character.CharacterData;
|
||||
|
||||
using StringTools;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/character-properties.xml"))
|
||||
class StageEditorCharacterToolbox extends StageEditorDefaultToolbox
|
||||
{
|
||||
var characterPosXStepper:NumberStepper;
|
||||
var characterPosYStepper:NumberStepper;
|
||||
var characterPosReset:Button;
|
||||
|
||||
var characterZIdxStepper:NumberStepper;
|
||||
var characterZIdxReset:Button;
|
||||
|
||||
var characterCamXStepper:NumberStepper;
|
||||
var characterCamYStepper:NumberStepper;
|
||||
var characterCamReset:Button;
|
||||
|
||||
var characterScaleSlider:Slider;
|
||||
var characterScaleReset:Button;
|
||||
|
||||
var characterTypeButton:Button;
|
||||
var charMenu:StageEditorCharacterMenu;
|
||||
|
||||
override public function new(state:StageEditorState)
|
||||
{
|
||||
super(state);
|
||||
|
||||
// position
|
||||
characterPosXStepper.onChange = characterPosYStepper.onChange = function(_) {
|
||||
repositionCharacter();
|
||||
state.saved = false;
|
||||
}
|
||||
|
||||
characterPosReset.onClick = function(_) {
|
||||
if (!StageEditorState.DEFAULT_POSITIONS.exists(state.selectedChar.characterType)) return;
|
||||
|
||||
var oldPositions = StageEditorState.DEFAULT_POSITIONS[state.selectedChar.characterType];
|
||||
characterPosXStepper.pos = oldPositions[0];
|
||||
characterPosYStepper.pos = oldPositions[1];
|
||||
}
|
||||
|
||||
// zidx
|
||||
characterZIdxStepper.max = StageEditorState.MAX_Z_INDEX;
|
||||
characterZIdxStepper.onChange = function(_) {
|
||||
state.charGroups[state.selectedChar.characterType].zIndex = Std.int(characterZIdxStepper.pos);
|
||||
state.saved = false;
|
||||
state.sortAssets();
|
||||
}
|
||||
|
||||
characterZIdxReset.onClick = function(_) {
|
||||
var thingies = [CharacterType.GF, CharacterType.DAD, CharacterType.BF];
|
||||
var thingIdxies = thingies.indexOf(state.selectedChar.characterType);
|
||||
|
||||
characterZIdxStepper.pos = (thingIdxies * 100);
|
||||
}
|
||||
|
||||
// camera
|
||||
characterCamXStepper.onChange = characterCamYStepper.onChange = function(_) {
|
||||
state.charCamOffsets[state.selectedChar.characterType] = [characterCamXStepper.pos, characterCamYStepper.pos];
|
||||
state.updateMarkerPos();
|
||||
state.saved = false;
|
||||
}
|
||||
|
||||
characterCamReset.onClick = function(_) characterCamXStepper.pos = characterCamYStepper.pos = 0; // lol
|
||||
|
||||
// scale
|
||||
characterScaleSlider.onChange = function(_) {
|
||||
state.selectedChar.setScale(state.selectedChar.getBaseScale() * characterScaleSlider.pos);
|
||||
repositionCharacter();
|
||||
state.saved = false;
|
||||
}
|
||||
|
||||
characterScaleReset.onChange = function(_) characterScaleSlider.pos = 1;
|
||||
|
||||
// character button
|
||||
characterTypeButton.onClick = function(_) {
|
||||
charMenu = new StageEditorCharacterMenu(state, this);
|
||||
Screen.instance.addComponent(charMenu);
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
override public function refresh()
|
||||
{
|
||||
var name = stageEditorState.selectedChar.characterType;
|
||||
|
||||
characterPosXStepper.step = characterPosYStepper.step = stageEditorState.moveStep;
|
||||
characterCamXStepper.step = characterCamYStepper.step = stageEditorState.moveStep;
|
||||
|
||||
if (characterPosXStepper.pos != stageEditorState.charPos[name][0]) characterPosXStepper.pos = stageEditorState.charPos[name][0];
|
||||
if (characterPosYStepper.pos != stageEditorState.charPos[name][1]) characterPosYStepper.pos = stageEditorState.charPos[name][1];
|
||||
|
||||
if (characterZIdxStepper.pos != stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex)
|
||||
characterZIdxStepper.pos = stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex;
|
||||
|
||||
if (characterCamXStepper.pos != stageEditorState.charCamOffsets[name][0]) characterCamXStepper.pos = stageEditorState.charCamOffsets[name][0];
|
||||
if (characterCamYStepper.pos != stageEditorState.charCamOffsets[name][1]) characterCamYStepper.pos = stageEditorState.charCamOffsets[name][1];
|
||||
|
||||
if (characterScaleSlider.pos != stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale())
|
||||
characterScaleSlider.pos = stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale();
|
||||
|
||||
var prevText = characterTypeButton.text;
|
||||
|
||||
var charData = CharacterDataParser.fetchCharacterData(stageEditorState.selectedChar.characterId);
|
||||
characterTypeButton.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(stageEditorState.selectedChar.characterId));
|
||||
characterTypeButton.text = (charData == null ? "None" : charData.name.length > 6 ? '${charData.name.substr(0, 6)}.' : '${charData.name}');
|
||||
|
||||
if (prevText != characterTypeButton.text)
|
||||
{
|
||||
Screen.instance.removeComponent(charMenu);
|
||||
}
|
||||
}
|
||||
|
||||
public function repositionCharacter()
|
||||
{
|
||||
stageEditorState.selectedChar.x = characterPosXStepper.pos - stageEditorState.selectedChar.characterOrigin.x
|
||||
+ stageEditorState.selectedChar.globalOffsets[0];
|
||||
stageEditorState.selectedChar.y = characterPosYStepper.pos - stageEditorState.selectedChar.characterOrigin.y
|
||||
+ stageEditorState.selectedChar.globalOffsets[1];
|
||||
|
||||
stageEditorState.selectedChar.setScale(stageEditorState.selectedChar.getBaseScale() * characterScaleSlider.pos);
|
||||
|
||||
stageEditorState.updateMarkerPos();
|
||||
}
|
||||
}
|
||||
|
||||
@:xml('
|
||||
<menu id="iconSelector" width="410" height="185" padding="8">
|
||||
<vbox width="100%" height="100%">
|
||||
<scrollview id="charSelectScroll" width="390" height="150" contentWidth="100%" />
|
||||
<label id="charIconName" text="(choose a character)" />
|
||||
</vbox>
|
||||
</menu>
|
||||
')
|
||||
class StageEditorCharacterMenu extends Menu // copied from chart editor
|
||||
{
|
||||
override public function new(state:StageEditorState, parent:StageEditorCharacterToolbox)
|
||||
{
|
||||
super();
|
||||
|
||||
this.x = Screen.instance.currentMouseX;
|
||||
this.y = Screen.instance.currentMouseY;
|
||||
|
||||
var charGrid = new Grid();
|
||||
charGrid.columns = 5;
|
||||
charGrid.width = this.width;
|
||||
charSelectScroll.addComponent(charGrid);
|
||||
|
||||
var charIds = CharacterDataParser.listCharacterIds();
|
||||
charIds.sort(SortUtil.alphabetically);
|
||||
|
||||
var defaultText:String = '(choose a character)';
|
||||
|
||||
for (charIndex => charId in charIds)
|
||||
{
|
||||
var charData:CharacterData = CharacterDataParser.fetchCharacterData(charId);
|
||||
|
||||
var charButton = new haxe.ui.components.Button();
|
||||
charButton.width = 70;
|
||||
charButton.height = 70;
|
||||
charButton.padding = 8;
|
||||
charButton.iconPosition = "top";
|
||||
|
||||
if (charId == state.selectedChar.characterId)
|
||||
{
|
||||
// Scroll to the character if it is already selected.
|
||||
charSelectScroll.hscrollPos = Math.floor(charIndex / 5) * 80;
|
||||
charButton.selected = true;
|
||||
|
||||
defaultText = '${charData.name} [${charId}]';
|
||||
}
|
||||
|
||||
var LIMIT = 6;
|
||||
charButton.icon = CharacterDataParser.getCharPixelIconAsset(charId);
|
||||
charButton.text = charData.name.length > LIMIT ? '${charData.name.substr(0, LIMIT)}.' : '${charData.name}';
|
||||
|
||||
charButton.onClick = _ -> {
|
||||
var type = state.selectedChar.characterType;
|
||||
if (state.selectedChar.characterId == charId) return; // saves on memory
|
||||
|
||||
var group = state.charGroups[type];
|
||||
group.killMembers();
|
||||
for (member in group.members)
|
||||
{
|
||||
member.kill();
|
||||
group.remove(member, true);
|
||||
member.destroy();
|
||||
}
|
||||
group.clear();
|
||||
|
||||
// okay i think that was enough cleaning phew you can see how clean this group is now!!!
|
||||
// anyways new character!!!!
|
||||
|
||||
var newChar = CharacterDataParser.fetchCharacter(charId, true);
|
||||
newChar.characterType = type;
|
||||
|
||||
newChar.resetCharacter(true);
|
||||
newChar.flipX = type == CharacterType.BF ? !newChar.getDataFlipX() : newChar.getDataFlipX();
|
||||
|
||||
state.selectedChar = newChar;
|
||||
group.add(newChar);
|
||||
|
||||
parent.repositionCharacter();
|
||||
};
|
||||
|
||||
charButton.onMouseOver = _ -> {
|
||||
charIconName.text = '${charData.name} [${charId}]';
|
||||
};
|
||||
charButton.onMouseOut = _ -> {
|
||||
charIconName.text = defaultText;
|
||||
};
|
||||
charGrid.addComponent(charButton);
|
||||
}
|
||||
|
||||
charIconName.text = defaultText;
|
||||
|
||||
this.alpha = 0;
|
||||
this.y -= 10;
|
||||
FlxTween.tween(this, {alpha: 1, y: this.y + 10}, 0.2, {ease: FlxEase.quartOut});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package funkin.ui.debug.stageeditor.toolboxes;
|
||||
|
||||
import haxe.ui.containers.dialogs.CollapsibleDialog;
|
||||
import funkin.audio.FunkinSound;
|
||||
|
||||
@:access(funkin.ui.debug.stageeditor.StageEditorState)
|
||||
class StageEditorDefaultToolbox extends CollapsibleDialog
|
||||
{
|
||||
var stageEditorState:StageEditorState;
|
||||
|
||||
public var dialogVisible:Bool = false;
|
||||
|
||||
private function new(stageEditorState:StageEditorState)
|
||||
{
|
||||
super();
|
||||
|
||||
this.stageEditorState = stageEditorState;
|
||||
|
||||
closable = false;
|
||||
modal = true;
|
||||
destroyOnClose = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the Sound and Visibility
|
||||
* @param on
|
||||
*/
|
||||
public function toggle(on:Bool)
|
||||
{
|
||||
if (!dialogVisible && on) FunkinSound.playOnce(Paths.sound('chartingSounds/openWindow'));
|
||||
else if (dialogVisible && !on) FunkinSound.playOnce(Paths.sound('chartingSounds/exitWindow'));
|
||||
|
||||
if (on) showDialog(false);
|
||||
else
|
||||
hide();
|
||||
|
||||
dialogVisible = on;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to implement this.
|
||||
*/
|
||||
public function refresh() {}
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
package funkin.ui.debug.stageeditor.toolboxes;
|
||||
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.components.TextArea;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.Image;
|
||||
import haxe.ui.containers.dialogs.Dialogs.FileDialogTypes;
|
||||
import haxe.ui.ToolkitAssets;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.containers.ListView;
|
||||
import haxe.ui.components.CheckBox;
|
||||
import haxe.ui.components.Switch;
|
||||
import flixel.util.FlxTimer;
|
||||
import haxe.ui.data.ArrayDataSource;
|
||||
import haxe.ui.events.ItemEvent;
|
||||
import haxe.ui.components.ColorPicker;
|
||||
import flixel.util.FlxColor;
|
||||
import haxe.ui.util.Color;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.animation.FlxAnimation;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.ui.debug.stageeditor.components.LoadFromUrlDialog;
|
||||
import openfl.display.BitmapData;
|
||||
|
||||
using StringTools;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-properties.xml"))
|
||||
class StageEditorObjectToolbox extends StageEditorDefaultToolbox
|
||||
{
|
||||
var linkedObject:StageEditorObject = null;
|
||||
|
||||
var objectImagePreview:Image;
|
||||
var objectLoadImageButton:Button;
|
||||
var objectLoadInternetButton:Button;
|
||||
var objectDownloadImageButton:Button;
|
||||
var objectResetImageButton:Button;
|
||||
var objectZIdxStepper:NumberStepper;
|
||||
var objectZIdxReset:Button;
|
||||
|
||||
var objectPosXStepper:NumberStepper;
|
||||
var objectPosYStepper:NumberStepper;
|
||||
var objectPosResetButton:Button;
|
||||
var objectAlphaSlider:HorizontalSlider;
|
||||
var objectAlphaResetButton:Button;
|
||||
var objectAngleSlider:HorizontalSlider;
|
||||
var objectAngleResetButton:Button;
|
||||
var objectScaleXStepper:NumberStepper;
|
||||
var objectScaleYStepper:NumberStepper;
|
||||
var objectScaleResetButton:Button;
|
||||
var objectSizeXStepper:NumberStepper;
|
||||
var objectSizeYStepper:NumberStepper;
|
||||
var objectSizeResetButton:Button;
|
||||
var objectScrollXSlider:HorizontalSlider;
|
||||
var objectScrollYSlider:HorizontalSlider;
|
||||
var objectScrollResetButton:Button;
|
||||
|
||||
var objectFrameText:TextArea;
|
||||
var objectFrameTextLoad:Button;
|
||||
var objectFrameTextSparrow:Button;
|
||||
var objectFrameTextPacker:Button;
|
||||
var objectFrameImageWidth:NumberStepper;
|
||||
var objectFrameImageHeight:NumberStepper;
|
||||
var objectFrameImageSetter:Button;
|
||||
var objectFrameReset:Button;
|
||||
|
||||
var objectAnimDropdown:DropDown;
|
||||
var objectAnimName:TextField;
|
||||
var objectAnimFrameList:ListView;
|
||||
var objectAnimPrefix:TextField;
|
||||
var objectAnimFrames:TextField;
|
||||
var objectAnimLooped:CheckBox;
|
||||
var objectAnimFlipX:CheckBox;
|
||||
var objectAnimFlipY:CheckBox;
|
||||
var objectAnimFramerate:NumberStepper;
|
||||
var objectAnimOffsetX:NumberStepper;
|
||||
var objectAnimOffsetY:NumberStepper;
|
||||
var objectAnimDanceBeat:NumberStepper;
|
||||
var objectAnimDanceBeatReset:Button;
|
||||
var objectAnimStart:TextField;
|
||||
var objectAnimStartReset:Button;
|
||||
|
||||
var objectMiscAntialias:CheckBox;
|
||||
var objectMiscAntialiasReset:Button;
|
||||
var objectMiscFlipReset:Button;
|
||||
var objectMiscBlendDrop:DropDown;
|
||||
var objectMiscBlendReset:Button;
|
||||
var objectMiscColor:ColorPicker;
|
||||
var objectMiscColorReset:Button;
|
||||
|
||||
override public function new(state:StageEditorState)
|
||||
{
|
||||
super(state);
|
||||
|
||||
// basic callbacks
|
||||
objectLoadImageButton.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
Dialogs.openBinaryFile("Open Image File", FileDialogTypes.IMAGES, function(selectedFile) {
|
||||
if (selectedFile == null) return;
|
||||
|
||||
objectImagePreview.resource = null;
|
||||
|
||||
ToolkitAssets.instance.imageFromBytes(selectedFile.bytes, function(imageInfo) {
|
||||
if (imageInfo == null) return;
|
||||
|
||||
objectImagePreview.resource = imageInfo.data;
|
||||
|
||||
linkedObject.frame = imageInfo.data;
|
||||
|
||||
var bit = linkedObject.updateFramePixels();
|
||||
var bitToLoad = state.addBitmap(bit);
|
||||
|
||||
linkedObject.loadGraphic(state.bitmaps[bitToLoad]);
|
||||
linkedObject.updateHitbox();
|
||||
|
||||
// update size stuff
|
||||
objectSizeXStepper.pos = linkedObject.width;
|
||||
objectSizeYStepper.pos = linkedObject.height;
|
||||
|
||||
// remove unused bitmaps
|
||||
state.removeUnusedBitmaps();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
objectLoadInternetButton.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
state.createURLDialog(function(bytes:lime.utils.Bytes) {
|
||||
linkedObject.loadGraphic(BitmapData.fromBytes(bytes));
|
||||
linkedObject.updateHitbox();
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
objectDownloadImageButton.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
FileUtil.saveFile(linkedObject.pixels.image.encode(PNG), [FileUtil.FILE_FILTER_PNG], null, null,
|
||||
linkedObject.name + "-graphic.png"); // i'on need any callbacks
|
||||
}
|
||||
|
||||
objectZIdxStepper.max = StageEditorState.MAX_Z_INDEX;
|
||||
objectZIdxStepper.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.zIndex = Std.int(objectZIdxStepper.pos);
|
||||
state.sortAssets();
|
||||
}
|
||||
}
|
||||
|
||||
// numeric callbacks
|
||||
objectPosXStepper.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.x = objectPosXStepper.pos;
|
||||
};
|
||||
|
||||
objectPosYStepper.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.y = objectPosYStepper.pos;
|
||||
};
|
||||
|
||||
objectAlphaSlider.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos;
|
||||
};
|
||||
|
||||
objectAngleSlider.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos;
|
||||
};
|
||||
|
||||
objectScaleXStepper.onChange = objectScaleYStepper.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.scale.set(objectScaleXStepper.pos, objectScaleYStepper.pos);
|
||||
linkedObject.updateHitbox();
|
||||
objectSizeXStepper.pos = linkedObject.width;
|
||||
objectSizeYStepper.pos = linkedObject.height;
|
||||
|
||||
linkedObject.playAnim(linkedObject.animation.name); // load offsets
|
||||
}
|
||||
};
|
||||
|
||||
objectSizeXStepper.onChange = objectSizeYStepper.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.setGraphicSize(Std.int(objectSizeXStepper.pos), Std.int(objectSizeYStepper.pos));
|
||||
linkedObject.updateHitbox();
|
||||
objectScaleXStepper.pos = linkedObject.scale.x;
|
||||
objectScaleYStepper.pos = linkedObject.scale.y;
|
||||
|
||||
linkedObject.playAnim(linkedObject.animation.name); // load offsets
|
||||
}
|
||||
};
|
||||
|
||||
objectScrollXSlider.onChange = objectScrollYSlider.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.scrollFactor.set(objectScrollXSlider.pos, objectScrollYSlider.pos);
|
||||
};
|
||||
|
||||
// frame callbacks
|
||||
objectFrameTextLoad.onClick = function(_) {
|
||||
Dialogs.openTextFile("Open Text File", FileDialogTypes.TEXTS, function(selectedFile) {
|
||||
if (selectedFile.text == null || (!selectedFile.name.endsWith(".xml") && !selectedFile.name.endsWith(".txt"))) return;
|
||||
|
||||
objectFrameText.text = selectedFile.text;
|
||||
|
||||
state.notifyChange("Frame Text Loaded", "The Text File " + selectedFile.name + " has been loaded.");
|
||||
});
|
||||
}
|
||||
|
||||
objectFrameTextSparrow.onClick = function(_) {
|
||||
if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return;
|
||||
|
||||
try
|
||||
{
|
||||
linkedObject.frames = FlxAtlasFrames.fromSparrow(linkedObject.graphic, objectFrameText.text);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
state.notifyChange("Frame Setup Error", e.toString(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// might as well clear animations because frames SUCK
|
||||
linkedObject.animDatas.clear();
|
||||
linkedObject.animation.destroyAnimations();
|
||||
linkedObject.updateHitbox();
|
||||
refresh();
|
||||
|
||||
state.notifyChange("Frame Setup Done", "Finished the Sparrow Frame Setup for the Object " + linkedObject.name + ".");
|
||||
}
|
||||
|
||||
objectFrameTextPacker.onClick = function(_) {
|
||||
if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return;
|
||||
|
||||
try // crash prevention
|
||||
{
|
||||
linkedObject.frames = FlxAtlasFrames.fromSpriteSheetPacker(linkedObject.graphic, objectFrameText.text);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
state.notifyChange("Frame Setup Error", e.toString(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// might as well clear animations because frames SUCK
|
||||
linkedObject.animDatas.clear();
|
||||
linkedObject.animation.destroyAnimations();
|
||||
|
||||
linkedObject.updateHitbox();
|
||||
refresh();
|
||||
|
||||
state.notifyChange("Frame Setup Done", "Finished the Packer Frame Setup for the Object " + linkedObject.name + ".");
|
||||
}
|
||||
|
||||
objectFrameImageSetter.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
linkedObject.loadGraphic(linkedObject.graphic, true, Std.int(objectFrameImageWidth.pos), Std.int(objectFrameImageHeight.pos));
|
||||
linkedObject.updateHitbox();
|
||||
|
||||
// set da names
|
||||
for (i in 0...linkedObject.frames.frames.length)
|
||||
{
|
||||
linkedObject.frames.framesHash.set("Frame" + i, linkedObject.frames.frames[i]);
|
||||
linkedObject.frames.frames[i].name = "Frame" + i;
|
||||
}
|
||||
|
||||
// might as well clear animations because frames SUCK
|
||||
linkedObject.animDatas.clear();
|
||||
linkedObject.animation.destroyAnimations();
|
||||
refresh();
|
||||
|
||||
state.notifyChange("Frame Setup Done", "Finished the Image Frame Setup for the Object " + linkedObject.name + ".");
|
||||
}
|
||||
|
||||
// animation
|
||||
objectAnimDropdown.onChange = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
if (objectAnimDropdown.selectedIndex == -1) // RESET EVERYTHING INSTANTENEOUSLY
|
||||
{
|
||||
objectAnimName.text = "";
|
||||
objectAnimLooped.selected = objectAnimFlipX.selected = objectAnimFlipY.selected = false;
|
||||
objectAnimFramerate.pos = 24;
|
||||
objectAnimOffsetX.pos = objectAnimOffsetY.pos = 0;
|
||||
objectAnimFrames.text = "";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var animData = linkedObject.animDatas[objectAnimDropdown.selectedItem.text];
|
||||
if (animData == null) return;
|
||||
|
||||
objectAnimName.text = objectAnimDropdown.selectedItem.text;
|
||||
objectAnimPrefix.text = animData.prefix ?? "";
|
||||
objectAnimFrames.text = (animData.frameIndices != null && animData.frameIndices.length > 0 ? animData.frameIndices.join(", ") : "");
|
||||
|
||||
objectAnimLooped.selected = animData.looped ?? false;
|
||||
objectAnimFlipX.selected = animData.flipX ?? false;
|
||||
objectAnimFlipY.selected = animData.flipY ?? false;
|
||||
objectAnimFramerate.pos = animData.frameRate ?? 24;
|
||||
|
||||
objectAnimOffsetX.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[0] : 0);
|
||||
objectAnimOffsetY.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[1] : 0);
|
||||
}
|
||||
|
||||
objectAnimSave.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
if (objectAnimName.text == null || objectAnimName.text == "")
|
||||
{
|
||||
state.notifyChange("Animation Saving Error", "Invalid Animation Name!", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (objectAnimPrefix.text == null || objectAnimPrefix.text == "")
|
||||
{
|
||||
state.notifyChange("Animation Saving Error", "Missing Animation Prefix!", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (linkedObject.animation.getNameList().contains(objectAnimName.text)) linkedObject.animation.remove(objectAnimName.text);
|
||||
|
||||
var indices = [];
|
||||
|
||||
if (objectAnimFrames.text != null && objectAnimFrames.text != "")
|
||||
{
|
||||
var splitter = objectAnimFrames.text.replace(" ", "").split(",");
|
||||
|
||||
for (num in splitter)
|
||||
{
|
||||
indices.push(Std.parseInt(num));
|
||||
}
|
||||
}
|
||||
|
||||
var shouldDoIndices:Bool = (indices.length > 0 && !indices.contains(null));
|
||||
|
||||
linkedObject.addAnim(objectAnimName.text, objectAnimPrefix.text, [objectAnimOffsetX.pos, objectAnimOffsetY.pos], (shouldDoIndices ? indices : []),
|
||||
Std.int(objectAnimFramerate.pos), objectAnimLooped.selected, objectAnimFlipX.selected, objectAnimFlipY.selected);
|
||||
|
||||
if (linkedObject.animation.getByName(objectAnimName.text) == null)
|
||||
{
|
||||
state.notifyChange("Animation Saving Error", "Invalid Frames!", true);
|
||||
return;
|
||||
}
|
||||
|
||||
linkedObject.playAnim(objectAnimName.text);
|
||||
|
||||
state.notifyChange("Animation Saving Done", "Animation " + objectAnimName.text + " has been saved to the Object " + linkedObject.name + ".");
|
||||
updateAnimList();
|
||||
|
||||
// stops the animation preview if animation is looped for too long
|
||||
FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
|
||||
if (linkedObject != null && linkedObject.animation.curAnim != null)
|
||||
linkedObject.animation.stop(); // null check cuz if we stop an anim for a null object the game crashes :[
|
||||
});
|
||||
}
|
||||
|
||||
objectAnimDelete.onClick = function(_) {
|
||||
if (linkedObject == null || linkedObject.animation.getNameList().length <= 0 || objectAnimDropdown.selectedIndex < 0) return;
|
||||
|
||||
linkedObject.animation.pause();
|
||||
linkedObject.animation.stop();
|
||||
linkedObject.animation.curAnim = null;
|
||||
|
||||
var daAnim = linkedObject.animation.getNameList()[objectAnimDropdown.selectedIndex];
|
||||
|
||||
linkedObject.animation.remove(daAnim);
|
||||
linkedObject.animDatas.remove(daAnim);
|
||||
linkedObject.offset.set();
|
||||
|
||||
state.notifyChange("Animation Deletion Done",
|
||||
"Animation "
|
||||
+ objectAnimDropdown.selectedItem.text
|
||||
+ " has been removed from the Object "
|
||||
+ linkedObject.name
|
||||
+ ".");
|
||||
|
||||
updateAnimList();
|
||||
|
||||
objectAnimDropdown.selectedIndex = objectAnimDropdown.dataSource.size - 1;
|
||||
}
|
||||
|
||||
objectAnimDanceBeat.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.danceEvery = Std.int(objectAnimDanceBeat.pos);
|
||||
}
|
||||
|
||||
objectAnimStart.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white";
|
||||
else
|
||||
objectAnimStart.styleString = "color: indianred";
|
||||
|
||||
linkedObject.startingAnimation = objectAnimStart.text;
|
||||
}
|
||||
}
|
||||
|
||||
// misc
|
||||
objectMiscAntialias.onClick = function(_) {
|
||||
if (linkedObject != null) linkedObject.antialiasing = objectMiscAntialias.selected;
|
||||
}
|
||||
|
||||
objectMiscBlendDrop.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
linkedObject.blend = objectMiscBlendDrop.selectedItem.text == "NONE" ? null : AssetDataHandler.blendFromString(objectMiscBlendDrop.selectedItem.text);
|
||||
}
|
||||
|
||||
objectMiscColor.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.color = FlxColor.fromRGB(objectMiscColor.currentColor.r, objectMiscColor.currentColor.g,
|
||||
objectMiscColor.currentColor.b);
|
||||
}
|
||||
|
||||
// reset button callbacks
|
||||
objectResetImageButton.onClick = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.loadGraphic(AssetDataHandler.getDefaultGraphic());
|
||||
linkedObject.updateHitbox();
|
||||
refresh();
|
||||
|
||||
// remove unused bitmaps
|
||||
state.removeUnusedBitmaps();
|
||||
}
|
||||
}
|
||||
|
||||
objectZIdxReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectZIdxStepper.pos = 0; // corner cutting because onChange will activate with this
|
||||
}
|
||||
|
||||
objectPosResetButton.onClick = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.screenCenter();
|
||||
objectPosXStepper.pos = linkedObject.x;
|
||||
objectPosYStepper.pos = linkedObject.y;
|
||||
}
|
||||
}
|
||||
|
||||
objectAlphaResetButton.onClick = function(_) {
|
||||
if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos = 1;
|
||||
}
|
||||
|
||||
objectAngleResetButton.onClick = function(_) {
|
||||
if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos = 0;
|
||||
}
|
||||
|
||||
objectScaleResetButton.onClick = objectSizeResetButton.onClick = function(_) // the corner cutting goes crazy
|
||||
{
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.scale.set(1, 1);
|
||||
refresh(); // refreshes like multiple shit
|
||||
}
|
||||
}
|
||||
|
||||
objectScrollResetButton.onClick = function(_) {
|
||||
if (linkedObject != null) linkedObject.scrollFactor.x = linkedObject.scrollFactor.y = objectScrollXSlider.pos = objectScrollYSlider.pos = 1;
|
||||
}
|
||||
|
||||
objectFrameReset.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
linkedObject.loadGraphic(linkedObject.pixels);
|
||||
linkedObject.animDatas.clear();
|
||||
linkedObject.animation.destroyAnimations();
|
||||
refresh();
|
||||
}
|
||||
|
||||
objectMiscAntialiasReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectMiscAntialias.selected = true;
|
||||
}
|
||||
|
||||
objectMiscBlendReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectMiscBlendDrop.selectedItem = "NORMAL";
|
||||
}
|
||||
|
||||
objectMiscColorReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectMiscColor.currentColor = Color.fromString("white");
|
||||
}
|
||||
|
||||
objectAnimDanceBeatReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectAnimDanceBeat.pos = 0;
|
||||
}
|
||||
|
||||
objectAnimStartReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectAnimStart.text = "";
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
var prevFrames:Array<FlxFrame> = [];
|
||||
var prevAnims:Array<String> = [];
|
||||
|
||||
override public function refresh()
|
||||
{
|
||||
linkedObject = stageEditorState.selectedSprite;
|
||||
|
||||
objectPosXStepper.step = stageEditorState.moveStep;
|
||||
objectPosYStepper.step = stageEditorState.moveStep;
|
||||
objectAngleSlider.step = funkin.save.Save.instance.stageEditorAngleStep;
|
||||
|
||||
if (linkedObject == null)
|
||||
{
|
||||
updateFrameList();
|
||||
updateAnimList();
|
||||
return;
|
||||
}
|
||||
|
||||
// saving fps
|
||||
if (objectImagePreview.resource != linkedObject.frame) objectImagePreview.resource = linkedObject.frame;
|
||||
|
||||
if (objectZIdxStepper.pos != linkedObject.zIndex) objectZIdxStepper.pos = linkedObject.zIndex;
|
||||
if (objectPosXStepper.pos != linkedObject.x) objectPosXStepper.pos = linkedObject.x;
|
||||
if (objectPosYStepper.pos != linkedObject.y) objectPosYStepper.pos = linkedObject.y;
|
||||
if (objectAlphaSlider.pos != linkedObject.alpha) objectAlphaSlider.pos = linkedObject.alpha;
|
||||
if (objectAngleSlider.pos != linkedObject.angle) objectAngleSlider.pos = linkedObject.angle;
|
||||
if (objectScaleXStepper.pos != linkedObject.scale.x) objectScaleXStepper.pos = linkedObject.scale.x;
|
||||
if (objectScaleYStepper.pos != linkedObject.scale.y) objectScaleYStepper.pos = linkedObject.scale.y;
|
||||
if (objectSizeXStepper.pos != linkedObject.width) objectSizeXStepper.pos = linkedObject.width;
|
||||
if (objectSizeYStepper.pos != linkedObject.height) objectSizeYStepper.pos = linkedObject.height;
|
||||
if (objectScrollXSlider.pos != linkedObject.scrollFactor.x) objectScrollXSlider.pos = linkedObject.scrollFactor.x;
|
||||
if (objectScrollYSlider.pos != linkedObject.scrollFactor.y) objectScrollYSlider.pos = linkedObject.scrollFactor.y;
|
||||
if (objectMiscAntialias.selected != linkedObject.antialiasing) objectMiscAntialias.selected = linkedObject.antialiasing;
|
||||
|
||||
if (objectMiscColor.currentColor != Color.fromString(linkedObject.color.toHexString() ?? "white"))
|
||||
objectMiscColor.currentColor = Color.fromString(linkedObject.color.toHexString());
|
||||
|
||||
if (objectAnimDanceBeat.pos != linkedObject.danceEvery) objectAnimDanceBeat.pos = linkedObject.danceEvery;
|
||||
if (objectAnimStart.text != linkedObject.startingAnimation) objectAnimStart.text = linkedObject.startingAnimation;
|
||||
|
||||
var objBlend = Std.string(linkedObject.blend) ?? "NONE";
|
||||
if (objectMiscBlendDrop.selectedItem != objBlend.toUpperCase()) objectMiscBlendDrop.selectedItem = objBlend.toUpperCase();
|
||||
|
||||
// ough the max
|
||||
if (objectFrameImageWidth.max != linkedObject.pixels.width) objectFrameImageWidth.max = linkedObject.graphic.width;
|
||||
if (objectFrameImageHeight.max != linkedObject.pixels.height) objectFrameImageHeight.max = linkedObject.graphic.height;
|
||||
|
||||
// update some anim shit
|
||||
if (prevFrames != linkedObject.frames.frames.copy()) updateFrameList();
|
||||
if (prevAnims != linkedObject.animation.getNameList().copy()) updateAnimList();
|
||||
}
|
||||
|
||||
function updateFrameList()
|
||||
{
|
||||
prevFrames = [];
|
||||
objectAnimFrameList.dataSource = new ArrayDataSource();
|
||||
|
||||
if (linkedObject == null) return;
|
||||
|
||||
for (fname in linkedObject.frames.frames)
|
||||
{
|
||||
if (fname != null) objectAnimFrameList.dataSource.add({name: fname.name, tooltip: fname.name});
|
||||
|
||||
prevFrames.push(fname);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAnimList()
|
||||
{
|
||||
objectAnimDropdown.dataSource.clear();
|
||||
prevAnims = [];
|
||||
if (linkedObject == null) return;
|
||||
|
||||
for (aname in linkedObject.animation.getNameList())
|
||||
{
|
||||
objectAnimDropdown.dataSource.add({text: aname});
|
||||
prevAnims.push(aname);
|
||||
}
|
||||
|
||||
if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white";
|
||||
else
|
||||
objectAnimStart.styleString = "color: indianred";
|
||||
|
||||
linkedObject.startingAnimation = objectAnimStart.text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package funkin.ui.debug.stageeditor.toolboxes;
|
||||
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.components.DropDown;
|
||||
import funkin.util.SortUtil;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/stage-settings.xml"))
|
||||
class StageEditorStageToolbox extends StageEditorDefaultToolbox
|
||||
{
|
||||
var stageNameText:TextField;
|
||||
var stageZoomStepper:NumberStepper;
|
||||
var stageLibraryDrop:DropDown;
|
||||
|
||||
override public function new(state:StageEditorState)
|
||||
{
|
||||
super(state);
|
||||
|
||||
stageNameText.onChange = function(_) {
|
||||
state.stageName = stageNameText.text;
|
||||
state.saved = false;
|
||||
}
|
||||
|
||||
stageZoomStepper.onChange = function(_) {
|
||||
state.stageZoom = stageZoomStepper.pos;
|
||||
state.updateMarkerPos();
|
||||
state.saved = false;
|
||||
}
|
||||
|
||||
final EXCLUDE_LIBS = ["art", "default", "vlc", "videos", "songs"];
|
||||
var allLibs = [];
|
||||
|
||||
@:privateAccess
|
||||
{
|
||||
for (lib => idk in lime.utils.Assets.libraryPaths)
|
||||
{
|
||||
if (!EXCLUDE_LIBS.contains(lib)) allLibs.push(lib);
|
||||
}
|
||||
}
|
||||
allLibs.sort(SortUtil.alphabetically); // this system is VERY stupid, it relies on the possibility that the future libraries will be named week(end)[x]
|
||||
|
||||
for (lib in allLibs)
|
||||
{
|
||||
stageLibraryDrop.dataSource.add({text: lib});
|
||||
}
|
||||
|
||||
stageLibraryDrop.onChange = function(_) {
|
||||
state.stageFolder = stageLibraryDrop.selectedItem.text;
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
override public function refresh()
|
||||
{
|
||||
stageNameText.text = stageEditorState.stageName;
|
||||
stageZoomStepper.pos = stageEditorState.stageZoom;
|
||||
stageLibraryDrop.selectedItem = stageEditorState.stageFolder;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ class SongMenuItem extends FlxSpriteGroup
|
||||
* Modify this by calling `init()`
|
||||
* If `null`, assume this SongMenuItem is for the "Random Song" option.
|
||||
*/
|
||||
public var songData(default, null):Null<FreeplaySongData> = null;
|
||||
public var freeplayData(default, null):Null<FreeplaySongData> = null;
|
||||
|
||||
public var selected(default, set):Bool;
|
||||
|
||||
@@ -422,6 +422,34 @@ class SongMenuItem extends FlxSpriteGroup
|
||||
return evilTrail.color;
|
||||
}
|
||||
|
||||
public function refreshDisplay():Void
|
||||
{
|
||||
if (freeplayData == null)
|
||||
{
|
||||
songText.text = 'Random';
|
||||
pixelIcon.visible = false;
|
||||
ranking.visible = false;
|
||||
blurredRanking.visible = false;
|
||||
favIcon.visible = false;
|
||||
favIconBlurred.visible = false;
|
||||
newText.visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
songText.text = freeplayData.fullSongName;
|
||||
if (freeplayData.songCharacter != null) pixelIcon.setCharacter(freeplayData.songCharacter);
|
||||
pixelIcon.visible = true;
|
||||
updateBPM(Std.int(freeplayData.songStartingBpm) ?? 0);
|
||||
updateDifficultyRating(freeplayData.difficultyRating ?? 0);
|
||||
updateScoringRank(freeplayData.scoringRank);
|
||||
newText.visible = freeplayData.isNew;
|
||||
favIcon.visible = freeplayData.isFav;
|
||||
favIconBlurred.visible = freeplayData.isFav;
|
||||
checkClip();
|
||||
}
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
function updateDifficultyRating(newRating:Int):Void
|
||||
{
|
||||
var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
|
||||
@@ -500,11 +528,11 @@ class SongMenuItem extends FlxSpriteGroup
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
public function init(?x:Float, ?y:Float, songData:Null<FreeplaySongData>, ?styleData:FreeplayStyle = null):Void
|
||||
public function init(?x:Float, ?y:Float, freeplayData:Null<FreeplaySongData>, ?styleData:FreeplayStyle = null):Void
|
||||
{
|
||||
if (x != null) this.x = x;
|
||||
if (y != null) this.y = y;
|
||||
this.songData = songData;
|
||||
this.freeplayData = freeplayData;
|
||||
|
||||
// im so mad i have to do this but im pretty sure with the capsules recycling i cant call the new function properly :/
|
||||
// if thats possible someone Please change the new function to be something like
|
||||
@@ -517,21 +545,13 @@ class SongMenuItem extends FlxSpriteGroup
|
||||
songText.applyStyle(styleData);
|
||||
}
|
||||
|
||||
// Update capsule text.
|
||||
songText.text = songData?.songName ?? 'Random';
|
||||
// Update capsule character.
|
||||
if (songData?.songCharacter != null) pixelIcon.setCharacter(songData.songCharacter);
|
||||
updateBPM(Std.int(songData?.songStartingBpm) ?? 0);
|
||||
updateDifficultyRating(songData?.difficultyRating ?? 0);
|
||||
updateScoringRank(songData?.scoringRank);
|
||||
newText.visible = songData?.isNew;
|
||||
updateScoringRank(freeplayData?.scoringRank);
|
||||
favIcon.animation.curAnim.curFrame = favIcon.animation.curAnim.numFrames - 1;
|
||||
favIconBlurred.animation.curAnim.curFrame = favIconBlurred.animation.curAnim.numFrames - 1;
|
||||
|
||||
// Update opacity, offsets, etc.
|
||||
updateSelected();
|
||||
refreshDisplay();
|
||||
|
||||
checkWeek(songData?.songId);
|
||||
checkWeek(freeplayData?.data.id);
|
||||
}
|
||||
|
||||
var frameInTicker:Float = 0;
|
||||
|
||||
@@ -183,7 +183,9 @@ class MainMenuState extends MusicBeatState
|
||||
FunkinSound.playMusic('freakyMenu',
|
||||
{
|
||||
overrideExisting: true,
|
||||
restartTrack: false
|
||||
restartTrack: false,
|
||||
// Continue playing this music between states, until a different music track gets played.
|
||||
persist: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class FunkinSoundTray extends FlxSoundTray
|
||||
var bg:Bitmap = new Bitmap(Assets.getBitmapData(Paths.image("soundtray/volumebox")));
|
||||
bg.scaleX = graphicScale;
|
||||
bg.scaleY = graphicScale;
|
||||
bg.smoothing = true;
|
||||
addChild(bg);
|
||||
|
||||
y = -height;
|
||||
@@ -44,6 +45,7 @@ class FunkinSoundTray extends FlxSoundTray
|
||||
backingBar.y = 5;
|
||||
backingBar.scaleX = graphicScale;
|
||||
backingBar.scaleY = graphicScale;
|
||||
backingBar.smoothing = true;
|
||||
addChild(backingBar);
|
||||
backingBar.alpha = 0.4;
|
||||
|
||||
@@ -60,6 +62,7 @@ class FunkinSoundTray extends FlxSoundTray
|
||||
bar.y = 5;
|
||||
bar.scaleX = graphicScale;
|
||||
bar.scaleY = graphicScale;
|
||||
bar.smoothing = true;
|
||||
addChild(bar);
|
||||
_bars.push(bar);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ class EnumPreferenceItem extends TextMenuItem
|
||||
}
|
||||
|
||||
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
|
||||
|
||||
this.fireInstantly = true;
|
||||
}
|
||||
|
||||
override function update(elapsed:Float):Void
|
||||
|
||||
@@ -58,6 +58,8 @@ class NumberPreferenceItem extends TextMenuItem
|
||||
this.precision = precision;
|
||||
this.onChangeCallback = callback;
|
||||
this.valueFormatter = valueFormatter;
|
||||
|
||||
this.fireInstantly = true;
|
||||
}
|
||||
|
||||
override function update(elapsed:Float):Void
|
||||
|
||||
@@ -242,7 +242,9 @@ class StoryMenuState extends MusicBeatState
|
||||
FunkinSound.playMusic('freakyMenu',
|
||||
{
|
||||
overrideExisting: true,
|
||||
restartTrack: false
|
||||
restartTrack: false,
|
||||
// Continue playing this music between states, until a different music track gets played.
|
||||
persist: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -233,6 +233,7 @@ class TitleState extends MusicBeatState
|
||||
startingVolume: 0.0,
|
||||
overrideExisting: true,
|
||||
restartTrack: false,
|
||||
// Continue playing this music between states, until a different music track gets played.
|
||||
persist: true
|
||||
});
|
||||
// Fade from 0.0 to 1 over 4 seconds
|
||||
@@ -270,7 +271,7 @@ class TitleState extends MusicBeatState
|
||||
#if desktop
|
||||
if (FlxG.keys.justPressed.ESCAPE)
|
||||
{
|
||||
Sys.exit(0);
|
||||
openfl.Lib.application.window.close();
|
||||
}
|
||||
#end
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class LoadingState extends MusicBeatSubState
|
||||
}
|
||||
|
||||
checkLibrary('shared');
|
||||
checkLibrary(PlayStatePlaylist.campaignId);
|
||||
checkLibrary(stageDirectory);
|
||||
checkLibrary('tutorial');
|
||||
|
||||
var fadeTime:Float = 0.5;
|
||||
@@ -204,6 +204,8 @@ class LoadingState extends MusicBeatSubState
|
||||
return Paths.inst(PlayState.instance.currentSong.id);
|
||||
}
|
||||
|
||||
static var stageDirectory:String = "shared";
|
||||
|
||||
/**
|
||||
* Starts the transition to a new `PlayState` to start a new song.
|
||||
* First switches to the `LoadingState` if assets need to be loaded.
|
||||
@@ -213,7 +215,13 @@ class LoadingState extends MusicBeatSubState
|
||||
*/
|
||||
public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false, asSubState = false, ?onConstruct:PlayState->Void):Void
|
||||
{
|
||||
Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
|
||||
var daChart = params.targetSong.getDifficulty(params.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY,
|
||||
params.targetVariation ?? Constants.DEFAULT_VARIATION);
|
||||
|
||||
var daStage = funkin.data.stage.StageRegistry.instance.fetchEntry(daChart.stage);
|
||||
stageDirectory = daStage?._data?.directory ?? "shared";
|
||||
Paths.setCurrentLevel(stageDirectory);
|
||||
|
||||
var playStateCtor:() -> PlayState = function() {
|
||||
return new PlayState(params);
|
||||
};
|
||||
|
||||
@@ -250,18 +250,18 @@ class FunkinPreloader extends FlxBasePreloader
|
||||
dspText.selectable = false;
|
||||
dspText.textColor = 0x000000;
|
||||
dspText.width = this._width;
|
||||
dspText.height = 20;
|
||||
dspText.height = 30;
|
||||
dspText.text = 'DSP';
|
||||
dspText.x = 10;
|
||||
dspText.y = -5;
|
||||
dspText.y = -7;
|
||||
box.addChild(dspText);
|
||||
|
||||
fnfText.selectable = false;
|
||||
fnfText.textColor = 0x000000;
|
||||
fnfText.width = this._width;
|
||||
fnfText.height = 20;
|
||||
fnfText.x = 75;
|
||||
fnfText.y = -5;
|
||||
fnfText.height = 30;
|
||||
fnfText.x = 78;
|
||||
fnfText.y = -7;
|
||||
fnfText.text = 'FNF';
|
||||
box.addChild(fnfText);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class FileUtil
|
||||
public static final FILE_FILTER_JSON:FileFilter = new FileFilter("JSON Data File (.json)", "*.json");
|
||||
public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip");
|
||||
public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png");
|
||||
public static final FILE_FILTER_FNFS:FileFilter = new FileFilter("Friday Night Funkin' Stage (.fnfs)", "*.fnfs");
|
||||
|
||||
public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo =
|
||||
{
|
||||
@@ -39,6 +40,12 @@ class FileUtil
|
||||
label: 'PNG Image',
|
||||
};
|
||||
|
||||
public static final FILE_EXTENSION_INFO_FNFS:FileDialogExtensionInfo =
|
||||
{
|
||||
extension: 'fnfs',
|
||||
label: 'Friday Night Funkin\' Stage',
|
||||
};
|
||||
|
||||
/**
|
||||
* Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected.
|
||||
* Powered by HaxeUI, so it works on all platforms.
|
||||
|
||||
@@ -102,6 +102,17 @@ class WindowUtil
|
||||
openfl.Lib.current.stage.addEventListener(openfl.events.KeyboardEvent.KEY_DOWN, (e:openfl.events.KeyboardEvent) -> {
|
||||
for (key in PlayerSettings.player1.controls.getKeysForAction(WINDOW_FULLSCREEN))
|
||||
{
|
||||
// FlxG.stage.focus is set to null by the debug console stuff,
|
||||
// so when that's in focus, we don't want to toggle fullscreen using F
|
||||
// (annoying when tying "FlxG" in console... lol)
|
||||
#if FLX_DEBUG
|
||||
@:privateAccess
|
||||
if (FlxG.game.debugger.visible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
#end
|
||||
|
||||
if (e.keyCode == key)
|
||||
{
|
||||
openfl.Lib.application.window.fullscreen = !openfl.Lib.application.window.fullscreen;
|
||||
|
||||
@@ -61,6 +61,12 @@ class CrashHandler
|
||||
{
|
||||
trace('Error while handling crash: ' + e);
|
||||
}
|
||||
|
||||
#if sys
|
||||
Sys.sleep(1); // wait a few moments of margin to process.
|
||||
// Exit the game. Since it threw an error, we use a non-zero exit code.
|
||||
openfl.Lib.application.window.close();
|
||||
#end
|
||||
}
|
||||
|
||||
static function onCriticalError(message:String):Void
|
||||
@@ -83,8 +89,9 @@ class CrashHandler
|
||||
}
|
||||
|
||||
#if sys
|
||||
Sys.sleep(1); // wait a few moments of margin to process.
|
||||
// Exit the game. Since it threw an error, we use a non-zero exit code.
|
||||
Sys.exit(1);
|
||||
openfl.Lib.application.window.close();
|
||||
#end
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user