mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-12-08 04:58:48 +00:00
Compare commits
170 commits
34e0cb4bd3
...
7ea7969c94
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ea7969c94 | ||
|
|
758f712eb5 | ||
|
|
01029817c0 | ||
|
|
8a31e12d10 | ||
|
|
90ab04caa1 | ||
|
|
ba7f89b9a2 | ||
|
|
ee36cbbbcf | ||
|
|
d74b8fb4ca | ||
|
|
8fa622e147 | ||
|
|
a12950d5a7 | ||
|
|
71bc847698 | ||
|
|
a560bf51a9 | ||
|
|
866e5aa008 | ||
|
|
3c213ad45c | ||
|
|
a0b933ce8f | ||
|
|
671e1435d2 | ||
|
|
369dad3951 | ||
|
|
8ecde809e6 | ||
|
|
c4394f0d12 | ||
|
|
63cbaef4c4 | ||
|
|
4120b20a24 | ||
|
|
a40972926d | ||
|
|
368531f6fa | ||
|
|
b9d4ce70cc | ||
|
|
6ef3fe4faf | ||
|
|
8abc9ab306 | ||
|
|
8349999833 | ||
|
|
cf52bcdf65 | ||
|
|
4e03cf0a5d | ||
|
|
8952acae30 | ||
|
|
16c685ac98 | ||
|
|
5472bf691f | ||
|
|
6a71f95cf9 | ||
|
|
48f3e984f4 | ||
|
|
b125a49410 | ||
|
|
de19d65ad6 | ||
|
|
6334215be4 | ||
|
|
a3b7891b42 | ||
|
|
53c62e219b | ||
|
|
adb96897dc | ||
|
|
9a77539664 | ||
|
|
0d91d68300 | ||
|
|
d49fd76bd1 | ||
|
|
329ec524c0 | ||
|
|
3f89464608 | ||
|
|
118238105b | ||
|
|
0d32ccc214 | ||
|
|
ccbae3da7d | ||
|
|
b4058fd7b0 | ||
|
|
c6add57710 | ||
|
|
c33ec8c0ea | ||
|
|
fe683eba43 | ||
|
|
1534eb2422 | ||
|
|
c2fd17c551 | ||
|
|
ec804394b7 | ||
|
|
04ce409f33 | ||
|
|
635f6c094e | ||
|
|
0fbd7a7998 | ||
|
|
9e645db007 | ||
|
|
3040692852 | ||
|
|
193c443bc4 | ||
|
|
cb184f89ec | ||
|
|
c2d89243b6 | ||
|
|
75d085d733 | ||
|
|
e550440407 | ||
|
|
f1eb67bf52 | ||
|
|
efafb1acea | ||
|
|
56c6c392b3 | ||
|
|
d5afb340a2 | ||
|
|
f95fbf57b6 | ||
|
|
6b5db32522 | ||
|
|
300ef8fef1 | ||
|
|
e09c256c80 | ||
|
|
7174e776ac | ||
|
|
f10ea83530 | ||
|
|
e19f2155b1 | ||
|
|
86a18067dd | ||
|
|
15caa7a6f7 | ||
|
|
b2897bc86c | ||
|
|
cc486ad94b | ||
|
|
c5c22fe1a5 | ||
|
|
302beb2d6e | ||
|
|
a702040dfb | ||
|
|
1510e5922e | ||
|
|
92344d2e30 | ||
|
|
5ce3315967 | ||
|
|
858199e298 | ||
|
|
0739a40abf | ||
|
|
215bbdb9da | ||
|
|
c9d4d07dc7 | ||
|
|
7e1442e969 | ||
|
|
fcfde61b24 | ||
|
|
fe6f5eb821 | ||
|
|
512a3fc927 | ||
|
|
3bf06a1cc9 | ||
|
|
2c3d61e629 | ||
|
|
20051109dd | ||
|
|
267cc55e40 | ||
|
|
766937d5fe | ||
|
|
18d8fe7248 | ||
|
|
f7cb738803 | ||
|
|
400ef63c5a | ||
|
|
1ff6a1656c | ||
|
|
20da3f85f1 | ||
|
|
d5e14a7101 | ||
|
|
ffa8622903 | ||
|
|
8b559639e8 | ||
|
|
628c0e8975 | ||
|
|
1734ec1dbf | ||
|
|
b10ae8dfa5 | ||
|
|
dc634fa35c | ||
|
|
06246aa3d7 | ||
|
|
7c1eb4f88c | ||
|
|
38634ef5ca | ||
|
|
5d76d23d33 | ||
|
|
da422c913f | ||
|
|
ba52b932ee | ||
|
|
1b90b7bebf | ||
|
|
23ca9c14c1 | ||
|
|
ab300be709 | ||
|
|
e6aaca19f6 | ||
|
|
0fe226132d | ||
|
|
452e3a58d8 | ||
|
|
e8f60a23ef | ||
|
|
958a6f49ce | ||
|
|
2005628358 | ||
|
|
439901b42e | ||
|
|
7bbe356009 | ||
|
|
9d710b0490 | ||
|
|
d140caf4ba | ||
|
|
15b691ad7b | ||
|
|
02b53e4900 | ||
|
|
5b5d76aa59 | ||
|
|
ae861214cb | ||
|
|
4d5558f3d8 | ||
|
|
0af6f61fb9 | ||
|
|
72a2e9c895 | ||
|
|
4f1bbd3f9b | ||
|
|
b99089dd88 | ||
|
|
380eb5cbaf | ||
|
|
a398eb17c4 | ||
|
|
9765f5efe5 | ||
|
|
d6e1f0c90d | ||
|
|
f56fa1955e | ||
|
|
277abfab2b | ||
|
|
5c1b0ae184 | ||
|
|
ee952c0789 | ||
|
|
eb1d54e750 | ||
|
|
0d8fd214a1 | ||
|
|
07b0af53f1 | ||
|
|
7ffa3d5bf2 | ||
|
|
615d52aff2 | ||
|
|
4961d4ee99 | ||
|
|
33a020a34b | ||
|
|
6ba44d6cb0 | ||
|
|
277d16895b | ||
|
|
dbd19159b6 | ||
|
|
9f5c88ffa1 | ||
|
|
a528a11b8c | ||
|
|
a52f4c5e95 | ||
|
|
0be42bf047 | ||
|
|
ba31f3611a | ||
|
|
3119a0cb6e | ||
|
|
5326c40347 | ||
|
|
bbcb19d476 | ||
|
|
e6d6972ab0 | ||
|
|
4f8da59577 | ||
|
|
6686812504 | ||
|
|
80993df87f | ||
|
|
30361954f3 |
112
.vscode/settings.json
vendored
112
.vscode/settings.json
vendored
|
|
@ -1,31 +1,22 @@
|
|||
{
|
||||
"[haxe]": {
|
||||
// Automatically keep Haxe files formatted.
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.codeActionsOnSave": { "source.organizeImports": "never" },
|
||||
"editor.defaultFormatter": "nadako.vshaxe",
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
|
||||
"[json]": {
|
||||
// Automatically keep JSON files formatted.
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
||||
"[jsonc]": {
|
||||
// Automatically keep JSONC files formatted.
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"prettier.tabWidth": 2,
|
||||
|
||||
// XML formatting style configuration
|
||||
// XML Formatting
|
||||
"xml.format.enabled": true,
|
||||
"xml.format.legacy": false,
|
||||
"xml.format.emptyElements": "collapse",
|
||||
|
|
@ -33,7 +24,7 @@
|
|||
"xml.format.enforceQuoteStyle": "preferred",
|
||||
"xml.format.preserveAttributeLineBreaks": false,
|
||||
"xml.format.preservedNewlines": 0,
|
||||
"xml.format.splitAttributes": false,
|
||||
"xml.format.splitAttributes": "preserve",
|
||||
"xml.format.joinCDATALines": true,
|
||||
"xml.format.preserveEmptyContent": false,
|
||||
"xml.format.joinCommentLines": false,
|
||||
|
|
@ -56,26 +47,19 @@
|
|||
"xml.format.maxLineWidth": 0,
|
||||
"xml.format.grammarAwareFormatting": true,
|
||||
|
||||
// Generic file formatting style configuration
|
||||
// General formatting
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": false,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
|
||||
// Automatically detect indentation.
|
||||
"editor.detectIndentation": true,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
|
||||
// Automatically enforce Linux style line endings.
|
||||
"prettier.tabWidth": 2,
|
||||
"files.eol": "\n",
|
||||
|
||||
"haxe.displayPort": "auto",
|
||||
"haxe.enableCompilationServer": true,
|
||||
"haxe.enableServerView": true,
|
||||
"haxe.displayServer": {
|
||||
"arguments": ["-v"]
|
||||
},
|
||||
// Fix file associations for HScript.
|
||||
"haxe.displayServer": { "arguments": ["-v"] },
|
||||
"files.associations": {
|
||||
"*.hxp": "haxe",
|
||||
"*.hscript": "haxe",
|
||||
|
|
@ -84,14 +68,27 @@
|
|||
"*.hxc": "haxe"
|
||||
},
|
||||
"projectManager.git.baseFolders": ["./"],
|
||||
|
||||
"haxecheckstyle.sourceFolders": ["src", "source"],
|
||||
"haxecheckstyle.externalSourceRoots": [],
|
||||
"haxecheckstyle.configurationFile": "checkstyle.json",
|
||||
"haxecheckstyle.codeSimilarityBufferSize": 100,
|
||||
|
||||
"lime.projectFile": "project.hxp",
|
||||
|
||||
"lime.targets": [
|
||||
{ "name": "windows", "enabled": true, "label": "Windows" },
|
||||
{ "name": "mac", "enabled": true, "label": "macOS" },
|
||||
{ "name": "linux", "enabled": true, "label": "Linux" },
|
||||
{ "name": "html5", "enabled": true, "label": "HTML5" },
|
||||
{ "name": "android", "enabled": true, "label": "Android" },
|
||||
{ "name": "ios", "enabled": true, "label": "iOS" },
|
||||
// Disabled targets
|
||||
{ "name": "hl", "enabled": false, "label": "HashLink" },
|
||||
{ "name": "air", "enabled": false },
|
||||
{ "name": "electron", "enabled": false },
|
||||
{ "name": "flash", "enabled": false },
|
||||
{ "name": "neko", "enabled": false },
|
||||
{ "name": "tvos", "enabled": false }
|
||||
],
|
||||
"lime.defaultTargetConfiguration": "Windows / Debug",
|
||||
"lime.targetConfigurations": [
|
||||
{
|
||||
"label": "Windows / Debug (Discord)",
|
||||
|
|
@ -103,21 +100,11 @@
|
|||
"target": "windows",
|
||||
"args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (FlxAnimate Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DANIMATE"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Freeplay)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Freeplay)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DFREEPLAY"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
|
||||
"target": "windows",
|
||||
|
|
@ -132,26 +119,6 @@
|
|||
"target": "windows",
|
||||
"args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Conversation Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Conversation Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DDIALOGUE"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Results Screen Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DRESULTS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Stage Editor)",
|
||||
"target": "windows",
|
||||
|
|
@ -172,36 +139,16 @@
|
|||
"target": "windows",
|
||||
"args": ["-debug", "-DHXVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Animation Editor)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DANIMDEBUG"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Latency Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Latency Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DLATENCY"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Waveform Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Release (GitHub Actions)",
|
||||
"target": "windows",
|
||||
"args": ["-release", "-DGITHUB_BUILD"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Waveform Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DWAVEFORM"]
|
||||
},
|
||||
{
|
||||
"label": "HTML5 / Debug (Watch)",
|
||||
"target": "html5",
|
||||
|
|
@ -209,10 +156,7 @@
|
|||
}
|
||||
],
|
||||
"lime.buildTypes": [
|
||||
{
|
||||
"label": "Debug",
|
||||
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{ "label": "Debug", "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] },
|
||||
{
|
||||
"label": "Debug (Unlock Everything)",
|
||||
"args": ["-debug", "-DUNLOCK_EVERYTHING"]
|
||||
|
|
@ -225,10 +169,7 @@
|
|||
"label": "Debug (Straight to Chart Editor)",
|
||||
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Release",
|
||||
"args": ["-release"]
|
||||
},
|
||||
{ "label": "Release", "args": ["-release"] },
|
||||
{
|
||||
"label": "Release (GitHub Actions)",
|
||||
"args": ["-release", "-DGITHUB_BUILD"]
|
||||
|
|
@ -244,9 +185,6 @@
|
|||
],
|
||||
"vscord.app.privacyMode.enable": true,
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/hmm.json"],
|
||||
"url": "./.vscode/schema/hmm.json"
|
||||
}
|
||||
{ "fileMatch": ["/hmm.json"], "url": "./.vscode/schema/hmm.json" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div align='center'><img src="https://fridaynightfunkin.wiki.gg/images/FNF_logo.png" width="800">
|
||||
<div align='center'><img src="docs/readme_images/FNF_logo.png" width="800">
|
||||
|
||||
<h2>Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for <a href="https://ldjam.com/events/ludum-dare/47">Ludum Dare 47.</a></h2>
|
||||
|
||||
|
|
@ -14,8 +14,8 @@ This game was made with love to Newgrounds and its community. Extra love to Tom
|
|||
<div align='center'>
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://fridaynightfunkin.wiki.gg/images/d/d7/Title_Card.gif" alt="Title Screen" width="350"/></td>
|
||||
<td><img src="https://fridaynightfunkin.wiki.gg/images/9/99/Menu.png" alt="Main Menu" width="350"/></td>
|
||||
<td><img src="docs/readme_images/Title_Card.gif" alt="Title Screen" width="350"/></td>
|
||||
<td><img src="docs/readme_images/Menu.png" alt="Main Menu" width="350"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
2
assets
2
assets
|
|
@ -1 +1 @@
|
|||
Subproject commit f81cc16c8f99655f76b7e27ce221819798c9c94e
|
||||
Subproject commit 1a961f111381eb3bfc452166c4e4b5a18b409781
|
||||
BIN
docs/readme_images/FNF_logo.png
Normal file
BIN
docs/readme_images/FNF_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 501 KiB |
BIN
docs/readme_images/Menu.png
Normal file
BIN
docs/readme_images/Menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 541 KiB |
BIN
docs/readme_images/Title_Card.gif
Normal file
BIN
docs/readme_images/Title_Card.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2 MiB |
5
docs/readme_images/credits.txt
Normal file
5
docs/readme_images/credits.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Images stored in this folder come from these links:
|
||||
|
||||
FNF_logo.png - https://fridaynightfunkin.wiki.gg/images/FNF_logo.png
|
||||
Menu.png - https://fridaynightfunkin.wiki.gg/images/Menu.png
|
||||
Title_Card.gif - https://fridaynightfunkin.wiki.gg/images/d/d7/Title_Card.gif
|
||||
18
hmm.json
18
hmm.json
|
|
@ -45,7 +45,7 @@
|
|||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "125c5b5a05a90918e0bd4f97d596ed1321bde5c2",
|
||||
"ref": "757f2d0ab241ad0fb99fbcdac598fee31a905ea5",
|
||||
"url": "https://github.com/FunkinCrew/flixel"
|
||||
},
|
||||
{
|
||||
|
|
@ -56,11 +56,11 @@
|
|||
"url": "https://github.com/FunkinCrew/flixel-addons"
|
||||
},
|
||||
{
|
||||
"name": "flxanimate",
|
||||
"name": "flixel-animate",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "49214278b9124823582cdcecd94f4a1de9a4b36b",
|
||||
"url": "https://github.com/FunkinCrew/flxanimate"
|
||||
"ref": "220463c8089444d6c9957b68919fe88e6cba495f",
|
||||
"url": "https://github.com/MaybeMaru/flixel-animate"
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
|
|
@ -90,8 +90,8 @@
|
|||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "578d61bc275cf71f473647eeb5aab002e45338be",
|
||||
"url": "https://github.com/haxeui/haxeui-core"
|
||||
"ref": "088b153b35985f8e4419ef710057ebd5b3749513",
|
||||
"url": "https://github.com/FunkinCrew/haxeui-core"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
"name": "hxcpp",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "6546fa5c3ad1bac065f144745122ab5a6d4195ff",
|
||||
"ref": "5932340d095a7eea8635fe4d1355f1c0efd0b3c2",
|
||||
"url": "https://github.com/FunkinCrew/hxcpp"
|
||||
},
|
||||
{
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
{
|
||||
"name": "hxvlc",
|
||||
"type": "haxelib",
|
||||
"version": "2.2.4"
|
||||
"version": "2.2.5"
|
||||
},
|
||||
{
|
||||
"name": "json2object",
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
"name": "lime",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "27092822abf7b8c2ec2905053cf5435be4de838e",
|
||||
"ref": "d1596fe7daa9c479384ad2705e7813dbccf28839",
|
||||
"url": "https://github.com/FunkinCrew/lime"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
197
project.hxp
197
project.hxp
|
|
@ -11,6 +11,7 @@ import haxe.ds.Map;
|
|||
import haxe.xml.Printer;
|
||||
|
||||
using StringTools;
|
||||
using tools.AnsiUtil;
|
||||
|
||||
/**
|
||||
* This HXP performs the functions of a Lime `project.xml` file,
|
||||
|
|
@ -113,7 +114,11 @@ class Project extends HXProject
|
|||
*/
|
||||
static final ANDROID_TARGET_SDK_VERSION:Int = 35;
|
||||
|
||||
static final ANDROID_EXTENSIONS:Array<String> = ["funkin.extensions.CallbackUtil", "funkin.extensions.FNFCExtension"];
|
||||
static final ANDROID_EXTENSIONS:Array<String> = [
|
||||
"funkin.extensions.CallbackUtil",
|
||||
"funkin.extensions.FNFCExtension",
|
||||
"funkin.extensions.AudioSession"
|
||||
];
|
||||
|
||||
/**
|
||||
* The team ID to use for the iOS app. Configured in XCode.
|
||||
|
|
@ -293,6 +298,13 @@ class Project extends HXProject
|
|||
*/
|
||||
static final FEATURE_DISCORD_RPC:FeatureFlag = "FEATURE_DISCORD_RPC";
|
||||
|
||||
/**
|
||||
* `-DFEATURE_LOST_FOCUS_VOLUME`
|
||||
* If this flag is enabled, the game will reduce application volume, when lost focus.
|
||||
* Allowed only on Desktop targets.
|
||||
*/
|
||||
static final FEATURE_LOST_FOCUS_VOLUME:FeatureFlag = "FEATURE_LOST_FOCUS_VOLUME";
|
||||
|
||||
/**
|
||||
* `-DFEATURE_FILE_DROP`
|
||||
* If this flag is enabled, the game will support dragging and dropping files onto it for various features.
|
||||
|
|
@ -857,6 +869,8 @@ class Project extends HXProject
|
|||
// We don't want testers to accidentally leak songs to their Discord friends!
|
||||
FEATURE_DISCORD_RPC.apply(this, isDesktop() && !FEATURE_DEBUG_FUNCTIONS.isEnabled(this));
|
||||
|
||||
FEATURE_LOST_FOCUS_VOLUME.apply(this, isDesktop());
|
||||
|
||||
// Newgrounds features
|
||||
FEATURE_NEWGROUNDS.apply(this, !isMobile());
|
||||
FEATURE_NEWGROUNDS_DEBUG.apply(this, false);
|
||||
|
|
@ -1088,6 +1102,9 @@ class Project extends HXProject
|
|||
addHaxeMacro("include('haxe.ui.containers.properties')");
|
||||
}
|
||||
|
||||
// Ensure all flixel-animate classes are available at runtime.
|
||||
addHaxeMacro("include('animate')");
|
||||
|
||||
// Ensure all Flixel classes are available at runtime.
|
||||
// Explicitly ignore packages which require additional dependencies.
|
||||
addHaxeMacro("include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*', 'flixel.addons.tile.FlxRayCastTilemap' ])");
|
||||
|
|
@ -1299,7 +1316,7 @@ class Project extends HXProject
|
|||
}
|
||||
|
||||
addHaxelib('polymod'); // Modding framework
|
||||
addHaxelib('flxanimate'); // Texture atlas rendering
|
||||
addHaxelib('flixel-animate'); // Texture atlas rendering
|
||||
|
||||
addHaxelib('json2object'); // JSON parsing
|
||||
addHaxelib('jsonpath'); // JSON parsing
|
||||
|
|
@ -1410,9 +1427,9 @@ class Project extends HXProject
|
|||
|
||||
if (!FEATURE_ANIMATION_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/animation-editor', "default");
|
||||
|
||||
if (!FEATURE_ANIMATION_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/chart-editor', "default");
|
||||
if (!FEATURE_CHART_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/chart-editor', "default");
|
||||
|
||||
if (!FEATURE_ANIMATION_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/stage-editor', "default");
|
||||
if (!FEATURE_STAGE_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/stage-editor', "default");
|
||||
|
||||
// Font assets
|
||||
var shouldEmbedFonts = true;
|
||||
|
|
@ -1470,8 +1487,8 @@ class Project extends HXProject
|
|||
|
||||
function clearAssets():Void
|
||||
{
|
||||
// We don't want the haxe compilation server deleting our assets, do we?
|
||||
if (!isDisplay()) return;
|
||||
// Don't run on non-build commands.
|
||||
if (!isBuild()) return;
|
||||
|
||||
var exportPath:Null<String> = app.path ?? "";
|
||||
|
||||
|
|
@ -1739,6 +1756,11 @@ class Project extends HXProject
|
|||
return this.command == "clean";
|
||||
}
|
||||
|
||||
public function isBuild():Bool
|
||||
{
|
||||
return this.command == "test" || this.command == "build";
|
||||
}
|
||||
|
||||
public function isDebug():Bool
|
||||
{
|
||||
return this.debug;
|
||||
|
|
@ -1906,7 +1928,7 @@ class Project extends HXProject
|
|||
}
|
||||
else
|
||||
{
|
||||
// info(' Found asset path "${path}".');
|
||||
// info(' Found asset path "${path}".');
|
||||
}
|
||||
|
||||
for (file in sys.FileSystem.readDirectory(path))
|
||||
|
|
@ -2013,7 +2035,7 @@ class Project extends HXProject
|
|||
if (process.exitCode() != 0)
|
||||
{
|
||||
var message = process.stderr.readAll().toString();
|
||||
error('[ERROR] Could not determine current git commit; is this a proper Git repository?');
|
||||
error('Could not determine current git commit; is this a proper Git repository?');
|
||||
}
|
||||
|
||||
var commitHash:String = process.stdout.readLine();
|
||||
|
|
@ -2025,9 +2047,9 @@ class Project extends HXProject
|
|||
}
|
||||
|
||||
@SuppressWarnings('checkstyle:Dynamic')
|
||||
static function checkLibraries():Void
|
||||
function checkLibraries():Void
|
||||
{
|
||||
var outdatedLibraries:Map<String, Array<String>> = new Map<String, Array<String>>();
|
||||
var diffrentLibraries:Map<String, Array<String>> = new Map<String, Array<String>>();
|
||||
|
||||
var hmmData:Dynamic = haxe.Json.parse(sys.io.File.getContent(#if ios '../../../../../' + #end 'hmm.json'));
|
||||
|
||||
|
|
@ -2041,57 +2063,80 @@ class Project extends HXProject
|
|||
|
||||
var libraryCurrentVersion:String = readLibraryCurrentVersion(libraryName);
|
||||
|
||||
switch (library.type)
|
||||
{
|
||||
case 'haxelib':
|
||||
if (libraryDev != "")
|
||||
{
|
||||
outdatedLibraries.set(libraryName, [libraryDev, library.version]);
|
||||
}
|
||||
else if (library.version != libraryCurrentVersion)
|
||||
{
|
||||
outdatedLibraries.set(libraryName, [libraryCurrentVersion, library.version]);
|
||||
}
|
||||
case 'git':
|
||||
if (libraryDev != "")
|
||||
{
|
||||
outdatedLibraries.set(libraryName, [libraryDev, library.ref]);
|
||||
}
|
||||
else
|
||||
{
|
||||
var commitHash:String = readLibraryGitCommitHash(libraryName);
|
||||
var libraryCurrentCommitHash:String = readLibraryGitCommitHash(libraryName);
|
||||
|
||||
if (commitHash != library.ref && commitHash != "") outdatedLibraries.set(libraryName, [commitHash, library.ref]);
|
||||
}
|
||||
if (libraryDev != "" && !isLibraryLocalGitDev(libraryName))
|
||||
{
|
||||
switch (library.type)
|
||||
{
|
||||
case 'haxelib':
|
||||
diffrentLibraries.set(libraryName, [libraryDev, libraryCurrentCommitHash, library.version, 'haxelib']);
|
||||
case 'git':
|
||||
if (libraryCurrentCommitHash != library.ref)
|
||||
{
|
||||
diffrentLibraries.set(libraryName, [libraryDev, libraryCurrentCommitHash, library.ref, 'git']);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (library.type)
|
||||
{
|
||||
case 'haxelib':
|
||||
if (library.version != libraryCurrentVersion)
|
||||
{
|
||||
diffrentLibraries.set(libraryName, ['', libraryCurrentVersion, library.version, 'haxelib']);
|
||||
}
|
||||
case 'git':
|
||||
if (libraryCurrentCommitHash != library.ref)
|
||||
{
|
||||
diffrentLibraries.set(libraryName, ['', libraryCurrentCommitHash, library.ref, 'git']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Lambda.count(outdatedLibraries) > 0)
|
||||
if (Lambda.count(diffrentLibraries) > 0)
|
||||
{
|
||||
Sys.println("\x1b[1;33m\nWARNING: The following haxelibs diverge from the versions set in hmm.json."
|
||||
+ "\n\nThey may be outdated, so it is recommended to abort compilation and run `hmm reinstall [library]` to update each library."
|
||||
+ "\n\nYou may ignore this warning if your libraries are newer than the versions in hmm.json, or if you know what you're doing.\n\x1b[0m");
|
||||
warn("Some libraries differ from the versions defined in hmm.json.".bold().yellow());
|
||||
warn("To ensure consistency, it's recommended to stop the build and run `hmm reinstall [library]`.".bold().yellow());
|
||||
warn("If you're intentionally using development builds or newer versions, you can ignore this warning.".bold().yellow());
|
||||
|
||||
for (libraryName in outdatedLibraries.keys())
|
||||
Sys.println('');
|
||||
|
||||
for (libraryName in diffrentLibraries.keys())
|
||||
{
|
||||
var versions:Null<Array<String>> = outdatedLibraries.get(libraryName);
|
||||
if (versions == null) continue;
|
||||
var infos:Null<Array<String>> = diffrentLibraries.get(libraryName);
|
||||
|
||||
var outdatedVersion:String = versions[0];
|
||||
var expectedVersion:String = versions[1];
|
||||
if (infos == null) continue;
|
||||
|
||||
var isDev:Bool = outdatedVersion.contains("/");
|
||||
var devPath:String = infos[0];
|
||||
var currentVersion:String = infos[1];
|
||||
var expectedVersion:String = infos[2];
|
||||
var libType:String = infos[3];
|
||||
|
||||
Sys.println("- " + libraryName.replace(",", ".") + (isDev ? "\x1b[1;34m (Development Build)\x1b[0m" : ""));
|
||||
Sys.println(" \x1b[38;5;203mCurrent version: " + outdatedVersion + "\x1b[0m");
|
||||
Sys.println(" \x1b[38;5;82mExpected version: " + expectedVersion + "\x1b[0m\n");
|
||||
if (haxe.io.Path.isAbsolute(devPath))
|
||||
{
|
||||
Sys.println("- " + libraryName.replace(",", ".") + " (Development Build)".blue().bold());
|
||||
Sys.println((" Path: " + haxe.io.Path.removeTrailingSlashes(devPath)).blue());
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Sys.println("- " + libraryName.replace(",", "."));
|
||||
}
|
||||
|
||||
Sys.println((" Current: " + currentVersion).red());
|
||||
Sys.println((" Expected: " + expectedVersion).green());
|
||||
}
|
||||
|
||||
Sys.println('');
|
||||
}
|
||||
}
|
||||
|
||||
static function readLibraryCurrentVersion(libraryName:String):String
|
||||
{
|
||||
var path = haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']);
|
||||
var path = getLibraryCurrentFile(libraryName);
|
||||
|
||||
if (sys.FileSystem.exists(path)) return sys.io.File.getContent(path);
|
||||
|
||||
|
|
@ -2100,7 +2145,7 @@ class Project extends HXProject
|
|||
|
||||
static function readLibraryDev(libraryName:String):String
|
||||
{
|
||||
var path = haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.dev']);
|
||||
var path = getLibraryDevFile(libraryName);
|
||||
|
||||
if (sys.FileSystem.exists(path)) return sys.io.File.getContent(path);
|
||||
|
||||
|
|
@ -2109,11 +2154,48 @@ class Project extends HXProject
|
|||
|
||||
static function readLibraryGitCommitHash(libraryName:String):String
|
||||
{
|
||||
var gitProccess = new sys.io.Process("git", ["-C", ".haxelib/" + libraryName + "/git", "rev-parse", "HEAD"]);
|
||||
var commit:String = '';
|
||||
|
||||
var gitProccess = new sys.io.Process("git", ["-C", getLibraryGitPath(libraryName), "rev-parse", "HEAD"]);
|
||||
gitProccess.exitCode(true);
|
||||
commit = gitProccess.stdout.readAll().toString().trim();
|
||||
|
||||
return gitProccess.stdout.readAll().toString().trim();
|
||||
if (commit.length <= 0)
|
||||
{
|
||||
gitProccess = new sys.io.Process("git", ["-C", readLibraryDev(libraryName), "rev-parse", "HEAD"]);
|
||||
gitProccess.exitCode(true);
|
||||
commit = gitProccess.stdout.readAll().toString().trim();
|
||||
}
|
||||
|
||||
return commit;
|
||||
}
|
||||
|
||||
static function getLibraryCurrentFile(libraryName:String):String
|
||||
{
|
||||
return haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']);
|
||||
}
|
||||
|
||||
static function getLibraryDevFile(libraryName:String):String
|
||||
{
|
||||
return haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.dev']);
|
||||
}
|
||||
|
||||
static function getLibraryGitPath(libraryName:String):String
|
||||
{
|
||||
return haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, 'git']);
|
||||
}
|
||||
|
||||
static function isLibraryLocalGitDev(libraryName:String):Bool
|
||||
{
|
||||
final gitPath:String = getLibraryGitPath(libraryName);
|
||||
final devFile:String = getLibraryDevFile(libraryName);
|
||||
|
||||
if (FileSystem.exists(gitPath) && FileSystem.exists(devFile))
|
||||
{
|
||||
return Path.normalize(readLibraryDev(libraryName)).startsWith(gitPath);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static function getLibraryPath(libraryName:String):String
|
||||
|
|
@ -2194,7 +2276,7 @@ class Project extends HXProject
|
|||
*/
|
||||
public function error(message:String):Void
|
||||
{
|
||||
Sys.stderr().write(Bytes.ofString('[ERROR] ${message}'));
|
||||
Sys.stderr().write(Bytes.ofString(' ERROR '.bold().bg_red() + " " + message));
|
||||
Sys.exit(1);
|
||||
}
|
||||
|
||||
|
|
@ -2205,7 +2287,18 @@ class Project extends HXProject
|
|||
{
|
||||
if (!(isDisplay() || isClean()))
|
||||
{
|
||||
Sys.println('[INFO] ${message}');
|
||||
Sys.println(' INFO '.bold().bg_blue() + " " + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a warning message. This should not interfere with the build process.
|
||||
*/
|
||||
public function warn(message:String):Void
|
||||
{
|
||||
if (!(isDisplay() || isClean()))
|
||||
{
|
||||
Sys.println(' WARNING '.bold().bg_yellow() + " " + message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2217,7 +2310,7 @@ class Project extends HXProject
|
|||
{
|
||||
if (!FileSystem.exists(file))
|
||||
{
|
||||
info('Environment file does not exist: ${file}');
|
||||
warn('Environment file does not exist: ${file}');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -2225,7 +2318,7 @@ class Project extends HXProject
|
|||
|
||||
if (envFile == null)
|
||||
{
|
||||
info('Failed to parse environment file: ${file}');
|
||||
warn('Failed to parse environment file: ${file}');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import openfl.media.Video;
|
|||
import openfl.net.NetStream;
|
||||
import funkin.util.WindowUtil;
|
||||
|
||||
using funkin.util.AnsiUtil;
|
||||
|
||||
/**
|
||||
* The main class which initializes HaxeFlixel and starts the game in its initial state.
|
||||
*/
|
||||
|
|
@ -133,11 +135,11 @@ class Main extends Sprite
|
|||
Handle.initAsync(function(success:Bool):Void {
|
||||
if (success)
|
||||
{
|
||||
trace('[HXVLC] LibVLC instance initialized!');
|
||||
trace(' HXVLC '.bold().bg_white() + ' LibVLC instance initialized!');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[HXVLC] LibVLC instance failed to initialize!');
|
||||
trace(' HXVLC '.bold().bg_white() + ' LibVLC instance failed to initialize!');
|
||||
}
|
||||
});
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import sys.FileSystem;
|
|||
import sys.io.File;
|
||||
|
||||
using StringTools;
|
||||
using tools.AnsiUtil;
|
||||
|
||||
/**
|
||||
* A script which executes after the game is built.
|
||||
|
|
@ -29,34 +30,32 @@ class Postbuild
|
|||
|
||||
sys.FileSystem.deleteFile(BUILD_TIME_FILE);
|
||||
|
||||
Sys.println('[INFO] Build took: ${format(end - start)}');
|
||||
Sys.println(' INFO '.bold().bg_blue() + ' Build took: ${format(end - start)}');
|
||||
}
|
||||
}
|
||||
|
||||
static function format(time:Float, decimals:Int = 1):String
|
||||
{
|
||||
var units = [
|
||||
{name: "day", secs: 86400},
|
||||
{name: "hour", secs: 3600},
|
||||
{name: "minute", secs: 60},
|
||||
{name: "second", secs: 1}
|
||||
];
|
||||
|
||||
var parts:Array<String> = [];
|
||||
var remaining:Float = time;
|
||||
var factor = Math.pow(10, decimals); // compute once because the old code was computing it twice.
|
||||
|
||||
for (u in units)
|
||||
{
|
||||
var value:Float = (u.name == "second") ? Math.round(remaining * factor) / factor : Math.floor(remaining / u.secs);
|
||||
|
||||
if (u.name != "second")
|
||||
remaining %= u.secs;
|
||||
|
||||
if (value > 0 || (u.name == "second" && parts.length == 0))
|
||||
parts.push('${value} ${u.name}${value == 1 ? "" : "s"}');
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
var units = [
|
||||
{name: "day", secs: 86400},
|
||||
{name: "hour", secs: 3600},
|
||||
{name: "minute", secs: 60},
|
||||
{name: "second", secs: 1}
|
||||
];
|
||||
|
||||
var parts:Array<String> = [];
|
||||
var remaining:Float = time;
|
||||
var factor = Math.pow(10, decimals); // compute once because the old code was computing it twice.
|
||||
|
||||
for (u in units)
|
||||
{
|
||||
var value:Float = (u.name == "second") ? Math.round(remaining * factor) / factor : Math.floor(remaining / u.secs);
|
||||
|
||||
if (u.name != "second") remaining %= u.secs;
|
||||
|
||||
if (value > 0 || (u.name == "second" && parts.length == 0)) parts.push('${value} ${u.name}${value == 1 ? "" : "s"}');
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package funkin;
|
||||
|
||||
import openfl.utils.Future;
|
||||
import funkin.util.macro.ConsoleMacro;
|
||||
|
||||
/**
|
||||
* A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions.
|
||||
* Later we'll add Funkin-specific caching to this.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Assets
|
||||
class Assets implements ConsoleClass
|
||||
{
|
||||
/**
|
||||
* The assets cache.
|
||||
|
|
|
|||
|
|
@ -526,7 +526,7 @@ class Conductor
|
|||
|
||||
/**
|
||||
* Can be called in-between frames, usually for input related things
|
||||
* that can potentially get processed on exact milliseconds/timestmaps.
|
||||
* that can potentially get processed on exact milliseconds/timestamps.
|
||||
* If you need song position, use `Conductor.instance.songPosition` instead
|
||||
* for use in update() related functions.
|
||||
* @param soundToCheck Which FlxSound object to check, defaults to FlxG.sound.music if no input
|
||||
|
|
|
|||
|
|
@ -111,6 +111,16 @@ class FunkinMemory
|
|||
|
||||
///// TEXTURES /////
|
||||
|
||||
/**
|
||||
* Determine whether the texture with the given key is cached.
|
||||
* @param key The key of the texture to check.
|
||||
* @return Whether the texture is cached.
|
||||
*/
|
||||
public static function isTextureCached(key:String):Bool
|
||||
{
|
||||
return FlxG.bitmap.get(key) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a texture with the given key is cached.
|
||||
* @param key The key of the texture to cache.
|
||||
|
|
@ -160,7 +170,7 @@ class FunkinMemory
|
|||
graphic.persist = true;
|
||||
permanentCachedTextures.set(key, graphic);
|
||||
forceRender(graphic);
|
||||
currentCachedTextures = permanentCachedTextures;
|
||||
currentCachedTextures = permanentCachedTextures.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -201,7 +211,7 @@ class FunkinMemory
|
|||
*/
|
||||
public inline static function preparePurgeTextureCache():Void
|
||||
{
|
||||
previousCachedTextures = currentCachedTextures;
|
||||
previousCachedTextures = currentCachedTextures.copy();
|
||||
|
||||
for (graphicKey in previousCachedTextures.keys())
|
||||
{
|
||||
|
|
@ -211,7 +221,7 @@ class FunkinMemory
|
|||
}
|
||||
}
|
||||
|
||||
currentCachedTextures = permanentCachedTextures;
|
||||
currentCachedTextures = permanentCachedTextures.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -233,6 +243,7 @@ class FunkinMemory
|
|||
if (graphic != null)
|
||||
{
|
||||
FlxG.bitmap.remove(graphic);
|
||||
graphic.persist = false;
|
||||
graphic.destroy();
|
||||
previousCachedTextures.remove(graphicKey);
|
||||
Assets.cache.clear(graphicKey);
|
||||
|
|
@ -250,7 +261,7 @@ class FunkinMemory
|
|||
{
|
||||
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
|
||||
|
||||
if (obj == null || obj.persist || permanentCachedTextures.exists(key) || key.contains("fonts"))
|
||||
if (obj == null || (obj.persist && permanentCachedTextures.exists(key)) || key.contains("fonts"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -262,6 +273,7 @@ class FunkinMemory
|
|||
if (key.contains(purgeEntry))
|
||||
{
|
||||
FlxG.bitmap.removeKey(key);
|
||||
obj.persist = false;
|
||||
obj.destroy();
|
||||
}
|
||||
}
|
||||
|
|
@ -346,7 +358,7 @@ class FunkinMemory
|
|||
|
||||
public static function preparePurgeSoundCache():Void
|
||||
{
|
||||
previousCachedSounds = currentCachedSounds;
|
||||
previousCachedSounds = currentCachedSounds.copy();
|
||||
|
||||
for (key in previousCachedSounds.keys())
|
||||
{
|
||||
|
|
@ -356,7 +368,7 @@ class FunkinMemory
|
|||
}
|
||||
}
|
||||
|
||||
currentCachedSounds = permanentCachedSounds;
|
||||
currentCachedSounds = permanentCachedSounds.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -153,6 +153,11 @@ class InitState extends FlxState
|
|||
// Set the game to a lower frame rate while it is in the background.
|
||||
FlxG.game.focusLostFramerate = 30;
|
||||
|
||||
// Persist controls inputs between states.
|
||||
// Without this, the game would release your keybinds when you switch states,
|
||||
// and then act like you released and re-pressed them the frame after.
|
||||
FlxG.inputs.resetOnStateSwitch = false;
|
||||
|
||||
// Makes Flixel use frame times instead of locked movements per frame for things like tweens
|
||||
FlxG.fixedTimestep = false;
|
||||
|
||||
|
|
@ -212,6 +217,10 @@ class InitState extends FlxState
|
|||
});
|
||||
#end
|
||||
|
||||
#if FEATURE_LOST_FOCUS_VOLUME
|
||||
FlxG.signals.focusLost.add(onLostFocus);
|
||||
FlxG.signals.focusGained.add(onGainFocus);
|
||||
#end
|
||||
//
|
||||
// ANDROID SETUP
|
||||
//
|
||||
|
|
@ -287,6 +296,23 @@ class InitState extends FlxState
|
|||
#end
|
||||
}
|
||||
|
||||
#if FEATURE_LOST_FOCUS_VOLUME
|
||||
@:noCompletion var _lastFocusVolume:Null<Float>;
|
||||
|
||||
function onLostFocus()
|
||||
{
|
||||
if (FlxG.sound.muted || FlxG.sound.volume == 0 || FlxG.autoPause) return;
|
||||
_lastFocusVolume = FlxG.sound.volume;
|
||||
FlxG.sound.volume *= Constants.LOST_FOCUS_VOLUME_MULTIPLIER;
|
||||
}
|
||||
|
||||
function onGainFocus()
|
||||
{
|
||||
if (FlxG.sound.muted || FlxG.autoPause) return;
|
||||
if (_lastFocusVolume != null) FlxG.sound.volume = _lastFocusVolume;
|
||||
}
|
||||
#end
|
||||
|
||||
/**
|
||||
* Start the game.
|
||||
*
|
||||
|
|
@ -558,19 +584,6 @@ class InitState extends FlxState
|
|||
// Disable using ~ to open the console (we use that for the Editor menu)
|
||||
FlxG.debugger.toggleKeys = [F2];
|
||||
TrackerUtil.initTrackers();
|
||||
// Adds an additional Close Debugger button.
|
||||
// This big obnoxious white button is for MOBILE, so that you can press it
|
||||
// easily with your finger when debug bullshit pops up during testing lol!
|
||||
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
|
||||
FlxG.debugger.visible = false;
|
||||
|
||||
// Make errors and warnings less annoying.
|
||||
// Forcing this always since I have never been happy to have the debugger to pop up
|
||||
LogStyle.ERROR.openConsole = false;
|
||||
LogStyle.ERROR.errorSound = null;
|
||||
LogStyle.WARNING.openConsole = false;
|
||||
LogStyle.WARNING.errorSound = null;
|
||||
});
|
||||
|
||||
// Adds a red button to the debugger.
|
||||
// This pauses the game AND the music! This ensures the Conductor stops.
|
||||
|
|
@ -613,6 +626,22 @@ class InitState extends FlxState
|
|||
FlxG.sound.music.pause();
|
||||
FlxG.sound.music.time += FlxG.elapsed * 1000;
|
||||
});
|
||||
|
||||
// Adds an additional Close Debugger button.
|
||||
// This big obnoxious white button is for MOBILE, so that you can press it
|
||||
// easily with your finger when debug bullshit pops up during testing lol!
|
||||
#if mobile
|
||||
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
|
||||
FlxG.debugger.visible = false;
|
||||
|
||||
// Make errors and warnings less annoying.
|
||||
// Forcing this always since I have never been happy to have the debugger to pop up
|
||||
LogStyle.ERROR.openConsole = false;
|
||||
LogStyle.ERROR.errorSound = null;
|
||||
LogStyle.WARNING.openConsole = false;
|
||||
LogStyle.WARNING.errorSound = null;
|
||||
});
|
||||
#end // mobile big butotn crap
|
||||
#end
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ package funkin;
|
|||
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import openfl.utils.AssetType;
|
||||
import funkin.util.macro.ConsoleMacro;
|
||||
import haxe.io.Path;
|
||||
|
||||
/**
|
||||
* A core class which handles determining asset paths.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Paths
|
||||
class Paths implements ConsoleClass
|
||||
{
|
||||
static var currentLevel:Null<String> = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class Preferences
|
|||
#else
|
||||
var save:Save = Save.instance;
|
||||
save.options.framerate = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
FlxG.updateFramerate = value;
|
||||
FlxG.drawFramerate = value;
|
||||
return value;
|
||||
|
|
@ -74,7 +74,7 @@ class Preferences
|
|||
|
||||
var save:Save = Save.instance;
|
||||
save.options.naughtyness = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.downscroll = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.flashingLights = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.zoomCamera = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ class Preferences
|
|||
|
||||
var save = Save.instance;
|
||||
save.options.debugDisplay = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ class Preferences
|
|||
|
||||
var save:Save = Save.instance;
|
||||
save.options.debugDisplayBGOpacity = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +219,7 @@ class Preferences
|
|||
|
||||
var save:Save = Save.instance;
|
||||
save.options.hapticsMode = string;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.hapticsIntensityMultiplier = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ class Preferences
|
|||
|
||||
var save:Save = Save.instance;
|
||||
save.options.autoPause = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -282,7 +282,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.autoFullscreen = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -302,7 +302,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.globalOffset = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -349,7 +349,7 @@ class Preferences
|
|||
|
||||
var save:Save = Save.instance;
|
||||
save.options.vsyncMode = string;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -371,7 +371,7 @@ class Preferences
|
|||
|
||||
var save:Save = Save.instance;
|
||||
save.options.unlockedFramerate = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -411,7 +411,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.strumlineBackgroundOpacity = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -430,7 +430,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.shouldHideMouse = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -449,7 +449,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.fancyPreview = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -468,7 +468,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.previewOnSave = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -535,7 +535,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.subtitles = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -557,7 +557,7 @@ class Preferences
|
|||
|
||||
var save:Save = Save.instance;
|
||||
save.mobileOptions.screenTimeout = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -576,7 +576,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.mobileOptions.controlsScheme = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -601,7 +601,7 @@ class Preferences
|
|||
{
|
||||
var save:Save = Save.instance;
|
||||
save.mobileOptions.noAds = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class DiscordClient
|
|||
|
||||
private function new()
|
||||
{
|
||||
trace('[DISCORD] Initializing event handlers...');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Initializing event handlers...');
|
||||
|
||||
handlers = new DiscordEventHandlers();
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ class DiscordClient
|
|||
|
||||
public function init():Void
|
||||
{
|
||||
trace('[DISCORD] Initializing connection...');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Initializing connection...');
|
||||
|
||||
if (!hasValidCredentials())
|
||||
{
|
||||
|
|
@ -86,7 +86,7 @@ class DiscordClient
|
|||
|
||||
public function shutdown():Void
|
||||
{
|
||||
trace('[DISCORD] Shutting down...');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Shutting down...');
|
||||
|
||||
Discord.Shutdown();
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ class DiscordClient
|
|||
// TODO: WHAT THE FUCK get this pointer bullfuckery out of here
|
||||
private static function onReady(request:cpp.RawConstPointer<DiscordUser>):Void
|
||||
{
|
||||
trace('[DISCORD] Client has connected!');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Client has connected!');
|
||||
|
||||
final username:String = request[0].username;
|
||||
final globalName:String = request[0].username;
|
||||
|
|
@ -148,22 +148,22 @@ class DiscordClient
|
|||
|
||||
if (discriminator != null && discriminator != 0)
|
||||
{
|
||||
trace('[DISCORD] User: ${username}#${discriminator} (${globalName})');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' User: ${username}#${discriminator} (${globalName})');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[DISCORD] User: @${username} (${globalName})');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' User: @${username} (${globalName})');
|
||||
}
|
||||
}
|
||||
|
||||
private static function onDisconnected(errorCode:Int, message:cpp.ConstCharStar):Void
|
||||
{
|
||||
trace('[DISCORD] Client has disconnected! ($errorCode) "${cast (message, String)}"');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Client has disconnected! ($errorCode) "${cast (message, String)}"');
|
||||
}
|
||||
|
||||
private static function onError(errorCode:Int, message:cpp.ConstCharStar):Void
|
||||
{
|
||||
trace('[DISCORD] Client has received an error! ($errorCode) "${cast (message, String)}"');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Client has received an error! ($errorCode) "${cast (message, String)}"');
|
||||
}
|
||||
|
||||
// public var type(get, set):DiscordActivityType;
|
||||
|
|
|
|||
|
|
@ -39,21 +39,21 @@ class Events
|
|||
switch (outcome)
|
||||
{
|
||||
case SUCCESS(data):
|
||||
trace('[NEWGROUNDS] Logged event: ${data.eventName}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logged event: ${data.eventName}');
|
||||
case FAIL(outcome):
|
||||
switch (outcome)
|
||||
{
|
||||
case HTTP(error):
|
||||
trace('[NEWGROUNDS] HTTP error while logging event: ${error}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' HTTP error while logging event: ${error}');
|
||||
case RESPONSE(error):
|
||||
trace('[NEWGROUNDS] Response error (${error.code}) while logging event: ${error.message}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Response error (${error.code}) while logging event: ${error.message}');
|
||||
case RESULT(error):
|
||||
switch (error.code)
|
||||
{
|
||||
case 103: // Invalid custom event name
|
||||
trace('[NEWGROUNDS] Invalid custom event name: ${eventName}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Invalid custom event name: ${eventName}');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Result error (${error.code}) while logging event: ${error.message}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Result error (${error.code}) while logging event: ${error.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class Leaderboards
|
|||
var leaderboardList:Null<ScoreBoardList> = NewgroundsClient.instance.leaderboards;
|
||||
if (leaderboardList == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, cannot fetch medal data!');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -44,9 +44,9 @@ class Leaderboards
|
|||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Submitted score!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitted score!');
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to submit score!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to submit score!');
|
||||
trace(error);
|
||||
}
|
||||
});
|
||||
|
|
@ -75,11 +75,11 @@ class Leaderboards
|
|||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Fetched scores!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched scores!');
|
||||
if (params != null && params.onComplete != null) params.onComplete(leaderboardData.scores);
|
||||
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to fetch scores!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to fetch scores!');
|
||||
trace(error);
|
||||
if (params != null && params.onFail != null) params.onFail();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class Medals
|
|||
|
||||
if (medalList == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, cannot fetch medal data!');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -34,12 +34,12 @@ class Medals
|
|||
@:privateAccess
|
||||
if (medalData == null || medalData._data == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Could not retrieve data for medal: ${medal}');
|
||||
return;
|
||||
}
|
||||
else if (!medalData.unlocked)
|
||||
{
|
||||
trace('[NEWGROUNDS] Awarding medal (${medal}).');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Awarding medal (${medal}).');
|
||||
medalData.sendUnlock();
|
||||
|
||||
// Play the medal unlock animation, but only if the user has not already unlocked it.
|
||||
|
|
@ -81,12 +81,12 @@ class Medals
|
|||
}
|
||||
else
|
||||
{
|
||||
trace('[NEWGROUNDS] User already has medal (${medal}).');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' User already has medal (${medal}).');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[NEWGROUNDS] Attempted to award medal (${medal}), but not logged into Newgrounds.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Attempted to award medal (${medal}), but not logged into Newgrounds.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,12 +98,12 @@ class Medals
|
|||
|
||||
var parser = new json2object.JsonParser<Array<MedalJSON>>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
trace('[NEWGROUNDS] Parsing local medal data...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Parsing local medal data...');
|
||||
parser.fromJson(jsonString, jsonPath);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to parse local medal data!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to parse local medal data!');
|
||||
for (error in parser.errors)
|
||||
funkin.data.DataError.printError(error);
|
||||
medalJSON = [];
|
||||
|
|
@ -120,7 +120,7 @@ class Medals
|
|||
@:privateAccess
|
||||
if (medalData == null || medalData._data == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Could not retrieve data for medal: ${medal}');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ class Medals
|
|||
var medal:Medal = Medal.getMedalByStoryLevel(id);
|
||||
if (medal == Medal.Unknown)
|
||||
{
|
||||
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Story level does not have a medal! (${id}).');
|
||||
return;
|
||||
}
|
||||
Medals.award(medal);
|
||||
|
|
@ -226,7 +226,7 @@ enum abstract Medal(Int) from Int to Int
|
|||
|
||||
/**
|
||||
* That's How You Do It!
|
||||
* Beat Tutoria l in Story Mode (on any difficulty).
|
||||
* Beat Tutorial in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryTutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80906 #else 83647 #end;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class NGSaveSlot
|
|||
|
||||
public static function loadInstance():NGSaveSlot
|
||||
{
|
||||
var loadedSave:NGSaveSlot = loadSlot(Save.BASE_SAVE_SLOT);
|
||||
var loadedSave:NGSaveSlot = loadSlot(Constants.BASE_SAVE_SLOT);
|
||||
if (_instance == null) _instance = loadedSave;
|
||||
|
||||
return loadedSave;
|
||||
|
|
@ -33,7 +33,7 @@ class NGSaveSlot
|
|||
|
||||
static function loadSlot(slot:Int):NGSaveSlot
|
||||
{
|
||||
trace('[NEWGROUNDS] Getting save slot from ID $slot');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Getting save slot from ID $slot');
|
||||
|
||||
var saveSlot:Null<SaveSlot> = NewgroundsClient.instance.saveSlots?.getById(slot);
|
||||
|
||||
|
|
@ -46,11 +46,6 @@ class NGSaveSlot
|
|||
public function new(?ngSaveSlot:Null<SaveSlot>)
|
||||
{
|
||||
this.ngSaveSlot = ngSaveSlot;
|
||||
|
||||
#if FLX_DEBUG
|
||||
FlxG.console.registerClass(NGSaveSlot);
|
||||
FlxG.console.registerClass(Save);
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,16 +62,16 @@ class NGSaveSlot
|
|||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Successfully saved save data to save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Successfully saved save data to save slot!');
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to save data to save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to save data to save slot!');
|
||||
trace(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error:String)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to save data to save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to save data to save slot!');
|
||||
trace(error);
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +84,7 @@ class NGSaveSlot
|
|||
switch (outcome)
|
||||
{
|
||||
case SUCCESS(value):
|
||||
trace('[NEWGROUNDS] Loaded save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Loaded save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
trace('Save Slot Data:');
|
||||
trace(value);
|
||||
|
|
@ -101,7 +96,7 @@ class NGSaveSlot
|
|||
onComplete(decodedData);
|
||||
}
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(error);
|
||||
|
||||
if (onError != null)
|
||||
|
|
@ -113,7 +108,7 @@ class NGSaveSlot
|
|||
}
|
||||
catch (error:String)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(error);
|
||||
|
||||
if (onError != null)
|
||||
|
|
@ -131,26 +126,26 @@ class NGSaveSlot
|
|||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Successfully cleared save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Successfully cleared save slot!');
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to clear save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to clear save slot!');
|
||||
trace(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error:String)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to clear save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to clear save slot!');
|
||||
trace(error);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkSlot():Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Checking save slot with the ID of ${ngSaveSlot?.id}...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Checking save slot with the ID of ${ngSaveSlot?.id}...');
|
||||
|
||||
trace(' Is null? ${ngSaveSlot == null}');
|
||||
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
|
||||
trace(' Is null? ${ngSaveSlot == null}');
|
||||
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -42,11 +42,11 @@ class NewgroundsClient
|
|||
|
||||
private function new()
|
||||
{
|
||||
trace('[NEWGROUNDS] Initializing client...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Initializing client...');
|
||||
|
||||
#if FEATURE_NEWGROUNDS_DEBUG
|
||||
trace('[NEWGROUNDS] App ID: ${API_NG_APP_ID}');
|
||||
trace('[NEWGROUNDS] Encryption Key: ${API_NG_ENC_KEY}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' App ID: ${API_NG_APP_ID}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Encryption Key: ${API_NG_ENC_KEY}');
|
||||
#end
|
||||
|
||||
if (!hasValidCredentials())
|
||||
|
|
@ -67,7 +67,7 @@ class NewgroundsClient
|
|||
{
|
||||
if (NG.core == null) return;
|
||||
|
||||
trace('[NEWGROUNDS] Setting up connection...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Setting up connection...');
|
||||
|
||||
#if FEATURE_NEWGROUNDS_DEBUG
|
||||
NG.core.verbose = true;
|
||||
|
|
@ -78,16 +78,16 @@ class NewgroundsClient
|
|||
if (NG.core.attemptingLogin)
|
||||
{
|
||||
// Session ID was valid and we should be logged in soon.
|
||||
trace('[NEWGROUNDS] Waiting for existing login!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Waiting for existing login!');
|
||||
}
|
||||
else
|
||||
{
|
||||
#if FEATURE_NEWGROUNDS_AUTOLOGIN
|
||||
// Attempt an automatic login.
|
||||
trace('[NEWGROUNDS] Attempting new login immediately!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Attempting new login immediately!');
|
||||
this.autoLogin();
|
||||
#else
|
||||
trace('[NEWGROUNDS] Not logged in, you have to login manually!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, you have to login manually!');
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ class NewgroundsClient
|
|||
|
||||
if (NG.core.attemptingLogin)
|
||||
{
|
||||
trace("[NEWGROUNDS] Login attempt ongoing, will not login until finished.");
|
||||
trace(" NEWGROUNDS '.bold().bg_orange() + ' Login attempt ongoing, will not login until finished.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +165,7 @@ class NewgroundsClient
|
|||
}
|
||||
}
|
||||
|
||||
Save.instance.ngSessionId = null;
|
||||
Save.instance.ngSessionId.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -245,17 +245,17 @@ class NewgroundsClient
|
|||
{
|
||||
if (NG.core == null) return;
|
||||
|
||||
trace('[NEWGROUNDS] Login successful!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login successful!');
|
||||
|
||||
// Persist the session ID.
|
||||
Save.instance.ngSessionId = NG.core.sessionId;
|
||||
Save.instance.ngSessionId.value = NG.core.sessionId;
|
||||
|
||||
trace('[NEWGROUNDS] Submitting medal request...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting medal request...');
|
||||
NG.core.requestMedals(onFetchedMedals);
|
||||
|
||||
trace('[NEWGROUNDS] Submitting leaderboard request...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting leaderboard request...');
|
||||
NG.core.scoreBoards.loadList(onFetchedLeaderboards);
|
||||
trace('[NEWGROUNDS] Submitting save slot request...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting save slot request...');
|
||||
NG.core.saveSlots.loadList(onFetchedSaveSlots);
|
||||
}
|
||||
|
||||
|
|
@ -267,32 +267,32 @@ class NewgroundsClient
|
|||
switch (type)
|
||||
{
|
||||
case PASSPORT:
|
||||
trace('[NEWGROUNDS] Login cancelled by passport website.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by passport website.');
|
||||
case MANUAL:
|
||||
trace('[NEWGROUNDS] Login cancelled by application.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by application.');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Login cancelled by unknown source.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by unknown source.');
|
||||
}
|
||||
case ERROR(error):
|
||||
switch (error)
|
||||
{
|
||||
case HTTP(error):
|
||||
trace('[NEWGROUNDS] Login failed due to HTTP error: ${error}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to HTTP error: ${error}');
|
||||
case RESPONSE(error):
|
||||
trace('[NEWGROUNDS] Login failed due to response error: ${error.message} (${error.code})');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to response error: ${error.message} (${error.code})');
|
||||
case RESULT(error):
|
||||
trace('[NEWGROUNDS] Login failed due to result error: ${error.message} (${error.code})');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to result error: ${error.message} (${error.code})');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Login failed due to unknown error: ${error}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to unknown error: ${error}');
|
||||
}
|
||||
default:
|
||||
trace('[NEWGROUNDS] Login failed due to unknown reason.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to unknown reason.');
|
||||
}
|
||||
}
|
||||
|
||||
function onLogoutSuccessful():Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Logout successful!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout successful!');
|
||||
}
|
||||
|
||||
function onLogoutFailed(result:CallError):Void
|
||||
|
|
@ -300,31 +300,31 @@ class NewgroundsClient
|
|||
switch (result)
|
||||
{
|
||||
case HTTP(error):
|
||||
trace('[NEWGROUNDS] Logout failed due to HTTP error: ${error}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to HTTP error: ${error}');
|
||||
case RESPONSE(error):
|
||||
trace('[NEWGROUNDS] Logout failed due to response error: ${error.message} (${error.code})');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to response error: ${error.message} (${error.code})');
|
||||
case RESULT(error):
|
||||
trace('[NEWGROUNDS] Logout failed due to result error: ${error.message} (${error.code})');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to result error: ${error.message} (${error.code})');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Logout failed due to unknown error: ${result}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to unknown error: ${result}');
|
||||
}
|
||||
}
|
||||
|
||||
function onFetchedMedals(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Fetched medals!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched medals!');
|
||||
}
|
||||
|
||||
function onFetchedLeaderboards(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Fetched leaderboards!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched leaderboards!');
|
||||
|
||||
// trace(funkin.api.newgrounds.Leaderboards.listLeaderboardData());
|
||||
}
|
||||
|
||||
function onFetchedSaveSlots(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Fetched save slots!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched save slots!');
|
||||
|
||||
NGSaveSlot.instance.checkSlot();
|
||||
}
|
||||
|
|
@ -362,7 +362,7 @@ class NewgroundsClient
|
|||
#end
|
||||
|
||||
// We have to fetch the session ID from the save file.
|
||||
return Save.instance.ngSessionId;
|
||||
return Save.instance.ngSessionId.value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
|
|||
for (i in 0...group.members.length)
|
||||
{
|
||||
// needs to be exponential growth / scaling
|
||||
// still need to optmize the FFT to run better, gets only samples needed?
|
||||
// still need to optimize the FFT to run better, gets only samples needed?
|
||||
// not every frequency is built the same!
|
||||
// 20hz to 40z is a LOT of subtle low ends, but somethin like 20,000hz to 20,020hz, the difference is NOT the same!
|
||||
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class WaveformData
|
|||
|
||||
var ratio = newSamplesPerPoint / samplesPerPoint;
|
||||
if (ratio == 1) return result;
|
||||
if (ratio < 1) trace('[WARNING] Downsampling will result in a low precision.');
|
||||
if (ratio < 1) trace(' WARNING '.bg_yellow().bold() + ' Downsampling will result in a low precision.');
|
||||
|
||||
var inputSampleCount = this.lenSamples();
|
||||
var outputSampleCount = Std.int(inputSampleCount * ratio);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ package funkin.audio.waveform;
|
|||
import funkin.graphics.rendering.MeshRender;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
/**
|
||||
* A sprite which displays the waveform of audio data.
|
||||
* Generate a WaveformData and provide it to this sprite.
|
||||
*/
|
||||
class WaveformSprite extends MeshRender
|
||||
{
|
||||
static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE;
|
||||
|
|
@ -18,14 +22,17 @@ class WaveformSprite extends MeshRender
|
|||
* Do this any time the data or drawable area of the waveform changes.
|
||||
* This often (but not always) needs to be done every frame.
|
||||
*/
|
||||
var isWaveformDirty:Bool = true;
|
||||
var isWaveformDirty:Bool;
|
||||
|
||||
/**
|
||||
* If true, force the waveform to redraw every frame.
|
||||
* Useful if the waveform's clipRect is constantly changing.
|
||||
*/
|
||||
public var forceUpdate:Bool = false;
|
||||
public var forceUpdate:Bool;
|
||||
|
||||
/**
|
||||
* The data to render the waveform with.
|
||||
*/
|
||||
public var waveformData(default, set):Null<WaveformData>;
|
||||
|
||||
function set_waveformData(value:Null<WaveformData>):Null<WaveformData>
|
||||
|
|
@ -52,6 +59,9 @@ class WaveformSprite extends MeshRender
|
|||
return waveformColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Waveform is horizontal or vertical.
|
||||
*/
|
||||
public var orientation(default, set):WaveformOrientation;
|
||||
|
||||
function set_orientation(value:WaveformOrientation):WaveformOrientation
|
||||
|
|
@ -68,7 +78,7 @@ class WaveformSprite extends MeshRender
|
|||
*/
|
||||
public var time(default, set):Float;
|
||||
|
||||
function set_time(value:Float)
|
||||
function set_time(value:Float):Float
|
||||
{
|
||||
if (time == value) return value;
|
||||
|
||||
|
|
@ -77,13 +87,22 @@ class WaveformSprite extends MeshRender
|
|||
return time;
|
||||
}
|
||||
|
||||
override function set_visible(value:Bool):Bool
|
||||
{
|
||||
if (visible == value) return value;
|
||||
|
||||
visible = value;
|
||||
isWaveformDirty = true;
|
||||
return visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* The duration, in seconds, that the waveform represents.
|
||||
* The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents.
|
||||
*/
|
||||
public var duration(default, set):Float;
|
||||
|
||||
function set_duration(value:Float)
|
||||
function set_duration(value:Float):Float
|
||||
{
|
||||
if (duration == value) return value;
|
||||
|
||||
|
|
@ -120,13 +139,13 @@ class WaveformSprite extends MeshRender
|
|||
*
|
||||
* NOTE: This is technically doubled since it's applied above and below the center of the waveform.
|
||||
*/
|
||||
public var minWaveformSize:Int = 1;
|
||||
public var minWaveformSize:Int;
|
||||
|
||||
/**
|
||||
* A multiplier on the size of the waveform.
|
||||
* Still capped at the width and height set for the sprite.
|
||||
*/
|
||||
public var amplitude:Float = 1.0;
|
||||
public var amplitude:Float;
|
||||
|
||||
public function new(?waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
|
|
@ -135,6 +154,11 @@ class WaveformSprite extends MeshRender
|
|||
this.width = DEFAULT_WIDTH;
|
||||
this.height = DEFAULT_HEIGHT;
|
||||
|
||||
this.minWaveformSize = 1;
|
||||
this.amplitude = 1.0;
|
||||
this.isWaveformDirty = true;
|
||||
this.forceUpdate = false;
|
||||
|
||||
this.waveformData = waveformData;
|
||||
this.orientation = orientation ?? DEFAULT_ORIENTATION;
|
||||
this.time = 0.0;
|
||||
|
|
@ -151,7 +175,7 @@ class WaveformSprite extends MeshRender
|
|||
isWaveformDirty = true;
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float)
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
|
|
@ -170,6 +194,11 @@ class WaveformSprite extends MeshRender
|
|||
makeGraphic(1, 1, this.waveformColor);
|
||||
}
|
||||
|
||||
public override function draw():Void
|
||||
{
|
||||
super.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param offsetX Horizontal offset to draw the waveform at, in samples.
|
||||
*/
|
||||
|
|
@ -183,12 +212,12 @@ class WaveformSprite extends MeshRender
|
|||
|
||||
this.clear();
|
||||
|
||||
if (waveformData == null) return;
|
||||
if (waveformData == null || !this.visible) return;
|
||||
|
||||
// Center point of the waveform. When horizontal this is half the height, when vertical this is half the width.
|
||||
var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2);
|
||||
|
||||
var oneSecondInIndices:Int = waveformData.secondsToIndex(1);
|
||||
// var oneSecondInIndices:Int = waveformData.secondsToIndex(1)
|
||||
|
||||
var startTime:Float = time;
|
||||
var endTime:Float = time + duration;
|
||||
|
|
@ -223,7 +252,10 @@ class WaveformSprite extends MeshRender
|
|||
|
||||
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
|
||||
|
||||
if (isBeforeClipRect) continue;
|
||||
if (isBeforeClipRect)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var isAfterClipRect:Bool = (clipRect != null)
|
||||
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
|
||||
|
|
@ -426,20 +458,40 @@ class WaveformSprite extends MeshRender
|
|||
}
|
||||
}
|
||||
|
||||
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
/**
|
||||
* Build a WaveformSprite from waveform data.
|
||||
* @param data The data for the waveform to use.
|
||||
* @param orientation Whether the waveform should be horizontal or vertical.
|
||||
* @param color The color of the waveform.
|
||||
* @param duration The width of the waveform, in seconds.
|
||||
*
|
||||
* @return The resulting WaveformSprite.
|
||||
*/
|
||||
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
|
||||
{
|
||||
return new WaveformSprite(data, orientation, color, duration);
|
||||
}
|
||||
|
||||
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
/**
|
||||
* Build a WaveformSprite from a FunkinSound's waveform data.
|
||||
* @param sound The audio for the waveform to use.
|
||||
* @param orientation Whether the waveform should be horizontal or vertical.
|
||||
* @param color The color of the waveform.
|
||||
* @param duration The width of the waveform, in seconds.
|
||||
*
|
||||
* @return The resulting WaveformSprite.
|
||||
*/
|
||||
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
|
||||
{
|
||||
// TODO: Build waveform data from FunkinSound.
|
||||
var data = null;
|
||||
var data = WaveformDataParser.interpretFlxSound(sound);
|
||||
|
||||
return buildFromWaveformData(data, orientation, color, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The possible orientations of a waveform.
|
||||
*/
|
||||
enum WaveformOrientation
|
||||
{
|
||||
HORIZONTAL;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ typedef EntryConstructorFunction = (String, ?Dynamic) -> Void;
|
|||
*/
|
||||
@:nullSafety
|
||||
@:generic
|
||||
@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry())
|
||||
@:autoBuild(funkin.util.macro.RegistryMacro.buildRegistry())
|
||||
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J, P>
|
||||
{
|
||||
/**
|
||||
|
|
@ -120,14 +120,14 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
var entry:Null<T> = createEntry(entryId);
|
||||
if (entry != null)
|
||||
{
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
entries.set(entry.id, entry);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Print the error.
|
||||
trace(' Failed to load entry data: ${entryId}');
|
||||
trace(' Failed to load entry data: ${entryId}');
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -157,7 +157,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
* @param id The ID of the entry.
|
||||
* @return `true` if the entry has an attached script, `false` otherwise.
|
||||
*/
|
||||
public function isScriptedEntry(id:String):Bool
|
||||
public function isScriptedEntry(id:String, ?params:Null<P>):Bool
|
||||
{
|
||||
return scriptedEntryIds.exists(id);
|
||||
}
|
||||
|
|
@ -167,7 +167,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
* @param id The ID of the entry.
|
||||
* @return The class name, or `null` if it does not exist.
|
||||
*/
|
||||
public function getScriptedEntryClassName(id:String):Null<String>
|
||||
public function getScriptedEntryClassName(id:String, ?params:Null<P>):Null<String>
|
||||
{
|
||||
return scriptedEntryIds.get(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,27 +12,27 @@ class DataError
|
|||
switch (error)
|
||||
{
|
||||
case IncorrectType(vari, expected, pos):
|
||||
trace(' Expected field "$vari" to be of type "$expected".');
|
||||
trace(' Expected field "$vari" to be of type "$expected".');
|
||||
printPos(pos);
|
||||
case IncorrectEnumValue(value, expected, pos):
|
||||
trace(' Invalid enum value (expected "$expected", got "$value")');
|
||||
trace(' Invalid enum value (expected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case InvalidEnumConstructor(value, expected, pos):
|
||||
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
|
||||
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case UninitializedVariable(vari, pos):
|
||||
trace(' Uninitialized variable "$vari"');
|
||||
trace(' Uninitialized variable "$vari"');
|
||||
printPos(pos);
|
||||
case UnknownVariable(vari, pos):
|
||||
trace(' Unknown variable "$vari"');
|
||||
trace(' Unknown variable "$vari"');
|
||||
printPos(pos);
|
||||
case ParserError(message, pos):
|
||||
trace(' Parsing error: ${message}');
|
||||
trace(' Parsing error: ${message}');
|
||||
printPos(pos);
|
||||
case CustomFunctionException(e, pos):
|
||||
if (Std.isOfType(e, String))
|
||||
{
|
||||
trace(' ${e}');
|
||||
trace(' ${e}');
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -49,11 +49,11 @@ class DataError
|
|||
switch (Type.typeof(e))
|
||||
{
|
||||
case TClass(c):
|
||||
trace(' [${Type.getClassName(c)}] ${e.toString()}');
|
||||
trace(' [${Type.getClassName(c)}] ${e.toString()}');
|
||||
case TEnum(c):
|
||||
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
|
||||
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
|
||||
default:
|
||||
trace(' [${Type.typeof(e)}] ${e.toString()}');
|
||||
trace(' [${Type.typeof(e)}] ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,11 +66,11 @@ class DataError
|
|||
{
|
||||
if (pos.lines[0].number == pos.lines[pos.lines.length - 1].number)
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ typedef UnnamedAnimationData =
|
|||
|
||||
/**
|
||||
* Optionally specify an asset path to use for this specific animation.
|
||||
* ONLY for use by MultiSparrow characters.
|
||||
* ONLY for use by MultiSparrow and MultiAnimateAtlas characters.
|
||||
* @default The assetPath of the parent sprite
|
||||
*/
|
||||
@:optional
|
||||
|
|
@ -138,4 +138,13 @@ typedef UnnamedAnimationData =
|
|||
@:default([])
|
||||
@:optional
|
||||
var frameIndices:Null<Array<Int>>;
|
||||
|
||||
/**
|
||||
* The type of animation to use.
|
||||
* Only available for texture atlases.
|
||||
* Options: "framelabel", "symbol"
|
||||
*/
|
||||
@:default("framelabel")
|
||||
@:optional
|
||||
var animType:String;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import funkin.modding.events.ScriptEventDispatcher;
|
|||
import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedBaseCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedMultiSparrowCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedMultiAnimateAtlasCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter;
|
||||
import funkin.play.character.AnimateAtlasCharacter;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.character.SparrowCharacter;
|
||||
import funkin.play.character.MultiSparrowCharacter;
|
||||
import funkin.play.character.MultiAnimateAtlasCharacter;
|
||||
import funkin.play.character.PackerCharacter;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.util.VersionUtil;
|
||||
|
|
@ -58,7 +60,7 @@ class CharacterDataParser
|
|||
var unscriptedCharIds:Array<String> = charIdList.filter(function(charId:String):Bool {
|
||||
return !characterCache.exists(charId);
|
||||
});
|
||||
trace(' Fetching data for ${unscriptedCharIds.length} characters...');
|
||||
trace(' Fetching data for ${unscriptedCharIds.length} characters...');
|
||||
for (charId in unscriptedCharIds)
|
||||
{
|
||||
try
|
||||
|
|
@ -66,7 +68,7 @@ class CharacterDataParser
|
|||
var charData:Null<CharacterData> = parseCharacterData(charId);
|
||||
if (charData != null)
|
||||
{
|
||||
trace(' Loaded character data: ${charId}');
|
||||
trace(' Loaded character data: ${charId}');
|
||||
characterCache.set(charId, charData);
|
||||
}
|
||||
}
|
||||
|
|
@ -86,18 +88,18 @@ class CharacterDataParser
|
|||
var scriptedCharClassNames1:Array<String> = ScriptedSparrowCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames1.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
|
||||
trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var character:SparrowCharacter = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
trace(' Initialized character ${character.characterName}');
|
||||
trace(' Initialized character ${character.characterName}');
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted Sparrow character: ${charCls}');
|
||||
trace(' FAILED to instantiate scripted Sparrow character: ${charCls}');
|
||||
trace(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -106,7 +108,7 @@ class CharacterDataParser
|
|||
var scriptedCharClassNames2:Array<String> = ScriptedPackerCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames2.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
|
||||
trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames2)
|
||||
{
|
||||
try
|
||||
|
|
@ -116,7 +118,7 @@ class CharacterDataParser
|
|||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted Packer character: ${charCls}');
|
||||
trace(' FAILED to instantiate scripted Packer character: ${charCls}');
|
||||
trace(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +127,7 @@ class CharacterDataParser
|
|||
var scriptedCharClassNames3:Array<String> = ScriptedMultiSparrowCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames3.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
|
||||
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames3)
|
||||
{
|
||||
try
|
||||
|
|
@ -135,7 +137,7 @@ class CharacterDataParser
|
|||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted Multi-Sparrow character: ${charCls}');
|
||||
trace(' FAILED to instantiate scripted Multi-Sparrow character: ${charCls}');
|
||||
trace(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -144,7 +146,7 @@ class CharacterDataParser
|
|||
var scriptedCharClassNames4:Array<String> = ScriptedAnimateAtlasCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames4.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
|
||||
trace(' Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames4)
|
||||
{
|
||||
try
|
||||
|
|
@ -154,7 +156,26 @@ class CharacterDataParser
|
|||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted Animate Atlas character: ${charCls}');
|
||||
trace(' FAILED to instantiate scripted Animate Atlas character: ${charCls}');
|
||||
trace(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var scriptedCharClassNames5:Array<String> = ScriptedMultiAnimateAtlasCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames5.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames5.length} (Multi-Animate Atlas) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames5)
|
||||
{
|
||||
try
|
||||
{
|
||||
var character:MultiAnimateAtlasCharacter = ScriptedMultiAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted Multi-Animate Atlas character: ${charCls}');
|
||||
trace(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -167,29 +188,30 @@ class CharacterDataParser
|
|||
return !(scriptedCharClassNames1.contains(charCls)
|
||||
|| scriptedCharClassNames2.contains(charCls)
|
||||
|| scriptedCharClassNames3.contains(charCls)
|
||||
|| scriptedCharClassNames4.contains(charCls));
|
||||
|| scriptedCharClassNames4.contains(charCls)
|
||||
|| scriptedCharClassNames5.contains(charCls));
|
||||
});
|
||||
|
||||
if (scriptedCharClassNames.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
|
||||
trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames)
|
||||
{
|
||||
var character:BaseCharacter = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID, Custom);
|
||||
if (character == null)
|
||||
{
|
||||
trace(' Failed to instantiate scripted character: ${charCls}');
|
||||
trace(' Failed to instantiate scripted character: ${charCls}');
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Successfully instantiated scripted character: ${charCls}');
|
||||
trace(' Successfully instantiated scripted character: ${charCls}');
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
|
||||
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -226,6 +248,8 @@ class CharacterDataParser
|
|||
char = ScriptedSparrowCharacter.init(charScriptClass, charId);
|
||||
case CharacterRenderType.Packer:
|
||||
char = ScriptedPackerCharacter.init(charScriptClass, charId);
|
||||
case CharacterRenderType.MultiAnimateAtlas:
|
||||
char = ScriptedMultiAnimateAtlasCharacter.init(charScriptClass, charId);
|
||||
default:
|
||||
// We're going to assume that the script class does the rendering.
|
||||
char = ScriptedBaseCharacter.init(charScriptClass, charId, CharacterRenderType.Custom);
|
||||
|
|
@ -243,8 +267,10 @@ class CharacterDataParser
|
|||
char = new SparrowCharacter(charId);
|
||||
case CharacterRenderType.Packer:
|
||||
char = new PackerCharacter(charId);
|
||||
case CharacterRenderType.MultiAnimateAtlas:
|
||||
char = new MultiAnimateAtlasCharacter(charId);
|
||||
default:
|
||||
trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Creating character with undefined renderType ${charData.renderType}');
|
||||
char = new BaseCharacter(charId, CharacterRenderType.Custom);
|
||||
}
|
||||
}
|
||||
|
|
@ -312,7 +338,7 @@ class CharacterDataParser
|
|||
|
||||
if (!Assets.exists(Paths.image(charPath)))
|
||||
{
|
||||
trace('[WARN] Character ${char} has no freeplay icon.');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Character ${char} has no freeplay icon.');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -329,7 +355,7 @@ class CharacterDataParser
|
|||
|
||||
if (idleFrame == null)
|
||||
{
|
||||
trace('[WARN] Character ${char} has no idle in their freeplay icon.');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Character ${char} has no idle in their freeplay icon.');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -404,8 +430,8 @@ class CharacterDataParser
|
|||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' Error parsing data for character: ${charId}');
|
||||
trace(' ${e}');
|
||||
trace(' Error parsing data for character: ${charId}');
|
||||
trace(' ${e}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -431,6 +457,15 @@ class CharacterDataParser
|
|||
public static final DEFAULT_SCALE:Float = 1;
|
||||
public static final DEFAULT_SCROLL:Array<Float> = [0, 0];
|
||||
public static final DEFAULT_STARTINGANIM:String = 'idle';
|
||||
public static final DEFAULT_APPLYSTAGEMATRIX:Bool = false;
|
||||
public static final DEFAULT_ANIMTYPE:String = "framelabel";
|
||||
public static final DEFAULT_ATLASSETTINGS:funkin.data.stage.StageData.TextureAtlasData =
|
||||
{
|
||||
swfMode: true,
|
||||
cacheOnLoad: false,
|
||||
filterQuality: 1,
|
||||
applyStageMatrix: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Set unspecified parameters to their defaults.
|
||||
|
|
@ -559,6 +594,16 @@ class CharacterDataParser
|
|||
input.flipX = DEFAULT_FLIPX;
|
||||
}
|
||||
|
||||
if (input.applyStageMatrix == null)
|
||||
{
|
||||
input.applyStageMatrix = DEFAULT_APPLYSTAGEMATRIX;
|
||||
}
|
||||
|
||||
if (input.atlasSettings == null)
|
||||
{
|
||||
input.atlasSettings = DEFAULT_ATLASSETTINGS;
|
||||
}
|
||||
|
||||
if (input.animations.length == 0 && input.startingAnimation != null)
|
||||
{
|
||||
return null;
|
||||
|
|
@ -596,6 +641,11 @@ class CharacterDataParser
|
|||
{
|
||||
inputAnimation.flipY = DEFAULT_FLIPY;
|
||||
}
|
||||
|
||||
if (inputAnimation.animType == null)
|
||||
{
|
||||
inputAnimation.animType = DEFAULT_ANIMTYPE;
|
||||
}
|
||||
}
|
||||
|
||||
// All good!
|
||||
|
|
@ -624,10 +674,15 @@ enum abstract CharacterRenderType(String) from String to String
|
|||
public var MultiSparrow = 'multisparrow';
|
||||
|
||||
/**
|
||||
* Renders the character using a spritesheet of symbols and JSON data.
|
||||
* Renders the character using a single spritesheet of symbols and JSON data.
|
||||
*/
|
||||
public var AnimateAtlas = 'animateatlas';
|
||||
|
||||
/**
|
||||
* Renders the character using multiple spritesheets of symbols and JSON data.
|
||||
*/
|
||||
public var MultiAnimateAtlas = 'multianimateatlas';
|
||||
|
||||
/**
|
||||
* Renders the character using a custom method.
|
||||
*/
|
||||
|
|
@ -674,6 +729,9 @@ typedef CharacterData =
|
|||
*/
|
||||
var healthIcon:Null<HealthIconData>;
|
||||
|
||||
/**
|
||||
* Optional data about the death animation for the character.
|
||||
*/
|
||||
var death:Null<DeathData>;
|
||||
|
||||
/**
|
||||
|
|
@ -734,6 +792,23 @@ typedef CharacterData =
|
|||
* @default false
|
||||
*/
|
||||
var flipX:Null<Bool>;
|
||||
|
||||
/**
|
||||
* NOTE: This only applies to animate atlas characters.
|
||||
*
|
||||
* Whether to apply the stage matrix, if it was exported from a symbol instance.
|
||||
* Also positions the Texture Atlas as it displays in Animate.
|
||||
* Turning this on is only recommended if you prepositioned the character in Animate.
|
||||
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
|
||||
*/
|
||||
var applyStageMatrix:Null<Bool>;
|
||||
|
||||
/**
|
||||
* Various settings for the prop.
|
||||
* Only available for texture atlases.
|
||||
*/
|
||||
@:optional
|
||||
var atlasSettings:funkin.data.stage.StageData.TextureAtlasData;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -46,12 +46,12 @@ class SongEventRegistry
|
|||
|
||||
if (event != null)
|
||||
{
|
||||
trace(' Loaded built-in song event: ${event.id}');
|
||||
trace(' Loaded built-in song event: ${event.id}');
|
||||
eventCache.set(event.id, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
|
||||
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,12 +68,12 @@ class SongEventRegistry
|
|||
|
||||
if (event != null)
|
||||
{
|
||||
trace(' Loaded scripted song event: ${event.id}');
|
||||
trace(' Loaded scripted song event: ${event.id}');
|
||||
eventCache.set(event.id, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted song event class: ${eventCls}');
|
||||
trace(' Failed to instantiate scripted song event class: ${eventCls}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class PlayerData
|
|||
updateVersionToLatest();
|
||||
|
||||
var writer = new json2object.JsonWriter<PlayerData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
|
|
@ -103,6 +103,10 @@ class PlayerFreeplayDJData
|
|||
var assetPath:String;
|
||||
var animations:Array<AnimationData>;
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
var applyStageMatrix:Bool;
|
||||
|
||||
@:optional
|
||||
@:default("BOYFRIEND")
|
||||
var text1:String;
|
||||
|
|
@ -158,11 +162,16 @@ class PlayerFreeplayDJData
|
|||
}
|
||||
|
||||
public inline function getAssetPath():String
|
||||
return assetPath; // return Paths.animateAtlas(assetPath);
|
||||
return assetPath; // return assetPath;
|
||||
|
||||
public inline function getAnimationsList():Array<AnimationData>
|
||||
return animations;
|
||||
|
||||
public function useApplyStageMatrix():Bool
|
||||
{
|
||||
return applyStageMatrix;
|
||||
}
|
||||
|
||||
public function getFreeplayDJText(index:Int):String
|
||||
{
|
||||
switch (index)
|
||||
|
|
@ -343,6 +352,10 @@ typedef PlayerResultsAnimationData =
|
|||
*/
|
||||
var renderType:String;
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
var applyStageMatrix:Bool;
|
||||
|
||||
@:optional
|
||||
var assetPath:Null<String>;
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
|
|||
public function hasNewCharacter():Bool
|
||||
{
|
||||
#if (!UNLOCK_EVERYTHING)
|
||||
var charactersSeen = Save.instance.charactersSeen.clone();
|
||||
var charactersSeen = Save.instance.charactersSeen.value.clone();
|
||||
|
||||
for (charId in listEntryIds())
|
||||
{
|
||||
|
|
@ -94,7 +94,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
|
|||
var result = [];
|
||||
|
||||
#if (!UNLOCK_EVERYTHING)
|
||||
var charactersSeen = Save.instance.charactersSeen.clone();
|
||||
var charactersSeen = Save.instance.charactersSeen.value.clone();
|
||||
for (charId in listEntryIds())
|
||||
{
|
||||
var player = fetchEntry(charId);
|
||||
|
|
@ -143,7 +143,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
|
|||
#if UNLOCK_EVERYTHING
|
||||
return true;
|
||||
#else
|
||||
return Save.instance.charactersSeen.contains(characterId);
|
||||
return Save.instance.charactersSeen.value.contains(characterId);
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ class SongMetadata implements ICloneable<SongMetadata>
|
|||
// I believe @:jignored should be ignored by the writer?
|
||||
// var output = this.clone();
|
||||
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
|
|
@ -664,7 +664,7 @@ class SongChartData implements ICloneable<SongChartData>
|
|||
|
||||
var ignoreNullOptionals = true;
|
||||
var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ class SongDataUtils
|
|||
{
|
||||
var ignoreNullOptionals = true;
|
||||
var writer = new json2object.JsonWriter<SongClipboardItems>(ignoreNullOptionals);
|
||||
var dataString:String = writer.write(data, ' ');
|
||||
var dataString:String = writer.write(data, ' ');
|
||||
|
||||
ClipboardUtil.setClipboard(dataString);
|
||||
|
||||
|
|
|
|||
|
|
@ -99,14 +99,14 @@ using funkin.data.song.migrator.SongDataMigrator;
|
|||
var entry:Null<Song> = createEntry(entryId);
|
||||
if (entry != null)
|
||||
{
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
entries.set(entry.id, entry);
|
||||
}
|
||||
}
|
||||
catch (e:Dynamic)
|
||||
{
|
||||
// Print the error.
|
||||
trace(' Failed to load entry data: ${entryId}');
|
||||
trace(' Failed to load entry data: ${entryId}');
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -129,6 +129,29 @@ using funkin.data.song.migrator.SongDataMigrator;
|
|||
return parseEntryMetadataRaw(contents);
|
||||
}
|
||||
|
||||
public override function isScriptedEntry(id:String, ?params:Null<SongEntryParams>)
|
||||
{
|
||||
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
|
||||
if (variation != Constants.DEFAULT_VARIATION)
|
||||
{
|
||||
return scriptedSongVariations.exists('${id}:${variation}');
|
||||
}
|
||||
return super.isScriptedEntry(id, params);
|
||||
}
|
||||
|
||||
public override function getScriptedEntryClassName(id:String, ?params:Null<SongEntryParams>):Null<String>
|
||||
{
|
||||
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
|
||||
if (variation != Constants.DEFAULT_VARIATION)
|
||||
{
|
||||
final variationSongId:ScriptedSong = cast scriptedSongVariations.get('${id}:${variation}');
|
||||
@:privateAccess
|
||||
var path:String = variationSongId._asc._c.name;
|
||||
return path;
|
||||
}
|
||||
return super.getScriptedEntryClassName(id, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* We override `fetchEntry` to handle song variations!
|
||||
*/
|
||||
|
|
@ -446,7 +469,7 @@ using funkin.data.song.migrator.SongDataMigrator;
|
|||
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
|
||||
if (!openfl.Assets.exists(entryFilePath))
|
||||
{
|
||||
trace(' [WARN] Could not locate file $entryFilePath');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Could not locate file $entryFilePath');
|
||||
return null;
|
||||
}
|
||||
var rawJson:Null<String> = openfl.Assets.getText(entryFilePath);
|
||||
|
|
@ -524,7 +547,7 @@ using funkin.data.song.migrator.SongDataMigrator;
|
|||
|
||||
if (character == null)
|
||||
{
|
||||
trace(' [WARN] Could not locate character $characterId');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Could not locate character $characterId');
|
||||
return allDifficulties;
|
||||
}
|
||||
|
||||
|
|
@ -542,7 +565,7 @@ using funkin.data.song.migrator.SongDataMigrator;
|
|||
|
||||
if (allDifficulties.length == 0)
|
||||
{
|
||||
trace(' [WARN] No difficulties found. Returning default difficulty list.');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' No difficulties found. Returning default difficulty list.');
|
||||
allDifficulties = Constants.DEFAULT_DIFFICULTY_LIST.copy();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class ChartManifestData
|
|||
* The metadata and chart data file names are derived from this.
|
||||
*/
|
||||
public var songId(default, set):String;
|
||||
|
||||
public function set_songId(value:String):String
|
||||
{
|
||||
return songId = invalidIdRegex.replace(value.trim(), '');
|
||||
|
|
@ -71,7 +72,7 @@ class ChartManifestData
|
|||
updateVersionToLatest();
|
||||
|
||||
var writer = new json2object.JsonWriter<ChartManifestData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
|
|
|
|||
82
source/funkin/data/song/importer/OsuManiaData.hx
Normal file
82
source/funkin/data/song/importer/OsuManiaData.hx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package funkin.data.song.importer;
|
||||
|
||||
/**
|
||||
* Structure of a parsed Osu!Mania .osu file
|
||||
* Stuctured like a INI file format by CSV for HitObjects and more
|
||||
*/
|
||||
typedef OsuManiaData =
|
||||
{
|
||||
var General:
|
||||
{
|
||||
var PreviewTime:Int;
|
||||
};
|
||||
var Editor:
|
||||
{
|
||||
var DistanceSpacing:Float;
|
||||
var BeatDivisor:Int;
|
||||
var GridSize:Int;
|
||||
};
|
||||
var Metadata:
|
||||
{
|
||||
var Title:String;
|
||||
var TitleUnicode:String;
|
||||
var Artist:String;
|
||||
var ArtistUnicode:String;
|
||||
var Creator:String;
|
||||
var Version:String;
|
||||
};
|
||||
var Difficulty:
|
||||
{
|
||||
var OverallDifficulty:Int;
|
||||
var SliderMultiplier:Float;
|
||||
var SliderTickRate:Float;
|
||||
var CircleSize:Int;
|
||||
};
|
||||
var HitObjects:Array<ManiaHitObject>;
|
||||
var TimingPoints:Array<TimingPoint>;
|
||||
var Events:Array<Any>;
|
||||
}
|
||||
|
||||
class TimingPoint
|
||||
{
|
||||
public var time:Float;
|
||||
public var beatLength:Float;
|
||||
public var meter:Int;
|
||||
public var sampleSet:Int;
|
||||
public var sampleIndex:Int;
|
||||
public var volume:Int;
|
||||
public var uninherited:Int;
|
||||
public var effects:Int;
|
||||
public var bpm:Null<Float>;
|
||||
public var sv:Null<Float>;
|
||||
|
||||
public function new(time:Float, beatLength:Float, meter:Int, sampleSet:Int, sampleIndex:Int, volume:Int, uninherited:Int, effects:Int)
|
||||
{
|
||||
this.time = time;
|
||||
this.beatLength = beatLength;
|
||||
this.meter = meter;
|
||||
this.sampleSet = sampleSet;
|
||||
this.sampleIndex = sampleIndex;
|
||||
this.volume = volume;
|
||||
this.uninherited = uninherited;
|
||||
this.effects = effects;
|
||||
|
||||
// Derived values
|
||||
this.bpm = (uninherited == 1) ? (Math.round((60000 / beatLength) * 10) / 10) : null;
|
||||
this.sv = (uninherited == 0) ? (beatLength / 100) : null; // Just incase someone wants to add Scroll Velocity Support
|
||||
}
|
||||
}
|
||||
|
||||
class ManiaHitObject
|
||||
{
|
||||
public var time:Int;
|
||||
public var column:Int;
|
||||
public var holdDuration:Int;
|
||||
|
||||
public function new(time:Int, column:Int, holdDuration:Int)
|
||||
{
|
||||
this.time = time;
|
||||
this.column = column;
|
||||
this.holdDuration = holdDuration;
|
||||
}
|
||||
}
|
||||
199
source/funkin/data/song/importer/OsuManiaImporter.hx
Normal file
199
source/funkin/data/song/importer/OsuManiaImporter.hx
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
package funkin.data.song.importer;
|
||||
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.importer.OsuManiaData;
|
||||
import funkin.data.song.importer.OsuManiaData.TimingPoint;
|
||||
import funkin.data.song.importer.OsuManiaData.ManiaHitObject;
|
||||
|
||||
class OsuManiaImporter
|
||||
{
|
||||
public static function parseOsuFile(osuContent:String):OsuManiaData
|
||||
{
|
||||
var lines:Array<String> = osuContent.split("\n");
|
||||
var result:Dynamic = {};
|
||||
var currentSection:String = null;
|
||||
|
||||
var nonCSVLikeSections = ["General", "Editor", "Metadata", "Difficulty"];
|
||||
|
||||
for (line in lines)
|
||||
{
|
||||
line = StringTools.trim(line);
|
||||
if (line == "" || StringTools.startsWith(line, "//")) continue;
|
||||
|
||||
// Section header like [General]
|
||||
var sectionRegex = ~/^\[(.+)\]$/;
|
||||
if (sectionRegex.match(line))
|
||||
{
|
||||
currentSection = sectionRegex.matched(1);
|
||||
if (nonCSVLikeSections.contains(currentSection))
|
||||
{
|
||||
Reflect.setField(result, currentSection, {});
|
||||
}
|
||||
else
|
||||
{
|
||||
Reflect.setField(result, currentSection, []);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key-value pairs (INI style)
|
||||
if (currentSection != null && nonCSVLikeSections.contains(currentSection))
|
||||
{
|
||||
var parts:Array<String> = line.split(":");
|
||||
var key:String = StringTools.trim(parts.shift());
|
||||
var value:String = StringTools.trim(parts.join(":"));
|
||||
if (Reflect.field(result, currentSection) == null) Reflect.setField(result, currentSection, {});
|
||||
Reflect.setField(Reflect.field(result, currentSection), key, value);
|
||||
}
|
||||
// For CSV-like sections
|
||||
else if (currentSection != null)
|
||||
{
|
||||
var theArray:Array<String> = cast Reflect.field(result, currentSection);
|
||||
theArray.push(line);
|
||||
Reflect.setField(result, currentSection, theArray);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
|
||||
* @param difficulty The difficulty name to assign to the migrated chart.
|
||||
* @return SongMetadata
|
||||
*/
|
||||
public static function migrateMetadata(songData:OsuManiaData, difficulty:String = 'normal'):SongMetadata
|
||||
{
|
||||
trace('Migrating song metadata from Osu!Mania.');
|
||||
|
||||
var songMetadata:SongMetadata = new SongMetadata('Import', songData.Metadata.ArtistUnicode ?? songData.Metadata.Artist ?? Constants.DEFAULT_ARTIST,
|
||||
songData.Metadata.Creator ?? Constants.DEFAULT_CHARTER, Constants.DEFAULT_VARIATION);
|
||||
|
||||
// Set generatedBy string for debugging.
|
||||
songMetadata.generatedBy = 'Chart Editor Import (Osu!Mania)';
|
||||
|
||||
songMetadata.playData.stage = 'mainStage';
|
||||
songMetadata.songName = songData.Metadata.TitleUnicode ?? songData.Metadata.Title ?? 'Import';
|
||||
songMetadata.playData.difficulties = [difficulty];
|
||||
|
||||
songMetadata.playData.songVariations = [];
|
||||
|
||||
songMetadata.timeChanges = rebuildTimeChanges(songData);
|
||||
|
||||
songMetadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
|
||||
songMetadata.playData.ratings.set(difficulty, songData.Difficulty.OverallDifficulty ?? 0);
|
||||
|
||||
return songMetadata;
|
||||
}
|
||||
|
||||
static function rebuildTimeChanges(songData:OsuManiaData):Array<SongTimeChange>
|
||||
{
|
||||
var timings:Array<TimingPoint> = parseTimingPoints(cast songData.TimingPoints);
|
||||
var bpmPoints:Array<TimingPoint> = timings.filter((tp) -> tp.uninherited == 1);
|
||||
|
||||
var result:Array<SongTimeChange> = [];
|
||||
if (bpmPoints.length >= 1)
|
||||
{
|
||||
result.push(new SongTimeChange(0, bpmPoints[0].bpm ?? Constants.DEFAULT_BPM));
|
||||
|
||||
for (i in 1...bpmPoints.length)
|
||||
{
|
||||
var bpmPoint:TimingPoint = bpmPoints[i];
|
||||
|
||||
result.push(new SongTimeChange(bpmPoint.time, bpmPoint.bpm ?? Constants.DEFAULT_BPM));
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length == 0)
|
||||
{
|
||||
result.push(new SongTimeChange(0, Constants.DEFAULT_BPM));
|
||||
trace("[WARN] No BPM points found, resulting to default BPM...");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
|
||||
* @param difficulty The difficulty name to assign to the migrated chart.
|
||||
* @return SongChartData
|
||||
*/
|
||||
public static function migrateChartData(songData:OsuManiaData, difficulty:String = 'normal'):SongChartData
|
||||
{
|
||||
trace('Migrating song chart data from Osu!Mania.');
|
||||
|
||||
// Osu!Mania doesn't have a scroll speed variable as its controlled by the player
|
||||
var songChartData:SongChartData = new SongChartData([difficulty => Constants.DEFAULT_SCROLLSPEED], [], [difficulty => []]);
|
||||
|
||||
// songData.HitObjects is a Array<String> here so im casting it so haxe stops yelling at me
|
||||
var osuNotes:Array<ManiaHitObject> = parseManiaHitObjects(cast songData.HitObjects, songData.Difficulty.CircleSize);
|
||||
songChartData.notes.set(difficulty, convertNotes(osuNotes, songData.Difficulty.CircleSize));
|
||||
|
||||
songChartData.events = [];
|
||||
|
||||
return songChartData;
|
||||
}
|
||||
|
||||
static final STRUMLINE_SIZE = 4;
|
||||
|
||||
static function convertNotes(hitObjects:Array<ManiaHitObject>, keyCount:Int):Array<SongNoteData>
|
||||
{
|
||||
var result:Array<SongNoteData> = [];
|
||||
|
||||
for (hitObject in hitObjects)
|
||||
{
|
||||
var wrappedColumn:Int = hitObject.column % (keyCount * 2); // wrap overflow for 9K+
|
||||
if (keyCount <= 4) // if its 4K or less, flip to BF side
|
||||
{
|
||||
wrappedColumn -= 4;
|
||||
var noteOffset:Int = Std.int(Math.abs(keyCount - STRUMLINE_SIZE));
|
||||
var flippedNoteData:Int = wrappedColumn + keyCount + noteOffset;
|
||||
result.push(new SongNoteData(hitObject.time, flippedNoteData, hitObject.holdDuration ?? 0, ''));
|
||||
}
|
||||
else
|
||||
result.push(new SongNoteData(hitObject.time, wrappedColumn, hitObject.holdDuration ?? 0, ''));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static function parseTimingPoints(timingLines:Array<String>):Array<TimingPoint>
|
||||
{
|
||||
return timingLines.map(function(line:String):TimingPoint {
|
||||
var parts = line.split(",");
|
||||
var time = Std.parseFloat(parts[0]);
|
||||
var beatLength = Std.parseFloat(parts[1]);
|
||||
var meter = Std.parseInt(parts[2]);
|
||||
var sampleSet = Std.parseInt(parts[3]);
|
||||
var sampleIndex = Std.parseInt(parts[4]);
|
||||
var volume = Std.parseInt(parts[5]);
|
||||
var uninherited = Std.parseInt(parts[6]);
|
||||
var effects = Std.parseInt(parts[7]);
|
||||
|
||||
return new TimingPoint(time, beatLength, meter, sampleSet, sampleIndex, volume, uninherited, effects);
|
||||
});
|
||||
}
|
||||
|
||||
static function parseManiaHitObjects(hitObjectsLines:Array<String>, ?columns:Int = 4):Array<ManiaHitObject>
|
||||
{
|
||||
return hitObjectsLines.map(function(line:String):ManiaHitObject {
|
||||
var parts = line.split(",");
|
||||
|
||||
var x:Int = Std.parseInt(parts[0]);
|
||||
var time:Int = Std.parseInt(parts[2]);
|
||||
var type:Int = Std.parseInt(parts[3]);
|
||||
var hasHold:Bool = (type & 128) == 128;
|
||||
|
||||
var noteD:Int = Std.int(x / (512 / columns));
|
||||
var holdEndTime:Null<Int> = hasHold ? Std.parseInt(parts[5].split(":")[0]) : null;
|
||||
|
||||
var holdDuration:Int = (holdEndTime != null) ? (holdEndTime - time) : 0;
|
||||
|
||||
return new ManiaHitObject(time, noteD, holdDuration);
|
||||
});
|
||||
}
|
||||
}
|
||||
123
source/funkin/data/song/importer/StepManiaData.hx
Normal file
123
source/funkin/data/song/importer/StepManiaData.hx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package funkin.data.song.importer;
|
||||
|
||||
typedef StepManiaData =
|
||||
{
|
||||
var Metadata:
|
||||
{
|
||||
var Title:String;
|
||||
var Artist:String;
|
||||
var Genre:String;
|
||||
var Credit:String;
|
||||
var Banner:String;
|
||||
var Background:String;
|
||||
|
||||
var Offset:Float;
|
||||
var SampleStart:Float;
|
||||
};
|
||||
var TimingPoints:Array<StepTimingPoint>;
|
||||
var Stops:Array<StepStop>;
|
||||
var Difficulties:Array<StepDifficulty>;
|
||||
}
|
||||
|
||||
enum StepManiaChartType {
|
||||
DanceSingle;
|
||||
DanceDouble;
|
||||
Unknown;
|
||||
}
|
||||
|
||||
enum StepManiaNoteType {
|
||||
Tap;
|
||||
Head;
|
||||
Tail;
|
||||
Roll;
|
||||
Mine;
|
||||
Fake;
|
||||
}
|
||||
|
||||
class StepNote
|
||||
{
|
||||
public var beat:Float;
|
||||
public var column:Int;
|
||||
public var type:StepManiaNoteType;
|
||||
|
||||
public function new(t:String, beat:Float, column:Int)
|
||||
{
|
||||
this.beat = beat;
|
||||
this.column = column;
|
||||
switch (t) {
|
||||
case "2":
|
||||
this.type = StepManiaNoteType.Head;
|
||||
case "3":
|
||||
this.type = StepManiaNoteType.Tail;
|
||||
case "4":
|
||||
this.type = StepManiaNoteType.Roll;
|
||||
case "M":
|
||||
this.type = StepManiaNoteType.Mine;
|
||||
case "F":
|
||||
this.type = StepManiaNoteType.Fake;
|
||||
default:
|
||||
this.type = StepManiaNoteType.Tap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StepDifficulty
|
||||
{
|
||||
public var name:String;
|
||||
public var charter:String;
|
||||
public var difficultyRating:Int;
|
||||
public var type:StepManiaChartType;
|
||||
|
||||
public var notes:Array<StepNote>;
|
||||
|
||||
public function parseChartType(chartTypeStr:String):StepManiaChartType
|
||||
{
|
||||
switch (chartTypeStr) {
|
||||
case "dance-single":
|
||||
return StepManiaChartType.DanceSingle;
|
||||
case "dance-double":
|
||||
return StepManiaChartType.DanceDouble;
|
||||
default:
|
||||
return StepManiaChartType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public function new(name:String, charter:String, difficultyRating:Int, type:String)
|
||||
{
|
||||
this.name = name;
|
||||
this.charter = charter;
|
||||
this.difficultyRating = difficultyRating;
|
||||
this.type = parseChartType(type);
|
||||
}
|
||||
}
|
||||
|
||||
class StepTimingPoint
|
||||
{
|
||||
public var bpm:Float;
|
||||
public var startBeat:Float = Math.NEGATIVE_INFINITY;
|
||||
public var endBeat:Float = Math.POSITIVE_INFINITY;
|
||||
public var startTimestamp:Float = 0;
|
||||
public var endTimestamp:Float = 0;
|
||||
|
||||
public function new(bpm:Float, startBeat:Float)
|
||||
{
|
||||
this.bpm = bpm;
|
||||
this.startBeat = startBeat;
|
||||
}
|
||||
}
|
||||
|
||||
// Not implemented, but if any chart uses them then the chart will break.
|
||||
// IE this messes with the timing of the notes.
|
||||
class StepStop
|
||||
{
|
||||
public var startBeat:Float = Math.NEGATIVE_INFINITY;
|
||||
public var duration:Float = 0;
|
||||
|
||||
public var startTimestamp:Float = Math.NEGATIVE_INFINITY;
|
||||
|
||||
public function new(startBeat:Float, duration:Float)
|
||||
{
|
||||
this.startBeat = startBeat;
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
613
source/funkin/data/song/importer/StepManiaImporter.hx
Normal file
613
source/funkin/data/song/importer/StepManiaImporter.hx
Normal file
|
|
@ -0,0 +1,613 @@
|
|||
package funkin.data.song.importer;
|
||||
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
|
||||
import funkin.data.song.importer.StepManiaData.StepTimingPoint;
|
||||
import funkin.data.song.importer.StepManiaData.StepDifficulty;
|
||||
import funkin.data.song.importer.StepManiaData.StepManiaChartType;
|
||||
import funkin.data.song.importer.StepManiaData.StepNote;
|
||||
import funkin.data.song.importer.StepManiaData.StepStop;
|
||||
import funkin.data.song.importer.StepManiaData.StepManiaNoteType;
|
||||
|
||||
enum StepStateEnum
|
||||
{
|
||||
Metadata;
|
||||
TimingPoints;
|
||||
Stops;
|
||||
Notes;
|
||||
}
|
||||
|
||||
class StepManiaImporter
|
||||
{
|
||||
private static var readDiffMetadata:Map<String, String> = new Map<String, String>();
|
||||
|
||||
static function parseMetadataLine(line:String, result:StepManiaData):StepManiaData
|
||||
{
|
||||
var parts:Array<String> = line.split(":");
|
||||
if (parts.length != 2) return result;
|
||||
|
||||
var key:String = StringTools.trim(parts[0]);
|
||||
var value:String = StringTools.trim(parts[1]);
|
||||
|
||||
// remove trailing ; on value
|
||||
if (StringTools.endsWith(value, ";")) value = value.substr(0, value.length - 1);
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case "TITLE":
|
||||
result.Metadata.Title = value;
|
||||
case "ARTIST":
|
||||
result.Metadata.Artist = value;
|
||||
case "GENRE":
|
||||
result.Metadata.Genre = value;
|
||||
case "CREDIT":
|
||||
if (result.Metadata.Credit == "") result.Metadata.Credit = value;
|
||||
else // .ssc
|
||||
readDiffMetadata.set("CREDIT", value);
|
||||
case "BANNER":
|
||||
result.Metadata.Banner = value;
|
||||
case "BACKGROUND":
|
||||
result.Metadata.Background = value;
|
||||
case "OFFSET":
|
||||
result.Metadata.Offset = Std.parseFloat(value);
|
||||
case "SAMPLESTART":
|
||||
result.Metadata.SampleStart = Std.parseFloat(value);
|
||||
// .ssc
|
||||
case "STEPSTYPE":
|
||||
readDiffMetadata.set("STEPSTYPE", value);
|
||||
case "DESCRIPTION":
|
||||
readDiffMetadata.set("DESCRIPTION", value);
|
||||
case "DIFFICULTY":
|
||||
readDiffMetadata.set("DIFFICULTY", value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static function parseTimingPointLine(line:String):Array<StepTimingPoint>
|
||||
{
|
||||
// Remove #BPMS: prefix if present
|
||||
if (StringTools.startsWith(line, "#BPMS:")) line = line.substr(6);
|
||||
|
||||
var parts:Array<String> = line.split(",");
|
||||
|
||||
var stepTimingPoints:Array<StepTimingPoint> = [];
|
||||
for (i in 0...parts.length)
|
||||
{
|
||||
// Split the =
|
||||
var split = parts[i].split("=");
|
||||
if (split.length != 2) continue;
|
||||
|
||||
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
|
||||
|
||||
// Check if split[i + 1] has ; and remove it
|
||||
var bpmSplit = split[1];
|
||||
if (StringTools.endsWith(bpmSplit, ";")) bpmSplit = bpmSplit.substr(0, bpmSplit.length - 1);
|
||||
|
||||
var tp:StepTimingPoint = new StepTimingPoint(Std.parseFloat(bpmSplit), beat);
|
||||
stepTimingPoints.push(tp);
|
||||
}
|
||||
|
||||
return stepTimingPoints;
|
||||
}
|
||||
|
||||
static function parseStopsLine(line:String):Array<StepStop>
|
||||
{
|
||||
// Remove #STOPS: prefix if present
|
||||
if (StringTools.startsWith(line, "#STOPS:")) line = line.substr(7);
|
||||
|
||||
// Same as timing points
|
||||
var parts:Array<String> = line.split(",");
|
||||
var stepStopPoints:Array<StepStop> = [];
|
||||
for (i in 0...parts.length)
|
||||
{
|
||||
// Split the =
|
||||
var split = parts[i].split("=");
|
||||
if (split.length != 2) continue;
|
||||
|
||||
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
|
||||
|
||||
// Check if split[i + 1] has ; and remove it
|
||||
var durSplit = split[1];
|
||||
if (StringTools.endsWith(durSplit, ";")) durSplit = durSplit.substr(0, durSplit.length - 1);
|
||||
|
||||
var tp:StepStop = new StepStop(beat, Std.parseFloat(durSplit));
|
||||
stepStopPoints.push(tp);
|
||||
}
|
||||
|
||||
return stepStopPoints;
|
||||
}
|
||||
|
||||
static function parseMeasure(lines:Array<String>, measureIndex:Int):Array<StepNote>
|
||||
{
|
||||
var lengthInRows:Float = 192.0 / lines.length;
|
||||
var rowIndex:Int = 0;
|
||||
var beat:Float = 0;
|
||||
|
||||
var stepNotes:Array<StepNote> = [];
|
||||
|
||||
for (i in 0...lines.length)
|
||||
{
|
||||
var stepNoteRow:Float = measureIndex * 192 + (lengthInRows * rowIndex);
|
||||
beat = stepNoteRow / 48.0; // 48 rows per beat
|
||||
|
||||
for (j in 0...lines[i].length)
|
||||
{
|
||||
var char:String = lines[i].charAt(j);
|
||||
if (char != "0")
|
||||
{
|
||||
var stepNote:StepNote = new StepNote(char, beat, j);
|
||||
stepNotes.push(stepNote);
|
||||
}
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
return stepNotes;
|
||||
}
|
||||
|
||||
static function synchronizeStepTimingPoints(stepStopPoints:Array<StepStop>, stepTimingPoints:Array<StepTimingPoint>):Array<StepTimingPoint>
|
||||
{
|
||||
// Initialize startTimestamp/endTimestamp and endBeat for timing points
|
||||
for (tpIndex in 0...stepTimingPoints.length)
|
||||
{
|
||||
var tp = stepTimingPoints[tpIndex];
|
||||
// ensure fields exist
|
||||
if (tp.startTimestamp == Math.NEGATIVE_INFINITY) tp.startTimestamp = 0.0;
|
||||
}
|
||||
|
||||
var tpIndex:Int = 1;
|
||||
var spIndex:Int = 0;
|
||||
|
||||
// if we have more than one timing point or any StepStops, synchronize
|
||||
if (stepTimingPoints.length > 1 || stepStopPoints.length > 0)
|
||||
{
|
||||
if (stepStopPoints.length > 1)
|
||||
{
|
||||
while (tpIndex < stepTimingPoints.length || spIndex < stepStopPoints.length)
|
||||
{
|
||||
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
|
||||
|
||||
if (tpIndex == stepTimingPoints.length)
|
||||
{
|
||||
var cts:Float = 0;
|
||||
while (spIndex < stepStopPoints.length)
|
||||
{
|
||||
var sp = stepStopPoints[spIndex];
|
||||
sp.startTimestamp = (prevTp.startTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60)) + cts;
|
||||
cts += sp.duration;
|
||||
spIndex++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (spIndex == stepStopPoints.length)
|
||||
{
|
||||
while (tpIndex < stepTimingPoints.length)
|
||||
{
|
||||
var tp = stepTimingPoints[tpIndex];
|
||||
prevTp = stepTimingPoints[tpIndex - 1];
|
||||
prevTp.endBeat = tp.startBeat;
|
||||
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
|
||||
tp.startTimestamp = prevTp.endTimestamp;
|
||||
tp.endTimestamp = tp.startTimestamp;
|
||||
tpIndex++;
|
||||
if (tpIndex == stepTimingPoints.length)
|
||||
{
|
||||
tp.endTimestamp = Math.POSITIVE_INFINITY;
|
||||
tp.endBeat = Math.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
|
||||
var sp:StepStop = stepStopPoints[spIndex];
|
||||
|
||||
prevTp.endBeat = tp.startBeat;
|
||||
|
||||
if (sp.startBeat < prevTp.endBeat)
|
||||
{
|
||||
sp.startTimestamp = prevTp.endTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60);
|
||||
prevTp.endTimestamp += sp.duration;
|
||||
spIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
|
||||
tp.startTimestamp = prevTp.endTimestamp;
|
||||
tp.endTimestamp = tp.startTimestamp;
|
||||
tpIndex++;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (tpIndex < stepTimingPoints.length)
|
||||
{
|
||||
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
|
||||
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
|
||||
prevTp.endBeat = tp.startBeat;
|
||||
prevTp.endTimestamp = prevTp.startTimestamp + (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
|
||||
tp.startTimestamp = prevTp.endTimestamp;
|
||||
tpIndex++;
|
||||
if (tpIndex == stepTimingPoints.length)
|
||||
{
|
||||
tp.endTimestamp = Math.POSITIVE_INFINITY;
|
||||
tp.endBeat = Math.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stepTimingPoints;
|
||||
}
|
||||
|
||||
static function pushWorking(workingDiff:StepDifficulty, result:StepManiaData):StepManiaData
|
||||
{
|
||||
if (workingDiff != null && workingDiff.notes.length > 0)
|
||||
{
|
||||
// Apply .ssc metadata if any
|
||||
for (metaKey in readDiffMetadata.keys())
|
||||
{
|
||||
var metaValue = readDiffMetadata.get(metaKey);
|
||||
switch (metaKey)
|
||||
{
|
||||
case "STEPSTYPE":
|
||||
workingDiff.type = workingDiff.parseChartType(metaValue);
|
||||
case "DIFFICULTY":
|
||||
workingDiff.name = metaValue;
|
||||
case "CREDIT":
|
||||
workingDiff.charter = metaValue;
|
||||
// DESCRIPTION is ignored for now
|
||||
}
|
||||
}
|
||||
result.Difficulties.push(workingDiff);
|
||||
}
|
||||
readDiffMetadata.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
static function parseBPMS(line:String, result:StepManiaData):StepManiaData
|
||||
{
|
||||
var tps = parseTimingPointLine(line);
|
||||
if (tps != null)
|
||||
{
|
||||
for (tp in tps)
|
||||
result.TimingPoints.push(tp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static function parseStops(line:String, result:StepManiaData):StepManiaData
|
||||
{
|
||||
var sps = parseStopsLine(line);
|
||||
if (sps != null)
|
||||
{
|
||||
for (sp in sps)
|
||||
result.Stops.push(sp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a StepMania file content into StepManiaData structure.
|
||||
* @param stepContent The content of the StepMania file as a string.
|
||||
* @return StepManiaData The parsed StepMania data.
|
||||
*/
|
||||
public static function parseStepManiaFile(stepContent:String):StepManiaData
|
||||
{
|
||||
readDiffMetadata = new Map<String, String>();
|
||||
var lines:Array<String> = stepContent.split("\n");
|
||||
|
||||
var currentMeasure:Int = 0;
|
||||
var measure:Array<String> = [];
|
||||
var workingDiff:StepDifficulty = null;
|
||||
|
||||
var state:StepStateEnum = StepStateEnum.Metadata;
|
||||
var headerLines:Int = 0;
|
||||
|
||||
var result:StepManiaData = {
|
||||
Metadata: {
|
||||
Title: "",
|
||||
Artist: "",
|
||||
Genre: "",
|
||||
Credit: "",
|
||||
Banner: "",
|
||||
Background: "",
|
||||
Offset: 0,
|
||||
SampleStart: 0
|
||||
},
|
||||
TimingPoints: [],
|
||||
Stops: [],
|
||||
Difficulties: []
|
||||
};
|
||||
|
||||
// Parsing metadata
|
||||
|
||||
for (line in lines)
|
||||
{
|
||||
line = StringTools.trim(line);
|
||||
if (line == "") continue;
|
||||
if (StringTools.startsWith(line, "//")) continue; // Comment line
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case StepStateEnum.Metadata:
|
||||
if (StringTools.startsWith(line, "#BPMS:"))
|
||||
{
|
||||
state = StepStateEnum.TimingPoints;
|
||||
result = parseBPMS(line, result);
|
||||
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
|
||||
}
|
||||
else if (StringTools.startsWith(line, "#STOPS:"))
|
||||
{
|
||||
state = StepStateEnum.Stops;
|
||||
result = parseStops(line, result);
|
||||
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
|
||||
}
|
||||
else if (StringTools.startsWith(line, "#NOTES:"))
|
||||
{
|
||||
if (workingDiff != null)
|
||||
{
|
||||
// Save previous difficulty
|
||||
result = pushWorking(workingDiff, result);
|
||||
}
|
||||
|
||||
// Start new difficulty
|
||||
workingDiff = new StepDifficulty("", "", 0, "");
|
||||
workingDiff.notes = [];
|
||||
|
||||
headerLines = 0;
|
||||
currentMeasure = 0;
|
||||
measure = [];
|
||||
state = StepStateEnum.Notes;
|
||||
}
|
||||
else if (StringTools.startsWith(line, "#")) result = parseMetadataLine(line.substr(1), result);
|
||||
case StepStateEnum.TimingPoints:
|
||||
if (line == ";") state = StepStateEnum.Metadata;
|
||||
else
|
||||
{
|
||||
result = parseBPMS(line, result);
|
||||
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
|
||||
}
|
||||
case StepStateEnum.Stops:
|
||||
if (line == ";") state = StepStateEnum.Metadata;
|
||||
else
|
||||
{
|
||||
result = parseStops(line, result);
|
||||
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
|
||||
}
|
||||
case StepStateEnum.Notes:
|
||||
if (line == "#NOTES:") continue;
|
||||
// remove comments in line (if any) ie, "0000 // some comment"
|
||||
var commentIndex = line.indexOf("//");
|
||||
if (commentIndex != -1) line = StringTools.trim(line.substr(0, commentIndex));
|
||||
|
||||
if (StringTools.contains(line, ":")) // header
|
||||
{
|
||||
switch (headerLines)
|
||||
{
|
||||
case 0:
|
||||
// First line is chart type
|
||||
var chartTypeStr = StringTools.trim(line).replace(":", "");
|
||||
workingDiff.type = workingDiff.parseChartType(chartTypeStr);
|
||||
case 1:
|
||||
// Second line is charter
|
||||
workingDiff.charter = StringTools.trim(line).replace(":", "");
|
||||
case 2:
|
||||
// Third line is difficulty name
|
||||
workingDiff.name = StringTools.trim(line).replace(":", "");
|
||||
case 3:
|
||||
// Fourth line is difficulty rating
|
||||
workingDiff.difficultyRating = Std.parseInt(StringTools.trim(line).replace(":", ""));
|
||||
// we dont care about the rest lol
|
||||
}
|
||||
headerLines++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// we're reading StepNote data now
|
||||
// start gathering measures until we hit a ,
|
||||
if (line == "," || line == ";")
|
||||
{
|
||||
// end of measure
|
||||
var stepNotesInMeasure = parseMeasure(measure, currentMeasure);
|
||||
for (stepNote in stepNotesInMeasure)
|
||||
workingDiff.notes.push(stepNote);
|
||||
measure = [];
|
||||
currentMeasure++;
|
||||
// end of StepNotes section
|
||||
if (line == ";") state = StepStateEnum.Metadata;
|
||||
}
|
||||
else
|
||||
{
|
||||
measure.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (workingDiff != null && workingDiff.notes.length > 0)
|
||||
{
|
||||
// Save last difficulty
|
||||
result = pushWorking(workingDiff, result);
|
||||
}
|
||||
|
||||
// Syncronize timing points with StepStops
|
||||
result.TimingPoints = synchronizeStepTimingPoints(result.Stops, result.TimingPoints);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static function getStepStopAtBeat(beat:Float, stepStops:Array<StepStop>):StepStop
|
||||
{
|
||||
var stepStop:StepStop = null;
|
||||
|
||||
for (s in stepStops)
|
||||
{
|
||||
if (s.startBeat < beat) stepStop = s;
|
||||
}
|
||||
|
||||
return stepStop;
|
||||
}
|
||||
|
||||
static function beatToTime(beat:Float, offset:Float, stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Float
|
||||
{
|
||||
var time:Float = 0;
|
||||
for (tp in stepTimingPoints)
|
||||
{
|
||||
if (tp.startBeat <= beat && tp.endBeat > beat)
|
||||
{
|
||||
var stepStop:StepStop = getStepStopAtBeat(beat, stepStops);
|
||||
var b:Float = tp.startBeat;
|
||||
var startTime:Float = tp.startTimestamp;
|
||||
if (stepStop != null)
|
||||
{
|
||||
if (stepStop.startBeat > tp.startBeat)
|
||||
{
|
||||
b = stepStop.startBeat;
|
||||
startTime = stepStop.startTimestamp + stepStop.duration;
|
||||
}
|
||||
}
|
||||
|
||||
var nb:Float = (beat - b) / (tp.bpm / 60);
|
||||
time = startTime + nb;
|
||||
}
|
||||
}
|
||||
|
||||
return (time * 1000) - (offset * 1000); // convert to ms
|
||||
}
|
||||
|
||||
static function convertStepNotes(offset:Float, type:StepManiaChartType, stepNotes:Array<StepNote>,
|
||||
stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Array<SongNoteData>
|
||||
{
|
||||
var result:Array<SongNoteData> = [];
|
||||
var holdArray:Array<Float> = [];
|
||||
if (type == StepManiaChartType.DanceSingle)
|
||||
{
|
||||
holdArray = [-1, -1, -1, -1];
|
||||
}
|
||||
else if (type == StepManiaChartType.DanceDouble)
|
||||
{
|
||||
holdArray = [-1, -1, -1, -1, -1, -1, -1, -1];
|
||||
}
|
||||
else
|
||||
{
|
||||
trace("[WARN] Unknown StepMania chart type when converting notes.");
|
||||
return result;
|
||||
}
|
||||
|
||||
for (stepNote in stepNotes)
|
||||
{
|
||||
var time = beatToTime(stepNote.beat, offset, stepTimingPoints, stepStops);
|
||||
|
||||
if (stepNote.type == StepManiaNoteType.Head || stepNote.type == StepManiaNoteType.Roll)
|
||||
{
|
||||
holdArray[stepNote.column] = time;
|
||||
continue;
|
||||
}
|
||||
|
||||
var snd:SongNoteData = new SongNoteData(time, stepNote.column, 0);
|
||||
if (stepNote.type == StepManiaNoteType.Mine) snd.kind = "mine";
|
||||
else if (stepNote.type == StepManiaNoteType.Fake) snd.kind = "fake";
|
||||
if (stepNote.type == StepManiaNoteType.Tail)
|
||||
{
|
||||
var length:Float = 0;
|
||||
if (holdArray[stepNote.column] != -1) length = time - holdArray[stepNote.column];
|
||||
snd.time = holdArray[stepNote.column];
|
||||
snd.length = length;
|
||||
holdArray[stepNote.column] = -1;
|
||||
}
|
||||
|
||||
if (type != StepManiaChartType.DanceSingle)
|
||||
{
|
||||
if (stepNote.column < 4) snd.data = stepNote.column + 4;
|
||||
else
|
||||
snd.data = stepNote.column - 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (snd.data >= 4) snd.data -= 4;
|
||||
}
|
||||
|
||||
result.push(snd);
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates StepManiaData to SongMetadata.
|
||||
* @param songData The StepManiaData to migrate.
|
||||
* @return SongMetadata The migrated SongMetadata.
|
||||
*/
|
||||
public static function migrateChartMetadata(songData:StepManiaData):SongMetadata
|
||||
{
|
||||
var metadata:SongMetadata = new SongMetadata(songData.Metadata.Title, songData.Metadata.Artist);
|
||||
|
||||
metadata.playData.stage = 'mainStage';
|
||||
metadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
|
||||
|
||||
metadata.generatedBy = 'Chart Editor Import (StepMania)';
|
||||
|
||||
metadata.playData.songVariations = [];
|
||||
|
||||
// Difficulties
|
||||
var difficulties:Array<String> = [];
|
||||
for (diff in songData.Difficulties)
|
||||
{
|
||||
if (diff.type == StepManiaChartType.Unknown)
|
||||
{
|
||||
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
|
||||
continue; // skip unknown chart types
|
||||
}
|
||||
difficulties.push(diff.name);
|
||||
metadata.playData.ratings.set(diff.name, diff.difficultyRating);
|
||||
}
|
||||
|
||||
metadata.playData.difficulties = difficulties;
|
||||
|
||||
metadata.charter = songData.Metadata.Credit != "" ? songData.Metadata.Credit : null;
|
||||
|
||||
// TimeChanges
|
||||
metadata.timeChanges = [];
|
||||
for (tp in songData.TimingPoints)
|
||||
{
|
||||
var timeChange = new SongTimeChange(tp.startTimestamp * 1000, tp.bpm);
|
||||
timeChange.beatTime = tp.startBeat;
|
||||
metadata.timeChanges.push(timeChange);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates StepManiaData to SongChartData.
|
||||
* @param songData The StepManiaData to migrate.
|
||||
* @return SongChartData The migrated SongChartData.
|
||||
*/
|
||||
public static function migrateChartData(songData:StepManiaData):SongChartData {
|
||||
var scrollsMap:Map<String, Float> = new Map<String, Float>();
|
||||
var stepNoteMap:Map<String, Array<SongNoteData>> = new Map<String, Array<SongNoteData>>();
|
||||
|
||||
for (diff in songData.Difficulties)
|
||||
{
|
||||
if (diff.type == StepManiaChartType.Unknown)
|
||||
{
|
||||
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
|
||||
continue; // skip unknown chart types
|
||||
}
|
||||
scrollsMap.set(diff.name, Constants.DEFAULT_SCROLLSPEED);
|
||||
stepNoteMap.set(diff.name, convertStepNotes(songData.Metadata.Offset, diff.type, diff.notes, songData.TimingPoints, songData.Stops));
|
||||
}
|
||||
|
||||
var songChartData:SongChartData = new SongChartData(scrollsMap, [], stepNoteMap);
|
||||
return songChartData;
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ class StageData
|
|||
updateVersionToLatest();
|
||||
|
||||
var writer = new json2object.JsonWriter<StageData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
|
|
@ -196,7 +196,7 @@ typedef StageDataProp =
|
|||
|
||||
/**
|
||||
* The animation type to use.
|
||||
* Options: "sparrow", "packer"
|
||||
* Options: "sparrow", "packer", "animateatlas"
|
||||
* @default "sparrow"
|
||||
*/
|
||||
@:default("sparrow")
|
||||
|
|
@ -228,6 +228,52 @@ typedef StageDataProp =
|
|||
@:default("#FFFFFF")
|
||||
@:optional
|
||||
var color:String;
|
||||
|
||||
/**
|
||||
* Various settings for the prop.
|
||||
* Only available for texture atlases.
|
||||
*/
|
||||
@:optional
|
||||
var atlasSettings:TextureAtlasData;
|
||||
};
|
||||
|
||||
typedef TextureAtlasData =
|
||||
{
|
||||
/**
|
||||
* If true, the texture atlas will behave as if it was exported as an SWF file.
|
||||
* Notably, this allows MovieClip symbols to play.
|
||||
*/
|
||||
@:optional
|
||||
var swfMode:Bool;
|
||||
|
||||
/**
|
||||
* If true, filters and masks will be cached when the atlas is loaded, instead of during runtime.
|
||||
*/
|
||||
@:optional
|
||||
var cacheOnLoad:Bool;
|
||||
|
||||
/**
|
||||
* The filter quality.
|
||||
* Available values are: HIGH, MEDIUM, LOW, and RUDY.
|
||||
*
|
||||
* If you're making an atlas sprite in HScript, you pass an Int instead:
|
||||
*
|
||||
* HIGH - 0
|
||||
* MEDIUM - 1
|
||||
* LOW - 2
|
||||
* RUDY - 3
|
||||
*/
|
||||
@:optional
|
||||
var filterQuality:Int;
|
||||
|
||||
/**
|
||||
* Whether to apply the stage matrix, if it was exported from a symbol instance.
|
||||
* Also positions the Texture Atlas as it displays in Animate.
|
||||
* Turning this on is only recommended if you prepositioned the character in Animate.
|
||||
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
|
||||
*/
|
||||
@:optional
|
||||
var applyStageMatrix:Bool;
|
||||
};
|
||||
|
||||
typedef StageDataCharacter =
|
||||
|
|
|
|||
|
|
@ -114,4 +114,11 @@ typedef LevelPropData =
|
|||
@:default([])
|
||||
@:optional
|
||||
var animations:Array<AnimationData>;
|
||||
|
||||
/**
|
||||
* Flips the sprite on X axis.
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var flipX:Null<Bool>;
|
||||
}
|
||||
|
|
|
|||
26
source/funkin/external/android/KeyboardUtil.hx
vendored
Normal file
26
source/funkin/external/android/KeyboardUtil.hx
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package funkin.external.android;
|
||||
|
||||
#if android
|
||||
import lime.system.JNI;
|
||||
|
||||
/**
|
||||
* Utility class for keyboard detection.
|
||||
*/
|
||||
class KeyboardUtil
|
||||
{
|
||||
/**
|
||||
* Returns `true` if a keyboard is currently connected to the device.
|
||||
*/
|
||||
public static var keyboardConnected(get, never):Bool;
|
||||
|
||||
@:noCompletion
|
||||
static function get_keyboardConnected():Bool
|
||||
{
|
||||
final method:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/util/KeyboardUtil', 'isKeyboardConnected', '()Z');
|
||||
|
||||
if (method == null) return false;
|
||||
|
||||
return inline JNI.callStatic(method, []);
|
||||
}
|
||||
}
|
||||
#end
|
||||
37
source/funkin/external/android/java/funkin/extension/AudioSession.java
vendored
Normal file
37
source/funkin/external/android/java/funkin/extension/AudioSession.java
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package funkin.extensions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import org.haxe.extension.Extension;
|
||||
|
||||
public class AudioSession extends Extension {
|
||||
private AudioManager audioManager;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
audioManager = (AudioManager) Extension.mainContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
requestAudioFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (audioManager != null) {
|
||||
audioManager.abandonAudioFocus(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
requestAudioFocus();
|
||||
}
|
||||
|
||||
private void requestAudioFocus() {
|
||||
if (audioManager != null) {
|
||||
audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
source/funkin/external/android/java/funkin/util/KeyboardUtil.java
vendored
Normal file
14
source/funkin/external/android/java/funkin/util/KeyboardUtil.java
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package funkin.util;
|
||||
|
||||
import org.haxe.extension.Extension;
|
||||
|
||||
public class KeyboardUtil
|
||||
{
|
||||
public static boolean isKeyboardConnected()
|
||||
{
|
||||
if (Extension.mainContext == null) return false;
|
||||
|
||||
// KEYBOARD_UNDEFINED = 0, KEYBOARD_NOKEYS = 1
|
||||
return Extension.mainContext.getResources().getConfiguration().keyboard > 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,32 @@
|
|||
#import <TargetConditionals.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AVFAudio/AVFAudio.h>
|
||||
#if TARGET_OS_IOS
|
||||
#import <UIKit/UIKit.h>
|
||||
#endif
|
||||
|
||||
void Apple_AudioSession_Initialize()
|
||||
{
|
||||
#if TARGET_OS_IOS
|
||||
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
|
||||
NSError *error;
|
||||
|
||||
[session setCategory:AVAudioSessionCategorySoloAmbient error:nil];
|
||||
[session setActive:YES error:nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:@"UISceneWillEnterForegroundNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
NSError *resumeError = nil;
|
||||
[session setCategory:AVAudioSessionCategorySoloAmbient withOptions:0 error:nil];
|
||||
|
||||
[session setActive:YES error:&resumeError];
|
||||
|
||||
if (resumeError)
|
||||
NSLog(@"AudioSession resume error: %@", resumeError);
|
||||
else
|
||||
NSLog(@"AudioSession resumed and reactivated");
|
||||
}];
|
||||
[session setCategory:AVAudioSessionCategoryPlayback mode:AVAudioSessionModeDefault options:AVAudioSessionCategoryOptionAllowBluetoothA2DP | AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers error:&error];
|
||||
|
||||
if (@available(iOS 17.0, *))
|
||||
|
|
|
|||
35
source/funkin/graphics/FunkinAnimationController.hx
Normal file
35
source/funkin/graphics/FunkinAnimationController.hx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package funkin.graphics;
|
||||
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import animate.FlxAnimateController;
|
||||
|
||||
class FunkinAnimationController extends FlxAnimateController
|
||||
{
|
||||
/**
|
||||
* The sprite that this animation controller is attached to.
|
||||
*/
|
||||
var _parentSprite:FunkinSprite;
|
||||
|
||||
public function new(sprite:FunkinSprite)
|
||||
{
|
||||
super(sprite);
|
||||
_parentSprite = sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* We override `FlxAnimationController`'s `play` method to account for texture atlases.
|
||||
*/
|
||||
public override function play(animName:String, force = false, reversed = false, frame = 0):Void
|
||||
{
|
||||
if (animName == null || animName == '') animName = _parentSprite.getDefaultSymbol();
|
||||
|
||||
if (!_parentSprite.hasAnimation(animName))
|
||||
{
|
||||
// Skip if the animation doesn't exist
|
||||
trace('Animation ${animName} does not exist!');
|
||||
return;
|
||||
}
|
||||
|
||||
super.play(animName, force, reversed, frame);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,28 +9,147 @@ import funkin.graphics.framebuffer.FixedBitmapData;
|
|||
import openfl.display.BitmapData;
|
||||
import flixel.math.FlxRect;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
|
||||
import flixel.math.FlxMatrix;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.FlxCamera;
|
||||
import openfl.system.System;
|
||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||
import funkin.FunkinMemory;
|
||||
import animate.internal.SymbolItem;
|
||||
import animate.internal.elements.Element;
|
||||
import animate.internal.elements.AtlasInstance;
|
||||
import animate.internal.elements.SymbolInstance;
|
||||
import animate.FlxAnimate;
|
||||
import animate.FlxAnimateFrames;
|
||||
import haxe.io.Path;
|
||||
|
||||
using StringTools;
|
||||
|
||||
typedef AtlasSpriteSettings =
|
||||
{
|
||||
/**
|
||||
* If true, the texture atlas will behave as if it was exported as an SWF file.
|
||||
* Notably, this allows MovieClip symbols to play.
|
||||
*/
|
||||
@:optional
|
||||
var swfMode:Bool;
|
||||
|
||||
/**
|
||||
* If true, filters and masks will be cached when the atlas is loaded, instead of during runtime.
|
||||
*/
|
||||
@:optional
|
||||
var cacheOnLoad:Bool;
|
||||
|
||||
/**
|
||||
* The filter quality.
|
||||
* Available values are: HIGH, MEDIUM, LOW, and RUDY.
|
||||
*
|
||||
* If you're making an atlas sprite in HScript, you pass an Int instead:
|
||||
*
|
||||
* HIGH - 0
|
||||
* MEDIUM - 1
|
||||
* LOW - 2
|
||||
* RUDY - 3
|
||||
*/
|
||||
@:optional
|
||||
var filterQuality:FilterQuality;
|
||||
|
||||
/**
|
||||
* Optional, an array of spritemaps for the atlas to load.
|
||||
*/
|
||||
@:optional
|
||||
var spritemaps:Array<SpritemapInput>;
|
||||
|
||||
/**
|
||||
* Optional, string of the metadata.json contents.
|
||||
*/
|
||||
@:optional
|
||||
var metadataJson:String;
|
||||
|
||||
/**
|
||||
* Optional, force the cache to use a specific key to index the texture atlas.
|
||||
*/
|
||||
@:optional
|
||||
var cacheKey:String;
|
||||
|
||||
/**
|
||||
* If true, the texture atlas will use a new slot in the cache.
|
||||
*/
|
||||
@:optional
|
||||
var uniqueInCache:Bool;
|
||||
|
||||
/**
|
||||
* Optional callback for when a symbol is created.
|
||||
*/
|
||||
@:optional
|
||||
var onSymbolCreate:animate.internal.SymbolItem->Void;
|
||||
|
||||
/**
|
||||
* Whether to apply the stage matrix, if it was exported from a symbol instance.
|
||||
* Also positions the Texture Atlas as it displays in Animate.
|
||||
* Turning this on is only recommended if you prepositioned the character in Animate.
|
||||
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
|
||||
*/
|
||||
@:optional
|
||||
var applyStageMatrix:Bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* An FlxSprite with additional functionality.
|
||||
* - A more efficient method for creating solid color sprites.
|
||||
* - TODO: Better cache handling for textures.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinSprite extends FlxSprite
|
||||
class FunkinSprite extends FlxAnimate
|
||||
{
|
||||
/**
|
||||
* @param x Starting X position
|
||||
* @param y Starting Y position
|
||||
* @param path The asset path for the graphic
|
||||
* @param atlasSettings The optional settings for the texture atlas
|
||||
*/
|
||||
public function new(?x:Float = 0, ?y:Float = 0)
|
||||
public function new(?x:Float = 0, ?y:Float = 0, ?path:String, ?atlasSettings:AtlasSpriteSettings)
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
if (path != null)
|
||||
{
|
||||
var ext:String = Path.extension(path);
|
||||
|
||||
switch (ext)
|
||||
{
|
||||
case 'png':
|
||||
this.loadGraphic(path);
|
||||
|
||||
case '':
|
||||
// Do the opposite of Paths.animateAtlas since that function is called in loadTextureAtlas.
|
||||
var lib:String = Paths.getLibrary(path);
|
||||
|
||||
if (lib == 'preload')
|
||||
{
|
||||
path = path.replace('assets/images/', '');
|
||||
}
|
||||
else
|
||||
{
|
||||
path = path.replace('$lib:assets/$lib/images/', '');
|
||||
}
|
||||
|
||||
this.loadTextureAtlas(path, lib, atlasSettings);
|
||||
|
||||
default:
|
||||
FlxG.log.warn('Texture path $path is not a valid path. Make sure the path points to either an image or a folder with the texture atlas files!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override function initVars():Void
|
||||
{
|
||||
super.initVars();
|
||||
|
||||
var newController:FunkinAnimationController = new FunkinAnimationController(this);
|
||||
|
||||
animation = newController;
|
||||
anim = newController;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -75,6 +194,20 @@ class FunkinSprite extends FlxSprite
|
|||
return sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new FunkinSprite with an Adobe Animate texture atlas.
|
||||
* @param x The starting X position.
|
||||
* @param y The starting Y position.
|
||||
* @param key The key of the texture to load.
|
||||
* @return The new FunkinSprite.
|
||||
*/
|
||||
public static function createTextureAtlas(x:Float = 0.0, y:Float = 0.0, key:String, ?assetLibrary:Null<String>, ?settings:AtlasSpriteSettings):FunkinSprite
|
||||
{
|
||||
var sprite:FunkinSprite = new FunkinSprite(x, y);
|
||||
sprite.loadTextureAtlas(key, assetLibrary ?? "", settings);
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a static image as the sprite's texture.
|
||||
* @param key The key of the texture to load.
|
||||
|
|
@ -83,7 +216,17 @@ class FunkinSprite extends FlxSprite
|
|||
public function loadTexture(key:String):FunkinSprite
|
||||
{
|
||||
var graphicKey:String = Paths.image(key);
|
||||
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
|
||||
if (!Assets.exists(graphicKey, IMAGE))
|
||||
{
|
||||
FlxG.log.error('Texture not found, check your path! $graphicKey');
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!FunkinMemory.isTextureCached(graphicKey))
|
||||
{
|
||||
FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
}
|
||||
|
||||
loadGraphic(graphicKey);
|
||||
|
||||
|
|
@ -163,6 +306,63 @@ class FunkinSprite extends FlxSprite
|
|||
return loadBitmapData(inputBitmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an Adobe Animate texture atlas as the sprite's texture.
|
||||
* @param key The key of the texture to load.
|
||||
* @param settings Additional settings for loading the atlas.
|
||||
* @return This sprite, for chaining.
|
||||
*/
|
||||
public function loadTextureAtlas(key:Null<String>, ?assetLibrary:Null<String>, ?settings:AtlasSpriteSettings):FunkinSprite
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw 'Null path specified for loadTextureAtlas()!';
|
||||
}
|
||||
|
||||
var validatedSettings:AtlasSpriteSettings =
|
||||
{
|
||||
swfMode: settings?.swfMode ?? false,
|
||||
cacheOnLoad: settings?.cacheOnLoad ?? false,
|
||||
filterQuality: settings?.filterQuality ?? MEDIUM,
|
||||
spritemaps: settings?.spritemaps ?? null,
|
||||
metadataJson: settings?.metadataJson ?? null,
|
||||
cacheKey: settings?.cacheKey ?? null,
|
||||
uniqueInCache: settings?.uniqueInCache ?? false,
|
||||
onSymbolCreate: settings?.onSymbolCreate ?? null,
|
||||
applyStageMatrix: settings?.applyStageMatrix ?? false
|
||||
};
|
||||
|
||||
var assetLibrary:String = assetLibrary ?? "";
|
||||
var graphicKey:String = "";
|
||||
|
||||
if (assetLibrary != "")
|
||||
{
|
||||
graphicKey = Paths.animateAtlas(key, assetLibrary);
|
||||
}
|
||||
else
|
||||
{
|
||||
graphicKey = Paths.animateAtlas(key);
|
||||
}
|
||||
|
||||
// Validate asset path.
|
||||
if (!Assets.exists('${graphicKey}/Animation.json'))
|
||||
{
|
||||
throw 'No Animation.json file exists at the specified path (${graphicKey})';
|
||||
}
|
||||
|
||||
this.applyStageMatrix = validatedSettings.applyStageMatrix ?? false;
|
||||
|
||||
frames = FlxAnimateFrames.fromAnimate(graphicKey, validatedSettings.spritemaps, validatedSettings.metadataJson, validatedSettings.cacheKey,
|
||||
validatedSettings.uniqueInCache, {
|
||||
swfMode: validatedSettings.swfMode,
|
||||
cacheOnLoad: validatedSettings.cacheOnLoad,
|
||||
filterQuality: validatedSettings.filterQuality,
|
||||
onSymbolCreate: validatedSettings.onSymbolCreate
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture.
|
||||
* @param key The key of the texture to load.
|
||||
|
|
@ -171,7 +371,7 @@ class FunkinSprite extends FlxSprite
|
|||
public function loadSparrow(key:String):FunkinSprite
|
||||
{
|
||||
var graphicKey:String = Paths.image(key);
|
||||
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
|
||||
this.frames = Paths.getSparrowAtlas(key);
|
||||
|
||||
|
|
@ -186,73 +386,13 @@ class FunkinSprite extends FlxSprite
|
|||
public function loadPacker(key:String):FunkinSprite
|
||||
{
|
||||
var graphicKey:String = Paths.image(key);
|
||||
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
|
||||
this.frames = Paths.getPackerAtlas(key);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the texture with the given key is cached.
|
||||
* @param key The key of the texture to check.
|
||||
* @return Whether the texture is cached.
|
||||
*/
|
||||
public static function isTextureCached(key:String):Bool
|
||||
{
|
||||
return FlxG.bitmap.get(key) != null;
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.cacheTexture() instead")
|
||||
public static function cacheTexture(key:String):Void
|
||||
{
|
||||
FunkinMemory.cacheTexture(Paths.image(key));
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.permanentCacheTexture() instead")
|
||||
public static function permanentCacheTexture(key:String):Void
|
||||
{
|
||||
@:privateAccess FunkinMemory.permanentCacheTexture(Paths.image(key));
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.cacheTexture() instead")
|
||||
public static function cacheSparrow(key:String):Void
|
||||
{
|
||||
FunkinMemory.cacheTexture(Paths.image(key));
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.cacheTexture() instead")
|
||||
public static function cachePacker(key:String):Void
|
||||
{
|
||||
FunkinMemory.cacheTexture(Paths.image(key));
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.preparePurgeTextureCache() instead")
|
||||
public static function preparePurgeCache():Void
|
||||
{
|
||||
FunkinMemory.preparePurgeTextureCache();
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.purgeCache() instead")
|
||||
public static function purgeCache():Void
|
||||
{
|
||||
FunkinMemory.purgeCache();
|
||||
}
|
||||
|
||||
static function isGraphicCached(graphic:FlxGraphic):Bool
|
||||
{
|
||||
var result = null;
|
||||
if (graphic == null) return false;
|
||||
result = FlxG.bitmap.get(graphic.key);
|
||||
if (result == null) return false;
|
||||
if (result != graphic)
|
||||
{
|
||||
FlxG.log.warn('Cached graphic does not match original: ${graphic.key}');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The animation ID to check.
|
||||
* @return Whether the animation is dynamic (has multiple frames). `false` for static, one-frame animations.
|
||||
|
|
@ -266,6 +406,107 @@ class FunkinSprite extends FlxSprite
|
|||
return animData.numFrames > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this sprite has an animation with the given ID.
|
||||
* @param id The ID of the animation to check.
|
||||
*/
|
||||
public function hasAnimation(id:String):Bool
|
||||
{
|
||||
var animationList:Array<String> = this.animation?.getNameList() ?? [];
|
||||
if (animationList.contains(id))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (this.isAnimate && !animationList.contains(id))
|
||||
{
|
||||
return addAnimationIfMissing(id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an animation if it doesn't exist.
|
||||
* @param id The animation ID to check.
|
||||
*/
|
||||
function addAnimationIfMissing(id:String):Bool
|
||||
{
|
||||
@:privateAccess
|
||||
var symbols:Array<String> = this.library.dictionary.keys().array();
|
||||
var frameLabels:Array<String> = listAnimations();
|
||||
|
||||
if (frameLabels.contains(id))
|
||||
{
|
||||
// Animation exists as a frame label but wasn't added, so we add it
|
||||
anim.addByFrameLabel(id, id, this.library.frameRate, false);
|
||||
return true;
|
||||
}
|
||||
else if (symbols.contains(id))
|
||||
{
|
||||
// Animation exists as a symbol but wasn't added, so we add it
|
||||
anim.addBySymbol(id, id, this.library.frameRate, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets every frame on every symbol that starts with the given keyword.
|
||||
* @param keyword The keyword to search for.
|
||||
* @return An array of frames.
|
||||
*/
|
||||
public function getFramesWithKeyword(keyword:String):Array<animate.internal.Frame>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getFramesWithKeyword() only works texture atlases!');
|
||||
return [];
|
||||
}
|
||||
|
||||
var symbolItems:Array<animate.internal.SymbolItem> = [];
|
||||
var frames:Array<animate.internal.Frame> = [];
|
||||
|
||||
@:privateAccess
|
||||
for (symbol in this.library.dictionary.keys())
|
||||
{
|
||||
var symbolItem:Null<animate.internal.SymbolItem> = this.library.getSymbol(symbol);
|
||||
if (symbolItem == null) continue;
|
||||
|
||||
if (symbolItem.name.contains(keyword))
|
||||
{
|
||||
symbolItems.push(symbolItem);
|
||||
}
|
||||
}
|
||||
|
||||
for (symbolItem in symbolItems)
|
||||
{
|
||||
symbolItem.timeline.forEachLayer((layer) -> {
|
||||
layer.forEachFrame((frame) -> {
|
||||
frames.push(frame);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current animation ID.
|
||||
*/
|
||||
public function getCurrentAnimation():String
|
||||
{
|
||||
return this.animation?.curAnim?.name ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the current animation is finished.
|
||||
*/
|
||||
public function isAnimationFinished():Bool
|
||||
{
|
||||
return this.animation?.finished ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts similarly to `makeGraphic`, but with improved memory usage,
|
||||
* at the expense of not being able to paint onto the resulting sprite.
|
||||
|
|
@ -286,6 +527,201 @@ class FunkinSprite extends FlxSprite
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A list of all the animations this sprite has available.
|
||||
*/
|
||||
public function listAnimations():Array<String>
|
||||
{
|
||||
var frameLabels:Array<String> = getFrameLabelList();
|
||||
var animationList:Array<String> = this.animation?.getNameList() ?? [];
|
||||
|
||||
return frameLabels.concat(animationList);
|
||||
}
|
||||
|
||||
/**
|
||||
* TEXTURE ATLAS-EXCLUSIVE FUNCTIONS
|
||||
* These functions only work if the sprite's texture is an Adobe Animate texture atlas.
|
||||
* Calling these functions on non-texture atlases will do nothing.
|
||||
*/
|
||||
/**
|
||||
* Gets a list of frame labels from the default timeline.
|
||||
*/
|
||||
public function getFrameLabelList():Array<String>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getFrameLabelList() only works texture atlases!');
|
||||
return [];
|
||||
}
|
||||
|
||||
var foundLabels:Array<String> = [];
|
||||
var mainTimeline:Null<animate.internal.Timeline> = this.library.timeline;
|
||||
|
||||
for (layer in mainTimeline.layers)
|
||||
{
|
||||
@:nullSafety(Off)
|
||||
for (frame in layer.frames)
|
||||
{
|
||||
if (frame.name.rtrim() != '')
|
||||
{
|
||||
foundLabels.push(frame.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a frame label by its name.
|
||||
* @param name The name of the frame label to retrieve.
|
||||
* @return The frame label, or null if it doesn't exist.
|
||||
*/
|
||||
public function getFrameLabel(name:String):Null<animate.internal.Frame>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getFrameLabel() only works texture atlases!');
|
||||
return null;
|
||||
}
|
||||
|
||||
for (layer in this.timeline.layers)
|
||||
{
|
||||
@:nullSafety(Off)
|
||||
for (frame in layer.frames)
|
||||
{
|
||||
if (frame.name == name)
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default symbol in the atlas.
|
||||
*/
|
||||
public function getDefaultSymbol():String
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getDefaultSymbol() only works texture atlases!');
|
||||
return '';
|
||||
}
|
||||
|
||||
return library.timeline.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the graphic of a symbol in the atlas.
|
||||
* @param symbol The symbol to replace.
|
||||
* @param graphic The new graphic to use.
|
||||
* @param adjustScale Whether to adjust the scale of new frame to match the old one.
|
||||
*/
|
||||
public function replaceSymbolGraphic(symbol:String, ?graphic:Null<FlxGraphicAsset>, ?adjustScale:Bool = true):Void
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: replaceSymbolGraphic() only works texture atlases!');
|
||||
return;
|
||||
}
|
||||
|
||||
var elements:Array<Element> = getSymbolElements(symbol);
|
||||
|
||||
for (element in elements)
|
||||
{
|
||||
var atlasInstance:AtlasInstance = element.toAtlasInstance();
|
||||
var frame:Null<FlxFrame> = graphic != null ? FlxG.bitmap.add(graphic).imageFrame.frame : null;
|
||||
|
||||
atlasInstance.replaceFrame(frame, adjustScale);
|
||||
element = atlasInstance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element of a symbol in the atlas.
|
||||
* @param symbol The symbol to get elements from.
|
||||
* @return The first element of the symbol. WARNING: Can be null.
|
||||
*/
|
||||
public function getFirstElement(symbol:String):Null<Element>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getFirstElement() only works texture atlases!');
|
||||
return null;
|
||||
}
|
||||
|
||||
var symbolElements:Array<Element> = getSymbolElements(symbol);
|
||||
return symbolElements.length > 0 ? symbolElements[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the elements of a symbol in the atlas.
|
||||
* @param symbol The symbol to get elements from.
|
||||
*/
|
||||
public function getSymbolElements(symbol:String):Array<Element>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getSymbolElements() only works texture atlases!');
|
||||
return [];
|
||||
}
|
||||
|
||||
var symbolInstance:Null<SymbolItem> = this.library.getSymbol(symbol);
|
||||
|
||||
if (symbolInstance == null)
|
||||
{
|
||||
throw 'Symbol not found in atlas: ${symbol}';
|
||||
return [];
|
||||
}
|
||||
|
||||
var elements:Array<Element> = symbolInstance.timeline.getElementsAtIndex(0);
|
||||
|
||||
if (elements?.length == 0)
|
||||
{
|
||||
trace('WARNING: No Atlas Elements found for "$symbol" symbol.');
|
||||
}
|
||||
|
||||
return elements ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales an element by a certain multiplier.
|
||||
* @param element The element to scale.
|
||||
* @param scale The scale multiplier.
|
||||
* @param positionOffset The offset to apply to `tx` and `ty` after scaling.
|
||||
* (Or in other words, the position of the element.)
|
||||
*/
|
||||
public function scaleElement(element:Element, scale:Float, positionOffset:Float = 0, scaleEverything:Bool = false):Void
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: scaleElement() only works texture atlases!');
|
||||
return;
|
||||
}
|
||||
|
||||
var elementMatrix:FlxMatrix = element.matrix;
|
||||
|
||||
if (scaleEverything)
|
||||
{
|
||||
elementMatrix.scale(scale, scale);
|
||||
return;
|
||||
}
|
||||
|
||||
var symbolInstance:SymbolInstance = element.parentFrame.convertToSymbol(0, 1);
|
||||
var transformPoint:FlxPoint = symbolInstance.transformationPoint;
|
||||
|
||||
elementMatrix.a += scale;
|
||||
elementMatrix.d += scale;
|
||||
|
||||
elementMatrix.tx -= transformPoint.x * scale;
|
||||
elementMatrix.ty -= transformPoint.y * scale;
|
||||
|
||||
elementMatrix.tx -= positionOffset;
|
||||
elementMatrix.ty -= positionOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure scale is applied when cloning a sprite.R
|
||||
* The default `clone()` method acts kinda weird TBH.
|
||||
|
|
@ -325,77 +761,10 @@ class FunkinSprite extends FlxSprite
|
|||
return _rect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the screen position of this object.
|
||||
*
|
||||
* @param result Optional arg for the returning point
|
||||
* @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used.
|
||||
* @return The screen position of this object.
|
||||
*/
|
||||
public override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
|
||||
override function preparePixelPerfectMatrix(matrix:FlxMatrix)
|
||||
{
|
||||
if (result == null) result = FlxPoint.get();
|
||||
|
||||
if (camera == null) camera = FlxG.camera;
|
||||
|
||||
result.set(x, y);
|
||||
if (pixelPerfectPosition)
|
||||
{
|
||||
_rect.width = _rect.width / this.scale.x;
|
||||
_rect.height = _rect.height / this.scale.y;
|
||||
_rect.x = _rect.x / this.scale.x;
|
||||
_rect.y = _rect.y / this.scale.y;
|
||||
_rect.round();
|
||||
_rect.x = _rect.x * this.scale.x;
|
||||
_rect.y = _rect.y * this.scale.y;
|
||||
_rect.width = _rect.width * this.scale.x;
|
||||
_rect.height = _rect.height * this.scale.y;
|
||||
}
|
||||
|
||||
return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y);
|
||||
}
|
||||
|
||||
override function drawSimple(camera:FlxCamera):Void
|
||||
{
|
||||
getScreenPosition(_point, camera).subtractPoint(offset);
|
||||
if (isPixelPerfectRender(camera))
|
||||
{
|
||||
_point.x = _point.x / this.scale.x;
|
||||
_point.y = _point.y / this.scale.y;
|
||||
_point.round();
|
||||
|
||||
_point.x = _point.x * this.scale.x;
|
||||
_point.y = _point.y * this.scale.y;
|
||||
}
|
||||
|
||||
_point.copyToFlash(_flashPoint);
|
||||
camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing);
|
||||
}
|
||||
|
||||
override function drawComplex(camera:FlxCamera):Void
|
||||
{
|
||||
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
|
||||
_matrix.translate(-origin.x, -origin.y);
|
||||
_matrix.scale(scale.x, scale.y);
|
||||
|
||||
if (bakedRotationAngle <= 0)
|
||||
{
|
||||
updateTrig();
|
||||
|
||||
if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
|
||||
}
|
||||
|
||||
getScreenPosition(_point, camera).subtractPoint(offset);
|
||||
_point.add(origin.x, origin.y);
|
||||
_matrix.translate(_point.x, _point.y);
|
||||
|
||||
if (isPixelPerfectRender(camera))
|
||||
{
|
||||
_matrix.tx = Math.round(_matrix.tx / this.scale.x) * this.scale.x;
|
||||
_matrix.ty = Math.round(_matrix.ty / this.scale.y) * this.scale.y;
|
||||
}
|
||||
|
||||
camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
|
||||
matrix.tx = Math.round(matrix.tx / this.scale.x) * this.scale.x;
|
||||
matrix.ty = Math.round(matrix.ty / this.scale.y) * this.scale.y;
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
|
|
|
|||
|
|
@ -1,414 +0,0 @@
|
|||
package funkin.graphics.adobeanimate;
|
||||
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
import flxanimate.FlxAnimate;
|
||||
import flxanimate.FlxAnimate.Settings;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||
import flixel.math.FlxPoint;
|
||||
import flxanimate.animate.FlxKeyFrame;
|
||||
|
||||
/**
|
||||
* A sprite which provides convenience functions for rendering a texture atlas with animations.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FlxAtlasSprite extends FlxAnimate
|
||||
{
|
||||
static final SETTINGS:Settings =
|
||||
{
|
||||
// ?ButtonSettings:Map<String, flxanimate.animate.FlxAnim.ButtonSettings>,
|
||||
FrameRate: 24.0,
|
||||
Reversed: false,
|
||||
// ?OnComplete:Void -> Void,
|
||||
ShowPivot: false,
|
||||
Antialiasing: true,
|
||||
ScrollFactor: null,
|
||||
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
|
||||
};
|
||||
|
||||
/**
|
||||
* Signal dispatched when an animation advances to the next frame.
|
||||
*/
|
||||
public var onAnimationFrame:FlxTypedSignal<String->Int->Void> = new FlxTypedSignal();
|
||||
|
||||
/**
|
||||
* Signal dispatched when a non-looping animation finishes playing.
|
||||
*/
|
||||
public var onAnimationComplete:FlxTypedSignal<String->Void> = new FlxTypedSignal();
|
||||
|
||||
/**
|
||||
* Signal dispatched when a looping animation finishes playing.
|
||||
*/
|
||||
public var onAnimationLoop:FlxTypedSignal<String->Void> = new FlxTypedSignal();
|
||||
|
||||
var currentAnimation:String = '';
|
||||
|
||||
var canPlayOtherAnims:Bool = true;
|
||||
|
||||
@:nullSafety(Off) // null safety HATES new classes atm, it'll be fixed in haxe 4.0.0?
|
||||
public function new(x:Float, y:Float, ?path:String, ?settings:Settings, ?pathSafety:Bool = true)
|
||||
{
|
||||
if (settings == null) settings = SETTINGS;
|
||||
|
||||
if (path == null && pathSafety) throw 'Null path specified for FlxAtlasSprite!';
|
||||
|
||||
// Validate asset path.
|
||||
if (!Assets.exists('${path}/Animation.json')
|
||||
&& pathSafety) throw 'FlxAtlasSprite does not have an Animation.json file at the specified path (${path})';
|
||||
|
||||
super(x, y, path, settings);
|
||||
|
||||
if (this.anim.stageInstance == null && pathSafety) throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
|
||||
|
||||
onAnimationComplete.add(cleanupAnimation);
|
||||
|
||||
// This defaults the sprite to play the first animation in the atlas,
|
||||
// then pauses it. This ensures symbols are intialized properly.
|
||||
if (this.anim.curInstance != null)
|
||||
{
|
||||
this.anim.play('');
|
||||
this.anim.pause();
|
||||
}
|
||||
|
||||
this.anim.onComplete.add(_onAnimationComplete);
|
||||
this.anim.onFrame.add(_onAnimationFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A list of all the animations this sprite has available.
|
||||
*/
|
||||
public function listAnimations():Array<String>
|
||||
{
|
||||
var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name];
|
||||
if (mainSymbol == null)
|
||||
{
|
||||
FlxG.log.error('FlxAtlasSprite does not have its main symbol!');
|
||||
return [];
|
||||
}
|
||||
return mainSymbol.getFrameLabels().map(keyFrame -> keyFrame.name).filterNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id A string ID of the animation.
|
||||
* @return Whether the animation was found on this sprite.
|
||||
*/
|
||||
public function hasAnimation(id:String):Bool
|
||||
{
|
||||
return getLabelIndex(id) != -1 || anim.symbolDictionary.exists(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current animation being played.
|
||||
*/
|
||||
public function getCurrentAnimation():String
|
||||
{
|
||||
return this.currentAnimation;
|
||||
}
|
||||
|
||||
var _completeAnim:Bool = false;
|
||||
|
||||
var fr:Null<FlxKeyFrame> = null;
|
||||
|
||||
var looping:Bool = false;
|
||||
|
||||
public var ignoreExclusionPref:Array<String> = [];
|
||||
|
||||
/**
|
||||
* Plays an animation.
|
||||
* @param id A string ID of the animation to play.
|
||||
* @param restart Whether to restart the animation if it is already playing.
|
||||
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
|
||||
* @param loop Whether to loop the animation
|
||||
* @param startFrame The frame to start the animation on
|
||||
* NOTE: `loop` and `ignoreOther` are not compatible with each other!
|
||||
*/
|
||||
public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, loop:Bool = false, startFrame:Int = 0):Void
|
||||
{
|
||||
// Skip if not allowed to play animations.
|
||||
if ((!canPlayOtherAnims))
|
||||
{
|
||||
if (this.currentAnimation == id && restart) {}
|
||||
else if (ignoreExclusionPref != null && ignoreExclusionPref.length > 0)
|
||||
{
|
||||
var detected:Bool = false;
|
||||
for (entry in ignoreExclusionPref)
|
||||
{
|
||||
if (StringTools.startsWith(id, entry))
|
||||
{
|
||||
detected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!detected) return;
|
||||
}
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
if (anim == null) return;
|
||||
|
||||
if (id == null || id == '') id = this.currentAnimation;
|
||||
|
||||
if (this.currentAnimation == id && !restart)
|
||||
{
|
||||
if (!anim.isPlaying)
|
||||
{
|
||||
if (fr != null) anim.curFrame = fr.index + startFrame;
|
||||
else
|
||||
anim.curFrame = startFrame;
|
||||
|
||||
// Resume animation if it's paused.
|
||||
anim.resume();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else if (!hasAnimation(id))
|
||||
{
|
||||
// Skip if the animation doesn't exist
|
||||
trace('Animation ' + id + ' not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentAnimation = id;
|
||||
anim.onComplete.removeAll();
|
||||
anim.onComplete.add(function() {
|
||||
_onAnimationComplete();
|
||||
});
|
||||
|
||||
looping = loop;
|
||||
|
||||
// Prevent other animations from playing if `ignoreOther` is true.
|
||||
if (ignoreOther) canPlayOtherAnims = false;
|
||||
|
||||
// Move to the first frame of the animation.
|
||||
// goToFrameLabel(id);
|
||||
// trace('Playing animation $id');
|
||||
if ((id == null || id == "") || this.anim.symbolDictionary.exists(id) || (this.anim.getByName(id) != null))
|
||||
{
|
||||
this.anim.play(id, restart, false, startFrame);
|
||||
|
||||
this.currentAnimation = anim.curSymbol.name;
|
||||
|
||||
fr = null;
|
||||
}
|
||||
var frameLabelNames = getFrameLabelNames();
|
||||
// Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
|
||||
if (frameLabelNames != null && frameLabelNames.indexOf(id) != -1)
|
||||
{
|
||||
goToFrameLabel(id);
|
||||
fr = anim.getFrameLabel(id);
|
||||
anim.curFrame += startFrame;
|
||||
// Resume animation if it's paused.
|
||||
anim.resume();
|
||||
}
|
||||
}
|
||||
|
||||
override public function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the animation has finished playing.
|
||||
* @return Whether the animation has finished playing.
|
||||
*/
|
||||
public function isAnimationFinished():Bool
|
||||
{
|
||||
return isLoopComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the animation has reached the last frame.
|
||||
* Can be true even if animation is configured to loop.
|
||||
* @return Whether the animation has reached the last frame.
|
||||
*/
|
||||
public function isLoopComplete():Bool
|
||||
{
|
||||
if (this.anim == null) return false;
|
||||
if (!this.anim.isPlaying) return false;
|
||||
|
||||
if (fr != null)
|
||||
{
|
||||
var curFrame = anim.curFrame;
|
||||
|
||||
var startFrame = fr.index;
|
||||
var endFrame = (fr.index + fr.duration);
|
||||
|
||||
return (anim.reversed) ? (curFrame < startFrame) : (curFrame >= endFrame);
|
||||
}
|
||||
|
||||
return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the current animation.
|
||||
*/
|
||||
public function stopAnimation():Void
|
||||
{
|
||||
if (this.currentAnimation == null) return;
|
||||
|
||||
this.anim.removeAllCallbacksFrom(getNextFrameLabel(this.currentAnimation));
|
||||
|
||||
goToFrameIndex(0);
|
||||
}
|
||||
|
||||
function addFrameCallback(label:String, callback:Void->Void):Void
|
||||
{
|
||||
var frameLabel = this.anim.getFrameLabel(label);
|
||||
frameLabel.add(callback);
|
||||
}
|
||||
|
||||
function goToFrameLabel(label:String):Void
|
||||
{
|
||||
this.anim.goToFrameLabel(label);
|
||||
}
|
||||
|
||||
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Null<Array<String>>
|
||||
{
|
||||
var labels = this.anim.getFrameLabels(layer);
|
||||
var array = [];
|
||||
for (label in labels)
|
||||
{
|
||||
array.push(label.name);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
function getNextFrameLabel(label:String):String
|
||||
{
|
||||
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
|
||||
}
|
||||
|
||||
function getLabelIndex(label:String):Int
|
||||
{
|
||||
return listAnimations().indexOf(label);
|
||||
}
|
||||
|
||||
function goToFrameIndex(index:Int):Void
|
||||
{
|
||||
this.anim.curFrame = index;
|
||||
}
|
||||
|
||||
public function cleanupAnimation(_:String):Void
|
||||
{
|
||||
canPlayOtherAnims = true;
|
||||
// this.currentAnimation = null;
|
||||
this.anim.pause();
|
||||
}
|
||||
|
||||
function _onAnimationFrame(frame:Int):Void
|
||||
{
|
||||
if (currentAnimation != null)
|
||||
{
|
||||
onAnimationFrame.dispatch(currentAnimation, frame);
|
||||
|
||||
if (isLoopComplete())
|
||||
{
|
||||
anim.pause();
|
||||
|
||||
if (looping)
|
||||
{
|
||||
anim.curFrame = (fr != null) ? fr.index : 0;
|
||||
anim.resume();
|
||||
_onAnimationLoop();
|
||||
}
|
||||
else if (fr != null && anim.curFrame != anim.length - 1)
|
||||
{
|
||||
anim.curFrame--;
|
||||
_onAnimationComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _onAnimationComplete():Void
|
||||
{
|
||||
if (currentAnimation != null)
|
||||
{
|
||||
onAnimationComplete.dispatch(currentAnimation);
|
||||
}
|
||||
else
|
||||
{
|
||||
onAnimationComplete.dispatch('');
|
||||
}
|
||||
}
|
||||
|
||||
function _onAnimationLoop():Void
|
||||
{
|
||||
if (currentAnimation != null)
|
||||
{
|
||||
onAnimationLoop.dispatch(currentAnimation);
|
||||
}
|
||||
else
|
||||
{
|
||||
onAnimationLoop.dispatch('');
|
||||
}
|
||||
}
|
||||
|
||||
var prevFrames:Map<Int, FlxFrame> = [];
|
||||
|
||||
public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void
|
||||
{
|
||||
var cond = false;
|
||||
|
||||
if (graphic == null) cond = true;
|
||||
else
|
||||
{
|
||||
if ((graphic is String)) cond = !Assets.exists(graphic)
|
||||
else
|
||||
cond = false;
|
||||
}
|
||||
if (cond)
|
||||
{
|
||||
var prevFrame:Null<FlxFrame> = prevFrames.get(index);
|
||||
if (prevFrame == null) return;
|
||||
|
||||
prevFrame.copyTo(frames.getByIndex(index));
|
||||
return;
|
||||
}
|
||||
|
||||
var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo();
|
||||
prevFrames.set(index, prevFrame);
|
||||
|
||||
@:nullSafety(Off) // TODO: Remove this once flixel.system.frontEnds.BitmapFrontEnd has been null safed
|
||||
var frame = FlxG.bitmap.add(graphic).imageFrame.frame;
|
||||
frame.copyTo(frames.getByIndex(index));
|
||||
|
||||
// Additional sizing fix.
|
||||
@:privateAccess
|
||||
if (true)
|
||||
{
|
||||
var frame = frames.getByIndex(index);
|
||||
frame.tileMatrix.a = prevFrame.frame.width / frame.frame.width;
|
||||
frame.tileMatrix.d = prevFrame.frame.height / frame.frame.height;
|
||||
}
|
||||
}
|
||||
|
||||
public function getBasePosition():Null<FlxPoint>
|
||||
{
|
||||
// var stagePos = new FlxPoint(anim.stageInstance.matrix.tx, anim.stageInstance.matrix.ty);
|
||||
var instancePos = new FlxPoint(anim.curInstance.matrix.tx, anim.curInstance.matrix.ty);
|
||||
var firstElement = anim.curSymbol.timeline?.get(0)?.get(0)?.get(0);
|
||||
if (firstElement == null) return instancePos;
|
||||
var firstElementPos = new FlxPoint(firstElement.matrix.tx, firstElement.matrix.ty);
|
||||
|
||||
return instancePos + firstElementPos;
|
||||
}
|
||||
|
||||
public function getPivotPosition():Null<FlxPoint>
|
||||
{
|
||||
return anim.curInstance.symbol.transformationPoint;
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
for (prevFrameId in prevFrames.keys())
|
||||
{
|
||||
replaceFrameGraphic(prevFrameId, null);
|
||||
}
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,11 @@ class MeshRender extends FlxStrip
|
|||
add_tri(a, c, d);
|
||||
}
|
||||
|
||||
public override function draw():Void
|
||||
{
|
||||
super.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a quad from four points.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import funkin.Paths;
|
|||
import funkin.Preferences;
|
||||
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
|
||||
import flixel.system.debug.watch.Tracker;
|
||||
import haxe.ds.Option;
|
||||
|
||||
// These are great.
|
||||
using Lambda;
|
||||
|
|
@ -23,4 +24,5 @@ using funkin.util.tools.MapTools;
|
|||
using funkin.util.tools.SongEventDataArrayTools;
|
||||
using funkin.util.tools.SongNoteDataArrayTools;
|
||||
using funkin.util.tools.StringTools;
|
||||
using funkin.util.AnsiUtil;
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -19,35 +19,20 @@ import flixel.math.FlxPoint;
|
|||
*/
|
||||
class Controls extends FlxActionSet
|
||||
{
|
||||
/**
|
||||
/*
|
||||
* A list of actions that a player would invoke via some input device.
|
||||
* Uses FlxActions to funnel various inputs to a single action.
|
||||
*/
|
||||
var _ui_up = new FunkinAction(Action.UI_UP);
|
||||
|
||||
var _ui_left = new FunkinAction(Action.UI_LEFT);
|
||||
var _ui_right = new FunkinAction(Action.UI_RIGHT);
|
||||
var _ui_down = new FunkinAction(Action.UI_DOWN);
|
||||
var _ui_upP = new FunkinAction(Action.UI_UP_P);
|
||||
var _ui_leftP = new FunkinAction(Action.UI_LEFT_P);
|
||||
var _ui_rightP = new FunkinAction(Action.UI_RIGHT_P);
|
||||
var _ui_downP = new FunkinAction(Action.UI_DOWN_P);
|
||||
var _ui_upR = new FunkinAction(Action.UI_UP_R);
|
||||
var _ui_leftR = new FunkinAction(Action.UI_LEFT_R);
|
||||
var _ui_rightR = new FunkinAction(Action.UI_RIGHT_R);
|
||||
var _ui_downR = new FunkinAction(Action.UI_DOWN_R);
|
||||
|
||||
var _note_up = new FunkinAction(Action.NOTE_UP);
|
||||
var _note_left = new FunkinAction(Action.NOTE_LEFT);
|
||||
var _note_right = new FunkinAction(Action.NOTE_RIGHT);
|
||||
var _note_down = new FunkinAction(Action.NOTE_DOWN);
|
||||
var _note_upP = new FunkinAction(Action.NOTE_UP_P);
|
||||
var _note_leftP = new FunkinAction(Action.NOTE_LEFT_P);
|
||||
var _note_rightP = new FunkinAction(Action.NOTE_RIGHT_P);
|
||||
var _note_downP = new FunkinAction(Action.NOTE_DOWN_P);
|
||||
var _note_upR = new FunkinAction(Action.NOTE_UP_R);
|
||||
var _note_leftR = new FunkinAction(Action.NOTE_LEFT_R);
|
||||
var _note_rightR = new FunkinAction(Action.NOTE_RIGHT_R);
|
||||
var _note_downR = new FunkinAction(Action.NOTE_DOWN_R);
|
||||
|
||||
var _accept = new FunkinAction(Action.ACCEPT);
|
||||
var _back = new FunkinAction(Action.BACK);
|
||||
var _pause = new FunkinAction(Action.PAUSE);
|
||||
|
|
@ -142,26 +127,6 @@ class Controls extends FlxActionSet
|
|||
inline function get_UI_DOWN_R()
|
||||
return _ui_down.checkJustReleased();
|
||||
|
||||
public var UI_UP_GAMEPAD(get, never):Bool;
|
||||
|
||||
inline function get_UI_UP_GAMEPAD()
|
||||
return _ui_up.checkPressedGamepad();
|
||||
|
||||
public var UI_LEFT_GAMEPAD(get, never):Bool;
|
||||
|
||||
inline function get_UI_LEFT_GAMEPAD()
|
||||
return _ui_left.checkPressedGamepad();
|
||||
|
||||
public var UI_RIGHT_GAMEPAD(get, never):Bool;
|
||||
|
||||
inline function get_UI_RIGHT_GAMEPAD()
|
||||
return _ui_right.checkPressedGamepad();
|
||||
|
||||
public var UI_DOWN_GAMEPAD(get, never):Bool;
|
||||
|
||||
inline function get_UI_DOWN_GAMEPAD()
|
||||
return _ui_down.checkPressedGamepad();
|
||||
|
||||
public var NOTE_UP(get, never):Bool;
|
||||
|
||||
inline function get_NOTE_UP()
|
||||
|
|
@ -225,22 +190,62 @@ class Controls extends FlxActionSet
|
|||
public var ACCEPT(get, never):Bool;
|
||||
|
||||
inline function get_ACCEPT()
|
||||
return _accept.check();
|
||||
return _accept.checkPressed();
|
||||
|
||||
public var ACCEPT_P(get, never):Bool;
|
||||
|
||||
inline function get_ACCEPT_P()
|
||||
return _accept.checkJustPressed();
|
||||
|
||||
public var ACCEPT_R(get, never):Bool;
|
||||
|
||||
inline function get_ACCEPT_R()
|
||||
return _accept.checkJustReleased();
|
||||
|
||||
public var BACK(get, never):Bool;
|
||||
|
||||
inline function get_BACK()
|
||||
return _back.check();
|
||||
return _back.checkPressed();
|
||||
|
||||
public var BACK_P(get, never):Bool;
|
||||
|
||||
inline function get_BACK_P()
|
||||
return _back.checkJustPressed();
|
||||
|
||||
public var BACK_R(get, never):Bool;
|
||||
|
||||
inline function get_BACK_R()
|
||||
return _back.checkJustReleased();
|
||||
|
||||
public var PAUSE(get, never):Bool;
|
||||
|
||||
inline function get_PAUSE()
|
||||
return _pause.check();
|
||||
return _pause.checkPressed();
|
||||
|
||||
public var PAUSE_P(get, never):Bool;
|
||||
|
||||
inline function get_PAUSE_P()
|
||||
return _pause.checkJustPressed();
|
||||
|
||||
public var PAUSE_R(get, never):Bool;
|
||||
|
||||
inline function get_PAUSE_R()
|
||||
return _pause.checkJustReleased();
|
||||
|
||||
public var RESET(get, never):Bool;
|
||||
|
||||
inline function get_RESET()
|
||||
return _reset.check();
|
||||
return _reset.checkPressed();
|
||||
|
||||
public var RESET_P(get, never):Bool;
|
||||
|
||||
inline function get_RESET_P()
|
||||
return _reset.checkJustPressed();
|
||||
|
||||
public var RESET_R(get, never):Bool;
|
||||
|
||||
inline function get_RESET_R()
|
||||
return _reset.checkJustReleased();
|
||||
|
||||
public var WINDOW_FULLSCREEN(get, never):Bool;
|
||||
|
||||
|
|
@ -503,9 +508,8 @@ class Controls extends FlxActionSet
|
|||
* Calls a function passing each action bound by the specified control
|
||||
* @param control
|
||||
* @param func
|
||||
* @return ->Void)
|
||||
*/
|
||||
function forEachBound(control:Control, func:FlxActionDigital->FlxInputState->Void)
|
||||
function forEachBound(control:Control, func:FunkinAction->FlxInputState->Void):Void
|
||||
{
|
||||
switch (control)
|
||||
{
|
||||
|
|
@ -542,13 +546,21 @@ class Controls extends FlxActionSet
|
|||
func(_note_down, JUST_PRESSED);
|
||||
func(_note_down, JUST_RELEASED);
|
||||
case ACCEPT:
|
||||
func(_accept, PRESSED);
|
||||
func(_accept, JUST_PRESSED);
|
||||
func(_accept, JUST_RELEASED);
|
||||
case BACK:
|
||||
func(_back, PRESSED);
|
||||
func(_back, JUST_PRESSED);
|
||||
func(_back, JUST_RELEASED);
|
||||
case PAUSE:
|
||||
func(_pause, PRESSED);
|
||||
func(_pause, JUST_PRESSED);
|
||||
func(_pause, JUST_RELEASED);
|
||||
case RESET:
|
||||
func(_reset, PRESSED);
|
||||
func(_reset, JUST_PRESSED);
|
||||
func(_reset, JUST_RELEASED);
|
||||
#if FEATURE_SCREENSHOTS
|
||||
case WINDOW_SCREENSHOT:
|
||||
func(_window_screenshot, JUST_PRESSED);
|
||||
|
|
@ -592,7 +604,7 @@ class Controls extends FlxActionSet
|
|||
}
|
||||
}
|
||||
|
||||
public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int)
|
||||
public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int):Void
|
||||
{
|
||||
if (toAdd == toRemove) return;
|
||||
|
||||
|
|
@ -606,7 +618,7 @@ class Controls extends FlxActionSet
|
|||
}
|
||||
}
|
||||
|
||||
function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState)
|
||||
function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState):Void
|
||||
{
|
||||
if (action.inputs.length == 0)
|
||||
{
|
||||
|
|
@ -657,7 +669,7 @@ class Controls extends FlxActionSet
|
|||
}
|
||||
}
|
||||
|
||||
function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState)
|
||||
function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState):Void
|
||||
{
|
||||
if (action.inputs.length == 0)
|
||||
{
|
||||
|
|
@ -685,7 +697,7 @@ class Controls extends FlxActionSet
|
|||
}
|
||||
}
|
||||
|
||||
public function copyFrom(controls:Controls, ?device:Device)
|
||||
public function copyFrom(controls:Controls, ?device:Device):Void
|
||||
{
|
||||
for (name in controls.byName.keys())
|
||||
{
|
||||
|
|
@ -712,7 +724,7 @@ class Controls extends FlxActionSet
|
|||
}
|
||||
}
|
||||
|
||||
inline public function copyTo(controls:Controls, ?device:Device)
|
||||
inline public function copyTo(controls:Controls, ?device:Device):Void
|
||||
{
|
||||
controls.copyFrom(this, device);
|
||||
}
|
||||
|
|
@ -735,7 +747,7 @@ class Controls extends FlxActionSet
|
|||
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
|
||||
* If binder is a literal you can inline this
|
||||
*/
|
||||
public function bindKeys(control:Control, keys:Array<FlxKey>)
|
||||
public function bindKeys(control:Control, keys:Array<FlxKey>):Void
|
||||
{
|
||||
forEachBound(control, function(action, state) addKeys(action, keys, state));
|
||||
}
|
||||
|
|
@ -744,12 +756,12 @@ class Controls extends FlxActionSet
|
|||
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
|
||||
* If binder is a literal you can inline this
|
||||
*/
|
||||
public function unbindKeys(control:Control, keys:Array<FlxKey>)
|
||||
public function unbindKeys(control:Control, keys:Array<FlxKey>):Void
|
||||
{
|
||||
forEachBound(control, function(action, _) removeKeys(action, keys));
|
||||
}
|
||||
|
||||
static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState)
|
||||
static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState):Void
|
||||
{
|
||||
for (key in keys)
|
||||
{
|
||||
|
|
@ -758,7 +770,7 @@ class Controls extends FlxActionSet
|
|||
}
|
||||
}
|
||||
|
||||
static function removeKeys(action:FlxActionDigital, keys:Array<FlxKey>)
|
||||
static function removeKeys(action:FlxActionDigital, keys:Array<FlxKey>):Void
|
||||
{
|
||||
var i = action.inputs.length;
|
||||
while (i-- > 0)
|
||||
|
|
@ -919,7 +931,7 @@ class Controls extends FlxActionSet
|
|||
return [];
|
||||
}
|
||||
|
||||
function removeKeyboard()
|
||||
function removeKeyboard():Void
|
||||
{
|
||||
for (action in this.digitalActions)
|
||||
{
|
||||
|
|
@ -939,16 +951,6 @@ class Controls extends FlxActionSet
|
|||
fromSaveData(padData, Gamepad(id));
|
||||
}
|
||||
|
||||
public function getGamepadIds():Array<Int>
|
||||
{
|
||||
return gamepadsAdded;
|
||||
}
|
||||
|
||||
public function getGamepads():Array<FlxGamepad>
|
||||
{
|
||||
return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
|
||||
}
|
||||
|
||||
inline function addGamepadLiteral(id:Int, ?buttonMap:Map<Control, Array<FlxGamepadInputID>>):Void
|
||||
{
|
||||
gamepadsAdded.push(id);
|
||||
|
|
@ -972,7 +974,7 @@ class Controls extends FlxActionSet
|
|||
gamepadsAdded.remove(deviceID);
|
||||
}
|
||||
|
||||
public function addDefaultGamepad(id):Void
|
||||
public function addDefaultGamepad(id:Int):Void
|
||||
{
|
||||
addGamepadLiteral(id, [
|
||||
Control.ACCEPT => getDefaultGamepadBinds(Control.ACCEPT),
|
||||
|
|
@ -1016,32 +1018,32 @@ class Controls extends FlxActionSet
|
|||
|
||||
function getDefaultGamepadBinds(control:Control):Array<FlxGamepadInputID>
|
||||
{
|
||||
switch (control)
|
||||
return switch (control)
|
||||
{
|
||||
case Control.ACCEPT:
|
||||
return [#if switch B #else A #end];
|
||||
[A];
|
||||
case Control.BACK:
|
||||
return [#if switch A #else B #end];
|
||||
[B];
|
||||
case Control.UI_UP:
|
||||
return [DPAD_UP, LEFT_STICK_DIGITAL_UP];
|
||||
[DPAD_UP, LEFT_STICK_DIGITAL_UP];
|
||||
case Control.UI_DOWN:
|
||||
return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
|
||||
[DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
|
||||
case Control.UI_LEFT:
|
||||
return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
|
||||
[DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
|
||||
case Control.UI_RIGHT:
|
||||
return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
|
||||
[DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
|
||||
case Control.NOTE_UP:
|
||||
return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
|
||||
[DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
|
||||
case Control.NOTE_DOWN:
|
||||
return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
|
||||
[DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
|
||||
case Control.NOTE_LEFT:
|
||||
return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
|
||||
[DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
|
||||
case Control.NOTE_RIGHT:
|
||||
return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
|
||||
[DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
|
||||
case Control.PAUSE:
|
||||
return [START];
|
||||
[START];
|
||||
case Control.RESET:
|
||||
return [FlxGamepadInputID.BACK]; // Back (i.e. Select)
|
||||
[FlxGamepadInputID.BACK]; // Back (i.e. Select)
|
||||
case Control.WINDOW_FULLSCREEN:
|
||||
[];
|
||||
#if FEATURE_SCREENSHOTS
|
||||
|
|
@ -1049,19 +1051,19 @@ class Controls extends FlxActionSet
|
|||
[];
|
||||
#end
|
||||
case Control.CUTSCENE_ADVANCE:
|
||||
return [A];
|
||||
[A];
|
||||
case Control.FREEPLAY_FAVORITE:
|
||||
return [Y]; // Back (i.e. Select)
|
||||
[Y]; // Back (i.e. Select)
|
||||
case Control.FREEPLAY_LEFT:
|
||||
return [LEFT_SHOULDER];
|
||||
[LEFT_SHOULDER];
|
||||
case Control.FREEPLAY_RIGHT:
|
||||
return [RIGHT_SHOULDER];
|
||||
[RIGHT_SHOULDER];
|
||||
case Control.FREEPLAY_CHAR_SELECT:
|
||||
return [X];
|
||||
[X];
|
||||
case Control.FREEPLAY_JUMP_TO_TOP:
|
||||
return [RIGHT_STICK_DIGITAL_UP];
|
||||
[RIGHT_STICK_DIGITAL_UP];
|
||||
case Control.FREEPLAY_JUMP_TO_BOTTOM:
|
||||
return [RIGHT_STICK_DIGITAL_DOWN];
|
||||
[RIGHT_STICK_DIGITAL_DOWN];
|
||||
case Control.VOLUME_UP:
|
||||
[];
|
||||
case Control.VOLUME_DOWN:
|
||||
|
|
@ -1083,16 +1085,15 @@ class Controls extends FlxActionSet
|
|||
case Control.DEBUG_DISPLAY:
|
||||
[];
|
||||
default:
|
||||
// Fallthrough.
|
||||
[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
|
||||
* If binder is a literal you can inline this
|
||||
*/
|
||||
public function bindButtons(control:Control, id, buttons)
|
||||
public function bindButtons(control:Control, id:Int, buttons):Void
|
||||
{
|
||||
forEachBound(control, function(action, state) addButtons(action, buttons, state, id));
|
||||
}
|
||||
|
|
@ -1101,12 +1102,12 @@ class Controls extends FlxActionSet
|
|||
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
|
||||
* If binder is a literal you can inline this
|
||||
*/
|
||||
public function unbindButtons(control:Control, gamepadID:Int, buttons)
|
||||
public function unbindButtons(control:Control, gamepadID:Int, buttons):Void
|
||||
{
|
||||
forEachBound(control, function(action, _) removeButtons(action, gamepadID, buttons));
|
||||
}
|
||||
|
||||
inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id)
|
||||
inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id:Int):Void
|
||||
{
|
||||
for (button in buttons)
|
||||
{
|
||||
|
|
@ -1115,7 +1116,7 @@ class Controls extends FlxActionSet
|
|||
}
|
||||
}
|
||||
|
||||
static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array<FlxGamepadInputID>)
|
||||
static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array<FlxGamepadInputID>):Void
|
||||
{
|
||||
var i = action.inputs.length;
|
||||
while (i-- > 0)
|
||||
|
|
@ -1145,17 +1146,6 @@ class Controls extends FlxActionSet
|
|||
return list;
|
||||
}
|
||||
|
||||
public function removeDevice(device:Device)
|
||||
{
|
||||
switch (device)
|
||||
{
|
||||
case Keys:
|
||||
setKeyboardScheme(None);
|
||||
case Gamepad(id):
|
||||
removeGamepad(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: When loading controls:
|
||||
* An EMPTY array means the control is uninitialized and needs to be reset to default.
|
||||
|
|
@ -1238,7 +1228,7 @@ class Controls extends FlxActionSet
|
|||
return isEmpty ? null : data;
|
||||
}
|
||||
|
||||
static function isDevice(input:FlxActionInput, device:Device)
|
||||
static function isDevice(input:FlxActionInput, device:Device):Bool
|
||||
{
|
||||
return switch (device)
|
||||
{
|
||||
|
|
@ -1247,7 +1237,7 @@ class Controls extends FlxActionSet
|
|||
}
|
||||
}
|
||||
|
||||
inline static function isGamepad(input:FlxActionInput, deviceID:Int)
|
||||
inline static function isGamepad(input:FlxActionInput, deviceID:Int):Bool
|
||||
{
|
||||
return input.device == GAMEPAD && (deviceID == FlxInputDeviceID.ALL || input.deviceID == deviceID);
|
||||
}
|
||||
|
|
@ -1361,14 +1351,8 @@ class FunkinAction extends FlxActionDigital
|
|||
|
||||
public function checkMultiFiltered(?filterTriggers:Array<FlxInputState>, ?filterDevices:Array<FlxInputDevice>):Bool
|
||||
{
|
||||
if (filterTriggers == null)
|
||||
{
|
||||
filterTriggers = [PRESSED, JUST_PRESSED];
|
||||
}
|
||||
if (filterDevices == null)
|
||||
{
|
||||
filterDevices = [];
|
||||
}
|
||||
filterTriggers ??= [PRESSED, JUST_PRESSED];
|
||||
filterDevices ??= [];
|
||||
|
||||
// Perform checkFiltered for each combination.
|
||||
for (i in filterTriggers)
|
||||
|
|
@ -1399,11 +1383,10 @@ class FunkinAction extends FlxActionDigital
|
|||
* @param action The action to check for.
|
||||
* @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`).
|
||||
* @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`).
|
||||
* @return bool if our input has been triggered
|
||||
*/
|
||||
public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool
|
||||
{
|
||||
// The normal
|
||||
|
||||
// Make sure we only update the inputs once per frame.
|
||||
var key = '${filterTrigger}:${filterDevice}';
|
||||
var cacheEntry = cache.get(key);
|
||||
|
|
@ -1412,20 +1395,22 @@ class FunkinAction extends FlxActionDigital
|
|||
{
|
||||
return cacheEntry.value;
|
||||
}
|
||||
// Use a for loop instead so we can remove inputs while iterating.
|
||||
|
||||
// We don't return early because we need to call check() on ALL inputs.
|
||||
var result = false;
|
||||
var len = inputs != null ? inputs.length : 0;
|
||||
for (i in 0...len)
|
||||
_x = null;
|
||||
_y = null;
|
||||
|
||||
_timestamp = FlxG.game.ticks;
|
||||
triggered = false;
|
||||
|
||||
var i = inputs?.length ?? 0;
|
||||
while (i-- > 0) // Iterate backwards, since we may remove items
|
||||
{
|
||||
var j = len - i - 1;
|
||||
var input = inputs[j];
|
||||
var input = inputs[i];
|
||||
|
||||
// Filter out dead inputs.
|
||||
if (input.destroyed)
|
||||
{
|
||||
inputs.splice(j, 1);
|
||||
inputs.remove(input);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1447,14 +1432,13 @@ class FunkinAction extends FlxActionDigital
|
|||
// Check whether the input has triggered.
|
||||
if (input.check(this))
|
||||
{
|
||||
result = true;
|
||||
triggered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to cache this result.
|
||||
cache.set(key, {timestamp: FlxG.game.ticks, value: result});
|
||||
cache.set(key, {timestamp: FlxG.game.ticks, value: triggered});
|
||||
|
||||
return result;
|
||||
return triggered;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1475,10 +1459,10 @@ enum Control
|
|||
UI_DOWN;
|
||||
UI_UP;
|
||||
UI_RIGHT;
|
||||
RESET;
|
||||
ACCEPT;
|
||||
BACK;
|
||||
PAUSE;
|
||||
RESET;
|
||||
// CUTSCENE
|
||||
CUTSCENE_ADVANCE;
|
||||
// FREEPLAY
|
||||
|
|
@ -1509,27 +1493,11 @@ enum abstract Action(String) to String from String
|
|||
var NOTE_LEFT = "note_left";
|
||||
var NOTE_RIGHT = "note_right";
|
||||
var NOTE_DOWN = "note_down";
|
||||
var NOTE_UP_P = "note_up-press";
|
||||
var NOTE_LEFT_P = "note_left-press";
|
||||
var NOTE_RIGHT_P = "note_right-press";
|
||||
var NOTE_DOWN_P = "note_down-press";
|
||||
var NOTE_UP_R = "note_up-release";
|
||||
var NOTE_LEFT_R = "note_left-release";
|
||||
var NOTE_RIGHT_R = "note_right-release";
|
||||
var NOTE_DOWN_R = "note_down-release";
|
||||
// UI
|
||||
var UI_UP = "ui_up";
|
||||
var UI_LEFT = "ui_left";
|
||||
var UI_RIGHT = "ui_right";
|
||||
var UI_DOWN = "ui_down";
|
||||
var UI_UP_P = "ui_up-press";
|
||||
var UI_LEFT_P = "ui_left-press";
|
||||
var UI_RIGHT_P = "ui_right-press";
|
||||
var UI_DOWN_P = "ui_down-press";
|
||||
var UI_UP_R = "ui_up-release";
|
||||
var UI_LEFT_R = "ui_left-release";
|
||||
var UI_RIGHT_R = "ui_right-release";
|
||||
var UI_DOWN_R = "ui_down-release";
|
||||
var ACCEPT = "accept";
|
||||
var BACK = "back";
|
||||
var PAUSE = "pause";
|
||||
|
|
|
|||
|
|
@ -303,7 +303,8 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
|||
onInputPressed.dispatch(
|
||||
{
|
||||
noteDirection: getDirectionForKey(key),
|
||||
timestamp: timestamp
|
||||
timestamp: timestamp,
|
||||
keyCode: keyCode
|
||||
});
|
||||
_dirPressTimestamps.set(getDirectionForKey(key), timestamp);
|
||||
}
|
||||
|
|
@ -325,7 +326,8 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
|||
onInputReleased.dispatch(
|
||||
{
|
||||
noteDirection: getDirectionForKey(key),
|
||||
timestamp: timestamp
|
||||
timestamp: timestamp,
|
||||
keyCode: keyCode
|
||||
});
|
||||
_dirReleaseTimestamps.set(getDirectionForKey(key), timestamp);
|
||||
}
|
||||
|
|
@ -349,7 +351,8 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
|||
onInputPressed.dispatch(
|
||||
{
|
||||
noteDirection: getDirectionForButton(gamepad, buttonId),
|
||||
timestamp: timestamp
|
||||
timestamp: timestamp,
|
||||
keyCode: button // implicit cast to int
|
||||
});
|
||||
_dirPressTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
|
||||
}
|
||||
|
|
@ -373,7 +376,8 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
|||
onInputReleased.dispatch(
|
||||
{
|
||||
noteDirection: getDirectionForButton(gamepad, buttonId),
|
||||
timestamp: timestamp
|
||||
timestamp: timestamp,
|
||||
keyCode: button // implicit cast to int
|
||||
});
|
||||
_dirReleaseTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
|
||||
}
|
||||
|
|
@ -491,4 +495,10 @@ typedef PreciseInputEvent =
|
|||
* The timestamp of the input. Measured in nanoseconds.
|
||||
*/
|
||||
timestamp:Int64,
|
||||
|
||||
/**
|
||||
* The key that was used for the input.
|
||||
* Used to distinguish between multiple inputs for the same direction.
|
||||
*/
|
||||
keyCode:Int
|
||||
};
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class TurboButtonHandler extends FlxBasic
|
|||
function get_allPressed():Bool
|
||||
{
|
||||
if (targetGamepad == null) return false;
|
||||
if (!targetGamepad.connected) return false;
|
||||
if (inputs == null || inputs.length == 0) return false;
|
||||
if (inputs.length == 1) return targetGamepad.anyPressed(inputs);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import funkin.mobile.ui.FunkinHitbox;
|
|||
import funkin.play.notes.NoteDirection;
|
||||
import openfl.events.KeyboardEvent;
|
||||
import openfl.events.TouchEvent;
|
||||
#if android
|
||||
import funkin.external.android.KeyboardUtil;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Handles setting up and managing input controls for the game.
|
||||
|
|
@ -125,7 +128,7 @@ class ControlsHandler
|
|||
@:noCompletion
|
||||
private static function get_hasExternalInputDevice():Bool
|
||||
{
|
||||
return FlxG.gamepads.numActiveGamepads > 0 #if android || extension.androidtools.Tools.isChromebook() #end;
|
||||
return FlxG.gamepads.numActiveGamepads > 0 #if android || KeyboardUtil.keyboardConnected || extension.androidtools.Tools.isChromebook() #end;
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class PreciseInputHandler
|
|||
@:privateAccess
|
||||
if (hint.input?.justPressed ?? false)
|
||||
{
|
||||
PreciseInputManager.instance.onInputPressed.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp});
|
||||
PreciseInputManager.instance.onInputPressed.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp, keyCode: 0});
|
||||
PreciseInputManager.instance._dirPressTimestamps.set(hint.noteDirection, timestamp);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ class PreciseInputHandler
|
|||
@:privateAccess
|
||||
if (hint.input?.justReleased ?? false)
|
||||
{
|
||||
PreciseInputManager.instance.onInputReleased.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp});
|
||||
PreciseInputManager.instance.onInputReleased.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp, keyCode: 0});
|
||||
PreciseInputManager.instance._dirPressTimestamps.set(hint.noteDirection, timestamp);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,16 +63,16 @@ class PolymodErrorHandler
|
|||
|
||||
static function logInfo(message:String):Void
|
||||
{
|
||||
trace('[INFO-] ${message}');
|
||||
trace(' INFO '.bg_blue().bold() + ' ${message}');
|
||||
}
|
||||
|
||||
static function logError(message:String):Void
|
||||
{
|
||||
trace('[ERROR] ${message}');
|
||||
trace(' ERROR '.bg_red().bold() + ' ${message}');
|
||||
}
|
||||
|
||||
static function logWarn(message:String):Void
|
||||
{
|
||||
trace('[WARN-] ${message}');
|
||||
trace(' WARNING '.bg_yellow().bold() + ' ${message}');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class PolymodHandler
|
|||
createModRoot();
|
||||
#end
|
||||
trace('Initializing Polymod (using configured mods)...');
|
||||
loadModsById(Save.instance.enabledModIds);
|
||||
loadModsById(Save.instance.enabledModIds.value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -194,7 +194,7 @@ class PolymodHandler
|
|||
loadedModIds = [];
|
||||
for (mod in loadedModList)
|
||||
{
|
||||
trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
|
||||
trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
|
||||
loadedModIds.push(mod.id);
|
||||
}
|
||||
|
||||
|
|
@ -203,35 +203,35 @@ class PolymodHandler
|
|||
trace('Installed mods have replaced ${fileList.length} images.');
|
||||
for (item in fileList)
|
||||
{
|
||||
trace(' * $item');
|
||||
trace(' * $item');
|
||||
}
|
||||
|
||||
fileList = Polymod.listModFiles(PolymodAssetType.TEXT);
|
||||
trace('Installed mods have added/replaced ${fileList.length} text files.');
|
||||
for (item in fileList)
|
||||
{
|
||||
trace(' * $item');
|
||||
trace(' * $item');
|
||||
}
|
||||
|
||||
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC);
|
||||
trace('Installed mods have replaced ${fileList.length} music files.');
|
||||
for (item in fileList)
|
||||
{
|
||||
trace(' * $item');
|
||||
trace(' * $item');
|
||||
}
|
||||
|
||||
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND);
|
||||
trace('Installed mods have replaced ${fileList.length} sound files.');
|
||||
for (item in fileList)
|
||||
{
|
||||
trace(' * $item');
|
||||
trace(' * $item');
|
||||
}
|
||||
|
||||
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC);
|
||||
trace('Installed mods have replaced ${fileList.length} generic audio files.');
|
||||
for (item in fileList)
|
||||
{
|
||||
trace(' * $item');
|
||||
trace(' * $item');
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
|
@ -274,6 +274,11 @@ class PolymodHandler
|
|||
Polymod.addImportAlias('funkin.play.character.CharacterDataParser', funkin.data.character.CharacterData.CharacterDataParser);
|
||||
Polymod.addImportAlias('funkin.play.character.CharacterData.CharacterDataParser', funkin.data.character.CharacterData.CharacterDataParser);
|
||||
|
||||
// `FlxAtlasSprite` was merged into `FunkinSprite` and then removed.
|
||||
// We add the import alias here so mods don't error out as much.
|
||||
Polymod.addImportAlias('funkin.graphics.adobeanimate.FlxAtlasSprite', funkin.graphics.FunkinSprite);
|
||||
Polymod.addImportAlias('funkin.modding.base.ScriptedFlxAtlasSprite', funkin.graphics.ScriptedFunkinSprite);
|
||||
|
||||
// `funkin.util.FileUtil` has unrestricted access to the file system.
|
||||
Polymod.addImportAlias('funkin.util.FileUtil', funkin.util.FileUtilSandboxed);
|
||||
|
||||
|
|
@ -542,7 +547,7 @@ class PolymodHandler
|
|||
*/
|
||||
public static function getEnabledMods():Array<ModMetadata>
|
||||
{
|
||||
var modIds:Array<String> = Save.instance.enabledModIds;
|
||||
var modIds:Array<String> = Save.instance.enabledModIds.value;
|
||||
var modMetadata:Array<ModMetadata> = getAllMods();
|
||||
var enabledMods:Array<ModMetadata> = [];
|
||||
for (item in modMetadata)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
/**
|
||||
* A script that can be tied to an FlxAtlasSprite
|
||||
* Create a scripted class that extends FlxAtlasSprite to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedFlxAtlasSprite extends funkin.graphics.adobeanimate.FlxAtlasSprite implements HScriptedClass {}
|
||||
|
|
@ -29,20 +29,20 @@ class ModuleHandler
|
|||
trace("[MODULEHANDLER] Loading module cache...");
|
||||
|
||||
var scriptedModuleClassNames:Array<String> = ScriptedModule.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedModuleClassNames.length} modules...');
|
||||
trace(' Instantiating ${scriptedModuleClassNames.length} modules...');
|
||||
for (moduleCls in scriptedModuleClassNames)
|
||||
{
|
||||
var module:Module = ScriptedModule.init(moduleCls, moduleCls);
|
||||
if (module != null)
|
||||
{
|
||||
trace(' Loaded module: ${moduleCls}');
|
||||
trace(' Loaded module: ${moduleCls}');
|
||||
|
||||
// Then store it.
|
||||
addToModuleCache(module);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate module: ${moduleCls}');
|
||||
trace(' Failed to instantiate module: ${moduleCls}');
|
||||
}
|
||||
}
|
||||
reorderModuleCache();
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
//
|
||||
|
||||
// Restart the level when pressing the assigned key.
|
||||
if ((controls.ACCEPT #if mobile || (TouchUtil.pressAction() && !TouchUtil.overlaps(backButton) && canInput) #end)
|
||||
if ((controls.ACCEPT_P #if mobile || (TouchUtil.pressAction() && !TouchUtil.overlaps(backButton) && canInput) #end)
|
||||
&& blueballed
|
||||
&& !mustNotExit)
|
||||
{
|
||||
|
|
@ -281,7 +281,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
confirmDeath();
|
||||
}
|
||||
|
||||
if (controls.BACK && !mustNotExit && !isEnding) goBack();
|
||||
if (controls.BACK_P && !mustNotExit && !isEnding) goBack();
|
||||
|
||||
if (gameOverMusic != null && gameOverMusic.playing)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -76,12 +76,13 @@ class GitarooPause extends MusicBeatState
|
|||
{
|
||||
if (controls.UI_LEFT_P || controls.UI_RIGHT_P #if mobile || SwipeUtil.justSwipedLeft || SwipeUtil.justSwipedRight #end) changeThing();
|
||||
|
||||
if (controls.ACCEPT #if mobile || checkSelectionPress() #end)
|
||||
if (controls.ACCEPT_P #if mobile || checkSelectionPress() #end)
|
||||
{
|
||||
if (replaySelect)
|
||||
{
|
||||
FlxTransitionableState.skipNextTransIn = false;
|
||||
FlxTransitionableState.skipNextTransOut = false;
|
||||
if (funkin.ui.FullScreenScaleMode.instance != null) funkin.ui.FullScreenScaleMode.instance.onMeasurePostAwait();
|
||||
FlxG.switchState(() -> new PlayState(previousParams));
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ typedef PauseSubStateParams =
|
|||
* Which mode to start in. Dictates what entries are displayed.
|
||||
*/
|
||||
?mode:PauseMode,
|
||||
|
||||
/**
|
||||
* Whether the game paused because the window lost focus.
|
||||
*/
|
||||
?lostFocus:Bool
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -151,6 +156,11 @@ class PauseSubState extends MusicBeatSubState
|
|||
*/
|
||||
var currentMode:PauseMode;
|
||||
|
||||
/**
|
||||
* Whether the game paused because the window lost focus.
|
||||
*/
|
||||
var lostFocus:Bool = false;
|
||||
|
||||
// ===============
|
||||
// Graphics Variables
|
||||
// ===============
|
||||
|
|
@ -231,6 +241,7 @@ class PauseSubState extends MusicBeatSubState
|
|||
{
|
||||
super();
|
||||
this.currentMode = params?.mode ?? Standard;
|
||||
this.lostFocus = params?.lostFocus ?? false;
|
||||
this.onPause = onPause;
|
||||
}
|
||||
|
||||
|
|
@ -256,6 +267,8 @@ class PauseSubState extends MusicBeatSubState
|
|||
|
||||
startPauseMusic();
|
||||
|
||||
if (lostFocus && Preferences.autoPause) pauseMusic.pause();
|
||||
|
||||
buildBackground();
|
||||
|
||||
buildMetadata();
|
||||
|
|
@ -338,7 +351,7 @@ class PauseSubState extends MusicBeatSubState
|
|||
function startPauseMusic():Void
|
||||
{
|
||||
var pauseMusicPath:String = Paths.music('breakfast$musicSuffix/breakfast$musicSuffix');
|
||||
pauseMusic = FunkinSound.load(pauseMusicPath, true, true);
|
||||
pauseMusic = FunkinSound.load(pauseMusicPath, 0, true, true);
|
||||
|
||||
if (pauseMusic == null)
|
||||
{
|
||||
|
|
@ -350,6 +363,24 @@ class PauseSubState extends MusicBeatSubState
|
|||
pauseMusic.fadeIn(MUSIC_FADE_IN_TIME, 0, MUSIC_FINAL_VOLUME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the game loses focus. Used to temporarily pause the sound.
|
||||
*/
|
||||
public override function onFocusLost():Void
|
||||
{
|
||||
super.onFocusLost();
|
||||
if (Preferences.autoPause) pauseMusic.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the game loses focus. Used to temporarily pause the sound.
|
||||
*/
|
||||
public override function onFocus():Void
|
||||
{
|
||||
super.onFocus();
|
||||
if (Preferences.autoPause) pauseMusic.resume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the semi-transparent black background.
|
||||
*/
|
||||
|
|
@ -581,66 +612,44 @@ class PauseSubState extends MusicBeatSubState
|
|||
{
|
||||
if (!allowInput) return;
|
||||
|
||||
// Doing this just so it'd look better i guess.
|
||||
final upP:Bool = controls.UI_UP_P;
|
||||
final downP:Bool = controls.UI_DOWN_P;
|
||||
// early return here if we are modifying our offsets stuff w/ shift + up/down
|
||||
if (handleModifyingOffsets()) return;
|
||||
|
||||
#if !mobile
|
||||
final up:Bool = controls.UI_UP;
|
||||
final down:Bool = controls.UI_DOWN;
|
||||
var offset:Int = Preferences.globalOffset ?? 0;
|
||||
if (FlxG.keys.pressed.SHIFT && (up || down))
|
||||
{
|
||||
lastOffsetPress += FlxG.elapsed;
|
||||
if (!fastOffset)
|
||||
{
|
||||
// If the last offset press was more than 0.5 seconds ago, reset the fast offset.
|
||||
if (lastOffsetPress > 0.5)
|
||||
{
|
||||
fastOffset = true;
|
||||
lastOffsetPress = 0;
|
||||
}
|
||||
handleDebugInputs();
|
||||
|
||||
if (upP || downP)
|
||||
{
|
||||
offset += (upP || up) ? 1 : -1;
|
||||
|
||||
offsetText.text = 'Global Offset: ${offset}ms';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
offset += (upP || up) ? 1 : -1;
|
||||
|
||||
offsetText.text = 'Global Offset: ${offset}ms';
|
||||
}
|
||||
|
||||
if (offset > 1500) offset = 1500;
|
||||
if (offset < -1500) offset = -1500;
|
||||
|
||||
Preferences.globalOffset = offset;
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reset the fast offset if the user is not holding SHIFT.
|
||||
fastOffset = false;
|
||||
lastOffsetPress = 0;
|
||||
}
|
||||
#end
|
||||
|
||||
if (upP)
|
||||
if (controls.UI_UP_P)
|
||||
{
|
||||
changeSelection(-1);
|
||||
}
|
||||
if (downP)
|
||||
if (controls.UI_DOWN_P)
|
||||
{
|
||||
changeSelection(1);
|
||||
}
|
||||
|
||||
// we only want justOpened to be true for 1 single frame, when we first get into the pause menu substate
|
||||
// we early return here so we don't need to check `if (!justOpened)` everywhere
|
||||
if (justOpened)
|
||||
{
|
||||
justOpened = false;
|
||||
return;
|
||||
}
|
||||
|
||||
handleTouchInputs();
|
||||
|
||||
if (controls.ACCEPT_P && currentMenuEntries.length > 0)
|
||||
{
|
||||
currentMenuEntries[currentEntry].callback(this);
|
||||
}
|
||||
else if (controls.PAUSE_P)
|
||||
{
|
||||
resume(this);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchInputs():Void
|
||||
{
|
||||
#if FEATURE_TOUCH_CONTROLS
|
||||
if (!SwipeUtil.justSwipedAny && !justOpened && currentMenuEntries.length > 0)
|
||||
if (!SwipeUtil.justSwipedAny && currentMenuEntries.length > 0)
|
||||
{
|
||||
for (i in 0...menuEntryText.members.length)
|
||||
{
|
||||
|
|
@ -660,17 +669,62 @@ class PauseSubState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
if (controls.ACCEPT && currentMenuEntries.length > 0)
|
||||
/**
|
||||
* used to both modify/change offsets, but also to early return so we don't interfere with other inputs while doing so
|
||||
* TODO: refactor to use state design pattern to handle inputs, see MainMenuState
|
||||
* @return Bool true if we are currently modifying our offsets (by holding shift and pressing UP or DOWN)
|
||||
*/
|
||||
function handleModifyingOffsets():Bool
|
||||
{
|
||||
#if !mobile
|
||||
var offset:Int = Preferences.globalOffset ?? 0;
|
||||
if (FlxG.keys.pressed.SHIFT && (controls.UI_UP || controls.UI_DOWN))
|
||||
{
|
||||
currentMenuEntries[currentEntry].callback(this);
|
||||
lastOffsetPress += FlxG.elapsed;
|
||||
if (!fastOffset)
|
||||
{
|
||||
// If the last offset press was more than 0.5 seconds ago, reset the fast offset.
|
||||
if (lastOffsetPress > 0.5)
|
||||
{
|
||||
fastOffset = true;
|
||||
lastOffsetPress = 0;
|
||||
}
|
||||
|
||||
if (controls.UI_UP_P || controls.UI_DOWN_P)
|
||||
{
|
||||
offset += (controls.UI_UP_P || controls.UI_UP) ? 1 : -1;
|
||||
|
||||
offsetText.text = 'Global Offset: ${offset}ms';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
offset += (controls.UI_UP_P || controls.UI_UP) ? 1 : -1;
|
||||
|
||||
offsetText.text = 'Global Offset: ${offset}ms';
|
||||
}
|
||||
|
||||
if (offset > 1500) offset = 1500;
|
||||
if (offset < -1500) offset = -1500;
|
||||
|
||||
Preferences.globalOffset = offset;
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (controls.PAUSE && !justOpened)
|
||||
else
|
||||
{
|
||||
resume(this);
|
||||
// Reset the fast offset if the user is not holding SHIFT.
|
||||
fastOffset = false;
|
||||
lastOffsetPress = 0;
|
||||
}
|
||||
// we only want justOpened to be true for 1 single frame, when we first get into the pause menu substate
|
||||
justOpened = false;
|
||||
#end
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleDebugInputs():Void
|
||||
{
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
// to pause the game and get screenshots easy, press H on pause menu!
|
||||
if (FlxG.keys.justPressed.H)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,12 @@ import funkin.play.scoring.Scoring;
|
|||
import funkin.play.song.Song;
|
||||
import funkin.play.stage.Stage;
|
||||
import funkin.save.Save;
|
||||
#if FEATURE_CHART_EDITOR
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
#end
|
||||
#if FEATURE_STAGE_EDITOR
|
||||
import funkin.ui.debug.stageeditor.StageEditorState;
|
||||
#end
|
||||
import funkin.ui.debug.stage.StageOffsetSubState;
|
||||
import funkin.ui.mainmenu.MainMenuState;
|
||||
import funkin.ui.MusicBeatSubState;
|
||||
|
|
@ -333,6 +338,18 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
public var isInCountdown:Bool = false;
|
||||
|
||||
/**
|
||||
* Determines whether opening a substate over this causes the game to pause.
|
||||
* Enable it before opening a Pause menu or Game Over screen, and disable it
|
||||
* for stuff like editors and overlays.
|
||||
*/
|
||||
public var shouldSubstatePause:Bool = false;
|
||||
|
||||
/**
|
||||
* Whether the game is currently in the Game Over state.
|
||||
*/
|
||||
public var isGameOverState:Bool = false;
|
||||
|
||||
/**
|
||||
* Whether the game is currently in Practice Mode.
|
||||
* If true, player will not gain or lose score from notes.
|
||||
|
|
@ -794,6 +811,10 @@ class PlayState extends MusicBeatSubState
|
|||
// This state receives draw calls even when a substate is active.
|
||||
this.persistentDraw = true;
|
||||
|
||||
// Make the player unable to pause if they're moving from the chart editor while the focus is still on since the input persists.
|
||||
@:privateAccess
|
||||
justUnpaused = isChartingMode && !FlxG.game._lostFocus;
|
||||
|
||||
// Stop any pre-existing music.
|
||||
if (!overrideMusic)
|
||||
{
|
||||
|
|
@ -1128,7 +1149,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
// Fallback to properly update the conductor incase the lerp messed up
|
||||
// Shouldn't be fallen back to unless you're lagging alot
|
||||
trace('[WARNING] Normal Conductor Update!! are you lagging?');
|
||||
trace(' WARNING '.bg_yellow().bold() + ' Normal Conductor Update!! are you lagging?');
|
||||
Conductor.instance.update();
|
||||
}
|
||||
}
|
||||
|
|
@ -1146,7 +1167,7 @@ class PlayState extends MusicBeatSubState
|
|||
#end
|
||||
|
||||
// Attempt to pause the game.
|
||||
if ((controls.PAUSE || androidPause || pauseButtonCheck)) pause();
|
||||
if ((controls.PAUSE_P || androidPause || pauseButtonCheck)) pause();
|
||||
|
||||
#if mobile
|
||||
if (justUnpaused)
|
||||
|
|
@ -1168,14 +1189,17 @@ class PlayState extends MusicBeatSubState
|
|||
if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX;
|
||||
if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN;
|
||||
|
||||
// Apply camera zoom + multipliers.
|
||||
if (subState == null && cameraZoomRate > 0.0) // && !isInCutscene)
|
||||
{
|
||||
cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x
|
||||
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier.
|
||||
if (!debugUnbindCameraZoom) FlxG.camera.zoom = zoomPlusBop; // Actually apply the zoom to the camera.
|
||||
var decayRate:Float = 0.95;
|
||||
var dt:Float = elapsed * 60; //
|
||||
|
||||
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
|
||||
if (subState == null && cameraZoomRate > 0.0)
|
||||
{
|
||||
cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, Math.pow(decayRate, dt));
|
||||
|
||||
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier;
|
||||
if (!debugUnbindCameraZoom) FlxG.camera.zoom = zoomPlusBop;
|
||||
|
||||
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, Math.pow(decayRate, dt));
|
||||
}
|
||||
|
||||
if (currentStage != null && currentStage.getBoyfriend() != null)
|
||||
|
|
@ -1295,7 +1319,12 @@ class PlayState extends MusicBeatSubState
|
|||
#end
|
||||
}
|
||||
|
||||
function pause(mode:PauseMode = Standard):Void
|
||||
/**
|
||||
* Pause the game.
|
||||
* @param mode Which set of pause menu options to display (distinguishes between standard, charting, and cutscene)
|
||||
* @param lostFocus Whether the game paused because the window lost focus
|
||||
*/
|
||||
function pause(mode:PauseMode = Standard, lostFocus:Bool = false):Void
|
||||
{
|
||||
if (!mayPauseGame || justUnpaused || isGamePaused || isPlayerDying) return;
|
||||
|
||||
|
|
@ -1303,11 +1332,11 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
case Conversation:
|
||||
preparePauseUI();
|
||||
openPauseSubState(Conversation, camPause, () -> currentConversation?.pauseMusic());
|
||||
openPauseSubState(Conversation, camPause, lostFocus, () -> currentConversation?.pauseMusic());
|
||||
|
||||
case Cutscene:
|
||||
preparePauseUI();
|
||||
openPauseSubState(Cutscene, camPause, () -> VideoCutscene.pauseVideo());
|
||||
openPauseSubState(Cutscene, camPause, lostFocus, () -> VideoCutscene.pauseVideo());
|
||||
|
||||
default: // also known as standard
|
||||
if (!isInCountdown || isInCutscene) return;
|
||||
|
|
@ -1320,6 +1349,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (!event.eventCanceled)
|
||||
{
|
||||
shouldSubstatePause = true;
|
||||
persistentUpdate = false;
|
||||
persistentDraw = true;
|
||||
|
||||
|
|
@ -1338,7 +1368,7 @@ class PlayState extends MusicBeatSubState
|
|||
boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
|
||||
}
|
||||
|
||||
openPauseSubState(isChartingMode ? Charting : Standard, camPause);
|
||||
openPauseSubState(isChartingMode ? Charting : Standard, camPause, lostFocus);
|
||||
}
|
||||
|
||||
#if FEATURE_DISCORD_RPC
|
||||
|
|
@ -1365,9 +1395,9 @@ class PlayState extends MusicBeatSubState
|
|||
#end
|
||||
}
|
||||
|
||||
function openPauseSubState(mode:PauseMode, cam:FlxCamera, ?onPause:Void->Void):Void
|
||||
function openPauseSubState(mode:PauseMode, cam:FlxCamera, lostFocus:Bool = false, ?onPause:Void->Void):Void
|
||||
{
|
||||
final pauseSubState = new PauseSubState({mode: mode}, onPause);
|
||||
final pauseSubState = new PauseSubState({mode: mode, lostFocus: lostFocus}, onPause);
|
||||
FlxTransitionableState.skipNextTransIn = true;
|
||||
FlxTransitionableState.skipNextTransOut = true;
|
||||
pauseSubState.camera = cam;
|
||||
|
|
@ -1397,6 +1427,8 @@ class PlayState extends MusicBeatSubState
|
|||
iconP2?.updatePosition();
|
||||
}
|
||||
|
||||
isGameOverState = true;
|
||||
shouldSubstatePause = true;
|
||||
// Transition to the game over substate.
|
||||
var gameOverSubState = new GameOverSubState(
|
||||
{
|
||||
|
|
@ -1471,11 +1503,7 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
public override function openSubState(subState:FlxSubState):Void
|
||||
{
|
||||
// If there is a substate which requires the game to continue,
|
||||
// then make this a condition.
|
||||
var shouldPause:Bool = (Std.isOfType(subState, PauseSubState) || Std.isOfType(subState, GameOverSubState));
|
||||
|
||||
if (shouldPause)
|
||||
if (shouldSubstatePause)
|
||||
{
|
||||
// Pause the music.
|
||||
if (FlxG.sound.music != null)
|
||||
|
|
@ -1554,21 +1582,48 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
public override function closeSubState():Void
|
||||
{
|
||||
if (Std.isOfType(subState, PauseSubState))
|
||||
if (shouldSubstatePause)
|
||||
{
|
||||
shouldSubstatePause = false;
|
||||
var event:ScriptEvent = new ScriptEvent(RESUME, true);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
if (event.eventCanceled) return;
|
||||
|
||||
// Pause any sounds that are playing and keep track of them.
|
||||
// Vocals are also paused here but are not included as they are handled separately.
|
||||
if (!isGameOverState)
|
||||
{
|
||||
FlxG.sound.list.forEachAlive(function(sound:FlxSound) {
|
||||
if (!sound.active || sound == FlxG.sound.music) return;
|
||||
// In case it's a scheduled sound
|
||||
if (Std.isOfType(sound, FunkinSound))
|
||||
{
|
||||
var funkinSound:FunkinSound = cast sound;
|
||||
if (funkinSound != null && !funkinSound.isPlaying) return;
|
||||
}
|
||||
if (!sound.playing && sound.time >= 0) return;
|
||||
sound.pause();
|
||||
soundsPausedBySubState.add(sound);
|
||||
});
|
||||
|
||||
vocals?.forEach(function(voice:FunkinSound) {
|
||||
soundsPausedBySubState.remove(voice);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
vocals?.pause();
|
||||
}
|
||||
|
||||
// Resume vwooshTimer
|
||||
if (!vwooshTimer.finished) vwooshTimer.active = true;
|
||||
|
||||
// Resume music if we paused it.
|
||||
if (musicPausedBySubState)
|
||||
{
|
||||
FlxG.sound.music.play();
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.play();
|
||||
musicPausedBySubState = false;
|
||||
}
|
||||
|
||||
|
|
@ -1629,10 +1684,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
justUnpaused = true;
|
||||
}
|
||||
else if (Std.isOfType(subState, Transition))
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
isGameOverState = false;
|
||||
|
||||
super.closeSubState();
|
||||
}
|
||||
|
|
@ -1708,15 +1760,15 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
if (currentConversation != null)
|
||||
{
|
||||
pause(Conversation);
|
||||
pause(Conversation, true);
|
||||
}
|
||||
else if (VideoCutscene.isPlaying())
|
||||
{
|
||||
pause(Cutscene);
|
||||
pause(Cutscene, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
pause();
|
||||
pause(true);
|
||||
}
|
||||
}
|
||||
super.onFocusLost();
|
||||
|
|
@ -2634,8 +2686,6 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
function onKeyRelease(event:PreciseInputEvent):Void
|
||||
{
|
||||
if (isGamePaused) return;
|
||||
|
||||
// Do the minimal possible work here.
|
||||
inputReleaseQueue.push(event);
|
||||
}
|
||||
|
|
@ -2873,7 +2923,10 @@ class PlayState extends MusicBeatSubState
|
|||
var input:Null<PreciseInputEvent> = inputPressQueue.shift();
|
||||
if (input == null) continue;
|
||||
|
||||
playerStrumline.pressKey(input.noteDirection);
|
||||
// Whether this direction is already held by another key.
|
||||
var isAlreadyHeld = playerStrumline.isKeyHeld(input.noteDirection);
|
||||
|
||||
playerStrumline.pressKey(input.noteDirection, input.keyCode);
|
||||
|
||||
// Don't credit or penalize inputs in Bot Play.
|
||||
if (isBotPlayMode) continue;
|
||||
|
|
@ -2881,9 +2934,9 @@ class PlayState extends MusicBeatSubState
|
|||
var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection];
|
||||
|
||||
#if FEATURE_GHOST_TAPPING
|
||||
if ((!playerStrumline.mayGhostTap()) && notesInDirection.length == 0)
|
||||
if ((!playerStrumline.mayGhostTap()) && notesInDirection.length == 0 && !isAlreadyHeld)
|
||||
#else
|
||||
if (notesInDirection.length == 0)
|
||||
if (notesInDirection.length == 0 && !isAlreadyHeld)
|
||||
#end
|
||||
{
|
||||
// Pressed a wrong key with no notes nearby.
|
||||
|
|
@ -2929,7 +2982,7 @@ class PlayState extends MusicBeatSubState
|
|||
// Play the strumline animation.
|
||||
playerStrumline.playStatic(input.noteDirection);
|
||||
|
||||
playerStrumline.releaseKey(input.noteDirection);
|
||||
playerStrumline.releaseKey(input.noteDirection, input.keyCode);
|
||||
}
|
||||
|
||||
playerStrumline.noteVibrations.tryNoteVibration();
|
||||
|
|
@ -3089,7 +3142,18 @@ class PlayState extends MusicBeatSubState
|
|||
// hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!!
|
||||
disableKeys = true;
|
||||
persistentUpdate = false;
|
||||
openSubState(new StageOffsetSubState());
|
||||
// The strings have to be get like this otherwise it just NORs when setting the params?
|
||||
// Or, none of the characters show up, in the case of the pico songs?
|
||||
var bf:String = currentStage?.getBoyfriend()?.characterId ?? '';
|
||||
var gf:String = currentStage?.getGirlfriend()?.characterId ?? '';
|
||||
var dad:String = currentStage?.getDad()?.characterId ?? '';
|
||||
FlxG.switchState(() -> new StageEditorState(
|
||||
{
|
||||
targetStageId: currentStageId,
|
||||
targetBfChar: bf,
|
||||
targetGfChar: gf,
|
||||
targetDadChar: dad
|
||||
}));
|
||||
}
|
||||
#end
|
||||
|
||||
|
|
@ -3255,7 +3319,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
currentConversation.advanceConversation();
|
||||
}
|
||||
else if ((controls.PAUSE || androidPause || pauseButtonCheck) && !justUnpaused)
|
||||
else if ((controls.PAUSE_P || androidPause || pauseButtonCheck) && !justUnpaused)
|
||||
{
|
||||
pause(Conversation);
|
||||
}
|
||||
|
|
@ -3263,7 +3327,7 @@ class PlayState extends MusicBeatSubState
|
|||
else if (VideoCutscene.isPlaying())
|
||||
{
|
||||
// This is a video cutscene.
|
||||
if ((controls.PAUSE || androidPause || pauseButtonCheck) && !justUnpaused)
|
||||
if ((controls.PAUSE_P || androidPause || pauseButtonCheck) && !justUnpaused)
|
||||
{
|
||||
pause(Cutscene);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,9 @@ import funkin.audio.FunkinSound;
|
|||
import funkin.data.freeplay.player.PlayerData.PlayerResultsAnimationData;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||
import funkin.graphics.FunkinCamera;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.graphics.shaders.LeftMaskShader;
|
||||
import funkin.modding.base.ScriptedFlxAtlasSprite;
|
||||
import funkin.play.components.ClearPercentCounter;
|
||||
import funkin.play.components.TallyCounter;
|
||||
import funkin.play.scoring.Scoring;
|
||||
|
|
@ -78,7 +76,7 @@ class ResultState extends MusicBeatSubState
|
|||
|
||||
var characterAtlasAnimations:Array<
|
||||
{
|
||||
sprite:FlxAtlasSprite,
|
||||
sprite:FunkinSprite,
|
||||
delay:Float,
|
||||
forceLoop:Bool,
|
||||
startFrameLabel:String,
|
||||
|
|
@ -119,8 +117,8 @@ class ResultState extends MusicBeatSubState
|
|||
// We build a lot of this stuff in the constructor, then place it in create().
|
||||
// This prevents having to do `null` checks everywhere.
|
||||
|
||||
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
|
||||
songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
|
||||
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890().-";
|
||||
songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 61)));
|
||||
songName.text = params.title;
|
||||
songName.letterSpacing = -15;
|
||||
songName.angle = -4.4;
|
||||
|
|
@ -193,8 +191,8 @@ class ResultState extends MusicBeatSubState
|
|||
add(soundSystem);
|
||||
|
||||
// Fetch playable character data. Default to BF on the results screen if we can't find it.
|
||||
playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId);
|
||||
playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf');
|
||||
playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId) ?? 'bf';
|
||||
playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId);
|
||||
|
||||
trace('Got playable character: ${playerCharacter?.getName()}');
|
||||
// Query JSON data based on the rank, then use that to build the animation(s) the player sees.
|
||||
|
|
@ -222,17 +220,22 @@ class ResultState extends MusicBeatSubState
|
|||
{
|
||||
case 'animateatlas':
|
||||
@:nullSafety(Off)
|
||||
var animation:FlxAtlasSprite = null;
|
||||
var animation:FunkinSprite = null;
|
||||
|
||||
var xPos = offsets[0] + (FullScreenScaleMode.gameCutoutSize.x / 2);
|
||||
var yPos = offsets[1];
|
||||
|
||||
if (animData.scriptClass != null) animation = ScriptedFlxAtlasSprite.init(animData.scriptClass, xPos, yPos);
|
||||
if (animData.scriptClass != null) animation = ScriptedFunkinSprite.init(animData.scriptClass, xPos, yPos);
|
||||
else
|
||||
animation = new FlxAtlasSprite(xPos, yPos, Paths.animateAtlas(animPath, animLibrary));
|
||||
animation = FunkinSprite.createTextureAtlas(xPos, yPos, animPath, animLibrary);
|
||||
|
||||
if (animation == null) continue;
|
||||
|
||||
if (animData?.applyStageMatrix ?? false)
|
||||
{
|
||||
animation.applyStageMatrix = true;
|
||||
}
|
||||
|
||||
animation.zIndex = animData.zIndex ?? 500;
|
||||
|
||||
animation.scale.set(animData.scale ?? 1.0, animData.scale ?? 1.0);
|
||||
|
|
@ -240,7 +243,7 @@ class ResultState extends MusicBeatSubState
|
|||
if (!(animData.looped ?? true))
|
||||
{
|
||||
// Animation is not looped.
|
||||
animation.onAnimationComplete.add((_name:String) -> {
|
||||
animation.anim.onFinish.add((_name:String) -> {
|
||||
if (animation != null)
|
||||
{
|
||||
animation.anim.pause();
|
||||
|
|
@ -249,23 +252,24 @@ class ResultState extends MusicBeatSubState
|
|||
}
|
||||
else if (animData.loopFrameLabel != null)
|
||||
{
|
||||
animation.onAnimationComplete.add((_name:String) -> {
|
||||
animation.anim.onFinish.add((_name:String) -> {
|
||||
if (animation != null)
|
||||
{
|
||||
animation.playAnimation(animData.loopFrameLabel ?? '', true, false, true); // unpauses this anim, since it's on PlayOnce!
|
||||
animation.anim.play(animData.loopFrameLabel ?? '', true); // unpauses this anim, since it's on PlayOnce!
|
||||
animation.anim.curAnim.looped = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (animData.loopFrame != null)
|
||||
{
|
||||
animation.onAnimationComplete.add((_name:String) -> {
|
||||
animation.anim.onFinish.add((_name:String) -> {
|
||||
if (animation != null)
|
||||
{
|
||||
animation.anim.curFrame = animData.loopFrame ?? 0;
|
||||
animation.anim.play(); // unpauses this anim, since it's on PlayOnce!
|
||||
animation.anim.play("", true, false, animData.loopFrame ?? 0); // unpauses this anim, since it's on PlayOnce!
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hide until ready to play.
|
||||
animation.visible = false;
|
||||
// Queue to play.
|
||||
|
|
@ -660,7 +664,7 @@ class ResultState extends MusicBeatSubState
|
|||
new FlxTimer().start(atlas.delay, _ -> {
|
||||
if (atlas.sprite == null) return;
|
||||
atlas.sprite.visible = true;
|
||||
atlas.sprite.playAnimation(atlas.startFrameLabel);
|
||||
atlas.sprite.anim.play(atlas.startFrameLabel);
|
||||
if (atlas.sound != "")
|
||||
{
|
||||
var sndPath:String = Paths.stripLibrary(atlas.sound);
|
||||
|
|
@ -770,7 +774,7 @@ class ResultState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
if (controls.PAUSE || controls.ACCEPT #if mobile || TouchUtil.pressAction() #end)
|
||||
if (controls.PAUSE_P || controls.ACCEPT_P #if mobile || TouchUtil.pressAction() #end)
|
||||
{
|
||||
if (busy) return;
|
||||
if (_parentState is funkin.ui.debug.results.ResultsDebugSubState)
|
||||
|
|
|
|||
|
|
@ -1,35 +1,11 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import flixel.animation.FlxAnimationController;
|
||||
import flixel.FlxCamera;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.math.FlxPoint.FlxCallbackPoint;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.math.FlxRect;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxDestroyUtil;
|
||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.data.character.CharacterData.CharacterRenderType;
|
||||
import flixel.util.FlxDirectionFlags;
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.display.BlendMode;
|
||||
|
||||
/**
|
||||
* Individual animation data for an AnimateAtlasCharacter.
|
||||
*/
|
||||
typedef AnimateAtlasAnimation =
|
||||
{
|
||||
name:String,
|
||||
prefix:String,
|
||||
offsets:Null<Array<Float>>,
|
||||
looped:Bool,
|
||||
}
|
||||
import flixel.math.FlxPoint;
|
||||
|
||||
/**
|
||||
* An AnimateAtlasCharacter is a Character which is rendered by
|
||||
|
|
@ -40,39 +16,13 @@ typedef AnimateAtlasAnimation =
|
|||
*/
|
||||
class AnimateAtlasCharacter extends BaseCharacter
|
||||
{
|
||||
// BaseCharacter extends FlxSprite but we can't make it also extend FlxAtlasSprite UGH
|
||||
// I basically copied the code from FlxSpriteGroup to make the FlxAtlasSprite a "child" of this class
|
||||
var mainSprite:FlxAtlasSprite;
|
||||
|
||||
var _skipTransformChildren:Bool = false;
|
||||
|
||||
var animations:Map<String, AnimateAtlasAnimation> = new Map<String, AnimateAtlasAnimation>();
|
||||
var currentAnimName:Null<String> = null;
|
||||
var animFinished:Bool = false;
|
||||
var originalSizes(default, never):FlxPoint = new FlxPoint(0, 0);
|
||||
|
||||
public function new(id:String)
|
||||
{
|
||||
super(id, CharacterRenderType.AnimateAtlas);
|
||||
}
|
||||
|
||||
override function initVars():Void
|
||||
{
|
||||
// this.flixelType = SPRITEGROUP;
|
||||
|
||||
// TODO: Make `animation` a stub that redirects calls to `mainSprite`?
|
||||
animation = new FlxAnimationController(this);
|
||||
|
||||
offset = new FlxCallbackPoint(offsetCallback);
|
||||
origin = new FlxCallbackPoint(originCallback);
|
||||
scale = new FlxCallbackPoint(scaleCallback);
|
||||
scrollFactor = new FlxCallbackPoint(scrollFactorCallback);
|
||||
|
||||
scale.set(1, 1);
|
||||
scrollFactor.set(1, 1);
|
||||
|
||||
initMotionVars();
|
||||
}
|
||||
|
||||
override function onCreate(event:ScriptEvent):Void
|
||||
{
|
||||
// Display a custom scope for debugging purposes.
|
||||
|
|
@ -83,756 +33,80 @@ class AnimateAtlasCharacter extends BaseCharacter
|
|||
try
|
||||
{
|
||||
trace('Loading assets for Animate Atlas character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0"));
|
||||
var atlasSprite:FlxAtlasSprite = loadAtlasSprite();
|
||||
setSprite(atlasSprite);
|
||||
|
||||
loadAtlas();
|
||||
loadAnimations();
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
throw "Exception thrown while building FlxAtlasSprite: " + e;
|
||||
throw "Exception thrown while building sprite: " + e;
|
||||
}
|
||||
|
||||
trace('[ATLASCHAR] Successfully loaded texture atlas for ${characterId} with ${_data.animations.length} animations.');
|
||||
super.onCreate(event);
|
||||
|
||||
originalSizes.set(this.width, this.height);
|
||||
}
|
||||
|
||||
public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void
|
||||
{
|
||||
var correctName = correctAnimationName(name);
|
||||
if (correctName == null)
|
||||
{
|
||||
trace('$characterName Could not find Atlas animation: ' + name);
|
||||
return;
|
||||
}
|
||||
|
||||
var animData = getAnimationData(correctName);
|
||||
currentAnimName = correctName;
|
||||
var prefix:String = animData.prefix;
|
||||
if (prefix == null) prefix = correctName;
|
||||
var loop:Bool = animData.looped;
|
||||
|
||||
this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop);
|
||||
}
|
||||
|
||||
public override function hasAnimation(name:String):Bool
|
||||
{
|
||||
return getAnimationData(name) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the animation has finished playing.
|
||||
* Never true if animation is configured to loop.
|
||||
*/
|
||||
public override function isAnimationFinished():Bool
|
||||
{
|
||||
return mainSprite?.isAnimationFinished() ?? false;
|
||||
}
|
||||
|
||||
function loadAtlasSprite():FlxAtlasSprite
|
||||
function loadAtlas():Void
|
||||
{
|
||||
trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.');
|
||||
var assetLibrary:String = Paths.getLibrary(_data.assetPath);
|
||||
var assetPath:String = Paths.stripLibrary(_data.assetPath);
|
||||
|
||||
var animLibrary:String = Paths.getLibrary(_data.assetPath);
|
||||
var animPath:String = Paths.stripLibrary(_data.assetPath);
|
||||
var assetPath:String = Paths.animateAtlas(animPath, animLibrary);
|
||||
loadTextureAtlas(assetPath, assetLibrary, getAtlasSettings());
|
||||
|
||||
var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, assetPath);
|
||||
|
||||
// sprite.onAnimationComplete.removeAll();
|
||||
sprite.onAnimationComplete.add(this.onAnimationFinished);
|
||||
|
||||
return sprite;
|
||||
}
|
||||
|
||||
override function onAnimationFinished(prefix:String):Void
|
||||
{
|
||||
super.onAnimationFinished(prefix);
|
||||
|
||||
if (!getCurrentAnimation().endsWith(Constants.ANIMATION_HOLD_SUFFIX)
|
||||
&& hasAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX))
|
||||
if (_data.isPixel)
|
||||
{
|
||||
playAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX);
|
||||
}
|
||||
|
||||
if (getAnimationData() != null && getAnimationData().looped)
|
||||
{
|
||||
if (StringTools.endsWith(prefix, "-hold")) trace(prefix);
|
||||
playAnimation(prefix, true, false);
|
||||
this.isPixel = true;
|
||||
this.antialiasing = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make the game hold on the last frame.
|
||||
this.mainSprite.cleanupAnimation(prefix);
|
||||
// currentAnimName = null;
|
||||
|
||||
// Fallback to idle!
|
||||
// playAnimation('idle', true, false);
|
||||
this.isPixel = false;
|
||||
this.antialiasing = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setSprite(sprite:FlxAtlasSprite):Void
|
||||
{
|
||||
trace('[ATLASCHAR] Applying sprite properties to ${characterId}');
|
||||
|
||||
this.mainSprite = sprite;
|
||||
|
||||
mainSprite.ignoreExclusionPref = ["sing"];
|
||||
|
||||
// This forces the atlas to recalcuate its width and height
|
||||
this.mainSprite.alpha = 0.0001;
|
||||
this.mainSprite.draw();
|
||||
this.mainSprite.alpha = 1.0;
|
||||
|
||||
var feetPos:FlxPoint = feetPosition;
|
||||
this.updateHitbox();
|
||||
|
||||
sprite.x = this.x;
|
||||
sprite.y = this.y;
|
||||
sprite.alpha *= alpha;
|
||||
sprite.flipX = flipX;
|
||||
sprite.flipY = flipY;
|
||||
sprite.scrollFactor.copyFrom(scrollFactor);
|
||||
sprite.cameras = _cameras; // _cameras instead of cameras because get_cameras() will not return null
|
||||
|
||||
if (clipRect != null) clipRectTransform(sprite, clipRect);
|
||||
this.setScale(_data.scale);
|
||||
}
|
||||
|
||||
function loadAnimations():Void
|
||||
{
|
||||
trace('[ATLASCHAR] Attempting to load ${_data.animations.length} animations for ${characterId}');
|
||||
trace('[ATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}');
|
||||
|
||||
var animData:Array<AnimateAtlasAnimation> = cast _data.animations;
|
||||
FlxAnimationUtil.addTextureAtlasAnimations(this, _data.animations);
|
||||
|
||||
for (anim in animData)
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
// Validate the animation before adding.
|
||||
var prefix = anim.prefix;
|
||||
if (!this.mainSprite.hasAnimation(prefix))
|
||||
if (anim.offsets == null)
|
||||
{
|
||||
FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}');
|
||||
trace('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}');
|
||||
continue;
|
||||
setAnimationOffsets(anim.name, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
|
||||
}
|
||||
animations.set(anim.name, anim);
|
||||
trace('[ATLASCHAR] - Successfully loaded animation ${anim.name} to ${characterId}');
|
||||
}
|
||||
|
||||
trace('[ATLASCHAR] Loaded ${animations.size()} animations for ${characterId}');
|
||||
}
|
||||
|
||||
public override function getCurrentAnimation():String
|
||||
{
|
||||
// return this.mainSprite.getCurrentAnimation();
|
||||
return currentAnimName;
|
||||
}
|
||||
|
||||
function getAnimationData(name:String = null):AnimateAtlasAnimation
|
||||
{
|
||||
if (name == null) name = getCurrentAnimation();
|
||||
return animations.get(name);
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// Code copied from FlxSpriteGroup
|
||||
//
|
||||
//
|
||||
|
||||
/**
|
||||
* Handy function that allows you to quickly transform one property of sprites in this group at a time.
|
||||
*
|
||||
* @param callback Function to transform the sprites. Example:
|
||||
* `function(sprite, v:Dynamic) { s.acceleration.x = v; s.makeGraphic(10,10,0xFF000000); }`
|
||||
* @param value Value which will passed to lambda function.
|
||||
*/
|
||||
@:generic
|
||||
public function transformChildren<V>(callback:FlxAtlasSprite->V->Void, value:V):Void
|
||||
{
|
||||
if (_skipTransformChildren || this.mainSprite == null) return;
|
||||
|
||||
callback(this.mainSprite, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `kill()` on the group's members and then on the group itself.
|
||||
* You can revive this group later via `revive()` after this.
|
||||
*/
|
||||
public override function kill():Void
|
||||
{
|
||||
_skipTransformChildren = true;
|
||||
super.kill();
|
||||
_skipTransformChildren = false;
|
||||
if (this.mainSprite != null)
|
||||
{
|
||||
this.mainSprite.kill();
|
||||
this.mainSprite = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revives the group.
|
||||
*/
|
||||
public override function revive():Void
|
||||
{
|
||||
_skipTransformChildren = true;
|
||||
super.revive(); // calls set_exists and set_alive
|
||||
_skipTransformChildren = false;
|
||||
this.mainSprite.revive();
|
||||
}
|
||||
|
||||
/**
|
||||
* **WARNING:** A destroyed `FlxBasic` can't be used anymore.
|
||||
* It may even cause crashes if it is still part of a group or state.
|
||||
* You may want to use `kill()` instead if you want to disable the object temporarily only and `revive()` it later.
|
||||
*
|
||||
* This function is usually not called manually (Flixel calls it automatically during state switches for all `add()`ed objects).
|
||||
*
|
||||
* Override this function to `null` out variables manually or call `destroy()` on class members if necessary.
|
||||
* Don't forget to call `super.destroy()`!
|
||||
*/
|
||||
public override function destroy():Void
|
||||
{
|
||||
// normally don't have to destroy FlxPoints, but these are FlxCallbackPoints!
|
||||
offset = FlxDestroyUtil.destroy(offset);
|
||||
origin = FlxDestroyUtil.destroy(origin);
|
||||
scale = FlxDestroyUtil.destroy(scale);
|
||||
scrollFactor = FlxDestroyUtil.destroy(scrollFactor);
|
||||
|
||||
this.mainSprite = FlxDestroyUtil.destroy(this.mainSprite);
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and see if any sprite in this group is currently on screen.
|
||||
*
|
||||
* @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera.
|
||||
* @return Whether the object is on screen or not.
|
||||
*/
|
||||
public override function isOnScreen(?camera:FlxCamera):Bool
|
||||
{
|
||||
if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible && this.mainSprite.isOnScreen(camera)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a point in 2D world space overlaps any `FlxSprite` object from this group.
|
||||
*
|
||||
* @param Point The point in world space you want to check.
|
||||
* @param InScreenSpace Whether to take scroll factors into account when checking for overlap.
|
||||
* @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera.
|
||||
* @return Whether or not the point overlaps this group.
|
||||
*/
|
||||
public override function overlapsPoint(point:FlxPoint, inScreenSpace:Bool = false, camera:FlxCamera = null):Bool
|
||||
{
|
||||
var result:Bool = false;
|
||||
result = this.mainSprite.overlapsPoint(point, inScreenSpace, camera);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a point in 2D world space overlaps any of FlxSprite object's current displayed pixels.
|
||||
* This check is ALWAYS made in screen space, and always takes scroll factors into account.
|
||||
*
|
||||
* @param Point The point in world space you want to check.
|
||||
* @param Mask Used in the pixel hit test to determine what counts as solid.
|
||||
* @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera.
|
||||
* @return Whether or not the point overlaps this object.
|
||||
*/
|
||||
public override function pixelsOverlapPoint(point:FlxPoint, Mask:Int = 0xFF, Camera:FlxCamera = null):Bool
|
||||
{
|
||||
var result:Bool = false;
|
||||
if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible)
|
||||
{
|
||||
result = this.mainSprite.pixelsOverlapPoint(point, Mask, Camera);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
this.mainSprite.update(elapsed);
|
||||
|
||||
if (moves) updateMotion(elapsed);
|
||||
}
|
||||
|
||||
public override function draw():Void
|
||||
{
|
||||
this.mainSprite.draw();
|
||||
|
||||
#if FLX_DEBUG
|
||||
if (FlxG.debugger.drawDebug) drawDebug();
|
||||
#end
|
||||
}
|
||||
|
||||
inline function xTransform(sprite:FlxSprite, x:Float):Void
|
||||
sprite.x += x; // addition
|
||||
|
||||
inline function yTransform(sprite:FlxSprite, y:Float):Void
|
||||
sprite.y += y; // addition
|
||||
|
||||
inline function angleTransform(sprite:FlxSprite, angle:Float):Void
|
||||
sprite.angle += angle; // addition
|
||||
|
||||
inline function alphaTransform(sprite:FlxSprite, alpha:Float):Void
|
||||
{
|
||||
if (sprite.alpha != 0 || alpha == 0)
|
||||
{
|
||||
sprite.alpha *= alpha; // multiplication
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.alpha = 1 / alpha; // direct set to avoid stuck sprites
|
||||
}
|
||||
}
|
||||
|
||||
inline function directAlphaTransform(sprite:FlxSprite, alpha:Float):Void
|
||||
sprite.alpha = alpha; // direct set
|
||||
|
||||
inline function facingTransform(sprite:FlxSprite, facing:FlxDirectionFlags):Void
|
||||
sprite.facing = facing;
|
||||
|
||||
inline function flipXTransform(sprite:FlxSprite, flipX:Bool):Void
|
||||
sprite.flipX = flipX;
|
||||
|
||||
inline function flipYTransform(sprite:FlxSprite, flipY:Bool):Void
|
||||
sprite.flipY = flipY;
|
||||
|
||||
inline function movesTransform(sprite:FlxSprite, moves:Bool):Void
|
||||
sprite.moves = moves;
|
||||
|
||||
inline function pixelPerfectTransform(sprite:FlxSprite, pixelPerfect:Bool):Void
|
||||
sprite.pixelPerfectRender = pixelPerfect;
|
||||
|
||||
inline function gColorTransform(sprite:FlxSprite, color:Int):Void
|
||||
sprite.color = color;
|
||||
|
||||
inline function blendTransform(sprite:FlxSprite, blend:BlendMode):Void
|
||||
sprite.blend = blend;
|
||||
|
||||
inline function immovableTransform(sprite:FlxSprite, immovable:Bool):Void
|
||||
sprite.immovable = immovable;
|
||||
|
||||
inline function visibleTransform(sprite:FlxSprite, visible:Bool):Void
|
||||
sprite.visible = visible;
|
||||
|
||||
inline function activeTransform(sprite:FlxSprite, active:Bool):Void
|
||||
sprite.active = active;
|
||||
|
||||
inline function solidTransform(sprite:FlxSprite, solid:Bool):Void
|
||||
sprite.solid = solid;
|
||||
|
||||
inline function aliveTransform(sprite:FlxSprite, alive:Bool):Void
|
||||
sprite.alive = alive;
|
||||
|
||||
inline function existsTransform(sprite:FlxSprite, exists:Bool):Void
|
||||
sprite.exists = exists;
|
||||
|
||||
inline function cameraTransform(sprite:FlxSprite, camera:FlxCamera):Void
|
||||
sprite.camera = camera;
|
||||
|
||||
inline function camerasTransform(sprite:FlxSprite, cameras:Array<FlxCamera>):Void
|
||||
sprite.cameras = cameras;
|
||||
|
||||
inline function offsetTransform(sprite:FlxSprite, offset:FlxPoint):Void
|
||||
sprite.offset.copyFrom(offset);
|
||||
|
||||
inline function originTransform(sprite:FlxSprite, origin:FlxPoint):Void
|
||||
sprite.origin.copyFrom(origin);
|
||||
|
||||
inline function scaleTransform(sprite:FlxSprite, scale:FlxPoint):Void
|
||||
sprite.scale.copyFrom(scale);
|
||||
|
||||
inline function scrollFactorTransform(sprite:FlxSprite, scrollFactor:FlxPoint):Void
|
||||
sprite.scrollFactor.copyFrom(scrollFactor);
|
||||
|
||||
inline function clipRectTransform(sprite:FlxSprite, clipRect:FlxRect):Void
|
||||
{
|
||||
if (clipRect == null)
|
||||
{
|
||||
sprite.clipRect = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.clipRect = FlxRect.get(clipRect.x - sprite.x + x, clipRect.y - sprite.y + y, clipRect.width, clipRect.height);
|
||||
}
|
||||
}
|
||||
|
||||
var resS:FlxPoint = new FlxPoint();
|
||||
|
||||
/**
|
||||
* Reset the character so it can be used at the start of the level.
|
||||
* Call this when restarting the level.
|
||||
*/
|
||||
override public function resetCharacter(resetCamera:Bool = true):Void
|
||||
{
|
||||
trace("RESETTING ATLAS " + characterName);
|
||||
|
||||
// Reset the animation offsets. This will modify x and y to be the absolute position of the character.
|
||||
// this.animOffsets = [0, 0];
|
||||
|
||||
// Now we can set the x and y to be their original values without having to account for animOffsets.
|
||||
this.resetPosition();
|
||||
mainSprite.setPosition(originalPosition.x, originalPosition.y);
|
||||
|
||||
// Then reapply animOffsets...
|
||||
// applyAnimationOffsets(getCurrentAnimation());
|
||||
|
||||
// Make sure we are playing the idle animation
|
||||
// ...then update the hitbox so that this.width and this.height are correct.
|
||||
|
||||
mainSprite.scale.set(1, 1);
|
||||
mainSprite.alpha = 0.0001;
|
||||
mainSprite.width = 0;
|
||||
mainSprite.height = 0;
|
||||
this.dance(true); // Force to avoid the old animation playing with the wrong offset at the start of the song.
|
||||
|
||||
mainSprite.draw(); // refresh frame
|
||||
|
||||
if (resS.x == 0)
|
||||
{
|
||||
resS.x = mainSprite.width; // clunky bizz
|
||||
resS.y = mainSprite.height;
|
||||
}
|
||||
|
||||
mainSprite.alpha = alpha;
|
||||
|
||||
mainSprite.width = resS.x;
|
||||
mainSprite.height = resS.y;
|
||||
frameWidth = 0;
|
||||
frameHeight = 0;
|
||||
|
||||
scaleCallback(scale);
|
||||
this.updateHitbox();
|
||||
|
||||
// Reset the camera focus point while we're at it.
|
||||
if (resetCamera) this.resetCameraFocusPoint();
|
||||
}
|
||||
|
||||
inline function offsetCallback(offset:FlxPoint):Void
|
||||
transformChildren(offsetTransform, offset);
|
||||
|
||||
inline function originCallback(origin:FlxPoint):Void
|
||||
transformChildren(originTransform, origin);
|
||||
|
||||
inline function scaleCallback(scale:FlxPoint):Void
|
||||
transformChildren(scaleTransform, scale);
|
||||
|
||||
inline function scrollFactorCallback(scrollFactor:FlxPoint):Void
|
||||
transformChildren(scrollFactorTransform, scrollFactor);
|
||||
|
||||
override function set_camera(value:FlxCamera):FlxCamera
|
||||
{
|
||||
if (camera != value) transformChildren(cameraTransform, value);
|
||||
return super.set_camera(value);
|
||||
}
|
||||
|
||||
override function set_cameras(value:Array<FlxCamera>):Array<FlxCamera>
|
||||
{
|
||||
if (cameras != value) transformChildren(camerasTransform, value);
|
||||
return super.set_cameras(value);
|
||||
}
|
||||
|
||||
override function set_exists(value:Bool):Bool
|
||||
{
|
||||
if (exists != value) transformChildren(existsTransform, value);
|
||||
return super.set_exists(value);
|
||||
}
|
||||
|
||||
override function set_visible(value:Bool):Bool
|
||||
{
|
||||
if (exists && visible != value) transformChildren(visibleTransform, value);
|
||||
return super.set_visible(value);
|
||||
}
|
||||
|
||||
override function set_active(value:Bool):Bool
|
||||
{
|
||||
if (exists && active != value) transformChildren(activeTransform, value);
|
||||
return super.set_active(value);
|
||||
}
|
||||
|
||||
override function set_alive(value:Bool):Bool
|
||||
{
|
||||
if (alive != value) transformChildren(aliveTransform, value);
|
||||
return super.set_alive(value);
|
||||
}
|
||||
|
||||
override function set_x(value:Float):Float
|
||||
{
|
||||
if (!exists || x == value) return x; // early return (no need to transform)
|
||||
|
||||
transformChildren(xTransform, value - x); // offset
|
||||
return x = value;
|
||||
}
|
||||
|
||||
override function set_y(value:Float):Float
|
||||
{
|
||||
if (exists && y != value) transformChildren(yTransform, value - y); // offset
|
||||
return y = value;
|
||||
}
|
||||
|
||||
override function set_angle(value:Float):Float
|
||||
{
|
||||
if (exists && angle != value) transformChildren(angleTransform, value - angle); // offset
|
||||
return angle = value;
|
||||
}
|
||||
|
||||
override function set_alpha(value:Float):Float
|
||||
{
|
||||
value = value.clamp(0, 1);
|
||||
|
||||
if (exists && alpha != value)
|
||||
{
|
||||
transformChildren(directAlphaTransform, value);
|
||||
}
|
||||
return alpha = value;
|
||||
}
|
||||
|
||||
override function set_facing(value:FlxDirectionFlags):FlxDirectionFlags
|
||||
{
|
||||
if (exists && facing != value) transformChildren(facingTransform, value);
|
||||
return facing = value;
|
||||
}
|
||||
|
||||
override function set_flipX(value:Bool):Bool
|
||||
{
|
||||
if (exists && flipX != value) transformChildren(flipXTransform, value);
|
||||
return flipX = value;
|
||||
}
|
||||
|
||||
override function set_flipY(value:Bool):Bool
|
||||
{
|
||||
if (exists && flipY != value) transformChildren(flipYTransform, value);
|
||||
return flipY = value;
|
||||
}
|
||||
|
||||
override function set_moves(value:Bool):Bool
|
||||
{
|
||||
if (exists && moves != value) transformChildren(movesTransform, value);
|
||||
return moves = value;
|
||||
}
|
||||
|
||||
override function set_immovable(value:Bool):Bool
|
||||
{
|
||||
if (exists && immovable != value) transformChildren(immovableTransform, value);
|
||||
return immovable = value;
|
||||
}
|
||||
|
||||
override function set_solid(value:Bool):Bool
|
||||
{
|
||||
if (exists && solid != value) transformChildren(solidTransform, value);
|
||||
return super.set_solid(value);
|
||||
}
|
||||
|
||||
override function set_color(value:Int):Int
|
||||
{
|
||||
if (exists && color != value) transformChildren(gColorTransform, value);
|
||||
return color = value;
|
||||
}
|
||||
|
||||
override function set_blend(value:BlendMode):BlendMode
|
||||
{
|
||||
if (exists && blend != value) transformChildren(blendTransform, value);
|
||||
return blend = value;
|
||||
}
|
||||
|
||||
override function set_clipRect(rect:FlxRect):FlxRect
|
||||
{
|
||||
if (exists) transformChildren(clipRectTransform, rect);
|
||||
return super.set_clipRect(rect);
|
||||
}
|
||||
|
||||
override function set_pixelPerfectRender(value:Bool):Bool
|
||||
{
|
||||
if (exists && pixelPerfectRender != value) transformChildren(pixelPerfectTransform, value);
|
||||
return super.set_pixelPerfectRender(value);
|
||||
}
|
||||
|
||||
override function set_width(value:Float):Float
|
||||
{
|
||||
return value;
|
||||
var animNames = this.anim.getNameList();
|
||||
trace('[ATLASCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
|
||||
}
|
||||
|
||||
override function get_width():Float
|
||||
{
|
||||
if (this.mainSprite == null) return 0;
|
||||
|
||||
return this.mainSprite.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the left-most position of the left-most member.
|
||||
* If there are no members, x is returned.
|
||||
*
|
||||
* @since 5.0.0
|
||||
* @return the left-most position of the left-most member
|
||||
*/
|
||||
public function findMinX():Float
|
||||
{
|
||||
return this.mainSprite == null ? x : findMinXHelper();
|
||||
}
|
||||
|
||||
function findMinXHelper():Float
|
||||
{
|
||||
return this.mainSprite.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the right-most position of the right-most member.
|
||||
* If there are no members, x is returned.
|
||||
*
|
||||
* @since 5.0.0
|
||||
* @return the right-most position of the right-most member
|
||||
*/
|
||||
public function findMaxX():Float
|
||||
{
|
||||
return this.mainSprite == null ? x : findMaxXHelper();
|
||||
}
|
||||
|
||||
function findMaxXHelper():Float
|
||||
{
|
||||
return this.mainSprite.x + this.mainSprite.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in SpriteGroup
|
||||
*/
|
||||
override function set_height(value:Float):Float
|
||||
{
|
||||
return value;
|
||||
return originalSizes.x;
|
||||
}
|
||||
|
||||
override function get_height():Float
|
||||
{
|
||||
if (this.mainSprite == null) return 0;
|
||||
|
||||
return this.mainSprite.height;
|
||||
return originalSizes.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top-most position of the top-most member.
|
||||
* If there are no members, y is returned.
|
||||
*
|
||||
* @since 5.0.0
|
||||
* @return the top-most position of the top-most member
|
||||
* Get the configuration for the texture atlas.
|
||||
* @return The configuration for the texture atlas.
|
||||
*/
|
||||
public function findMinY():Float
|
||||
public function getAtlasSettings():AtlasSpriteSettings
|
||||
{
|
||||
return this.mainSprite == null ? y : findMinYHelper();
|
||||
return cast _data.atlasSettings;
|
||||
}
|
||||
|
||||
function findMinYHelper():Float
|
||||
{
|
||||
return this.mainSprite.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top-most position of the top-most member.
|
||||
* If there are no members, y is returned.
|
||||
*
|
||||
* @since 5.0.0
|
||||
* @return the bottom-most position of the bottom-most member
|
||||
*/
|
||||
public function findMaxY():Float
|
||||
{
|
||||
return this.mainSprite == null ? y : findMaxYHelper();
|
||||
}
|
||||
|
||||
function findMaxYHelper():Float
|
||||
{
|
||||
return this.mainSprite.y + this.mainSprite.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in SpriteGroup
|
||||
* @return this sprite group
|
||||
*/
|
||||
public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite
|
||||
{
|
||||
#if FLX_DEBUG
|
||||
throw "This function is not supported in FlxSpriteGroup";
|
||||
#end
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in SpriteGroup
|
||||
* @return this sprite group
|
||||
*/
|
||||
public override function loadGraphic(Graphic:FlxGraphicAsset, Animated:Bool = false, Width:Int = 0, Height:Int = 0, Unique:Bool = false,
|
||||
?Key:String):FlxSprite
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in SpriteGroup
|
||||
* @return this sprite group
|
||||
*/
|
||||
public override function loadRotatedGraphic(Graphic:FlxGraphicAsset, Rotations:Int = 16, Frame:Int = -1, AntiAliasing:Bool = false, AutoBuffer:Bool = false,
|
||||
?Key:String):FlxSprite
|
||||
{
|
||||
#if FLX_DEBUG
|
||||
throw "This function is not supported in FlxSpriteGroup";
|
||||
#end
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in SpriteGroup
|
||||
* @return this sprite group
|
||||
*/
|
||||
public override function makeGraphic(Width:Int, Height:Int, Color:Int = FlxColor.WHITE, Unique:Bool = false, ?Key:String):FlxSprite
|
||||
{
|
||||
#if FLX_DEBUG
|
||||
throw "This function is not supported in FlxSpriteGroup";
|
||||
#end
|
||||
return this;
|
||||
}
|
||||
|
||||
override function set_pixels(value:BitmapData):BitmapData
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
override function set_frame(value:FlxFrame):FlxFrame
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
override function get_pixels():BitmapData
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function to update the current animation frame.
|
||||
*
|
||||
* @param RunOnCpp Whether the frame should also be recalculated if we're on a non-flash target
|
||||
*/
|
||||
override inline function calcFrame(RunOnCpp:Bool = false):Void
|
||||
{
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in SpriteGroup
|
||||
*/
|
||||
override inline function resetHelpers():Void {}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in SpriteGroup
|
||||
*/
|
||||
public override inline function stamp(Brush:FlxSprite, X:Int = 0, Y:Int = 0):Void {}
|
||||
|
||||
override function set_frames(Frames:FlxFramesCollection):FlxFramesCollection
|
||||
{
|
||||
return Frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in SpriteGroup
|
||||
*/
|
||||
override inline function updateColorTransform():Void {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ class BaseCharacter extends Bopper
|
|||
{
|
||||
if (PlayState.instance.iconP1 == null)
|
||||
{
|
||||
trace('[WARN] Player 1 health icon not found!');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Player 1 health icon not found!');
|
||||
return;
|
||||
}
|
||||
PlayState.instance.iconP1.configure(_data?.healthIcon);
|
||||
|
|
@ -347,7 +347,7 @@ class BaseCharacter extends Bopper
|
|||
{
|
||||
if (PlayState.instance.iconP2 == null)
|
||||
{
|
||||
trace('[WARN] Player 2 health icon not found!');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Player 2 health icon not found!');
|
||||
return;
|
||||
}
|
||||
PlayState.instance.iconP2.configure(_data?.healthIcon);
|
||||
|
|
|
|||
138
source/funkin/play/character/MultiAnimateAtlasCharacter.hx
Normal file
138
source/funkin/play/character/MultiAnimateAtlasCharacter.hx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import animate.FlxAnimateFrames;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.data.character.CharacterData.CharacterRenderType;
|
||||
import flixel.math.FlxPoint;
|
||||
|
||||
/**
|
||||
* This render type is the most complex, and is used by characters which use
|
||||
* multiple Adobe Animate texture atlases. This render type concatenates multiple
|
||||
* texture atlases into a single sprite.
|
||||
*
|
||||
* BaseCharacter has game logic, MultiAnimateAtlasCharacter has only rendering logic.
|
||||
* KEEP THEM SEPARATE!
|
||||
*/
|
||||
class MultiAnimateAtlasCharacter extends BaseCharacter
|
||||
{
|
||||
var originalSizes(default, never):FlxPoint = new FlxPoint(0, 0);
|
||||
|
||||
public function new(id:String)
|
||||
{
|
||||
super(id, CharacterRenderType.MultiAnimateAtlas);
|
||||
}
|
||||
|
||||
override function onCreate(event:ScriptEvent):Void
|
||||
{
|
||||
// Display a custom scope for debugging purposes.
|
||||
#if FEATURE_DEBUG_TRACY
|
||||
cpp.vm.tracy.TracyProfiler.zoneScoped('MultiAnimateAtlasCharacter.create(${this.characterId})');
|
||||
#end
|
||||
|
||||
try
|
||||
{
|
||||
trace('Loading assets for Multi-Animate Atlas character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0"));
|
||||
loadAtlases();
|
||||
loadAnimations();
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
throw "Exception thrown while building sprite: " + e;
|
||||
}
|
||||
|
||||
trace('[MULTIATLASCHAR] Successfully loaded texture atlases for ${characterId} with ${_data.animations.length} animations.');
|
||||
super.onCreate(event);
|
||||
|
||||
originalSizes.set(this.width, this.height);
|
||||
}
|
||||
|
||||
function loadAtlases():Void
|
||||
{
|
||||
trace('[MULTIATLASCHAR] Loading sprite atlases for ${characterId}.');
|
||||
|
||||
var assetList:Array<String> = [];
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.assetPath != null && !assetList.contains(anim.assetPath))
|
||||
{
|
||||
assetList.push(anim.assetPath);
|
||||
}
|
||||
}
|
||||
|
||||
var baseAssetLibrary:String = Paths.getLibrary(_data.assetPath);
|
||||
var baseAssetPath:String = Paths.stripLibrary(_data.assetPath);
|
||||
|
||||
loadTextureAtlas(baseAssetPath, baseAssetLibrary, getAtlasSettings());
|
||||
|
||||
for (asset in assetList)
|
||||
{
|
||||
var subAssetLibrary:String = Paths.getLibrary(asset);
|
||||
var subAssetPath:String = Paths.stripLibrary(asset);
|
||||
|
||||
var clone:FunkinSprite = FunkinSprite.createTextureAtlas(0, 0, subAssetPath, subAssetLibrary, cast _data.atlasSettings);
|
||||
var subTexture:FlxAnimateFrames = clone.library;
|
||||
|
||||
trace('Concatenating texture atlas: ${asset}');
|
||||
subTexture.parent.destroyOnNoUse = false;
|
||||
|
||||
this.library.addAtlas(subTexture);
|
||||
}
|
||||
|
||||
if (_data.isPixel)
|
||||
{
|
||||
this.isPixel = true;
|
||||
this.antialiasing = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.isPixel = false;
|
||||
this.antialiasing = true;
|
||||
}
|
||||
|
||||
this.setScale(_data.scale);
|
||||
}
|
||||
|
||||
function loadAnimations():Void
|
||||
{
|
||||
trace('[MULTIATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}');
|
||||
|
||||
FlxAnimationUtil.addTextureAtlasAnimations(this, _data.animations);
|
||||
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.offsets == null)
|
||||
{
|
||||
setAnimationOffsets(anim.name, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
|
||||
}
|
||||
}
|
||||
|
||||
var animNames = this.anim.getNameList();
|
||||
trace('[MULTIATLASCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
|
||||
}
|
||||
|
||||
override function get_width():Float
|
||||
{
|
||||
return originalSizes.x;
|
||||
}
|
||||
|
||||
override function get_height():Float
|
||||
{
|
||||
return originalSizes.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration for the texture atlas.
|
||||
* @return The configuration for the texture atlas.
|
||||
*/
|
||||
public function getAtlasSettings():AtlasSpriteSettings
|
||||
{
|
||||
return cast _data.atlasSettings;
|
||||
}
|
||||
}
|
||||
|
|
@ -126,9 +126,4 @@ class MultiSparrowCharacter extends BaseCharacter
|
|||
var animNames = this.animation.getNameList();
|
||||
trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
|
||||
}
|
||||
|
||||
public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void
|
||||
{
|
||||
super.playAnimation(name, restart, ignoreOther, reverse);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ class ScriptedSparrowCharacter extends SparrowCharacter implements polymod.hscri
|
|||
@:hscriptClass
|
||||
class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements polymod.hscript.HScriptedClass {}
|
||||
|
||||
/**
|
||||
* A script that can be tied to a MultiAnimateAtlasCharacter, which persists across states.
|
||||
* Create a scripted class that extends MultiAnimateAtlasCharacter,
|
||||
* then call `super('charId')` in the constructor to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedMultiAnimateAtlasCharacter extends MultiAnimateAtlasCharacter implements polymod.hscript.HScriptedClass {}
|
||||
|
||||
/**
|
||||
* A script that can be tied to a PackerCharacter, which persists across states.
|
||||
* Create a scripted class that extends PackerCharacter,
|
||||
|
|
|
|||
|
|
@ -49,8 +49,6 @@ class SparrowCharacter extends BaseCharacter
|
|||
{
|
||||
this.isPixel = true;
|
||||
this.antialiasing = false;
|
||||
// pixelPerfectRender = true;
|
||||
// pixelPerfectPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ class HealthIcon extends FunkinSprite
|
|||
this.size.set(1.0, 1.0);
|
||||
this.iconOffset.set();
|
||||
this.flipX = false;
|
||||
this.updatePosition();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -212,6 +213,7 @@ class HealthIcon extends FunkinSprite
|
|||
}
|
||||
|
||||
this.flipX = data.flipX ?? false; // Face the OTHER way by default, since that is more common.
|
||||
this.updatePosition();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +224,12 @@ class HealthIcon extends FunkinSprite
|
|||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (bopEvery != 0) this.angle = MathUtil.smoothLerpPrecision(this.angle, 0, elapsed, 0.512);
|
||||
if (bopEvery != 0)
|
||||
{
|
||||
var dt:Float = elapsed * 60;
|
||||
|
||||
this.angle = MathUtil.smoothLerpPrecision(this.angle, 0, dt, 0.512);
|
||||
}
|
||||
|
||||
this.updatePosition();
|
||||
}
|
||||
|
|
@ -436,35 +443,6 @@ class HealthIcon extends FunkinSprite
|
|||
this.antialiasing = !isPixel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Name of the current animation being played by this health icon.
|
||||
*/
|
||||
public function getCurrentAnimation():String
|
||||
{
|
||||
if (this.animation == null || this.animation.curAnim == null) return "";
|
||||
return this.animation.curAnim.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The name of the animation to check for.
|
||||
* @return Whether this sprite posesses the given animation.
|
||||
* Only true if the animation was successfully loaded from the XML.
|
||||
*/
|
||||
public function hasAnimation(id:String):Bool
|
||||
{
|
||||
if (this.animation == null) return false;
|
||||
|
||||
return this.animation.getByName(id) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the current animation is in the finished state.
|
||||
*/
|
||||
public function isAnimationFinished():Bool
|
||||
{
|
||||
return this.animation.finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the animation with the given name.
|
||||
* @param name The name of the animation to play.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class Subtitles extends FlxSpriteGroup
|
|||
background.alpha = 0.5;
|
||||
add(background);
|
||||
|
||||
subtitleText = new SubtitlesText(0, 0, 30, Paths.font('vcr.ttf'));
|
||||
subtitleText = new SubtitlesText(0, 0, 30, 'VCR OSD Mono');
|
||||
add(subtitleText);
|
||||
|
||||
setText([], true);
|
||||
|
|
@ -165,6 +165,11 @@ class SubtitlesText extends FlxText
|
|||
}
|
||||
return Text;
|
||||
}
|
||||
|
||||
override function applyFormats(_:openfl.text.TextFormat, __:Bool = false):Void
|
||||
{
|
||||
// This function shouldn't get called because it messes up `htmlText`.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
|
|||
}
|
||||
else
|
||||
{
|
||||
trace('[WARNING] Unexpected state transition from ${this.state}');
|
||||
trace(' WARNING '.bg_yellow().bold() + ' Unexpected state transition from ${this.state}');
|
||||
this.state = ConversationState.Idle;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,12 +201,6 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
|
|||
applyAnimationOffsets(correctName);
|
||||
}
|
||||
|
||||
public function getCurrentAnimation():String
|
||||
{
|
||||
if (this.animation == null || this.animation.curAnim == null) return "";
|
||||
return this.animation.curAnim.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a given animation exists before playing it.
|
||||
* Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ class FocusCameraSongEvent extends SongEvent
|
|||
{
|
||||
name: 'ease',
|
||||
title: 'Easing Type',
|
||||
defaultValue: 'linear',
|
||||
defaultValue: 'CLASSIC',
|
||||
type: SongEventFieldType.ENUM,
|
||||
keys: [
|
||||
'Linear' => 'linear',
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class SetHealthIconSongEvent extends SongEvent
|
|||
trace('Applying Opponent health icon via song event: ${healthIconData.id}');
|
||||
PlayState.instance.iconP2.configure(healthIconData);
|
||||
default:
|
||||
trace('[WARN] Unknown character index: ' + data.value.char);
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Unknown character index: ' + data.value.char);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package funkin.play.notes;
|
|||
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
import flixel.FlxG;
|
||||
import funkin.play.notes.NoteVibrationsHandler.NoteStatus;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
||||
|
|
@ -9,11 +10,6 @@ import flixel.tweens.FlxEase;
|
|||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxSort;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.play.notes.NoteHoldCover;
|
||||
import funkin.play.notes.NoteSplash;
|
||||
import funkin.play.notes.NoteSprite;
|
||||
import funkin.play.notes.SustainTrail;
|
||||
import funkin.play.notes.NoteVibrationsHandler;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.util.GRhythmUtil;
|
||||
|
|
@ -69,7 +65,8 @@ class Strumline extends FlxSpriteGroup
|
|||
function get_renderDistanceMs():Float
|
||||
{
|
||||
if (useCustomRenderDistance) return customRenderDistanceMs;
|
||||
// Only divide by lower scroll speeds to fix renderDistance being too short. Dividing by higher scroll speeds breaks the input system by hitting later notes first!
|
||||
// Only divide by lower scroll speeds to fix renderDistance being too short.
|
||||
// Dividing by higher scroll speeds breaks the input system by hitting later notes first!
|
||||
return FlxG.height / Constants.PIXELS_PER_MS / (scrollSpeed < 1 ? scrollSpeed : 1);
|
||||
}
|
||||
|
||||
|
|
@ -146,7 +143,9 @@ class Strumline extends FlxSpriteGroup
|
|||
* The strumline notes (the receptors) themselves.
|
||||
*/
|
||||
public var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>;
|
||||
|
||||
var noteSplashes:FlxTypedSpriteGroup<NoteSplash>;
|
||||
|
||||
/**
|
||||
* Hold note covers.
|
||||
*/
|
||||
|
|
@ -159,17 +158,26 @@ class Strumline extends FlxSpriteGroup
|
|||
|
||||
var noteSpacingScale:Float = 1;
|
||||
|
||||
/**
|
||||
* The scale of the strumline. Use this to resize it rather than setting the scale directly.
|
||||
*/
|
||||
public var strumlineScale(default, null):FlxPoint;
|
||||
|
||||
#if FEATURE_GHOST_TAPPING
|
||||
var ghostTapTimer:Float = 0.0;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Handles note vibrations for this strumline
|
||||
*/
|
||||
public var noteVibrations:NoteVibrationsHandler = new NoteVibrationsHandler();
|
||||
|
||||
final inArrowContorlSchemeMode:Bool = #if mobile (Preferences.controlsScheme == FunkinHitboxControlSchemes.Arrows
|
||||
final inArrowControlSchemeMode:Bool = #if mobile (Preferences.controlsScheme == FunkinHitboxControlSchemes.Arrows
|
||||
&& !ControlsHandler.usingExternalInputDevice) #else false #end;
|
||||
|
||||
/**
|
||||
* Whether the strumline is downscroll.
|
||||
*/
|
||||
public var isDownscroll:Bool = #if mobile (Preferences.controlsScheme == FunkinHitboxControlSchemes.Arrows
|
||||
&& !ControlsHandler.usingExternalInputDevice)
|
||||
|| #end Preferences.downscroll;
|
||||
|
|
@ -187,7 +195,12 @@ class Strumline extends FlxSpriteGroup
|
|||
*/
|
||||
public var nextNoteIndex:Int = -1;
|
||||
|
||||
var heldKeys:Array<Bool> = [];
|
||||
/**
|
||||
* Indicates which keys are pressed for which directions.
|
||||
* The direction is pressed as long as at least one key is held,
|
||||
* and released when no keys are held.
|
||||
*/
|
||||
var heldKeys:Array<Array<Int>>;
|
||||
|
||||
static final BACKGROUND_PAD:Int = 16;
|
||||
|
||||
|
|
@ -229,7 +242,7 @@ class Strumline extends FlxSpriteGroup
|
|||
|
||||
var backgroundWidth:Float = KEY_COUNT * Strumline.NOTE_SPACING + BACKGROUND_PAD * 2;
|
||||
#if mobile
|
||||
if (inArrowContorlSchemeMode && isPlayer)
|
||||
if (inArrowControlSchemeMode && isPlayer)
|
||||
{
|
||||
backgroundWidth = backgroundWidth * 1.84;
|
||||
}
|
||||
|
|
@ -240,7 +253,7 @@ class Strumline extends FlxSpriteGroup
|
|||
this.background.scrollFactor.set(0, 0);
|
||||
this.background.x = -BACKGROUND_PAD;
|
||||
#if mobile
|
||||
if (inArrowContorlSchemeMode && isPlayer) this.background.x -= 100;
|
||||
if (inArrowControlSchemeMode && isPlayer) this.background.x -= 100;
|
||||
#end
|
||||
this.add(this.background);
|
||||
|
||||
|
|
@ -261,9 +274,10 @@ class Strumline extends FlxSpriteGroup
|
|||
this.strumlineNotes.add(child);
|
||||
}
|
||||
|
||||
this.heldKeys = [];
|
||||
for (i in 0...KEY_COUNT)
|
||||
{
|
||||
heldKeys.push(false);
|
||||
this.heldKeys[i] = [];
|
||||
}
|
||||
|
||||
strumlineScale.set(1, 1);
|
||||
|
|
@ -458,7 +472,7 @@ class Strumline extends FlxSpriteGroup
|
|||
* Enter mini mode, which displays only small strumline notes
|
||||
* @param scale scale of strumline
|
||||
*/
|
||||
public function enterMiniMode(scale:Float = 1)
|
||||
public function enterMiniMode(scale:Float = 1):Void
|
||||
{
|
||||
forEach(function(obj:flixel.FlxObject):Void {
|
||||
if (obj != strumlineNotes) obj.visible = false;
|
||||
|
|
@ -467,11 +481,15 @@ class Strumline extends FlxSpriteGroup
|
|||
this.strumlineScale.set(scale, scale);
|
||||
}
|
||||
|
||||
public function strumlineScaleCallback(Scale:FlxPoint)
|
||||
/**
|
||||
* Called whenever the `strumlineScale` value is updated.
|
||||
* @param Scale The new value.
|
||||
*/
|
||||
function strumlineScaleCallback(scale:FlxPoint):Void
|
||||
{
|
||||
strumlineNotes.forEach(function(note:StrumlineNote):Void {
|
||||
var styleScale = noteStyle.getStrumlineScale();
|
||||
note.scale.set(styleScale * Scale.x, styleScale * Scale.y);
|
||||
note.scale.set(styleScale * scale.x, styleScale * scale.y);
|
||||
});
|
||||
setNoteSpacing(noteSpacingScale);
|
||||
}
|
||||
|
|
@ -545,6 +563,9 @@ class Strumline extends FlxSpriteGroup
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every frame to update the position and hitbox of each child note.
|
||||
*/
|
||||
public function updateNotes():Void
|
||||
{
|
||||
if (noteData.length == 0) return;
|
||||
|
|
@ -566,7 +587,6 @@ class Strumline extends FlxSpriteGroup
|
|||
{
|
||||
// Note is in the past, skip it.
|
||||
nextNoteIndex = noteIndex + 1;
|
||||
// trace("Strumline: Skipping note at index " + noteIndex + " with strum time " + note.time);
|
||||
continue;
|
||||
}
|
||||
if (note.time > renderWindowStart) break; // Note is too far ahead to render
|
||||
|
|
@ -813,19 +833,29 @@ class Strumline extends FlxSpriteGroup
|
|||
/**
|
||||
* Called when a key is pressed.
|
||||
* @param dir The direction of the key that was pressed.
|
||||
* @param keyCode The key input used to press the direction. Used to distinguish when two keys for the same direction are pressed.
|
||||
*/
|
||||
public function pressKey(dir:NoteDirection):Void
|
||||
public function pressKey(dir:NoteDirection, keyCode:Int):Void
|
||||
{
|
||||
heldKeys[dir] = true;
|
||||
heldKeys[dir].push(keyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a key is released.
|
||||
* @param dir The direction of the key that was released.
|
||||
* @param keyCode The key input used to press the direction. Used to distinguish when two keys for the same direction are pressed.
|
||||
* If null, all keys for the direction are released.
|
||||
*/
|
||||
public function releaseKey(dir:NoteDirection):Void
|
||||
public function releaseKey(dir:NoteDirection, ?keyCode:Int):Void
|
||||
{
|
||||
heldKeys[dir] = false;
|
||||
if (keyCode == null)
|
||||
{
|
||||
heldKeys[dir].clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
heldKeys[dir].remove(keyCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -835,7 +865,7 @@ class Strumline extends FlxSpriteGroup
|
|||
*/
|
||||
public function isKeyHeld(dir:NoteDirection):Bool
|
||||
{
|
||||
return heldKeys[dir];
|
||||
return heldKeys[dir].length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -869,7 +899,7 @@ class Strumline extends FlxSpriteGroup
|
|||
cover.kill();
|
||||
}
|
||||
|
||||
heldKeys = [false, false, false, false];
|
||||
heldKeys = [[], [], [], []];
|
||||
|
||||
for (dir in DIRECTIONS)
|
||||
{
|
||||
|
|
@ -1113,7 +1143,7 @@ class Strumline extends FlxSpriteGroup
|
|||
|
||||
var trueScale = new FlxPoint(strumlineScale.x, strumlineScale.y);
|
||||
#if mobile
|
||||
if (inArrowContorlSchemeMode)
|
||||
if (inArrowControlSchemeMode)
|
||||
{
|
||||
final amplification:Float = (FlxG.width / FlxG.height) / (FlxG.initialWidth / FlxG.initialHeight);
|
||||
trueScale.set(strumlineScale.x - ((FlxG.height / FlxG.width) * 0.2) * amplification,
|
||||
|
|
@ -1300,7 +1330,7 @@ class Strumline extends FlxSpriteGroup
|
|||
{
|
||||
var pos:Float = 0;
|
||||
#if mobile
|
||||
if (inArrowContorlSchemeMode && isPlayer) pos = 35 * (FlxG.width / FlxG.height) / (FlxG.initialWidth / FlxG.initialHeight);
|
||||
if (inArrowControlSchemeMode && isPlayer) pos = 35 * (FlxG.width / FlxG.height) / (FlxG.initialWidth / FlxG.initialHeight);
|
||||
#end
|
||||
return switch (direction)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -175,22 +175,6 @@ class StrumlineNote extends FunkinSprite
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the animation that is currently playing.
|
||||
* If no animation is playing (usually this means the sprite is BROKEN!),
|
||||
* returns an empty string to prevent NPEs.
|
||||
*/
|
||||
public function getCurrentAnimation():String
|
||||
{
|
||||
if (this.animation == null || this.animation.curAnim == null) return "";
|
||||
return this.animation.curAnim.name;
|
||||
}
|
||||
|
||||
public function isAnimationFinished():Bool
|
||||
{
|
||||
return this.animation.finished;
|
||||
}
|
||||
|
||||
static final DEFAULT_OFFSET:Int = 13;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class NoteKind implements INoteScriptedClass
|
|||
* Only accessible in scripts
|
||||
* Defaults to true
|
||||
*/
|
||||
public var scoreable:Bool = true;
|
||||
public var scoreable(default, default):Bool = true;
|
||||
|
||||
public function new(noteKind:String, description:String = "", ?noteStyleId:String, ?params:Array<NoteKindParam>, ?noanim:Bool, ?suffix:String)
|
||||
{
|
||||
|
|
@ -62,13 +62,27 @@ class NoteKind implements INoteScriptedClass
|
|||
|
||||
/**
|
||||
* Retrieve all notes of this kind
|
||||
* @param visibleCheck If true, only visible notes will be returned
|
||||
* @return Array<NoteSprite>
|
||||
*/
|
||||
function getNotes():Array<NoteSprite>
|
||||
function getNotes(visibleCheck:Bool = false):Array<NoteSprite>
|
||||
{
|
||||
var allNotes:Array<NoteSprite> = PlayState.instance.playerStrumline.notes.members.concat(PlayState.instance.opponentStrumline.notes.members);
|
||||
return allNotes.filter(function(note:NoteSprite) {
|
||||
return note != null && note.noteData.kind == this.noteKind;
|
||||
return note != null && note.noteData.kind == this.noteKind && (!visibleCheck || note.visible);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all notes NOT of this kind
|
||||
* @param visibleCheck If true, only visible notes will be returned
|
||||
* @return Array<NoteSprite>
|
||||
*/
|
||||
function getOtherNotes(visibleCheck:Bool = false):Array<NoteSprite>
|
||||
{
|
||||
var allNotes:Array<NoteSprite> = PlayState.instance.playerStrumline.notes.members.concat(PlayState.instance.opponentStrumline.notes.members);
|
||||
return allNotes.filter(function(note:NoteSprite) {
|
||||
return note != null && note.noteData.kind != this.noteKind && (!visibleCheck || note.visible);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,12 +65,12 @@ class NoteKindManager
|
|||
|
||||
if (kind != null)
|
||||
{
|
||||
trace(' Loaded built-in note kind: ${kind.noteKind}');
|
||||
trace(' Loaded built-in note kind: ${kind.noteKind}');
|
||||
noteKinds.set(kind.noteKind, kind);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to load built-in note kind: ${noteKindClsName}');
|
||||
trace(' Failed to load built-in note kind: ${noteKindClsName}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
|
||||
target.antialiasing = !(_data.assets?.note?.isPixel ?? false);
|
||||
|
||||
var noteOffsets:Array<Float> = getNoteOffsets();
|
||||
target.offset.set(noteOffsets[0], noteOffsets[1]);
|
||||
|
||||
// Apply the animations.
|
||||
buildNoteAnimations(target);
|
||||
|
||||
|
|
@ -115,7 +118,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!FunkinSprite.isTextureCached(Paths.image(noteAssetPath)))
|
||||
if (!FunkinMemory.isTextureCached(Paths.image(noteAssetPath)))
|
||||
{
|
||||
FlxG.log.warn('Note texture is not cached: ${noteAssetPath}');
|
||||
}
|
||||
|
|
@ -180,6 +183,11 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
return _data.assets?.note?.scale ?? fallback?.getNoteScale() ?? 1.0;
|
||||
}
|
||||
|
||||
public function getNoteOffsets():Array<Float>
|
||||
{
|
||||
return _data?.assets?.note?.offsets ?? fallback?.getNoteOffsets() ?? [0.0, 0.0];
|
||||
}
|
||||
|
||||
function fetchNoteAnimationData(dir:NoteDirection):Null<AnimationData>
|
||||
{
|
||||
var result:Null<AnimationData> = switch (dir)
|
||||
|
|
@ -896,7 +904,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!FunkinSprite.isTextureCached(Paths.image(splashAssetPath)))
|
||||
if (!FunkinMemory.isTextureCached(Paths.image(splashAssetPath)))
|
||||
{
|
||||
FlxG.log.warn('Note Splash texture not cached: ${splashAssetPath}');
|
||||
}
|
||||
|
|
@ -1046,7 +1054,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!FunkinSprite.isTextureCached(Paths.image(holdCoverAssetPath)))
|
||||
if (!FunkinMemory.isTextureCached(Paths.image(holdCoverAssetPath)))
|
||||
{
|
||||
FlxG.log.warn('Hold Note Cover texture not cached: ${holdCoverAssetPath}');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
{
|
||||
if (!validateVariationId(vari))
|
||||
{
|
||||
trace(' [WARN] Variation id "$vari" is invalid, skipping...');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Variation id "$vari" is invalid, skipping...');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -162,19 +162,19 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
if (variMeta != null)
|
||||
{
|
||||
_metadata.set(variMeta.variation, variMeta);
|
||||
trace(' Loaded variation: $vari');
|
||||
trace(' Loaded variation: $vari');
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.log.warn('[SONG] Failed to load variation metadata (${id}:${vari}), is the path correct?');
|
||||
trace(' FAILED to load variation: $vari');
|
||||
trace(' FAILED to load variation: $vari');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_metadata.size() == 0)
|
||||
{
|
||||
trace('[WARN] Could not find song data for songId: $id');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Could not find song data for songId: $id');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -193,15 +193,15 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
* @param validScore Whether the song is elegible for highscores.
|
||||
* @return The constructed song object.
|
||||
*/
|
||||
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>,
|
||||
includeScript:Bool = true, validScore:Bool = false):Song
|
||||
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variation:String, charts:Map<String, SongChartData>, includeScript:Bool = true,
|
||||
validScore:Bool = false):Song
|
||||
{
|
||||
@:privateAccess
|
||||
var result:Null<Song> = null;
|
||||
|
||||
if (includeScript && SongRegistry.instance.isScriptedEntry(songId))
|
||||
if (includeScript && SongRegistry.instance.isScriptedEntry(songId, {variation: variation}))
|
||||
{
|
||||
var songClassName:Null<String> = SongRegistry.instance.getScriptedEntryClassName(songId);
|
||||
var songClassName:Null<String> = SongRegistry.instance.getScriptedEntryClassName(songId, {variation: variation});
|
||||
@:privateAccess
|
||||
if (songClassName != null) result = SongRegistry.instance.createScriptedEntry(songClassName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,13 +203,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
|
|||
}
|
||||
}
|
||||
|
||||
public function hasAnimation(id:String):Bool
|
||||
{
|
||||
if (this.animation == null) return false;
|
||||
|
||||
return this.animation.getByName(id) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a given animation exists before playing it.
|
||||
* Will gracefully check for name, then name with stripped suffixes, then fail to play.
|
||||
|
|
@ -327,27 +320,11 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
|
|||
this.animOffsets = offsets;
|
||||
}
|
||||
|
||||
public function isAnimationFinished():Bool
|
||||
{
|
||||
return this.animation?.finished ?? false;
|
||||
}
|
||||
|
||||
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
|
||||
{
|
||||
animationOffsets.set(name, [xOffset, yOffset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the animation that is currently playing.
|
||||
* If no animation is playing (usually this means the character is BROKEN!),
|
||||
* returns an empty string to prevent NPEs.
|
||||
*/
|
||||
public function getCurrentAnimation():String
|
||||
{
|
||||
if (this.animation == null || this.animation.curAnim == null) return "";
|
||||
return this.animation.curAnim.name;
|
||||
}
|
||||
|
||||
// override getScreenPosition (used by FlxSprite's draw method) to account for animation offsets.
|
||||
override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
|
||||
{
|
||||
|
|
|
|||
|
|
@ -161,13 +161,13 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
|
||||
for (dataProp in _data.props)
|
||||
{
|
||||
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
|
||||
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
|
||||
|
||||
var isSolidColor = dataProp.assetPath.startsWith('#');
|
||||
var isAnimated = dataProp.animations.length > 0;
|
||||
|
||||
var propSprite:StageProp;
|
||||
if (dataProp.danceEvery != 0)
|
||||
if (dataProp.danceEvery != 0 || isAnimated)
|
||||
{
|
||||
propSprite = new Bopper(dataProp.danceEvery);
|
||||
}
|
||||
|
|
@ -183,6 +183,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
{
|
||||
case 'packer':
|
||||
propSprite.loadPacker(dataProp.assetPath);
|
||||
case 'animateatlas':
|
||||
propSprite.loadTextureAtlas(dataProp.assetPath, _data.directory, cast dataProp.atlasSettings);
|
||||
default: // 'sparrow'
|
||||
propSprite.loadSparrow(dataProp.assetPath);
|
||||
}
|
||||
|
|
@ -215,7 +217,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
if (propSprite.frames == null || propSprite.frames.numFrames == 0)
|
||||
{
|
||||
@:privateAccess
|
||||
trace(' ERROR: Could not build texture for prop. Check the asset path (${Paths.currentLevel ?? 'default'}, ${dataProp.assetPath}).');
|
||||
trace(' ERROR: Could not build texture for prop. Check the asset path (${Paths.currentLevel ?? 'default'}, ${dataProp.assetPath}).');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -268,6 +270,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
|
||||
}
|
||||
}
|
||||
case 'animateatlas':
|
||||
FlxAnimationUtil.addTextureAtlasAnimations(propSprite, dataProp.animations);
|
||||
default: // 'sparrow'
|
||||
FlxAnimationUtil.addAtlasAnimations(propSprite, dataProp.animations);
|
||||
if (Std.isOfType(propSprite, Bopper))
|
||||
|
|
@ -293,9 +297,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
}
|
||||
}
|
||||
|
||||
if (dataProp.startingAnimation != null)
|
||||
if (dataProp.startingAnimation != null && propSprite is Bopper)
|
||||
{
|
||||
propSprite.animation.play(dataProp.startingAnimation);
|
||||
cast(propSprite, Bopper).playAnimation(dataProp.startingAnimation);
|
||||
}
|
||||
|
||||
if (Std.isOfType(propSprite, BaseCharacter))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
96
source/funkin/save/SaveProperty.hx
Normal file
96
source/funkin/save/SaveProperty.hx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package funkin.save;
|
||||
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
|
||||
@:nullSafety
|
||||
class SaveProperty<T>
|
||||
{
|
||||
var _value:T;
|
||||
var _getter:Null<Void->T>;
|
||||
var _setter:Null<T->Void>;
|
||||
var _autoFlush:Bool;
|
||||
|
||||
public var onChange(default, null):FlxTypedSignal<T->Void>;
|
||||
|
||||
public var value(get, set):T;
|
||||
|
||||
function get_value():T
|
||||
{
|
||||
return _getter != null ? _getter() : _value;
|
||||
}
|
||||
|
||||
function set_value(newValue:T):T
|
||||
{
|
||||
var oldValue:T = get_value();
|
||||
|
||||
if (oldValue != newValue)
|
||||
{
|
||||
if (_setter != null) _setter(newValue);
|
||||
else
|
||||
_value = newValue;
|
||||
|
||||
if (_autoFlush) Save.system.flush();
|
||||
|
||||
trace('[Save Property]: Changing value of $oldValue TO $newValue');
|
||||
|
||||
onChange.dispatch(newValue);
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
public function new(initialValue:T, ?getter:Void->T, ?setter:T->Void, autoFlush:Bool = true)
|
||||
{
|
||||
_value = initialValue;
|
||||
_getter = getter;
|
||||
_setter = setter;
|
||||
_autoFlush = autoFlush;
|
||||
onChange = new FlxTypedSignal<T->Void>();
|
||||
}
|
||||
|
||||
public function bind(callback:T->Void, fireImmediately:Bool = true):Void
|
||||
{
|
||||
onChange.add(callback);
|
||||
if (fireImmediately) callback(get_value());
|
||||
}
|
||||
|
||||
public function bindOnce(callback:T->Void, fireImmediately:Bool = true):Void
|
||||
{
|
||||
onChange.addOnce(callback);
|
||||
if (fireImmediately) callback(get_value());
|
||||
}
|
||||
|
||||
public function unbind(callback:T->Void):Void
|
||||
{
|
||||
onChange.remove(callback);
|
||||
}
|
||||
|
||||
public function unbindAll():Void
|
||||
{
|
||||
onChange.removeAll();
|
||||
}
|
||||
|
||||
public function destroy():Void
|
||||
{
|
||||
onChange.destroy();
|
||||
_getter = null;
|
||||
_setter = null;
|
||||
}
|
||||
|
||||
@:op(A == B)
|
||||
public function equals(other:T):Bool
|
||||
{
|
||||
return get_value() == other;
|
||||
}
|
||||
|
||||
@:op(A != B)
|
||||
public function notEquals(other:T):Bool
|
||||
{
|
||||
return get_value() != other;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return Std.string(get_value());
|
||||
}
|
||||
}
|
||||
81
source/funkin/save/SaveSystem.hx
Normal file
81
source/funkin/save/SaveSystem.hx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package funkin.save;
|
||||
|
||||
import funkin.save.migrator.RawSaveData_v1_0_0;
|
||||
import funkin.save.migrator.SaveDataMigrator;
|
||||
import flixel.util.FlxSave;
|
||||
|
||||
/**
|
||||
* A bit more of the backend and nitty gritty of FNF's save system
|
||||
*/
|
||||
class SaveSystem
|
||||
{
|
||||
public function new():Void {}
|
||||
|
||||
/**
|
||||
* Call this to make sure the save data is written to disk.
|
||||
*/
|
||||
public function flush():Void
|
||||
{
|
||||
FlxG.save.flush();
|
||||
}
|
||||
|
||||
public function clearSlot(slot:Int):Save
|
||||
{
|
||||
FlxG.save.bind(Constants.SAVE_NAME + slot, Constants.SAVE_PATH);
|
||||
|
||||
if (FlxG.save.status == EMPTY) return new Save();
|
||||
|
||||
// Archive the save data just in case.
|
||||
// Not reliable but better than nothing.
|
||||
var backupSlot:Int = Save.system.archiveBadSaveData(FlxG.save.data);
|
||||
|
||||
FlxG.save.erase();
|
||||
return new Save();
|
||||
}
|
||||
|
||||
public function fetchLegacySaveData():Option<RawSaveData_v1_0_0>
|
||||
{
|
||||
trace("[SAVE] Checking for legacy save data...");
|
||||
var legacySave:FlxSave = new FlxSave();
|
||||
legacySave.bind(Constants.SAVE_NAME_LEGACY, Constants.SAVE_PATH_LEGACY);
|
||||
|
||||
if (legacySave.isEmpty())
|
||||
{
|
||||
trace("[SAVE] No legacy save data found.");
|
||||
return None;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace("[SAVE] Legacy save data found.");
|
||||
return Some(cast legacySave.data);
|
||||
}
|
||||
}
|
||||
|
||||
public function archiveBadSaveData(data:Dynamic):Int
|
||||
{
|
||||
// We want to save this somewhere so we can try to recover it for the user in the future!
|
||||
final RECOVERY_SLOT_START = 1000;
|
||||
return writeToAvailableSlot(RECOVERY_SLOT_START, data);
|
||||
}
|
||||
|
||||
function writeToAvailableSlot(slot:Int, data:Dynamic):Int
|
||||
{
|
||||
trace('[SAVE] Finding slot to write data to (starting with ${slot})...');
|
||||
|
||||
var targetSaveData:FlxSave = new FlxSave();
|
||||
targetSaveData.bind(Constants.SAVE_NAME + slot, Constants.SAVE_PATH);
|
||||
while (!targetSaveData.isEmpty())
|
||||
{
|
||||
// Keep trying to bind to slots until we find an empty slot.
|
||||
trace('[SAVE] Slot ${slot} is taken, continuing...');
|
||||
slot++;
|
||||
targetSaveData.bind(Constants.SAVE_NAME + slot, Constants.SAVE_PATH);
|
||||
}
|
||||
|
||||
trace('[SAVE] Writing data to slot ${slot}...');
|
||||
targetSaveData.mergeData(data, true);
|
||||
|
||||
trace('[SAVE] Data written to slot ${slot}!');
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ class SaveDataMigrator
|
|||
{
|
||||
trace('[SAVE] No version found in save data! Returning blank data.');
|
||||
trace(inputData);
|
||||
return new Save(Save.getDefault());
|
||||
return new Save(Save.getDefaultData());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -28,7 +28,7 @@ class SaveDataMigrator
|
|||
if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
|
||||
{
|
||||
// Import the structured data.
|
||||
var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData);
|
||||
var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefaultData(), inputData);
|
||||
var save:Save = new Save(saveDataWithDefaults);
|
||||
return save;
|
||||
}
|
||||
|
|
@ -38,11 +38,13 @@ class SaveDataMigrator
|
|||
}
|
||||
else
|
||||
{
|
||||
var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.';
|
||||
var slot:Int = Save.archiveBadSaveData(inputData);
|
||||
var fullMessage:String = 'An error occurred migrating your save data.\n${message}\nInvalid data has been moved to save slot ${slot}.';
|
||||
funkin.util.WindowUtil.showError("Save Data Failure", fullMessage);
|
||||
return new Save(Save.getDefault());
|
||||
var slot:Int = Save.system.archiveBadSaveData(inputData);
|
||||
var message:String = 'An error occurred migrating your save data.'
|
||||
+ '\nError migrating save data, expected ${Save.SAVE_DATA_VERSION}.'
|
||||
+ '\nInvalid data has been moved to save slot ${slot}.';
|
||||
|
||||
funkin.util.WindowUtil.showError("Save Data Failure", message);
|
||||
return new Save(Save.getDefaultData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +52,7 @@ 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);
|
||||
var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefaultData(), inputData);
|
||||
|
||||
// Reset these values to valid ones.
|
||||
saveDataWithDefaults.optionsChartEditor.chartEditorLiveInputStyle = funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle.None;
|
||||
|
|
@ -68,12 +70,12 @@ class SaveDataMigrator
|
|||
{
|
||||
var inputSaveData:RawSaveData_v1_0_0 = cast inputData;
|
||||
|
||||
var result:Save = new Save(Save.getDefault());
|
||||
var result:Save = new Save(Save.getDefaultData());
|
||||
|
||||
result.volume = inputSaveData.volume;
|
||||
result.mute = inputSaveData.mute;
|
||||
result.volume.value = inputSaveData.volume;
|
||||
result.mute.value = inputSaveData.mute;
|
||||
|
||||
result.ngSessionId = inputSaveData.sessionId;
|
||||
result.ngSessionId.value = inputSaveData.sessionId;
|
||||
|
||||
// TODO: Port over the save data from the legacy save data format.
|
||||
migrateLegacyScores(result, inputSaveData);
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ import flixel.tweens.FlxEase;
|
|||
import flixel.tweens.FlxTween;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.util.FlxAxes;
|
||||
import flixel.util.FlxHorizontalAlign;
|
||||
import flixel.util.FlxVerticalAlign;
|
||||
import flixel.FlxG;
|
||||
import openfl.display.Bitmap;
|
||||
import openfl.display.BitmapData;
|
||||
import funkin.util.MathUtil;
|
||||
import funkin.graphics.FunkinCamera;
|
||||
|
||||
class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
||||
{
|
||||
|
|
@ -95,6 +98,15 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
|||
@:noCompletion
|
||||
private static var cutoutBitmaps:Array<Bitmap> = [null, null];
|
||||
|
||||
@:noCompletion
|
||||
private static var mustAwait:Bool = false;
|
||||
|
||||
@:noCompletion
|
||||
private static var awaitedSize:FlxPoint = FlxPoint.get(0, 0);
|
||||
|
||||
@:noCompletion
|
||||
private static var finishingAwait:Bool = false;
|
||||
|
||||
/**
|
||||
* Constructor for `FullScreenScaleMode`.
|
||||
*
|
||||
|
|
@ -119,6 +131,61 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
|||
*/
|
||||
override public function onMeasure(Width:Int, Height:Int):Void
|
||||
{
|
||||
if (mustAwait)
|
||||
{
|
||||
onMeasureAwait(Width, Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
onMeasureInstant(Width, Height);
|
||||
mustAwait = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the game to the current aspect ratio and assignes the requested resolution as awaited for later.
|
||||
* @param Width The width of the screen.
|
||||
* @param Height The height of the screen.
|
||||
*/
|
||||
public function onMeasureAwait(Width:Int, Height:Int):Void
|
||||
{
|
||||
horizontalAlign = CENTER;
|
||||
verticalAlign = CENTER;
|
||||
|
||||
updateGameSize(FlxG.width, FlxG.height);
|
||||
updateDeviceSize(Width, Height);
|
||||
#if mobile
|
||||
updateDeviceNotch(funkin.mobile.util.ScreenUtil.getNotchRect());
|
||||
#end
|
||||
updateScaleOffset();
|
||||
updateGamePosition();
|
||||
|
||||
awaitedSize.set(Width, Height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock the game resolution and swap into the awaited one.
|
||||
*/
|
||||
public function onMeasurePostAwait():Void
|
||||
{
|
||||
if (awaitedSize.x == 0 && awaitedSize.y == 0) return;
|
||||
|
||||
horizontalAlign = enabled ? LEFT : CENTER;
|
||||
verticalAlign = enabled ? TOP : CENTER;
|
||||
onMeasureInstant(Math.ceil(awaitedSize.x), Math.ceil(awaitedSize.y));
|
||||
FlxG.cameras.reset(new FunkinCamera('default'));
|
||||
|
||||
awaitedSize.set(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantly apply the measured resolution to the game
|
||||
* @param Width The width of the screen.
|
||||
* @param Height The height of the screen.
|
||||
*/
|
||||
public function onMeasureInstant(Width:Int, Height:Int):Void
|
||||
{
|
||||
finishingAwait = true;
|
||||
untyped FlxG.width = FlxG.initialWidth;
|
||||
untyped FlxG.height = FlxG.initialHeight;
|
||||
|
||||
|
|
@ -132,6 +199,8 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
|||
updateGamePosition();
|
||||
|
||||
adjustGameSize();
|
||||
|
||||
finishingAwait = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -204,7 +273,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
|||
{
|
||||
if (bitmap == null)
|
||||
{
|
||||
trace("[WARNING] Tried to remove a cutout bar but there don't seem to be any.");
|
||||
trace(" WARNING ".bg_yellow().bold() + " Tried to remove a cutout bar but there don't seem to be any.");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -265,12 +334,50 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
|||
|
||||
override public function updateScaleOffset():Void
|
||||
{
|
||||
scale.x = ratioAxis == X ? logicalSize.x / FlxG.width : deviceSize.x / FlxG.width;
|
||||
scale.y = ratioAxis == Y ? logicalSize.y / FlxG.height : deviceSize.y / FlxG.height;
|
||||
if (finishingAwait)
|
||||
{
|
||||
scale.x = ratioAxis == X ? logicalSize.x / FlxG.width : deviceSize.x / FlxG.width;
|
||||
scale.y = ratioAxis == Y ? logicalSize.y / FlxG.height : deviceSize.y / FlxG.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
scale.x = deviceSize.x / FlxG.width;
|
||||
scale.y = deviceSize.y / FlxG.height;
|
||||
|
||||
if (scale.x > scale.y) scale.x = scale.y;
|
||||
else
|
||||
scale.y = scale.x;
|
||||
}
|
||||
updateOffsetX();
|
||||
updateOffsetY();
|
||||
}
|
||||
|
||||
override function updateOffsetX():Void
|
||||
{
|
||||
offset.x = switch (horizontalAlign)
|
||||
{
|
||||
case FlxHorizontalAlign.LEFT:
|
||||
0;
|
||||
case FlxHorizontalAlign.CENTER:
|
||||
Math.ceil(finishingAwait ? (deviceSize.x - gameSize.x) : (deviceSize.x - (gameSize.x * scale.x)) * 0.5);
|
||||
case FlxHorizontalAlign.RIGHT:
|
||||
deviceSize.x - gameSize.x;
|
||||
}
|
||||
}
|
||||
|
||||
override function updateOffsetY():Void
|
||||
{
|
||||
offset.y = switch (verticalAlign)
|
||||
{
|
||||
case FlxVerticalAlign.TOP:
|
||||
0;
|
||||
case FlxVerticalAlign.CENTER:
|
||||
Math.ceil(finishingAwait ? (deviceSize.y - gameSize.y) : (deviceSize.y - (gameSize.y * scale.y)) * 0.5);
|
||||
case FlxVerticalAlign.BOTTOM:
|
||||
deviceSize.y - gameSize.y;
|
||||
}
|
||||
}
|
||||
|
||||
#if mobile
|
||||
private function updateDeviceNotch(notch:lime.math.Rectangle):Void
|
||||
{
|
||||
|
|
@ -319,7 +426,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
|||
var gameHeight:Float = gameSize.y / scale.y;
|
||||
|
||||
#if desktop
|
||||
if (MathUtil.gcd(FlxG.width, Math.ceil(gameHeight)) == 1)
|
||||
if (MathUtil.gcd(FlxG.width, Math.ceil(gameHeight)) == 1 || maxRatioAxis != ratioAxis)
|
||||
{
|
||||
gameSize.y -= cutoutSize.y;
|
||||
offset.y = Math.ceil((deviceSize.y - gameSize.y) * 0.5);
|
||||
|
|
@ -357,7 +464,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
|||
var gameWidth:Float = gameSize.x / scale.x;
|
||||
|
||||
#if desktop
|
||||
if (MathUtil.gcd(Math.ceil(gameWidth), FlxG.height) == 1)
|
||||
if (MathUtil.gcd(Math.ceil(gameWidth), FlxG.height) == 1 || maxRatioAxis != ratioAxis)
|
||||
{
|
||||
gameSize.x -= cutoutSize.x;
|
||||
offset.x = Math.ceil((deviceSize.x - gameSize.x) * 0.5);
|
||||
|
|
@ -409,6 +516,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
|||
|
||||
if (instance != null)
|
||||
{
|
||||
mustAwait = false;
|
||||
instance.horizontalAlign = enabled ? LEFT : CENTER;
|
||||
instance.verticalAlign = enabled ? TOP : CENTER;
|
||||
instance.onMeasure(FlxG.stage.stageWidth, FlxG.stage.stageHeight);
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
|
|||
#end
|
||||
|
||||
// Todo: bypass popup blocker on firefox
|
||||
if (controls.ACCEPT) accept();
|
||||
if (controls.ACCEPT_P) accept();
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import funkin.modding.module.ModuleHandler;
|
|||
import funkin.util.SortUtil;
|
||||
import funkin.util.WindowUtil;
|
||||
import funkin.input.Controls;
|
||||
import funkin.ui.FullScreenScaleMode;
|
||||
#if mobile
|
||||
import funkin.graphics.FunkinCamera;
|
||||
import funkin.mobile.ui.FunkinHitbox;
|
||||
|
|
@ -55,6 +56,8 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
|
|||
|
||||
public function new()
|
||||
{
|
||||
if (FullScreenScaleMode.instance != null) FullScreenScaleMode.instance.onMeasurePostAwait();
|
||||
|
||||
super();
|
||||
|
||||
initCallbacks();
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class Page<T:PageName> extends FlxGroup
|
|||
|
||||
function updateEnabled(elapsed:Float)
|
||||
{
|
||||
if (canExit && controls.BACK)
|
||||
if (canExit && controls.BACK_P)
|
||||
{
|
||||
exit();
|
||||
FunkinSound.playOnce(Paths.sound('cancelMenu'));
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class PixelatedIcon extends FlxFilteredSprite
|
|||
|
||||
if (!Assets.exists(Paths.image(charPath)))
|
||||
{
|
||||
trace('[WARN] Character ${char} has no freeplay icon.');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Character ${char} has no freeplay icon.');
|
||||
this.visible = false;
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
package funkin.ui.charSelect;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
|
||||
@:nullSafety
|
||||
class CharIcon extends FlxSprite
|
||||
{
|
||||
public var locked:Bool = false;
|
||||
|
||||
public function new(x:Float, y:Float, locked:Bool = false)
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
this.locked = locked;
|
||||
|
||||
makeGraphic(128, 128);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
package funkin.ui.charSelect;
|
||||
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.filters.DropShadowFilter;
|
||||
import openfl.filters.ConvolutionFilter;
|
||||
import funkin.graphics.shaders.StrokeShader;
|
||||
|
||||
@:nullSafety
|
||||
class CharIconCharacter extends CharIcon
|
||||
{
|
||||
// public var dropShadowFilter:DropShadowFilter;
|
||||
|
||||
var matrixFilter:Array<Float> = [
|
||||
1, 1, 1,
|
||||
1, 1, 1,
|
||||
1, 1, 1
|
||||
];
|
||||
|
||||
var divisor:Int = 1;
|
||||
var bias:Int = 0;
|
||||
// var convolutionFilter:ConvolutionFilter;
|
||||
|
||||
// public var noDropShadow:BitmapData;
|
||||
// public var withDropShadow:BitmapData;
|
||||
|
||||
// var strokeShader:StrokeShader;
|
||||
|
||||
public function new(path:String)
|
||||
{
|
||||
super(0, 0, false);
|
||||
|
||||
loadGraphic(Paths.image('freeplay/icons/' + path + 'pixel'));
|
||||
setGraphicSize(128, 128);
|
||||
updateHitbox();
|
||||
antialiasing = false;
|
||||
|
||||
// strokeShader = new StrokeShader();
|
||||
// shader = strokeShader;
|
||||
|
||||
// noDropShadow = pixels.clone();
|
||||
|
||||
// dropShadowFilter = new DropShadowFilter(5, 45, 0, 1, 0, 0);
|
||||
// convolutionFilter = new ConvolutionFilter(3, 3, matrixFilter, divisor, bias);
|
||||
// pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), dropShadowFilter);
|
||||
// pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), convolutionFilter);
|
||||
// withDropShadow = pixels.clone();
|
||||
|
||||
// pixels = noDropShadow.clone();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package funkin.ui.charSelect;
|
||||
|
||||
@:nullSafety
|
||||
class CharIconLocked extends CharIcon {}
|
||||
45
source/funkin/ui/charSelect/CharSelectAtlasHandler.hx
Normal file
45
source/funkin/ui/charSelect/CharSelectAtlasHandler.hx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package funkin.ui.charSelect;
|
||||
|
||||
import animate.FlxAnimateFrames;
|
||||
import flixel.FlxG;
|
||||
|
||||
/**
|
||||
* Utility class for handling the atlases loaded by CharSelect & co. in an efficient way.
|
||||
* TODO: Maybe this should be a general utility class instead?
|
||||
*/
|
||||
class CharSelectAtlasHandler
|
||||
{
|
||||
static final framesCache:Map<String, FlxAnimateFrames> = [];
|
||||
|
||||
public static function loadAtlas(path:String, ?settings:FlxAnimateSettings):Null<FlxAnimateFrames>
|
||||
{
|
||||
if (framesCache.exists(path)) return framesCache.get(path);
|
||||
|
||||
var result:FlxAnimateFrames = FlxAnimateFrames.fromAnimate(Paths.animateAtlas(path),
|
||||
{
|
||||
swfMode: settings?.swfMode ?? true,
|
||||
filterQuality: settings?.filterQuality ?? MEDIUM,
|
||||
cacheOnLoad: settings?.cacheOnLoad ?? false
|
||||
});
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
FlxG.log.error('Failed to load atlas at path $path!');
|
||||
return null;
|
||||
}
|
||||
|
||||
result.parent.destroyOnNoUse = false;
|
||||
framesCache.set(path, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static function clearAtlasCache():Void
|
||||
{
|
||||
for (frames in framesCache.iterator())
|
||||
{
|
||||
// NOTE: Doing this already calls checkUseCount!
|
||||
frames.parent.destroyOnNoUse = true;
|
||||
}
|
||||
framesCache.clear();
|
||||
}
|
||||
}
|
||||
136
source/funkin/ui/charSelect/CharSelectCursors.hx
Normal file
136
source/funkin/ui/charSelect/CharSelectCursors.hx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package funkin.ui.charSelect;
|
||||
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.math.FlxPoint;
|
||||
import openfl.display.BlendMode;
|
||||
import flixel.group.FlxSpriteContainer.FlxTypedSpriteContainer;
|
||||
import funkin.util.MathUtil;
|
||||
|
||||
class CharSelectCursors extends FlxTypedSpriteContainer<FunkinSprite>
|
||||
{
|
||||
/**
|
||||
* The main cursor sprite for this class.
|
||||
*/
|
||||
public var main:FunkinSprite;
|
||||
|
||||
var lightBlue:FunkinSprite;
|
||||
var darkBlue:FunkinSprite;
|
||||
|
||||
var cursorConfirmed:FunkinSprite;
|
||||
var cursorDenied:FunkinSprite;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
darkBlue = new FunkinSprite(0, 0);
|
||||
lightBlue = new FunkinSprite(0, 0);
|
||||
main = new FunkinSprite(0, 0);
|
||||
|
||||
cursorConfirmed = new FunkinSprite(0, 0);
|
||||
cursorDenied = new FunkinSprite(0, 0);
|
||||
|
||||
darkBlue.loadGraphic(Paths.image('charSelect/charSelector'));
|
||||
lightBlue.loadGraphic(Paths.image('charSelect/charSelector'));
|
||||
main.loadGraphic(Paths.image('charSelect/charSelector'));
|
||||
|
||||
darkBlue.color = 0xFF3C74F7;
|
||||
lightBlue.color = 0xFF3EBBFF;
|
||||
main.color = 0xFFFFFF00;
|
||||
FlxTween.color(main, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: PINGPONG});
|
||||
|
||||
darkBlue.blend = BlendMode.SCREEN;
|
||||
lightBlue.blend = BlendMode.SCREEN;
|
||||
|
||||
add(darkBlue);
|
||||
add(lightBlue);
|
||||
add(main);
|
||||
|
||||
cursorConfirmed.frames = Paths.getSparrowAtlas("charSelect/charSelectorConfirm");
|
||||
cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED instance 1", 24, true);
|
||||
cursorConfirmed.visible = false;
|
||||
add(cursorConfirmed);
|
||||
|
||||
cursorDenied.frames = Paths.getSparrowAtlas("charSelect/charSelectorDenied");
|
||||
cursorDenied.animation.addByPrefix("idle", "cursor DENIED instance 1", 24, false);
|
||||
cursorDenied.visible = false;
|
||||
add(cursorDenied);
|
||||
|
||||
scrollFactor.set();
|
||||
directAlpha = true;
|
||||
}
|
||||
|
||||
public function confirm():Void
|
||||
{
|
||||
cursorConfirmed.visible = true;
|
||||
cursorConfirmed.animation.play("idle", true);
|
||||
|
||||
main.visible = lightBlue.visible = darkBlue.visible = false;
|
||||
}
|
||||
|
||||
public function resetDeny():Void
|
||||
{
|
||||
cursorDenied.visible = false;
|
||||
}
|
||||
|
||||
public function deny():Void
|
||||
{
|
||||
cursorDenied.visible = true;
|
||||
cursorDenied.animation.play('idle', true);
|
||||
cursorDenied.animation.onFinish.add((_) -> {
|
||||
cursorDenied.visible = false;
|
||||
});
|
||||
}
|
||||
|
||||
public function unconfirm():Void
|
||||
{
|
||||
cursorConfirmed.visible = false;
|
||||
main.visible = lightBlue.visible = darkBlue.visible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snaps the cursors to the given position.
|
||||
* @param intendedPosition The position to snap to as a `FlxPoint`.
|
||||
*/
|
||||
public function snapToLocation(intendedPosition:FlxPoint):Void
|
||||
{
|
||||
main.x = intendedPosition.x;
|
||||
main.y = intendedPosition.y;
|
||||
|
||||
lightBlue.x = main.x;
|
||||
lightBlue.y = main.y;
|
||||
|
||||
darkBlue.x = intendedPosition.x;
|
||||
darkBlue.y = intendedPosition.y;
|
||||
|
||||
cursorConfirmed.x = main.x - 2;
|
||||
cursorConfirmed.y = main.y - 4;
|
||||
|
||||
cursorDenied.x = main.x - 2;
|
||||
cursorDenied.y = main.y - 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lerps the cursors to the given position.
|
||||
* @param intendedPosition The position to lerp to as a `FlxPoint`.
|
||||
*/
|
||||
public function lerpToLocation(intendedPosition:FlxPoint):Void
|
||||
{
|
||||
main.x = MathUtil.snap(MathUtil.smoothLerpPrecision(main.x, intendedPosition.x, FlxG.elapsed, 0.1), intendedPosition.x, 1);
|
||||
main.y = MathUtil.snap(MathUtil.smoothLerpPrecision(main.y, intendedPosition.y, FlxG.elapsed, 0.1), intendedPosition.y, 1);
|
||||
|
||||
lightBlue.x = MathUtil.smoothLerpPrecision(lightBlue.x, main.x, FlxG.elapsed, 0.202);
|
||||
lightBlue.y = MathUtil.smoothLerpPrecision(lightBlue.y, main.y, FlxG.elapsed, 0.202);
|
||||
|
||||
darkBlue.x = MathUtil.smoothLerpPrecision(darkBlue.x, intendedPosition.x, FlxG.elapsed, 0.404);
|
||||
darkBlue.y = MathUtil.smoothLerpPrecision(darkBlue.y, intendedPosition.y, FlxG.elapsed, 0.404);
|
||||
|
||||
cursorConfirmed.x = main.x - 2;
|
||||
cursorConfirmed.y = main.y - 4;
|
||||
|
||||
cursorDenied.x = main.x - 2;
|
||||
cursorDenied.y = main.y - 4;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue