Merge branch 'FunkinCrew:main' into faves-week-sort

This commit is contained in:
Hundrec 2025-01-26 14:07:44 -07:00 committed by GitHub
commit b252a9e5dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 4848 additions and 451 deletions

View file

@ -41,7 +41,7 @@ body:
attributes:
label: Version
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
placeholder: ex. 0.5.3
validations:
required: true

70
.github/ISSUE_TEMPLATE/charting.yml vendored Normal file
View file

@ -0,0 +1,70 @@
name: Charting Issue
description: Report an issue with the placement of notes in the game.
labels: ["type: charting issue", "status: pending triage"]
title: "Charting Issue: "
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 my issue
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
- type: dropdown
attributes:
label: Platform
description: Which platform are you playing on?
options:
- 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: Browser
description: (Web/HTML5 users only) Which browser are you playing on?
options:
- Google Chrome
- Microsoft Edge
- Firefox
- Opera
- Safari
- Other (Specify in Description field)
- type: input
attributes:
label: Version
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
placeholder: ex. 0.5.3
validations:
required: true
- type: markdown
attributes:
value: "## Describe the charting issue(s)."
- type: markdown
attributes:
value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
- type: textarea
attributes:
label: Location
description: Where did you find the issue(s)? Include the name of the song, the variation, the difficulty, the time/section of the song, and any images or videos.
placeholder: ex. Cocoa Erect on Erect/Nightmare difficulties at Section 30
validations:
required: true
- type: textarea
attributes:
label: Description
description: Why do you believe this is an issue? Is a note at the wrong time, a hold note too short/long, or something else?
placeholder: Describe the charting issue(s) here...
validations:
required: true

