diff --git a/.gitignore b/.gitignore
index b013f65..4a82dc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
assets/images/gay porn/
mods/
+dump/
export/
.vscode/
.DS_Store
diff --git a/CREDITS.md b/CREDITS.md
index 018305f..311d34a 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -25,7 +25,7 @@ NOTE: THIS WILL BE MOVED TO THE CREDITS STATE WHEN IT'S FINISHED, including link
## SPECIAL THANKS
**funkin' crew** - made FRIDAY NIGHT FUNKIN'!! damn!! proprietary of most assets, borrowed parts of backend class implementations
**psych engine** - formats support, discord rpc base, paths implementations, a lot of ideas generally
-**codename engine** - chart format support
+**codename engine** - chart format support, "play animation context"
**sword** - some useful pointers
**crowplexus** - some useful pointers (crash handler)
**unholywanderer04** - obligatory unholywanderer04 mention
diff --git a/Project.xml b/Project.xml
index 4a61901..5b81ad9 100644
--- a/Project.xml
+++ b/Project.xml
@@ -60,11 +60,10 @@
+
-
-
diff --git a/README.md b/README.md
index 7142748..a3c700a 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ haxelib install moonchart
haxelib install hxdiscord_rpc
haxelib git funkin.vis https://github.com/FunkinCrew/funkVis
haxelib git grig.audio https://gitlab.com/haxe-grig/grig.audio.git
-haxelib git flxanimate https://github.com/Dot-Stuff/flxanimate.git dev
+haxelib git flxanimate https://github.com/Dot-Stuff/flxanimate.git 884606823b39b41ae460cd5f0ec1a07310654aa2
haxelib git hscript-iris https://github.com/pisayesiwsi/hscript-iris.git dev
```
(hscript-iris and flxanimate use indev versions)
diff --git a/assets/data/stages/phillyBlazin.json b/assets/data/stages/phillyBlazin.json
index 314684d..66024a0 100644
--- a/assets/data/stages/phillyBlazin.json
+++ b/assets/data/stages/phillyBlazin.json
@@ -61,7 +61,7 @@
"bf": {
"zIndex": 2000,
"scale": 1.75,
- "position": [-237, 100],
+ "position": [1400, 1660],
"cameraOffsets": [-350, -100]
},
"gf": {
@@ -72,7 +72,7 @@
"dad": {
"zIndex": 3000,
"scale": 1.75,
- "position": [-237, 150],
+ "position": [1480, 1660],
"cameraOffsets": [500, 200]
}
}
diff --git a/assets/data/styles/notes/funkin-6k.json b/assets/data/styles/notes/funkin-6k.json
new file mode 100644
index 0000000..bb32603
--- /dev/null
+++ b/assets/data/styles/notes/funkin-6k.json
@@ -0,0 +1,136 @@
+{
+ "version": "1.0.0",
+ "name": "Funkin' (6 Key)",
+ "author": "PhantomArcade",
+ "data": {
+ "general": {
+ "laneSpacing": 140,
+ "directions": [ //
+ {
+ "name": "left",
+ "sing": "singLEFT",
+ "colorSave": "funkin-left",
+ "keybindSave": "funkin-0-6k",
+ "defaultColors": ["#C24B99", "#FFFFFF", "#3C1F56"]
+ },
+ {
+ "name": "up",
+ "sing": "singUP",
+ "colorSave": "funkin-up",
+ "keybindSave": "funkin-1-6k",
+ "defaultColors": ["#12FA05", "#FFFFFF", "#0A4447"]
+ },
+ {
+ "name": "right",
+ "sing": "singRIGHT",
+ "colorSave": "funkin-right",
+ "keybindSave": "funkin-2-6k",
+ "defaultColors": ["#F9393F", "#FFFFFF", "#651038"]
+ },
+ {
+ "name": "left",
+ "sing": "singLEFT",
+ "colorSave": "funkin-left-alt",
+ "keybindSave": "funkin-3-6k",
+ "defaultColors": ["#FFFF00", "#FFFFFF", "#993300"]
+ },
+ {
+ "name": "down",
+ "sing": "singDOWN",
+ "colorSave": "funkin-down",
+ "keybindSave": "funkin-4-6k",
+ "defaultColors": ["#00FFFF", "#FFFFFF", "#1542B7"]
+ },
+ {
+ "name": "right",
+ "sing": "singRIGHT",
+ "colorSave": "funkin-right-alt",
+ "keybindSave": "funkin-5-6k",
+ "defaultColors": ["#0033FF", "#FFFFFF", "#000066"]
+ }
+ ]
+ },
+ "notes": {
+ "assetPath": "gameplay/funkin/notes",
+ "animations": [ // these animations will be added to all notes!
+ {
+ "name": "hit",
+ "suffix": "note",
+ "looped": true
+ }
+ ]
+ },
+ "receptors": {
+ "assetPath": "gameplay/funkin/notes",
+ "animations": [
+ {
+ "name": "static",
+ "suffix": "receptor",
+ "disableRGB": true,
+ "looped": true
+ },
+ {
+ "name": "confirm",
+ "suffix": "confirm"
+ },
+ {
+ "name": "press",
+ "suffix": "press"
+ }
+ ]
+ },
+ "holds": {
+ "assetPath": "gameplay/funkin/notes",
+ "animations": [
+ {
+ "name": "hold",
+ "suffix": "hold piece",
+ "looped": true
+ },
+ {
+ "name": "tail",
+ "suffix": "hold tail",
+ "looped": true
+ }
+ ]
+ },
+ "noteCovers": {
+ "assetPath": "gameplay/funkin/noteCovers",
+ "animations": [
+ {
+ "name": "start",
+ "prefix": "hold cover start",
+ "offsets": [10, -46]
+ },
+ {
+ "name": "loop",
+ "prefix": "hold cover loop",
+ "offsets": [10, -46]
+ }
+ {
+ "name": "spark",
+ "prefix": "hold cover spark",
+ "offsets": [10, -46]
+ }
+ ]
+ },
+ "noteSplashes": {
+ "assetPath": "gameplay/funkin/noteSplashes",
+ "variants": 2,
+ "animations": [
+ {
+ "name": "splash-1",
+ "prefix": "notesplash",
+ "suffix": "1",
+ "frameRateRange": [22, 26]
+ },
+ {
+ "name": "splash-2",
+ "prefix": "notesplash",
+ "suffix": "2",
+ "frameRateRange": [22, 26]
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/assets/data/styles/notes/funkin-9k.json b/assets/data/styles/notes/funkin-9k.json
new file mode 100644
index 0000000..8537118
--- /dev/null
+++ b/assets/data/styles/notes/funkin-9k.json
@@ -0,0 +1,157 @@
+{
+ "version": "1.0.0",
+ "name": "Funkin' (9 Key)",
+ "author": "PhantomArcade",
+ "data": {
+ "general": {
+ "laneSpacing": 120,
+ "directions": [ //
+ {
+ "name": "left",
+ "sing": "singLEFT",
+ "colorSave": "funkin-left",
+ "keybindSave": "funkin-0-9k",
+ "defaultColors": ["#C24B99", "#FFFFFF", "#3C1F56"]
+ },
+ {
+ "name": "down",
+ "sing": "singDOWN",
+ "colorSave": "funkin-down",
+ "keybindSave": "funkin-1-9k",
+ "defaultColors": ["#00FFFF", "#FFFFFF", "#1542B7"]
+ },
+ {
+ "name": "up",
+ "sing": "singUP",
+ "colorSave": "funkin-up",
+ "keybindSave": "funkin-2-9k",
+ "defaultColors": ["#12FA05", "#FFFFFF", "#0A4447"]
+ },
+ {
+ "name": "right",
+ "sing": "singRIGHT",
+ "colorSave": "funkin-right",
+ "keybindSave": "funkin-3-9k",
+ "defaultColors": ["#F9393F", "#FFFFFF", "#651038"]
+ },
+ {
+ "name": "up",
+ "sing": "singUP",
+ "colorSave": "funkin-diamond",
+ "keybindSave": "funkin-4-9k",
+ "defaultColors": ["#999999", "#FFFFFF", "#201E31"]
+ },
+ {
+ "name": "left",
+ "sing": "singLEFT",
+ "colorSave": "funkin-left-alt",
+ "keybindSave": "funkin-5-9k",
+ "defaultColors": ["#FFFF00", "#FFFFFF", "#993300"]
+ },
+ {
+ "name": "down",
+ "sing": "singDOWN",
+ "colorSave": "funkin-down-alt",
+ "keybindSave": "funkin-6-9k",
+ "defaultColors": ["#8B4AFF", "#FFFFFF", "#3B177D"]
+ },
+ {
+ "name": "up",
+ "sing": "singUP",
+ "colorSave": "funkin-up-alt",
+ "keybindSave": "funkin-7-9k",
+ "defaultColors": ["#FF0000", "#FFFFFF", "#660000"]
+ },
+ {
+ "name": "right",
+ "sing": "singRIGHT",
+ "colorSave": "funkin-right-alt",
+ "keybindSave": "funkin-8-9k",
+ "defaultColors": ["#0033FF", "#FFFFFF", "#000066"]
+ }
+ ]
+ },
+ "notes": {
+ "assetPath": "gameplay/funkin/notes",
+ "animations": [ // these animations will be added to all notes!
+ {
+ "name": "hit",
+ "suffix": "note",
+ "looped": true
+ }
+ ]
+ },
+ "receptors": {
+ "assetPath": "gameplay/funkin/notes",
+ "animations": [
+ {
+ "name": "static",
+ "suffix": "receptor",
+ "disableRGB": true,
+ "looped": true
+ },
+ {
+ "name": "confirm",
+ "suffix": "confirm"
+ },
+ {
+ "name": "press",
+ "suffix": "press"
+ }
+ ]
+ },
+ "holds": {
+ "assetPath": "gameplay/funkin/notes",
+ "animations": [
+ {
+ "name": "hold",
+ "suffix": "hold piece",
+ "looped": true
+ },
+ {
+ "name": "tail",
+ "suffix": "hold tail",
+ "looped": true
+ }
+ ]
+ },
+ "noteCovers": {
+ "assetPath": "gameplay/funkin/noteCovers",
+ "animations": [
+ {
+ "name": "start",
+ "prefix": "hold cover start",
+ "offsets": [10, -46]
+ },
+ {
+ "name": "loop",
+ "prefix": "hold cover loop",
+ "offsets": [10, -46]
+ }
+ {
+ "name": "spark",
+ "prefix": "hold cover spark",
+ "offsets": [10, -46]
+ }
+ ]
+ },
+ "noteSplashes": {
+ "assetPath": "gameplay/funkin/noteSplashes",
+ "variants": 2,
+ "animations": [
+ {
+ "name": "splash-1",
+ "prefix": "notesplash",
+ "suffix": "1",
+ "frameRateRange": [22, 26]
+ },
+ {
+ "name": "splash-2",
+ "prefix": "notesplash",
+ "suffix": "2",
+ "frameRateRange": [22, 26]
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/assets/data/styles/notes/funkin.json b/assets/data/styles/notes/funkin.json
new file mode 100644
index 0000000..7d0c5bc
--- /dev/null
+++ b/assets/data/styles/notes/funkin.json
@@ -0,0 +1,122 @@
+{
+ "version": "1.0.0",
+ "name": "Funkin' (4 Key)",
+ "author": "PhantomArcade",
+ "data": {
+ "general": {
+ "laneSpacing": 160,
+ "directions": [ //
+ {
+ "name": "left",
+ "sing": "singLEFT",
+ "colorSave": "funkin-left",
+ "keybindSave": "funkin-left-4k",
+ "defaultColors": ["#C24B99", "#FFFFFF", "#3C1F56"]
+ },
+ {
+ "name": "down",
+ "sing": "singDOWN",
+ "colorSave": "funkin-down",
+ "keybindSave": "funkin-down-4k",
+ "defaultColors": ["#00FFFF", "#FFFFFF", "#1542B7"]
+ },
+ {
+ "name": "up",
+ "sing": "singUP",
+ "colorSave": "funkin-up",
+ "keybindSave": "funkin-up-4k",
+ "defaultColors": ["#12FA05", "#FFFFFF", "#0A4447"]
+ },
+ {
+ "name": "right",
+ "sing": "singRIGHT",
+ "colorSave": "funkin-right",
+ "keybindSave": "funkin-right-4k",
+ "defaultColors": ["#F9393F", "#FFFFFF", "#651038"]
+ }
+ ]
+ },
+ "notes": {
+ "assetPath": "gameplay/funkin/notes",
+ "animations": [ // these animations will be added to all notes!
+ {
+ "name": "hit",
+ "suffix": "note",
+ "looped": true
+ }
+ ]
+ },
+ "receptors": {
+ "assetPath": "gameplay/funkin/notes",
+ "animations": [
+ {
+ "name": "static",
+ "suffix": "receptor",
+ "disableRGB": true,
+ "looped": true
+ },
+ {
+ "name": "confirm",
+ "suffix": "confirm"
+ },
+ {
+ "name": "press",
+ "suffix": "press"
+ }
+ ]
+ },
+ "holds": {
+ "assetPath": "gameplay/funkin/notes",
+ "animations": [
+ {
+ "name": "hold",
+ "suffix": "hold piece",
+ "looped": true
+ },
+ {
+ "name": "tail",
+ "suffix": "hold tail",
+ "looped": true
+ }
+ ]
+ },
+ "noteCovers": {
+ "assetPath": "gameplay/funkin/noteCovers",
+ "animations": [
+ {
+ "name": "start",
+ "prefix": "hold cover start",
+ "offsets": [10, -46]
+ },
+ {
+ "name": "loop",
+ "prefix": "hold cover loop",
+ "offsets": [10, -46]
+ }
+ {
+ "name": "spark",
+ "prefix": "hold cover spark",
+ "offsets": [10, -46]
+ }
+ ]
+ },
+ "noteSplashes": {
+ "assetPath": "gameplay/funkin/noteSplashes",
+ "variants": 2,
+ "animations": [
+ {
+ "name": "splash-1",
+ "prefix": "notesplash",
+ "suffix": "1",
+ "frameRateRange": [22, 26]
+ },
+ {
+ "name": "splash-2",
+ "prefix": "notesplash",
+ "suffix": "2",
+ "frameRateRange": [22, 26]
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/assets/images/go.png b/assets/images/gameplay/funkin/GO.png
similarity index 100%
rename from assets/images/go.png
rename to assets/images/gameplay/funkin/GO.png
diff --git a/assets/images/ONE.png b/assets/images/gameplay/funkin/ONE.png
similarity index 100%
rename from assets/images/ONE.png
rename to assets/images/gameplay/funkin/ONE.png
diff --git a/assets/images/TWO.png b/assets/images/gameplay/funkin/TWO.png
similarity index 100%
rename from assets/images/TWO.png
rename to assets/images/gameplay/funkin/TWO.png
diff --git a/assets/images/bad.png b/assets/images/gameplay/funkin/bad.png
similarity index 100%
rename from assets/images/bad.png
rename to assets/images/gameplay/funkin/bad.png
diff --git a/assets/images/combo.png b/assets/images/gameplay/funkin/combo.png
similarity index 100%
rename from assets/images/combo.png
rename to assets/images/gameplay/funkin/combo.png
diff --git a/assets/images/gameplay/funkin/good.png b/assets/images/gameplay/funkin/good.png
new file mode 100644
index 0000000..e001917
Binary files /dev/null and b/assets/images/gameplay/funkin/good.png differ
diff --git a/assets/images/gameplay/funkin/killer.png b/assets/images/gameplay/funkin/killer.png
new file mode 100644
index 0000000..94ee6c2
Binary files /dev/null and b/assets/images/gameplay/funkin/killer.png differ
diff --git a/assets/images/miss.png b/assets/images/gameplay/funkin/miss.png
similarity index 100%
rename from assets/images/miss.png
rename to assets/images/gameplay/funkin/miss.png
diff --git a/assets/images/noteCovers.png b/assets/images/gameplay/funkin/noteCovers.png
similarity index 100%
rename from assets/images/noteCovers.png
rename to assets/images/gameplay/funkin/noteCovers.png
diff --git a/assets/images/noteCovers.xml b/assets/images/gameplay/funkin/noteCovers.xml
similarity index 100%
rename from assets/images/noteCovers.xml
rename to assets/images/gameplay/funkin/noteCovers.xml
diff --git a/assets/images/noteSparks.png b/assets/images/gameplay/funkin/noteSparks.png
similarity index 100%
rename from assets/images/noteSparks.png
rename to assets/images/gameplay/funkin/noteSparks.png
diff --git a/assets/images/noteSparks.xml b/assets/images/gameplay/funkin/noteSparks.xml
similarity index 100%
rename from assets/images/noteSparks.xml
rename to assets/images/gameplay/funkin/noteSparks.xml
diff --git a/assets/images/gameplay/funkin/noteSplashes.png b/assets/images/gameplay/funkin/noteSplashes.png
new file mode 100644
index 0000000..1200266
Binary files /dev/null and b/assets/images/gameplay/funkin/noteSplashes.png differ
diff --git a/assets/images/noteSplashes.xml b/assets/images/gameplay/funkin/noteSplashes.xml
similarity index 100%
rename from assets/images/noteSplashes.xml
rename to assets/images/gameplay/funkin/noteSplashes.xml
diff --git a/assets/images/gameplay/funkin/notes.png b/assets/images/gameplay/funkin/notes.png
new file mode 100644
index 0000000..e68cdda
Binary files /dev/null and b/assets/images/gameplay/funkin/notes.png differ
diff --git a/assets/images/gameplay/funkin/notes.xml b/assets/images/gameplay/funkin/notes.xml
new file mode 100644
index 0000000..a81546d
--- /dev/null
+++ b/assets/images/gameplay/funkin/notes.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/images/num0.png b/assets/images/gameplay/funkin/num0.png
similarity index 100%
rename from assets/images/num0.png
rename to assets/images/gameplay/funkin/num0.png
diff --git a/assets/images/num1.png b/assets/images/gameplay/funkin/num1.png
similarity index 100%
rename from assets/images/num1.png
rename to assets/images/gameplay/funkin/num1.png
diff --git a/assets/images/num2.png b/assets/images/gameplay/funkin/num2.png
similarity index 100%
rename from assets/images/num2.png
rename to assets/images/gameplay/funkin/num2.png
diff --git a/assets/images/num3.png b/assets/images/gameplay/funkin/num3.png
similarity index 100%
rename from assets/images/num3.png
rename to assets/images/gameplay/funkin/num3.png
diff --git a/assets/images/num4.png b/assets/images/gameplay/funkin/num4.png
similarity index 100%
rename from assets/images/num4.png
rename to assets/images/gameplay/funkin/num4.png
diff --git a/assets/images/num5.png b/assets/images/gameplay/funkin/num5.png
similarity index 100%
rename from assets/images/num5.png
rename to assets/images/gameplay/funkin/num5.png
diff --git a/assets/images/num6.png b/assets/images/gameplay/funkin/num6.png
similarity index 100%
rename from assets/images/num6.png
rename to assets/images/gameplay/funkin/num6.png
diff --git a/assets/images/num7.png b/assets/images/gameplay/funkin/num7.png
similarity index 100%
rename from assets/images/num7.png
rename to assets/images/gameplay/funkin/num7.png
diff --git a/assets/images/num8.png b/assets/images/gameplay/funkin/num8.png
similarity index 100%
rename from assets/images/num8.png
rename to assets/images/gameplay/funkin/num8.png
diff --git a/assets/images/num9.png b/assets/images/gameplay/funkin/num9.png
similarity index 100%
rename from assets/images/num9.png
rename to assets/images/gameplay/funkin/num9.png
diff --git a/assets/images/sadmiss.png b/assets/images/gameplay/funkin/sadmiss.png
similarity index 100%
rename from assets/images/sadmiss.png
rename to assets/images/gameplay/funkin/sadmiss.png
diff --git a/assets/images/shit.png b/assets/images/gameplay/funkin/shit.png
similarity index 100%
rename from assets/images/shit.png
rename to assets/images/gameplay/funkin/shit.png
diff --git a/assets/images/sick.png b/assets/images/gameplay/funkin/sick.png
similarity index 100%
rename from assets/images/sick.png
rename to assets/images/gameplay/funkin/sick.png
diff --git a/assets/images/healthBar.png b/assets/images/gameplay/healthBar.png
similarity index 100%
rename from assets/images/healthBar.png
rename to assets/images/gameplay/healthBar.png
diff --git a/assets/images/lose.png b/assets/images/gameplay/lose.png
similarity index 100%
rename from assets/images/lose.png
rename to assets/images/gameplay/lose.png
diff --git a/assets/images/lose.xml b/assets/images/gameplay/lose.xml
similarity index 100%
rename from assets/images/lose.xml
rename to assets/images/gameplay/lose.xml
diff --git a/assets/images/restart.png b/assets/images/gameplay/restart.png
similarity index 100%
rename from assets/images/restart.png
rename to assets/images/gameplay/restart.png
diff --git a/assets/images/good.png b/assets/images/good.png
deleted file mode 100644
index 8e738ab..0000000
Binary files a/assets/images/good.png and /dev/null differ
diff --git a/assets/images/killer.png b/assets/images/killer.png
deleted file mode 100644
index 8e1f69a..0000000
Binary files a/assets/images/killer.png and /dev/null differ
diff --git a/assets/images/noteSplashes.png b/assets/images/noteSplashes.png
deleted file mode 100644
index a5449d3..0000000
Binary files a/assets/images/noteSplashes.png and /dev/null differ
diff --git a/assets/images/notes.png b/assets/images/notes.png
deleted file mode 100644
index c188a44..0000000
Binary files a/assets/images/notes.png and /dev/null differ
diff --git a/assets/images/notes.xml b/assets/images/notes.xml
deleted file mode 100644
index 06f7df6..0000000
--- a/assets/images/notes.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/assets/images/x3.png b/assets/images/x3.png
new file mode 100644
index 0000000..bb89873
Binary files /dev/null and b/assets/images/x3.png differ
diff --git a/assets/scripts/characters/nene-dark.hx b/assets/scripts/characters/nene-dark.hx
index 8c1d6cc..f4f99a4 100644
--- a/assets/scripts/characters/nene-dark.hx
+++ b/assets/scripts/characters/nene-dark.hx
@@ -143,7 +143,7 @@ function setState(state) {
var MIN_BLINK_DELAY:Int = 3;
var MAX_BLINK_DELAY:Int = 7;
var blinkCountdown:Int = MIN_BLINK_DELAY;
-function dance(beat:Int = 0, forced:Bool = false) {
+function dance(?beat:Int = 0, ?forced:Bool = false) {
var stopDance:Bool = true;
switch (getVar('state')) {
diff --git a/assets/scripts/characters/nene.hx b/assets/scripts/characters/nene.hx
index 8229b92..af724bd 100644
--- a/assets/scripts/characters/nene.hx
+++ b/assets/scripts/characters/nene.hx
@@ -71,14 +71,14 @@ function update(elapsed:Float) {
}
function draw() {
aBotSpeaker.setPosition(x, y);
+ // aBotSpeaker.color = color;
aBotSpeaker.alpha = alpha;
- aBotSpeaker.color = color;
aBotSpeaker.draw();
super.draw();
}
function shouldTransitionState() {
- return (game.inputEnabled && game.player1 != null && game.player1.current.loadedCharacter != 'pico-blazin');
+ return (!game.inputDisabled && game.player1 != null && game.player1.current.loadedCharacter != 'pico-blazin');
}
function transitionState() {
switch (getVar('state')) {
@@ -136,8 +136,10 @@ function dance(?beat:Int = 0, ?forced:Bool = false) {
}
}
- if (!stopDance)
- super.dance(beat, forced);
+ if (stopDance)
+ return false;
+
+ return super.dance(beat, forced);
}
function animationFinishedC(anim:String) {
switch (getVar('state')) {
diff --git a/assets/scripts/stages/limoRide.hx b/assets/scripts/stages/limoRide.hx
index ebc0238..274484f 100644
--- a/assets/scripts/stages/limoRide.hx
+++ b/assets/scripts/stages/limoRide.hx
@@ -6,7 +6,10 @@ function setupStage(id:String, stage:Stage) {
stage.characters.get('gf').current.idleSuffix = '-hairblowCar';
- // todo: whatever shader shit is in the base stage
+ var skyOverlay:RuntimeShader = new RuntimeShader('limoOverlay');
+ skyOverlay.setSampler2D('image', Paths.bmd('limo/limoOverlay', 'week4'));
+ stage.getProp('limoSunset').shader = skyOverlay;
+
resetFastCar();
}
diff --git a/assets/scripts/stages/phillyBlazin.hx b/assets/scripts/stages/phillyBlazin.hx
index 374e48f..66a0375 100644
--- a/assets/scripts/stages/phillyBlazin.hx
+++ b/assets/scripts/stages/phillyBlazin.hx
@@ -38,9 +38,9 @@ function createPost() {
rainShader.setFloat('uScale', FlxG.height / 200);
rainShader.setFloatArray('uRainColor', [0x66 / 0xff, 0x80 / 0xff, 0xcc / 0xff]);
camGame.addFilter(rainShader);
-
- player1.setPosition(1360, 1720);
- player2.setPosition(1440, 1720); // uh?
+
+ player1.dance(0, true);
+ player2.dance(0, true);
player1.color = player2.color = 0xffdedede;
player3.color = 0xff888888;
ratingGroup.x += 560;
diff --git a/assets/shaders/limoOverlay.frag b/assets/shaders/limoOverlay.frag
new file mode 100644
index 0000000..d37798c
--- /dev/null
+++ b/assets/shaders/limoOverlay.frag
@@ -0,0 +1,19 @@
+#pragma header
+
+uniform sampler2D image;
+
+vec4 blendOverlay(vec4 base, vec4 blend) {
+ vec4 mixed = mix(1.0 - 2.0 * (1.0 - base) * (1.0 - blend), 2.0 * base * blend, step(base, vec4(0.5)));
+
+ return mixed;
+}
+
+void main() {
+ vec2 funnyUv = openfl_TextureCoordv;
+ vec4 color = flixel_texture2D(bitmap, funnyUv);
+
+ vec2 reallyFunnyUv = vec2(vec2(0.0, 0.0) - gl_FragCoord.xy / openfl_TextureSize.xy);
+ vec4 gf = flixel_texture2D(image, openfl_TextureCoordv.xy + vec2(0.1, 0.2));
+
+ gl_FragColor = blendOverlay(color, gf);
+}
\ No newline at end of file
diff --git a/source/Main.hx b/source/Main.hx
index e923d2e..47143fd 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -9,7 +9,7 @@ class Main extends openfl.display.Sprite {
public static var instance:Main;
public static var engineVersion(default, never):String = '0.0.8';
- public static var apiVersion(default, never):String = '0.0.2';
+ public static var apiVersion(default, never):String = '0.0.3';
public static var compiledTo(get, never):String;
public static var compiledWith(get, never):String;
@@ -18,13 +18,36 @@ class Main extends openfl.display.Sprite {
public static var windowTitle(default, null):String;
public static var showWatermark(default, set):Bool;
public static var debugDisplay:DebugDisplay;
- public static var watermark:FlxText;
public function new() {
super();
instance = this;
windowTitle = FlxG.stage.window.title;
+ printStartup();
+
+ Mods.refresh();
+ HScript.init();
+ DiscordRpc.prepare();
+
+ var game:FunkinGame = new FunkinGame(0, 0, funkin.states.TitleState);
+ addChild(game);
+ addChild(debugDisplay = new DebugDisplay(10, 3));
+
+ FlxG.maxElapsed = 1;
+ FlxG.drawFramerate = 144;
+ FlxG.updateFramerate = 144;
+ FlxG.signals.postUpdate.add(() -> DiscordRpc.update());
+
+ showWatermark = true;
+
+ DiscordRpc.presence.largeImageText = 'FUNKINX3 $engineVersion';
+ openfl.Lib.current.loaderInfo.uncaughtErrorEvents.addEventListener(openfl.events.UncaughtErrorEvent.UNCAUGHT_ERROR, CrashState.handleUncaughtError);
+ #if cpp
+ untyped __global__.__hxcpp_set_critical_error_handler((error) -> throw error);
+ #end
+ }
+ inline function printStartup():Void {
final timeText:String = 'GAME STARTED ON ${Date.now().toString()}';
Sys.println('');
#if I_AM_BORING_ZZZ
@@ -49,45 +72,15 @@ class Main extends openfl.display.Sprite {
}
#end
Sys.println('');
-
- Mods.refresh();
- HScript.init();
- DiscordRPC.prepare();
- var game:FunkinGame = new FunkinGame(0, 0, funkin.states.TitleState);
- addChild(game);
- addChild(debugDisplay = new DebugDisplay(10, 3));
-
- FlxG.maxElapsed = 1;
- FlxG.drawFramerate = 144;
- FlxG.updateFramerate = 144;
- FlxG.fixedTimestep = false;
-
- watermark = new FlxText(10, FlxG.height + 5, FlxG.width, 'FUNKINX3 $engineVersion\nengine by emi3');
- watermark.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
- watermark.alpha = .7;
- watermark.updateHitbox();
- watermark.borderSize = 1.25;
- watermark.scrollFactor.set();
-
- FlxG.signals.postUpdate.add(() -> DiscordRPC.update());
-
- FlxG.plugins.drawOnTop = true;
- FlxG.plugins.addPlugin(watermark);
- showWatermark = true;
-
- DiscordRPC.presence.largeImageText = 'FUNKINX3 $engineVersion';
- openfl.Lib.current.loaderInfo.uncaughtErrorEvents.addEventListener(openfl.events.UncaughtErrorEvent.UNCAUGHT_ERROR, CrashState.handleUncaughtError);
- #if cpp
- untyped __global__.__hxcpp_set_critical_error_handler((error) -> throw error);
- #end
}
- public static function get_soundTray() {
- return cast(FlxG.game.soundTray, funkin.backend.FunkinSoundTray);
+ public static function get_soundTray():funkin.backend.FunkinSoundTray {
+ return cast FlxG.game.soundTray;
}
public static function set_showWatermark(show:Bool) {
if (showWatermark == show) return showWatermark;
- FlxTween.tween(watermark, {y: FlxG.height + (show ? -40 : 5)}, 1, {ease: FlxEase.quartOut});
+
+ debugDisplay.showWatermark = show;
return showWatermark = show;
}
@@ -121,9 +114,10 @@ class Pride {
public static var flagsMap:Map> = [
'transgender' => [brightCyan, brightMagenta, brightWhite, brightMagenta, brightCyan],
'lesbian' => [brightRed, brightYellow, brightWhite, brightMagenta, magenta],
- 'pride' => [brightRed, brightYellow, green, brightBlue, magenta],
- 'bisexual' => [brightRed, brightRed, magenta, blue, blue],
- 'pansexual' => [brightRed, brightRed, brightYellow, brightCyan, brightCyan]
+ 'pride' => [brightRed, yellow, brightYellow, green, brightBlue, magenta],
+ 'bisexual' => [brightRed, brightRed, magenta, magenta, blue, blue],
+ 'pansexual' => [brightRed, brightRed, brightYellow, brightYellow, brightCyan, brightCyan],
+ 'nonbinary' => [brightYellow, brightWhite, magenta, brightBlack]
];
public static var flags(get, never):Array>;
@@ -133,7 +127,8 @@ class Pride {
array.push(item);
return array;
}
- public static function getFlagSlices(array:Array, width:Int = 15):Array {
+ public static function getFlagSlices(array:Array, ?width:Int):Array {
+ width ??= array.length * 3;
var rectangle:String = StringTools.rpad('', ' ', width);
var slices:Array = [];
for (color in array)
diff --git a/source/flixel/FlxCamera.hx b/source/flixel/FlxCamera.hx
index 9cb624d..c73515b 100644
--- a/source/flixel/FlxCamera.hx
+++ b/source/flixel/FlxCamera.hx
@@ -859,12 +859,14 @@ class FlxCamera extends FlxBasic
}
public function drawTriangles(graphic:FlxGraphic, vertices:DrawData, indices:DrawData, uvtData:DrawData, ?colors:DrawData, ?position:FlxPoint, ?blend:BlendMode, repeat:Bool = false, smoothing:Bool = false, ?transform:ColorTransform, ?shader:FlxShader):Void {
+ _bounds.set(0, 0, width / zoom, height / zoom);
+ _bounds.y = (height - _bounds.height) * .5;
+ _bounds.x = (width - _bounds.width) * .5;
+
if (FlxG.renderBlit) {
if (position == null)
position = FlxCamera.renderPoint.set();
- _bounds.set(0, 0, width, height);
-
var verticesLength:Int = vertices.length;
var currentVertexPosition:Int = 0;
@@ -873,9 +875,20 @@ class FlxCamera extends FlxBasic
var bounds = FlxCamera.renderRect.set();
FlxCamera.drawVertices.splice(0, FlxCamera.drawVertices.length);
+ var vertPoint:FlxPoint = FlxPoint.get();
while (i < verticesLength) {
- tempX = position.x + vertices[i];
- tempY = position.y + vertices[i + 1];
+ vertPoint.set(position.x + vertices[i], position.y + vertices[i + 1]);
+
+ if (rotation == 0) {
+ tempX = vertPoint.x;
+ tempY = vertPoint.y;
+ } else {
+ var pivot:FlxPoint = _bounds.getMidpoint(FlxPoint.weak());
+ var rotatedPoint:FlxPoint = vertPoint.pivotDegrees(pivot, rotation);
+
+ tempX = rotatedPoint.x;
+ tempY = rotatedPoint.y;
+ }
FlxCamera.drawVertices[currentVertexPosition++] = tempX;
FlxCamera.drawVertices[currentVertexPosition++] = tempY;
@@ -888,7 +901,7 @@ class FlxCamera extends FlxBasic
i += 2;
}
-
+ vertPoint.put();
position.putWeak();
if (!_bounds.overlaps(bounds)) {
@@ -924,17 +937,16 @@ class FlxCamera extends FlxBasic
bounds.put();
} else {
- _bounds.set(0, 0, width, height);
var isColored:Bool = (colors != null && colors.length != 0);
#if !flash
var hasColorOffsets:Bool = (transform != null && transform.hasRGBAOffsets());
isColored = isColored || (transform != null && transform.hasRGBMultipliers());
var drawItem:FlxDrawTrianglesItem = startTrianglesBatch(graphic, smoothing, isColored, blend, hasColorOffsets, shader);
- drawItem.addTriangles(vertices, indices, uvtData, colors, position, _bounds, transform);
+ drawItem.addTriangles(vertices, indices, uvtData, colors, position, _bounds, transform, rotation);
#else
var drawItem:FlxDrawTrianglesItem = startTrianglesBatch(graphic, smoothing, isColored, blend);
- drawItem.addTriangles(vertices, indices, uvtData, colors, position, _bounds);
+ drawItem.addTriangles(vertices, indices, uvtData, colors, position, _bounds, rotation);
#end
}
}
@@ -1143,6 +1155,7 @@ class FlxCamera extends FlxBasic
if (target != null)
{
updateFollow();
+ updateLerp(elapsed);
}
updateScroll();
@@ -1197,7 +1210,7 @@ class FlxCamera extends FlxBasic
if (deadzone == null)
{
target.getMidpoint(_point);
- _point.addPoint(targetOffset);
+ _point.add(targetOffset);
_scrollTarget.set(_point.x - width * 0.5, _point.y - height * 0.5);
}
else
@@ -1267,15 +1280,21 @@ class FlxCamera extends FlxBasic
_lastTargetPosition.y = target.y;
}
}
-
- if (followLerp >= 60 / FlxG.updateFramerate)
+ }
+
+ function updateLerp(elapsed:Float)
+ {
+ if (followLerp >= 1.0)
{
scroll.copyFrom(_scrollTarget); // no easing
}
- else
+ else if (followLerp > 0.0)
{
- scroll.x += (_scrollTarget.x - scroll.x) * followLerp * (60 / FlxG.updateFramerate);
- scroll.y += (_scrollTarget.y - scroll.y) * followLerp * (60 / FlxG.updateFramerate);
+ // Adjust lerp based on the current frame rate so lerp is less framerate dependant
+ final adjustedLerp = 1.0 - Math.pow(1.0 - followLerp, elapsed * 60);
+
+ scroll.x += (_scrollTarget.x - scroll.x) * adjustedLerp;
+ scroll.y += (_scrollTarget.y - scroll.y) * adjustedLerp;
}
}
diff --git a/source/flixel/graphics/tile/FlxDrawTrianglesItem.hx b/source/flixel/graphics/tile/FlxDrawTrianglesItem.hx
new file mode 100644
index 0000000..5ce6653
--- /dev/null
+++ b/source/flixel/graphics/tile/FlxDrawTrianglesItem.hx
@@ -0,0 +1,386 @@
+package flixel.graphics.tile;
+
+import flixel.FlxCamera;
+import flixel.graphics.frames.FlxFrame;
+import flixel.graphics.tile.FlxDrawBaseItem.FlxDrawItemType;
+import flixel.math.FlxMatrix;
+import flixel.math.FlxPoint;
+import flixel.math.FlxRect;
+import flixel.system.FlxAssets.FlxShader;
+import flixel.util.FlxColor;
+import openfl.display.Graphics;
+import openfl.display.ShaderParameter;
+import openfl.display.TriangleCulling;
+import openfl.geom.ColorTransform;
+
+typedef DrawData = openfl.Vector;
+
+/**
+ * @author Zaphod
+ */
+class FlxDrawTrianglesItem extends FlxDrawBaseItem
+{
+ static var point:FlxPoint = FlxPoint.get();
+ static var rect:FlxRect = FlxRect.get();
+
+ #if !flash
+ public var shader:FlxShader;
+ var alphas:Array;
+ var colorMultipliers:Array;
+ var colorOffsets:Array;
+ #end
+
+ public var vertices:DrawData = new DrawData();
+ public var indices:DrawData = new DrawData();
+ public var uvtData:DrawData = new DrawData();
+ public var colors:DrawData = new DrawData();
+
+ public var verticesPosition:Int = 0;
+ public var indicesPosition:Int = 0;
+ public var colorsPosition:Int = 0;
+
+ var bounds:FlxRect = FlxRect.get();
+
+ public function new()
+ {
+ super();
+ type = FlxDrawItemType.TRIANGLES;
+ #if !flash
+ alphas = [];
+ #end
+ }
+
+ override public function render(camera:FlxCamera):Void
+ {
+ if (!FlxG.renderTile)
+ return;
+
+ if (numTriangles <= 0)
+ return;
+
+ #if !flash
+ var shader = shader != null ? shader : graphics.shader;
+ shader.bitmap.input = graphics.bitmap;
+ shader.bitmap.filter = (camera.antialiasing || antialiasing) ? LINEAR : NEAREST;
+ shader.bitmap.wrap = REPEAT; // in order to prevent breaking tiling behaviour in classes that use drawTriangles
+ shader.alpha.value = alphas;
+
+ if (colored || hasColorOffsets)
+ {
+ shader.colorMultiplier.value = colorMultipliers;
+ shader.colorOffset.value = colorOffsets;
+ }
+ else
+ {
+ shader.colorMultiplier.value = null;
+ shader.colorOffset.value = null;
+ }
+
+ setParameterValue(shader.hasTransform, true);
+ setParameterValue(shader.hasColorTransform, colored || hasColorOffsets);
+
+ camera.canvas.graphics.overrideBlendMode(blend);
+
+ camera.canvas.graphics.beginShaderFill(shader);
+ #else
+ camera.canvas.graphics.beginBitmapFill(graphics.bitmap, null, true, (camera.antialiasing || antialiasing));
+ #end
+
+ camera.canvas.graphics.drawTriangles(vertices, indices, uvtData, TriangleCulling.NONE);
+ camera.canvas.graphics.endFill();
+
+ #if FLX_DEBUG
+ if (FlxG.debugger.drawDebug)
+ {
+ var gfx:Graphics = camera.debugLayer.graphics;
+ gfx.lineStyle(1, FlxColor.BLUE, 0.5);
+ gfx.drawTriangles(vertices, indices, uvtData);
+ }
+ #end
+
+ super.render(camera);
+ }
+
+ override public function reset():Void
+ {
+ super.reset();
+ vertices.length = 0;
+ indices.length = 0;
+ uvtData.length = 0;
+ colors.length = 0;
+
+ verticesPosition = 0;
+ indicesPosition = 0;
+ colorsPosition = 0;
+ #if !flash
+ alphas.splice(0, alphas.length);
+ if (colorMultipliers != null)
+ colorMultipliers.splice(0, colorMultipliers.length);
+ if (colorOffsets != null)
+ colorOffsets.splice(0, colorOffsets.length);
+ #end
+ }
+
+ override public function dispose():Void
+ {
+ super.dispose();
+
+ vertices = null;
+ indices = null;
+ uvtData = null;
+ colors = null;
+ bounds = null;
+ #if !flash
+ alphas = null;
+ colorMultipliers = null;
+ colorOffsets = null;
+ #end
+ }
+
+ public function addTriangles(vertices:DrawData, indices:DrawData, uvtData:DrawData, ?colors:DrawData, ?position:FlxPoint,
+ ?cameraBounds:FlxRect #if !flash , ?transform:ColorTransform #end, rotation:Float = 0):Void
+ {
+ position ??= point.set();
+ cameraBounds ??= rect.set(0, 0, FlxG.width, FlxG.height);
+
+ var verticesLength:Int = vertices.length;
+ var prevVerticesLength:Int = this.vertices.length;
+ var numberOfVertices:Int = Std.int(verticesLength / 2);
+ var prevIndicesLength:Int = this.indices.length;
+ var prevUVTDataLength:Int = this.uvtData.length;
+ var prevColorsLength:Int = this.colors.length;
+ var prevNumberOfVertices:Int = this.numVertices;
+
+ var i:Int = 0;
+ var currentVertexPosition:Int = prevVerticesLength;
+
+ var vertPoint:FlxPoint = FlxPoint.get();
+ while (i < verticesLength) {
+ vertPoint.set(position.x + vertices[i], position.y + vertices[i + 1]);
+
+ if (rotation != 0) {
+ var pivot:FlxPoint = cameraBounds.getMidpoint(FlxPoint.weak());
+ vertPoint.pivotDegrees(pivot, rotation);
+ }
+
+ this.vertices[currentVertexPosition ++] = vertPoint.x;
+ this.vertices[currentVertexPosition ++] = vertPoint.y;
+
+ if (i == 0) {
+ bounds.set(vertPoint.x, vertPoint.y, 0, 0);
+ } else {
+ inflateBounds(bounds, vertPoint.x, vertPoint.y);
+ }
+
+ i += 2;
+ }
+ vertPoint.put();
+
+ var indicesLength:Int = indices.length;
+ if (!cameraBounds.overlaps(bounds))
+ {
+ this.vertices.splice(this.vertices.length - verticesLength, verticesLength);
+ }
+ else
+ {
+ var uvtDataLength:Int = uvtData.length;
+ for (i in 0...uvtDataLength)
+ {
+ this.uvtData[prevUVTDataLength + i] = uvtData[i];
+ }
+
+ for (i in 0...indicesLength)
+ {
+ this.indices[prevIndicesLength + i] = indices[i] + prevNumberOfVertices;
+ }
+
+ if (colored)
+ {
+ for (i in 0...numberOfVertices)
+ {
+ this.colors[prevColorsLength + i] = colors[i];
+ }
+
+ colorsPosition += numberOfVertices;
+ }
+
+ verticesPosition += verticesLength;
+ indicesPosition += indicesLength;
+ }
+
+ position.putWeak();
+ cameraBounds.putWeak();
+
+ #if !flash
+ for (_ in 0...indicesLength)
+ {
+ alphas.push(transform != null ? transform.alphaMultiplier : 1.0);
+ }
+
+ if (colored || hasColorOffsets)
+ {
+ if (colorMultipliers == null)
+ colorMultipliers = [];
+
+ if (colorOffsets == null)
+ colorOffsets = [];
+
+ for (_ in 0...indicesLength)
+ {
+ if(transform != null)
+ {
+ colorMultipliers.push(transform.redMultiplier);
+ colorMultipliers.push(transform.greenMultiplier);
+ colorMultipliers.push(transform.blueMultiplier);
+
+ colorOffsets.push(transform.redOffset);
+ colorOffsets.push(transform.greenOffset);
+ colorOffsets.push(transform.blueOffset);
+ colorOffsets.push(transform.alphaOffset);
+ }
+ else
+ {
+ colorMultipliers.push(1);
+ colorMultipliers.push(1);
+ colorMultipliers.push(1);
+
+ colorOffsets.push(0);
+ colorOffsets.push(0);
+ colorOffsets.push(0);
+ colorOffsets.push(0);
+ }
+
+ colorMultipliers.push(1);
+ }
+ }
+ #end
+ }
+
+ inline function setParameterValue(parameter:ShaderParameter, value:Bool):Void
+ {
+ if (parameter.value == null)
+ parameter.value = [];
+ parameter.value[0] = value;
+ }
+
+ public static inline function inflateBounds(bounds:FlxRect, x:Float, y:Float):FlxRect
+ {
+ if (x < bounds.x)
+ {
+ bounds.width += bounds.x - x;
+ bounds.x = x;
+ }
+
+ if (y < bounds.y)
+ {
+ bounds.height += bounds.y - y;
+ bounds.y = y;
+ }
+
+ if (x > bounds.x + bounds.width)
+ {
+ bounds.width = x - bounds.x;
+ }
+
+ if (y > bounds.y + bounds.height)
+ {
+ bounds.height = y - bounds.y;
+ }
+
+ return bounds;
+ }
+
+ override public function addQuad(frame:FlxFrame, matrix:FlxMatrix, ?transform:ColorTransform):Void
+ {
+ var prevVerticesPos:Int = verticesPosition;
+ var prevIndicesPos:Int = indicesPosition;
+ var prevColorsPos:Int = colorsPosition;
+ var prevNumberOfVertices:Int = numVertices;
+
+ var point = FlxPoint.get();
+ point.transform(matrix);
+
+ vertices[prevVerticesPos] = point.x;
+ vertices[prevVerticesPos + 1] = point.y;
+
+ uvtData[prevVerticesPos] = frame.uv.left;
+ uvtData[prevVerticesPos + 1] = frame.uv.top;
+
+ point.set(frame.frame.width, 0);
+ point.transform(matrix);
+
+ vertices[prevVerticesPos + 2] = point.x;
+ vertices[prevVerticesPos + 3] = point.y;
+
+ uvtData[prevVerticesPos + 2] = frame.uv.right;
+ uvtData[prevVerticesPos + 3] = frame.uv.top;
+
+ point.set(frame.frame.width, frame.frame.height);
+ point.transform(matrix);
+
+ vertices[prevVerticesPos + 4] = point.x;
+ vertices[prevVerticesPos + 5] = point.y;
+
+ uvtData[prevVerticesPos + 4] = frame.uv.right;
+ uvtData[prevVerticesPos + 5] = frame.uv.bottom;
+
+ point.set(0, frame.frame.height);
+ point.transform(matrix);
+
+ vertices[prevVerticesPos + 6] = point.x;
+ vertices[prevVerticesPos + 7] = point.y;
+
+ point.put();
+
+ uvtData[prevVerticesPos + 6] = frame.uv.left;
+ uvtData[prevVerticesPos + 7] = frame.uv.bottom;
+
+ indices[prevIndicesPos] = prevNumberOfVertices;
+ indices[prevIndicesPos + 1] = prevNumberOfVertices + 1;
+ indices[prevIndicesPos + 2] = prevNumberOfVertices + 2;
+ indices[prevIndicesPos + 3] = prevNumberOfVertices + 2;
+ indices[prevIndicesPos + 4] = prevNumberOfVertices + 3;
+ indices[prevIndicesPos + 5] = prevNumberOfVertices;
+
+ if (colored)
+ {
+ var red = 1.0;
+ var green = 1.0;
+ var blue = 1.0;
+ var alpha = 1.0;
+
+ if (transform != null)
+ {
+ red = transform.redMultiplier;
+ green = transform.greenMultiplier;
+ blue = transform.blueMultiplier;
+
+ #if !neko
+ alpha = transform.alphaMultiplier;
+ #end
+ }
+
+ var color = FlxColor.fromRGBFloat(red, green, blue, alpha);
+
+ colors[prevColorsPos] = color;
+ colors[prevColorsPos + 1] = color;
+ colors[prevColorsPos + 2] = color;
+ colors[prevColorsPos + 3] = color;
+
+ colorsPosition += 4;
+ }
+
+ verticesPosition += 8;
+ indicesPosition += 6;
+ }
+
+ override function get_numVertices():Int
+ {
+ return Std.int(vertices.length / 2);
+ }
+
+ override function get_numTriangles():Int
+ {
+ return Std.int(indices.length / 3);
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/backend/FunkinAnimate.hx b/source/funkin/backend/FunkinAnimate.hx
index 37d3c1b..364eb89 100644
--- a/source/funkin/backend/FunkinAnimate.hx
+++ b/source/funkin/backend/FunkinAnimate.hx
@@ -1,9 +1,11 @@
package funkin.backend;
import openfl.Assets;
+import openfl.geom.Matrix;
import openfl.display.BlendMode;
import openfl.geom.ColorTransform;
import flixel.math.FlxMatrix;
+import flixel.util.FlxDestroyUtil;
import flixel.graphics.frames.FlxFrame;
import flxanimate.zip.Zip;
import flxanimate.animate.*;
@@ -16,12 +18,18 @@ import flixel.graphics.frames.FlxFramesCollection;
using StringTools;
-class FunkinAnimate extends FlxAnimate implements funkin.backend.FunkinSprite.IZoomFactor { // this is kind of useless, but pop off
+class FunkinAnimate extends FlxAnimate implements funkin.backend.FunkinSprite.IFunkinSpriteVars { // this is kind of useless, but pop off
public var funkAnim:FunkinAnimateAnim;
public var zoomFactor(default, set):Float = 1;
public var initialZoom(default, set):Float = 1;
+ public var transformMatrix(default, null):Matrix = new Matrix();
+ public var skew(default, null):FlxPoint = FlxPoint.get();
+ public var matrixExposed:Bool = false;
+
+ var _skewMatrix:Matrix = new Matrix();
+
public function new(x:Float = 0, y:Float = 0, ?path:String, ?settings:flxanimate.Settings) {
super(x, y);
@@ -154,6 +162,10 @@ class FunkinAnimate extends FlxAnimate implements funkin.backend.FunkinSprite.IZ
} catch (e:Dynamic) {
destroyAnim();
}
+
+ skew = FlxDestroyUtil.put(skew);
+ transformMatrix = null;
+ _skewMatrix = null;
}
override function drawLimb(limb:FlxFrame, _matrix:FlxMatrix, ?colorTransform:ColorTransform = null, filterin:Bool = false, ?blendMode:BlendMode, ?scrollFactor:FlxPoint = null, cameras:Array = null) {
@@ -178,14 +190,21 @@ class FunkinAnimate extends FlxAnimate implements funkin.backend.FunkinSprite.IZ
matrix.translate(-origin.x, -origin.y);
matrix.scale(scale.x, scale.y);
-
- if (bakedRotationAngle <= 0) {
- updateTrig();
-
- if (angle != 0)
- matrix.rotateWithTrig(_cosAngle, _sinAngle);
+
+ if (matrixExposed) {
+ matrix.concat(transformMatrix);
+ } else {
+ if (bakedRotationAngle <= 0) {
+ updateTrig();
+
+ if (angle != 0)
+ matrix.rotateWithTrig(_cosAngle, _sinAngle);
+ }
+
+ updateSkewMatrix();
+ matrix.concat(_skewMatrix);
}
-
+
_point.addPoint(origin);
} else {
matrix.scale(.9, .9);
@@ -232,6 +251,15 @@ class FunkinAnimate extends FlxAnimate implements funkin.backend.FunkinSprite.IZ
#end
}
+ function updateSkewMatrix():Void {
+ _skewMatrix.identity();
+
+ if (skew.x != 0 || skew.y != 0) {
+ _skewMatrix.b = Math.tan(skew.y / 180 * Math.PI);
+ _skewMatrix.c = Math.tan(skew.x / 180 * Math.PI);
+ }
+ }
+
function set_zoomFactor(value:Float):Float {
return zoomFactor = value;
}
@@ -284,6 +312,9 @@ class FunkinAnimateAnim extends FlxAnim {
public function exists(name:String):Bool {
return (animsMap.exists(name) || (symbolDictionary != null && symbolDictionary.exists(name)));
}
+ public function remove(name:String):Void {
+ animsMap.remove(name);
+ }
public function rename(oldName:String, newName:String):Void {
var anim:SymbolStuff = animsMap.get(oldName);
if (anim == null) {
diff --git a/source/funkin/backend/FunkinCamera.hx b/source/funkin/backend/FunkinCamera.hx
index 310d5c5..7fe7f70 100644
--- a/source/funkin/backend/FunkinCamera.hx
+++ b/source/funkin/backend/FunkinCamera.hx
@@ -8,22 +8,21 @@ typedef ShaderOrFilter = flixel.util.typeLimit.OneOfTwo;
class FunkinCamera extends FlxCamera {
public var pauseZoomLerp:Bool = false; // OK, this is hacky but i cant be arsed
public var pauseFollowLerp:Bool = false;
+
public var zoomTarget:Null = null;
public var zoomFollowLerp:Float = -1;
public var zoomOffset:Float = 0;
- var time:Float = -1;
-
- override public function update(elapsed:Float):Void {
- if (target != null) updateFollowMod(elapsed);
- if (zoomTarget != null) updateZoomFollow(elapsed);
+
+ public static var topCamera(get, never):FlxCamera;
+
+ public override function update(elapsed:Float):Void {
+ if (target != null) updateFollow();
+ updateLerp(elapsed);
updateScroll();
updateFlash(elapsed);
updateFade(elapsed);
-
- flashSprite.filters = filtersEnabled ? filters : null;
-
- updateFlashSpritePosition();
+
updateShake(elapsed);
}
public override function follow(target:FlxObject, ?style:FlxCameraFollowStyle, ?lerp:Float):Void {
@@ -36,6 +35,9 @@ class FunkinCamera extends FlxCamera {
zoom = zoomTarget;
}
override function render() {
+ flashSprite.filters = filtersEnabled ? filters : null;
+ updateFlashSpritePosition();
+
if (filters != null) {
for (filter in filters) {
if (!Std.isOfType(filter, openfl.filters.ShaderFilter))
@@ -49,6 +51,7 @@ class FunkinCamera extends FlxCamera {
}
}
}
+
super.render();
}
@@ -99,80 +102,30 @@ class FunkinCamera extends FlxCamera {
filters.remove(cast filter);
}
}
-
- public function updateFollowMod(elapsed:Float):Void {
- // Either follow the object closely,
- // or double check our deadzone and update accordingly.
- if (deadzone == null) {
- target.getMidpoint(_point);
- _point.addPoint(targetOffset);
- _scrollTarget.set(_point.x - width * 0.5, _point.y - height * 0.5);
- }
- else {
- var edge:Float;
- var targetX:Float = target.x + targetOffset.x;
- var targetY:Float = target.y + targetOffset.y;
-
- if (style == SCREEN_BY_SCREEN) {
- if (targetX >= viewRight)
- _scrollTarget.x += viewWidth;
- else if (targetX + target.width < viewLeft)
- _scrollTarget.x -= viewWidth;
-
- if (targetY >= viewBottom)
- _scrollTarget.y += viewHeight;
- else if (targetY + target.height < viewTop)
- _scrollTarget.y -= viewHeight;
-
- // without this we see weird behavior when switching to SCREEN_BY_SCREEN at arbitrary scroll positions
- bindScrollPos(_scrollTarget);
- }
- else
- {
- edge = targetX - deadzone.x;
- if (_scrollTarget.x > edge)
- _scrollTarget.x = edge;
- edge = targetX + target.width - deadzone.x - deadzone.width;
- if (_scrollTarget.x < edge)
- _scrollTarget.x = edge;
-
- edge = targetY - deadzone.y;
- if (_scrollTarget.y > edge)
- _scrollTarget.y = edge;
- edge = targetY + target.height - deadzone.y - deadzone.height;
- if (_scrollTarget.y < edge)
- _scrollTarget.y = edge;
- }
-
- if (target is FlxSprite) {
- if (_lastTargetPosition == null)
- _lastTargetPosition = FlxPoint.get(target.x, target.y); // Creates this point.
-
- _scrollTarget.x += (target.x - _lastTargetPosition.x) * followLead.x;
- _scrollTarget.y += (target.y - _lastTargetPosition.y) * followLead.y;
-
- _lastTargetPosition.x = target.x;
- _lastTargetPosition.y = target.y;
+
+ public override function updateLerp(elapsed:Float):Void {
+ if (target != null && !pauseFollowLerp) {
+ if (followLerp < 0) {
+ scroll.copyFrom(_scrollTarget); // no easing
+ } else if (followLerp > 0) {
+ scroll.x = Util.smoothLerp(scroll.x, _scrollTarget.x, followLerp * elapsed);
+ scroll.y = Util.smoothLerp(scroll.y, _scrollTarget.y, followLerp * elapsed);
}
}
-
- if (pauseFollowLerp) return;
- if (followLerp < 0) {
- scroll.copyFrom(_scrollTarget); // no easing
- } else if (followLerp > 0) {
- scroll.x = Util.smoothLerp(scroll.x, _scrollTarget.x, followLerp * elapsed);
- scroll.y = Util.smoothLerp(scroll.y, _scrollTarget.y, followLerp * elapsed);
- }
- }
- public function updateZoomFollow(elapsed:Float) {
- if (pauseZoomLerp) return;
- if (zoomFollowLerp < 0) {
- zoom = zoomTarget + zoomOffset;
- } else if (zoomFollowLerp > 0) {
- zoom = Util.smoothLerp(zoom, zoomTarget + zoomOffset, zoomFollowLerp * elapsed);
+
+ if (zoomTarget != null && !pauseZoomLerp) {
+ if (zoomFollowLerp < 0) {
+ zoom = zoomTarget + zoomOffset;
+ } else if (zoomFollowLerp > 0) {
+ zoom = Util.smoothLerp(zoom, zoomTarget + zoomOffset, zoomFollowLerp * elapsed);
+ }
}
}
- override function set_followLerp(value:Float)
+ override function set_followLerp(value:Float) {
return followLerp = value;
+ }
+ static function get_topCamera():FlxCamera {
+ return FlxG.cameras.list[FlxG.cameras.list.length - 1];
+ }
}
\ No newline at end of file
diff --git a/source/funkin/backend/FunkinGame.hx b/source/funkin/backend/FunkinGame.hx
index d8d1abb..fc2935e 100644
--- a/source/funkin/backend/FunkinGame.hx
+++ b/source/funkin/backend/FunkinGame.hx
@@ -1,79 +1,15 @@
package funkin.backend;
class FunkinGame extends flixel.FlxGame {
- var _time:Float = -1;
-
public function new(width:Int = 0, height:Int = 0, ?initialState:flixel.util.typeLimit.NextState.InitialState, updateFramerate:Int = 60, drawFramerate:Int = 60, skipSplash:Bool = false, startFullscreen:Bool = false) {
super(width, height, initialState, updateFramerate, drawFramerate, skipSplash, startFullscreen);
+
+ #if FLX_SOUND_TRAY
_customSoundTray = funkin.backend.FunkinSoundTray;
- }
-
- override function switchState() {
- _time = -1;
- super.switchState();
+ #end
}
function crashGame(mes:String = 'Triggered a manual crash') {
throw mes;
}
-
- override function update():Void {
- if (!_state.active || !_state.exists)
- return;
-
- if (FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.F2)
- crashGame();
-
- if (_nextState != null)
- switchState();
-
- #if FLX_DEBUG
- if (FlxG.debugger.visible)
- ticks = getTicks();
- #end
-
- var curTime:Float = haxe.Timer.stamp();
- var realTime:Float = 0;
- if (_time >= 0)
- realTime = Math.min(curTime - _time, FlxG.maxElapsed);
- _elapsedMS = realTime * 1000;
- _time = curTime;
- _total = ticks;
-
- updateElapsed();
-
- FlxG.signals.preUpdate.dispatch();
-
- updateInput();
-
- #if FLX_POST_PROCESS
- if (postProcesses[0] != null)
- postProcesses[0].update(realTime);
- #end
-
- #if FLX_SOUND_SYSTEM
- FlxG.sound.update(realTime);
- #end
- FlxG.plugins.update(realTime);
-
- _state.tryUpdate(realTime);
-
- FlxG.cameras.update(realTime);
- FlxG.signals.postUpdate.dispatch();
-
- #if FLX_DEBUG
- debugger.stats.flixelUpdate(getTicks() - ticks);
- #end
-
- #if FLX_POINTER_INPUT
- var len = FlxG.swipes.length;
- while (len-- > 0) {
- final swipe = FlxG.swipes.pop();
- if (swipe != null)
- swipe.destroy();
- }
- #end
-
- filters = filtersEnabled ? _filters : null;
- }
}
\ No newline at end of file
diff --git a/source/funkin/backend/FunkinRuntimeShader.hx b/source/funkin/backend/FunkinRuntimeShader.hx
index 6a154e6..deb3d3c 100644
--- a/source/funkin/backend/FunkinRuntimeShader.hx
+++ b/source/funkin/backend/FunkinRuntimeShader.hx
@@ -147,7 +147,11 @@ class FunkinRuntimeShader extends FlxRuntimeShader {
}
public function postUpdateFrame(frame:flixel.graphics.frames.FlxFrame) {
if (hasParameter('uFrameBounds'))
+ #if (flixel >= "6.1.0")
+ setFloatArray('uFrameBounds', [frame.uv.left, frame.uv.top, frame.uv.right, frame.uv.bottom]);
+ #else
setFloatArray('uFrameBounds', [frame.uv.x, frame.uv.y, frame.uv.width, frame.uv.height]);
+ #end
}
function set_postProcessing(isPost:Bool) {
diff --git a/source/funkin/backend/FunkinSoundTray.hx b/source/funkin/backend/FunkinSoundTray.hx
index 06bc3fd..867de23 100644
--- a/source/funkin/backend/FunkinSoundTray.hx
+++ b/source/funkin/backend/FunkinSoundTray.hx
@@ -1,7 +1,9 @@
package funkin.backend;
+#if (flixel >= "6.1.0") import flixel.system.FlxAssets; #end
import openfl.display.BitmapData;
import openfl.display.Bitmap;
+import openfl.Lib;
// Hello funkin crew
class FunkinSoundTray extends flixel.system.ui.FlxSoundTray {
@@ -14,7 +16,6 @@ class FunkinSoundTray extends flixel.system.ui.FlxSoundTray {
public var scale(default, set):Float;
public var barsY(default, set):Float;
- public var volumeMaxSound:String;
public function new() {
super();
@@ -26,6 +27,7 @@ class FunkinSoundTray extends flixel.system.ui.FlxSoundTray {
addChild(bg);
addChild(bgBar);
+ _bg = bg;
_bars.resize(0);
for (i in 0...10) {
var bar:Bitmap = new Bitmap();
@@ -36,10 +38,10 @@ class FunkinSoundTray extends flixel.system.ui.FlxSoundTray {
scale = .6;
barsY = 18;
-
+
reloadSoundtrayGraphics();
y = -height;
-
+
volumeUpSound = 'soundtray/volUP';
volumeDownSound = 'soundtray/volDOWN';
volumeMaxSound = 'soundtray/volMAX';
@@ -50,13 +52,12 @@ class FunkinSoundTray extends flixel.system.ui.FlxSoundTray {
public function reloadSoundtrayGraphics() {
bg.bitmapData = Paths.bmd('soundtray/volumebox');
bgBar.bitmapData = Paths.bmd('soundtray/bars_bg');
- _width = bg.bitmapData.width;
+
for (i => bar in _bars) {
- var bmd:Null = Paths.bmd('soundtray/bars_${i + 1}');
- bar.x = ((bg.bitmapData?.width ?? 0) - (bmd?.width ?? 0)) * .5;
- bar.bitmapData = bmd;
+ bar.bitmapData = Paths.bmd('soundtray/bars_${i + 1}');
+ bar.x = (bg.width - bar.width) * .5;
}
- bgBar.x = ((bg.bitmapData?.width ?? 0) - (bgBar.bitmapData?.width ?? 0)) * .5;
+ bgBar.x = (bg.width - bgBar.width) * .5;
screenCenter();
}
@@ -102,7 +103,46 @@ class FunkinSoundTray extends flixel.system.ui.FlxSoundTray {
#end
}
}
-
+
+ #if (flixel >= "6.1.0")
+ public var volumeMaxSound:FlxSoundAsset;
+
+ override public function showAnim(volume:Float, ?sound:FlxSoundAsset, duration:Float = 1, label:String = 'VOLUME'):Void {
+ if (sound != null) {
+ if (sound is String) {
+ FlxG.sound.play(Paths.sound(sound));
+ } else {
+ FlxG.sound.play(sound);
+ }
+ }
+
+ var nVolume:Int = Math.round(volume * 10);
+
+ _timer = duration;
+ lerpYPos = 10;
+ visible = true;
+ active = true;
+
+ for (i => bar in _bars)
+ bar.visible = (i + 1 == nVolume);
+
+ max = (nVolume >= 10);
+ }
+
+ override function showIncrement():Void {
+ final volume = FlxG.sound.muted ? 0 : FlxG.sound.volume;
+ showAnim(volume, silent ? null : (max ? volumeMaxSound : volumeUpSound));
+ }
+
+ override function showDecrement():Void {
+ final volume = FlxG.sound.muted ? 0 : FlxG.sound.volume;
+ showAnim(volume, silent ? null : volumeDownSound);
+ }
+
+ override function updateSize():Void {} // just useless here
+ #else
+ public var volumeMaxSound:String;
+
override public function show(up:Bool = false):Void {
_timer = 1;
lerpYPos = 10;
@@ -126,4 +166,5 @@ class FunkinSoundTray extends flixel.system.ui.FlxSoundTray {
bar.visible = (i + 1 == globalVolume);
max = (baseVolume == 10);
}
+ #end
}
\ No newline at end of file
diff --git a/source/funkin/backend/FunkinSprite.hx b/source/funkin/backend/FunkinSprite.hx
index c052efe..435dc7e 100644
--- a/source/funkin/backend/FunkinSprite.hx
+++ b/source/funkin/backend/FunkinSprite.hx
@@ -5,66 +5,60 @@ import flixel.util.FlxAxes;
import flixel.util.FlxSignal;
import flixel.util.FlxDestroyUtil;
import flixel.system.FlxAssets;
+import flxanimate.animate.FlxSymbol;
import flixel.graphics.frames.FlxFrame;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFramesCollection;
+import flixel.animation.FlxAnimationController;
+import flixel.animation.FlxAnimation;
+
import funkin.backend.FunkinAnimate;
-class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFactor implements IFunkinSpriteAnim {
+class FunkinSprite extends flixel.addons.effects.FlxSkewedSprite implements IFunkinSpriteVars implements IFunkinSpriteAnim {
+ public var onAnimationFrame:FlxTypedSignal String -> Void> = new FlxTypedSignal();
public var onAnimationComplete:FlxTypedSignal Void> = new FlxTypedSignal();
- public var onAnimationFrame:FlxTypedSignal Void> = new FlxTypedSignal();
+ public var onAnimationLoop:FlxTypedSignal Void> = new FlxTypedSignal();
public var currentAnimation(get, never):Null;
public var animationList:Map = new Map();
- public var extraData:Map = new Map();
public var offsets:Map = new Map();
public var smooth(default, set):Bool = true;
public var spriteOffset:FlxPoint;
public var animOffset:FlxPoint;
- public var rotateOffsets:Bool = false;
+ public var rotateOffsets:Bool = true;
public var scaleOffsets:Bool = true;
+ public var skewOffsets:Bool = true;
public var zoomFactor(default, set):Float = 1;
public var initialZoom(default, set):Float = 1;
var renderType:SpriteRenderType = SPARROW;
public var isAnimate(get, never):Bool;
- public var anim(get, never):Dynamic; // for scripting purposes
+ public var anim(default, null):FunkinSpriteAnimHandler;
public var animate:FunkinAnimate;
var _loadedAtlases:Array = [];
var _transPoint:FlxPoint;
- public function setVar(k:String, v:Dynamic):Dynamic {
- if (extraData == null) extraData = new Map();
- extraData.set(k, v);
- return v;
- }
- public function getVar(k:String):Dynamic {
- if (extraData == null) return null;
- return extraData.get(k);
- }
- public function hasVar(k:String):Bool {
- if (extraData == null) return false;
- return extraData.exists(k);
- }
- public function removeVar(k:String):Bool {
- if (extraData == null) return false;
- return extraData.remove(k);
- }
-
public function new(x:Float = 0, y:Float = 0, isSmooth:Bool = true) {
super(x, y);
_transPoint = new FlxPoint();
+ anim = new FunkinSpriteAnimHandler();
spriteOffset = FlxPoint.get();
animOffset = FlxPoint.get();
smooth = isSmooth;
+
+ anim.onFrame.add((number:Int, anim:String) -> onAnimationFrame.dispatch(number, anim));
+ anim.onComplete.add((anim:String) -> onAnimationComplete.dispatch(anim));
+ anim.onLoop.add((anim:String) -> onAnimationLoop.dispatch(anim));
+ anim.attachedFunk = this;
}
public override function destroy() {
- _transPoint = FlxDestroyUtil.put(_transPoint);
+ anim = FlxDestroyUtil.destroy(anim);
animOffset = FlxDestroyUtil.put(animOffset);
spriteOffset = FlxDestroyUtil.put(spriteOffset);
- if (animate != null) animate.destroy();
+ _transPoint = FlxDestroyUtil.put(_transPoint);
+ animate = FlxDestroyUtil.destroy(animate);
super.destroy();
}
public override function update(elapsed:Float) {
@@ -77,19 +71,26 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
}
}
public override function draw() {
- transformSpriteOffset(_transPoint);
if (renderType == ANIMATEATLAS && animate != null) {
+ transformSpriteOffset(_transPoint);
+
+ animate.matrixExposed = matrixExposed;
+ animate.skew.copyFrom(skew);
+ if (matrixExposed)
+ animate.transformMatrix.copyFrom(transformMatrix);
+
+ animate.offset.set(_transPoint.x + offset.x, _transPoint.y + offset.y);
+ animate.scrollFactor.copyFrom(scrollFactor);
+ animate.origin.copyFrom(origin);
+ animate.scale.copyFrom(scale);
+
animate.colorTransform = colorTransform; // lmao
animate.antialiasing = antialiasing;
- animate.scrollFactor = scrollFactor;
animate.initialZoom = initialZoom;
animate.zoomFactor = zoomFactor;
animate.setPosition(x, y);
animate.cameras = cameras;
animate.shader = shader;
- animate.offset.set(_transPoint.x + offset.x, _transPoint.y + offset.y);
- animate.origin = origin;
- animate.scale = scale;
animate.alpha = alpha;
animate.angle = angle;
animate.flipX = flipX;
@@ -137,7 +138,6 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing);
}
public override function drawComplex(camera:FlxCamera) {
- // todo: implement this in flxsprite instead of funkinsprite? (zoomFactor wont work for flxtexts and such)
updateShader(camera);
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
@@ -145,11 +145,18 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
_matrix.translate(-origin.x, -origin.y);
_matrix.scale(scale.x, scale.y);
- if (bakedRotationAngle <= 0) {
- updateTrig();
+ if (matrixExposed) {
+ _matrix.concat(transformMatrix);
+ } else {
+ if (bakedRotationAngle <= 0) {
+ updateTrig();
- if (angle != 0)
- _matrix.rotateWithTrig(_cosAngle, _sinAngle);
+ if (angle != 0)
+ _matrix.rotateWithTrig(_cosAngle, _sinAngle);
+ }
+
+ updateSkewMatrix();
+ _matrix.concat(_skewMatrix);
}
transformSpriteOffset(_transPoint);
@@ -179,11 +186,17 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
return (zoomFactor == 1 && super.isSimpleRenderBlit(camera));
}
- function resetData() {
+ public function resetData() {
unloadAnimate();
offsets.clear();
animationList.clear();
_loadedAtlases.resize(0);
+ anim.isAnimate = false;
+ anim.attachedAnimate = null;
+ }
+ public function unloadAnimate() {
+ if (isAnimate && animate != null)
+ animate = FlxDestroyUtil.destroy(animate);
}
public function loadAuto(path:String, ?library:String) {
final pngExists:Bool = Paths.exists('images/$path.png', library);
@@ -215,38 +228,13 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
default: Paths.sparrowAtlas(path, library);
}
this.renderType = renderType;
- #if (flixel >= "5.9.0")
- animation.onFinish.add((anim:String) -> {
- if (this.renderType != ANIMATEATLAS)
- _onAnimationComplete(anim);
- });
- animation.onFrameChange.add((anim:String, frameNumber:Int, frameIndex:Int) -> {
- if (this.renderType != ANIMATEATLAS)
- _onAnimationFrame(frameNumber);
- });
- #else
- animation.finishCallback = (anim:String) -> {
- if (this.renderType != ANIMATEATLAS)
- _onAnimationComplete(anim);
- };
- animation.callback = (anim:String, frameNumber:Int, frameIndex:Int) -> {
- if (this.renderType != ANIMATEATLAS)
- _onAnimationFrame(frameNumber);
- }
- #end
return this;
}
public function loadAnimate(path:String, ?library:String) {
resetData();
animate = new FunkinAnimate().loadAnimate(path, library);
- animate.funkAnim.onComplete.add(() -> {
- if (renderType == ANIMATEATLAS)
- _onAnimationComplete();
- });
- animate.funkAnim.onFrame.add((frameNumber:Int) -> {
- if (renderType == ANIMATEATLAS)
- _onAnimationFrame(frameNumber);
- });
+ anim.attachedAnimate = animate;
+ anim.isAnimate = true;
renderType = ANIMATEATLAS;
return this;
}
@@ -286,13 +274,13 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
public function hasAnimationPrefix(prefix:String) {
var frames:Array = [];
- @:privateAccess //why is it private :sob:
- animation.findByPrefix(frames, prefix);
+ try { @:privateAccess animation.findByPrefix(frames, prefix); } catch (e:Dynamic) {} //why is it private :sob:
return (frames.length > 0);
}
inline public function transformSpriteOffset(point:FlxPoint):FlxPoint {
var xP:Float = (spriteOffset.x + animOffset.x) * (scaleOffsets ? scale.x : 1);
var yP:Float = (spriteOffset.y + animOffset.y) * (scaleOffsets ? scale.y : 1);
+
if (rotateOffsets && angle % 360 != 0) {
var rad:Float = angle / 180 * Math.PI;
var cos:Float = FlxMath.fastCos(rad);
@@ -301,6 +289,14 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
} else {
point.set(xP, yP);
}
+
+ if (skewOffsets && (skew.x != 0 || skew.y != 0)) {
+ point.set(
+ point.x + point.y * Math.tan(skew.x / 180 * Math.PI),
+ point.y + point.x * Math.tan(skew.y / 180 * Math.PI)
+ );
+ }
+
return point;
}
@@ -327,16 +323,20 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
}
public override function updateHitbox() {
if (isAnimate) {
- animate.alpha = .001;
+ animate.alpha = .0001;
animate.draw();
animate.alpha = 1;
+
width = animate.width * scale.x;
height = animate.height * scale.y;
+ frameWidth = animate.frameWidth;
+ frameHeight = animate.frameHeight;
+
+ offset.set(-0.5 * (width - frameWidth), -0.5 * (height - frameHeight));
+ centerOrigin();
} else {
super.updateHitbox();
}
- // Sys.println('HITBOX UPDATED $width x $height -> $offset');
- // spriteOffset.set(offset.x / (scaleOffsets ? scale.x : 1), offset.y / (scaleOffsets ? scale.y : 1));
}
public function setAnimationOffset(name:String, x:Float = 0, y:Float = 0):FlxPoint {
@@ -351,65 +351,16 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
if (!overwrite && animationExists(name, false))
return;
- if (isAnimate) {
- if (animate == null || animate.funkAnim == null) return;
- var anim:FunkinAnimateAnim = animate.funkAnim;
- var symbolExists:Bool = (anim.symbolDictionary != null && anim.symbolDictionary.exists(prefix));
- if (frameIndices == null || frameIndices.length == 0) {
- if (symbolExists) {
- anim.addBySymbol(name, '$prefix\\', fps, loop);
- } else {
- try { anim.addByFrameLabel(name, prefix, fps, loop); }
- catch (e:Dynamic) { Log.warning('no frame label or symbol with the name of "$prefix" was found...'); }
- }
- } else {
- if (symbolExists) {
- anim.addBySymbolIndices(name, prefix, frameIndices, fps, loop);
- } else { // frame label by indices
- var keyFrame = anim.getFrameLabel(prefix); // todo: move to FunkinAnimateAnim
- try {
- var keyFrameIndices:Array = keyFrame.getFrameIndices();
- var finalIndices:Array = [];
- for (index in frameIndices) finalIndices.push(keyFrameIndices[index] ?? (keyFrameIndices.length - 1));
- try { anim.addBySymbolIndices(name, anim.stageInstance.symbol.name, finalIndices, fps, loop); }
- catch (e:Dynamic) {}
- } catch (e:Dynamic) {
- Log.warning('no frame label or symbol with the name of "$prefix" was found...');
- }
- }
- }
- } else {
- if (assetPath == null) { // wait for the asset to be loaded
- if (overwrite)
- animation.remove(name);
-
- if (frameIndices == null || frameIndices.length == 0) {
- animation.addByPrefix(name, prefix, fps, loop, flipX, flipY);
- } else {
- if (prefix == null)
- animation.add(name, frameIndices, fps, loop, flipX, flipY);
- else
- animation.addByIndices(name, prefix, frameIndices, '', fps, loop, flipX, flipY);
- }
- }
- }
+ if (isAnimate || assetPath == null) // if asset path provided wait for the asset to be loaded
+ this.anim.add(name, prefix, fps, loop, frameIndices, flipX, flipY, overwrite);
animationList[name] = {prefix: prefix, fps: fps, loop: loop, assetPath: assetPath, frameIndices: frameIndices, flipX: flipX, flipY: flipY};
}
public function playAnimation(anim:String, forced:Bool = false, reversed:Bool = false, frame:Int = 0) {
- var animExists:Bool = false;
- if (isAnimate) {
- if (animate == null) return;
- if (animationExists(anim)) {
- animate.funkAnim.play(anim, forced, reversed, frame);
- animExists = true;
- }
- } else {
- if (animationExists(anim)) {
- animation.play(anim, forced, reversed, frame);
- animExists = true;
- }
- }
- if (animExists) {
+ preloadAnimAsset(anim);
+
+ var played:Bool = this.anim.play(anim, forced, reversed, frame);
+
+ if (played) {
if (offsets.exists(anim)) {
var offset:FlxPoint = offsets[anim];
setAnimOffset(offset.x, offset.y);
@@ -418,6 +369,12 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
}
}
}
+ public function animationExists(anim:String, preload:Bool = true):Bool {
+ if (preload)
+ preloadAnimAsset(anim);
+
+ return this.anim.exists(anim);
+ }
public function setAnimOffset(x:Float = 0, y:Float = 0):Void {
animOffset.set(x, y);
}
@@ -430,96 +387,360 @@ class FunkinSprite extends FlxSprite implements ISpriteVars implements IZoomFact
addAnimation(anim, animData.prefix, animData.fps, animData.loop, animData.frameIndices, null, animData.flipX, animData.flipY);
}
}
- public function animationExists(anim:String, preload:Bool = true):Bool {
- if (preload)
- preloadAnimAsset(anim);
+ function get_currentAnimation():String { return anim.name; }
+ public function renameAnimation(oldAnim:String, newAnim:String):Void { return anim.rename(oldAnim, newAnim); }
+ public function getAnimationNameList():Array { return anim.getNameList(); }
+ public function isAnimationFinished():Bool { return anim.finished; }
+ public function finishAnimation():Void { anim.finish(); }
+
+ function set_smooth(newSmooth:Bool):Bool {
+ antialiasing = (newSmooth && Options.data.antialiasing);
+ return (smooth = newSmooth);
+ }
+ function set_zoomFactor(value:Float):Float {
+ return zoomFactor = value;
+ }
+ function set_initialZoom(value:Float):Float {
+ return initialZoom = value;
+ }
+
+ override function get_width() {
+ if (isAnimate) return animate.width;
+ else return width;
+ }
+ override function get_height() {
+ if (isAnimate) return animate.height;
+ else return height;
+ }
+ override function set_clipRect(rect:FlxRect):FlxRect { // dont gaf
+ return clipRect = rect;
+ }
+ function get_isAnimate() {
+ return (renderType == ANIMATEATLAS && animate != null);
+ }
+}
+
+// @:access(flixel.animation.FlxAnimation)
+class FunkinSpriteAnimHandler implements IFlxDestroyable {
+ public var curInstance(get, never):Dynamic; // for hscript usage, mostly...
+ public var curSymbol(get, never):FlxSymbol;
+ public var curAnim(get, never):FlxAnimation;
+
+ public var frameName(get, never):String;
+ public var curFrameFloat(get, set):Float;
+ public var curFrame(get, set):Int;
+ public var name(get, never):String;
+ public var paused(get, set):Bool;
+ public var looped(get, set):Bool;
+ public var length(get, never):Int;
+ public var finished(get, set):Bool;
+ public var reversed(get, set):Bool;
+ public var timeScale(get, set):Float;
+
+ public var isAnimate:Bool = false;
+ public var attachedFunk(default, set):FunkinSprite;
+ public var attachedAnimate(default, set):FunkinAnimate;
+
+ public var onLoop:FlxTypedSignal Void> = new FlxTypedSignal();
+ public var onComplete:FlxTypedSignal Void> = new FlxTypedSignal();
+ public var onFrame:FlxTypedSignal String -> Void> = new FlxTypedSignal();
+
+ var spriteC(get, never):FlxAnimationController;
+ var animateC(get, never):FunkinAnimateAnim;
+
+ inline function set_attachedFunk(newFunk:FunkinSprite):FunkinSprite {
+ var oldAnimation:FlxAnimationController = attachedFunk?.animation;
+ if (oldAnimation != null) {
+ #if (flixel >= "5.9.0")
+ oldAnimation.onLoop.remove(_onLoop);
+ oldAnimation.onFinish.remove(_funkComplete);
+ oldAnimation.onFrameChange.remove(_funkFrame);
+ #else
+ if (oldAnimation.callback == _funkFrame) oldAnimation.callback = null;
+ if (oldAnimation.finishCallback == _funkComplete) oldAnimation.finishCallback = null;
+ #end
+ }
+
+ if (newFunk == null) return attachedFunk = newFunk;
+
+ #if (flixel >= "5.9.0")
+ newFunk.animation.onLoop.add(_onLoop);
+ newFunk.animation.onFinish.add(_funkComplete);
+ newFunk.animation.onFrameChange.add(_funkFrame);
+ #else
+ newFunk.animation.callback = _funkFrame;
+ newFunk.animation.finishCallback = _funkComplete;
+ #end
+ return attachedFunk = newFunk;
+ }
+ inline function set_attachedAnimate(newAnimate:FunkinAnimate):FunkinAnimate {
+ var oldAnim:FunkinAnimateAnim = attachedAnimate?.funkAnim;
+ if (oldAnim != null) {
+ oldAnim.onComplete.remove(_animateComplete);
+ oldAnim.onFrame.remove(_animateFrame);
+ }
+
+ if (newAnimate == null) return attachedAnimate = newAnimate;
+
+ newAnimate.anim.onComplete.add(_animateComplete);
+ newAnimate.anim.onFrame.add(_animateFrame);
+
+ return attachedAnimate = newAnimate;
+ }
+
+ function _funkComplete(anim:String):Void {
+ if (isAnimate) return;
+ if (looped) { _onLoop(anim); }
+ else { onComplete.dispatch(anim); }
+ }
+ function _funkFrame(anim:String, frameNumber:Int, frameIndex:Int):Void { if (!isAnimate) onFrame.dispatch(frameNumber, anim); }
+ function _animateComplete():Void {
+ if (!isAnimate) return;
+ if (looped) { _onLoop(name); }
+ else { onComplete.dispatch(name); }
+ }
+ function _animateFrame(frameNumber:Int):Void { if (isAnimate) onFrame.dispatch(frameNumber, name); }
+ function _onLoop(anim:String):Void { onLoop.dispatch(anim); }
+
+ inline function get_spriteC():FlxAnimationController { return attachedFunk?.animation; }
+ inline function get_animateC():FunkinAnimateAnim { return attachedAnimate?.funkAnim; }
+
+ inline function get_curAnim():FlxAnimation { return spriteC?.curAnim; }
+ inline function get_curSymbol():FlxSymbol { return animateC?.curSymbol; }
+ inline function get_curInstance():Dynamic { return (isAnimate ? curAnim : curSymbol); }
+
+ inline function get_curFrame():Int { return (isAnimate ? animateC.curFrame : curAnim?.curFrame) ?? 0; }
+ inline function get_frameName():String { return (isAnimate ? animateC.curSymbol?.name : attachedFunk?.frame?.name) ?? ''; }
+ inline function get_name():String { return (isAnimate ? animateC.name : spriteC?.name) ?? ''; }
+ inline function get_paused():Bool { return (isAnimate ? !(animateC.isPlaying ?? true) : curAnim?.paused) ?? false; }
+ inline function get_looped():Bool { return (isAnimate ? (animateC.loopType == Loop) : curAnim?.looped) ?? false; }
+ inline function get_length():Int { return (isAnimate ? animateC.length : curAnim?.frames.length) ?? 0; }
+ inline function get_finished():Bool { return (isAnimate ? animateC.finished : curAnim?.finished) ?? false; }
+ inline function get_reversed():Bool { return (isAnimate ? animateC.reversed : curAnim?.reversed) ?? false; }
+ inline function get_timeScale():Float { return (isAnimate ? animateC.timeScale : spriteC?.timeScale) ?? 1; }
+ function get_curFrameFloat():Float {
+ @:privateAccess {
+ if (isAnimate) {
+ if (animateC == null) return 0;
+ return (animateC._tick / animateC.frameDelay + animateC.curFrame);
+ } else {
+ if (curAnim == null) return 0;
+ var curFrameDuration = curAnim.getCurrentFrameDuration();
+ return (curAnim._frameTimer / curFrameDuration + curAnim.curFrame);
+ }
+ }
+ }
+ function set_curFrameFloat(newFrame:Float):Float {
+ function fract(n:Float):Float { return n - Math.floor(n); }
+
+ @:privateAccess {
+ if (isAnimate) {
+ if (animateC == null) return newFrame;
+ if (newFrame < animateC.length) {
+ animateC.curFrame = Math.floor(newFrame);
+ animateC._tick = animateC.frameDelay * fract(newFrame);
+ } else {
+ animateC.curFrame = animateC.length;
+ animateC._tick = animateC.frameDelay;
+ }
+ } else {
+ if (curAnim == null) return newFrame;
+ if (newFrame < curAnim.numFrames) {
+ curAnim.curFrame = Math.floor(newFrame);
+ curAnim._frameTimer = curAnim.getCurrentFrameDuration() * fract(newFrame);
+ } else {
+ curAnim.curFrame = curAnim.numFrames;
+ curAnim._frameTimer = curAnim.getCurrentFrameDuration();
+ }
+ }
+ }
+ return newFrame;
+ }
+
+ inline function set_curFrame(newFrame:Int):Int {
if (isAnimate) {
- return animate.funkAnim.exists(anim);
+ if (animateC == null) return newFrame;
+ return animateC.curFrame = newFrame;
} else {
- return animation.exists(anim);
+ if (curAnim == null) return newFrame;
+ return curAnim.curFrame = newFrame;
}
}
- public function renameAnimation(oldAnim:String, newAnim:String) {
+ inline function set_paused(isIt:Bool):Bool {
if (isAnimate) {
- animate.funkAnim.rename(oldAnim, newAnim);
+ if (animateC == null) return isIt;
+ (isIt ? animateC.pause : animateC.resume)();
+ return isIt;
} else {
- animation.rename(oldAnim, newAnim);
+ if (curAnim == null) return isIt;
+ return curAnim.paused = isIt;
}
}
- public function getAnimationNameList():Array {
+ inline function set_looped(isIt:Bool):Bool {
if (isAnimate) {
- return animate.funkAnim.getNameList();
+ if (animateC == null) return isIt;
+ animateC.loopType = (isIt ? Loop : PlayOnce);
+ return isIt;
} else {
- return animation.getNameList();
+ if (curAnim == null) return isIt;
+ return curAnim.looped = isIt;
}
}
- public function isAnimationFinished():Bool {
+ inline function set_finished(isIt:Bool):Bool {
if (isAnimate) {
- return animate.funkAnim.finished ?? false;
+ if (animateC == null) return isIt;
+ if (isIt) animateC.finish();
+ return animateC.finished;
} else {
- return animation.finished ?? false;
+ if (curAnim == null) return isIt;
+ if (isIt) curAnim.finish();
+ return isIt;
}
}
- public function finishAnimation() {
+ inline function set_reversed(isIt:Bool):Bool {
if (isAnimate) {
- animate.funkAnim.finish();
+ if (animateC == null) return isIt;
+ return animateC.reversed = isIt;
} else {
- animation.finish();
+ if (curAnim == null) return isIt;
+ if (reversed != curAnim.reversed) curAnim.reverse();
+ return isIt;
}
}
- public function unloadAnimate() {
- if (isAnimate && animate != null) {
- animate.destroy();
- animate = null;
+ inline function set_timeScale(newScale:Float):Float {
+ if (isAnimate) {
+ if (animateC == null) return newScale;
+ return animateC.timeScale = newScale;
+ } else {
+ if (spriteC == null) return newScale;
+ return spriteC.timeScale = newScale;
}
}
- function _onAnimationComplete(?anim:String) {
- onAnimationComplete.dispatch(anim ?? currentAnimation ?? '');
- }
- function _onAnimationFrame(frameNumber:Int) {
- onAnimationFrame.dispatch(frameNumber);
- }
- function set_smooth(newSmooth:Bool):Bool {
- antialiasing = (newSmooth && Options.data.antialiasing);
- return (smooth = newSmooth);
+ public function new() {}
+ public function add(name:String, ?prefix:String, fps:Float = 24, loop:Bool = false, ?frameIndices:Array, flipX:Bool = false, flipY:Bool = false, overwrite:Bool = false):Bool {
+ if (isAnimate) {
+ if (animateC == null) return false;
+
+ if (overwrite) {
+ animateC.remove(name);
+ } else if (exists(name)) {
+ return false;
+ }
+
+ var symbolExists:Bool = (animateC.symbolDictionary != null && animateC.symbolDictionary.exists(prefix));
+ if (frameIndices == null || frameIndices.length == 0) {
+ if (symbolExists) {
+ animateC.addBySymbol(name, '$prefix\\', fps, loop);
+ } else {
+ try { animateC.addByFrameLabel(name, prefix, fps, loop); }
+ catch (e:Dynamic) { Log.warning('no frame label or symbol with the name of "$prefix" was found...'); }
+ }
+ } else {
+ if (symbolExists) {
+ animateC.addBySymbolIndices(name, prefix, frameIndices, fps, loop);
+ } else { // frame label by indices
+ var keyFrame = animateC.getFrameLabel(prefix); // todo: move to FunkinAnimateAnim
+ try {
+ var keyFrameIndices:Array = keyFrame.getFrameIndices();
+ var finalIndices:Array = [];
+ for (index in frameIndices) finalIndices.push(keyFrameIndices[index] ?? (keyFrameIndices.length - 1));
+ try { animateC.addBySymbolIndices(name, animateC.stageInstance.symbol.name, finalIndices, fps, loop); }
+ } catch (e:Dynamic) {
+ Log.warning('no frame label or symbol with the name of "$prefix" was found...');
+ }
+ }
+ }
+
+ return animateC.exists(name);
+ } else {
+ if (spriteC == null) return false;
+
+ if (overwrite) {
+ spriteC.remove(name);
+ } else if (exists(name)) {
+ return false;
+ }
+
+ if (frameIndices == null || frameIndices.length == 0) {
+ spriteC.addByPrefix(name, prefix, fps, loop, flipX, flipY);
+ } else {
+ if (prefix == null) {
+ spriteC.add(name, frameIndices, fps, loop, flipX, flipY);
+ } else {
+ spriteC.addByIndices(name, prefix, frameIndices, '', fps, loop, flipX, flipY);
+ }
+ }
+
+ return spriteC.exists(name);
+ }
}
- function set_zoomFactor(value:Float):Float {
- return zoomFactor = value;
+ public function play(anim:String, forced:Bool = false, reversed:Bool = false, frame:Int = 0):Bool {
+ if (exists(anim)) {
+ if (isAnimate) {
+ animateC?.play(anim, forced, reversed, frame);
+ } else {
+ spriteC?.play(anim, forced, reversed, frame);
+ }
+ return true;
+ }
+ return false;
}
- function set_initialZoom(value:Float):Float {
- return initialZoom = value;
+ public function reverse():Void {
+ if (isAnimate) {
+ if (animateC == null) return;
+ animateC.reversed = !animateC.reversed;
+ } else {
+ spriteC?.reverse();
+ }
}
-
- override function get_width() {
- if (isAnimate) return animate.width;
- else return width;
+ public function exists(anim:String):Bool {
+ if (isAnimate) {
+ return animateC?.exists(anim) ?? false;
+ } else {
+ return spriteC?.exists(anim) ?? false;
+ }
}
- override function get_height() {
- if (isAnimate) return animate.height;
- else return height;
+ public function rename(oldAnim:String, newAnim:String):Void {
+ if (isAnimate) {
+ animateC?.rename(oldAnim, newAnim);
+ } else {
+ spriteC?.rename(oldAnim, newAnim);
+ }
}
- function get_anim() {
- return (isAnimate ? animate.funkAnim : animation);
+ public function remove(anim:String):Void {
+ if (isAnimate) {
+ animateC?.remove(anim);
+ } else {
+ spriteC?.remove(anim);
+ }
}
- function get_isAnimate() {
- return (renderType == ANIMATEATLAS && animate != null);
+ public function getNameList():Array {
+ if (isAnimate) {
+ return animateC?.getNameList() ?? [];
+ } else {
+ return spriteC?.getNameList() ?? [];
+ }
}
- function get_currentAnimation() {
- if (isAnimate) return animate.funkAnim.name;
- else return animation.name;
+ public function finish():Void {
+ if (isAnimate) {
+ animateC?.finish();
+ } else {
+ spriteC?.finish();
+ }
}
-}
-interface ISpriteVars {
- public var extraData:Map;
- public function setVar(k:String, v:Dynamic):Dynamic;
- public function getVar(k:String):Dynamic;
- public function hasVar(k:String):Bool;
- public function removeVar(k:String):Bool;
+ public function destroy():Void {
+ FlxDestroyUtil.destroy(onLoop);
+ FlxDestroyUtil.destroy(onFrame);
+ FlxDestroyUtil.destroy(onComplete);
+ // idk
+ }
}
-interface IZoomFactor {
+
+interface IFunkinSpriteVars {
+ public var skew(default, null):FlxPoint;
public var zoomFactor(default, set):Float;
public var initialZoom(default, set):Float;
}
@@ -533,8 +754,9 @@ interface IFunkinSpriteAnim { // the essentials, anyway
public function isAnimationFinished():Bool;
public function finishAnimation():Void;
+ public var onAnimationFrame:FlxTypedSignal String -> Void>;
public var onAnimationComplete:FlxTypedSignal Void>;
- public var onAnimationFrame:FlxTypedSignal Void>;
+ public var onAnimationLoop:FlxTypedSignal Void>;
}
enum abstract SpriteRenderType(String) to String {
diff --git a/source/funkin/backend/FunkinSpriteGroup.hx b/source/funkin/backend/FunkinSpriteGroup.hx
index fd1929b..5c275dc 100644
--- a/source/funkin/backend/FunkinSpriteGroup.hx
+++ b/source/funkin/backend/FunkinSpriteGroup.hx
@@ -1,34 +1,54 @@
package funkin.backend;
+import flixel.util.FlxDestroyUtil;
import funkin.backend.FunkinSprite;
+import haxe.iterators.ArrayKeyValueIterator;
+
+typedef FunkinGroup = FunkinTypedGroup;
+class FunkinTypedGroup extends FlxTypedGroup {
+ public function sortZIndex() {
+ sort(Util.sortZIndex, FlxSort.ASCENDING);
+ }
+ public function insertZIndex(obj:T, ?zIndex:Int) {
+ if (members.contains(obj)) remove(obj, true);
+ if (zIndex != null) obj.zIndex = zIndex;
+
+ var low:Float = Math.POSITIVE_INFINITY;
+ for (pos => mem in members) {
+ low = Math.min(mem.zIndex, low);
+ if (obj.zIndex < mem.zIndex) {
+ insert(pos, obj);
+ return obj;
+ }
+ }
+ if (obj.zIndex < low) {
+ insert(0, obj);
+ } else {
+ add(obj);
+ }
+
+ return obj;
+ }
+}
typedef FunkinSpriteGroup = FunkinTypedSpriteGroup;
-class FunkinTypedSpriteGroup implements ISpriteVars implements IZoomFactor extends FlxTypedSpriteGroup {
+class FunkinTypedSpriteGroup implements ISpriteGroup implements IFunkinSpriteVars extends FlxTypedSpriteGroup {
+ public var skew(default, null):FlxPoint;
public var zoomFactor(default, set):Float = 1;
public var initialZoom(default, set):Float = 1;
- public var extraData:Map = new Map();
- public function setVar(k:String, v:Dynamic):Dynamic {
- if (extraData == null) extraData = new Map();
- extraData.set(k, v);
- return v;
- }
- public function getVar(k:String):Dynamic {
- if (extraData == null) return null;
- return extraData.get(k);
- }
- public function hasVar(k:String):Bool {
- if (extraData == null) return false;
- return extraData.exists(k);
+ override function initVars():Void {
+ skew = new FlxCallbackPoint(skewCallback);
+ super.initVars();
}
- public function removeVar(k:String):Bool {
- if (extraData == null) return false;
- return extraData.remove(k);
+ override public function destroy():Void {
+ skew = FlxDestroyUtil.destroy(skew);
+ super.destroy();
}
- inline function getFunk(sprite:T):IZoomFactor {
- if (Std.isOfType(sprite, IZoomFactor))
- return cast(sprite, IZoomFactor);
+ inline function getFunk(sprite:T):IFunkinSpriteVars {
+ if (Std.isOfType(sprite, IFunkinSpriteVars))
+ return cast(sprite, IFunkinSpriteVars);
return null;
}
public override function updateHitbox():Void {}
@@ -38,12 +58,16 @@ class FunkinTypedSpriteGroup implements ISpriteVars implements IZoo
sprite.updateHitbox();
}
}
+ public inline function killMembers():Void { group.killMembers(); }
+ public inline function reviveMembers():Void { group.reviveMembers(); }
public function sortZIndex() {
sort(Util.sortZIndex, FlxSort.ASCENDING);
}
- public function insertZIndex(obj:T) {
- if (members.contains(obj)) remove(obj);
+ public function insertZIndex(obj:T, ?zIndex:Int) {
+ if (members.contains(obj)) remove(obj, true);
+ if (zIndex != null) obj.zIndex = zIndex;
+
var low:Float = Math.POSITIVE_INFINITY;
for (pos => mem in members) {
low = Math.min(mem.zIndex, low);
@@ -57,12 +81,25 @@ class FunkinTypedSpriteGroup implements ISpriteVars implements IZoo
} else {
add(obj);
}
+
return obj;
}
+ public inline function moveToTop(sprite:T):T {
+ if (!members.contains(sprite)) return add(sprite);
+ members.remove(sprite);
+ members.push(sprite);
+ return sprite;
+ }
+ public inline function moveToBottom(sprite:T):T {
+ if (!members.contains(sprite)) return insert(0, sprite);
+ members.remove(sprite);
+ members.unshift(sprite);
+ return sprite;
+ }
override function preAdd(sprite:T):Void {
super.preAdd(sprite);
- var funk:IZoomFactor = getFunk(sprite);
+ var funk:IFunkinSpriteVars = getFunk(sprite);
if (funk != null) {
funk.zoomFactor = zoomFactor;
funk.initialZoom = initialZoom;
@@ -72,7 +109,7 @@ class FunkinTypedSpriteGroup implements ISpriteVars implements IZoo
function set_zoomFactor(value:Float):Float {
for (sprite in members) {
if (sprite == null) continue;
- var funk:IZoomFactor = getFunk(sprite);
+ var funk:IFunkinSpriteVars = getFunk(sprite);
if (funk != null) funk.zoomFactor = value;
}
return zoomFactor = value;
@@ -80,9 +117,98 @@ class FunkinTypedSpriteGroup implements ISpriteVars implements IZoo
function set_initialZoom(value:Float):Float {
for (sprite in members) {
if (sprite == null) continue;
- var funk:IZoomFactor = getFunk(sprite);
+ var funk:IFunkinSpriteVars = getFunk(sprite);
if (funk != null) funk.initialZoom = value;
}
return initialZoom = value;
}
+ inline function skewCallback(Scale:FlxPoint):Void {
+ for (sprite in members) {
+ if (sprite == null) continue;
+ var funk:IFunkinSpriteVars = getFunk(sprite);
+ if (funk != null) funk.skew.copyFrom(skew);
+ }
+ }
+
+ public inline function keyValueIterator():ArrayKeyValueIterator { return new ArrayKeyValueIterator(members); }
+
+ override function findMinXHelper():Float {
+ var value = Math.POSITIVE_INFINITY;
+ for (member in group.members) {
+ if (member == null) continue;
+
+ var minX:Float;
+ if (Std.isOfType(member, ISpriteGroup)) {
+ minX = cast(member, ISpriteGroup).findMinX();
+ } else if (member.flixelType == SPRITEGROUP) {
+ minX = (cast member:FlxSpriteGroup).findMinX();
+ } else {
+ minX = member.x;
+ }
+
+ if (minX < value) value = minX;
+ }
+ return value;
+ }
+ override function findMaxXHelper():Float {
+ var value = Math.NEGATIVE_INFINITY;
+ for (member in group.members) {
+ if (member == null) continue;
+
+ var maxX:Float;
+ if (Std.isOfType(member, ISpriteGroup)) {
+ maxX = cast(member, ISpriteGroup).findMaxX();
+ } else if (member.flixelType == SPRITEGROUP) {
+ maxX = (cast member:FlxSpriteGroup).findMaxX();
+ } else {
+ maxX = member.x + member.width;
+ }
+
+ if (maxX > value) value = maxX;
+ }
+ return value;
+ }
+ override function findMinYHelper():Float {
+ var value = Math.POSITIVE_INFINITY;
+ for (member in group.members) {
+ if (member == null) continue;
+
+ var minY:Float;
+ if (Std.isOfType(member, ISpriteGroup)) {
+ minY = cast(member, ISpriteGroup).findMinY();
+ } else if (member.flixelType == SPRITEGROUP) {
+ minY = (cast member:FlxSpriteGroup).findMinY();
+ } else {
+ minY = member.y;
+ }
+
+ if (minY < value) value = minY;
+ }
+ return value;
+ }
+ override function findMaxYHelper():Float {
+ var value = Math.NEGATIVE_INFINITY;
+ for (member in group.members) {
+ if (member == null) continue;
+
+ var maxY:Float;
+ if (Std.isOfType(member, ISpriteGroup)) {
+ maxY = cast(member, ISpriteGroup).findMaxY();
+ } else if (member.flixelType == SPRITEGROUP) {
+ maxY = (cast member:FlxSpriteGroup).findMaxY();
+ } else {
+ maxY = member.y + member.height;
+ }
+
+ if (maxY > value) value = maxY;
+ }
+ return value;
+ }
+}
+
+interface ISpriteGroup {
+ function findMinX():Float;
+ function findMaxX():Float;
+ function findMinY():Float;
+ function findMaxY():Float;
}
\ No newline at end of file
diff --git a/source/funkin/backend/FunkinState.hx b/source/funkin/backend/FunkinState.hx
index 2112d1a..1bbda0e 100644
--- a/source/funkin/backend/FunkinState.hx
+++ b/source/funkin/backend/FunkinState.hx
@@ -10,26 +10,31 @@ class FunkinState extends FlxSubState {
public var curBar:Int = -1;
public var curBeat:Int = -1;
public var curStep:Int = -1;
-
public var paused:Bool = false;
- public var conductorInUse:Conductor = Conductor.global;
+
+ public var events:Array> = [];
+ public var conductorInUse(default, set):Conductor;
public var barHit:FlxTypedSignal Void> = new FlxTypedSignal();
public var beatHit:FlxTypedSignal Void> = new FlxTypedSignal();
public var stepHit:FlxTypedSignal Void> = new FlxTypedSignal();
- public var events:Array> = [];
-
- public var hscripts:HScripts;
+ public var hscripts:HScriptGroup;
static var clearAssetsNow:Bool = false;
+ var firstRun:Bool = true;
- public function new() { // no one gaf about your bg color
+ public function new() {
super();
- hscripts = new HScripts([this], ['this' => this]);
+ conductorInUse = Conductor.global;
+ add(hscripts = new HScriptGroup([this], ['this' => this]));
+
+ persistentUpdate = true;
}
override public function create() {
+ FlxG.fixedTimestep = false;
+
Main.soundTray.reloadSoundtrayGraphics();
Paths.trackedAssets.resize(0);
if (clearAssetsNow) {
@@ -39,23 +44,48 @@ class FunkinState extends FlxSubState {
Paths.clean();
}
- super.create();
+ subStateOpened.add(onSubStateOpened);
+ subStateClosed.add(onSubStateClosed);
- conductorInUse.barHit.add(rhythmBarHit);
- conductorInUse.beatHit.add(rhythmBeatHit);
- conductorInUse.stepHit.add(rhythmStepHit);
+ super.create();
}
- function rhythmBarHit(t:Int) barHit.dispatch(t);
- function rhythmBeatHit(t:Int) beatHit.dispatch(t);
- function rhythmStepHit(t:Int) stepHit.dispatch(t);
+
+ public function onSubStateOpened(substate):Void {}
+ public function onSubStateClosed(substate):Void {}
+
override public function destroy() {
- conductorInUse.stepHit.remove(rhythmStepHit);
- conductorInUse.beatHit.remove(rhythmBeatHit);
- conductorInUse.barHit.remove(rhythmBarHit);
+ conductorInUse = null;
- hscripts.destroyAll();
super.destroy();
}
+
+ function set_conductorInUse(?newConductor:Conductor):Conductor {
+ if (conductorInUse == newConductor) return newConductor;
+
+ unhookConductor(conductorInUse);
+ hookConductor(newConductor);
+
+ return conductorInUse = newConductor;
+ }
+ function unhookConductor(conductor:Conductor) {
+ if (conductor == null) return;
+
+ conductor.advance.remove(updateEvents);
+ conductor.stepHit.remove(rhythmStepHit);
+ conductor.beatHit.remove(rhythmBeatHit);
+ conductor.barHit.remove(rhythmBarHit);
+ }
+ function hookConductor(conductor:Conductor) {
+ if (conductor == null) return;
+
+ if (!conductor.barHit.has(rhythmBarHit)) conductor.barHit.add(rhythmBarHit);
+ if (!conductor.beatHit.has(rhythmBeatHit)) conductor.beatHit.add(rhythmBeatHit);
+ if (!conductor.stepHit.has(rhythmStepHit)) conductor.stepHit.add(rhythmStepHit);
+ if (!conductor.advance.has(updateEvents)) conductor.advance.add(updateEvents);
+ }
+ function rhythmBarHit(t:Int) barHit.dispatch(t);
+ function rhythmBeatHit(t:Int) beatHit.dispatch(t);
+ function rhythmStepHit(t:Int) stepHit.dispatch(t);
public function sortZIndex() {
sort(Util.sortZIndex, FlxSort.ASCENDING);
@@ -98,26 +128,50 @@ class FunkinState extends FlxSubState {
updateConductor(elapsed);
super.update(elapsed);
}
+ @:allow(flixel.FlxGame)
+ override function tryUpdate(elapsed:Float):Void {
+ if (persistentUpdate || subState == null) {
+ if (firstRun) {
+ firstRun = false;
+ update(0);
+ } else {
+ update(elapsed);
+ }
+ }
+
+ if (subState != null)
+ subState.tryUpdate(elapsed);
+ if (_requestSubStateReset) {
+ _requestSubStateReset = false;
+ resetSubState();
+ }
+ }
public function updateConductor(elapsed:Float = 0) {
- conductorInUse.update(elapsed * 1000);
+ conductorInUse.update(elapsed * 1000 / FlxG.timeScale);
curBar = Math.floor(conductorInUse.bar);
curBeat = Math.floor(conductorInUse.beat);
curStep = Math.floor(conductorInUse.step);
-
- var limit:Int = 50; //avoid lags
- while (events.length > 0 && conductorInUse.songPosition >= events[0].msTime && limit > 0) {
- var event:ITimedEvent = events.shift();
- if (event.func != null)
- event.func(event);
- limit --;
- }
}
public function queueEvent(ms:Float = 0, ?func:Event -> Void) {
events.push(new Event(ms, func));
}
+ public function updateEvents(time:Float) {
+ var limit:Int = 50; //avoid lags
+ while (events.length > 0 && time >= events[0].msTime && limit > 0) {
+ var event:ITimedEvent = events.shift();
+ if (event.func != null) {
+ try {
+ event.func(event);
+ } catch (e:haxe.Exception) {
+ Log.error('error when triggering event -> ${e.details()}');
+ }
+ }
+ limit --;
+ }
+ }
public function playMusic(mus:String, forced:Bool = false) {
MusicHandler.playMusic(mus, forced);
@@ -129,4 +183,10 @@ class FunkinState extends FlxSubState {
return cast(FlxG.state, FunkinState).conductorInUse;
return Conductor.global;
}
+ public static function getCurrentSubState():FlxState {
+ var state:FlxState = FlxG.state;
+ while (state.subState != null)
+ state = state.subState;
+ return state;
+ }
}
\ No newline at end of file
diff --git a/source/funkin/backend/FunkinStrip.hx b/source/funkin/backend/FunkinStrip.hx
new file mode 100644
index 0000000..848bc59
--- /dev/null
+++ b/source/funkin/backend/FunkinStrip.hx
@@ -0,0 +1,49 @@
+package funkin.backend;
+
+import flixel.graphics.tile.FlxDrawTrianglesItem.DrawData;
+
+class FunkinStrip extends FunkinSprite {
+ public var vertices:DrawData = new DrawData();
+ public var uvtData:DrawData = new DrawData();
+ public var indices:DrawData = new DrawData();
+ public var colors:DrawData = new DrawData();
+
+ public var repeat:Bool = false;
+
+ override public function destroy():Void {
+ vertices = null;
+ indices = null;
+ uvtData = null;
+ colors = null;
+
+ super.destroy();
+ }
+
+ override public function draw():Void {
+ if (alpha == 0 || graphic == null || vertices == null)
+ return;
+
+ final cameras = getCamerasLegacy();
+ for (camera in cameras) {
+ if (!camera.visible || !camera.exists) return;
+
+ drawToCamera(camera);
+
+ #if FLX_DEBUG FlxBasic.visibleCount ++; #end
+ }
+ }
+
+ public function drawToCamera(camera:FlxCamera):Void {
+ var prev:Float = alpha;
+ alpha *= camera.alpha; // maybe figure out what is actually going on that causes strips not to consider alpha ...?
+
+ getScreenPosition(_point, camera).subtractPoint(offset);
+ #if !flash
+ camera.drawTriangles(graphic, vertices, indices, uvtData, colors, _point, blend, repeat, antialiasing, colorTransform, shader);
+ #else
+ camera.drawTriangles(graphic, vertices, indices, uvtData, colors, _point, blend, repeat, antialiasing);
+ #end
+
+ alpha = prev;
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/backend/FunkinText.hx b/source/funkin/backend/FunkinText.hx
new file mode 100644
index 0000000..c1937bc
--- /dev/null
+++ b/source/funkin/backend/FunkinText.hx
@@ -0,0 +1,142 @@
+package funkin.backend;
+
+import openfl.geom.Matrix;
+import flixel.util.FlxDestroyUtil;
+import flixel.graphics.frames.FlxFrame;
+
+class FunkinText extends FlxText implements funkin.backend.FunkinSprite.IFunkinSpriteVars {
+ public var zoomFactor(default, set):Float = 1;
+ public var initialZoom(default, set):Float = 1;
+ public var smooth(default, set):Bool = true;
+
+ public var transformMatrix(default, null):Matrix = new Matrix();
+ public var skew(default, null):FlxPoint = FlxPoint.get();
+ public var matrixExposed:Bool = false;
+
+ public var animOffset:FlxPoint;
+ public var spriteOffset:FlxPoint;
+ public var rotateOffsets:Bool = true;
+ public var scaleOffsets:Bool = true;
+ public var skewOffsets:Bool = true;
+
+ var _skewMatrix:Matrix = new Matrix();
+ var _transPoint:FlxPoint;
+
+ public function new(x:Float = 0, y:Float = 0, fieldWidth:Float = 0, ?text:String, size:Int = 8, isSmooth:Bool = false) {
+ super(x, y, fieldWidth, text, size);
+
+ _transPoint = new FlxPoint();
+ spriteOffset = FlxPoint.get();
+ animOffset = FlxPoint.get();
+ smooth = isSmooth;
+ }
+ public override function destroy() {
+ _transPoint = FlxDestroyUtil.put(_transPoint);
+ spriteOffset = FlxDestroyUtil.put(spriteOffset);
+ animOffset = FlxDestroyUtil.put(animOffset);
+ super.destroy();
+ }
+ public function setAnimOffset(x:Float = 0, y:Float = 0):Void {
+ animOffset.set(x, y);
+ }
+
+ public override function drawSimple(camera:FlxCamera) {
+ updateShader(camera);
+
+ getScreenPosition(_point, camera).subtractPoint(offset);
+ if (isPixelPerfectRender(camera))
+ _point.floor();
+
+ _point.copyToFlash(_flashPoint);
+ camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing);
+ }
+ public override function drawComplex(camera:FlxCamera) {
+ updateShader(camera);
+
+ _frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
+
+ _matrix.translate(-origin.x, -origin.y);
+ _matrix.scale(scale.x, scale.y);
+
+ if (matrixExposed) {
+ _matrix.concat(transformMatrix);
+ } else {
+ if (bakedRotationAngle <= 0) {
+ updateTrig();
+
+ if (angle != 0)
+ _matrix.rotateWithTrig(_cosAngle, _sinAngle);
+ }
+
+ updateSkewMatrix();
+ _matrix.concat(_skewMatrix);
+ }
+
+ transformSpriteOffset(_transPoint);
+ getScreenPosition(_point, camera);
+ _point.add(-offset.x, -offset.y);
+ _point.add(-_transPoint.x, -_transPoint.y);
+ _matrix.translate(_point.x + origin.x, _point.y + origin.y);
+
+ if (isPixelPerfectRender(camera)) {
+ _matrix.tx = Math.floor(_matrix.tx);
+ _matrix.ty = Math.floor(_matrix.ty);
+ }
+
+ FunkinSprite.transformMatrixZoom(_matrix, camera, zoomFactor, initialZoom);
+ camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
+ }
+
+ inline function transformSpriteOffset(point:FlxPoint):FlxPoint {
+ var xP:Float = (spriteOffset.x + animOffset.x) * (scaleOffsets ? scale.x : 1);
+ var yP:Float = (spriteOffset.y + animOffset.y) * (scaleOffsets ? scale.y : 1);
+
+ if (rotateOffsets && angle % 360 != 0) {
+ var rad:Float = angle / 180 * Math.PI;
+ var cos:Float = FlxMath.fastCos(rad);
+ var sin:Float = FlxMath.fastSin(rad);
+ point.set(cos * xP - sin * yP, cos * yP + sin * xP);
+ } else {
+ point.set(xP, yP);
+ }
+
+ if (skewOffsets && (skew.x != 0 || skew.y != 0)) {
+ point.set(
+ point.x + point.y * Math.tan(skew.x / 180 * Math.PI),
+ point.y + point.x * Math.tan(skew.y / 180 * Math.PI)
+ );
+ }
+
+ return point;
+ }
+ function updateSkewMatrix():Void {
+ _skewMatrix.identity();
+
+ if (skew.x != 0 || skew.y != 0) {
+ _skewMatrix.b = Math.tan(skew.y / 180 * Math.PI);
+ _skewMatrix.c = Math.tan(skew.x / 180 * Math.PI);
+ }
+ }
+ function updateShader(camera:FlxCamera) {
+ if (shader == null || !Std.isOfType(shader, FunkinRuntimeShader))
+ return;
+
+ var funk:FunkinRuntimeShader = cast shader;
+ funk.postUpdateView(camera);
+ funk.postUpdateFrame(frame);
+ }
+ public override function isSimpleRenderBlit(?camera:FlxCamera):Bool {
+ return (zoomFactor == 1 && skew.x == 0 && skew.y == 0 && super.isSimpleRenderBlit(camera));
+ }
+
+ function set_smooth(newSmooth:Bool):Bool {
+ antialiasing = (newSmooth && Options.data.antialiasing);
+ return (smooth = newSmooth);
+ }
+ function set_zoomFactor(value:Float):Float {
+ return zoomFactor = value;
+ }
+ function set_initialZoom(value:Float):Float {
+ return initialZoom = value;
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/backend/Paths.hx b/source/funkin/backend/Paths.hx
index a022273..cce3104 100644
--- a/source/funkin/backend/Paths.hx
+++ b/source/funkin/backend/Paths.hx
@@ -1,13 +1,14 @@
package funkin.backend;
-import flixel.util.FlxDestroyUtil.IFlxDestroyable;
import openfl.utils.Assets as OFLAssets;
import lime.utils.Assets as LimeAssets;
import flxanimate.data.AnimationData;
+import flixel.util.FlxDestroyUtil;
import flixel.graphics.FlxGraphic;
import openfl.display.BitmapData;
import flixel.graphics.frames.*;
import flixel.system.FlxAssets;
+import funkin.util.MemoryUtil;
import openfl.utils.AssetType;
import openfl.media.Sound;
import openfl.Assets;
@@ -52,16 +53,12 @@ class Paths {
}
for (key => dyn in dynamicCache) {
if (!trackedAssets.contains(key) && !excludeKeys.contains(key)) {
- if (dyn != null) {
- if (Std.isOfType(dyn, IFlxDestroyable))
- try dyn.destroy();
- dyn = null;
- }
+ if (dyn is IFlxDestroyable) FlxDestroyUtil.destroy(dyn);
dynamicCache.remove(key);
}
}
FlxG.bitmap.clearUnused();
- runGC();
+ MemoryUtil.collect();
}
inline public static function excludedGraphicKeys():Array {
var exclusions:Array = excludeKeys.copy();
@@ -69,11 +66,10 @@ class Paths {
for (spr in excludeSprites) exclusions.push(spr.graphic.key);
return exclusions;
}
+
+ @:deprecated('Paths.runGC is deprecated, use MemoryUtil.collect instead!')
public static function runGC() {
- openfl.system.System.gc();
- #if hl
- hl.Gc.major();
- #end
+ MemoryUtil.collect();
}
public static function getPath(key:String, allowMods:Bool = true, ?library:String) {
@@ -83,26 +79,9 @@ class Paths {
var path:String;
var allMods:Bool = (Mods.currentMod == null);
- var priorize:Bool = (!allMods);
-
- if (!allMods && Mods.currentMod != '') { // current mod is high priority
- var curMod:Mod = Mods.modByDirectory(Mods.currentMod);
-
- priorize = true;
- if (curMod.doLoad) {
- path = modPath(key, Mods.currentMod, library);
- if (FileSystem.exists(path)) {
- return path;
- } else {
- path = modPath(key, Mods.currentMod);
- if (FileSystem.exists(path))
- return path;
- }
- }
- }
for (mod in Mods.get()) {
- if (!mod.doLoad || !mod.enabled || (!allMods && !mod.global) || (priorize && mod.directory == Mods.currentMod))
+ if (!mod.doLoad || !mod.enabled || (!allMods && !mod.global && mod.directory != Mods.currentMod))
continue;
path = modPath(key, mod.directory, library);
@@ -139,22 +118,10 @@ class Paths {
files.push({path: globalModPath(key), type: GLOBAL});
var path:String;
- var priorize:Bool = (!allMods);
-
- if (Mods.currentMod == null) {
- allMods = true;
- priorize = false;
- } else if (Mods.currentMod != '') { // current mod is high priority
- var curMod:Mod = Mods.modByDirectory(Mods.currentMod);
-
- priorize = true;
- path = modPath(key, Mods.currentMod, library);
- if (curMod.doLoad && FileSystem.exists(path))
- files.push({mod: Mods.currentMod, path: path, type: MOD});
- }
+ var allMods:Bool = (Mods.currentMod == null);
for (mod in Mods.get()) {
- if (!mod.doLoad || !mod.enabled || (!allMods && !mod.global) || (priorize && mod.directory == Mods.currentMod))
+ if (!mod.doLoad || !mod.enabled || (!allMods && !mod.global && mod.directory != Mods.currentMod))
continue;
path = modPath(key, mod.directory, library);
@@ -185,7 +152,9 @@ class Paths {
return (FileSystem.exists(modPath(key, mod, library)));
inline public static function exists(key:String, allowMods:Bool = true, ?library:String):Bool
return (getPath(key, allowMods, library) != null);
-
+
+ inline public static function video(key:String, ?library:String, ?format:String = 'mp4')
+ return getPath('videos/$key.$format', library);
inline public static function sound(key:String, ?library:String)
return ogg('sounds/$key', false, library);
inline public static function music(key:String, ?library:String)
diff --git a/source/funkin/backend/DiscordRpc.hx b/source/funkin/backend/api/DiscordRpc.hx
similarity index 86%
rename from source/funkin/backend/DiscordRpc.hx
rename to source/funkin/backend/api/DiscordRpc.hx
index cb5d603..32b3883 100644
--- a/source/funkin/backend/DiscordRpc.hx
+++ b/source/funkin/backend/api/DiscordRpc.hx
@@ -1,4 +1,4 @@
-package funkin.backend;
+package funkin.backend.api;
#if hxdiscord_rpc
import hxdiscord_rpc.Discord;
@@ -6,7 +6,7 @@ import hxdiscord_rpc.Types;
import sys.thread.Thread;
#end
-class DiscordRPC {
+class DiscordRpc {
public static var supported(default, never):Bool = #if hxdiscord_rpc true #else false #end;
public static var dirty:Bool = false;
@@ -15,6 +15,8 @@ class DiscordRPC {
public static var details(default, set):String = '';
public static var state(default, set):String = '';
+
+ static var success:Null = false;
static function set_details(newDetails:String):String {
if (details == newDetails) return newDetails;
@@ -89,15 +91,16 @@ class DiscordRPC {
private static function onReady(request:cpp.RawConstPointer):Void {
final username:String = request[0].username;
- final globalName:String = request[0].username;
final discriminator:Int = Std.parseInt(request[0].discriminator);
if (discriminator != 0) {
- Log.info('Discord: connected to user $username#$discriminator ($globalName)!');
+ Log.info('discord: connected to user @$username#$discriminator ($username)!');
} else {
- Log.info('Discord: connected to user @$username ($globalName)!');
+ Log.info('discord: connected to user @$username ($username)!');
}
+ success = true;
+
var gitButton:DiscordButton = new DiscordButton();
gitButton.url = 'https://github.com/inky03/FUNKINX3';
gitButton.label = 'On GitHub';
@@ -107,10 +110,14 @@ class DiscordRPC {
refresh();
}
private static function onDisconnected(errorCode:Int, message:cpp.ConstCharStar):Void {
- Log.error('Discord: disconnected ($errorCode:$message)');
+ if (success != null)
+ Log.info('discord: disconnected (code $errorCode -> $message)');
+ success = null;
}
private static function onError(errorCode:Int, message:cpp.ConstCharStar):Void {
- Log.error('Discord: $errorCode:$message');
+ if (success != false)
+ Log.error('discord: error (code $errorCode)... -> $message');
+ success = false;
}
#else
public static var presence:Dynamic = {};
diff --git a/source/funkin/backend/play/Chart.hx b/source/funkin/backend/play/Chart.hx
index 7cb06ce..25d6a6a 100644
--- a/source/funkin/backend/play/Chart.hx
+++ b/source/funkin/backend/play/Chart.hx
@@ -35,6 +35,7 @@ class Chart {
public var name:String = 'Unnamed';
public var artist:String = 'Unknown';
public var difficulty:String = '';
+ public var noteStyle:String = 'funkin';
public var format:ChartFormat = UNKNOWN;
public var chart:Any; //BasicFormat?
@@ -50,6 +51,7 @@ class Chart {
public var instLoaded:Bool;
public var inst:FunkinSound;
public var songLength:Float = 0;
+ public var audioOffset:Float = 0;
public var audioSuffix:String = '';
public var player1:String = 'bf';
@@ -95,7 +97,7 @@ class Chart {
}
}
- function loadGeneric(format:Dynamic, difficulty:String, ?playerNoteFilter:BasicNote -> Bool) {
+ function loadGeneric(format:Dynamic, difficulty:String, ?strumlineNoteFilter:BasicNote -> Int) {
this.difficulty = difficulty;
this.chart = format;
@@ -143,21 +145,30 @@ class Chart {
tempMetronome.tempoChanges = this.tempoChanges;
var notes:Array = format.getNotes(difficulty);
for (note in notes) {
- var isPlayer:Bool;
- if (playerNoteFilter != null) {
- isPlayer = playerNoteFilter(note);
+ var strumlineIndex:Int = 0;
+ if (strumlineNoteFilter != null) {
+ strumlineIndex = strumlineNoteFilter(note);
} else {
- isPlayer = note.lane >= 4;
+ strumlineIndex = note.lane % keyCount;
}
tempMetronome.setMS(note.time + 1);
var stepCrochet:Float = tempMetronome.getCrochet(tempMetronome.bpm, tempMetronome.timeSignature.denominator) * .25;
- this.notes.push({player: isPlayer, msTime: note.time, laneIndex: Std.int(note.lane % 4), msLength: note.length - stepCrochet, kind: note.type});
+ this.notes.push({strumlineIndex: strumlineIndex, msTime: note.time, laneIndex: Std.int(note.lane % 4), msLength: note.length - stepCrochet, kind: note.type});
}
this.sort();
this.findSongLength();
+ this.clearStackedNotes();
return this;
}
+ public function getStrumlineCount():Int {
+ var strumlines:Int = 2;
+ for (note in notes) {
+ if (strumlines < note.strumlineIndex)
+ strumlines = note.strumlineIndex;
+ }
+ return strumlines;
+ }
public function findSongLength() {
if (instLoaded) {
this.songLength = inst.length;
@@ -167,14 +178,45 @@ class Chart {
}
return this.songLength;
}
+ public function clearStackedNotes(minDifference:Float = 6, suicide:Bool = false) {
+ if (notes.length >= 10000 && !suicide) {
+ Log.warning('chart contains TOO MANY notes (${notes.length} / 10000), won\'t check for note stacking');
+ return;
+ }
+
+ var previousNotes:Array> = [];
+ var caught:Int = 0;
+ var i:Int = notes.length;
+
+ while (i > 0) {
+ var note:ChartNote = notes[-- i];
+
+ var lane:Int = note.laneIndex;
+ var strumline:Int = note.strumlineIndex;
+
+ while (previousNotes.length <= strumline) previousNotes.push([]);
+ while (previousNotes[strumline].length <= lane) previousNotes[strumline].push(null);
+ var prevNote:ChartNote = previousNotes[strumline][lane];
+
+ if (prevNote != null && Math.abs(note.msTime - prevNote.msTime) < minDifference &&
+ prevNote.kind == note.kind && prevNote.laneIndex == lane && prevNote.strumlineIndex == strumline) {
+ notes.remove(note);
+ caught ++;
+ }
+ previousNotes[strumline][lane] = note;
+ }
+
+ if (caught > 0)
+ Log.warning('caught and deleted $caught stacked ${caught == 1 ? 'note' : 'notes'} in chart!');
+ }
// TODO: these could just not be static
// suffix is for playable characters
- static function loadLegacyChart(path:String, difficulty:String = 'normal', suffix:String = '', keyCount:Int = 4) { // move to moonchart format???
+ static function loadLegacyChart(path:String, difficulty:String = 'normal', suffix:String = '', ?keyCount:Int) { // move to moonchart format???
difficulty = difficulty.toLowerCase();
Log.minor('loading legacy FNF song "$path" with difficulty "$difficulty"${suffix == '' ? '' : ' ($suffix)'}');
- var song = new Chart(path, keyCount);
+ var song = new Chart(path);
song.json = loadLegacyJson(path, difficulty);
song.difficulty = difficulty;
@@ -234,13 +276,18 @@ class Chart {
}
song.name = song.json.song;
song.initialBpm = song.json.bpm;
+ song.audioOffset = song.json.offset ?? 0;
song.tempoChanges = [new TempoChange(-4, song.initialBpm, new TimeSignature())];
song.scrollSpeed = songSpeed;
+ song.keyCount = (song.json.keys ?? keyCount ?? song.keyCount);
+ song.noteStyle = song.json.noteStyle ?? 'funkin';
+
var ms:Float = 0;
var beat:Float = 0;
- var sectionNumerator:Float = 0;
- var osectionNumerator:Float = 0;
+
+ var numerator:Int = 0;
+ var onumerator:Int = 0;
var bpm:Float = song.initialBpm;
var crochet:Float = 60000 / song.initialBpm;
@@ -277,22 +324,32 @@ class Chart {
}
var sectionDenominator:Int = 4;
- var sectionNumerator:Null = section.sectionBeats;
- if (sectionNumerator == null) sectionNumerator = section.lengthInSteps * .25;
- if (sectionNumerator == null) sectionNumerator = 4;
+ var sectionNumerator:Null = section.sectionBeats ?? ((section?.lengthInSteps ?? 16) * .25);
while (sectionNumerator % 1 > 0 && sectionDenominator < 32) {
sectionNumerator *= 2;
sectionDenominator *= 2;
}
- var changeSign:Bool = (sectionNumerator != osectionNumerator);
- if (section.changeBPM || changeSign) {
- osectionNumerator = sectionNumerator;
- if (section.changeBPM) bpm = section.bpm;
- crochet = 60000 / bpm / sectionDenominator * 4;
+ numerator = Std.int(sectionNumerator);
+
+ var changeTempo:Bool = false;
+ var newSign:TimeSignature = null;
+
+ if (numerator != onumerator) {
+ newSign = new TimeSignature(Math.ceil(numerator), sectionDenominator);
+ onumerator = numerator;
+ changeTempo = true;
+ }
+ if (section.changeBPM) {
+ bpm = section.bpm;
+ changeTempo = true;
+ }
+
+ if (changeTempo) {
+ crochet = (60000 / bpm / sectionDenominator * 4);
stepCrochet = crochet * .25;
-
- song.tempoChanges.push(new TempoChange(beat, section.changeBPM ? section.bpm : null, changeSign ? new TimeSignature(Std.int(sectionNumerator), sectionDenominator) : null));
+ song.tempoChanges.push(new TempoChange(beat, section.changeBPM ? bpm : null, newSign));
}
+
beat += sectionNumerator;
ms += sectionNumerator * crochet;
@@ -307,14 +364,18 @@ class Chart {
var noteLength:Float = dataNote[2];
var noteKind:Dynamic = dataNote[3];
if (!Std.isOfType(noteKind, String)) noteKind = '';
- var playerNote:Bool;
+ var strumlineIndex:Int = 0;
if (fromSong) {
- playerNote = ((noteData < keyCount) == section.mustHitSection);
+ strumlineIndex = Std.int(noteData / song.keyCount);
+ if (section.mustHitSection)
+ strumlineIndex += (strumlineIndex % 2 == 0 ? 1 : -1);
} else { // assume psych 1.0
- playerNote = (noteData < keyCount);
+ strumlineIndex = Std.int(noteData / song.keyCount);
+ if (strumlineIndex < 2) // how silly
+ strumlineIndex = 1 - strumlineIndex;
}
- song.notes.push({player: playerNote, msTime: noteTime, laneIndex: noteData % keyCount, msLength: noteLength, kind: noteKind});
+ song.notes.push({strumlineIndex: strumlineIndex, msTime: noteTime, laneIndex: noteData % song.keyCount, msLength: noteLength, kind: noteKind});
}
}
song.sort();
@@ -332,12 +393,13 @@ class Chart {
}
song.audioSuffix = suffix;
+ song.clearStackedNotes();
return song;
}
static function loadStepMania(path:String, difficulty:String = 'Beginner', suffix:String = '') {
difficulty = difficulty.toLowerCase();
Log.minor('loading StepMania simfile "$path" with difficulty "$difficulty"${suffix == '' ? '' : ' ($suffix)'}');
-
+
var songPath:String = 'data/songs/$path/$path';
var sscPath:String = '${Util.pathSuffix(songPath, suffix)}.ssc';
var smPath:String = '$songPath.sm';
@@ -350,7 +412,7 @@ class Chart {
Log.minor('- chart: $smPath OR $sscPath');
return song;
}
-
+
var time = Sys.time();
var shark:StepManiaShark;
@:privateAccess try {
@@ -364,7 +426,7 @@ class Chart {
}
var notes:Array = shark.getNotes(difficulty);
var dance:StepManiaDance = shark.resolveDance(notes);
- song.loadGeneric(shark, difficulty, (note:BasicNote) -> (dance == SINGLE ? note.lane < 4 : note.lane >= 4));
+ song.loadGeneric(shark, difficulty, (note:BasicNote) -> ((dance == SINGLE ? note.lane < 4 : note.lane >= 4) ? 1 : 0));
song.format = (useShark ? SHARK : STEPMANIA);
Log.info('chart loaded successfully! (${Math.round((Sys.time() - time) * 1000) / 1000}s)');
@@ -378,12 +440,12 @@ class Chart {
static function loadModernChart(path:String, difficulty:String = 'normal', suffix:String = '') {
difficulty = difficulty.toLowerCase();
Log.minor('loading modern FNF song "$path" with difficulty "$difficulty"${suffix == '' ? '' : ' ($suffix)'}');
-
+
var songPath:String = 'data/songs/$path/$path';
var chartPath:String = '${Util.pathSuffix('$songPath-chart', suffix)}.json';
var metaPath:String = '${Util.pathSuffix('$songPath-metadata', suffix)}.json';
var song:Chart = new Chart(path, 4);
-
+
if (!Paths.exists(chartPath) || !Paths.exists(metaPath)) {
Log.warning('chart or metadata JSON not found... (chart not generated)');
Log.minor('verify paths:');
@@ -391,39 +453,40 @@ class Chart {
Log.minor('- metadata: $metaPath');
return song;
}
-
+
var time = Sys.time();
var vslice:FNFVSlice;
try {
var chartContent:String = Paths.text(chartPath);
var metaContent:String = Paths.text(metaPath);
vslice = new FNFVSlice().fromJson(chartContent, metaContent);
- song.loadGeneric(vslice, difficulty, (note:BasicNote) -> note.lane >= 4);
+ song.loadGeneric(vslice, difficulty, (note:BasicNote) -> Std.int(note.lane / song.keyCount));
var meta:BasicMetaData = vslice.getChartMeta();
song.player1 = meta.extraData['FNF_P1'] ?? 'bf';
song.player2 = meta.extraData['FNF_P2'] ?? 'dad';
song.player3 = meta.extraData['FNF_P3'] ?? 'gf';
song.stage = meta.extraData['FNF_STAGE'] ?? 'placeholder';
+ song.noteStyle = vslice.meta.playData?.noteStyle ?? 'funkin';
song.format = MODERN;
Log.info('chart loaded successfully! (${Math.round((Sys.time() - time) * 1000) / 1000}s)');
} catch (e:Exception) {
Log.error('chart error... -> <<< ${e.details()} >>>');
}
-
+
song.audioSuffix = suffix;
return song;
}
static function loadCNEChart(path:String, difficulty:String = 'Normal', suffix:String = '') {
Log.minor('loading CNE song "$path" with difficulty "$difficulty"${suffix == '' ? '' : ' ($suffix)'}');
-
+
var songPath:String = 'data/songs/$path';
var chartPath:String = '$songPath/charts/${Util.pathSuffix(difficulty, suffix)}.json';
var metaPath:String = '$songPath/${Util.pathSuffix('meta', suffix)}.json';
var chartPathA:String = chartPath;
var song:Chart = new Chart(path, 4);
-
+
if (!Paths.exists(chartPath)) chartPath = '$songPath/$difficulty.json';
if (!Paths.exists(chartPath) || !Paths.exists(metaPath)) {
Log.warning('chart or metadata JSON not found... (chart not generated)');
@@ -432,14 +495,14 @@ class Chart {
Log.minor('- metadata: $metaPath');
return song;
}
-
+
var time = Sys.time();
var cne:FNFCodename;
try {
var metaContent:String = Paths.text(metaPath);
var chartContent:String = Paths.text(chartPath);
cne = new FNFCodename().fromJson(chartContent, metaContent);
- song.loadGeneric(cne, difficulty, (note:BasicNote) -> note.lane >= 4);
+ song.loadGeneric(cne, difficulty, (note:BasicNote) -> Std.int(note.lane / song.keyCount));
var meta:BasicMetaData = cne.getChartMeta();
song.player1 = meta.extraData['FNF_P1'] ?? 'bf';
@@ -452,7 +515,7 @@ class Chart {
} catch (e:Exception) {
Log.error('chart error... -> <<< ${e.details()} >>>');
}
-
+
song.audioSuffix = suffix;
return song;
}
@@ -539,64 +602,6 @@ class Chart {
return null;
}
}
-
- public function generateNotes(singleSegmentHolds:Bool = false):Array {
- var time:Float = Sys.time();
- Log.minor('generating notes from song');
- var notes:Array = generateNotesFromArray(notes, singleSegmentHolds, this);
- Log.info('generated ${notes.length} note objects! (${Math.round((Sys.time() - time) * 1000) / 1000}s)');
- return notes;
- }
- public static function generateNotesFromArray(songNotes:Array, singleSegmentHolds:Bool = false, ?chart:Chart) {
- var noteArray:Array = [];
- var tempMetronome:Metronome = null;
- var type:Dynamic = (CharterState.inEditor ? CharterNote : Note);
- if (chart != null) {
- tempMetronome = new Metronome();
- tempMetronome.tempoChanges = chart.tempoChanges;
- }
-
- for (songNote in songNotes) {
- tempMetronome?.setMS(songNote.msTime);
- var hitNote:Note = Type.createInstance(type, [songNote.player, songNote.msTime, songNote.laneIndex, songNote.msLength, songNote.kind]);
- noteArray.push(hitNote);
-
- if (hitNote.msLength > 0) { //hold bits
- var endMs:Float = songNote.msTime + songNote.msLength;
- if (!singleSegmentHolds && tempMetronome != null) {
- var bitTime:Float = songNote.msTime;
- while (bitTime < endMs) {
- tempMetronome.setStep(Std.int(tempMetronome.step + .05) + 1);
- var newTime:Float = tempMetronome.ms;
- if (bitTime < songNote.msTime) {
- Log.warning('??? $bitTime < ${songNote.msTime} (sustain bit off by ${songNote.msTime - bitTime}ms)');
- bitTime = newTime;
- break;
- }
- var bitLength:Float = Math.min(newTime - bitTime, endMs - bitTime);
- var holdBit:Note = Type.createInstance(type, [songNote.player, bitTime, songNote.laneIndex, bitLength, songNote.kind, true]);
- hitNote.children.push(holdBit);
- holdBit.parent = hitNote;
- noteArray.push(holdBit);
- bitTime = newTime;
- }
- } else {
- var holdBit:Note = Type.createInstance(type, [songNote.player, songNote.msTime, songNote.laneIndex, songNote.msLength, songNote.kind, true]);
- hitNote.children.push(holdBit);
- holdBit.parent = hitNote;
- noteArray.push(holdBit);
- }
- var endBit:Note = Type.createInstance(type, [songNote.player, endMs, songNote.laneIndex, 0, songNote.kind, true]);
- hitNote.children.push(endBit);
- noteArray.push(endBit);
-
- endBit.parent = hitNote;
- hitNote.tail = endBit;
- }
- }
-
- return noteArray;
- }
public function loadMusic(path:String, overwrite:Bool = true) { // this could be better
if (instLoaded && !overwrite) return true;
@@ -613,6 +618,7 @@ class Chart {
inst.play();
inst.stop();
inst.volume = 1;
+ inst.looped = false;
Log.info('instrumental loaded!! (${Math.round((Sys.time() - time) * 1000) / 1000}s)');
return true;
}
@@ -632,29 +638,22 @@ class Chart {
}
}
-enum ChartFormat {
- AUTO;
- MODERN;
- LEGACY; // psych / pre0.3
- STEPMANIA;
- SHARK;
- CNE;
+enum abstract ChartFormat(String) to String {
+ var AUTO = 'auto';
+ var MODERN = 'modern';
+ var LEGACY = 'legacy'; // psych / pre0.3
+ var STEPMANIA = 'stepmania';
+ var SHARK = 'stepmaniashark';
+ var CNE = 'codenameengine';
- UNKNOWN;
+ var UNKNOWN = 'unknown';
}
-@:structInit class ChartNote implements ITimeSortable {
- public var laneIndex:Int;
- public var msTime:Float = 0;
- public var kind:String = '';
- public var msLength:Float = 0;
- public var player:Bool = true;
-}
@:structInit class ChartEvent implements ITimedEvent {
public var name:String;
public var msTime:Float = 0;
public var params:Map;
- public var func:ChartEvent -> Void = genericFunction;
+ public var func:#if hl Dynamic #else ChartEvent #end -> Void = genericFunction;
public static function genericFunction(e:ChartEvent) {
var chartEvent:ChartEvent = cast e;
diff --git a/source/funkin/backend/play/HitWindow.hx b/source/funkin/backend/play/HitWindow.hx
deleted file mode 100644
index fd9340f..0000000
--- a/source/funkin/backend/play/HitWindow.hx
+++ /dev/null
@@ -1,21 +0,0 @@
-package funkin.backend.play;
-
-class HitWindow {
- public var count:Int;
- public var score:Float;
- public var rating:String;
- public var threshold:Float;
- public var healthMod:Float;
- public var accuracyMod:Float;
- public var splash:Bool = false;
- public var breaksCombo:Bool = false;
-
- public function new(rating:String, score:Float, threshold:Float, ratingMod:Float, healthMod:Float = 1) {
- this.count = 0;
- this.score = score;
- this.rating = rating;
- this.threshold = threshold;
- this.healthMod = healthMod;
- this.accuracyMod = ratingMod;
- }
-}
\ No newline at end of file
diff --git a/source/funkin/backend/play/IPlayEvent.hx b/source/funkin/backend/play/IPlayEvent.hx
new file mode 100644
index 0000000..d04c595
--- /dev/null
+++ b/source/funkin/backend/play/IPlayEvent.hx
@@ -0,0 +1,8 @@
+package funkin.backend.play;
+
+interface IPlayEvent {
+ public var cancelled:Bool;
+
+ public function cancel():Void;
+ public function dispatch():Void;
+}
\ No newline at end of file
diff --git a/source/funkin/backend/play/NoteEvent.hx b/source/funkin/backend/play/NoteEvent.hx
index 54f3d22..796c172 100644
--- a/source/funkin/backend/play/NoteEvent.hx
+++ b/source/funkin/backend/play/NoteEvent.hx
@@ -4,169 +4,218 @@ import funkin.states.PlayState;
import funkin.objects.Character;
import funkin.objects.play.Note;
import funkin.objects.play.Lane;
-import funkin.backend.play.Scoring;
import funkin.objects.play.Strumline;
+import funkin.backend.play.ScoreSystem;
+import funkin.backend.play.ScoreHandler;
-@:structInit class NoteEvent {
+using StringTools;
+
+@:structInit class NoteEvent implements IPlayEvent { // TODO: EVENT RECYCLER
+ public var type(default, null):NoteEventType;
+ public var cancelled:Bool = false;
+
public var note:Note;
public var lane:Lane;
public var receptor:Receptor;
- public var type:NoteEventType;
public var strumline:Strumline;
- public var cancelled:Bool = false;
public var animSuffix:String = '';
+ public var songPosition:Float = 0;
+ public var holdDelta:Float = 0;
public var spark:NoteSpark = null;
public var splash:NoteSplash = null;
- public var scoring:Scoring.Score = null;
+ public var score:Score = null;
+ public var scoring(get, set):Score;
public var scoreHandler:ScoreHandler = null;
- public var targetCharacter:ICharacter = null;
public var perfect:Bool = false; // release event
public var doSpark:Bool = false; // many vars...
public var doSplash:Bool = false;
public var playSound:Bool = false;
+ public var popCover:Bool = true;
+ public var popRating:Bool = true;
+ public var applyHealth:Bool = false;
public var applyRating:Bool = false;
public var playAnimation:Bool = true;
public var animateReceptor:Bool = true;
-
+ public var singAnimation:Null = null;
+ public var targetCharacter:ICharacter = null;
+
+ var game:PlayState = null;
+ var inGame:Bool = false;
+
public function cancel() cancelled = true;
- public function dispatch() { // hahaaa
- if (cancelled) return;
- var game:PlayState;
- if (Std.isOfType(FlxG.state, PlayState)) {
+ public inline function setup() {
+ inGame = Std.isOfType(FlxG.state, PlayState);
+ if (inGame) {
game = cast FlxG.state;
scoreHandler ??= game.scoring;
- } else {
- throw(new haxe.Exception('note event can\'t be dispatched outside of PlayState!!'));
- return;
}
+
+ targetCharacter ??= lane.character;
+ singAnimation ??= lane.getSingAnimation();
+ }
+ public function dispatch() { // hahaaa
+ if (cancelled) return;
+
switch (type) {
case HIT:
if (game.genericVocals != null)
game.genericVocals.volume = 1;
- if (targetCharacter != null)
+ if (targetCharacter != null) {
targetCharacter.volume = 1;
+ targetCharacter.held = true;
+ }
- note.hitTime = lane.conductorInUse.songPosition;
- if (!note.isHoldPiece) {
- // if (lane.heldNote != null)
- // lane.hitSustainsOf(lane.heldNote);
- lane.heldNote = note;
+ note.hitTime = note.holdTime = songPosition;
- if (playSound)
- game.hitsound.play(true);
-
- if (applyRating) {
- scoring ??= scoreHandler?.judgeNoteHit(note, (lane.cpu ? 0 : note.msTime - lane.conductorInUse.songPosition));
- var rating:FunkinSprite = game.popRating(scoring.rating);
- rating.velocity.y = -FlxG.random.int(140, 175);
- rating.velocity.x = FlxG.random.int(0, 10);
- rating.acceleration.y = 550;
- applyExtraWindow(6);
-
- game.totalHits ++;
- game.totalNotes ++;
- game.health += note.healthGain * scoring.healthMod;
- if (scoreHandler != null) {
- scoreHandler.countRating(scoring.rating);
- note.score = scoring;
-
- scoreHandler.score += scoring.score;
- scoreHandler.addMod(scoring.accuracyMod);
- if (scoring.hitWindow != null && scoring.hitWindow.breaksCombo) {
- scoreHandler.combo = 0; // maybe add the ghost note here?
- } else {
- scoreHandler.combo ++;
- }
+ if (playSound)
+ game.hitsound.play(true);
+
+ if (applyRating) {
+ applyExtraWindow(6);
+ score ??= scoreHandler?.judgeNoteHit(note, note.msTime - songPosition);
+
+ if (inGame) {
+ if (popRating) {
+ var rating:FunkinSprite = game.popRating('gameplay/funkin/${score.rating}');
+ rating.velocity.y = -FlxG.random.int(140, 175);
+ rating.velocity.x = FlxG.random.int(0, 10);
+ rating.acceleration.y = 550;
}
- game.updateScoreText();
+ if (applyHealth)
+ game.health += note.healthGain * score.healthMod;
}
- if (doSplash && (scoring.hitWindow == null || scoring.hitWindow.splash))
- splash = lane.splash();
+ applyScore(scoreHandler, score, game);
+ note.score = score;
}
+ if (doSplash && (score?.hitWindow == null || score.hitWindow.splash))
+ splash = lane.splash(note);
+
if (playAnimation && targetCharacter != null) {
- var anim:String = 'sing${game.singAnimations[note.noteData]}';
- var suffixAnim:String = anim + targetCharacter.animSuffix;
- if (targetCharacter.animationExists(suffixAnim)) {
- if (!note.isHoldPiece)
- targetCharacter.playAnimationSoft(suffixAnim, true);
- targetCharacter.timeAnimSteps();
- }
+ var suffixAnim:String = '$singAnimation$animSuffix';
+ if (targetCharacter.animationExists(suffixAnim + targetCharacter.animSuffix))
+ targetCharacter.playAnimationSteps(suffixAnim, true);
}
- if (animateReceptor) lane.receptor.playAnimation('confirm', true);
- if (!note.isHoldPiece) {
- if (note.msLength > 0) {
- lane.held = true;
- for (child in note.children) {
- child.canHit = true;
- lane.updateNote(child);
+ if (animateReceptor)
+ lane.receptor.playAnimation('confirm', true);
+
+ if (note.isHoldNote) {
+ lane.held = true;
+ lane.heldNote = note;
+ if (popCover) spark = lane.popCover(note);
+ } else if (animateReceptor && !lane.cpu) {
+ lane.receptor.grayBeat = note.beatTime + .5;
+ }
+ case PRESSED:
+ lane.pressed = true;
+
+ if (note != null) {
+ lane.hitNote(note, true, songPosition);
+ } else {
+ lane.ghostTapped(songPosition);
+ }
+ case HELD | RELEASED:
+ final released:Bool = (type == RELEASED);
+
+ if (released && note == null) {
+ lane.held = false;
+ lane.pressed = false;
+
+ if (animateReceptor && !lane.cpu)
+ receptor.playAnimation('static');
+
+ if (targetCharacter != null) {
+ var canUnhold:Bool = true;
+
+ if (strumline != null) {
+ for (lane in strumline.lanes)
+ canUnhold = canUnhold && !lane.pressed;
}
- } else if (!lane.cpu && animateReceptor) {
- lane.receptor.grayBeat = note.beatTime + 1;
+
+ if (canUnhold)
+ targetCharacter.held = false;
}
+
+ return;
}
- case HELD | RELEASED:
+
var perfectRelease:Bool = true;
- final released:Bool = (type == RELEASED);
- final songPos:Float = lane.conductorInUse.songPosition;
- perfect = (released && songPos >= note.endMs - Scoring.holdLeniencyMS);
- if (applyRating) {
+ final songPos:Float = songPosition;
+
+ perfect = (released && songPos >= note.endMs - scoreHandler.system.holdLeniencyMS);
+
+ if (applyRating && scoreHandler.system.holdScoring) {
perfectRelease = perfect;
- }
- /* ... ill do this later
- if (applyRating) {
- if (note.isHoldPiece && note.endMs > note.msTime) {
- var prevHitTime:Float;
- if (!note.held && note.hitTime <= note.msTime + Scoring.holdLeniencyMS)
- prevHitTime = note.msTime;
- else
- prevHitTime = Math.max(note.hitTime, note.msTime);
-
- perfectRelease = (released && songPos >= note.endMs - Scoring.holdLeniencyMS);
- var nextHitTime:Float;
- if (perfectRelease)
- nextHitTime = note.endMs;
- else
- nextHitTime = Math.min(songPos, note.endMs);
- if (!note.held) trace('started hitting ${Math.round(note.msTime)} -> ${Math.round(prevHitTime)} / ${Math.round(note.endMs)}');
- if (released) trace('released ${Math.round(nextHitTime)} / ${Math.round(note.endMs)} (last : ${Math.round(prevHitTime)})');
-
- final secondDiff:Float = Math.max(0, (nextHitTime - prevHitTime) * .001);
- final scoreGain:Float = game.scoring.holdScorePerSecond * secondDiff;
- scoring ??= {score: scoreGain, healthMod: secondDiff};
- note.hitTime = nextHitTime;
+
+ var prevHitTime:Float;
+ if (!note.held && note.holdTime <= note.msTime + scoreHandler.system.holdLeniencyMS) {
+ prevHitTime = note.msTime;
+ } else {
+ prevHitTime = Math.max(note.holdTime, note.msTime);
}
- if (scoring != null) {
- game.health += scoring.healthMod * note.healthGainPerSecond;
- game.score += scoring.score;
- game.updateRating();
+
+ var nextHitTime:Float;
+ if (perfectRelease) {
+ nextHitTime = note.endMs;
+ } else {
+ nextHitTime = Math.max(Math.min(songPos, note.endMs), prevHitTime);
}
- } else {
- note.hitTime = songPos;
- } */
- if (released && note.isHoldTail) {
- if (lane.held && (lane.heldNote == null || lane.heldNote == note.parent)) {
- lane.heldNote = null;
+
+ holdDelta = Math.max(0, nextHitTime - prevHitTime);
+
+ final secondDiff:Float = holdDelta * .001;
+ score ??= {score: 0, healthMod: secondDiff};
+
+ if (scoreHandler != null)
+ score.score = scoreHandler.system.holdScorePerSecond * secondDiff;
+
+ if (inGame && applyRating && applyHealth)
+ game.health += (score.healthMod ?? 1) * note.healthGainPerSecond;
+
+ applyScore(scoreHandler, score, game);
+
+ if (!released)
+ note.held = true;
+ note.holdTime = nextHitTime;
+ }
+
+ if (playAnimation && targetCharacter != null) {
+ var suffixAnim:String = '$singAnimation$animSuffix${targetCharacter.animSuffix}';
+ if (targetCharacter.currentAnimation == suffixAnim || targetCharacter.animationIsLooping(suffixAnim))
+ targetCharacter.timeAnimSteps();
+ }
+
+ if (released && note.isHoldNote) {
+ note.consumed = true;
+
+ if (lane.heldNote == note) {
lane.held = false;
- if (!lane.cpu && animateReceptor)
- lane.receptor.playAnimation('press', true);
+ lane.heldNote = null;
+ if (animateReceptor)
+ lane.receptor.playAnimation(lane.cpu ? 'static' : 'press');
}
+
if (perfectRelease) {
- if (doSpark)
- spark = lane.spark();
+ if (popCover) spark = lane.spark(note, doSpark);
if (playSound)
FunkinSound.playOnce(Paths.sound('gameplay/hitsounds/hitsoundTail'), .7);
} else {
+ if (popCover) spark = lane.spark(note, false);
if (playSound)
FunkinSound.playOnce(Paths.sound('gameplay/hitsounds/hitsoundFail'), .7);
}
+
+ if (lane.cpu && targetCharacter != null)
+ targetCharacter.held = false;
+
+ note.held = false;
+ lane.killNote(note);
}
- note.held = true;
case GHOST:
if (animateReceptor)
lane.receptor.playAnimation('press', true);
@@ -176,72 +225,91 @@ import funkin.objects.play.Strumline;
}
if (playAnimation && targetCharacter != null) {
targetCharacter.specialAnim = false;
- targetCharacter.playAnimationSteps('sing${game.singAnimations[lane.noteData]}miss', true);
+ targetCharacter.playAnimationSteps('${singAnimation}miss', true);
}
-
+
applyExtraWindow(15);
if (applyRating) {
- game.score -= 10;
- game.health -= .01;
- game.updateScoreText();
+ score ??= scoreHandler?.judgeNoteGhost();
+
+ if (inGame && applyHealth)
+ game.health += (score.healthMod ?? -.01);
}
+
+ applyScore(scoreHandler, score, game);
case LOST:
- if (game.genericVocals != null)
+ note.multAlpha *= .3;
+
+ if (inGame && game.genericVocals != null)
game.genericVocals.volume = 0;
+
if (targetCharacter != null) {
targetCharacter.volume = 0;
- if (playAnimation) {
- targetCharacter.specialAnim = false;
- targetCharacter.playAnimationSteps('sing${game.singAnimations[note.noteData]}miss', true);
- }
+ if (playAnimation)
+ targetCharacter.playAnimationSteps('${singAnimation}miss', true);
}
if (playSound)
FunkinSound.playOnce(Paths.sound('gameplay/hitsounds/miss${FlxG.random.int(1, 3)}'), FlxG.random.float(0.5, 0.6));
if (applyRating) {
- scoring ??= scoreHandler?.judgeNoteMiss(note);
- var rating:FunkinSprite = game.popRating('sadmiss');
- rating.velocity.y = -FlxG.random.int(80, 95);
- rating.velocity.x = FlxG.random.int(-6, 6);
- rating.acceleration.y = 240;
-
- if (scoreHandler != null) {
- game.totalNotes ++;
- scoreHandler.combo = 0;
- scoreHandler.misses ++;
- scoreHandler.score += scoring.score;
- scoreHandler.addMod(scoring.accuracyMod);
- }
+ score ??= scoreHandler?.judgeNoteMiss(note);
- game.health -= note.healthLoss * scoring.healthMod;
+ if (inGame) {
+ if (popRating) {
+ var rating:FunkinSprite = game.popRating('gameplay/funkin/sadmiss');
+ rating.velocity.y = -FlxG.random.int(80, 95);
+ rating.velocity.x = FlxG.random.int(-6, 6);
+ rating.acceleration.y = 240;
+ }
+
+ if (applyHealth)
+ game.health -= note.healthLoss * (score.healthMod ?? 1);
+ }
- game.updateScoreText();
+ applyScore(scoreHandler, score, game);
}
default:
}
}
- function applyExtraWindow(window:Float) {
- @:privateAccess {
- var extraWin:Float = Math.min(lane.extraWindow + window, 200);
- if (strumline != null) {
- for (lane in strumline.lanes)
- lane.extraWindow = extraWin;
- } else {
+ inline function applyScore(handler:ScoreHandler, score:Score, playState:PlayState) {
+ if (handler == null || score == null || !applyRating) return;
+
+ if (playState != null) {
+ playState.totalNotes += score.hits + score.misses;
+ playState.totalHits += score.hits;
+ }
+
+ handler.applyScore(score);
+ playState?.updateScoreText();
+ }
+ inline function applyExtraWindow(window:Float) {
+ var extraWin:Float = Math.min(lane.extraWindow + window, 200);
+ if (strumline != null) {
+ for (lane in strumline.lanes)
lane.extraWindow = extraWin;
- }
+ } else {
+ lane.extraWindow = extraWin;
}
}
+
+ function get_scoring():Score {
+ return score;
+ }
+ function set_scoring(now:Score):Score {
+ return score = now;
+ }
}
-enum NoteEventType {
- SPAWNED;
- DESPAWNED;
+enum abstract NoteEventType(String) to String {
+ var SPAWNED = 'spawned';
+ var DESPAWNED = 'despawned';
- HIT;
- HELD;
- RELEASED;
+ var HIT = 'hit';
+ var HELD = 'held';
+ var PRESSED = 'pressed';
+ var RELEASED = 'released';
- LOST;
- GHOST;
+ var LOST = 'lost';
+ var GHOST = 'ghost';
}
\ No newline at end of file
diff --git a/source/funkin/backend/play/NoteStyle.hx b/source/funkin/backend/play/NoteStyle.hx
new file mode 100644
index 0000000..d3c151a
--- /dev/null
+++ b/source/funkin/backend/play/NoteStyle.hx
@@ -0,0 +1,278 @@
+package funkin.backend.play;
+
+import funkin.objects.play.Lane;
+
+using Lambda;
+
+typedef NoteStyleAsset = flixel.util.typeLimit.OneOfTwo;
+
+class NoteStyle {
+ public static var defaultColors:Array = [FlxColor.RED, FlxColor.LIME, FlxColor.BLUE];
+ public static var defaultDirection:String = 'left';
+
+ public static var cache:Map = [];
+
+ public var path:String;
+ public var name:String;
+ public var author:String;
+ public var info:NoteStyleInfo;
+ public var data:NoteStyleData;
+ public var assets:Map = [];
+ public var modColors:Map>> = [];
+ public var colors:Array> = [];
+
+ public var _success(default, null):Bool = false;
+
+ public static function wipe():Void {
+ cache.clear();
+ }
+ public static function exists(path:String):Bool {
+ return (Paths.exists('data/styles/notes/$path.json') || cache.exists(path));
+ }
+ public static function fetch(?asset:NoteStyleAsset):NoteStyle {
+ if (asset == null)
+ return null;
+ if (Std.isOfType(asset, NoteStyle))
+ return asset;
+
+ var path:String = cast asset;
+ if (cache.exists(path))
+ return cache[path];
+
+ var style:NoteStyle = new NoteStyle(path);
+ if (style._success) {
+ cache[path] = style;
+ return style;
+ }
+
+ return null;
+ }
+ public static function getPath(?style:NoteStyleAsset):String {
+ if (Std.isOfType(style, NoteStyle)) {
+ return cast(style, NoteStyle).path;
+ } else if (Std.isOfType(style, String)) {
+ return cast(style, String);
+ } else {
+ return '';
+ }
+ }
+
+ public function new(path:String) {
+ this.path = path;
+ loadStyle(path);
+ }
+ function loadStyle(path:String):Bool {
+ Log.minor('loading notestyle $path');
+
+ var styleContent:Null = Paths.text('data/styles/notes/$path.json');
+ if (styleContent == null) {
+ Log.warning('notestyle $path not found...');
+ Log.minor('verify path:');
+ Log.minor('- data/styles/notes/$path.json');
+ return false;
+ }
+
+ try {
+ var styleInfo:NoteStyleInfo = TJSON.parse(styleContent);
+ info = styleInfo;
+ data = info.data;
+
+ name = info.name ?? 'Unknown';
+ author = info.author ?? 'Unknown';
+
+ updateInfo();
+ _success = true;
+ Log.info('notestyle $path loaded successfully!');
+ return true;
+ } catch (e:haxe.Exception) {
+ Log.error('error loading notestyle @ "$path" -> ${e.details()}');
+ }
+
+ _success = false;
+ return false;
+ }
+ function updateInfo():Void {
+ this.colors.resize(0);
+
+ for (direction in data.general.directions) {
+ // parse colors
+ if (direction.defaultColors != null) {
+ var colors:Array = [];
+ for (color in direction.defaultColors) {
+ colors.push(FlxColor.fromString(color));
+ }
+ this.colors.push(colors);
+ } else {
+ this.colors.push(null);
+ }
+ }
+ this.modColors[NORMAL] = generateModColors(colors, NORMAL);
+ this.modColors[LOWCONTRAST] = generateModColors(colors, LOWCONTRAST);
+ this.modColors[HIGHCONTRAST] = generateModColors(colors, HIGHCONTRAST);
+ }
+
+ static function generateModColors(colors:Array>, mod:NoteStyleColorMod):Array> {
+ var newColors:Array> = [];
+ for (colorSet in colors) {
+ newColors.push(switch (mod) {
+ case NORMAL:
+ colorSet.copy();
+ case LOWCONTRAST:
+ var grayRim:FlxColor = colorSet[1];
+ grayRim.saturation *= .5;
+ [Receptor.makeGrayColor(colorSet[0]), grayRim, 0xff201e31];
+ case HIGHCONTRAST:
+ var highRim:FlxColor = colorSet[1];
+ highRim.saturation *= 1.5;
+ highRim.brightness *= 1.5;
+ var highColors:Array = NoteSplash.makeSplashColors(colorSet[0]);
+ [highColors[0], highRim, highColors[1]];
+ });
+ }
+
+ return newColors;
+ }
+
+ public function getAssetAnimation(asset:NoteStyleAssetData, find:String):NoteStyleAnimData {
+ if (asset == null) return null;
+
+ return asset.animations.find((anim:NoteStyleAnimData) -> anim.name == find);
+ }
+ public function getDirection(dir:Int):NoteStyleDirData {
+ var dirs:Array = data.general.directions;
+ return dirs[FlxMath.wrap(dir, 0, dirs.length - 1)];
+ }
+
+ public static function getDirectionName(style:NoteStyleAsset, dir:Int):String {
+ var style:NoteStyle = fetch(style);
+
+ return style?.getDirection(dir).name ?? defaultDirection;
+ }
+ public static function getDirectionSing(style:NoteStyleAsset, dir:Int):String {
+ var style:NoteStyle = fetch(style);
+
+ var dir:NoteStyleDirData = style?.getDirection(dir);
+ var anim:Null = dir?.sing;
+ if (anim == null)
+ anim = 'sing${dir?.name?.toUpperCase() ?? defaultDirection.toUpperCase()}';
+ return anim;
+ }
+ public static function getDirectionColors(style:NoteStyleAsset, dir:Int):Array {
+ var style:NoteStyle = fetch(style);
+ if (style == null || style.colors.length == 0) return defaultColors;
+
+ return style.colors[FlxMath.wrap(dir, 0, style.colors.length - 1)] ?? defaultColors;
+ }
+ public static function getDirectionColorMod(style:NoteStyleAsset, dir:Int, mod:NoteStyleColorMod = NORMAL):Array {
+ var style:NoteStyle = fetch(style);
+ if (style == null || style.colors.length == 0) return defaultColors;
+
+ var colorMod:Array> = style.modColors[mod];
+
+ if (colorMod == null) return defaultColors;
+ return colorMod[FlxMath.wrap(dir, 0, colorMod.length - 1)];
+ }
+
+ public function toString():String {
+ return 'NoteStyle($name by $author)';
+ }
+}
+
+class NoteStyleUtil {
+ public static function getDirectionName(style:NoteStyle, dir:Int):String { return NoteStyle.getDirectionName(style, dir); }
+ public static function getDirectionSing(style:NoteStyle, dir:Int):String { return NoteStyle.getDirectionSing(style, dir); }
+ public static function getDirectionColors(style:NoteStyle, dir:Int):Array { return NoteStyle.getDirectionColors(style, dir); }
+ public static function getDirectionColorMod(style:NoteStyle, dir:Int, mod:NoteStyleColorMod = NORMAL):Array { return NoteStyle.getDirectionColorMod(style, dir, mod); }
+
+ public static function loadNoteStyleAnimations(sprite:FunkinSprite, asset:NoteStyleAssetData, direction:String = 'down'):Void {
+ if (asset == null) return;
+
+ sprite.resetData();
+ sprite.loadAtlas(asset.assetPath);
+ sprite.animation?.destroyAnimations();
+ sprite.smooth = asset.antialiasing ?? true;
+
+ if (sprite.frames == null) return;
+ for (data in asset.animations) {
+ var animName:String = direction;
+ if (data.suffix != null) animName += ' ${data.suffix}';
+ if (data.prefix != null) animName = '${data.prefix} $animName';
+
+ if (sprite.hasAnimationPrefix(animName)) {
+ sprite.addAnimation(data.name, animName, data.frameRate, data.looped, data.frameIndices, data.assetPath);
+ sprite.preloadAnimAsset(data.name);
+ } else {
+ animName = data.prefix;
+ if (data.suffix != null) animName += ' ${data.suffix}';
+
+ sprite.addAnimation(data.name, animName, data.frameRate, data.looped, data.frameIndices, data.assetPath);
+ sprite.preloadAnimAsset(data.name);
+ }
+
+ if (data.offsets != null && sprite.animationExists(data.name)) {
+ sprite.setAnimationOffset(data.name, data.offsets[0], data.offsets[1]);
+ } else {
+ sprite.setAnimationOffset(data.name);
+ }
+ }
+ }
+}
+
+typedef NoteStyleInfo = {
+ var name:String;
+ var ?author:String;
+ var ?version:String;
+ var data:NoteStyleData;
+}
+
+typedef NoteStyleData = {
+ var general:NoteStyleGeneral;
+ var notes:NoteStyleAssetData;
+ var holds:NoteStyleAssetData;
+ var receptors:NoteStyleAssetData;
+ var ?noteCovers:NoteStyleAssetData;
+ var ?noteSplashes:NoteStyleAssetData;
+}
+
+typedef NoteStyleGeneral = {
+ var ?disableRGB:Bool;
+ var ?laneSpacing:Float;
+ var directions:Array;
+}
+
+typedef NoteStyleDirData = {
+ var name:String;
+ var ?sing:String;
+ var ?colorSave:String;
+ var ?keybindSave:String;
+ var ?defaultColors:Array;
+}
+
+typedef NoteStyleAssetData = {
+ var assetPath:String;
+ var animations:Array;
+ var ?antialiasing:Bool;
+ var ?variants:Int; // notesplash only (for now)
+ var ?scale:Float;
+ var ?alpha:Float;
+}
+
+typedef NoteStyleAnimData = {
+ var name:String;
+ var ?prefix:String;
+ var ?suffix:String;
+ var ?disableRGB:Bool;
+ var ?colorMod:NoteStyleColorMod;
+ var ?frameRateRange:Array;
+ var ?frameIndices:Array;
+ var ?offsets:Array;
+ var ?assetPath:String;
+ var ?frameRate:Int;
+ var ?looped:Bool;
+}
+
+enum abstract NoteStyleColorMod(String) to String {
+ var NORMAL = 'normal';
+ var LOWCONTRAST = 'lowContrast';
+ var HIGHCONTRAST = 'highContrast';
+}
\ No newline at end of file
diff --git a/source/funkin/backend/play/ScoreHandler.hx b/source/funkin/backend/play/ScoreHandler.hx
index c433881..663b09e 100644
--- a/source/funkin/backend/play/ScoreHandler.hx
+++ b/source/funkin/backend/play/ScoreHandler.hx
@@ -1,14 +1,13 @@
package funkin.backend.play;
-import funkin.backend.play.Scoring;
+import funkin.backend.play.ScoreSystem;
import flixel.util.FlxSignal.FlxTypedSignal;
-using Lambda;
-
class ScoreHandler {
public var score:Float = 0;
public var accuracyMod:Float = 0;
public var accuracyDiv:Float = 0;
+ public var hits(default, set):Int = 0;
public var combo(default, set):Int = 0;
public var misses(default, set):Int = 0;
@:isVar public var accuracy(get, never):Float = 0;
@@ -16,60 +15,63 @@ class ScoreHandler {
public var onMissesChange:FlxTypedSignal Void> = new FlxTypedSignal();
public var onComboChange:FlxTypedSignal Void> = new FlxTypedSignal();
+ public var onHit:FlxTypedSignal Void> = new FlxTypedSignal();
- public var hitWindows:Array = [];
- public var holdScorePerSecond:Float;
- public var system:ScoringSystem;
+ public var system:ScoreSystem;
- public function new(system:ScoringSystem = LEGACY) {
- this.system = system;
- this.hitWindows = switch (system) {
- case EMI:
- holdScorePerSecond = 250;
- Scoring.emiDefault();
- case PBOT1:
- holdScorePerSecond = 250;
- Scoring.pbotDefault();
- default:
- holdScorePerSecond = 0;
- Scoring.legacyDefault();
- }
+ public function new(?system:ScoreSystem) {
+ this.system = (system ?? new ScoreSystem());
}
public function reset() {
score = accuracyMod = accuracyDiv = combo = misses = 0;
ratingCount.clear();
}
- public function judgeNoteHit(note:funkin.objects.play.Note, time:Float):Score {
- return switch (system) {
- case EMI | WEEK7 | LEGACY:
- var score:Score = Scoring.judgeLegacy(hitWindows, note.hitWindow, time);
- // todo : fun stuff!
- score;
- case PBOT1:
- var score:Score = Scoring.judgePBOT1(hitWindows, note.hitWindow, time);
- score;
+ public function applyScore(score:Score) {
+ this.hits += (score.hits ?? 0);
+ this.score += (score.score ?? 0);
+ this.misses += (score.misses ?? 0);
+
+ if (score.rating != null)
+ countRating(score.rating);
+ if (score.accuracyMod != null)
+ addMod(score.accuracyMod);
+ if (score.breaksCombo != null && score.breaksCombo) {
+ combo = 0;
+ } else {
+ combo += score.hits;
}
}
+
+ public function judgeNoteHit(note:funkin.objects.play.Note, time:Float):Score {
+ return system.judgeHit(time, note.hitWindow);
+ }
public function judgeNoteMiss(note:funkin.objects.play.Note):Score {
- return switch (system) {
- case EMI:
- {score: -50};
- default:
- {score: -10};
- }
+ return system.judgeMiss(note);
+ }
+ public function judgeNoteGhost():Score {
+ return system.judgeGhost();
+ }
+ public function getHitWindow(rating:String) {
+ return system.hitFromName(rating);
}
- public function getHitWindow(rating:String)
- return hitWindows.find((win:HitWindow) -> win.rating == rating);
- public function getRatingCount(rating:String)
+ public function getRatingCount(rating:String) {
return ratingCount.get(rating) ?? 0;
- public function countRating(rating:String, mod:Int = 1)
+ }
+ public function countRating(rating:String, mod:Int = 1) {
ratingCount.set(rating, getRatingCount(rating) + mod);
+ }
public function addMod(mod:Float = 0, div:Float = 1) {
accuracyMod += mod;
accuracyDiv += div;
}
+ function set_hits(newHits:Int):Int {
+ if (newHits == hits)
+ return newHits;
+ onHit.dispatch(newHits);
+ return hits = newHits;
+ }
function set_combo(newCombo:Int):Int {
if (newCombo == combo)
return newCombo;
diff --git a/source/funkin/backend/play/ScoreSystem.hx b/source/funkin/backend/play/ScoreSystem.hx
new file mode 100644
index 0000000..5620953
--- /dev/null
+++ b/source/funkin/backend/play/ScoreSystem.hx
@@ -0,0 +1,269 @@
+package funkin.backend.play;
+
+import funkin.objects.play.Note;
+
+// modern scoring system (PBOT1)
+class ModernScoreSystem extends ScoreSystem {
+ public var scoringOffset:Float = 54.99;
+ public var scoringSlope:Float = .080;
+
+ public var maxScore:Float = 500;
+ public var minScore:Float = 9;
+
+ public var perfectThreshold:Float = 5;
+ public var missThreshold:Float = 160;
+
+ public function new() {
+ super();
+ name = 'PBOT1';
+ useMilliseconds = true;
+ }
+
+ public override function makeHitWindows():Array {
+ var hitWindows:Array = [
+ new HitWindow('killer', 0, 12.5, 1, 2 / 1.5),
+ new HitWindow('sick', 0, 45, 1, 1),
+ new HitWindow('good', 0, 90, .8, .75 / 1.5),
+ new HitWindow('bad', 0, 135, .5, 0),
+ new HitWindow('shit', 0, 160, .2, -1 / 1.5),
+ new HitWindow('shit', 0, 160, 0, -2) // HORRIBLE (key mashing)
+ ]; // score is 0 because its calculated in judging !
+ hitWindows[0].splash = hitWindows[1].splash = true;
+ hitWindows[3].breaksCombo = hitWindows[4].breaksCombo = hitWindows[5].breaksCombo = true;
+
+ return hitWindows;
+ }
+
+ public override function judgeHit(time:Float, hitWindow:Float):Score {
+ var hit:HitWindow = hitFromTime(time, hitWindow);
+
+ var score:Float;
+ var accuracyMod:Float;
+ var absTime:Float = Math.abs(time);
+
+ if (absTime <= perfectThreshold) {
+ score = maxScore;
+ accuracyMod = 1;
+ } else if (absTime >= missThreshold) {
+ score = minScore;
+ accuracyMod = 0;
+ } else {
+ var factor:Float = (1 - (1 / (1 + Math.exp(-scoringSlope * (absTime - scoringOffset)))));
+ score = Math.max(Math.floor(maxScore * factor + minScore), 0);
+ accuracyMod = (score / maxScore);
+ }
+
+ return {
+ hits: 1,
+ hitWindow: hit,
+ rating: hit.rating,
+ healthMod: hit.healthMod,
+ breaksCombo: hit.breaksCombo,
+
+ accuracyMod: accuracyMod,
+ score: score
+ };
+ }
+
+ public override function judgeMiss(note:Note):Score {
+ var miss:Score = super.judgeMiss(note);
+ miss.score = -100;
+ return miss;
+ }
+}
+
+// emi scoring system (fx3)
+class EmiScoreSystem extends ScoreSystem {
+ public function new() {
+ super();
+ name = 'Emi';
+ }
+
+ public override function makeHitWindows():Array {
+ var hitWindows:Array = [
+ new HitWindow('killer', 500, .06, 1, 1),
+ new HitWindow('sick', 350, .3, 1, .75),
+ new HitWindow('good', 200, .6, .8, .25),
+ new HitWindow('bad', 100, .9, .5, -.25),
+ new HitWindow('shit', 50, 1, .2, -.5),
+ new HitWindow('shit', -50, 1, 0, -2) // HORRIBLE (key mashing)
+ ];
+ hitWindows[0].splash = hitWindows[1].splash = true;
+ hitWindows[3].breaksCombo = hitWindows[4].breaksCombo = hitWindows[5].breaksCombo = true;
+
+ return hitWindows;
+ }
+
+ public override function judgeMiss(note:Note):Score {
+ var miss:Score = super.judgeMiss(note);
+ miss.score = -50;
+ return miss;
+ }
+}
+
+// legacy scoring system
+class LegacyScoreSystem extends ScoreSystem {
+ public function new() {
+ super();
+ name = 'Legacy';
+ holdScoring = false;
+ }
+
+ public override function makeHitWindows():Array {
+ var hitWindows:Array = [
+ new HitWindow('sick', 350, .2, 1),
+ new HitWindow('good', 200, .75, 1),
+ new HitWindow('bad', 100, .9, 1),
+ new HitWindow('shit', 50, 1, 1)
+ ];
+ hitWindows[0].splash = true;
+
+ return hitWindows;
+ }
+}
+
+// custom scoring system (scripting purposes)
+class CustomScoreSystem extends ScoreSystem {
+ public var customJudgeHit:Float -> Float -> Score = null;
+ public var customJudgeMiss:Note -> Score = null;
+ public var customJudgeGhost:Void -> Score = null;
+
+ public function new(name:String = 'Custom', ?customHitWindows:Array) {
+ super();
+ this.name = name;
+ this.hitWindows = (customHitWindows ?? hitWindows);
+ }
+
+ public override function judgeHit(time:Float, range:Float):Score {
+ if (customJudgeHit != null)
+ return customJudgeHit(time, range);
+ return super.judgeHit(time, range);
+ }
+
+ public override function judgeMiss(note:Note):Score {
+ if (customJudgeMiss != null)
+ return customJudgeMiss(note);
+ return super.judgeMiss(note);
+ }
+
+ public override function judgeGhost():Score {
+ if (customJudgeGhost != null)
+ return customJudgeGhost();
+ return super.judgeGhost();
+ }
+}
+
+// default scoring system
+class ScoreSystem {
+ public static var safeFrames:Float = 10;
+
+ public var name:String = 'Default';
+
+ public var holdScoring:Bool = true;
+ public var holdLeniencyMS:Float = 75;
+ public var holdScorePerSecond:Float = 250;
+ public var useMilliseconds:Bool = false;
+
+ public var hitWindows:Array;
+
+ public function new() {
+ hitWindows = makeHitWindows();
+ }
+
+ public function makeHitWindows():Array {
+ var hitWindows:Array = [
+ new HitWindow('sick', 350, .2, 1),
+ new HitWindow('good', 200, .75, .8),
+ new HitWindow('bad', 100, .9, .5),
+ new HitWindow('shit', 50, 1, .2),
+ new HitWindow('shit', 0, 1, 0) // HORRIBLE (key mashing)
+ ];
+ hitWindows[0].splash = true;
+
+ return hitWindows;
+ }
+
+ public function hitFromName(name:String):HitWindow {
+ return Lambda.find(hitWindows, (window:HitWindow) -> window.rating == name);
+ }
+
+ public function hitFromTime(time:Float, range:Float):HitWindow {
+ if (useMilliseconds) range = 1;
+
+ for (window in hitWindows) {
+ if (Math.abs(time) <= window.threshold * range)
+ return window;
+ }
+
+ return hitWindows[hitWindows.length - 1];
+ }
+
+ public function judgeHit(time:Float, range:Float):Score {
+ var hit:HitWindow = hitFromTime(time, range);
+
+ return {
+ hits: 1,
+ hitWindow: hit,
+ rating: hit.rating,
+ healthMod: hit.healthMod,
+ accuracyMod: hit.accuracyMod,
+ breaksCombo: hit.breaksCombo,
+ score: hit.score
+ };
+ }
+
+ public function judgeMiss(note:Note):Score {
+ return {
+ score: -10,
+ misses: 1,
+ accuracyMod: 0,
+ breaksCombo: true
+ }
+ }
+
+ public function judgeGhost():Score {
+ return {
+ score: -10,
+ healthMod: -.01
+ }
+ }
+
+ public function toString():String {
+ return 'ScoreSystem($name)';
+ }
+}
+
+class HitWindow {
+ public var count:Int;
+ public var score:Float;
+ public var rating:String;
+ public var threshold:Float;
+ public var healthMod:Float;
+ public var accuracyMod:Float;
+ public var splash:Bool = false;
+ public var breaksCombo:Bool = false;
+
+ public function new(rating:String, score:Float, threshold:Float, ratingMod:Float, healthMod:Float = 1) {
+ this.count = 0;
+ this.score = score;
+ this.rating = rating;
+ this.threshold = threshold;
+ this.healthMod = healthMod;
+ this.accuracyMod = ratingMod;
+ }
+
+ public function toString():String {
+ return 'HitWindow($rating | ${Math.round(accuracyMod * 10000) / 100}%)';
+ }
+}
+
+typedef Score = {
+ var ?rating:String;
+ var ?hitWindow:HitWindow;
+ var ?accuracyMod:Float;
+ var ?breaksCombo:Bool;
+ var ?healthMod:Float;
+ var ?score:Float;
+ var ?misses:Int;
+ var ?hits:Int;
+}
\ No newline at end of file
diff --git a/source/funkin/backend/play/Scoring.hx b/source/funkin/backend/play/Scoring.hx
deleted file mode 100644
index 39bd501..0000000
--- a/source/funkin/backend/play/Scoring.hx
+++ /dev/null
@@ -1,114 +0,0 @@
-package funkin.backend.play;
-
-class Scoring {
- public static var safeFrames:Float = 10;
- public static var holdLeniencyMS:Float = 75;
-
- public static function legacyDefault() {
- var windows:Array = [
- new HitWindow('sick', 350, .2, 1),
- new HitWindow('good', 200, .75, .8),
- new HitWindow('bad', 100, .9, .5),
- new HitWindow('shit', 50, 1, .2),
- new HitWindow('shit', 50, 1.1, 0) // HORRIBLE (key mashing)
- ];
- windows[0].splash = true;
-
- return windows;
- }
- public static function emiDefault() {
- var windows:Array = legacyDefault();
- windows[2].breaksCombo = true;
- windows[3].breaksCombo = true;
- windows[4].breaksCombo = true;
-
- windows[0].threshold = .3;
- windows[1].threshold = .6;
-
- windows[1].healthMod = .75;
- windows[2].healthMod = .25;
- windows[3].healthMod = -.5;
- windows[4].healthMod = -2;
-
- var killer:HitWindow = new HitWindow('killer', 500, .06, 1);
- windows.unshift(killer);
- killer.splash = true;
-
- return windows;
- }
- public static function pbotDefault() {
- final thresholdMS:Float = 160;
- var windows:Array = [
- new HitWindow('killer', 0, 12.5 / thresholdMS, 1, 2 / 1.5),
- new HitWindow('sick', 0, 45 / thresholdMS, 1, 1),
- new HitWindow('good', 0, 90 / thresholdMS, .8, .75 / 1.5),
- new HitWindow('bad', 0, 135 / thresholdMS, .5, 0),
- new HitWindow('shit', 0, 1, .2, -1 / 1.5),
- new HitWindow('shit', 0, 1.1, 0, -2)
- ];
- windows[0].splash = true;
- windows[1].splash = true;
-
- windows[3].breaksCombo = true;
- windows[4].breaksCombo = true;
- windows[5].breaksCombo = true;
-
- return windows;
- }
-
- public static function judgeLegacy(hitWindows:Array, hitWindow:Float, time:Float):Score {
- var win:HitWindow = hitWindows[hitWindows.length - 1];
- for (window in hitWindows) {
- if (Math.abs(time) <= window.threshold * hitWindow) {
- win = window;
- break;
- }
- }
-
- return {hitWindow: win, rating: win.rating, healthMod: win.healthMod, accuracyMod: win.accuracyMod, score: win.score};
- }
- public static function judgePBOT1(hitWindows:Array, hitWindow:Float, time:Float):Score {
- var win:HitWindow = hitWindows[hitWindows.length - 1];
- for (window in hitWindows) {
- if (Math.abs(time) <= window.threshold * hitWindow) {
- win = window;
- break;
- }
- }
-
- final scoringOffset:Float = 54.99; // probably move these to Scoring
- final scoringSlope:Float = .080;
- final maxScore:Float = 500;
- final minScore:Float = 9;
-
- var score:Float;
- var accuracyMod:Float;
- var absTime:Float = Math.abs(time);
- if (absTime / hitWindow <= 5 / 160) {
- score = maxScore;
- accuracyMod = 1;
- } else {
- var factor:Float = 1 - (1 / (1 + Math.exp(-scoringSlope * (absTime - scoringOffset))));
- score = Math.floor(maxScore * factor + minScore);
- accuracyMod = score / maxScore;
- }
-
- return {hitWindow: win, rating: win.rating, healthMod: win.healthMod, accuracyMod: accuracyMod, score: score};
- }
-}
-
-@:structInit class Score {
- public var hitWindow:HitWindow = null;
- public var accuracyMod:Float = 0;
- public var healthMod:Float = 1;
- public var rating:String = '';
- public var score:Float = 0;
-}
-
-enum abstract ScoringSystem(String) to String {
- var LEGACY = 'legacy'; // rating
- var WEEK7 = 'week7';
- var EMI = 'emis';
-
- var PBOT1 = 'pbot1'; // timing
-}
\ No newline at end of file
diff --git a/source/funkin/backend/play/SongEvent.hx b/source/funkin/backend/play/SongEvent.hx
new file mode 100644
index 0000000..beaef11
--- /dev/null
+++ b/source/funkin/backend/play/SongEvent.hx
@@ -0,0 +1,278 @@
+package funkin.backend.play;
+
+import funkin.states.PlayState;
+import funkin.states.GameOverSubState;
+import funkin.objects.Character;
+import funkin.objects.CharacterGroup;
+import funkin.backend.scripting.HScript;
+
+@:structInit class SongEvent implements IPlayEvent {
+ public var type(default, null):SongEventType;
+ public var cancelled:Bool = false;
+
+ public var time:Null = null;
+ public var sprite:FlxSprite = null;
+ public var character:Character = null;
+ public var chartEvent:Chart.ChartEvent = null;
+
+ public var countdown:Null = null;
+ public var countdownSprite:FunkinSprite = null;
+
+ public var subState:FunkinState = null;
+
+ public function cancel() cancelled = true;
+ public function dispatch() { // hahaaa
+ if (!Std.isOfType(FlxG.state, PlayState)) {
+ throw(new haxe.Exception('song event can\'t be dispatched outside of PlayState!!'));
+ return;
+ }
+ var game:PlayState = cast FlxG.state;
+
+ if (cancelled) {
+ switch (type) {
+ case START_COUNTDOWN:
+ game.conductorInUse.paused = true;
+
+ case SONG_START:
+ game.conductorInUse.songPosition = 0;
+ game.conductorInUse.paused = true;
+
+ default:
+ }
+ return;
+ }
+
+ switch (type) {
+ case START_COUNTDOWN:
+ if (game.fadeNotes) {
+ for (strumline in game.strumlineGroup)
+ strumline.fadeIn();
+ }
+ case TICK_COUNTDOWN:
+ var folder:String = 'funkin';
+ FunkinSound.playOnce(Paths.sound('gameplay/countdown/$folder/intro$countdown'));
+
+ countdownSprite = game.popCountdown('gameplay/funkin/$countdown');
+
+ case SONG_START:
+ game.music.play(true, -game.audioOffset);
+ game.syncMusic(true, true);
+ game.songStarted = true;
+ case SONG_FINISH:
+ if (HScript.stopped(game.hscripts.run('finishSong'))) {
+ game.conductorInUse.paused = true;
+ } else {
+ FlxG.switchState(() -> new funkin.states.FreeplayState());
+ }
+
+ case STEP_HIT:
+ game.stepHitEvent(time);
+ case BEAT_HIT:
+ game.beatHitEvent(time);
+ case BAR_HIT:
+ game.barHitEvent(time);
+
+ case DEATH_FIRST:
+ if (Std.isOfType(subState, GameOverSubState))
+ cast(subState, GameOverSubState).firstDeathEvent();
+ case DEATH_START:
+ if (Std.isOfType(subState, GameOverSubState))
+ cast(subState, GameOverSubState).startDeathEvent();
+ case DEATH_CONFIRM:
+ if (Std.isOfType(subState, GameOverSubState))
+ cast(subState, GameOverSubState).confirmDeathEvent();
+
+ case PUSH_EVENT:
+ PlayStateEventHandler.pushEvent(chartEvent, game);
+ case TRIGGER_EVENT:
+ PlayStateEventHandler.triggerEvent(chartEvent, game);
+
+ default:
+ }
+ }
+}
+
+enum abstract SongEventType(String) to String {
+ var SONG_START = 'songStart';
+ var SONG_FINISH = 'songFinish';
+
+ var PUSH_EVENT = 'pushEvent';
+ var TRIGGER_EVENT = 'triggerEvent';
+ var CHANGE_SPOTLIGHT = 'changeSpotlight';
+
+ var START_COUNTDOWN = 'startCountdown';
+ var TICK_COUNTDOWN = 'tickCountdown';
+
+ var DEATH_INIT = 'deathInit';
+ var DEATH_FIRST = 'deathFirst';
+ var DEATH_START = 'deathStart';
+ var DEATH_CONFIRM = 'deathConfirm';
+
+ var STEP_HIT = 'stepHit';
+ var BEAT_HIT = 'beatHit';
+ var BAR_HIT = 'barHit';
+}
+
+class PlayStateEventHandler {
+ public static function pushEvent(chartEvent:Chart.ChartEvent, game:PlayState) {
+ var params:Map = chartEvent.params;
+ var simple:Bool = game.simple;
+
+ switch (chartEvent.name) {
+ case 'PlayAnimation':
+ if (simple) return;
+
+ var focusChara:Null = null;
+ switch (params['target']) {
+ case 'girlfriend', 'gf': focusChara = game.player3;
+ case 'boyfriend', 'bf': focusChara = game.player1;
+ case 'dad': focusChara = game.player2;
+ }
+
+ if (focusChara != null)
+ focusChara.preloadAnimAsset(params['anim']);
+ }
+
+ game.events.push(chartEvent);
+ game.hscripts.run('eventPushed', [chartEvent]);
+ }
+
+ public static function triggerEvent(chartEvent:Chart.ChartEvent, game:PlayState) {
+ var params:Map = chartEvent.params;
+ var simple:Bool = game.simple;
+
+ switch (chartEvent.name) {
+ case 'FocusCamera':
+ if (simple) return;
+
+ var focusCharaInt:Int;
+ var focusChara:Null = null;
+ if (params.exists('char')) {
+ focusCharaInt = Util.parseInt(params['char']);
+ } else {
+ focusCharaInt = Util.parseInt(params['value']);
+ }
+
+ switch (focusCharaInt) {
+ case 0: // player focus
+ focusChara = game.player1;
+ case 1: // opponent focus
+ focusChara = game.player2;
+ case 2: // gf focus
+ focusChara = game.player3;
+ }
+
+ if (game.camLocked) { // change "spotlight", NOT camera
+ game.spotlight = focusChara?.current;
+ return;
+ }
+
+ if (focusChara != null) {
+ game.focusOnCharacter(focusChara?.current);
+ } else {
+ game.camFocusTarget.x = 0;
+ game.camFocusTarget.y = 0;
+ game.spotlight = null;
+ }
+ if (params.exists('x')) game.camFocusTarget.x += Util.parseFloat(params['x']);
+ if (params.exists('y')) game.camFocusTarget.y += Util.parseFloat(params['y']);
+
+ FlxTween.cancelTweensOf(game.camGame.scroll);
+ switch (params['ease']) {
+ case 'CLASSIC' | null:
+ game.camGame.pauseFollowLerp = false;
+ case 'INSTANT':
+ game.camGame.snapToTarget();
+ game.camGame.pauseFollowLerp = false;
+ default:
+ var duration:Float = Util.parseFloat(params['duration'], 4) * game.conductorInUse.stepCrochet * .001;
+ if (duration <= 0) {
+ game.camGame.snapToTarget();
+ game.camGame.pauseFollowLerp = false;
+ } else {
+ var easeFunction:Null Float> = Reflect.field(FlxEase, params['ease'] ?? 'linear');
+ if (easeFunction == null) {
+ Log.warning('FocusCamera event: ease function invalid');
+ easeFunction = FlxEase.linear;
+ }
+ game.camGame.pauseFollowLerp = true;
+ FlxTween.tween(game.camGame.scroll, {x: game.camFocusTarget.x - FlxG.width * .5, y: game.camFocusTarget.y - FlxG.height * .5}, duration, {ease: easeFunction, onComplete: (_) -> {
+ game.camGame.pauseFollowLerp = false;
+ }});
+ }
+ }
+
+ case 'ZoomCamera':
+ if (simple) return;
+
+ var targetZoom:Float = Util.parseFloat(params['zoom'], 1);
+ var direct:Bool = (params['mode'] ?? 'direct' == 'direct');
+ targetZoom *= (direct ? FlxCamera.defaultZoom : (game.stage?.zoom ?? 1));
+ game.camGame.zoomTarget = targetZoom;
+ FlxTween.cancelTweensOf(game.camGame, ['zoom']);
+ switch (params['ease']) {
+ case 'INSTANT':
+ game.camGame.zoom = targetZoom;
+ game.camGame.pauseZoomLerp = false;
+ default:
+ var duration:Float = Util.parseFloat(params['duration'], 4) * game.conductorInUse.stepCrochet * .001;
+ if (duration <= 0) {
+ game.camGame.zoom = targetZoom;
+ game.camGame.pauseZoomLerp = false;
+ } else {
+ var easeFunction:Null Float> = Reflect.field(FlxEase, params['ease'] ?? 'linear');
+ if (easeFunction == null) {
+ Log.warning('FocusCamera event: ease function invalid');
+ easeFunction = FlxEase.linear;
+ }
+ game.camGame.pauseZoomLerp = true;
+ FlxTween.tween(game.camGame, {zoom: targetZoom}, duration, {ease: easeFunction, onComplete: (_) -> {
+ game.camGame.pauseZoomLerp = false;
+ }});
+ }
+ }
+
+ case 'SetCameraBop':
+ var targetRate:Int = Util.parseInt(params['rate'], -1);
+ var targetIntensity:Float = Util.parseFloat(params['intensity'], 1);
+
+ game.hudZoomIntensity = targetIntensity * 2;
+ game.camZoomIntensity = targetIntensity;
+ game.camZoomRate = targetRate;
+
+ case 'PlayAnimation':
+ if (simple) return;
+
+ var anim:String = params['anim'];
+ var target:String = params['target'];
+ var focus:FlxSprite = null;
+
+ switch (target) {
+ case 'dad' | 'opponent': focus = game.player2;
+ case 'girlfriend' | 'gf': focus = game.player3;
+ case 'boyfriend' | 'bf' | 'player': focus = game.player1;
+ default: focus = game.stage?.getProp(target);
+ }
+
+ if (focus != null) {
+ var forced:Bool = params['force'];
+
+ if (Std.isOfType(focus, CharacterGroup)) {
+ var chara:CharacterGroup = cast focus;
+ if (chara.animationExists(anim)) {
+ if (forced) {
+ chara.playAnimationSpecial(anim, true);
+ } else {
+ chara.playAnimation(anim, true);
+ }
+ chara.timeAnimSteps();
+ }
+ } else if (Std.isOfType(focus, FunkinSprite)) {
+ var funk:FunkinSprite = cast focus;
+ funk.playAnimation(anim, forced);
+ }
+ }
+ }
+ game.hscripts.run('eventTriggered', [chartEvent]);
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/backend/rhythm/Conductor.hx b/source/funkin/backend/rhythm/Conductor.hx
index 0a0da26..235585b 100644
--- a/source/funkin/backend/rhythm/Conductor.hx
+++ b/source/funkin/backend/rhythm/Conductor.hx
@@ -13,6 +13,7 @@ class Conductor {
public var stepCrochet(get, never):Float;
public var timeSignature(get, never):TimeSignature;
@:isVar public var songPosition(get, set):Float = 0;
+ @:isVar public var tempoChanges(get, set):Array;
@:isVar public var step(get, set):Float;
@:isVar public var beat(get, set):Float;
@@ -23,37 +24,54 @@ class Conductor {
public var barHit:FlxTypedSignal Void> = new FlxTypedSignal();
public var beatHit:FlxTypedSignal Void> = new FlxTypedSignal();
public var stepHit:FlxTypedSignal Void> = new FlxTypedSignal();
+ public var advance:FlxTypedSignal Void> = new FlxTypedSignal();
public var metronome:Metronome;
public var syncTracker:FlxSound;
+ public var audioOffset:Float = 0;
public var maxDisparity:Float = 33.34;
public static var global(default, never):Conductor = new Conductor();
+ var prevBar:Int;
+ var prevBeat:Int;
+ var prevStep:Int;
+ var prevPosition:Float;
+
public function new(?metronome:Metronome) {
this.metronome = metronome ?? new Metronome();
}
public function update(elapsedMS:Float) {
if (paused) return;
- var prevStep:Int = Math.floor(metronome.step);
- var prevBeat:Int = Math.floor(metronome.beat);
- var prevBar:Int = Math.floor(metronome.bar);
+ setPosition(songPosition + Math.min(elapsedMS, 250) * timeScale);
+ }
+
+ public inline function setPosition(position:Float):Void {
+ prevPosition = metronome.ms;
+ prevStep = Math.floor(metronome.step);
+ prevBeat = Math.floor(metronome.beat);
+ prevBar = Math.floor(metronome.bar);
- songPosition += Math.min(elapsedMS, 250) * timeScale;
- if (syncTracker != null) {
- timeScale = syncTracker.pitch;
- if (syncTracker.playing && Math.abs(songPosition - syncTracker.time) > maxDisparity * timeScale)
- songPosition = syncTracker.time;
- }
+ songPosition = position;
+ sync();
if (dispatchEvents) {
var curBar:Int = Math.floor(metronome.bar);
var curBeat:Int = Math.floor(metronome.beat);
var curStep:Int = Math.floor(metronome.step);
- if (prevBar != curBar) barHit.dispatch(curBar);
- if (prevBeat != curBeat) beatHit.dispatch(curBeat);
+ if (prevPosition != metronome.ms) advance.dispatch(metronome.ms);
if (prevStep != curStep) stepHit.dispatch(curStep);
+ if (prevBeat != curBeat) beatHit.dispatch(curBeat);
+ if (prevBar != curBar) barHit.dispatch(curBar);
+ }
+ }
+ public inline function sync():Void {
+ if (syncTracker != null) {
+ timeScale = syncTracker.pitch;
+ var offsetTime:Float = syncTracker.time + audioOffset;
+ if (syncTracker.playing && Math.abs(metronome.ms - offsetTime) > maxDisparity * timeScale)
+ songPosition = offsetTime;
}
}
@@ -63,6 +81,8 @@ class Conductor {
public function get_crochet():Float { return metronome.getCrochet(metronome.bpm, metronome.timeSignature.denominator); }
public function get_stepCrochet():Float { return (crochet * .25); }
+ public function get_tempoChanges():Array { return metronome.tempoChanges; }
+ public function set_tempoChanges(newArray:Array):Array { return metronome.tempoChanges = newArray; }
public function get_songPosition():Float { return metronome.ms; }
public function set_songPosition(newMS:Float):Float { return metronome.setMS(newMS); }
public function get_timeSignature():TimeSignature { return metronome.timeSignature; }
@@ -77,7 +97,14 @@ class Conductor {
public function set_bar(newBar:Float):Float { return metronome.setBar(newBar); }
// public function set_ms(newMS:Float):Float { return metronome.setMS(newMS); }
- public function resetToDefault() {
+ public function resetToDefault():Void {
metronome = new Metronome();
}
+
+ public function copyTempoChanges(tempoChanges:Array):Array {
+ return metronome.copyTempoChanges(tempoChanges);
+ }
+ public function sortTempoChanges():Void {
+ metronome.sortTempoChanges();
+ }
}
\ No newline at end of file
diff --git a/source/funkin/backend/rhythm/Event.hx b/source/funkin/backend/rhythm/Event.hx
index 757d079..c357b15 100644
--- a/source/funkin/backend/rhythm/Event.hx
+++ b/source/funkin/backend/rhythm/Event.hx
@@ -4,14 +4,14 @@ interface ITimeSortable {
public var msTime:Float;
}
interface ITimedEvent extends ITimeSortable {
- public var func:T -> Void;
+ public var func:#if hl Dynamic #else T #end -> Void;
}
class Event implements ITimedEvent {
public var msTime:Float;
- public var func:Event -> Void;
+ public var func:#if hl Dynamic #else Event #end -> Void;
- public function new(msTime:Float, ?func:Event -> Void) {
+ public function new(msTime:Float, ?func:#if hl Dynamic #else Event #end -> Void) {
this.msTime = msTime;
this.func = func;
}
diff --git a/source/funkin/backend/rhythm/Metronome.hx b/source/funkin/backend/rhythm/Metronome.hx
index f0293ee..bc6d89c 100644
--- a/source/funkin/backend/rhythm/Metronome.hx
+++ b/source/funkin/backend/rhythm/Metronome.hx
@@ -165,6 +165,18 @@ class Metronome {
bpm = prevBPM;
return target;
}
+
+ public function copyTempoChanges(copyChanges:Array):Array {
+ tempoChanges.resize(0);
+ for (change in copyChanges) {
+ var newChange:TempoChange = new TempoChange(change.beatTime, change.bpm, change.timeSignature?.clone());
+ tempoChanges.push(newChange);
+ }
+ return tempoChanges;
+ }
+ public function sortTempoChanges():Void {
+ tempoChanges.sort((a:TempoChange, b:TempoChange) -> Std.int(a.beatTime) - Std.int(b.beatTime));
+ }
}
enum abstract Measure(String) to String {
diff --git a/source/funkin/backend/rhythm/TempoChange.hx b/source/funkin/backend/rhythm/TempoChange.hx
index d2d7a34..7df1d2c 100644
--- a/source/funkin/backend/rhythm/TempoChange.hx
+++ b/source/funkin/backend/rhythm/TempoChange.hx
@@ -24,6 +24,14 @@ class TempoChange {
return (bpm != null);
public function get_changeSign()
return (timeSignature != null);
+
+ public function toString():String {
+ var str:String = '(beatTime: $beatTime';
+ if (changeBPM) str += ' | bpm: $bpm';
+ if (changeSign) str += ' | timeSignature: $timeSignature';
+
+ return '$str)';
+ }
}
class TimeSignature { //should this be a class?
@@ -51,6 +59,9 @@ class TimeSignature { //should this be a class?
denominator = sign.denominator;
return this;
}
+ public function clone():TimeSignature {
+ return new TimeSignature(numerator, denominator);
+ }
public function toString():String {
return '$numerator/$denominator';
}
diff --git a/source/funkin/backend/scripting/HScript.hx b/source/funkin/backend/scripting/HScript.hx
index b9eee97..afc83c3 100644
--- a/source/funkin/backend/scripting/HScript.hx
+++ b/source/funkin/backend/scripting/HScript.hx
@@ -1,12 +1,14 @@
package funkin.backend.scripting;
-#if ALLOW_SCRIPTS // TODO: make the game actually compile without the define
+#if ALLOW_SCRIPTS // TODO: make the game actually compile without this define
import funkin.backend.FunkinRuntimeShader;
import funkin.backend.scripting.HScriptClasses;
+import haxe.PosInfos;
import crowplexus.iris.Iris;
import crowplexus.iris.IrisConfig;
import crowplexus.iris.ErrorSeverity;
+import crowplexus.hscript.*;
import crowplexus.hscript.Printer;
import crowplexus.hscript.Expr.Error as IrisError;
@@ -16,14 +18,15 @@ enum HScriptFunctionEnum {
STOP;
STOPALL;
}
-class HScript extends Iris {
+class HScript extends FlxBasic {
public static var staticVariables:Map = [];
public static var STOP(default, never):HScriptFunctionEnum = HScriptFunctionEnum.STOP;
public static var STOPALL(default, never):HScriptFunctionEnum = HScriptFunctionEnum.STOPALL;
@:noReflection public static var defaultVariables:Map = [
- #if hl
- 'Math' => HScriptMath,
- #end
+ 'Std' => Std,
+ 'Math' => #if hl HScriptMath #else Math #end,
+ 'StringTools' => StringTools,
+
'Main' => Main,
'Type' => Type,
'Reflect' => Reflect,
@@ -40,8 +43,10 @@ class HScript extends Iris {
'FlxSpriteGroup' => FlxSpriteGroup,
'ShaderFilter' => openfl.filters.ShaderFilter,
+ 'FunkinText' => funkin.backend.FunkinText,
'FunkinSound' => funkin.backend.FunkinSound,
'FunkinSprite' => funkin.backend.FunkinSprite,
+ 'FunkinCamera' => funkin.backend.FunkinCamera,
'FunkinAnimate' => funkin.backend.FunkinAnimate,
'FunkinSpriteGroup' => funkin.backend.FunkinSpriteGroup,
@@ -55,34 +60,40 @@ class HScript extends Iris {
'Character' => funkin.objects.Character,
'HealthIcon' => funkin.objects.HealthIcon,
'NoteEvent' => funkin.backend.play.NoteEvent,
+ 'NoteStyle' => funkin.backend.play.NoteStyle,
'Strumline' => funkin.objects.play.Strumline,
'StageProp' => funkin.objects.Stage.StageProp,
'Conductor' => funkin.backend.rhythm.Conductor,
'Metronome' => funkin.backend.rhythm.Metronome,
'CharacterGroup' => funkin.objects.CharacterGroup,
- 'Measure' => funkin.backend.rhythm.Metronome.Measure,
- 'NoteEventType' => funkin.backend.play.NoteEvent.NoteEventType,
- 'SpriteRenderType' => funkin.backend.FunkinSprite.SpriteRenderType,
+ 'NoteEventType' => {SPAWNED: 'spawned', DESPAWNED: 'despawned', HIT: 'hit', HELD: 'held', RELEASED: 'released', LOST: 'lost', GHOST: 'ghost'},
+ // THIS WILL BE DEPRECATED
'STOP' => STOP,
'STOPALL' => STOPALL,
'FlxAxes' => HScriptFlxAxes,
'FlxColor' => HScriptFlxColor,
'BlendMode' => HScriptBlendMode,
- 'RuntimeShader' => HScriptRuntimeShader
+ 'RuntimeShader' => HScriptRuntimeShader,
+
+ 'experimentalVars' => true
];
+ var expr:Expr;
+ var parser:ModParser;
+ var interp:ModInterp;
+ var executed:Bool = false;
+ public var failed:Bool = false;
+ public var compiled:Bool = false;
+
public var interceptArray:Array = null;
public var defaultVars:Map = null;
public var scriptString(default, set):String = '';
public var scriptPath:Null = null;
+ public var packageName:String = '';
public var scriptName:String = '';
- public var compiled:Bool = false;
- public var active:Bool = true;
- var executed:Bool = false;
- var modInterp:ModInterp;
public static function init() {
Iris.logLevel = customLog;
@@ -91,10 +102,13 @@ class HScript extends Iris {
return (result == STOP || result == STOPALL);
}
public function new(name:String, code:String, ?interceptArray:Array, ?defaultVars:Map) {
- super('', new IrisConfig(name, false, false, []));
+ super();
+
+ parser = new ModParser();
+ interp = new ModInterp();
+ interp.hscript = this;
- interp = modInterp = new ModInterp();
- modInterp.hscript = this;
+ parser.allowTypes = parser.allowJSON = parser.allowMetadata = true;
preset();
this.interceptArray = interceptArray;
@@ -104,62 +118,49 @@ class HScript extends Iris {
this.scriptString = code;
}
- public function errorCaught(e:IrisError, ?extra:String) {
- Log.fatal(Printer.errorToString(e));
- }
- public static function customLog(level:ErrorSeverity, x, ?pos:haxe.PosInfos) {
- if (pos == null) pos = Iris.getDefaultPos();
-
- var out:String = Std.string(x);
- if (pos != null && pos.customParams != null)
- for (i in pos.customParams)
- out += "," + Std.string(i);
-
- var posPrefix:String = pos.fileName;
- if (pos.lineNumber != -1)
- posPrefix += ':${pos.lineNumber}';
-
- switch (level) {
- #if I_AM_BORING_ZZZ
- case FATAL: posPrefix = '[ FATAL:$posPrefix ]';
- case ERROR: posPrefix = '[ ERROR:$posPrefix ]';
- case WARN: posPrefix = '[ WARNING:$posPrefix ]';
- default:
- #else
- case FATAL: posPrefix = Log.colorTag(' FATAL:$posPrefix ', black, brightRed);
- case ERROR: posPrefix = Log.colorTag(' ERROR:$posPrefix ', black, red);
- case WARN: posPrefix = Log.colorTag(' WARNING:$posPrefix ', black, yellow);
- default: posPrefix = Log.colorTag(' $posPrefix ', black, blue);
- #end
- }
- Sys.println('$posPrefix $out');
- }
-
- public function run(?func:String, ?args:Array, safe:Bool = true):Any {
- if (!compiled || !active) return null;
+ public function run(?func:String, ?args:Array, safe:Bool = true, forceRun:Bool = false):Any {
+ if (!compiled || failed || (!active && !forceRun)) return null;
try {
if (func != null) {
if (!executed) execute();
executed = true;
- if (safe && !exists(func)) return null;
+ if (safe && !hasVar(func)) return null;
var result:IrisCall = call(func, args);
return result?.returnValue ?? null;
} else {
return execute();
}
- } catch (e:IrisError) {
+ } catch (e:Dynamic) {
+ if (!executed)
+ failed = true;
+
errorCaught(e);
+
return null;
}
}
- public override function destroy() {
- run('destroy');
+ public override function kill():Void {
+ if (alive)
+ run('kill', true, true);
+
+ super.kill();
+ }
+ public override function revive():Void {
+ if (!alive)
+ run('revive', true, true);
+
+ super.revive();
+ }
+ public override function destroy():Void {
+ if (exists)
+ run('destroy', true, true);
+
+ interp = null;
+ parser = null;
super.destroy();
}
- public override function preset() {
- super.preset();
-
+ public function preset():Void {
for (field => val in defaultVariables)
set(field, val);
@@ -171,31 +172,125 @@ class HScript extends Iris {
}
#if hscriptPos
- set("trace", Reflect.makeVarArgs(function(x:Array) { // fix static trace
- var pos = this.interp != null ? this.interp.posInfos() : Iris.getDefaultPos(this.name);
+ set('trace', Reflect.makeVarArgs(function(x:Array) { // fix static trace
+ @:privateAccess var pos = (interp != null ? this.interp.posInfos() : Iris.getDefaultPos(scriptName));
+
var v = x.shift();
- if (x.length > 0)
- pos.customParams = x;
- var str:String = Std.string(v);
- Iris.print(str, pos);
+ if (x.length > 0) pos.customParams = x;
+
+ Iris.print(Std.string(v), pos);
}));
#end
}
+ public function parse(string:String, force:Bool = false) {
+ if (force || expr == null)
+ expr = parser.parseString(string, scriptName);
+ return expr;
+ }
+ public function execute():Dynamic {
+ packageName = parser.packageName;
+ return interp.execute(expr);
+ }
+ public function set(name:String, value:Dynamic, allowOverride:Bool = true):Void {
+ if (allowOverride || !hasVar(name))
+ setVar(name, value);
+ }
+ public function call(fun:String, ?args:Array):IrisCall {
+ var ny:Dynamic = getVar(fun); // function signature
+ var isFunction:Bool = false;
+
+ try {
+ isFunction = (ny != null && Reflect.isFunction(ny));
+ if (!isFunction) throw 'Tried to call a non-function, for "$fun"';
+
+ final ret = Reflect.callMethod(null, ny, args ?? []);
+ return {funName: fun, signature: ny, returnValue: ret};
+ }
+
+ #if hscriptPos
+ catch (e:Expr.Error) {
+ Iris.error(Printer.errorToString(e, false), this.interp.posInfos());
+ }
+ #end
+ catch (e:haxe.Exception) {
+ @:privateAccess var pos = (isFunction ? this.interp.posInfos() : Iris.getDefaultPos(scriptName));
+ Iris.error(
+ Std.string(e)
+ #if IRIS_DEBUG + "\n" + CallStack.toString(CallStack.exceptionStack(true)) #end,
+ pos
+ );
+ }
+
+ return null;
+ }
+
+ public override function setVar(name:String, value:Dynamic):Dynamic {
+ interp.variables.set(name, value);
+ return value;
+ }
+ public override function getVar(name:String):Dynamic {
+ return interp.variables.get(name);
+ }
+ public override function removeVar(name:String):Void {
+ interp.variables.remove(name);
+ }
+ public override function hasVar(name:String):Bool {
+ return interp.variables.exists(name);
+ }
+
function set_scriptString(newCode:String):String {
if (newCode == scriptString) return scriptString;
- scriptCode = newCode;
+ failed = false;
try {
- parse(true);
- compiled = true;
+ parse(newCode, true);
executed = false;
+ compiled = true;
} catch (e:IrisError) {
compiled = false;
errorCaught(e);
}
+
return scriptString = newCode;
}
+
+ function errorCaught(e:Dynamic):Void {
+ if (Std.isOfType(e, IrisError)) {
+ var pos:PosInfos = cast {fileName: e.origin, lineNumber: e.line};
+ Iris.fatal(Printer.errorToString(e, false), pos);
+ } else {
+ var pos:PosInfos = @:privateAccess { cast interp.posInfos(); }
+ Iris.fatal(Std.string(e), pos);
+ }
+ }
+ public static function customLog(level:ErrorSeverity, x, ?pos:haxe.PosInfos) {
+ @:privateAccess if (pos == null) pos = Iris.getDefaultPos();
+
+ var out:String = Std.string(x);
+ if (pos != null && pos.customParams != null)
+ for (i in pos.customParams)
+ out += ',$i';
+
+ var posPrefix:String = pos.fileName;
+ if (pos.lineNumber != -1)
+ posPrefix += ':${pos.lineNumber}';
+
+ switch (level) {
+ #if I_AM_BORING_ZZZ
+ case FATAL: posPrefix = '[ FATAL:$posPrefix ]';
+ case ERROR: posPrefix = '[ ERROR:$posPrefix ]';
+ case WARN: posPrefix = '[ WARNING:$posPrefix ]';
+ default:
+ #else
+ case FATAL: posPrefix = Log.colorTag(' FATAL:$posPrefix ', black, brightRed);
+ case ERROR: posPrefix = Log.colorTag(' ERROR:$posPrefix ', black, red);
+ case WARN: posPrefix = Log.colorTag(' WARNING:$posPrefix ', black, yellow);
+ default: posPrefix = Log.colorTag(' $posPrefix ', black, blue);
+ #end
+ }
+ Sys.println('$posPrefix $out');
+ }
}
#else
class HScript {}
diff --git a/source/funkin/backend/scripting/HScriptClasses.hx b/source/funkin/backend/scripting/HScriptClasses.hx
index 7809b13..c81742a 100644
--- a/source/funkin/backend/scripting/HScriptClasses.hx
+++ b/source/funkin/backend/scripting/HScriptClasses.hx
@@ -44,14 +44,14 @@ class HScriptFlxColor { // i hate it in here
public static var TRANSPARENT(default, never):Int = cast FlxColor.TRANSPARENT;
public var color:FlxColor;
- public var alpha(default, set):Int;
- public var alphaFloat(default, set):Float;
@:isVar public var red(get, set):Int;
@:isVar public var green(get, set):Int;
@:isVar public var blue(get, set):Int;
+ @:isVar public var alpha(get, set):Int;
@:isVar public var redFloat(get, set):Float;
@:isVar public var greenFloat(get, set):Float;
@:isVar public var blueFloat(get, set):Float;
+ @:isVar public var alphaFloat(get, set):Float;
@:isVar public var hue(get, set):Float;
@:isVar public var cyan(get, set):Float;
@:isVar public var magenta(get, set):Float;
@@ -65,7 +65,9 @@ class HScriptFlxColor { // i hate it in here
this.color = cast (color, FlxColor);
} // this is so horrible i could kill myself
function set_alpha(newAlpha:Int) return alpha = color.alpha = newAlpha;
+ function get_alpha() return color.alpha;
function set_alphaFloat(newAlpha:Float) return alphaFloat = color.alphaFloat = newAlpha;
+ function get_alphaFloat() return color.alphaFloat;
function get_red() return color.red;
function set_red(newRed:Int) return red = color.red = newRed;
function get_green() return color.green;
diff --git a/source/funkin/backend/scripting/HScriptGroup.hx b/source/funkin/backend/scripting/HScriptGroup.hx
new file mode 100644
index 0000000..880d48c
--- /dev/null
+++ b/source/funkin/backend/scripting/HScriptGroup.hx
@@ -0,0 +1,195 @@
+package funkin.backend.scripting;
+
+using StringTools;
+using Lambda;
+
+typedef HScriptAsset = flixel.util.typeLimit.OneOfTwo;
+
+class HScriptGroup extends FlxTypedGroup {
+ public var interceptArray:Array;
+ public var defaultVars:Map;
+
+ public function new(?interceptArray:Array, ?defaultVars:Map) {
+ super();
+ this.interceptArray = interceptArray;
+ this.defaultVars = defaultVars;
+ }
+ public function find(test:HScriptAsset):HScript {
+ if (Std.isOfType(test, HScript)) {
+ return (members.contains(test) ? test : null);
+ } else {
+ return members.find((hscript:HScript) -> hscript.scriptName == test);
+ }
+ }
+ public function scriptExists(test:HScriptAsset):Bool {
+ return (find(test) != null);
+ }
+ public function findFromSuffix(test:String):HScript {
+ return members.find((hscript:HScript) -> (hscript.exists && hscript.scriptName.endsWith(test)));
+ }
+ public function destroyScript(?hscript:HScript):Void {
+ if (hscript == null) return;
+ hscript.destroy();
+ remove(hscript);
+ cleanup();
+ }
+ public function destroyScripts():Void {
+ while (members.length > 0)
+ destroyScript(members.shift());
+ }
+ public function set(field:String, value:Any):Void {
+ for (hscript in members) {
+ if (!hscript.exists) continue;
+
+ hscript.set(field, value);
+ }
+ }
+ public function run(?name:String, ?args:Array):Any {
+ var returnLocked:Bool = false;
+ var returnValue:Dynamic = null;
+ for (hscript in members) {
+ if (!hscript.exists) continue;
+
+ var result:Dynamic = hscript.run(name, args, true);
+ switch (result) {
+ case null: // dont change return value to null
+ case HScript.STOPALL:
+ return result;
+ case HScript.STOP:
+ returnLocked = true;
+ returnValue = result;
+ default:
+ if (!returnLocked)
+ returnValue = result;
+ }
+ }
+ return returnValue;
+ }
+ public function concat(group:Dynamic) {
+ if (Std.isOfType(group, HScriptGroup)) {
+ for (script in cast(group, HScriptGroup).members)
+ add(script);
+ } else if (Std.isOfType(group, Array)) {
+ var array:Array = cast group;
+ for (script in array)
+ add(script);
+ } else {
+ throw 'Invalid type';
+ }
+ }
+ public override function destroy():Void {
+ destroyScripts();
+ super.destroy();
+ }
+
+ function getScriptName(name:String, unique:Bool = false, warn:Bool = false):String {
+ var found:HScript = find(name);
+ if (found != null && unique) {
+ var n:Int = 1;
+ while (scriptExists('${name}_$n')) n ++;
+ name = '${name}_$n';
+ }
+ return name;
+ }
+ function cleanup():Void {
+ while (true) {
+ var deadScript:HScript = members.find((hscript:HScript) -> !hscript.exists);
+ if (deadScript == null) return;
+ else remove(deadScript, true);
+ }
+ }
+
+ public function loadFromString(code:String, ?name:String):HScript {
+ name ??= 'hscript';
+ if (scriptExists(name)) {
+ Log.warning('hscript @ "$name" is already active!');
+ name = getScriptName(name, true);
+ Log.minor('using name "$name"...');
+ }
+
+ cleanup();
+ var hs:HScript = new HScript(name, code, interceptArray, defaultVars);
+ if (hs.compiled) {
+ Log.info('hscript "$name" loaded successfully!');
+ hs.run('create');
+ add(hs);
+ return hs;
+ } else {
+ hs.destroy();
+ return null;
+ }
+ }
+ public function loadFromFile(file:String, unique:Bool = false, ?newName:String, ?defaultVars:Map):HScript {
+ if (scriptExists(newName ?? file) && !unique) {
+ Log.warning('hscript @ "$file" is already active!');
+ return find(file);
+ }
+
+ var name:String = getScriptName(newName ?? file, unique, true);
+ var code:String;
+ if (FileSystem.exists(file)) {
+ code = File.getContent(file);
+ } else {
+ Log.error('hscript @ "$file" wasn\'t found...');
+ code = '';
+ }
+
+ var defaultestVars:Map = this.defaultVars;
+ if (defaultVars != null) {
+ defaultestVars = defaultVars.copy();
+ for (k => v in this.defaultVars)
+ defaultestVars.set(k, v);
+ }
+
+ cleanup();
+ var hs:HScript = new HScript(name, code, interceptArray, defaultestVars);
+ if (hs.compiled) {
+ Log.info('hscript @ "$file" loaded successfully!');
+ hs.run('create');
+ add(hs);
+ return hs;
+ } else {
+ hs.destroy();
+ return null;
+ }
+ }
+ public function loadFromFolder(path:String, allMods:Bool = false, ?defaultVars:Map):Array {
+ var dirList:Array = [Paths.sharedPath(path), Paths.globalModPath(path)];
+ var loaded:Array = [];
+
+ for (mod in Mods.getLocal(allMods)) {
+ dirList.push(Paths.modPath(path, mod.directory));
+ }
+
+ for (dir in dirList) {
+ if (FileSystem.exists(dir)) {
+ Log.minor('loading hscripts @ "$dir"');
+ for (file in FileSystem.readDirectory(dir)) {
+ if (!file.endsWith('.hx')) continue;
+
+ var script:HScript = loadFromFile('$dir/$file', defaultVars);
+ if (script != null) loaded.push(script);
+ }
+ }
+ }
+
+ return loaded;
+ }
+ public function loadFromPaths(basePath:String, allMods:Bool = false, unique:Bool = false, ?defaultVars:Map):Array {
+ var loaded:Array = [];
+
+ for (path in Paths.getPaths(basePath, true, allMods)) {
+ var scriptFile:String = path.path;
+ if (!scriptFile.endsWith('.hx')) continue;
+ if (!unique && scriptExists(scriptFile)) continue;
+
+ var script:HScript = loadFromFile(scriptFile, unique, defaultVars);
+ if (script != null) loaded.push(script);
+ }
+
+ return loaded;
+ }
+
+ @:deprecated('activeScripts is deprecated, use members instead!') public var activeScripts(get, null):Array;
+ function get_activeScripts():Array { return members; }
+}
\ No newline at end of file
diff --git a/source/funkin/backend/scripting/HScripts.hx b/source/funkin/backend/scripting/HScripts.hx
deleted file mode 100644
index d0d6fb7..0000000
--- a/source/funkin/backend/scripting/HScripts.hx
+++ /dev/null
@@ -1,152 +0,0 @@
-package funkin.backend.scripting;
-
-using StringTools;
-
-typedef HScriptAsset = flixel.util.typeLimit.OneOfTwo;
-class HScripts { // todo: make this a flxtypedgroup?
- public var interceptArray:Array;
- public var defaultVars:Map;
-
- public var activeScripts:Array = [];
-
- public function new(?interceptArray:Array, ?defaultVars:Map) {
- this.interceptArray = interceptArray;
- this.defaultVars = defaultVars;
- }
- public function find(test:HScriptAsset) {
- if (Std.isOfType(test, HScript)) {
- return activeScripts.contains(test) ? test : null;
- } else {
- for (hscript in activeScripts) {
- if (hscript.scriptName == test)
- return hscript;
- }
- return null;
- }
- }
- public function exists(test:HScriptAsset) return (find(test) != null);
- public function findFromSuffix(test:String) {
- for (hscript in activeScripts) {
- if (hscript.scriptName.endsWith(test)) return hscript;
- }
- return null;
- }
- public function add(hscript:HScript):Void {
- if (!activeScripts.contains(hscript)) activeScripts.push(hscript);
- }
- public function destroy(hscript:HScript):Void {
- if (activeScripts.contains(hscript))
- activeScripts.remove(hscript);
- hscript.destroy();
- }
- public function destroyAll():Void {
- while (activeScripts.length > 0)
- destroy(activeScripts[0]);
- }
- public function set(field:String, value:Any):Void {
- for (hscript in activeScripts) {
- hscript.set(field, value);
- }
- }
- public function run(?name:String, ?args:Array):Any {
- var returnLocked:Bool = false;
- var returnValue:Dynamic = null;
- for (hscript in activeScripts) {
- var result:Dynamic = hscript.run(name, args, true);
- switch (result) {
- case null: // dont change return value to null
- case HScript.STOPALL:
- return result;
- case HScript.STOP:
- returnLocked = true;
- returnValue = result;
- default:
- if (!returnLocked)
- returnValue = result;
- }
- }
- return returnValue;
- }
-
- function getScriptName(name:String, unique:Bool = false, warn:Bool = false) {
- var found:HScript = find(name);
- if (found != null && unique) {
- var n:Int = 1;
- while (exists('${name}_$n')) n ++;
- name = '${name}_$n';
- }
- return name;
- }
-
- public function loadFromString(code:String, ?name:String):Null {
- name ??= 'hscript';
- if (exists(name)) {
- Log.warning('hscript @ "$name" is already active!');
- name = getScriptName(name, true);
- Log.minor('using name "$name"...');
- }
-
- var hs:HScript = new HScript(name, code, interceptArray, defaultVars);
- if (hs.compiled) {
- Log.info('hscript "$name" loaded successfully!');
- hs.run('create');
- add(hs);
- return hs;
- } else {
- hs.destroy();
- return null;
- }
- }
- public function loadFromFile(file:String, unique:Bool = false):Null {
- if (exists(file) && !unique) {
- Log.warning('hscript @ "$file" is already active!');
- return find(file);
- }
-
- var name:String = getScriptName(file, unique, true);
- var code:String;
- if (FileSystem.exists(file)) {
- code = File.getContent(file);
- } else {
- Log.error('hscript @ "$file" wasn\'t found...');
- code = '';
- }
-
- var hs:HScript = new HScript(name, code, interceptArray, defaultVars);
- if (hs.compiled) {
- Log.info('hscript @ "$file" loaded successfully!');
- hs.run('create');
- add(hs);
- return hs;
- } else {
- hs.destroy();
- return null;
- }
- }
- public function loadFromFolder(path:String, allMods:Bool = false):Void {
- var dirList:Array = [Paths.sharedPath(path), Paths.globalModPath(path)];
-
- for (mod in Mods.getLocal(allMods)) {
- dirList.push(Paths.modPath(path, mod.directory));
- }
- for (dir in dirList) {
- if (FileSystem.exists(dir)) {
- Log.minor('loading hscripts @ "$dir"');
- for (file in FileSystem.readDirectory(dir)) {
- if (!file.endsWith('.hx')) continue;
- loadFromFile('$dir/$file');
- }
- }
- }
- }
- public function loadFromPaths(basePath:String, allMods:Bool = false, unique:Bool = false):Bool {
- var found:Bool = false;
- for (path in Paths.getPaths(basePath, true, allMods)) {
- var scriptFile:String = path.path;
- if (exists(scriptFile) && !unique) continue;
- loadFromFile(scriptFile, unique);
- found = true;
- }
- return found;
- }
-}
\ No newline at end of file
diff --git a/source/funkin/backend/scripting/ModInterp.hx b/source/funkin/backend/scripting/ModInterp.hx
index 3e70fa7..7e1e4a8 100644
--- a/source/funkin/backend/scripting/ModInterp.hx
+++ b/source/funkin/backend/scripting/ModInterp.hx
@@ -3,8 +3,17 @@ package funkin.backend.scripting;
import crowplexus.iris.Iris;
import crowplexus.hscript.Expr;
import crowplexus.hscript.Tools;
+import crowplexus.hscript.Interp;
-class ModInterp extends crowplexus.hscript.Interp {
+import funkin.backend.FunkinSprite;
+
+enum Exit { // all of this because Stop IS PRIVATW AHHHHHHHHH
+ Continue;
+ Return;
+ Break;
+}
+
+class ModInterp extends Interp {
public var hscript:HScript;
override function setVar(name:String, v:Dynamic) {
@@ -62,6 +71,14 @@ class ModInterp extends crowplexus.hscript.Interp {
if (o == null)
error(EInvalidAccess(f));
+ if (variables.get('experimentalVars') == true) {
+ if (Std.isOfType(o, FlxBasic)) {
+ var basic:FlxBasic = cast o;
+ if (basic.hasVar(f))
+ return basic.getVar(f);
+ }
+ }
+
#if hl
if (Type.typeof(o) == Type.ValueType.TObject && Reflect.hasField(o, '__evalues__')) { // hashlink enums
try {
@@ -75,6 +92,7 @@ class ModInterp extends crowplexus.hscript.Interp {
error(EInvalidAccess(f));
}
#end
+
return Reflect.getProperty(o, f);
}
override function makeIterator(v:Dynamic):Iterator {
@@ -157,8 +175,179 @@ class ModInterp extends crowplexus.hscript.Interp {
switch (eDef) {
case EImport(v, as):
return doImport(v, as);
+ case EBreak:
+ throw Break;
+ case EContinue:
+ throw Continue;
+ case EReturn(e):
+ returnValue = (e == null ? null : expr(e));
+ throw Return;
+ case EFunction(params, fexpr, name, _):
+ var capturedLocals = duplicate(locals);
+ var minParams:Int = 0;
+ var me = this;
+ for (p in params) {
+ if (!p.opt)
+ minParams ++;
+ }
+
+ var f = function(args: Array) {
+ if (args?.length ?? 0 != params.length) {
+ if (args.length < minParams) {
+ var str = "Invalid number of parameters. Got " + args.length + ", required " + minParams;
+ if (name != null)
+ str += " for function '" + name + "'";
+ error(ECustom(str));
+ }
+ // make sure mandatory args are forced
+ var args2 = [];
+ var extraParams = args.length - minParams;
+ var pos = 0;
+ for (p in params) {
+ if (p.opt) {
+ if (extraParams > 0) {
+ args2.push(args[pos++]);
+ extraParams--;
+ } else {
+ args2.push(p.value == null ? null : expr(p.value)); // GENIUS
+ }
+ } else {
+ args2.push(args[pos++]);
+ }
+ }
+ args = args2;
+ }
+
+ var old = me.locals, depth = me.depth;
+ me.depth ++;
+ me.locals = me.duplicate(capturedLocals);
+ for (i in 0...params.length)
+ me.locals.set(params[i].name, {r: args[i], const: false});
+ var r = null;
+ var oldDecl = declared.length;
+ if (inTry)
+ try {
+ r = me.exprReturn(fexpr);
+ } catch (e:Dynamic) {
+ me.locals = old;
+ me.depth = depth;
+ #if neko
+ neko.Lib.rethrow(e);
+ #else
+ throw e;
+ #end
+ }
+ else {
+ r = me.exprReturn(fexpr);
+ }
+ restore(oldDecl);
+ me.locals = old;
+ me.depth = depth;
+ return r;
+ };
+ var f = Reflect.makeVarArgs(f);
+ if (name != null) {
+ if (depth == 0) {
+ // global function
+ variables.set(name, f);
+ } else {
+ // function-in-function is a local function
+ declared.push({n: name, old: locals.get(name)});
+ var ref:LocalVar = {r: f, const: false};
+ locals.set(name, ref);
+ capturedLocals.set(name, ref); // allow self-recursion
+ }
+ }
+ return f;
default:
}
return super.expr(e);
}
+ override function exprReturn(e): Dynamic {
+ try {
+ return expr(e);
+ } catch (e:Exit) {
+ switch (e) {
+ case Break:
+ throw "Invalid break";
+ case Continue:
+ throw "Invalid continue";
+ case Return:
+ var v = returnValue;
+ returnValue = null;
+ return v;
+ }
+ }
+ return null;
+ }
+
+ override function doWhileLoop(eCond, e) {
+ var old: Int = declared.length;
+ do {
+ try {
+ expr(e);
+ } catch (err:Exit) {
+ switch (err) {
+ case Continue:
+ case Break: break;
+ case Return: throw err;
+ }
+ }
+ } while (expr(eCond) == true);
+ restore(old);
+ }
+ override function whileLoop(eCond, e) {
+ var old: Int = declared.length;
+ while (expr(eCond) == true) {
+ try {
+ expr(e);
+ } catch (err:Exit) {
+ switch (err) {
+ case Continue:
+ case Break: break;
+ case Return: throw err;
+ }
+ }
+ }
+ restore(old);
+ }
+ override function forLoop(n, v, itExpr, e): Void {
+ var old: Int = declared.length;
+ declared.push({n: n, old: locals.get(n)});
+ var keyValue: Bool = false;
+ if (v != null) {
+ keyValue = true;
+ declared.push({n: v, old: locals.get(v)});
+ }
+ var it: Dynamic = (keyValue ? makeKVIterator : makeIterator)(expr(itExpr));
+ var _itHasNext: Dynamic = it.hasNext;
+ var _itNext: Dynamic = it.next;
+
+ while (_itHasNext()) {
+ if (keyValue) {
+ var next = _itNext();
+ if (next.key == null || next.value == null) {
+ var nulled: String = (next.key == null ? 'key' : 'value');
+ error(ECustom('${Std.isOfType(next, Int) ? 'Int' : Type.getClassName(Type.getClass(next))} has no field $nulled'));
+ }
+ locals.set(n, {r: next.key, const: false});
+ locals.set(v, {r: next.value, const: false});
+ } else {
+ locals.set(n, {r: _itNext(), const: false});
+ }
+
+ try {
+ expr(e);
+ } catch (err:Exit) {
+ switch (err) {
+ case Continue:
+ case Break:
+ break;
+ case Return:
+ throw err;
+ }
+ }
+ }
+ restore(old);
+ }
}
\ No newline at end of file
diff --git a/source/funkin/backend/scripting/ModParser.hx b/source/funkin/backend/scripting/ModParser.hx
new file mode 100644
index 0000000..0398938
--- /dev/null
+++ b/source/funkin/backend/scripting/ModParser.hx
@@ -0,0 +1,65 @@
+package funkin.backend.scripting;
+
+import crowplexus.hscript.Expr;
+import crowplexus.hscript.Parser;
+
+class ModParser extends Parser {
+ public function new() {
+ super();
+
+ for (proc => value in crowplexus.iris.macro.DefineMacro.defines)
+ preprocesorValues.set(proc, value);
+ }
+
+ override function parseFunctionArgs() {
+ var args:Array = [];
+ var tk = token();
+
+ if (tk != TPClose) {
+ var done = false;
+ while (!done) {
+ var name:String = null, opt:Bool = false;
+
+ switch (tk) {
+ case TQuestion:
+ opt = true;
+ tk = token();
+ default:
+ }
+ switch (tk) {
+ case TId(id):
+ name = id;
+ default:
+ unexpected(tk);
+ break;
+ }
+
+ var arg:Argument = {name: name, opt: opt};
+
+ if (allowTypes) {
+ if (maybe(TDoubleDot))
+ arg.t = parseType();
+ if (maybe(TOp("="))) {
+ arg.value = parseExpr();
+ arg.opt = true;
+ }
+ }
+
+ tk = token();
+ switch (tk) {
+ case TComma:
+ tk = token();
+ case TPClose:
+ done = true;
+ default:
+ unexpected(tk);
+ break;
+ }
+
+ args.push(arg);
+ }
+ }
+
+ return args;
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/debug/DebugDisplay.hx b/source/funkin/debug/DebugDisplay.hx
index bbb86ad..52f62a6 100644
--- a/source/funkin/debug/DebugDisplay.hx
+++ b/source/funkin/debug/DebugDisplay.hx
@@ -1,22 +1,85 @@
package funkin.debug;
-import flixel.FlxG;
+import openfl.geom.Point;
+import openfl.display.Sprite;
+import openfl.display.Bitmap;
import openfl.text.TextField;
import openfl.text.TextFormat;
-import openfl.system.System;
-class DebugDisplay extends TextField {
- public var currentFPS(default, null):Int;
- public var mem:Float;
- public var maxMem:Float;
+import funkin.util.MemoryUtil;
+
+class DebugDisplay extends Sprite {
+ public var perfCounter:PerfCounter;
+ public var watermark:X3Watermark;
+ public var offset:Point;
+
+ public var showWatermark:Bool = false;
+
+ public function new(x:Float = 10, y:Float = 3) {
+ super();
+
+ offset = new Point(x, y);
+ watermark = new X3Watermark(x);
+ perfCounter = new PerfCounter(x, y);
+
+ addChild(watermark);
+ addChild(perfCounter);
+ }
+
+ override function __enterFrame(deltaTime:Float):Void {
+ perfCounter.update(deltaTime);
+
+ final deltaSec:Float = deltaTime * .001;
+ if (showWatermark) {
+ watermark.p = Math.min(watermark.p + deltaSec / watermark.time, 1);
+ } else {
+ watermark.p = Math.max(watermark.p - deltaSec / watermark.time, 0);
+ }
+
+ final winHeight:Int = FlxG.stage.window.height;
+ watermark.y = winHeight - Std.int((watermark.height + offset.y) * watermark.p);
+ watermark.visible = (watermark.y < winHeight);
+ }
+}
+
+class X3Watermark extends Sprite {
+ public var time:Float = 1.25;
+ public var p:Float = 0;
+
+ public function new(x:Float = 0, y:Float = 0) {
+ super();
+
+ this.x = x;
+ this.y = y;
+
+ var icon:Bitmap = new Bitmap(Paths.bmd('x3'));
+
+ var text:TextField = new TextField();
+ text.x = icon.width + 8;
+ text.autoSize = LEFT;
+ text.selectable = false;
+ text.mouseEnabled = false;
+ text.text = '${Main.engineVersion} by emi3';
+ text.defaultTextFormat = new TextFormat('_sans', 12, FlxColor.WHITE);
+
+ addChild(icon);
+ addChild(text);
+ }
+}
+
+class PerfCounter extends TextField {
public var showFPS:Bool = true;
public var showMem:Bool = true;
- public static var byteUnits:Array = ['bytes', 'kb', 'mb', 'gb'];
- @:noCompletion private var currentTime:Float;
- @:noCompletion private var times:Array;
+ var mem:Float;
+ var maxMem:Float;
+ var currentFPS:Int;
+
+ var currentTime:Float;
+ var times:Array;
+ static var byteUnits:Array = ['bytes', 'kb', 'mb', 'gb'];
- public function new(x:Float = 10, y:Float = 10, color:Int = 0xffffff) {
+ public function new(x:Float = 0, y:Float = 0) {
super();
this.x = x;
@@ -29,7 +92,7 @@ class DebugDisplay extends TextField {
mouseEnabled = false;
// filters = [new openfl.filters.GlowFilter(0, 4, 2, 2)];
- var tf:TextFormat = new TextFormat('_sans', 12, color);
+ var tf:TextFormat = new TextFormat('_sans', 12, FlxColor.WHITE);
tf.leading = -4;
defaultTextFormat = tf;
@@ -37,7 +100,7 @@ class DebugDisplay extends TextField {
times = [];
}
- override function __enterFrame(deltaTime:Float):Void {
+ public function update(deltaTime:Float):Void {
var oldFPS:Int = currentFPS;
var oldMem:Float = mem;
@@ -46,12 +109,11 @@ class DebugDisplay extends TextField {
while (times[0] < currentTime - 1000) times.shift();
currentFPS = Math.round((oldFPS + times.length) / 2);
- mem = cast(System.totalMemory, UInt);
+ mem = MemoryUtil.getMemoryUsed();
if (oldFPS != currentFPS || oldMem != mem) {
maxMem = Math.max(mem, maxMem);
- text = (showFPS ? 'FPS: ${Math.min(currentFPS, FlxG.drawFramerate)}' : '') +
- (showMem ? '\nGC MEM: ${DebugDisplay.formatBytes(mem)} / ${DebugDisplay.formatBytes(maxMem)}' : '');
+ text = (showFPS ? 'FPS: ${Math.min(currentFPS, FlxG.drawFramerate)}' : '') + (showMem ? '\nMEM: ${formatBytes(mem)} / ${formatBytes(maxMem)}' : '');
}
}
diff --git a/source/funkin/debug/Log.hx b/source/funkin/debug/Log.hx
index 2e9e1ef..10e6311 100644
--- a/source/funkin/debug/Log.hx
+++ b/source/funkin/debug/Log.hx
@@ -28,7 +28,7 @@ class Log {
public static function error(text:String) return Sys.println(colorTag(' ERROR ', black, red) + ' $text');
public static function fatal(text:String) return Sys.println(colorTag(' FATAL ', black, brightRed) + ' $text');
public static function info(text:String) return Sys.println(colorTag(' INFO ', black, cyan) + ' $text');
- public static function minor(text:String) return Sys.println(colorTag(text, white, none));
+ public static function minor(text:String) return Sys.println(colorTag(text, brightBlack, none));
#end
}
diff --git a/source/funkin/macros/FunkinMacro.hx b/source/funkin/macros/FunkinMacro.hx
index 22ebadd..4c1dd01 100644
--- a/source/funkin/macros/FunkinMacro.hx
+++ b/source/funkin/macros/FunkinMacro.hx
@@ -12,11 +12,12 @@ class FunkinMacro {
var fields:Array = Context.getBuildFields();
fields = fields.concat([{
+ pos: pos,
name: "zIndex",
access: [Access.APublic],
- kind: FieldType.FProp('default', 'set', macro:Int, macro $v{0}),
- pos: pos
+ kind: FieldType.FProp('default', 'set', macro:Int, macro $v{0})
}, {
+ pos: pos,
name: "set_zIndex",
access: [Access.APublic],
kind: FieldType.FFun({
@@ -27,10 +28,90 @@ class FunkinMacro {
ret: macro:Int,
expr: macro { return zIndex = value; }
}),
- pos: pos
+ }, {
+ pos: pos, // extradata stuffs
+ name: 'extraData',
+ access: [APublic],
+ kind: FieldType.FProp('default', 'null', macro:Map, macro $v{[]})
+ }, {
+ pos: pos,
+ name: 'getVar',
+ access: [APublic],
+ kind: FieldType.FFun({
+ ret: macro:Dynamic,
+ args: [{type: macro:String, name: 'id'}],
+ expr: macro { return extraData.get(id); }
+ })
+ }, {
+ pos: pos,
+ name: 'setVar',
+ access: [APublic],
+ kind: FieldType.FFun({
+ ret: macro:Dynamic,
+ args: [{type: macro:String, name: 'id'}, {type: macro:Dynamic, name: 'value'}],
+ expr: macro { extraData.set(id, value); return value; }
+ })
+ }, {
+ pos: pos,
+ name: 'removeVar',
+ access: [APublic],
+ kind: FieldType.FFun({
+ args: [{type: macro:String, name: 'id'}],
+ expr: macro { extraData.remove(id); }
+ })
+ }, {
+ pos: pos,
+ name: 'hasVar',
+ access: [APublic],
+ kind: FieldType.FFun({
+ ret: macro:Bool,
+ args: [{type: macro:String, name: 'id'}],
+ expr: macro { return extraData.exists(id); }
+ })
}]);
return fields;
}
+
+ public static macro function buildReset(isOverride:Bool = false):Array {
+ var pos:Position = Context.currentPos();
+ var cls:ClassType = Context.getLocalClass().get();
+ var fields:Array = Context.getBuildFields();
+
+ var resetExpr:Array = [];
+ var access = [APublic];
+
+ if (isOverride) { // just genius bro
+ access.push(AOverride); // theres prob a better way to do this, but cant really figure it out
+ resetExpr.push(macro { super.resetVars(); });
+ }
+
+ for (field in fields) {
+ if (field.meta == null) continue;
+
+ for (meta in field.meta) {
+ if (meta.name != 'resetVar') continue;
+
+ switch (field.kind) {
+ case FVar(type, expr):
+ resetExpr.push(macro { $i{field.name} = $expr; });
+
+ default: // nothing ...
+ }
+ }
+ }
+
+ fields.push({
+ name: 'resetVars',
+ access: access,
+ pos: pos,
+ kind: FFun({
+ args: [],
+ expr: macro $b{resetExpr}
+ }),
+ });
+
+ return fields;
+ }
}
#end
diff --git a/source/funkin/objects/Alphabet.hx b/source/funkin/objects/Alphabet.hx
index 3b2b5a0..1135dcc 100644
--- a/source/funkin/objects/Alphabet.hx
+++ b/source/funkin/objects/Alphabet.hx
@@ -7,7 +7,7 @@ class Alphabet extends FlxSpriteGroup {
public var text(default, set):String;
public var padding(default, set):Float = -3;
public var letterCase(default, set):LetterCase = NONE;
- public var characters:Array = [];
+ public var characters:FunkinTypedSpriteGroup = new FunkinTypedSpriteGroup();
public var white(default, set):FlxColor = FlxColor.WHITE;
public var black(default, set):FlxColor = FlxColor.BLACK;
@@ -16,6 +16,7 @@ class Alphabet extends FlxSpriteGroup {
super(x, y);
this.type = type;
this.text = text;
+ this.add(characters);
}
public function scaleTo(x:Float = 1, y:Float = 1):Alphabet {
@@ -34,6 +35,12 @@ class Alphabet extends FlxSpriteGroup {
}
}
+ public override function revive():Void {
+ super.revive();
+ for (i => character in characters)
+ if (i >= text.length) character.kill();
+ }
+
public function setColors(white:FlxColor = FlxColor.WHITE, black:FlxColor = FlxColor.BLACK):Alphabet {
for (character in characters)
character.setColors(white, black);
@@ -91,29 +98,23 @@ class Alphabet extends FlxSpriteGroup {
function set_text(newText:String = ''):String {
if (newText == text) return newText;
- while (characters.length > newText.length) {
- var character:AlphabetCharacter = characters.shift();
- remove(character, true);
- character.destroy(); //todo: pool letters?
- }
+ var letters:Array = newText.split('');
- var stringLetters:Array = newText.split('');
- var i:Int = 0;
- for (letter in stringLetters) {
- var character:AlphabetCharacter;
- if (i >= characters.length) {
- character = new AlphabetCharacter(0, 0, letter, type);
- character.setColors(white, black);
- character.scale.copyFrom(scale);
- character.updateHitbox();
- characters.push(character);
- add(character);
- } else {
- character = characters[i];
- character.character = letter;
+ while (characters.length < newText.length) // how economic
+ characters.add(new AlphabetCharacter(0, 0, ' ', type));
+ for (i => character in characters) {
+ if (i >= newText.length) {
+ character.kill();
+ continue;
}
+
character.letterCase = letterCase;
- i ++;
+ character.character = letters[i];
+ character.type = type;
+ character.setColors(white, black);
+ character.scale.copyFrom(scale);
+ character.updateHitbox();
+ character.revive();
}
recalculateLetters();
@@ -306,17 +307,19 @@ class AlphabetCharacter extends FunkinSprite {
}
function set_letterCase(newCase:LetterCase):LetterCase {
+ if (letterCase == newCase) return newCase;
+
letterCase = newCase;
set_character(character);
return letterCase = newCase;
}
function set_type(newType:String):String {
- if (type != newType) {
- offsets.clear();
- loadAtlas('fonts/$newType');
- setupFont();
- set_character(character);
- }
+ if (type == newType) return newType;
+
+ offsets.clear();
+ loadAtlas('fonts/$newType');
+ setupFont();
+ set_character(character);
return type = newType;
}
function set_baseX(newX:Float):Float {
diff --git a/source/funkin/objects/Bar.hx b/source/funkin/objects/Bar.hx
index ac359c1..301872e 100644
--- a/source/funkin/objects/Bar.hx
+++ b/source/funkin/objects/Bar.hx
@@ -9,33 +9,39 @@ class Bar extends FunkinSpriteGroup {
public var leftBar:FunkinSprite;
public var rightBar:FunkinSprite;
- public var targetPercent:Float = 100;
+ public var value:Float = .5;
+ public var targetPercent:Float = 50;
public var percent(default, set):Float;
- public var percentLerp:Float = .15 * 60;
+ public var percentLerp:Null = .15 * 60;
public var valueFunc:Bar -> Float = null;
public var bounds:BarBounds = {min: 0, max: 1};
public var barRect:FlxRect = new FlxRect(4, 4);
- public var barCenter:FlxPoint = new FlxPoint();
+ public var barCenter(get, null):FlxPoint;
public var leftToRight(default, set):Bool = true;
+ public var overlayOnTop(default, set):Bool = false;
- public function new(x:Float = 0, y:Float = 0, valueFunction:Bar -> Float = null, overlayImage:String = 'healthBar') {
+ var _barPoint:FlxPoint = FlxPoint.get();
+
+ public function new(x:Float = 0, y:Float = 0, valueFunction:Bar -> Float = null, overlayImage:String = 'healthBar', ?newBounds:BarBounds) {
super(x, y);
overlay = new FunkinSprite().loadTexture(overlayImage);
leftBar = new FunkinSprite().makeGraphic(Std.int(overlay.width), Std.int(overlay.height), -1);
rightBar = new FunkinSprite().makeGraphic(Std.int(overlay.width), Std.int(overlay.height), -1);
rightBar.clipRect = new FlxRect();
leftBar.clipRect = new FlxRect();
- add(overlay);
- add(leftBar);
- add(rightBar);
valueFunc = valueFunction;
+ if (newBounds != null)
+ bounds = newBounds;
+
+ insertZIndex(leftBar, 5);
+ insertZIndex(rightBar, 10);
- barRect.width = leftBar.width - barRect.x * 2;
barRect.height = leftBar.height - barRect.y * 2;
- percent = updateTargetPercent();
- updateBars();
+ barRect.width = leftBar.width - barRect.x * 2;
+ overlayOnTop = false;
+ snapToPercent();
setColors();
}
public function loadTexture(overlayImage:String = 'healthBar'):Bar {
@@ -59,19 +65,34 @@ class Bar extends FunkinSpriteGroup {
rightBar.color = rightColor;
return this;
}
+ public function snapToPercent():Bar {
+ percent = updateTargetPercent();
+ updateBars();
+ return this;
+ }
public override function update(elapsed:Float) {
super.update(elapsed);
updateTargetPercent();
- if (percentLerp >= 0) {
+ if (percentLerp != null && percentLerp >= 0) {
percent = Util.smoothLerp(percent, targetPercent, percentLerp * elapsed);
} else {
percent = targetPercent;
}
- updateBarCenter();
}
- function set_percent(newPercent:Float) {
+ function get_barCenter():FlxPoint {
+ var result:FlxPoint = _barPoint.set(leftBar.clipRect.x + leftBar.clipRect.width, leftBar.clipRect.y + leftBar.clipRect.height * .5);
+ result.subtract(leftBar.origin);
+ result.scale(leftBar.scale.x, leftBar.scale.y);
+ result.degrees += leftBar.angle;
+ result.add(leftBar.origin);
+ result.subtract(leftBar.offset);
+ result.add(leftBar.x, leftBar.y);
+
+ return result;
+ }
+ function set_percent(newPercent:Float):Float {
if (percent != newPercent) {
percent = newPercent;
updateBars();
@@ -93,37 +114,38 @@ class Bar extends FunkinSpriteGroup {
if (bounds.max <= bounds.min)
return 0;
- var val:Float = valueFunc(this);
- return targetPercent = Util.clamp((val - bounds.min) / bounds.max * 100, 0, 100);
+ value = valueFunc(this);
+ return targetPercent = Util.clamp((value - bounds.min) / (bounds.max - bounds.min) * 100, 0, 100);
} else {
return Util.clamp(targetPercent, 0, 100);
}
}
- function set_leftToRight(isIt:Bool) {
+ function set_leftToRight(isIt:Bool):Bool {
if (leftToRight == isIt) return isIt;
leftToRight = isIt;
updateBars();
return isIt;
}
+ function set_overlayOnTop(yea:Bool):Bool {
+ insertZIndex(overlay, (yea ? 15 : 0));
+ return overlayOnTop = yea;
+ }
public function updateBars() {
var fPercent:Float = (leftToRight ? 100 - percent : percent) * .01;
var leftWidth:Float = FlxMath.lerp(0, barRect.width, fPercent);
+ var yM:Float = (leftBar.frameHeight / overlay.frameHeight);
+ var xM:Float = (leftBar.frameWidth / overlay.frameWidth);
- leftBar.clipRect.x = barRect.x;
- leftBar.clipRect.y = barRect.y;
- leftBar.clipRect.width = leftWidth;
+ leftBar.clipRect.x = barRect.x * xM;
+ leftBar.clipRect.y = barRect.y * yM;
+ leftBar.clipRect.width = leftWidth * xM;
- rightBar.clipRect.y = barRect.y;
- rightBar.clipRect.x = barRect.x + leftWidth;
- rightBar.clipRect.width = barRect.width - leftWidth;
+ rightBar.clipRect.y = barRect.y * yM;
+ rightBar.clipRect.x = (barRect.x + leftWidth) * xM;
+ rightBar.clipRect.width = (barRect.width - leftWidth) * xM;
- rightBar.clipRect.height = leftBar.clipRect.height = barRect.height;
+ rightBar.clipRect.height = leftBar.clipRect.height = barRect.height * yM;
rightBar.clipRect = rightBar.clipRect;
leftBar.clipRect = leftBar.clipRect;
-
- updateBarCenter();
- }
- inline function updateBarCenter() {
- barCenter.set(leftBar.x + leftBar.clipRect.x + leftBar.clipRect.width, leftBar.y + leftBar.height * .5);
}
}
\ No newline at end of file
diff --git a/source/funkin/objects/Character.hx b/source/funkin/objects/Character.hx
index c5d4537..b3c1dfc 100644
--- a/source/funkin/objects/Character.hx
+++ b/source/funkin/objects/Character.hx
@@ -2,20 +2,24 @@ package funkin.objects;
import funkin.objects.HealthIcon;
import funkin.backend.scripting.HScript;
-import funkin.backend.scripting.HScripts;
+import funkin.backend.scripting.HScriptGroup;
using StringTools;
class Character extends FunkinSprite implements ICharacter {
public var bopFrequency:Int = 2;
public var bop(default, set):Bool = true;
+ public var held(default, set):Bool = false;
public var animReset(default, set):Float = 0;
public var singForSteps(default, set):Float = 4;
public var specialAnim(default, set):Bool = false;
+ public var idleAfterAnim(default, set):Bool = true; // like held but for special anim
public var conductorInUse(default, set):Conductor = FunkinState.getCurrentConductor();
public var scaleMultiplier:Float = 1;
public var sway:Bool = false;
+ public var hasDropAnimations(get, never):Bool;
+ public var hasComboAnimations(get, never):Bool;
public var comboNoteCounts(default, null):Array;
public var dropNoteCounts(default, null):Array;
@@ -23,6 +27,7 @@ class Character extends FunkinSprite implements ICharacter {
var binSide:CharacterSide;
var dataSide:CharacterSide;
var characterGroup:CharacterGroup;
+ public var flipSingAnimations:Bool = false;
public var side(default, set):CharacterSide;
public var classicFlip(default, set):Bool = false;
@@ -48,13 +53,17 @@ class Character extends FunkinSprite implements ICharacter {
public var volume(default, set):Float = 1;
public var vocals:FunkinSound;
- public var hscripts:HScripts;
+ public var hscripts:HScriptGroup;
var safeH:Null = null;
public function new(x:Float, y:Float, ?character:String, side:CharacterSide = IDGAF, ?fallback:String, runScripts:Bool = true) {
super(x, y);
- hscripts = new HScripts([this], ['this' => this, 'super' => this]);
+ anim.onFrame.add((number:Int, anim:String) -> characterGroup?.onAnimationFrame.dispatch(number, anim));
+ anim.onComplete.add((anim:String) -> characterGroup?.onAnimationComplete.dispatch(anim));
+ anim.onLoop.add((anim:String) -> characterGroup?.onAnimationLoop.dispatch(anim));
+
+ hscripts = new HScriptGroup([this], ['this' => this, 'super' => this]);
rotateOffsets = true;
vocals = new FunkinSound();
@@ -80,7 +89,7 @@ class Character extends FunkinSprite implements ICharacter {
public function startScripts() {
var scriptPath:String = Paths.getPath('scripts/characters/$loadedCharacter.hx');
if (scriptPath != null)
- hscripts.loadFromFile(scriptPath);
+ hscripts.loadFromFile(scriptPath, '($loadedCharacter) Character Script');
}
public static function getPathSuffix(basePath:String = '', baseSuffix:String = '', chara:String = ''):String {
@@ -125,15 +134,11 @@ class Character extends FunkinSprite implements ICharacter {
}
function set_classicFlip(isIt:Bool) {
- if (classicFlip == isIt)
- return isIt;
classicFlip = isIt;
refreshSide();
return isIt;
}
function set_side(newSide:CharacterSide) {
- if (side == newSide)
- return newSide;
side = newSide;
refreshSide();
return newSide;
@@ -162,9 +167,10 @@ class Character extends FunkinSprite implements ICharacter {
var offset:FlxPoint = offsets[currentAnimation];
setAnimOffset(offset.x, offset.y);
}
+ flipSingAnimations = (!classicFlip && !sideMatches());
}
function flipAnim(anim:String):String {
- if (classicFlip || sideMatches()) return anim;
+ if (!flipSingAnimations) return anim;
if (anim.startsWith('singLEFT')) {
return anim.replace('singLEFT', 'singRIGHT');
@@ -214,18 +220,19 @@ class Character extends FunkinSprite implements ICharacter {
super.update(elapsed);
if (animReset > 0) {
animReset -= elapsed;
- if (animReset <= 0 && !specialAnim) {
+ if (animReset <= 0) {
animReset = 0;
- dance();
+ if (!specialAnim && idleAfterAnim && !held)
+ idle();
}
}
if (specialAnim) {
if (isAnimationFinished() && animReset <= 0) {
- specialAnim = false;
animReset = 0;
- if (singForSteps <= 0) {
- dance();
- }
+ specialAnim = false;
+
+ if (idleAfterAnim && singForSteps <= 0)
+ idle();
}
}
}
@@ -237,17 +244,65 @@ class Character extends FunkinSprite implements ICharacter {
super.draw();
}
+ public override function kill() {
+ hscripts.kill();
+ super.kill();
+ }
+ public override function revive() {
+ hscripts.revive();
+ super.revive();
+ }
public override function destroy() {
- hscripts.destroyAll();
+ hscripts.destroy();
super.destroy();
}
- public function timeAnimSteps(?steps:Float) {
- animReset = (steps ?? singForSteps) * conductorInUse.stepCrochet * .001;
+ public function timeAnimSteps(?steps:Float, max:Bool = true) {
+ // Sys.println('timed animation $currentAnimation steps $steps');
+ var time:Float = (steps ?? singForSteps) * conductorInUse.stepCrochet * .001;
+ if (max) {
+ animReset = Math.max(animReset, time);
+ } else {
+ animReset = time;
+ }
}
public function animationIsLooping(anim:String):Bool {
return (currentAnimation == '$anim-loop' || currentAnimation == '$anim-hold');
}
+ public function playAnim(anim:String, context:PlayAnimContext = SOFT, forced:Bool = false, reversed:Bool = false, frame:Int = 0, ?time:Float) {
+ if (safeH != 'playAnim' && functionOverridden('playAnim')) {
+ safeCall('playAnim', [anim, context, forced, reversed, frame, time]);
+ return;
+ }
+
+ switch (context) {
+ case SOFT:
+ playAnimationSoft(anim, forced, reversed, frame);
+ case SING:
+ playAnimationSteps(anim, forced, time, reversed, frame);
+ case SPECIAL:
+ playAnimationSpecial(anim, forced, time, reversed, frame);
+ default:
+ playAnimation(anim, forced, reversed, frame);
+ }
+ }
+ public function playAnimationSpecial(anim:String, forced:Bool = false, ?steps:Float, reversed:Bool = false, frame:Int = 0) {
+ if (safeH != 'playAnimationSpecial' && functionOverridden('playAnimationSpecial')) {
+ safeCall('playAnimationSpecial', [anim, forced, steps, reversed, frame]);
+ return;
+ }
+
+ if (animationExists(anim)) {
+ var sameAnim:Bool = (currentAnimation == anim);
+ var animWasDone:Bool = isAnimationFinished();
+ playAnimation(anim, forced, reversed, frame);
+
+ if (forced || !sameAnim || animWasDone) {
+ timeAnimSteps(steps);
+ specialAnim = true;
+ }
+ }
+ }
public function playAnimationSoft(anim:String, forced:Bool = false, reversed:Bool = false, frame:Int = 0) {
if (safeH != 'playAnimationSoft' && functionOverridden('playAnimationSoft')) {
safeCall('playAnimationSoft', [anim, forced, reversed, frame]);
@@ -263,9 +318,11 @@ class Character extends FunkinSprite implements ICharacter {
return;
}
- animReset = 0;
- specialAnim = false;
- super.playAnimation(flipAnim(anim) + animSuffix, forced, reversed, frame);
+ if (animationExists(anim)) {
+ animReset = 0;
+ specialAnim = false;
+ super.playAnimation(flipAnim(anim) + animSuffix, forced, reversed, frame);
+ }
}
public function playAnimationSteps(anim:String, forced:Bool = false, ?steps:Float, reversed:Bool = false, frame:Int = 0) {
if (safeH != 'playAnimationSteps' && functionOverridden('playAnimationSteps')) {
@@ -274,10 +331,11 @@ class Character extends FunkinSprite implements ICharacter {
}
if (!specialAnim && animationExists(anim)) {
+ var sameAnim:Bool = (currentAnimation == anim);
+ var animWasDone:Bool = isAnimationFinished();
playAnimation(anim, forced, reversed, frame);
- var sameAnim:Bool = (currentAnimation == anim);
- if (forced || !sameAnim || isAnimationFinished())
+ if (forced || !sameAnim || animWasDone)
timeAnimSteps(steps ?? singForSteps);
}
}
@@ -285,17 +343,28 @@ class Character extends FunkinSprite implements ICharacter {
if (safeH != 'dance' && functionOverridden('dance'))
return cast safeCall('dance', [beat, forced], Bool, false);
- if (!forced && (animReset > 0 || bopFrequency <= 0 || !bop || specialAnim))
+ if (!forced && (animReset > 0 || bopFrequency <= 0 || !bop || specialAnim || held))
return false;
- if (sway) {
- playAnimation((beat % 2 == 0 ? 'danceLeft' : 'danceRight') + idleSuffix);
- } else if (beat % 2 == 0) {
- playAnimation('idle$idleSuffix');
+ if (forced || beat % bopFrequency == 0) {
+ if (sway) {
+ var swayLeft:Bool = (beat % (bopFrequency * 2) == 0);
+ playAnimation((swayLeft ? 'danceLeft' : 'danceRight') + idleSuffix, forced);
+ } else {
+ playAnimation('idle$idleSuffix', forced);
+ }
}
return true;
}
+ public function idle():Void {
+ if (!idleAfterAnim) return;
+
+ specialAnim = false;
+ animReset = 0;
+
+ dance();
+ }
public override function setAnimOffset(x:Float = 0, y:Float = 0):Void {
if (!classicFlip && !sideMatches()) {
animOffset.set(-x + frameWidth - idleFrameSize.x, y);
@@ -310,22 +379,22 @@ class Character extends FunkinSprite implements ICharacter {
if (isAnimate) return flipAnim(animate.funkAnim.name);
else return flipAnim(animation.name);
}
- override function _onAnimationComplete(?anim:String) {
- onAnimationComplete.dispatch(currentAnimation ?? '');
- if (characterGroup != null && this == characterGroup.current)
- characterGroup.onAnimationComplete.dispatch(currentAnimation ?? '');
- }
- override function _onAnimationFrame(frame:Int) {
- onAnimationFrame.dispatch(frame);
- if (characterGroup != null && this == characterGroup.current)
- characterGroup.onAnimationFrame.dispatch(frame);
+ function set_held(value:Bool):Bool {
+ if (held == value) return value;
+
+ held = value;
+ if (!value && animReset <= 0 && !specialAnim)
+ idle();
+
+ return value;
}
function set_bop(value:Bool):Bool { return bop = value; }
function set_animReset(value:Float):Float { return animReset = value; }
function set_specialAnim(value:Bool):Bool { return specialAnim = value; }
function set_idleSuffix(value:String):String { return idleSuffix = value; }
function set_animSuffix(value:String):String { return animSuffix = value; }
- function set_singForSteps(value:Float):Float { return singForSteps = value; };
+ function set_idleAfterAnim(value:Bool):Bool { return idleAfterAnim = value; }
+ function set_singForSteps(value:Float):Float { return singForSteps = value; }
function set_conductorInUse(conductor:Conductor):Conductor { return conductorInUse = conductor; }
function safeCall(func:String, ?args:Array, ?expectedReturn:Dynamic, ?defaultVal:Dynamic):Dynamic {
@@ -340,8 +409,8 @@ class Character extends FunkinSprite implements ICharacter {
return res;
}
function functionOverridden(id:String):Bool {
- for (script in hscripts.activeScripts) {
- if (Reflect.isFunction(script.get(id)))
+ for (script in hscripts) {
+ if (Reflect.isFunction(script.getVar(id)))
return true;
}
return false;
@@ -467,7 +536,8 @@ class Character extends FunkinSprite implements ICharacter {
scaleMultiplier = charData.scale;
smooth = !charData.no_antialiasing;
- singForSteps = charData.sing_duration;
+ bopFrequency = (animationExists('danceLeft') && animationExists('danceRight') ? 1 : 2);
+ singForSteps = Math.max(charData.sing_duration, 1);
defaultFlipX = charData.flip_x ?? false;
scale.set(scaleMultiplier, scaleMultiplier);
cameraOffset.set(charData.camera_position[0], charData.camera_position[1]);
@@ -485,7 +555,10 @@ class Character extends FunkinSprite implements ICharacter {
setBaseSize();
dance();
}
+
finishAnimation();
+ if (anim.looped)
+ playAnimation(currentAnimation, true);
dropNoteCounts = findCountAnimations('drop');
comboNoteCounts = findCountAnimations('combo');
@@ -501,10 +574,8 @@ class Character extends FunkinSprite implements ICharacter {
return;
var comboAnim:String = 'combo$combo';
- if (animationExists(comboAnim, true)) {
- playAnimationSteps(comboAnim, true);
- specialAnim = true;
- }
+ if (animationExists(comboAnim, true))
+ playAnimationSpecial(comboAnim, true);
}
public function playComboDropAnimation(combo:Int) {
if (safeH != 'playComboDropAnimation' && functionOverridden('playComboDropAnimation')) {
@@ -521,12 +592,12 @@ class Character extends FunkinSprite implements ICharacter {
dropAnim = 'drop$count';
}
- if (dropAnim != null) {
- playAnimationSteps(dropAnim, true);
- specialAnim = true;
- }
+ if (dropAnim != null)
+ playAnimationSpecial(dropAnim, true);
}
+ function get_hasComboAnimations():Bool { return (comboNoteCounts.length > 0); }
+ function get_hasDropAnimations():Bool { return (dropNoteCounts.length > 0); }
function findCountAnimations(prefix:String):Array {
var counts:Array = [];
@@ -601,20 +672,25 @@ class Character extends FunkinSprite implements ICharacter {
}
}
interface ICharacter extends IBopper extends IFunkinSpriteAnim {
+ public var held(default, set):Bool;
public var volume(default, set):Float;
public var animReset(default, set):Float;
public var specialAnim(default, set):Bool;
public var animSuffix(default, set):String;
+ public var idleAfterAnim(default, set):Bool;
public var side(default, set):CharacterSide;
public var character(default, set):Null;
public var conductorInUse(default, set):Conductor;
- public function timeAnimSteps(?steps:Float):Void;
public function animationIsLooping(anim:String):Bool;
+ public function timeAnimSteps(?steps:Float, max:Bool = true):Void;
public function playAnimationSoft(anim:String, forced:Bool = false, reversed:Bool = false, frame:Int = 0):Void;
public function playAnimationSteps(anim:String, forced:Bool = false, ?steps:Float, reversed:Bool = false, frame:Int = 0):Void;
+ public function playAnimationSpecial(anim:String, forced:Bool = false, ?steps:Float, reversed:Bool = false, frame:Int = 0):Void;
+ public function playAnim(anim:String, context:PlayAnimContext = SOFT, forced:Bool = false, reversed:Bool = false, frame:Int = 0, ?time:Float):Void;
public function playComboDropAnimation(combo:Int):Void;
public function playComboAnimation(combo:Int):Void;
+ public function idle():Void;
}
interface IBopper {
public var bop(default, set):Bool;
@@ -623,6 +699,13 @@ interface IBopper {
public function dance(beat:Int = 0, forced:Bool = false):Bool;
}
+enum abstract PlayAnimContext(String) to String {
+ var SOFT = 'soft';
+ var SING = 'sing';
+ var SPECIAL = 'special';
+ var DEFAULT = 'default';
+}
+
enum abstract CharacterDataType(String) to String {
var PSYCH = 'psych';
var MODERN = 'modern';
diff --git a/source/funkin/objects/CharacterGroup.hx b/source/funkin/objects/CharacterGroup.hx
index 7e1f795..9151ee9 100644
--- a/source/funkin/objects/CharacterGroup.hx
+++ b/source/funkin/objects/CharacterGroup.hx
@@ -9,28 +9,32 @@ using Lambda;
typedef CharacterOrString = flixel.util.typeLimit.OneOfTwo;
typedef CharacterOrGroup = flixel.util.typeLimit.OneOfTwo;
-class CharacterGroup extends FunkinTypedSpriteGroup implements ICharacter { // TODO: implement interface so currently CharacterGroup type fields can be both group and character instead?
+class CharacterGroup extends FunkinTypedSpriteGroup implements ICharacter {
+ public var onAnimationFrame:FlxTypedSignal String -> Void> = new FlxTypedSignal();
public var onAnimationComplete:FlxTypedSignal Void> = new FlxTypedSignal();
- public var onAnimationFrame:FlxTypedSignal