47
.github/ISSUE_TEMPLATE/compiling.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Compiling Report
description: Report an issue with compiling the game.
labels: ["type: compilation help", "status: pending triage"]
title: "Compiling Report: "
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 my issue
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
- type: dropdown
attributes:
label: Platform
description: Which platform are you compiling to/for?
options:
- Web/HTML5
- Desktop
- Other
validations:
required: true
- type: input
attributes:
label: Version
description: Which version are you compiling? The game version is in the bottom left corner of the main menu or in the project.hxp file.
placeholder: ex. 0.5.3
validations:
required: true
- type: markdown
attributes:
value: "## Describe your compiling error."
- type: markdown
attributes:
value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
- type: textarea
attributes:
label: Description (include any images, videos, errors of terminal or console, error 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

View file

@ -41,7 +41,7 @@ body:
attributes:
label: Version
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
placeholder: ex. 0.5.3
validations:
required: true

View file

@ -1,12 +1,12 @@
# Add 'small' to any changes below 10 lines
small:
# Add 'size: small' to any changes below 10 lines
'size: small':
max: 9
# Add 'medium' to any changes between 10 and 100 lines
medium:
# Add 'size: medium' to any changes between 10 and 100 lines
'size: medium':
min: 10
max: 99
# Add 'large' to any changes for more than 100 lines
large:
# Add 'size: large' to any changes of at least 100 lines
'size: large':
min: 100

View file

@ -7,38 +7,151 @@
comment: >
This issue is a duplicate. Please direct all discussion to the original issue.
# Close the issue
close: true
close: false
# Set a close reason
# close-reason: 'not planned'
# Remove other status labels
unlabel:
- 'status: accepted'
- 'status: bug reproduced'
- 'status: cannot reproduce'
- 'status: needs clarification'
- 'status: needs r&d'
- 'status: needs revision'
- 'status: pending pull request'
- 'status: pending triage'
# Set a close reason
close-reason: 'not planned'
- 'status: planned'
- 'status: rejected'
- 'status: resolved'
- 'status: resolved internally'
- 'status: reviewing internally'
- 'status: stale'
prs:
# Post a comment
comment: >
This pull request is a duplicate. Please direct all discussion to the original pull request.
# Close the pull request
close: false
# Remove other status labels
unlabel:
- 'status: accepted'
- 'status: bug reproduced'
- 'status: cannot reproduce'
- 'status: needs clarification'
- 'status: needs r&d'
- 'status: needs revision'
- 'status: pending pull request'
- 'status: pending triage'
# Close the pull request
- 'status: planned'
- 'status: rejected'
- 'status: resolved'
- 'status: resolved internally'
- 'status: reviewing internally'
- 'status: stale'
'status: stale':
issues:
# Close the issue
close: true
# Set a close reason
close-reason: 'not planned'
# Remove other status labels
unlabel:
- 'status: accepted'
- 'status: bug reproduced'
- 'status: cannot reproduce'
- 'status: duplicate'
- 'status: needs clarification'
- 'status: needs r&d'
- 'status: needs revision'
- 'status: pending pull request'
- 'status: pending triage'
- 'status: planned'
- 'status: rejected'
- 'status: resolved'
- 'status: resolved internally'
- 'status: reviewing internally'
prs:
# Close the pull request
close: true
# Remove other status labels
unlabel:
- 'status: accepted'
- 'status: bug reproduced'
- 'status: cannot reproduce'
- 'status: duplicate'
- 'status: needs clarification'
- 'status: needs r&d'
- 'status: needs revision'
- 'status: pending pull request'
- 'status: pending triage'
- 'status: planned'
- 'status: rejected'
- 'status: resolved'
- 'status: resolved internally'
- 'status: reviewing internally'
'status: rejected':
issues:
# Close the issue
close: true
# Remove other status labels
unlabel:
- 'status: pending triage'
# Set a close reason
close-reason: 'not planned'
# Remove other status labels
unlabel:
- 'status: accepted'
- 'status: bug reproduced'
- 'status: cannot reproduce'
- 'status: duplicate'
- 'status: needs clarification'
- 'status: needs r&d'
- 'status: needs revision'
- 'status: pending pull request'
- 'status: pending triage'
- 'status: planned'
- 'status: resolved'
- 'status: resolved internally'
- 'status: reviewing internally'
- 'status: stale'
prs:
# Close the pull request
close: true
# Remove other status labels
unlabel:
- 'status: accepted'
- 'status: bug reproduced'
- 'status: cannot reproduce'
- 'status: duplicate'
- 'status: needs clarification'
- 'status: needs r&d'
- 'status: needs revision'
- 'status: pending pull request'
- 'status: pending triage'
- 'status: planned'
- 'status: resolved'
- 'status: resolved internally'
- 'status: reviewing internally'
- 'status: stale'
'status: resolved':
issues:
# Close the issue
close: true
# Set a close reason
close-reason: 'not planned'
close-reason: 'completed'
# Remove other status labels
unlabel:
- 'status: accepted'
- 'status: bug reproduced'
- 'status: cannot reproduce'
- 'status: duplicate'
- 'status: needs clarification'
- 'status: needs r&d'
- 'status: needs revision'
- 'status: pending pull request'
- 'status: pending triage'
- 'status: planned'
- 'status: rejected'
- 'status: resolved internally'
- 'status: reviewing internally'
- 'status: stale'

15
.github/labeler.yml vendored
View file

@ -1,11 +1,18 @@
# Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder
Documentation:
# Add Documentation tag to PR's changing markdown files, or anything in the docs folder
'pr: documentation':
- changed-files:
- any-glob-to-any-file:
- docs/*
- '**/*.md'
# Adds Haxe tag to PR's changing haxe code files
Haxe:
# Add Haxe tag to PR's changing haxe code files
'pr: haxe':
- changed-files:
- any-glob-to-any-file: '**/*.hx'
# Add GitHub tag to PR's changing yml files, or anything in the .github folder
'pr: github':
- changed-files:
- any-glob-to-any-file:
- github/*
- '**/*.yml'

View file

@ -7,36 +7,6 @@ on:
- edited
jobs:
# When an issue is opened, perform a similarity check for potential duplicates.
# If some are found, add a label and comment listing the potential duplicate issues.
potential-duplicate:
name: Detect potential duplicate issues
runs-on: ubuntu-latest
steps:
- uses: wow-actions/potential-duplicates@v1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Issue title filter work with anymatch https://www.npmjs.com/package/anymatch.
# Any matched issue will stop detection immediately.
# You can specify multi filters in each line.
filter: ''
# Exclude keywords in title before detecting.
exclude: ''
# Label to set, when potential duplicates are detected.
label: 'potential duplicate'
# Get issues with state to compare. Supported state: 'all', 'closed', 'open'.
state: all
# If similarity is higher than this threshold([0,1]), issue will be marked as duplicate.
# Turn this up if the detection is too sensitive
threshold: 0.6
# Reactions to be add to comment when potential duplicates are detected.
# Available reactions: "-1", "+1", "confused", "laugh", "heart", "hooray", "rocket", "eyes"
# reactions: '-1'
# Comment to post when potential duplicates are detected.
comment: >
Potential duplicates: {{#issues}}
- [#{{ number }}] {{ title }} ({{ accuracy }}%)
{{/issues}}
# When an issue is opened, detect if it has an empty body or incomplete issue form.
# If it does, close the issue immediately.
empty-issues:

View file

@ -4,14 +4,64 @@ 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-??
## [0.5.3] - 2024-10-18
This patch resolves a critical issue which could cause user's save data to become corrupted. It is recommended that users switch to this version immediately and avoid using version 0.5.2.
### Fixed
- Fixed an issue where exiting the Chart Editor would sometimes crash the game.
- Fixed an issue where holding down a direction key then selecting a character would select the locked character and crash the game (community fix 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)
- Fixed a critical issue in which the Stage Editor theme value could not be parsed by older versions of the game, resulting in all save data being destroyed.
- Added a check which prevents save data from being loaded if it is corrupted rather than overriding it.
- `optionsStageEditor.theme` in the save data converted from an Enum to a String to fix save data compatibility issues.
- In the future, Enum values should not be used in order to prevent incompatibilities caused by introducing new types to the save data that older versions cannot parse.
- `optionsChartEditor.theme` in the save data converted from an Enum to a String to fix save data compatibility issues.
- `optionsChartEditor.chartEditorLiveInputStyle` in the save data converted from an Enum to a String to fix save data compatibility issues.
- Fixed an issue where some publicly distributed builds of the game were debug builds instead of release builds.
## [0.5.2] - 2024-10-11
### Added
- Added InverseDotsShader that emulates flash selections ([097dbf5](https://github.com/FunkinCrew/Funkin/commit/097dbf5bb4346d431d8ca9f0ec4bc5b5e6f4523f)) - by @ninjamuffin99
- Added a new reworked Stage Editor ([27a0b44](https://github.com/FunkinCrew/Funkin/pull/3482/commits/27a0b4426f86f04362f97e16e2eff580c9402f34)) - by @JustKolosaki in [#3482](https://github.com/FunkinCrew/Funkin/pull/3482)
- Added the `color` attribute to stage prop JSON data to allow them to be tinted without code. ([27a0b44](https://github.com/FunkinCrew/Funkin/pull/3482/commits/27a0b4426f86f04362f97e16e2eff580c9402f34)) - by @JustKolosaki
- Added the `angle` attribute to stage prop JSON data to allow them to be rotated without code. ([27a0b44](https://github.com/FunkinCrew/Funkin/pull/3482/commits/27a0b4426f86f04362f97e16e2eff580c9402f34)) - by @JustKolosaki
- Added the `blend` attribute to the stage prop JSON data to allow blend modes to be applied without code. ([27a0b44](https://github.com/FunkinCrew/Funkin/pull/3482/commits/27a0b4426f86f04362f97e16e2eff580c9402f34)) - by @JustKolosaki
### 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)
- (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 in [#3082](https://github.com/FunkinCrew/Funkin/pull/3082)
- (docs) Fix some misspellings and grammar in code documentation ([2175bea](https://github.com/FunkinCrew/Funkin/commit/2175beaa651e009332202985be4b7eb4ed36e5a4)) - by @Hundrec in [#3477](https://github.com/FunkinCrew/Funkin/pull/3477)
- (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)
### Fixed
- Fix the user song offsets being applied incorrectly, causing stuttering or skipping ([410cfe9](https://github.com/FunkinCrew/Funkin/commit/410cfe972d6df9de4d4d128375cf8380c4f06d92)) - by @JustKolosaki in [#3546](https://github.com/FunkinCrew/Funkin/pull/3546) in [#3506](https://github.com/FunkinCrew/Funkin/pull/3506)
- Fixed issues with variation / difficulty loading for Freeplay Menu which caused some songs to disappear ([c0314c8](https://github.com/FunkinCrew/Funkin/commit/c0314c85ecd5116641aff3de8e9153f7fe48e79c)) - by @ninjamuffin99
- Pico's songs now display properly on the Freeplay Menu ([1d2bd61](https://github.com/FunkinCrew/Funkin/commit/1d2bd61119e5f418df7f11d7ef2a0fdedee17d3d)) - by @ninjamuffin99 in [#3506](https://github.com/FunkinCrew/Funkin/pull/3506)
- Exiting the chart editor doesn't crash the game anymore ([f52472a](https://github.com/FunkinCrew/Funkin/commit/f52472a4767388b22cfbab0f5f7860f6e6762856)) - by @EliteMasterEric and @ianharrigan in [#3519](https://github.com/FunkinCrew/Funkin/pull/3519)
- Disable flickering when attempting to select 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)
- 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)
- Stop 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 in [#3552](https://github.com/FunkinCrew/Funkin/pull/3552)
- 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 in [#3530](https://github.com/FunkinCrew/Funkin/pull/3530)
- 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)
- (debug) No more fullscreening when typing "F" in the flixel debugger console ([29b6763](https://github.com/FunkinCrew/Funkin/commit/29b6763290df05d42039806f3d142740568c80f0)) - by @ninjamuffin99
- Fix crash in LatencyState when exiting / cleaning up state data ([39b1a42](https://github.com/FunkinCrew/Funkin/commit/39b1a42cfeafe2b7be8b66e2fe529e853d9ae197)) - by @lemz1 in [#3493](https://github.com/FunkinCrew/Funkin/pull/3493)
- Add additional classes to Polymod blacklist for security ([b0b73c8](https://github.com/FunkinCrew/Funkin/commit/b0b73c83994f33118c6a69550da9ec8ec1c07adc)) - by @EliteMasterEric in [#3558](https://github.com/FunkinCrew/Funkin/pull/3558)
- Stop allowing inputs after selecting a character ([dbf66ac](https://github.com/FunkinCrew/Funkin/commit/dbf66ac250137262866d75f7c1387645b35d88d0)) - by @ACrazyTown in [#3398](https://github.com/FunkinCrew/Funkin/pull/3398)
- Fix conflict with modded StrumlineNote sprite looping animation ([bc546e8](https://github.com/FunkinCrew/Funkin/commit/bc546e86aa77ffc795b3f079de5f590289a9c583)) - by @DaWaterMalone in [#3577](https://github.com/FunkinCrew/Funkin/pull/3577)
- Properly format the millisecond counter in the chart editor playbar ([f1b6e6c](https://github.com/FunkinCrew/Funkin/commit/f1b6e6c4e42455e0c2900d738ebc24893f2479a0)) - by @afreetoplaynoob in [#3537](https://github.com/FunkinCrew/Funkin/pull/3537)
- Fixed an issue where the player and girlfriend would disappear or overlap themselves in Character Select ([community fix by @gamerbross](https://github.com/FunkinCrew/Funkin/pull/3457))
- Fixed an issue where the game would show the wrong girlfriend in Character Select ([community fix by @gamerbross](https://github.com/FunkinCrew/Funkin/pull/3457))
- Fixed an issue where the cursor wouldn't update properly in Character Select ([community fix by @gamerbross](https://github.com/FunkinCrew/Funkin/pull/3457))
- Fixed an issue where the player would display double after entering character select or when spamming buttons ([community fix by @gamerbross](https://github.com/FunkinCrew/Funkin/pull/3457))
## New Contributors for 0.5.2
* @JustKolosaki made their first contribution in [#3482](https://github.com/FunkinCrew/Funkin/pull/3482)
* @Kn1ghtNight made their first contribution in [#2853](https://github.com/FunkinCrew/Funkin/pull/2853)
* @DaWaterMalone made their first contribution in [#3577](https://github.com/FunkinCrew/Funkin/pull/3577)
* @amyspark-ng made their first contribution in [#3552](https://github.com/FunkinCrew/Funkin/pull/3552)
* @Cartridge-Man made their first contribution in [#3082](https://github.com/FunkinCrew/Funkin/pull/3082)
* @afreetoplaynoob made their first contribution in [#3537](https://github.com/FunkinCrew/Funkin/pull/3537)
## [0.5.1] - 2024-09-30
### Added
@ -28,7 +78,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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)
- Added the `flipX` and `flipY` parameters to props in the Stage data. ([community feature by AbnormalPoof](https://github.com/FunkinCrew/Funkin/pull/3474))
### 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`.
@ -77,18 +127,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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)
- Fixed an issue where parts of the Chart Editor could not be interacted with. ([community fix by Kade-github](https://github.com/FunkinCrew/Funkin/pull/3337))
- Fixed an issue where classic FocusCamera song events could cause the camera to snap in place. ([community fix by nebulazorua](https://github.com/FunkinCrew/Funkin/pull/2331))
- 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](https://github.com/FunkinCrew/Funkin/pull/3019))
- Fixed an issue where the FPS counter would display even if Debug Display was turned off. ([community fix by Lethrial](https://github.com/FunkinCrew/Funkin/pull/3356))
- 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 NotHyper-474](https://github.com/FunkinCrew/Funkin/pull/3093))
- Fixed an issue where pixel icons in the Chart Editor would not display correctly. ([community fix by Techniktil](https://github.com/FunkinCrew/Funkin/pull/3339))
- Fixed an issue where `Stage.addCharacter` would not properly assign the `characterType`. ([community fix by Kade-github](https://github.com/FunkinCrew/Funkin/pull/3357))
- Fixed an issue where players could interact with Character Select during the unlock sequence, causing a crash. ([community fix by ActualMandM](https://github.com/FunkinCrew/Funkin/pull/3355))
- Fixed an issue where hold notes in Week 6 were not scaled/positioned correctly. ([community fix by dombomb64](https://github.com/FunkinCrew/Funkin/pull/3351))
- Fixed an issue where audio offets would not interact with the Chart Editor properly. ([community fix by Kade-github](https://github.com/FunkinCrew/Funkin/pull/3384))
- Fixed an issue where fetching Modules during the `onDestroy` event would fail at random. ([community fix by cyn0x8](https://github.com/FunkinCrew/Funkin/pull/3131))
- Fixed an issue where `onSubStateOpenEnd` and `onSubStateCloseEnd` script events would not always get called. ([community fix by lemz1](https://github.com/FunkinCrew/Funkin/pull/3138))
## New Contributors for 0.5.1
* @Lethrial made their first contribution in [#3356](https://github.com/FunkinCrew/Funkin/pull/3356)
* @dombomb64 made their first contribution in [#3351](https://github.com/FunkinCrew/Funkin/pull/3351)
## [0.5.0] - 2024-09-12
### Added
@ -125,8 +180,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Implemented alternate animations and music for Pico in the results screen.
- These display on Pico remixes, as well as when playing Weekend 1.
- Implemented support for scripted Note Kinds. You can use HScript define a different note style to display for these notes as well as custom behavior. (community feature by lemz1)
- Implemented support for Numeric and Selector options in the Options menu. (community feature by FlooferLand)
- Implemented new animations for Tankman and Pico
- Implemented support for Numeric and Selector options in the Options menu. ([community feature by FlooferLand](https://github.com/FunkinCrew/Funkin/pull/2942))
- Implemented new animations for Tankman and Pico.
## Changed
- Girlfriend and Nene now perform previously unused animations when you achieve a large combo, or drop a large combo.
- The pixel character icons in the Freeplay menu now display an animation!
@ -134,40 +189,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Character offsets are now independent of the character's scale.
- This should resolve issues with offsets when porting characters from older mods.
- Pixel character offsets have been modified to compensate.
- Note style data can now specify custom combo count graphics, judgement graphics, countdown graphics, and countdown audio. (community feature by anysad)
- Note style data can now specify custom combo count graphics, judgement graphics, countdown graphics, and countdown audio. ([community feature by anysad](https://github.com/FunkinCrew/Funkin/pull/3020))
- These were previously using hardcoded values based on whether the stage was `school` or `schoolEvil`.
- The `danceEvery` property of characters and stage props can now use values with a precision of `0.25`, to play their idle animation up to four times per beat.
- Reworked the JSON merging system in Polymod; you can now include JSONPatch files under `_merge` in your mod folder to add, modify, or remove values in a JSON without replacing it entirely!
- Cutscenes now automatically pause when tabbing out (community fix by AbnormalPoof)
- Characters will now respect the `danceEvery` property (community fix by gamerbross)
- The F5 function now reloads the current song's chart data from disc (community feature by gamerbross)
- Refactored the compilation guide and added common troubleshooting steps (community fix by Hundrec)
- Made several layout improvements and fixes to the Animation Offsets editor in the Debug menu (community fix by gamerbross)
- Fixed a bug where the Back sound would be not played when leaving the Story menu and Options menu (community fix by AppleHair)
- Animation offsets no longer directly modify the `x` and `y` position of props, which makes props work better with tweens (community fix by Sword352)
- The YEAH! events in Tutorial now use chart events rather than being hard-coded (community fix by anysad)
- Cutscenes now automatically pause when tabbing out ([community fix by AbnormalPoof](https://github.com/FunkinCrew/Funkin/pull/2903))
- Characters will now respect the `danceEvery` property ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2925))
- The F5 function now reloads the current song's chart data from disc ([community feature by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2990))
- Refactored the compilation guide and added common troubleshooting steps ([community fix by Hundrec](https://github.com/FunkinCrew/Funkin/pull/2813))
- Made several layout improvements and fixes to the Animation Offsets editor in the Debug menu ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2820))
- Fixed a bug where the Back sound would be not played when leaving the Story menu and Options menu ([community fix by AppleHair](https://github.com/FunkinCrew/Funkin/pull/2986))
- Animation offsets no longer directly modify the `x` and `y` position of props, which makes props work better with tweens ([community fix by Sword352](https://github.com/FunkinCrew/Funkin/pull/2310))
- The YEAH! events in Tutorial now use chart events rather than being hard-coded ([community fix by anysad](https://github.com/FunkinCrew/Funkin/pull/3007))
- The player's Score now displays commas in it (community fix by loggo)
## Fixed
- Fixed an issue where songs with no notes would crash on the Results screen.
- Fixed an issue where the old icon easter egg would not work properly on pixel levels.
- Fixed an issue where you could play notes during the Thorns cutscene.
- Fixed an issue where the Heart icon when favoriting a song in Freeplay would be malformed.
- Fixed an issue where Pico's death animation displays a faint blue background (community fix by doggogit)
- Fixed an issue where mod songs would not play a preview in the Freeplay menu (community fix by KarimAkra)
- Fixed an issue where the Memory Usage counter could overflow and display a negative number (community fix by KarimAkra)
- Fixed an issue where pressing the Chart Editor keybind while playtesting a chart would reset the chart editor (community fix by gamerbross)
- Fixed a crash bug when pressing F5 after seeing the sticker transition (community fix by gamerbross)
- Fixed an issue where the Story Mode menu couldn't be scrolled with a mouse (community fix by JVNpixels)
- Fixed an issue causing the song to majorly desync sometimes (community fix by Burgerballs)
- Fixed an issue where the Freeplay song preview would not respect the instrumental ID specified in the song metadata (community fix by AppleHair)
- Fixed an issue where Tankman's icon wouldn't display in the Chart Editor (community fix by hundrec)
- Fixed an issue where pausing the game during a camera zoom would zoom the pause menu. (community fix by gamerbros)
- Fixed an issue where certain UI elements would not flash at a consistent rate (community fix by cyn0x8)
- Fixed an issue where the game would not use the placeholder health icon as a fallback (community fix by gamerbross)
- Fixed an issue where the chart editor could get stuck creating a hold note when using Live Inputs (community fix by gamerbross)
- Fixed an issue where character graphics could not be placed in week folders (community fix by 7oltan)
- Fixed a crash issue when a Freeplay song has no `Normal` difficulty (community fix by Applehair and gamerbross)
- Fixed an issue in Story Mode where a song that isn't valid for the current variation could be selected (community fix by Applehair)
- Fixed an issue where Pico's death animation displays a faint blue background ([community fix by doggogit](https://github.com/FunkinCrew/funkin.assets/pull/1))
- Fixed an issue where mod songs would not play a preview in the Freeplay menu ([community fix by KarimAkra](https://github.com/FunkinCrew/Funkin/pull/2724))
- Fixed an issue where the Memory Usage counter could overflow and display a negative number ([community fix by KarimAkra](https://github.com/FunkinCrew/Funkin/pull/2713))
- Fixed an issue where pressing the Chart Editor keybind while playtesting a chart would reset the chart editor ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2739))
- Fixed a crash bug when pressing F5 after seeing the sticker transition ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2863))
- Fixed an issue where the Story Mode menu couldn't be scrolled with a mouse ([community fix by JVNpixels](https://github.com/FunkinCrew/Funkin/pull/2873))
- Fixed an issue causing the song to majorly desync sometimes ([community fix by Burgerballs](https://github.com/FunkinCrew/Funkin/pull/3058))
- Fixed an issue where the Freeplay song preview would not respect the instrumental ID specified in the song metadata ([community fix by AppleHair](https://github.com/FunkinCrew/Funkin/pull/2742))
- Fixed an issue where Tankman's icon wouldn't display in the Chart Editor ([community fix by Hundrec](https://github.com/FunkinCrew/Funkin/pull/2912))
- Fixed an issue where pausing the game during a camera zoom would zoom the pause menu. ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2567))
- Fixed an issue where certain UI elements would not flash at a consistent rate ([community fix by cyn0x8](https://github.com/FunkinCrew/Funkin/pull/2494))
- Fixed an issue where the game would not use the placeholder health icon as a fallback ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/3005))
- Fixed an issue where the chart editor could get stuck creating a hold note when using Live Inputs ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2992))
- Fixed an issue where character graphics could not be placed in week folders ([community fix by 7oltan](https://github.com/FunkinCrew/Funkin/pull/3035))
- Fixed a crash issue when a Freeplay song has no `Normal` difficulty ([community fix by AppleHair](https://github.com/FunkinCrew/Funkin/pull/3036) and [gamerbross](https://github.com/FunkinCrew/Funkin/pull/2712))
- Fixed an issue in Story Mode where a song that isn't valid for the current variation could be selected ([community fix by AppleHair](https://github.com/FunkinCrew/Funkin/pull/3037))
## New Contributors for 0.5.0
* @Flooferland made their first contribution in [#2942](https://github.com/FunkinCrew/Funkin/pull/2942)
* @anysad made their first contribution in [#3007](https://github.com/FunkinCrew/Funkin/pull/3007)
* @Sword352 made their first contribution in [#2310](https://github.com/FunkinCrew/Funkin/pull/2310)
* @KarimAkra made their first contribution in [#2713](https://github.com/FunkinCrew/Funkin/pull/2713)
* @JVNpixels made their first contribution in [#2873](https://github.com/FunkinCrew/Funkin/pull/2873)
* @AppleHair made their first contribution in [#2742](https://github.com/FunkinCrew/Funkin/pull/2742)
* @7oltan made their first contribution in [#3035](https://github.com/FunkinCrew/Funkin/pull/3035)
* @cyn0x8 made their first contribution in [#2494](https://github.com/FunkinCrew/Funkin/pull/2494)
## [0.4.1] - 2024-06-12
### Added
@ -176,7 +242,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Highscores and ranks are now saved separately, which fixes the issue where people would overwrite their saves with higher scores,
which would remove their rank if they had a lower one.
- A-Bot speaker now reacts to the user's volume preference on desktop (thanks to [M7theguy for the issue report/suggestion](https://github.com/FunkinCrew/Funkin/issues/2744)!)
- A-Bot speaker now reacts to the user's volume preference on desktop ([thanks to M7theguy for the issue report/suggestion](https://github.com/FunkinCrew/Funkin/issues/2744)!)
- On Freeplay, heart icons are shifted to the right when you favorite a song that has no rank on it.
- Only play `scrollMenu` sound effect when there's a real change on the freeplay menu ([thanks gamerbross for the PR!](https://github.com/FunkinCrew/Funkin/pull/2741))
- Gave antialiasing to the edge of the dad graphic on Freeplay
@ -184,20 +250,25 @@ which would remove their rank if they had a lower one.
- Made several chart revisions
- Re-enabled custom camera events in Roses (Erect/Nightmare)
- Tweaked the chart for Lit Up (Hard)
- Corrected the difficulty ratings for M.I.L.F. (Easy/Normal/Hard)
- Corrected the difficulty ratings for M.I.L.F (Easy/Normal/Hard)
### Fixed
- Fixed an issue in the controls menu where some control binds would overlap their names
- Fixed crash when attempting to exit the gameover screen when also attempting to retry the song ([thanks DMMaster636 for the PR!](https://github.com/FunkinCrew/Funkin/pull/2709))
- Fix botplay sustain release bug ([thanks Hundrec!](Fix botplay sustain release bug #2683))
- Fix botplay sustain release bug ([thanks Hundrec!](https://github.com/FunkinCrew/Funkin/pull/2683))
- Fix for the camera not pausing during a gameplay pause ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2684))
- Fixed issue where Pico's gameplay sprite would unintentionally appear on the gameover screen when dying on 2Hot from an explosion
- Fixed issue where Pico's gameplay sprite would unintentionally appear on the gameover screen when dying on 2hot from an explosion
- Freeplay previews properly fade volume during the BF idle animation
- Fixed bug where Dadbattle incorrectly appeared as Dadbattle Erect when returning to freeplay on Hard
- Fixed 2Hot not appearing under the "#" category in Freeplay menu
- Fixed bug where DadBattle incorrectly appeared as DadBattle Erect when returning to freeplay on Hard
- Fixed 2hot not appearing under the "#" category in Freeplay menu
- Fixed a bug where the Chart Editor would crash when attempting to select an event with the Event toolbox open
- Improved offsets for Pico and Tankman opponents so they don't slide around as much.
- Fixed the black "temp" graphic on freeplay from being incorrectly sized / masked, now it's identical to the dad freeplay graphic
## New Contributors for 0.4.1
* @DMMaster636 made their first contribution in [#2709](https://github.com/FunkinCrew/Funkin/pull/2709)
* @Hundrec made their first contribution in [#2683](https://github.com/FunkinCrew/Funkin/pull/2683)
## [0.4.0] - 2024-06-06
### Added
- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from the Freeplay menu!
@ -206,14 +277,14 @@ which would remove their rank if they had a lower one.
- Freeplay now plays a preview of songs when you hover over them.
- Added a Charter field to the chart format, to allow for crediting the creator of a level's chart.
- You can see who charted a song from the Pause menu.
- Added a new Scroll Speed chart event to change the note speed mid-song (thanks burgerballs!)
- Added a new Scroll Speed chart event to change the note speed mid-song ([thanks Burgerballs!](https://github.com/FunkinCrew/Funkin/pull/2409))
### Changed
- Tweaked the charts for several songs:
- Tutorial (increased the note speed slightly)
- Spookeez
- Monster
- Winter Horrorland
- M.I.L.F.
- M.I.L.F
- Senpai (increased the note speed)
- Roses
- Thorns (increased the note speed slightly)
@ -223,55 +294,75 @@ which would remove their rank if they had a lower one.
- Favorite songs marked in Freeplay are now stored between sessions.
- The Freeplay easter eggs are now easier to see.
- In the event that the game cannot load your save data, it will now perform a backup before clearing it, so that we can try to repair it in the future.
- Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. (thanks Keoiki!)
- Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. (thanks gamerbross!)
- Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. ([thanks Keoiki!](https://github.com/FunkinCrew/Funkin/pull/2581))
- Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2593))
- Remember that for more complex behaviors such as animations or transitions, you should use an XML file to define each frame.
- Improved the Event Toolbox in the Chart Editor; dropdowns are now bigger, include search field, and display elements in alphabetical order rather than a random order.
### Fixed
- Fixed an issue where Nene's visualizer would not play on Desktop builds
- Fixed a bug where the game would silently fail to load saves on HTML5
- Fixed some bugs with the props on the Story Menu not bopping properly
- Additional fixes to the Loading bar on HTML5 (thanks lemz1!)
- Fixed several bugs with the TitleState, including missing music when returning from the Main Menu (thanks gamerbross!)
- Fixed a camera bug in the Main Menu (thanks richTrash21!)
- Fixed a bug where changing difficulties in Story mode wouldn't update the score (thanks sectorA!)
- Fixed a crash in Freeplay caused by a level referencing an invalid song (thanks gamerbross!)
- Fixed a bug where pressing the volume keys would stop the Toy commercial (thanks gamerbross!)
- Fixed a bug where the Chart Editor Playtest would crash when losing (thanks gamerbross!)
- Fixed a bug where hold notes would display improperly in the Chart Editor when downscroll was enabled for gameplay (thanks gamerbross!)
- Fixed a bug where hold notes would be positioned wrong on downscroll (thanks MaybeMaru!)
- Removed a large number of unused imports to optimize builds (thanks Ethan-makes-music!)
- Improved debug logging for unscripted stages (thanks gamerbross!)
- Additional fixes to the Loading bar on HTML5 ([thanks lemz1!](https://github.com/FunkinCrew/Funkin/pull/2553))
- Fixed several bugs with the TitleState, including missing music when returning from the Main Menu ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2539))
- Fixed a camera bug in the Main Menu ([thanks richTrash21!](https://github.com/FunkinCrew/Funkin/pull/2576))
- Fixed a bug where changing difficulties in Story mode wouldn't update the score ([thanks sector-a!](https://github.com/FunkinCrew/Funkin/pull/2585))
- Fixed a crash in Freeplay caused by a level referencing an invalid song ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2457))
- Fixed a bug where pressing the volume keys would stop the Toy commercial ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2540))
- Fixed a bug where the Chart Editor Playtest would crash when losing ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2518))
- Fixed a bug where hold notes would display improperly in the Chart Editor when downscroll was enabled for gameplay ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2565))
- Fixed a bug where hold notes would be positioned wrong on downscroll ([thanks MaybeMaru!](https://github.com/FunkinCrew/Funkin/pull/2488))
- Removed a large number of unused imports to optimize builds ([thanks Ethan-makes-music!](https://github.com/FunkinCrew/Funkin/pull/2624))
- Improved debug logging for unscripted stages ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2603))
- Fixed a crash on Linux caused by an old version of hxCodec ([thanks Noobz4Life!](https://github.com/FunkinCrew/Funkin/pull/2472))
- Optimized animation handling for characters ([thanks richTrash21!](https://github.com/FunkinCrew/Funkin/pull/2493))
- Made improvements to compiling documentation (thanks gedehari!)
- Fixed a crash on Linux caused by an old version of hxCodec (thanks Noobz4Life!)
- Optimized animation handling for characters (thanks richTrash21!)
- Made improvements to compiling documentation (thanks gedehari!)
- Fixed an issue where the Chart Editor would use an incorrect instrumental on imported Legacy songs (thanks gamerbross!)
- Fixed a camera bug in the Main Menu (thanks richTrash21!)
- Fixed a bug where opening the game from the command line would crash the preloader (thanks NotHyper474!)
- Fixed a bug where characters would sometimes use the wrong scale value (thanks PurSnake!)
- Fixed an issue where the Chart Editor would use an incorrect instrumental on imported Legacy songs ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2604))
- Fixed a bug where opening the game from the command line would crash the preloader ([thanks NotHyper-474!](https://github.com/FunkinCrew/Funkin/pull/2629))
- Fixed a bug where characters would sometimes use the wrong scale value ([thanks PurSnake!](https://github.com/FunkinCrew/Funkin/pull/2610))
- Additional bug fixes and optimizations.
## New Contributors for 0.4.0
* @Keoiki made their first contribution in [#2581](https://github.com/FunkinCrew/Funkin/pull/2581)
* @richTrash21 made their first contribution in [#2493](https://github.com/FunkinCrew/Funkin/pull/2493)
* @sector-a made their first contribution in [#2585](https://github.com/FunkinCrew/Funkin/pull/2585)
* @MaybeMaru made their first contribution in [#2488](https://github.com/FunkinCrew/Funkin/pull/2488)
* @Ethan-makes-music made their first contribution in [#2624](https://github.com/FunkinCrew/Funkin/pull/2624)
* @Noobz4Life made their first contribution in [#2472](https://github.com/FunkinCrew/Funkin/pull/2472)
* @NotHyper-474 made their first contribution in [#2629](https://github.com/FunkinCrew/Funkin/pull/2629)
* @PurSnake made their first contribution in [#2610](https://github.com/FunkinCrew/Funkin/pull/2610)
## [0.3.3] - 2024-05-14
### Changed
- Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!)
- Cleaned up some code in `PlayAnimationSongEvent.hx` ([thanks Burgerballs!](https://github.com/FunkinCrew/Funkin/pull/2308))
### Fixed
- Fixes to the Loading bar on HTML5 (thanks lemz1!)
- Don't allow any more inputs when exiting freeplay (thanks gamerbros!)
- Fixed using mouse wheel to scroll on freeplay (thanks JugieNoob!)
- Fixed the reset's of the health icons, score, and notes when re-entering gameplay from gameover (thanks ImCodist!)
- Fixed the chart editor character selector's hitbox width (thanks MadBear422!)
- Fixed camera stutter once a wipe transition to the Main Menu completes (thanks ImCodist!)
- Fixed an issue where hold note would be invisible for a single frame (thanks ImCodist!)
- Fix tween accumulation on title screen when pressing Y multiple times (thanks TheGaloXx!)
- Fixes to the Loading bar on HTML5 ([thanks lemz1!](https://github.com/FunkinCrew/Funkin/pull/2499))
- Don't allow any more inputs when exiting freeplay ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2470))
- Fixed using mouse wheel to scroll on freeplay ([thanks JugieNoob!](https://github.com/FunkinCrew/Funkin/pull/2466))
- Fixed the resets of the health icons, score, and notes when re-entering gameplay from gameover ([thanks ImCodist!](https://github.com/FunkinCrew/Funkin/pull/2390))
- Fixed the chart editor character selector's hitbox width ([thanks MadBear422!](https://github.com/FunkinCrew/Funkin/pull/2370))
- Fixed camera stutter once a wipe transition to the Main Menu completes ([thanks ImCodist!](https://github.com/FunkinCrew/Funkin/pull/2315))
- Fixed an issue where hold note would be invisible for a single frame ([thanks ImCodist!](https://github.com/FunkinCrew/Funkin/pull/2309))
- Fix tween accumulation on title screen when pressing Y multiple times ([thanks TheGaloXx!](https://github.com/FunkinCrew/Funkin/pull/2300))
- Fix a crash when querying FlxG.state in the crash handler
- Fix for a game over easter egg so you don't accidentally exit it when viewing
- Fix an issue where the Freeplay menu never displays 100% clear
- Fix an issue where Weekend 1 Pico attempted to retrieve a missing asset.
- Fix an issue where duplicate keybinds would be stoed, potentially causing a crash
- Chart debug key now properly returns you to the previous chart editor session if you were playtesting a chart (thanks nebulazorua!)
- Fix an issue where duplicate keybinds would be stored, potentially causing a crash
- Chart debug key now properly returns you to the previous chart editor session if you were playtesting a chart ([thanks nebulazorua!](https://github.com/FunkinCrew/Funkin/pull/2323))
- Fix a crash on Freeplay found on AMD graphics cards
## New Contributors for 0.3.3
* @Burgerballs made their first contribution in [#2308](https://github.com/FunkinCrew/Funkin/pull/2308)
* @lemz1 made their first contribution in [#2499](https://github.com/FunkinCrew/Funkin/pull/2499)
* @gamerbross made their first contribution in [#2470](https://github.com/FunkinCrew/Funkin/pull/2470)
* @JugieNoob made their first contribution in [#2466](https://github.com/FunkinCrew/Funkin/pull/2466)
* @MadBear422 made their first contribution in [#2370](https://github.com/FunkinCrew/Funkin/pull/2370)
* @ImCodist made their first contribution in [#2309](https://github.com/FunkinCrew/Funkin/pull/2309)
* @TheGaloXx made their first contribution in [#2300](https://github.com/FunkinCrew/Funkin/pull/2300)
* @nebulazorua made their first contribution in [#2323](https://github.com/FunkinCrew/Funkin/pull/2323)
## [0.3.2] - 2024-05-03
### Added
- Added `,` and `.` keybinds to the Chart Editor. These place Focus Camera events at the playhead, for the opponent and player respectively.
@ -297,6 +388,7 @@ which would remove their rank if they had a lower one.
### Removed
- Removed some unused `.txt` files in the `assets/data` folder.
## [0.3.1] - 2024-05-01
### Changed
- Ensure the Git commit hash always displays in the log files.
@ -313,6 +405,7 @@ which would remove their rank if they had a lower one.
- When exiting from a song into freeplay, main menu no longer takes inputs unintentionally (aka issues with merch links opening up when selecting songs)
- Fix for arrow keys causing web browser page scroll
## [0.3.0] - 2024-04-30
### Added
- New Story Level: Weekend 1, starting Pico, Darnell, and Nene.
@ -336,7 +429,7 @@ which would remove their rank if they had a lower one.
- Improvements to video cutscenes and dialogue, allowing them to be easily skipped or restarted.
- Updated Polymod by several major versions, allowing for fully dynamic asset replacement and support for scripted classes.
- Completely refactored almost every part of the game's code for performance, stability, and extensibility.
- This is not the Ludem Dare game held together with sticks and glue you played three years ago.
- This is not the Ludum Dare game held together with sticks and glue you played three years ago.
- Characters, stages, songs, story levels, and dialogue are now built from JSON data registries rather than being hardcoded.
- All of these also support attaching scripts for custom behavior, more documentation on this soon.
- You can forcibly reload the game's JSON data and scripts by pressing F5.
@ -346,6 +439,7 @@ which would remove their rank if they had a lower one.
### Fixed
- 17 quadrillion bugs across hundreds of PRs.
## [0.2.8] - 2021-04-18 (note, this one is iffy cuz we slacked wit it lol!)
### Added
- TANKMAN! 3 NEW SONGS BY KAWAISPRITE (UGH, GUNS, STRESS)! Charting help by MtH!
@ -360,6 +454,7 @@ which would remove their rank if they had a lower one.
### Fixed
- That one random note on Bopeebo
## [0.2.7.1] - 2021-02-14
### Added
- Easter eggs
@ -370,13 +465,14 @@ which would remove their rank if they had a lower one.
- Offset of the Newgrounds logo on boot screen.
- Made the changelog txt so it can be opened easier by normal people who don't have a markdown reader (most normal people);
### Fixed
- Fixed crashes on Week 6 story mode dialogue if spam too fast ([Thanks to Lotusotho for the Pull Request!](https://github.com/ninjamuffin99/Funkin/pull/357))
- Fixed crashes on Week 6 story mode dialogue if spam too fast ([Thanks to Lotusotho for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/357))
- Should show intro credits on desktop versions of the game more consistently
- Layering on Week 4 songs with GF and the LIMO LOL HOW TF I MISS THIS
- Chart's and chart editor now support changeBPM, GOD BLESS MTH FOR THIS ONE I BEEN STRUGGLIN WIT THAT SINCE OCTOBER LMAO ([GOD BLESS MTH](https://github.com/ninjamuffin99/Funkin/pull/382))
- Fixed sustain note trails ALSO THANKS TO MTH U A REAL ONE ([MTH VERY POWERFUL](https://github.com/ninjamuffin99/Funkin/pull/415))
- Chart's and chart editor now support changeBPM, GOD BLESS MTH FOR THIS ONE I BEEN STRUGGLIN WIT THAT SINCE OCTOBER LMAO ([GOD BLESS MTH](https://github.com/FunkinCrew/Funkin/pull/382))
- Fixed sustain note trails ALSO THANKS TO MTH U A REAL ONE ([MTH VERY POWERFUL](https://github.com/FunkinCrew/Funkin/pull/415))
- Antialiasing on the skyscraper lights
## [0.2.7] - 2021-02-02
### Added
- PIXEL DAY UPDATE LOL 1 WEEK LATER
@ -388,12 +484,13 @@ which would remove their rank if they had a lower one.
- Made it so you lose sliiiightly more health when you miss a note.
- Removed the default HaxeFlixel pause screen when the game window loses focus, can get screenshots of the game easier hehehe
### Fixed
- Idle animation bug with BF christmas and BF hair blow sprites ([Thanks to Injourn for the Pull Request!](https://github.com/ninjamuffin99/Funkin/pull/237))
- Idle animation bug with BF christmas and BF hair blow sprites ([Thanks to Injourn for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/237))
## [0.2.6] - 2021-01-20
### Added
- 3 NEW CHRISTMAS SONGS. 2 BY KAWAISPRITE, 1 BY BASSETFILMS!!!!! BF WITH DRIP! SANTA HANGIN OUT!
- Enemy icons change when they you are winning a lot ([Thanks to pahaze for the Pull Request!](https://github.com/ninjamuffin99/Funkin/pull/138))
- Enemy icons change when they you are winning a lot ([Thanks to pahaze for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/138))
- Holding CTRL in charting editor places notes on both sides
- Q and E changes sustain lengths in note editor
- Other charting editor workflow improvements
@ -405,11 +502,12 @@ which would remove their rank if they had a lower one.
- Removed APE
### Fixed
- Maybe fixed double notes / jump notes. Need to tweak it for balance, but should open things up for cooler charts in the future.
- Old Verison popup screen weirdness ([Thanks to gedehari for the Pull Request!](https://github.com/ninjamuffin99/Funkin/pull/155))
- Song no longer loops when finishing the song. ([Thanks Injourn for the Pull Request!](https://github.com/ninjamuffin99/Funkin/pull/132))
- Old Verison popup screen weirdness ([Thanks to gedehari for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/155))
- Song no longer loops when finishing the song. ([Thanks Injourn for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/132))
- Screen wipe being cut off in the limo/mom stage. Should fill the whole screen now.
- Boyfriend animations on hold notes, and pressing on repeating notes should behave differently
## [0.2.5] - 2020-12-27
### Added
- MOMMY GF, 3 NEW ASS SONGS BY KAWAISPRITE, NEW ART BY PHANTOMARCADE,WOOOOOOAH!!!!
@ -424,8 +522,9 @@ which would remove their rank if they had a lower one.
- Mouse is now visible in note editor
### Fixed
- Crash when playing Week 3 and then playing a non-week 3 song
- When pausing music at the start, it doesn't continue the song anyways. ([shoutouts gedehari for the Pull Request!](https://github.com/ninjamuffin99/Funkin/pull/48))
- IDK i think backing out of song menu should play main menu songs again hehe ([shoutouts gedehari for the Pull Request!](https://github.com/ninjamuffin99/Funkin/pull/48))
- When pausing music at the start, it doesn't continue the song anyways. ([shoutouts gedehari for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/48))
- IDK i think backing out of song menu should play main menu songs again hehe ([shoutouts gedehari for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/48))
## [0.2.4] - 2020-12-11
### Added
@ -435,17 +534,19 @@ which would remove their rank if they had a lower one.
### Changed
- Made it less punishing to ATTEMPT to hit a note and miss, rather than let it pass you
### Fixed
- Song desync of you paused and unpaused frequently ([shoutouts SonicBlam](https://github.com/ninjamuffin99/Funkin/issues/37))
- Song desync of you paused and unpaused frequently ([shoutouts SonicBlam](https://github.com/FunkinCrew/Funkin/issues/37))
- Animation offsets when GF is scared
## [0.2.3] - 2020-12-04
### Added
- More intro texts
### Fixed
- Exploit where you could potentially give yourself a high score via the debug menu
- Issue/bug where you could spam the confirm button on the story menu ([shoutouts lotusotho for the CODE contribution/pull request!](https://github.com/ninjamuffin99/Funkin/pull/19))
- Glitch where if you never would lose health if you missed a note on a fast song (shoutouts [MrDulfin](https://github.com/ninjamuffin99/Funkin/issues/10), [HotSauceBurritos](https://github.com/ninjamuffin99/Funkin/issues/13) and [LobsterMango](https://lobstermango.newgrounds.com))
- Fixed tiny note bleed over thingies (shoutouts [lotusotho](https://github.com/ninjamuffin99/Funkin/pull/24))
- Issue/bug where you could spam the confirm button on the story menu ([shoutouts lotusotho for the CODE contribution/pull request!](https://github.com/FunkinCrew/Funkin/pull/19))
- Glitch where if you never would lose health if you missed a note on a fast song (shoutouts [MrDulfin](https://github.com/FunkinCrew/Funkin/issues/10), [HotSauceBurritos](https://github.com/FunkinCrew/Funkin/issues/13) and [LobsterMango](https://lobstermango.newgrounds.com))
- Fixed tiny note bleed over thingies (shoutouts [lotusotho](https://github.com/FunkinCrew/Funkin/pull/24))
## [0.2.2] - 2020-11-20
### Added
@ -454,16 +555,16 @@ which would remove their rank if they had a lower one.
- Score now shows mid-song.
- Menu on pause screen! Can resume, and restart song, or go back to main menu.
- New music made for pause menu!
### Changed
- Moved all the intro texts to its own txt file instead of being hardcoded, this allows for much easier customization. File is in the data folder, called "introText.txt", follow the format in there and you're probably good to go!
### Fixed
- Fixed soft lock when pausing on song finish ([shoutouts gedehari](https://github.com/ninjamuffin99/Funkin/issues/15))
- Think I fixed issue that led to in-game scores being off by 2 ([shoutouts Mike](https://github.com/ninjamuffin99/Funkin/issues/4))
- Should have fixed the 1 frame note appearance thing. ([shoutouts Mike](https://github.com/ninjamuffin99/Funkin/issues/6))
- Fixed soft lock when pausing on song finish ([shoutouts gedehari](https://github.com/FunkinCrew/Funkin/issues/15))
- Think I fixed issue that led to in-game scores being off by 2 ([shoutouts Mike](https://github.com/FunkinCrew/Funkin/issues/4))
- Should have fixed the 1 frame note appearance thing. ([shoutouts Mike](https://github.com/FunkinCrew/Funkin/issues/6))
- Cleaned up some charting on South on hard mode
- Fixed some animation timings, should feel both better to play, and watch. (shoutouts Dave/Ivan lol)
- Animation issue where GF would freak out on the title screen if you returned to it([shoutouts MultiXIII](https://github.com/ninjamuffin99/Funkin/issues/12)).
- Animation issue where GF would freak out on the title screen if you returned to it([shoutouts MultiXIII](https://github.com/FunkinCrew/Funkin/issues/12)).
## [0.2.1.2] - 2020-11-06
### Fixed
@ -471,10 +572,12 @@ which would remove their rank if they had a lower one.
- Difficulty on storymode and in freeplay scores
- Hard mode difficulty on campaign levels have been fixed
## [0.2.1.1] - 2020-11-06
### Fixed
- Week 2 not unlocking properly
## [0.2.1] - 2020-11-06
### Added
- Scores to the freeplay menu
@ -482,18 +585,18 @@ which would remove their rank if they had a lower one.
- Lightning effect in Spooky stages
- Campaign scores, can now compete on scoreboards for campaign!
- Can now change difficulties in Freeplay mode
### Changed
- Balanced out Normal mode for the harder songs(Dadbattle and Spookeez, not South yet). Should be much easier all around.
- Balanced out Normal mode for the harder songs(DadBattle and Spookeez, not South yet). Should be much easier all around.
- Put tutorial in it's own 'week', so that if you want to play week 1, you don't have to play the tutorial.
### Fixed
- One of the charting bits on South and Spookeez during the intro.
## [0.2.0] - 2020-11-01
### Added
- Uhh Newgrounds release lolol I always lose track of shit.
## [0.1.0] - 2020-10-05
### Added
- Uh, everything. This the game's initial gamejam release. We put it out

View file

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

View file

@ -8,7 +8,7 @@ All Rights Reserved. "Friday Night Funkin'" and the "Friday Night Funkin'" logo
You can view the `funkin-assets` license here: (https://github.com/FunkinCrew/funkin.assets/blob/main/LICENSE.md)
## Apache 2.0 License
```
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@ -210,3 +210,4 @@ You can view the `funkin-assets` license here: (https://github.com/FunkinCrew/fu
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```

View file

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

View file

@ -15,7 +15,7 @@ To learn how to install the necessary dependencies and compile the game from sou
# Contributing
Please check out our [Contributor's guide](./CONTRIBUTORS.md) on how you can actively participate in the development of Friday Night Funkin'.
You can actively participate in the development of Friday Night Funkin' by opening a [bug report](https://github.com/FunkinCrew/Funkin/issues) or submitting a [code contribution](https://github.com/FunkinCrew/Funkin/pulls)!
# Modding

2
art

@ -1 +1 @@
Subproject commit fbd3e3df77734606d88516770b71b56e6fa04bce
Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa

2
assets

@ -1 +1 @@
Subproject commit 7efad31cf80a42600375bf08b397786df5c1037c
Subproject commit c1899ffbefb9a7c98b030c75a33623431d7ea6ba

90
cliff.toml Normal file
View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "f2b090d6c608471e730b051c8ee22b8b378964b1",
"ref": "ffa691cb2d2d81de35b900a4411e4062ac84ab58",
"url": "https://github.com/FunkinCrew/flixel"
},
{

View file

@ -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.3";
/**
* The game's name. Used as the default window title.
@ -460,7 +460,6 @@ class Project extends HXProject {
// Should be false unless explicitly requested.
GITHUB_BUILD.apply(this, false);
FEATURE_STAGE_EDITOR.apply(this, false);
FEATURE_NEWGROUNDS.apply(this, false);
FEATURE_GHOST_TAPPING.apply(this, false);
@ -471,10 +470,14 @@ class Project extends HXProject {
FEATURE_FUNKVIS.apply(this, true);
FEATURE_PARTIAL_SOUNDS.apply(this, true);
FEATURE_VIDEO_PLAYBACK.apply(this, true);
FEATURE_STAGE_EDITOR.apply(this, true);
// Should be true on debug builds or if GITHUB_BUILD is enabled.
FEATURE_DEBUG_FUNCTIONS.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this));
FEATURE_LOG_TRACE.apply(this, isDebug());
// Got a lot of complains about this being turned off by default on some builds.
// TODO: Look into ways to optimize logging (maybe by using a thread pool?)
FEATURE_LOG_TRACE.apply(this, true);
// Should default to true on workspace builds and false on release builds.
REDIRECT_ASSETS_FOLDER.apply(this, isDebug() && isDesktop());

View file

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.3]
### Added
- Added the `color` attribute on stage props to tint them.
- Added the `blend` attribute on stage props to apply blend modes.
- Added the `angle` attribute on stage props to apply a rotation to them.
## [1.0.2]
### Added
- Added the ability to specify `flipX` and `flipY` on stage props to horizontally or vertically flip, respectively.

View file

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

View file

@ -11,9 +11,9 @@ class StageRegistry extends BaseRegistry<Stage, StageData>
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.2";
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.3";
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <=1.0.2";
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <=1.0.3";
public static var instance(get, never):StageRegistry;
static var _instance:Null<StageRegistry> = null;

View file

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

View file

@ -156,6 +156,8 @@ class PolymodHandler
// Parsing rules for various data formats.
parseRules: buildParseRules(),
skipDependencyErrors: true,
// Parse hxc files and register the scripted classes in them.
useScriptedClasses: true,
loadScriptsAsync: #if html5 true #else false #end,
@ -288,6 +290,7 @@ class PolymodHandler
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.

View file

@ -1,17 +1,13 @@
package funkin.play;
import flixel.addons.display.FlxPieDial;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.transition.Transition;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxState;
import flixel.FlxSubState;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.ui.FlxBar;
import flixel.util.FlxColor;
@ -22,7 +18,6 @@ import funkin.audio.FunkinSound;
import funkin.audio.VoicesGroup;
import funkin.data.dialogue.conversation.ConversationRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.notestyle.NoteStyleData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongEventData;
@ -45,7 +40,6 @@ import funkin.play.cutscene.VanillaCutscenes;
import funkin.play.cutscene.VideoCutscene;
import funkin.play.notes.NoteDirection;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.notes.NoteSplash;
import funkin.play.notes.NoteSprite;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.Strumline;
@ -58,15 +52,9 @@ import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.debug.stage.StageOffsetSubState;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.options.PreferencesMenu;
import funkin.ui.story.StoryMenuState;
import funkin.ui.transition.LoadingState;
import funkin.util.SerializerUtil;
import haxe.Int64;
import lime.ui.Haptic;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
import openfl.Lib;
#if FEATURE_DISCORD_RPC
import funkin.api.discord.DiscordClient;
#end
@ -760,24 +748,6 @@ class PlayState extends MusicBeatSubState
refresh();
}
public override function draw():Void
{
// if (FlxG.renderBlit)
// {
// camGame.fill(BACKGROUND_COLOR);
// }
// else if (FlxG.renderTile)
// {
// FlxG.log.warn("PlayState background not displayed properly on tile renderer!");
// }
// else
// {
// FlxG.log.warn("PlayState background not displayed properly, unknown renderer!");
// }
super.draw();
}
function assertChartExists():Bool
{
// Returns null if the song failed to load or doesn't have the selected difficulty.
@ -2576,7 +2546,7 @@ class PlayState extends MusicBeatSubState
Highscore.tallies.totalNotesHit++;
// Display the hit on the strums
playerStrumline.hitNote(note, !isComboBreak);
playerStrumline.hitNote(note, !event.isComboBreak);
if (event.doesNotesplash) playerStrumline.playNoteSplash(note.noteData.getDirection());
if (note.isHoldNote && note.holdNoteSprite != null) playerStrumline.playNoteHoldCover(note.holdNoteSprite);
vocals.playerVolume = 1;

View file

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

View file

@ -1,24 +1,23 @@
package funkin.save;
import flixel.util.FlxSave;
import funkin.util.FileUtil;
import funkin.input.Controls.Device;
import funkin.play.scoring.Scoring;
import funkin.play.scoring.Scoring.ScoringRank;
import funkin.save.migrator.RawSaveData_v1_0_0;
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.FileUtil;
import funkin.util.SerializerUtil;
import thx.semver.Version;
import thx.semver.Version;
@:nullSafety
class Save
{
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.4";
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.1.0";
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=2.1.0 <2.2.0";
// We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
static final SAVE_PATH:String = 'FunkinCrew';
@ -146,6 +145,14 @@ class Save
hitsoundVolumeOpponent: 1.0,
themeMusic: true
},
optionsStageEditor:
{
previousFiles: [],
moveStep: "1px",
angleStep: 5,
theme: StageEditorTheme.Light
}
};
}
@ -428,6 +435,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
@ -867,39 +959,61 @@ class Save
*/
static function loadFromSlot(slot:Int):Save
{
trace("[SAVE] Loading save from slot " + slot + "...");
// Prevent crashes if the save data is corrupted.
SerializerUtil.initSerializer();
trace('[SAVE] Loading save from slot $slot...');
FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH);
if (FlxG.save.isEmpty())
switch (FlxG.save.status)
{
trace('[SAVE] Save data is empty, checking for legacy save data...');
var legacySaveData = fetchLegacySaveData();
if (legacySaveData != null)
{
trace('[SAVE] Found legacy save data, converting...');
var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData);
case EMPTY:
trace('[SAVE] Save data in slot ${slot} is empty, checking for legacy save data...');
var legacySaveData = fetchLegacySaveData();
if (legacySaveData != null)
{
trace('[SAVE] Found legacy save data, converting...');
var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData);
FlxG.save.mergeData(gameSave.data, true);
return gameSave;
}
else
{
trace('[SAVE] No legacy save data found.');
var gameSave = new Save();
FlxG.save.mergeData(gameSave.data, true);
return gameSave;
}
case ERROR(_):
return handleSaveDataError(slot);
case BOUND(_, _):
trace('[SAVE] Loaded existing save data in slot ${slot}.');
var gameSave = SaveDataMigrator.migrate(FlxG.save.data);
FlxG.save.mergeData(gameSave.data, true);
return gameSave;
}
else
{
trace('[SAVE] No legacy save data found.');
var gameSave = new Save();
FlxG.save.mergeData(gameSave.data, true);
return gameSave;
}
}
}
/**
* Call this when there is an error loading the save data in slot X.
*/
static function handleSaveDataError(slot:Int):Save
{
var msg = 'There was an error loading your save data in slot ${slot}.';
msg += '\nPlease report this issue to the developers.';
lime.app.Application.current.window.alert(msg, "Save Data Failure");
// Don't touch that slot anymore.
// Instead, load the next available slot.
var nextSlot = slot + 1;
if (nextSlot < 1000)
{
return loadFromSlot(nextSlot);
}
else
{
trace('[SAVE] Found existing save data.');
var gameSave = SaveDataMigrator.migrate(FlxG.save.data);
FlxG.save.mergeData(gameSave.data, true);
return gameSave;
throw "End of save data slots. Can't load any more.";
}
}
@ -964,7 +1078,15 @@ class Save
{
var targetSaveData = new FlxSave();
targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
return !targetSaveData.isEmpty();
switch (targetSaveData.status)
{
case EMPTY:
return false;
case ERROR(_):
return false;
case BOUND(_, _):
return true;
}
}
/**
@ -1068,6 +1190,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 =
@ -1441,3 +1568,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;
};

View file

@ -5,11 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.4] - 2024-09-12
Note to self: Only update to 2.1.0 when migration is needed.
## [2.1.0] - 2024-10-18
This version introduces changes to save data loading in order to improve compatibility with older versions.
### Changed
- `optionsStageEditor.theme` converted from an Enum to a String to fix save data compatibility issues.
- In the future, Enum values should not be used in order to prevent incompatibilities caused by introducing new types to the save data that older versions cannot parse.
- `optionsChartEditor.theme` converted from an Enum to a String to fix save data compatibility issues.
- `optionsChartEditor.chartEditorLiveInputStyle` converted from an Enum to a String to fix save data compatibility issues.
## [2.0.6] - 2024-10-11
### Added
- `unlocks.charactersSeen:Array<String>` to `Save`
- `unlocks.oldChar:Bool` to `Save`
- `optionsStageEditor` to `Save` for storing user preferences for the stage editor.
## [2.0.5] - 2024-05-21
### Fixed
@ -17,6 +23,8 @@ Note to self: Only update to 2.1.0 when migration is needed.
## [2.0.4] - 2024-05-21
### Added
- `unlocks.charactersSeen:Array<String>` to `Save`
- `unlocks.oldChar:Bool` to `Save`
- `favoriteSongs:Array<String>` to `Save`
## [2.0.3] - 2024-01-09

View file

@ -32,6 +32,10 @@ class SaveDataMigrator
var save:Save = new Save(saveDataWithDefaults);
return save;
}
else if (VersionUtil.validateVersion(version, "2.0.x"))
{
return migrate_v2_0_0(inputData);
}
else
{
var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.';
@ -43,6 +47,20 @@ class SaveDataMigrator
}
}
static function migrate_v2_0_0(inputData:Dynamic):Save
{
// Import the structured data.
var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData);
// Reset these values to valid ones.
saveDataWithDefaults.optionsChartEditor.chartEditorLiveInputStyle = funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle.None;
saveDataWithDefaults.optionsChartEditor.theme = funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme.Light;
saveDataWithDefaults.optionsStageEditor.theme = funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme.Light;
var save:Save = new Save(saveDataWithDefaults);
return save;
}
/**
* Migrate from 1.x to the latest version.
*/

View file

@ -0,0 +1,26 @@
package funkin.save.migrator;
// Internal enums used to ensure old save data can be parsed by the default Haxe unserializer.
// In the future, only primitive types and abstract enums should be used in save data!
@:native("funkin.ui.debug.stageeditor.StageEditorTheme")
enum StageEditorTheme
{
Light;
Dark;
}
@:native("funkin.ui.debug.charting.ChartEditorTheme")
enum ChartEditorTheme
{
Light;
Dark;
}
@:native("funkin.ui.debug.charting.ChartEditorLiveInputStyle")
enum ChartEditorLiveInputStyle
{
None;
NumberKeys;
WASDKeys;
}

View file

@ -55,16 +55,19 @@ class DebugMenuSubState extends MusicBeatSubState
// Create each menu item.
// Call onMenuChange when the first item is created to move the camera .
#if FEATURE_CHART_EDITOR
onMenuChange(createItem("CHART EDITOR", openChartEditor));
createItem("CHART EDITOR", openChartEditor);
#end
createItem("ANIMATION EDITOR", openAnimationEditor);
#if FEATURE_STAGE_EDITOR
createItem("STAGE EDITOR", openStageEditor);
#end
// createItem("Input Offset Testing", openInputOffsetTesting);
createItem("CHARACTER SELECT", openCharSelect, true);
createItem("ANIMATION EDITOR", openAnimationEditor);
// createItem("STAGE EDITOR", openStageEditor);
// createItem("CHARACTER SELECT", openCharSelect, true);
// createItem("TEST STICKERS", testStickers);
#if sys
createItem("OPEN CRASH LOG FOLDER", openLogFolder);
#end
onMenuChange(items.members[0]);
FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y + 500));
}
@ -125,6 +128,7 @@ class DebugMenuSubState extends MusicBeatSubState
function openStageEditor()
{
trace('Stage Editor');
FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState());
}
#if sys

View file

@ -94,6 +94,7 @@ import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.transition.LoadingState;
import funkin.util.Constants;
import funkin.util.FileUtil;
import funkin.util.MathUtil;
import funkin.util.logging.CrashHandler;
import funkin.util.SortUtil;
import funkin.util.WindowUtil;
@ -244,7 +245,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/**
* Duration, in seconds, for the scroll easing animation.
*/
public static final SCROLL_EASE_DURATION:Float = 0.2;
public static final SCROLL_EASE_DURATION:Float = 0.4;
// Other
@ -773,9 +774,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/**
* The current process that is lerping the scroll position.
* Used to cancel the previous lerp if the user scrolls again.
*/
var currentScrollEase:Null<VarTween>;
var currentScrollEase:Null<Float>;
/**
* The position where the user middle clicked to place a scroll anchor.
@ -2707,6 +2707,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
playbarHeadLayout.playbarHead.width = FlxG.width;
playbarHeadLayout.playbarHead.height = 10;
playbarHeadLayout.playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;';
playbarHeadLayout.playbarHead.min = 0;
playbarHeadLayout.playbarHead.onDragStart = function(_:DragEvent) {
playbarHeadDragging = true;
@ -2723,13 +2724,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
}
playbarHeadLayout.playbarHead.onDrag = function(_:DragEvent) {
playbarHeadLayout.playbarHead.onDrag = function(d:DragEvent) {
if (playbarHeadDragging)
{
// Set the song position to where the playhead was moved to.
scrollPositionInPixels = (songLengthInPixels) * playbarHeadLayout.playbarHead.value / 100;
// Update the conductor and audio tracks to match.
moveSongToScrollPosition();
currentScrollEase = d.value;
easeSongToScrollPosition(currentScrollEase);
}
}
@ -2740,8 +2740,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (playbarHeadDraggingWasPlaying)
{
playbarHeadDraggingWasPlaying = false;
// Disabled code to resume song playback on drag.
// startAudioPlayback();
startAudioPlayback();
}
}
@ -3417,10 +3418,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
audioInstTrack.time = -Conductor.instance.instrumentalOffset;
}
}
if (!audioInstTrack.isPlaying && currentScrollEase != scrollPositionInPixels) easeSongToScrollPosition(currentScrollEase);
}
if (audioInstTrack != null && audioInstTrack.isPlaying)
{
currentScrollEase = scrollPositionInPixels;
if (FlxG.keys.pressed.ALT)
{
// If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat!
@ -3869,7 +3874,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
// Mouse Wheel = Scroll
if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL)
if (FlxG.mouse.wheel != 0)
{
scrollAmount = -50 * FlxG.mouse.wheel;
shouldPause = true;
@ -4057,27 +4062,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
shouldPause = true;
}
if (Math.abs(scrollAmount) > GRID_SIZE * 8)
{
shouldEase = true;
}
shouldEase = true;
if (shouldPause) stopAudioPlayback();
// Resync the conductor and audio tracks.
if (scrollAmount != 0 || playheadAmount != 0)
{
this.playheadPositionInPixels += playheadAmount;
if (shouldEase)
{
easeSongToScrollPosition(this.scrollPositionInPixels + scrollAmount);
}
else
{
// Apply the scroll amount.
this.scrollPositionInPixels += scrollAmount;
moveSongToScrollPosition();
}
}
if (shouldPause) stopAudioPlayback();
if (playheadAmount != 0) this.playheadPositionInPixels += playheadAmount;
if (scrollAmount != 0) currentScrollEase += scrollAmount;
}
/**
@ -4333,15 +4324,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// Scroll up.
var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.viewY;
scrollPositionInPixels -= diff * 0.5; // Too fast!
moveSongToScrollPosition();
currentScrollEase -= diff * 0.5; // Too fast!
}
else if (FlxG.mouse.viewY > (playbarHeadLayout?.y ?? 0.0))
{
// Scroll down.
var diff:Float = FlxG.mouse.viewY - (playbarHeadLayout?.y ?? 0.0);
scrollPositionInPixels += diff * 0.5; // Too fast!
moveSongToScrollPosition();
currentScrollEase += (diff * 0.5); // Too fast!
}
// Render the selection box.
@ -4480,8 +4469,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.viewY, (notePreview?.y ?? 0.0), (notePreview?.y ?? 0.0) + (notePreview?.height ?? 0.0),
0, songLengthInPixels);
scrollPositionInPixels = clickedPosInPixels;
moveSongToScrollPosition();
currentScrollEase = clickedPosInPixels;
easeSongToScrollPosition(currentScrollEase);
}
else if (scrollAnchorScreenPos != null)
{
@ -4540,15 +4529,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// Scroll up.
var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.viewY;
scrollPositionInPixels -= diff * 0.5; // Too fast!
moveSongToScrollPosition();
currentScrollEase -= (diff * 0.5);
}
else if (FlxG.mouse.viewY > (playbarHeadLayout?.y ?? 0.0))
{
// Scroll down.
var diff:Float = FlxG.mouse.viewY - (playbarHeadLayout?.y ?? 0.0);
scrollPositionInPixels += diff * 0.5; // Too fast!
moveSongToScrollPosition();
currentScrollEase += (diff * 0.5);
}
// Calculate distance between the position dragged to and the original position.
@ -5142,18 +5129,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
if (playbarHeadLayout == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!";
// Move the playhead to match the song position, if we aren't dragging it.
playbarHeadLayout.playbarHead.pos = currentScrollEase;
playbarHeadLayout.playbarHead.max = songLengthInPixels;
// Make sure the playbar is never nudged out of the correct spot.
playbarHeadLayout.x = 4;
playbarHeadLayout.y = FlxG.height - 48 - 8;
// Move the playhead to match the song position, if we aren't dragging it.
if (!playbarHeadDragging)
{
var songPosPercent = scrollPositionInPixels / (songLengthInPixels) * 100;
if (playbarHeadLayout.playbarHead.value != songPosPercent) playbarHeadLayout.playbarHead.value = songPosPercent;
}
var songPos:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
var songPosMilliseconds:String = Std.string(Math.floor(Math.abs(songPos) % 1000)).lpad('0', 2).substr(0, 2);
var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2);
@ -5654,7 +5638,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handleHelpKeybinds():Void
{
// F1 = Open Help
if (FlxG.keys.justPressed.F1 && !isHaxeUIDialogOpen) {
if (FlxG.keys.justPressed.F1 && !isHaxeUIDialogOpen)
{
this.openUserGuideDialog();
}
}
@ -6133,43 +6118,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
function easeSongToScrollPosition(targetScrollPosition:Float):Void
{
if (currentScrollEase != null) cancelScrollEase(currentScrollEase);
currentScrollEase = FlxTween.tween(this, {scrollPositionInPixels: targetScrollPosition}, SCROLL_EASE_DURATION,
{
ease: FlxEase.quintInOut,
onUpdate: this.onScrollEaseUpdate,
onComplete: this.cancelScrollEase,
type: ONESHOT
});
}
/**
* Callback function executed every frame that the scroll position is being eased.
* @param _
*/
function onScrollEaseUpdate(_:FlxTween):Void
{
currentScrollEase = Math.max(0, targetScrollPosition);
currentScrollEase = Math.min(currentScrollEase, songLengthInPixels);
scrollPositionInPixels = MathUtil.smoothLerp(scrollPositionInPixels, currentScrollEase, FlxG.elapsed, SCROLL_EASE_DURATION, 1 / 1000);
moveSongToScrollPosition();
}
/**
* Callback function executed when cancelling an existing scroll position ease.
* Ensures that the ease is immediately cancelled and the scroll position is set to the target value.
*/
function cancelScrollEase(_:FlxTween):Void
{
if (currentScrollEase != null)
{
@:privateAccess
var targetScrollPosition:Float = currentScrollEase._properties.scrollPositionInPixels;
currentScrollEase.cancel();
currentScrollEase = null;
this.scrollPositionInPixels = targetScrollPosition;
}
}
/**
* Fix the current scroll position after exiting the PlayState used when testing.
*/
@ -6350,6 +6304,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
if (audioInstTrack == null) return;
currentScrollEase = this.scrollPositionInPixels;
if (audioInstTrack.isPlaying)
{
// Pause
@ -6543,22 +6499,22 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/**
* Available input modes for the chart editor state. Numbers/arrows/WASD available for other keybinds.
*/
enum ChartEditorLiveInputStyle
enum abstract ChartEditorLiveInputStyle(String)
{
/**
* No hotkeys to place notes at the playbar.
*/
None;
var None;
/**
* 1/2/3/4 to place notes on opponent's side, 5/6/7/8 to place notes on player's side.
*/
NumberKeys;
var NumberKeys;
/**
* WASD to place notes on opponent's side, Arrow keys to place notes on player's side.
*/
WASDKeys;
var WASDKeys;
}
typedef ChartEditorParams =
@ -6577,15 +6533,15 @@ typedef ChartEditorParams =
/**
* Available themes for the chart editor state.
*/
enum ChartEditorTheme
enum abstract ChartEditorTheme(String)
{
/**
* The default theme for the chart editor.
*/
Light;
var Light;
/**
* A theme which introduces darker colors.
*/
Dark;
var Dark;
}

View file

@ -253,42 +253,23 @@ class ChartEditorThemeHandler
var bottomTickY:Float = state.measureTickBitmap.height - (measureTickWidth / 2);
state.measureTickBitmap.fillRect(new Rectangle(0, bottomTickY, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Draw the beat ticks.
var beatTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2);
var beatTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2);
var beatTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2);
var beatTickLength:Float = state.measureTickBitmap.width * 2 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, beatTick2Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, beatTick3Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, beatTick4Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Draw the step ticks.
// TODO: Make this a loop or something.
var stepTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick6Y:Float = state.measureTickBitmap.height * 5 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick7Y:Float = state.measureTickBitmap.height * 6 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick8Y:Float = state.measureTickBitmap.height * 7 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick10Y:Float = state.measureTickBitmap.height * 9 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick11Y:Float = state.measureTickBitmap.height * 10 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick12Y:Float = state.measureTickBitmap.height * 11 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick14Y:Float = state.measureTickBitmap.height * 13 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick15Y:Float = state.measureTickBitmap.height * 14 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick16Y:Float = state.measureTickBitmap.height * 15 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTickLength:Float = state.measureTickBitmap.width * 1 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick2Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick3Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick4Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick6Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick7Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick8Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick10Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick11Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick12Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick14Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick15Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick16Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Draw the beat and step ticks. No need for two seperate loops thankfully.
// This'll be fun to update when beat tuplets become functional.
for (i in 1...(Conductor.instance.stepsPerMeasure))
{
if ((i % Constants.STEPS_PER_BEAT) == 0) // If we're on a beat, draw a beat tick.
{
var beatTickY:Float = state.measureTickBitmap.height * i / Conductor.instance.stepsPerMeasure - (beatTickWidth / 2);
var beatTickLength:Float = state.measureTickBitmap.width * 2 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, beatTickY, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
}
else // Else, draw a step tick.
{
var stepTickY:Float = state.measureTickBitmap.height * i / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTickLength:Float = state.measureTickBitmap.width * 1 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, stepTickY, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
}
}
}
/**
@ -314,23 +295,16 @@ class ChartEditorThemeHandler
state.offsetTickBitmap.fillRect(new Rectangle(rightTickX, 0, majorTickWidth / 2, majorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Draw the minor ticks.
var minorTick2X:Float = state.offsetTickBitmap.width * 1 / 10 - (minorTickWidth / 2);
var minorTick3X:Float = state.offsetTickBitmap.width * 2 / 10 - (minorTickWidth / 2);
var minorTick4X:Float = state.offsetTickBitmap.width * 3 / 10 - (minorTickWidth / 2);
var minorTick5X:Float = state.offsetTickBitmap.width * 4 / 10 - (minorTickWidth / 2);
var minorTick7X:Float = state.offsetTickBitmap.width * 6 / 10 - (minorTickWidth / 2);
var minorTick8X:Float = state.offsetTickBitmap.width * 7 / 10 - (minorTickWidth / 2);
var minorTick9X:Float = state.offsetTickBitmap.width * 8 / 10 - (minorTickWidth / 2);
var minorTick10X:Float = state.offsetTickBitmap.width * 9 / 10 - (minorTickWidth / 2);
var minorTickLength:Float = state.offsetTickBitmap.height * 1 / 3;
state.offsetTickBitmap.fillRect(new Rectangle(minorTick2X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.offsetTickBitmap.fillRect(new Rectangle(minorTick3X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.offsetTickBitmap.fillRect(new Rectangle(minorTick4X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.offsetTickBitmap.fillRect(new Rectangle(minorTick5X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.offsetTickBitmap.fillRect(new Rectangle(minorTick7X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.offsetTickBitmap.fillRect(new Rectangle(minorTick8X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.offsetTickBitmap.fillRect(new Rectangle(minorTick9X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.offsetTickBitmap.fillRect(new Rectangle(minorTick10X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
for (i in 1...11)
{
if (i % 5 == 0)
{
continue;
}
var minorTickX:Float = state.offsetTickBitmap.width * i / 10 - (minorTickWidth / 2);
var minorTickLength:Float = state.offsetTickBitmap.height * 1 / 3;
state.offsetTickBitmap.fillRect(new Rectangle(minorTickX, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
}
// Draw the offset ticks.
// var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT); // 1 grid squares wide.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1688,6 +1688,7 @@ class FreeplayState extends MusicBeatSubState
function changeDiff(change:Int = 0, force:Bool = false):Void
{
touchTimer = 0;
var previousVariation:String = currentVariation;
// Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico`
// but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell)
@ -1783,7 +1784,7 @@ class FreeplayState extends MusicBeatSubState
}
// Reset the song preview in case we changed variations (normal->erect etc)
playCurSongPreview();
if (currentVariation != previousVariation) playCurSongPreview();
}
// Set the album graphic and play the animation if relevant.

View file

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

View file

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

View file

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

View file

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

View file

@ -136,8 +136,6 @@ class FunkinPreloader extends FlxBasePreloader
// We can't even call trace() yet, until Flixel loads.
trace('Initializing custom preloader...');
funkin.util.CLIUtil.resetWorkingDir();
this.siteLockTitleText = Constants.SITE_LOCK_TITLE;
this.siteLockBodyText = Constants.SITE_LOCK_DESC;
}

View file

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

View file

@ -63,31 +63,6 @@ class SerializerUtil
}
}
public static function initSerializer():Void
{
haxe.Unserializer.DEFAULT_RESOLVER = new FunkinTypeResolver();
}
/**
* Serialize a Haxe object using the built-in Serializer.
* @param input The object to serialize
* @return The serialized object as a string
*/
public static function fromHaxeObject(input:Dynamic):String
{
return haxe.Serializer.run(input);
}
/**
* Convert a serialized Haxe object back into a Haxe object.
* @param input The serialized object as a string
* @return The deserialized object
*/
public static function toHaxeObject(input:String):Dynamic
{
return haxe.Unserializer.run(input);
}
/**
* Customize how certain types are serialized when converting to JSON.
*/
@ -115,26 +90,3 @@ class SerializerUtil
return result;
}
}
class FunkinTypeResolver
{
public function new()
{
// Blank constructor.
}
public function resolveClass(name:String):Class<Dynamic>
{
if (name == 'Dynamic')
{
FlxG.log.warn('Found invalid class type in save data, indicates partial save corruption.');
return null;
}
return Type.resolveClass(name);
};
public function resolveEnum(name:String):Enum<Dynamic>
{
return Type.resolveEnum(name);
};
}