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 Void> = new FlxTypedSignal(); + public var onAnimationLoop:FlxTypedSignal Void> = new FlxTypedSignal(); + public var anim(get, never):FunkinSpriteAnimHandler; public var bop(default, set):Bool = true; + public var held(default, set):Bool = false; public var side(default, set):CharacterSide; public var animReset(default, set):Float = 0; + public var cameraOffset(get, never):FlxPoint; public var idleSuffix(default, set):String = ''; public var animSuffix(default, set):String = ''; public var specialAnim(default, set):Bool = false; public var conductorInUse(default, set):Conductor; + public var idleAfterAnim(default, set):Bool = true; public var stageCameraOffset(default, null):FlxCallbackPoint; public var onCharacterChanged:FlxTypedSignal Character -> Void> = new FlxTypedSignal(); - @:isVar public var cameraOffset(get, never):FlxPoint; public var volume(default, set):Float = 1; public var character(default, set):String; public var current(default, set):Character = null; - @:isVar public var healthIcon(get, never):String; - @:isVar public var healthIconData(get, never):ModernCharacterHealthIconData; - @:isVar public var currentAnimation(get, never):String; + public var healthIcon(get, never):String; + public var healthIconData(get, never):ModernCharacterHealthIconData; + public var currentAnimation(get, never):String; public var fallbackCharacter:Null; var fallbackChara:Null; @@ -80,6 +84,9 @@ class CharacterGroup extends FunkinTypedSpriteGroup implements IChara } return side = newSide; } + function get_anim():FunkinSpriteAnimHandler { + return current?.anim; + } function get_healthIcon():String { return current?.healthIcon; } @@ -115,7 +122,21 @@ class CharacterGroup extends FunkinTypedSpriteGroup implements IChara if (chara == null) continue; chara.bop = value; } - return specialAnim = value; + return bop = value; + } + function set_held(value:Bool):Bool { + for (chara in members) { + if (chara == null) continue; + chara.held = value; + } + return held = value; + } + function set_idleAfterAnim(value:Bool):Bool { + for (chara in members) { + if (chara == null) continue; + chara.idleAfterAnim = value; + } + return idleAfterAnim = value; } function set_animReset(value:Float):Float { if (current != null) @@ -162,6 +183,7 @@ class CharacterGroup extends FunkinTypedSpriteGroup implements IChara function get_currentAnimation():String { return (current?.currentAnimation); } + override function set_zIndex(newZ:Int):Int { for (chara in members) { if (chara == null) continue; @@ -223,12 +245,13 @@ class CharacterGroup extends FunkinTypedSpriteGroup implements IChara newChara.y += newChara.height * -1 + newChara.originOffset.y; newChara.stageCameraOffset.copyFrom(off); newChara.conductorInUse = conductorInUse; + newChara.alpha = invisible; newChara.bop = bop; off.put(); + add(newChara); newChara.startScripts(); - newChara.alpha = invisible; - return add(newChara); + return newChara; } public function unloadCharacter(?chara:CharacterOrString) { var toDestroy:Character; @@ -262,77 +285,62 @@ class CharacterGroup extends FunkinTypedSpriteGroup implements IChara return group.remove(chara, splice); } - public function timeAnimSteps(?steps:Float):Void { - for (chara in members) { - if (chara == null) continue; - chara.timeAnimSteps(steps); - } - } - public function setOffset(x:Float = 0, y:Float = 0):Void { - if (current != null) - current.setOffset(x, y); - } - public function finishAnimation():Void { - if (current != null) - current.finishAnimation(); - } - public function isAnimationFinished():Bool { - if (current != null) - return current.isAnimationFinished(); - return false; - } - public function animationExists(anim:String, includeUnloaded:Bool = true):Bool { - if (current != null) - return current.animationExists(anim, includeUnloaded); - return false; - } - public function animationIsLooping(anim:String):Bool { - if (current != null) - return current.animationIsLooping(anim); - return false; + public function finishAnimation():Void { current?.finishAnimation(); } + public function isAnimationFinished():Bool { return current?.isAnimationFinished() ?? false; } + public function animationExists(anim:String, preload:Bool = true):Bool { return current?.animationExists(anim, preload) ?? false; } + public function animationIsLooping(anim:String):Bool { return current?.animationIsLooping(anim) ?? false; } + public function setOffset(x:Float = 0, y:Float = 0):Void { current?.setOffset(x, y); } + + public function timeAnimSteps(?steps:Float, max:Bool = true):Void { + for (chara in members) + chara?.timeAnimSteps(steps, max); } public function playAnimationSoft(anim:String, forced:Bool = false, reversed:Bool = false, frame:Int = 0):Void { - for (chara in members) { - if (chara == null) continue; - chara.playAnimationSoft(anim, forced, reversed, frame); - } + for (chara in members) + chara?.playAnimationSoft(anim, forced, reversed, frame); + } + public function playAnim(anim:String, context:PlayAnimContext = SOFT, forced:Bool = false, reversed:Bool = false, frame:Int = 0, ?time:Float):Void { + for (chara in members) + chara?.playAnim(anim, context, forced, reversed, frame, time); } public function playAnimation(anim:String, forced:Bool = false, reversed:Bool = false, frame:Int = 0):Void { - for (chara in members) { - if (chara == null) continue; - chara.playAnimation(anim, forced, reversed, frame); - } + for (chara in members) + chara?.playAnimation(anim, forced, reversed, frame); } public function playAnimationSteps(anim:String, forced:Bool = false, ?steps:Float, reversed:Bool = false, frame:Int = 0):Void { - for (chara in members) { - if (chara == null) continue; - chara.playAnimationSteps(anim, forced, steps, reversed, frame); - } + for (chara in members) + chara?.playAnimationSteps(anim, forced, steps, reversed, frame); + } + public function playAnimationSpecial(anim:String, forced:Bool = false, ?time:Float, reversed:Bool = false, frame:Int = 0):Void { + for (chara in members) + chara?.playAnimationSpecial(anim, forced, time, reversed, frame); } public function playComboAnimation(combo:Int) { - for (chara in members) { - if (chara == null) continue; - chara.playComboAnimation(combo); - } + for (chara in members) + chara?.playComboAnimation(combo); } public function playComboDropAnimation(combo:Int) { - for (chara in members) { - if (chara == null) continue; - chara.playComboDropAnimation(combo); - } + for (chara in members) + chara?.playComboDropAnimation(combo); } public function preloadAnimAsset(anim:String) { // preloads animation with a different spritesheet path - for (chara in members) { - if (chara == null) continue; - chara.preloadAnimAsset(anim); - } + for (chara in members) + chara?.preloadAnimAsset(anim); } public function dance(beat:Int = 0, forced:Bool = false):Bool { + var danced:Bool = false; for (chara in members) { - if (chara == null) continue; - chara.dance(beat, forced); + if (chara == current) { + danced = chara?.dance(beat, forced) ?? false; + } else { + chara?.dance(beat, forced); + } } - return true; + return danced; + } + public function idle():Void { + for (chara in members) + chara?.idle(); } public function flip():CharacterGroup { diff --git a/source/funkin/objects/HealthIcon.hx b/source/funkin/objects/HealthIcon.hx index 9598827..9b10873 100644 --- a/source/funkin/objects/HealthIcon.hx +++ b/source/funkin/objects/HealthIcon.hx @@ -60,14 +60,17 @@ class HealthIcon extends FunkinSprite { } function set_state(newState:IconState):IconState { if (state == newState) return newState; + updateIconState(newState); + return state = newState; } - function set_iconData(newIcon:ModernCharacterHealthIconData) { - if (iconData == newIcon) return newIcon; - + function set_iconData(?newIcon:ModernCharacterHealthIconData):ModernCharacterHealthIconData { newIcon ??= {id: 'face', flipX: false}; + if (iconData == newIcon) return newIcon; + + resetData(); name = newIcon.id; loadGraphic(Paths.image('icons/$name') ?? Paths.image('icons/icon-$name') ?? Paths.image('icons/face')); var wFrameRatio:Int = Math.round(width / height); @@ -95,6 +98,7 @@ class HealthIcon extends FunkinSprite { spriteOffset.set(); } + state = NEUTRAL; updateIconState(state); return iconData = newIcon; diff --git a/source/funkin/objects/Stage.hx b/source/funkin/objects/Stage.hx index db34ca3..6102e89 100644 --- a/source/funkin/objects/Stage.hx +++ b/source/funkin/objects/Stage.hx @@ -1,101 +1,93 @@ package funkin.objects; import funkin.backend.play.Chart; +import funkin.backend.scripting.*; import funkin.objects.CharacterGroup; import funkin.objects.Character; using StringTools; //THIS IS ALL KINDOF A MESS BUT IT WORKS??? I THINK -class Stage extends FlxSpriteGroup { - var chart:Chart; - public var name:String; +class Stage extends FunkinSpriteGroup { + public var stage:String; public var json:Dynamic; public var library:String = ''; public var hasContent:Bool = false; public var stageValid:Bool = false; public var format:StageFormat = NONE; + public var loadCharacters:Bool = true; public var props:Map = new Map(); public var characters:Map = new Map(); - + + public var hscripts:HScriptGroup = new HScriptGroup(); public var zoom:Float = 1; - var state = FlxG.state; + var state:FunkinState; - public function new(?chart:Chart) { + public function new(?stageId:String) { super(); - this.chart = chart; + + this.stage = stageId; + + setup(stageId); + } + public override function destroy():Void { + hscripts.destroy(); + super.destroy(); } - public function setup(?stageId:String, ?chartData:Chart) { - // right now only works with vslice stage jsons - chartData ??= chart; - if (stageId != null) { - Log.minor('loading stage "$stageId"'); + public function setup(?stageId:String) { + if (stageId != null) { + Log.minor('loading stage "$stageId"'); - var jsonPath:String = 'data/stages/$stageId.json'; - if (Paths.exists(jsonPath)) { - var time:Float = Sys.time(); - try { - var content:String = Paths.text(jsonPath); - var jsonData:Dynamic = TJSON.parse(content); - loadModernStageData(jsonData); - json = jsonData; - format = MODERN; - stageValid = true; - hasContent = true; - Log.info('stage loaded successfully! (${FlxMath.roundDecimal(Sys.time() - time, 3)}s)'); - } catch (e:haxe.Exception) { - format = NONE; - Log.error('error while loading stage "$stageId"... -> ${e.details()}'); - } - } else { - Log.warning('stage "$stageId" not found...'); + var jsonPath:String = 'data/stages/$stageId.json'; + if (Paths.exists(jsonPath)) { + var time:Float = Sys.time(); + try { + var content:String = Paths.text(jsonPath); + var jsonData:Dynamic = TJSON.parse(content); + loadModernStageData(jsonData); + json = jsonData; + format = MODERN; + stageValid = true; + hasContent = true; + Log.info('stage loaded successfully! (${FlxMath.roundDecimal(Sys.time() - time, 3)}s)'); + } catch (e:haxe.Exception) { + format = NONE; + Log.error('error while loading stage "$stageId"... -> ${e.details()}'); + } + } else { + Log.warning('stage "$stageId" not found...'); Log.minor('verify path:'); Log.minor('- $jsonPath'); - } - } - - // loads hscript file - var state:FunkinState = cast(FlxG.state, FunkinState); - var scriptPath:String = 'scripts/stages/$stageId.hx'; - if (Paths.exists(scriptPath)) { - if (state != null) - state.hscripts.loadFromPaths(scriptPath); - hasContent = true; - } - - if (state != null) - state.hscripts.run('setupStage', [stageId, this]); - - if (!hasContent) { + } + } + + if (!stageValid) { Log.warning('no stage content (json or script): loading fallback stage'); - loadFallback(); + loadFallbackStage(); } } - - public function sortZIndex() { - sort(Util.sortZIndex, FlxSort.ASCENDING); - } - public function insertZIndex(obj:FlxSprite) { - if (members.contains(obj)) remove(obj); - 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; - } + public function addCharacters(?baseChart:Chart):Void { + switch (format) { + case MODERN: + loadModernCharData(json, baseChart); + default: + loadFallbackCharacters(baseChart); } - if (obj.zIndex < low) { - insert(0, obj); - } else { - add(obj); + } + public function start(state:FunkinState):Void { // runs stage hscript + var scriptPath:String = 'scripts/stages/$stage.hx'; + if (Paths.exists(scriptPath)) { + hscripts.concat(state.hscripts.loadFromPaths(scriptPath)); + hasContent = true; } - return obj; + + if (state != null) + state.hscripts.run('setupStage', [stage, this]); } - public function beatHit(beat:Int) { + public function beatHit(beat:Int):Void { for (prop in props) { if (prop != null && prop.alive && prop.exists && Std.isOfType(prop, IBopper)) { var bopper:IBopper = cast prop; @@ -107,23 +99,20 @@ class Stage extends FlxSpriteGroup { chara.dance(beat); } } - public function destroyProps() { - for (prop in props) prop.destroy(); - } - public function getProp(name:String):FunkinSprite { - return props[name]; + return props.get(name); } public function getCharacter(name:String):CharacterGroup { - return characters[name]; + return characters.get(name); } - public function loadModernStageData(data:ModernStageData) { + + function loadModernStageData(data:ModernStageData):Void { library = data.directory ?? data.library ?? ''; + Paths.library = library; zoom = data.cameraZoom; for (prop in data.props) { var propSprite:StageProp = new StageProp(); - add(propSprite); propSprite.zIndex = prop.zIndex; propSprite.x = prop.position[0]; propSprite.y = prop.position[1]; @@ -154,67 +143,66 @@ class Stage extends FlxSpriteGroup { else propSprite.loadTexture(prop.assetPath, library); } + insertZIndex(propSprite); propSprite.sway = (propSprite.animationExists('danceLeft') && propSprite.animationExists('danceRight')); if (prop.scroll != null) propSprite.scrollFactor.set(prop.scroll[0], prop.scroll[1]); if (prop.scale != null) propSprite.scale.set(prop.scale[0], prop.scale[1]); var assetName:String = prop.name ?? prop.assetPath; propSprite.updateHitbox(); - this.props[assetName] = propSprite; + this.props.set(assetName, propSprite); } - + } + function loadFallbackStage():Void { + var basicBG:StageProp = new StageProp(); + props['basicBG'] = basicBG; + basicBG.loadTexture('bg'); + basicBG.setPosition(-basicBG.width * .5, (FlxG.height - basicBG.height) * .5 + 75); + basicBG.scrollFactor.set(.95, .95); + basicBG.scale.set(2.25, 2.25); + basicBG.zIndex = 0; + add(basicBG); + } + function loadModernCharData(data:ModernStageData, ?chart:Chart):Void { var charas:Dynamic = data.characters; + for (name in Reflect.fields(charas)) { var chara:ModernStageChar = Reflect.field(charas, name); - var char:Null = null; var side:CharacterSide = (switch (name) { case 'bf': RIGHT; case 'gf': IDGAF; default: LEFT; }); - if (chart != null) { - char = Reflect.field(chart, switch (name) { - case 'bf': 'player1'; - case 'dad': 'player2'; - case 'gf': 'player3'; - default: name; - }); - } + var char:Null = (switch (name) { + case 'bf': chart?.player1; + case 'dad': chart?.player2; + case 'gf': chart?.player3; + default: name; + }); var charaGroup:CharacterGroup = new CharacterGroup(chara.position[0], chara.position[1], char, side, name); - add(charaGroup); charaGroup.zIndex = chara.zIndex; charaGroup.stageCameraOffset.set(chara.cameraOffsets[0], chara.cameraOffsets[1]); - if (chara.scale != null) charaGroup.scale.set(chara.scale, chara.scale); - - this.characters[name] = charaGroup; + if (chara.scale != null) charaGroup.scale.set(chara.scale, chara.scale); + insertZIndex(charaGroup); + + this.characters.set(name, charaGroup); } } - public function loadFallback() { - var basicBG:StageProp = new StageProp(); - props['basicBG'] = basicBG; - basicBG.loadTexture('bg'); - basicBG.setPosition(-basicBG.width * .5, (FlxG.height - basicBG.height) * .5 + 75); - basicBG.scrollFactor.set(.95, .95); - basicBG.scale.set(2.25, 2.25); - basicBG.zIndex = 0; - add(basicBG); - loadCharactersGeneric(); - } - function loadCharactersGeneric() { + function loadFallbackCharacters(?chart:Chart):Void { var player1:CharacterGroup = new CharacterGroup(400, 750, chart?.player1 ?? 'bf', RIGHT, 'bf'); var player2:CharacterGroup = new CharacterGroup(-400, 750, chart?.player2 ?? 'dad', LEFT, 'dad'); var player3:CharacterGroup = new CharacterGroup(0, 680, chart?.player3 ?? 'gf', IDGAF, 'gf'); player1.zIndex = 300; player2.zIndex = 200; player3.zIndex = 100; - characters['bf'] = player1; - characters['dad'] = player2; - characters['gf'] = player3; - for (chara in [player1, player2, player3]) { - add(chara); - } + characters.set('bf', player1); + characters.set('dad', player2); + characters.set('gf', player3); + + for (chara in characters) + insertZIndex(chara); } } @@ -248,7 +236,6 @@ class StageProp extends FunkinSprite implements IBopper { // maybe unify charact enum StageFormat { MODERN; - PSYCH; NONE; } diff --git a/source/funkin/objects/play/ArrowPath.hx b/source/funkin/objects/play/ArrowPath.hx new file mode 100644 index 0000000..b2704a8 --- /dev/null +++ b/source/funkin/objects/play/ArrowPath.hx @@ -0,0 +1,165 @@ +package funkin.objects.play; + +import funkin.objects.play.Lane; +import funkin.objects.play.Note; +import funkin.backend.FunkinStrip; +import flixel.graphics.tile.FlxDrawTrianglesItem.DrawData; + +class ArrowPath extends NoteObject { + public var strip:ArrowPathStrip; + public var render:Bool = true; + + public var thickness(get, set):Float; + public var multAlpha:Float = 1; + + public var adaptiveDirection:Bool = false; + public var minDistance:Float = -180; + public var maxDistance:Float = 720; + public var steps:Float = 44; + + var drawData:Array = []; + var drawItems:Int = 0; + + public function new(lane:Lane) { + super(); + + this.strip = new ArrowPathStrip(); + this.laneIndex = lane.noteData; + this.lane = lane; + } + public function copyReceptor(receptor:Receptor) { + if (receptor == null) return; + + scrollPosition.set(receptor.x + receptor.width * .5, receptor.y + receptor.height * .5); + angle = direction + (receptor.lane.direction ?? 0); + alpha = receptor.alpha * multAlpha; + visible = receptor.visible; + } + public override function update(elapsed:Float):Void { + super.update(elapsed); + strip.update(elapsed); + + copyReceptor(lane?.receptor); + if (render && visible && alpha > 0) + updateTriangles(lane); + + } + public override function draw():Void { + if (!visible || !render || alpha <= 0) return; + + strip.alpha = alpha; + strip.color = color; + + for (camera in getCamerasLegacy()) { + if (camera.visible && camera.exists) + drawComplex(camera); + } + } + + public override function drawComplex(camera:FlxCamera):Void { + updateShader(camera); + + for (i in 0 ... drawItems) { + strip.updateRender(drawData[i]); + strip.drawToCamera(camera); + } + } + public function updateTriangles(lane:Lane):Void { + if (!updateModchart) return; + + if (steps < 5) { + Log.warning('$steps px/step for arrow path is too low !!'); + steps = 5; + } + + var modchartFunc = (customModchart ?? genericModchart); + + var scrollDistance:Float = minDistance; + try { + (customScrollDistance ?? genericScrollDistance)(this, lane, 0); // error check + modchartFunc(this, lane, minDistance); + } catch (e:haxe.Exception) { + Log.error('error on path modchart function -> ${e.details()}'); + + customModchart = null; + customScrollDistance = null; + modchartFunc = genericModchart; + genericModchart(this, lane, minDistance); + } + + drawItems = 0; + var defaultAngle:Float = angle; + var prevAngle:Null = null; + + while (scrollDistance < maxDistance) { + var prevScale:FlxPoint = FlxPoint.weak(scale.x, scale.y); + var prevPosition:FlxPoint = FlxPoint.weak(x, y); + + scrollDistance += steps; + modchartFunc(this, lane, scrollDistance); + + var curPosition:FlxPoint = FlxPoint.weak(x, y); + + if (adaptiveDirection) { + angle = (prevPosition.degreesTo(curPosition) + 180); + prevAngle ??= angle; + } else { + prevAngle ??= defaultAngle; + } + + var data:NoteTailDrawData = (drawData[drawItems] ?? new NoteTailDrawData()); + data.copyPosition(prevPosition, curPosition); + data.setScale(prevScale.x, scale.x); + data.setAngle(prevAngle, angle); + drawData[drawItems ++] = data; + + prevAngle = angle; + } + } + + inline function get_thickness():Float { + return strip.thickness; + } + inline function set_thickness(now:Float):Float { + return strip.thickness = now; + } +} + +class ArrowPathStrip extends FunkinStrip { + public var thickness:Float = 2; + + public function new() { + super(); + + this.smooth = false; + this.makeGraphic(25, 25, FlxColor.WHITE); + + indices = new DrawData(6, true, [0, 1, 2, 1, 2, 3]); + uvtData = new DrawData(8, true, [0, 0, 0, 1, 1, 0, 1, 1]); + vertices = new DrawData(8, true, [0, 0, 0, 0, 0, 0, 0, 0]); + // topleft topright bottomleft bottomright + } + + public function updateRender(drawData:NoteTailDrawData):Void { + if (graphic == null) return; + + // update vertices + var width:Float = (thickness * .5 * drawData.scaleTo); + var sin:Float = (FlxMath.fastSin(drawData.angleTo) * width); + var cos:Float = (FlxMath.fastCos(drawData.angleTo) * width); + + vertices[0] = (-sin + drawData.xTo); // top left + vertices[1] = (cos + drawData.yTo); + vertices[2] = (sin + drawData.xTo); // top right + vertices[3] = (-cos + drawData.yTo); + + width = (thickness * .5 * drawData.scaleFrom); + sin = (FlxMath.fastSin(drawData.angleFrom) * width); + cos = (FlxMath.fastCos(drawData.angleFrom) * width); + + vertices[4] = (-sin + drawData.xFrom); // bottom left + vertices[5] = (cos + drawData.yFrom); + vertices[6] = (sin + drawData.xFrom); // bottom right + vertices[7] = (-cos + drawData.yFrom); + } +} \ No newline at end of file diff --git a/source/funkin/objects/play/Lane.hx b/source/funkin/objects/play/Lane.hx index 0148c05..7dbac5b 100644 --- a/source/funkin/objects/play/Lane.hx +++ b/source/funkin/objects/play/Lane.hx @@ -1,145 +1,209 @@ package funkin.objects.play; +import haxe.Constraints; import funkin.shaders.RGBSwap; -import funkin.backend.play.Scoring; +import funkin.objects.Character; +import funkin.objects.play.Note; import funkin.backend.play.NoteEvent; +import funkin.backend.play.NoteStyle; +import funkin.backend.play.ScoreSystem; import funkin.backend.rhythm.Conductor; import flixel.input.keyboard.FlxKey; import flixel.util.FlxSignal.FlxTypedSignal; import flixel.graphics.frames.FlxFramesCollection; +using Lambda; using StringTools; +using funkin.backend.play.NoteStyle.NoteStyleUtil; class Lane extends FunkinSpriteGroup { public var rgbShader:RGBSwap; public var splashRGB:RGBSwap; - - public var held(default, set):Bool = false; + + public var startX:Float; + public var startY:Float; + + public var held:Bool = false; public var heldNote:Note = null; - + public var pressed:Bool = false; + public var noteData:Int; public var oneWay:Bool = true; + public var noteClass:Class = Note; + public var style(default, set):NoteStyle; public var scrollSpeed(default, set):Float = 1; public var direction:Float = 90; public var spawnRadius:Float; - public var hitWindow:Float = Scoring.safeFrames / 60 * 1000; + public var hitWindow:Float = (ScoreSystem.safeFrames * 1000 / 60); public var conductorInUse:Conductor = FunkinState.getCurrentConductor(); public var inputKeys:Array = []; + public var character:ICharacter = null; public var strumline:Strumline; public var cpu(default, set):Bool = false; public var allowInput:Bool = true; public var inputFilter:Note -> Bool; public var noteEvent:FlxTypedSignal Void> = new FlxTypedSignal(); - var extraWindow:Float = 0; // antimash mechanic + public var extraWindow:Float = 0; // antimash mechanic + var queueComputeLimit:Int = 500; + var spawnLimit:Int = 75; public var receptor:Receptor; - public var noteCover:NoteCover; + public var arrowPath:ArrowPath; public var notes:FunkinTypedSpriteGroup; + public var wooshNotes:FunkinTypedSpriteGroup; public var noteSparks:FunkinTypedSpriteGroup; public var noteSplashes:FunkinTypedSpriteGroup; - public var queue:Array = []; + public var queue:Array = []; + + public var canSplash:Bool = true; + public var canSpark:Bool = true; public var selfDraw:Bool = false; public var topMembers:Array = []; - public function set_scrollSpeed(newSpeed:Float) { - var cam = camera ?? FlxG.camera; - spawnRadius = Note.distanceToMS(camera.height / camera.zoom, newSpeed) + 50; + public var songPosition(get, never):Float; + + function set_scrollSpeed(newSpeed:Float) { return scrollSpeed = newSpeed; } - public function set_held(newHeld:Bool) { - if (held == newHeld) return newHeld; - if (newHeld) popCover(); - else noteCover.kill(); - return held = newHeld; - } - public function set_cpu(isCpu:Bool) { + function set_cpu(isCpu:Bool) { if (cpu == isCpu) return isCpu; if (receptor != null) receptor.autoReset = isCpu; return cpu = isCpu; } - public function new(x:Float, y:Float, data:Int) { + inline function get_songPosition():Float { + return conductorInUse.songPosition; + } + public function new(x:Float, y:Float, data:Int, dir:Float = 90, speed:Float = 1, ?style:NoteStyleAsset = 'funkin') { super(x, y); - + + rgbShader = new RGBSwap(); + splashRGB = new RGBSwap(); + + startX = x; + startY = y; + noteData = data; + direction = dir; + scrollSpeed = speed; + this.style = NoteStyle.fetch(style); + inputFilter = (note:Note) -> { var time:Float = note.msTime - conductorInUse.songPosition; return (time <= note.hitWindow + extraWindow) && (time >= -note.hitWindow); }; - var splashColors:Array = NoteSplash.makeSplashColors(Note.directionColors[data][0]); - rgbShader = new RGBSwap(Note.directionColors[data][0], FlxColor.WHITE, Note.directionColors[data][1]); - splashRGB = new RGBSwap(splashColors[0], FlxColor.WHITE, splashColors[1]); - - noteCover = new NoteCover(data); - receptor = new Receptor(0, 0, data); + receptor = new Receptor(0, 0, data, style); + arrowPath = new ArrowPath(this); + arrowPath.render = false; notes = new FunkinTypedSpriteGroup(); + wooshNotes = new FunkinTypedSpriteGroup(); noteSparks = new FunkinTypedSpriteGroup(0, 0, 5); noteSplashes = new FunkinTypedSpriteGroup(0, 0, 5); - spawnRadius = Note.distanceToMS(FlxG.height, scrollSpeed); + spawnRadius = Note.distanceToMS(FlxG.height + 75, scrollSpeed); receptor.lane = this; //lol - this.noteData = data; + + this.add(arrowPath); this.add(receptor); - topMembers.push(notes); - topMembers.push(noteCover); - topMembers.push(noteSparks); - topMembers.push(noteSplashes); - - noteCover.shader = splashRGB.shader; - + for (mem in [wooshNotes, notes, noteSparks, noteSplashes]) { + topMembers.push(mem); // render conditionally + this.add(mem); + } + spark().alpha = .0001; splash().alpha = .0001; + + updateLaneScale(scale); + } + override function initVars():Void { + super.initVars(); + scale.destroy(); + scale = new flixel.math.FlxPoint.FlxCallbackPoint(updateLaneScale); + scale.set(1, 1); + } + function updateLaneScale(point:FlxPoint) { + if (receptor != null) { + var mult:Float = receptor.defaultScale; + receptor.scale.set(point.x * mult, point.y * mult); + receptor.updateHitbox(); + } } + public function woosh():Void { + for (note in wooshNotes) { + var startX:Float = note.x; + var startY:Float = note.y; + + FlxTween.tween(note, {x: note.x + FlxG.height * Math.cos(direction / 180 * Math.PI), y: note.y + FlxG.height * Math.sin(direction / 180 * Math.PI)}, .5, { + ease: FlxEase.expoIn, + onComplete: (_) -> { + note.kill(); + wooshNotes.remove(note, true); + note.destroy(); + }, + onUpdate: (_) -> { + if (note.tail != null) { + @:privateAccess note.tail.holdStrip?.setPosition(note.x - startX, note.y - startY); + @:privateAccess note.tail.tailStrip?.setPosition(note.x - startX, note.y - startY); + } + } + }); + } + } public override function update(elapsed:Float) { + updateQueue(); + updateNotes(); + + super.update(elapsed); + extraWindow = Math.max(extraWindow - elapsed * 200, 0); + } + public function updateQueue() { var i:Int = 0; var early:Bool; - var limit:Int = 50; + var limit:Int = queueComputeLimit; + while (i < queue.length) { - var note:Note = queue[i]; + var note:ChartNote = queue[i]; if (note == null) { Log.warning('note was null in lane $noteData!!'); queue.remove(note); continue; } - early = (note.msTime - conductorInUse.songPosition > spawnRadius); - if (!early && (oneWay || (note.endMs - conductorInUse.songPosition) >= -spawnRadius)) { + + early = (note.msTime - conductorInUse.songPosition > Math.max(spawnRadius, hitWindow)); + if (!early && (oneWay || (note.msTime + note.msLength - conductorInUse.songPosition) >= -spawnRadius)) { queue.remove(note); insertNote(note); - limit --; - if (limit < 0) break; - } else + if (notes.countLiving() >= spawnLimit || --limit < 0) break; + } else { i ++; - if (early && oneWay) break; + } + + if (early && oneWay) + break; } - - updateNotes(); - - super.update(elapsed); - extraWindow = Math.max(extraWindow - elapsed * 200, 0); - for (member in topMembers) member.update(elapsed); } public function updateNotes() { var i:Int = notes.length; while (i > 0) { - i --; - var note:Note = notes.members[i]; - if (note == null) continue; + var note:Note = notes.members[-- i]; + if (note == null || !note.alive) continue; updateNote(note); } } public override function draw() { - super.draw(); - if (selfDraw) drawTop(); + drawThing(selfDraw ? null : false); } - public function drawTop() { + function drawThing(?top:Bool):Void { @:privateAccess { final oldDefaultCameras = FlxCamera._defaultCameras; if (_cameras != null) FlxCamera._defaultCameras = _cameras; - for (member in topMembers) { + for (member in members) { + if (top != null && topMembers.contains(member) != top) + continue; if (member != null && member.exists && member.visible) member.draw(); } @@ -147,105 +211,142 @@ class Lane extends FunkinSpriteGroup { FlxCamera._defaultCameras = oldDefaultCameras; } } - public function forEachNote(func:Note -> Void, includeQueued:Bool = false) { + public function forEachNote(func:ChartNote -> Void, includeQueued:Bool = false) { if (includeQueued) { for (note in queue) func(note); } - for (note in notes) - func(note); + for (note in notes) { + if (note.alive && note.chartNote != null) + func(note.chartNote); + } + } + public function forEachActiveNote(func:Note -> Void) { + for (note in notes) { + if (note.alive) + func(note); + } } public function fireInput(key:FlxKey, pressed:Bool):Bool { if (!inputKeys.contains(key) || !allowInput) return false; if (pressed) { - var note = getHighestNote(inputFilter); - if (note != null) { - hitNote(note); - } else { - ghostTapped(); - } + _noteEvent(basicEvent(PRESSED, getHighestNote(inputFilter))); } else { - held = false; - receptor.playAnimation('static', true); - if (heldNote != null) { - killSustainsOf(heldNote); - heldNote.consumed = true; - heldNote = null; - } + var note:Note = heldNote; + _noteEvent(basicEvent(RELEASED, note)); + if (note != null) + _noteEvent(basicEvent(RELEASED)); } return true; } - public function ghostTapped() - _noteEvent(basicEvent(GHOST)); - public function basicEvent(type:NoteEventType, ?note:Note):NoteEvent - return {lane: this, strumline: strumline, receptor: receptor, note: note, type: type}; + public function ghostTapped(?position:Float) + _noteEvent(basicEvent(GHOST, null, position)); + public function basicEvent(type:NoteEventType, ?note:Note, ?position:Float):NoteEvent + return {lane: this, strumline: strumline, receptor: receptor, note: note, type: type, songPosition: position ?? songPosition}; function _noteEvent(event:NoteEvent) { strumline?.noteEvent.dispatch(event); noteEvent.dispatch(event); } - public function getHighestNote(?filter:Note -> Bool) { + public function getHighestNote(?filter:Note -> Bool, hittableOnly:Bool = true) { var highNote:Null = null; for (note in notes) { + if (!note.alive) continue; + var valid:Bool = (filter == null ? true : filter(note)); - var canHit:Bool = (note.canHit && !note.isHoldPiece && valid); - if (!canHit) continue; - if (highNote == null || (note.hitPriority >= highNote.hitPriority || (note.hitPriority == highNote.hitPriority && note.msTime < highNote.msTime))) + + if (!valid) + continue; + if (hittableOnly && (!note.canHit || note.goodHit)) + continue; + if (highNote == null || note.hitPriority > highNote.hitPriority || (note.hitPriority == highNote.hitPriority && note.msTime < highNote.msTime)) highNote = note; } return highNote; } public function getAllNotes() { - var notes:Array = []; - for (note in this.notes) notes.push(note); - for (note in this.queue) notes.push(note); + var notes:Array = []; + + for (note in this.queue) + notes.push(note); + for (note in this.notes) { + if (note.alive && note.chartNote != null) + notes.push(note.chartNote); + } + return notes; } public function resetLane() { clearNotes(); - receptor?.playAnimation('static'); - noteCover.kill(); + receptor.playAnimation('static'); + removeCovers(); heldNote = null; held = false; } - public function splash():NoteSplash { - var splash:NoteSplash = noteSplashes.recycle(NoteSplash, () -> new NoteSplash(noteData)); - splash.camera = camera; //silly. freaking silly - splash.alpha = alpha * .7; - splash.shader = splashRGB.shader; - splash.splashOnReceptor(receptor); + public function splash(?note:Note):NoteSplash { + var splash:NoteSplash = noteSplashes.recycle(NoteSplash, () -> new NoteSplash(noteData, style), true); + + preAdd(splash); + noteSplashes.moveToTop(splash); + splash.reload(note?.style ?? style); + splash.popOnReceptor(receptor); + splash.alpha = alpha * splash.defaultAlpha; + splash.scale.set(scale.x * splash.defaultScale, scale.y * splash.defaultScale); + return splash; } - public function popCover():NoteCover { - noteCover.popOnReceptor(receptor); - return noteCover; + public function popCover(?note:Note):NoteSpark { + var spark:NoteSpark = noteSparks.recycle(NoteSpark, () -> new NoteSpark(noteData, style), true); + + preAdd(spark); + noteSparks.moveToTop(spark); + spark.reload(note?.style ?? style); + spark.heldNote = note; + spark.popOnReceptor(receptor); + spark.alpha = alpha * spark.defaultAlpha; + spark.scale.set(scale.x * spark.defaultScale, scale.y * spark.defaultScale); + + return spark; } - public function spark():NoteSpark { - var spark:NoteSpark = noteSparks.recycle(NoteSpark, () -> new NoteSpark(noteData)); - spark.alpha = alpha; - spark.camera = camera; - spark.shader = splashRGB.shader; - spark.sparkOnReceptor(receptor); + public function spark(?note:Note, animate:Bool = true):NoteSpark { + var spark:NoteSpark = noteSparks.members.find((spark:NoteSpark) -> spark.heldNote == note); + spark ??= popCover(); + if (animate) { + spark.spark(); + } else { + spark.kill(); + } return spark; } + public function removeCovers():Void { // rename to removeSpakrs maybe :sob: + for (spark in noteSparks) { + if (spark.alive && !spark.sparking) { + spark.heldNote = null; + spark.kill(); + } + } + } - public function queueNote(note:Note, sorted:Bool = false):Note { - if (!queue.contains(note)) { - if (sorted) { - for (i => otherNote in queue) { - if (otherNote.msTime >= note.msTime) { + public inline function queueNote(note:ChartNote, sorted:Bool = false, checkExists:Bool = true):ChartNote { + var pushed:Bool = false; + + if (sorted) { + for (i => otherNote in queue) { + if (otherNote.msTime >= note.msTime) { + if (!checkExists || !queue.contains(note)) queue.insert(i, note); - return note; - } + pushed = true; + break; } } - note.lane = this; - queue.push(note); } + if (!pushed && (!checkExists || !queue.contains(note))) + queue.push(note); + return note; } - public function dequeueNote(note:Note) { + public function dequeueNote(note:ChartNote) { queue.remove(note); } public function clearNotes() { @@ -255,136 +356,176 @@ class Lane extends FunkinSpriteGroup { queue.resize(0); } public function updateNote(note:Note) { - note.followLane(this, scrollSpeed); - if (note.ignore) return; - if ((cpu || (held && note.isHoldPiece)) && conductorInUse.songPosition >= note.msTime && !note.lost && note.canHit) { + if (!note.ignore && (cpu || (held && note.goodHit)) && songPosition >= note.msTime && !note.lost && note.canHit) { + var killingNote:Bool = false; + if (!note.goodHit) - hitNote(note, false); - var canKillNote:Bool = (conductorInUse.songPosition >= note.endMs); - if (note.isHoldPiece) { - var holdEvent:NoteEvent = basicEvent(canKillNote ? RELEASED : HELD, note); - _noteEvent(holdEvent); - } - if (canKillNote) { + _noteEvent(basicEvent(PRESSED, note, cpu ? note.msTime : songPosition)); + + if (songPosition >= note.endMs) + note.consumed = killingNote = true; + + _noteEvent(basicEvent(HELD, note)); + + if (killingNote) { + var releaseTime:Null = (cpu ? note.endMs : songPosition); + + _noteEvent(basicEvent(RELEASED, note, releaseTime)); + if (cpu) _noteEvent(basicEvent(RELEASED, null, releaseTime)); + killNote(note); return; } } + + note.followLane(this); + var canDespawn:Bool = !note.preventDespawn; - if (note.lost || note.goodHit || note.isHoldPiece) { - if (canDespawn && (note.endMs - conductorInUse.songPosition) < -spawnRadius) { - if (!oneWay) // bye bye note - queue.push(note); - killNote(note); - } + if (note.lost || note.goodHit) { + if (canDespawn && (note.endMs - songPosition) < -spawnRadius) + killNote(note, !oneWay); } else { - if (conductorInUse.songPosition - hitWindow > note.msTime) { + if (songPosition - hitWindow > note.msTime) { note.lost = true; - _noteEvent(basicEvent(LOST, note)); + if (!note.ignore) _noteEvent(basicEvent(LOST, note)); } } - if (!oneWay && (note.msTime - conductorInUse.songPosition) > spawnRadius) { - queue.push(note); - killNote(note); + + if (!oneWay && (note.msTime - songPosition) > spawnRadius) + killNote(note, true); + } + public function findNoteByChartNote(songNote:ChartNote):Note { + return notes.members.find((note:Note) -> note.chartNote == songNote); + } + public function generateNote(?cls:Class, songNote:ChartNote, pool:Bool = true):Note { + if (pool) { + return notes.recycle(noteClass, () -> generateNote(noteClass, songNote, false)); + } else { + return Type.createInstance(cls ?? noteClass, [songNote, conductorInUse]); } } - public function insertNote(note:Note, pos:Int = -1) { - if (notes.members.contains(note)) return; - if (note.parent != null && note.parent.consumed) return; + public function insertNote(songNote:ChartNote):Note { + var note:Note = generateNote(noteClass, songNote); + preAdd(note); + if (note.tail?.alive) + preAdd(note.tail); + notes.moveToBottom(note); - note.shader ??= rgbShader.shader; - note.hitWindow = hitWindow; note.lane = this; - - note.scale.copyFrom(receptor.scale); + note.chartNote = songNote; + note.hitWindow = hitWindow; + note.arrowPath = arrowPath; + + note.reload(style, this); + note.scale.set(scale.x * note.defaultScale, scale.y * note.defaultScale); note.updateHitbox(); - note.revive(); - updateNote(note); - if (pos < 0) { - pos = 0; - for (note in notes) { - if (note.isHoldPiece) pos ++; - else break; - } - } - notes.insert(pos, note); + _noteEvent(basicEvent(SPAWNED, note)); + updateNote(note); + + return note; } - public dynamic function hitNote(note:Note, kill:Bool = true) { + public dynamic function hitNote(note:Note, kill:Bool = true, ?position:Float) { note.goodHit = true; - var event:NoteEvent = basicEvent(HIT, note); + var event:NoteEvent = basicEvent(HIT, note, position); _noteEvent(event); - if (kill && !note.ignore && !event.cancelled) + if (kill && !note.isHoldNote && !event.cancelled) killNote(note); } - public function killNote(note:Note) { + public function killNote(note:Note, requeue:Bool = false, sort:Bool = true) { + if (requeue) + queueNote(note.chartNote, sort, false); + note.kill(); - notes.remove(note, true); _noteEvent(basicEvent(DESPAWNED, note)); } - public function killSustainsOf(note:Note) { - for (child in note.children) { - if (!child.alive) - continue; - child.lost = true; - child.canHit = false; - _noteEvent(basicEvent(RELEASED, child)); - killNote(child); - } + + function set_style(newStyle:NoteStyle):NoteStyle { + if (style == newStyle) return newStyle; + style = newStyle; + loadStyle(newStyle); + return newStyle; + } + public function loadStyle(newStyle:NoteStyleAsset) { + var style:NoteStyle = NoteStyle.fetch(newStyle); + + if (receptor != null) + receptor.style = style; + + var colors:Array = NoteStyle.getDirectionColors(style, noteData); + rgbShader.set(colors[0], colors[1], colors[2]); + + var splashColors:Array = NoteSplash.makeSplashColors(colors[0]); + splashRGB.set(splashColors[0], FlxColor.WHITE, splashColors[1]); + + updateLaneScale(scale); + } + + public function getDirection(?dir:Int):String { + return NoteStyle.getDirectionName(style, dir ?? noteData); + } + public function getSingAnimation(?dir:Int):String { + return NoteStyle.getDirectionSing(style, dir ?? noteData); } + public function getColors(?dir:Int):Array { + return NoteStyle.getDirectionColors(style, dir ?? noteData); + } + + override function findMinXHelper():Float { return receptor.x; } + override function findMaxXHelper():Float { return receptor.x + receptor.width; } + override function findMinYHelper():Float { return receptor.y; } + override function findMaxYHelper():Float { return receptor.y + receptor.height; } - public override function get_width() - return receptor?.width ?? 0; - public override function get_height() - return receptor?.height ?? 0; + override function set_zoomFactor(value:Float):Float { + super.set_zoomFactor(value); + for (sprite in topMembers) { + if (sprite == null) continue; + var funk:IFunkinSpriteVars = getFunk(sprite); + if (funk != null) funk.zoomFactor = value; + } + return zoomFactor = value; + } + override function set_initialZoom(value:Float):Float { + super.set_initialZoom(value); + for (sprite in topMembers) { + if (sprite == null) continue; + var funk:IFunkinSpriteVars = getFunk(sprite); + if (funk != null) funk.initialZoom = value; + } + return initialZoom = value; + } } class Receptor extends FunkinSprite { public var lane:Lane; public var noteData:Int; + public var rgbShader:RGBSwap; - public var missColor:Array; - public var glowColor:Array; public var rgbEnabled(default, set):Bool; - public var autoReset:Bool = false; + public var style(default, set):NoteStyle; + public var grayBeat:Null; + public var autoReset:Bool = false; + public var defaultScale:Float = 1; + + public var updateRGBShader:Bool = true; - public function new(x:Float, y:Float, data:Int) { + public function new(x:Float, y:Float, data:Int, ?style:NoteStyleAsset = 'funkin') { super(x, y); loadAtlas('notes'); + rgbShader = new RGBSwap(); this.noteData = data; - - rgbShader = new RGBSwap(); - rgbShader.green = 0xffffff; - rgbShader.red = Note.directionColors[data][0]; - rgbShader.blue = Note.directionColors[data][1]; - glowColor = [rgbShader.red, rgbShader.blue]; - missColor = [makeGrayColor(rgbShader.red), FlxColor.fromRGB(32, 30, 49)]; - - loadAtlas('notes'); - reloadAnimations(); - + this.style = NoteStyle.fetch(style); + onAnimationComplete.add((anim:String) -> { - if (anim != 'confirm') return; - if (lane == null || (autoReset && !lane.held)) { + if (!anim.startsWith('static') && autoReset && (lane == null || !lane.held)) playAnimation('static', true); - } }); } - public function reloadAnimations() { - animation.destroyAnimations(); - var dirName:String = Note.directionNames[noteData]; - addAnimation('static', '$dirName receptor', 24, true); - addAnimation('confirm', '$dirName confirm', 24, false); - addAnimation('press', '$dirName press', 24, false); - playAnimation('static', true); - updateHitbox(); - } - public override function update(elapsed:Float) { super.update(elapsed); if (grayBeat != null && lane.conductorInUse.metronome.beat >= grayBeat) @@ -392,26 +533,51 @@ class Receptor extends FunkinSprite { } public override function playAnimation(anim:String, forced:Bool = false, reversed:Bool = false, frame:Int = 0) { - if (anim == 'static') { - rgbEnabled = false; - } else { - var baseColor:Array = (anim == 'press' ? missColor : glowColor); - rgbShader.blue = baseColor[1]; - rgbShader.red = baseColor[0]; - rgbEnabled = true; + var overrideAnim:String = '$anim-$noteData'; + if (animationExists(overrideAnim)) + anim = overrideAnim; + + if (updateRGBShader) { + var animData:NoteStyleAnimData = style?.getAssetAnimation(style.data.receptors, anim); + if (animData != null) { + if (animData.disableRGB) { + rgbEnabled = false; + } else { + var colors:Array = style.getDirectionColorMod(noteData, animData.colorMod); + rgbShader.set(colors[0], colors[1], colors[2]); + rgbEnabled = true; + } + } } if (anim != 'confirm') grayBeat = null; + super.playAnimation(anim, forced, reversed, frame); centerOffsets(); centerOrigin(); } - + + function set_style(newStyle:NoteStyle) { + if (style == newStyle) return newStyle; + style = newStyle; + loadStyle(newStyle); + return newStyle; + } + public function loadStyle(newStyle:NoteStyleAsset) { + var style:NoteStyle = NoteStyle.fetch(newStyle); + + NoteStyleUtil.loadNoteStyleAnimations(this, style?.data?.receptors, style?.getDirectionName(noteData)); + updateRGBShader = !(style?.data.general.disableRGB ?? false); + defaultScale = style?.data?.receptors?.scale ?? 1; + playAnimation('static', true); + updateHitbox(); + } + public function set_rgbEnabled(newE:Bool) { shader = (newE ? rgbShader.shader : null); return rgbEnabled = newE; } - + public static function makeGrayColor(col:FlxColor) { var pCol:FlxColor = col; col.red = Std.int(FlxMath.bound(col.red - 40 - (col.blue - col.red) * .1 + Math.abs(col.red - col.blue) * .1 + Math.min(col.red - Math.pow(col.blue / 255, 2) * 255 * 3 + col.green * .4, 0) * .1, 0, 255)); @@ -426,28 +592,114 @@ class Receptor extends FunkinSprite { } class NoteSplash extends FunkinSprite { + public var lane:Lane; public var noteData:Int; + + public var rgbShader:RGBSwap; + public var updateRGBShader:Bool = true; + public var rgbEnabled(default, set):Bool; + public var style(default, set):NoteStyle; + + public var defaultScale:Float = 1; + public var defaultAlpha:Float = 1; + public var animationVariants:Int = 1; + public var frameRateRange:Array; + + var asset:NoteStyleAssetData = null; - public function new(data:Int) { + public function new(data:Int, ?style:NoteStyleAsset = 'funkin') { super(); - loadAtlas('noteSplashes'); + + rgbShader = new RGBSwap(); + shader = rgbShader.shader; this.noteData = data; - var dirName:String = Note.directionNames[data]; - addAnimation('splash1', 'notesplash $dirName 1', 24, false); - addAnimation('splash2', 'notesplash $dirName 2', 24, false); - onAnimationComplete.add((anim:String) -> { kill(); }); + this.style = NoteStyle.fetch(style); + + initAnimation(); + } + function initAnimation():Void { + onAnimationComplete.add((anim:String) -> kill()); } - public function splashOnReceptor(receptor:Receptor) { //lol + function set_style(newStyle:NoteStyle) { + if (style == newStyle) return newStyle; + style = newStyle; + loadStyle(newStyle); + return newStyle; + } + public function loadStyle(newStyle:NoteStyleAsset) { + var style:NoteStyle = NoteStyle.fetch(newStyle); + + updateRGBShader = !(style?.data.general.disableRGB ?? false); + asset = style?.data.noteSplashes; + + NoteStyleUtil.loadNoteStyleAnimations(this, asset, style?.getDirectionName(noteData)); + animationVariants = asset?.variants ?? 1; + playAnimation('splash-1', true); + updateHitbox(); + + defaultScale = asset?.scale ?? 1; + defaultAlpha = asset?.alpha ?? 1; + } + public function reload(style:NoteStyle) { + blend = NORMAL; + + this.style = style; + } + + public function popOnReceptor(receptor:Receptor) { //lol setPosition(receptor.x + receptor.width * .5, receptor.y + receptor.height * .5); - splash(); + pop(); } - public function splash() { - playAnimation('splash${FlxG.random.int(1, 2)}', true); - animation.curAnim.frameRate = FlxG.random.int(22, 26); + public function pop():NoteSplash { + var splashAnim:String = 'splash-${FlxG.random.int(1, animationVariants)}'; + + var range:Array = frameRateRange; + if (range == null) { + var animation:NoteStyleAnimData = style?.getAssetAnimation(asset, splashAnim); + if (animation != null) { + if (animation.frameRateRange != null) + range = animation.frameRateRange; + } + } + + playAnimation(splashAnim, true); updateHitbox(); - spriteOffset.set(width * .5, height * .5); + offset.set(frameWidth * .5, frameHeight * .5); + + if (anim.curAnim == null) { + kill(); + } else if (range != null) { + anim.curAnim.frameRate = FlxG.random.int(range[0], range[1]); + } + + return this; + } + public override function playAnimation(anim:String, forced:Bool = false, reversed:Bool = false, frame:Int = 0) { + var overrideAnim:String = '$anim-$noteData'; + if (animationExists(overrideAnim)) + anim = overrideAnim; + + if (updateRGBShader) { + var animData:NoteStyleAnimData = style?.getAssetAnimation(asset, anim); + if (animData != null) { + if (animData.disableRGB) { + rgbEnabled = false; + } else { + var colors:Array = style.getDirectionColorMod(noteData, animData.colorMod); + rgbShader.set(colors[0], colors[1], colors[2]); + rgbEnabled = true; + } + } + } + + super.playAnimation(anim, forced, reversed, frame); + } + + public function set_rgbEnabled(newE:Bool) { + shader = (newE ? rgbShader.shader : null); + return rgbEnabled = newE; } public static function makeSplashColors(baseFill:FlxColor):Array { @@ -461,60 +713,68 @@ class NoteSplash extends FunkinSprite { fill.brightness = fill.brightness * .5 + .5; var ring:FlxColor = baseFill; - ring.red = Std.int(ring.red * .65); - ring.green = Std.int(ring.green * Math.max(.75 - ring.blue * .2, 0)); - ring.blue = Std.int(Math.min((ring.blue + 80) * ring.brightness, 255)); - ring.saturation = Math.min(1 - Math.pow(1 - ring.saturation * 1.4, 2), 1) * Math.min(ring.brightness / .125, 1); - ring.brightness = ring.brightness * .75 + .25; + ring.red = Std.int(ring.red * .9); + ring.green = Std.int(ring.green * Math.max(.95 - ring.blue / 255 * .3 - ring.red / 255 * .3, 0)); + ring.blue = Std.int(Math.min((ring.blue * 2 + 80 - ring.red * .3) * ring.brightness, 255)); + ring.saturation = Math.min(ring.saturation * 1.2 * Math.min(ring.brightness / .125, 1), 1); + ring.brightness = ring.brightness * .875 + .125; return [fill, ring]; } } -class NoteCover extends FunkinSprite { - public function new(data:Int) { - super(); - loadAtlas('noteCovers'); - - var dir:String = Note.directionNames[data]; - if (!hasAnimationPrefix('hold cover start $dir')) dir = ''; - addAnimation('start', 'hold cover start $dir'.trim(), 24, false); - addAnimation('loop', 'hold cover loop $dir'.trim(), 24, true); - onAnimationComplete.add((anim:String) -> { playAnimation('loop'); }); - - kill(); - } +class NoteSpark extends NoteSplash { + public var heldNote:Note = null; + public var sparking:Bool = false; - public function popOnReceptor(receptor:Receptor) { - setPosition(receptor.x + receptor.width * .5, receptor.y + receptor.height * .5); - pop(); + public function new(data:Int, ?style:NoteStyleAsset) { + super(data, style); } - public function pop() { + override function initAnimation():Void { + onAnimationComplete.add((anim:String) -> { + if (anim.startsWith('start')) + playAnimation('loop', true); + if (anim.startsWith('spark')) + kill(); + }); + } + + public override function loadStyle(newStyle:NoteStyleAsset) { + var oldAnim:String = currentAnimation; + var oldFrame:Float = anim.curFrameFloat; + var style:NoteStyle = NoteStyle.fetch(newStyle); + + updateRGBShader = !(style?.data.general.disableRGB ?? false); + asset = style?.data.noteCovers; + + NoteStyleUtil.loadNoteStyleAnimations(this, asset, style?.getDirectionName(noteData)); + defaultScale = asset?.scale ?? 1; + defaultAlpha = asset?.alpha ?? 1; playAnimation('start', true); - revive(); updateHitbox(); - spriteOffset.set(width * .5 + 10, height * .5 - 46); - } -} - -class NoteSpark extends FunkinSprite { - public function new(data:Int) { - super(data); - loadAtlas('noteCovers'); - var dir:String = Note.directionNames[data]; - if (!hasAnimationPrefix('hold cover $dir')) dir = ''; - addAnimation('spark', 'hold cover spark ${dir}'.trim(), 24, false); - onAnimationComplete.add((anim:String) -> { kill(); }); + offset.set(frameWidth * .5, frameHeight * .5); + if (animationExists(oldAnim)) { + playAnimation(oldAnim, true); + anim.curFrameFloat = oldFrame; + } } - public function sparkOnReceptor(receptor:Receptor) { - setPosition(receptor.x + receptor.width * .5, receptor.y + receptor.height * .5); - spark(); + public override function pop():NoteSpark { + playAnimation('start', true); + sparking = false; + revive(); + return this; } - public function spark() { + public function spark():NoteSpark { playAnimation('spark', true); - updateHitbox(); - spriteOffset.set(width * .5 + 10, height * .5 - 46); + sparking = true; + heldNote = null; + return this; + } + public override function kill():Void { + super.kill(); + heldNote = null; + sparking = false; } } \ No newline at end of file diff --git a/source/funkin/objects/play/Note.hx b/source/funkin/objects/play/Note.hx index 03dfd41..3035700 100644 --- a/source/funkin/objects/play/Note.hx +++ b/source/funkin/objects/play/Note.hx @@ -1,215 +1,849 @@ package funkin.objects.play; +import funkin.shaders.RGBSwap; import funkin.objects.play.Lane; -import funkin.backend.play.Scoring; +import funkin.backend.rhythm.Event; +import funkin.backend.FunkinSprite; +import funkin.backend.play.NoteStyle; +import funkin.backend.play.ScoreSystem; import funkin.objects.CharacterGroup; +import funkin.backend.FunkinStrip; +import openfl.geom.ColorTransform; import flixel.graphics.frames.FlxFrame; -import flixel.addons.display.FlxTiledSprite; -import flixel.graphics.frames.FlxFramesCollection; +import flixel.graphics.tile.FlxDrawTrianglesItem.DrawData; -class Note extends FunkinSprite { // todo: pooling?? maybe?? how will this affect society +using flixel.util.FlxColorTransformUtil; +using funkin.backend.play.NoteStyle.NoteStyleUtil; + +@:structInit class ChartNote extends FlxBasic implements ITimeSortable { + public var laneIndex:Int; + public var kind:String = ''; + public var msTime:Float = 0; + public var msLength:Float = 0; + public var strumlineIndex:Int = 0; + + public inline function copy(?toNote:ChartNote):ChartNote { + if (toNote == null) { + return {strumlineIndex: strumlineIndex, laneIndex: laneIndex, msLength: msLength, msTime: msTime, kind: kind}; + } else { + toNote.strumlineIndex = strumlineIndex; + toNote.laneIndex = laneIndex; + toNote.msLength = msLength; + toNote.msTime = msTime; + toNote.kind = kind; + return toNote; + } + } + + function set_kind(v:String):String { return kind = v; } + function set_msTime(v:Float):Float { return msTime = v; } + function set_msLength(v:Float):Float { return msLength = v; } +} + +@:build(funkin.macros.FunkinMacro.buildReset()) +class NoteObject extends FunkinSprite { + public var lane:Lane; + public var laneIndex:Int = 0; + + public var defaultAlpha:Float = 1; + public var defaultScale:Float = 1; + + @resetVar public var updateModchart:Bool = true; + @resetVar public var followReceptor:Bool = true; + + @resetVar public var direction:Float = 0; + @resetVar public var distanceOffset:Float = 0; + @resetVar public var scrollDistance:Float = 0; + @resetVar public var scrollMultiplier:Float = 1; + @:deprecated('noteData is deprecated, use laneIndex instead!') public var noteData(get, set):Int; + @:deprecated('directionOffset is deprecated, use direction instead!') public var directionOffset(get, set):Float; + + function get_noteData():Int { return laneIndex; } + function set_noteData(value:Int):Int { return laneIndex = value; } + function get_directionOffset():Float { return direction; } + function set_directionOffset(alpha:Float):Float { return direction = alpha; } + + public function followLane(lane:Lane) {} + + // modcharting stuff!! + public var scrollPosition:FlxPoint = FlxPoint.get(); + + @resetVar public var customModchart:(note:NoteObject, lane:Lane, distance:Float) -> Void = null; + @resetVar public var customScrollDistance:(note:NoteObject, lane:Lane, timeDiff:Float) -> Float = null; + + public function genericScrollDistance(note:NoteObject, lane:Lane, timeDiff:Float):Float { + var speed:Float = note.scrollMultiplier; + if (lane != null) speed *= lane.scrollSpeed; + + return Note.msToDistance(timeDiff, speed) + note.distanceOffset; + } + public function genericModchart(note:NoteObject, lane:Lane, distance:Float):Void { + var dir:Float = note.direction; + if (lane != null) dir += lane.direction; + var rad:Float = (dir / 180 * Math.PI); + + note.setPosition(note.scrollPosition.x + FlxMath.fastCos(rad) * distance, note.scrollPosition.y + FlxMath.fastSin(rad) * distance); + } +} + +@:build(funkin.macros.FunkinMacro.buildReset(true)) +class Note extends NoteObject { public static var directionNames:Array = ['left', 'down', 'up', 'right']; public static var directionColors:Array> = [ - [FlxColor.fromRGB(194, 75, 153), FlxColor.fromRGB(60, 31, 86)], - [FlxColor.fromRGB(0, 255, 255), FlxColor.fromRGB(21, 66, 183)], - [FlxColor.fromRGB(18, 250, 5), FlxColor.fromRGB(10, 68, 71)], - [FlxColor.fromRGB(249, 57, 63), FlxColor.fromRGB(101, 16, 56)], + [FlxColor.fromRGB(194, 75, 153), FlxColor.WHITE, FlxColor.fromRGB(60, 31, 86)], + [FlxColor.fromRGB(0, 255, 255), FlxColor.WHITE, FlxColor.fromRGB(21, 66, 183)], + [FlxColor.fromRGB(18, 250, 5), FlxColor.WHITE, FlxColor.fromRGB(10, 68, 71)], + [FlxColor.fromRGB(249, 57, 63), FlxColor.WHITE, FlxColor.fromRGB(101, 16, 56)], ]; + public static inline function getColors(data:Int):Array { return directionColors[FlxMath.wrap(data, 0, directionColors.length - 1)]; } + public static inline function getDirection(data:Int):String { return directionNames[FlxMath.wrap(data, 0, directionNames.length - 1)]; } + public var conductorInUse:Conductor; // mostly charting stuff - - public var children:Array = []; - public var parent:Note; - public var tail:Note; - public var lane:Lane; - public var score:Score; - public var consumed:Bool = false; - public var goodHit:Bool = false; - public var lost:Bool = false; - public var noteOffset:FlxPoint; - public var clipDistance:Float = 0; - public var scrollDistance:Float = 0; - public var preventDespawn:Bool = false; - public var followAngle:Bool = true; - public var canHit:Bool = true; - public var hitTime:Float = 0; - public var held:Bool = false; - - public var healthLoss:Float = 6.0 / 100; - public var healthGain:Float = 1.5 / 100; - public var healthGainPerSecond:Float = 7.5 / 100; // hold bonus - public var hitWindow:Float = Scoring.safeFrames * 1000 / 60; - - public var noteKind(default, set):String = ''; - public var scrollMultiplier:Float = 1; - public var directionOffset:Float = 0; - public var hitPriority:Float = 1; - public var multAlpha:Float = 1; - public var player:Bool = false; - public var ignore:Bool = false; - public var noteData:Int = 0; - + public var tail:NoteTail; + public var rgbShader:RGBSwap; + public var arrowPath:ArrowPath; + public var updateRGBShader:Bool = true; + public var rgbEnabled(default, set):Bool; + public var tailOffset(default, null):FlxPoint; + + public var chartNote(default, set):ChartNote; + public var strumlineIndex:Int = 0; + + @resetVar public var preventDespawn:Bool = false; + @resetVar public var consumed:Bool = false; + @resetVar public var goodHit:Bool = false; + @resetVar public var lost:Bool = false; + @resetVar public var canHit:Bool = true; + + @resetVar public var followVisible:Bool = true; + @resetVar public var followAlpha:Bool = true; + @resetVar public var followAngle:Bool = true; + @resetVar public var followScale:Bool = true; + + @resetVar public var score:Score = null; + @resetVar public var held:Bool = false; + @resetVar public var hitTime:Float = -1; + @resetVar public var holdTime:Float = -1; + + @resetVar public var healthLoss:Float = 6.0 / 100; + @resetVar public var healthGain:Float = 1.5 / 100; + @resetVar public var healthGainPerSecond:Float = 7.5 / 100; // hold bonus + @resetVar public var hitWindow:Float = (ScoreSystem.safeFrames * 1000 / 60); + + @resetVar public var hitPriority:Float = 1; + @resetVar public var multAlpha:Float = 1; + @resetVar public var ignore:Bool = false; + + public var isHoldTail(default, null):Bool = false; + + public var style(default, set):NoteStyle; + public var kind(default, set):String = ''; + @:deprecated('noteKind is deprecated, use kind instead!') public var noteKind(get, set):String; + @:deprecated('player is deprecated, use strumlineIndex instead!') public var player(get, never):Bool; + public var endMs(get, never):Float; public var endBeat(get, never):Float; public var msTime(default, set):Float = 0; public var beatTime(default, set):Float = 0; public var msLength(default, set):Float = 0; public var beatLength(default, set):Float = 0; + public var isHoldNote(default, null):Bool = false; - public var isHoldPiece:Bool = false; - public var isHoldTail:Bool = false; + function get_player():Bool { return (strumlineIndex == 0); } + function set_noteKind(newKind:String):String { return kind = newKind; } + function get_noteKind():String { return kind; } - public override function destroy() { - for (child in children) { - lane?.dequeueNote(child); - lane?.killNote(child); - child.destroy(); - } - lane?.dequeueNote(this); - lane?.killNote(this); + var renderingTail:Bool = false; + var forceDraw:Bool = false; + + public override function destroy():Void { + tailOffset.put(); + tail?.destroy(); + tail = null; + super.destroy(); } - public override function revive() { - hitTime = 0; - held = false; - lost = false; - goodHit = false; - consumed = false; - clipDistance = 0; - if (!isHoldPiece) { - canHit = true; - for (child in children) - child.canHit = false; - } + public override function update(elapsed:Float):Void { + super.update(elapsed); + tail?.update(elapsed); + } + public override function draw():Void { + if (isHoldNote && tail != null) + tail.draw(); + + if (!goodHit || forceDraw) + super.draw(); + } + public override function kill():Void { + super.kill(); + tail?.kill(); + } + public override function revive():Void { super.revive(); + tail?.revive(); } - public function new(player:Bool, msTime:Float, noteData:Int, msLength:Float = 0, type:String = '', isHoldPiece:Bool = false, ?conductor:Conductor) { + public function new(songNote:ChartNote, ?conductor:Conductor) { super(); + rgbShader = new RGBSwap(); + shader = rgbShader.shader; + this.conductorInUse = conductor ?? FunkinState.getCurrentConductor(); - - this.player = player; - this.msTime = msTime; - this.noteKind = type; - this.noteData = noteData; - this.msLength = Math.max(msLength, 0); + this.tailOffset = FlxPoint.get(); - this.isHoldPiece = isHoldPiece; - this.isHoldTail = (isHoldPiece && msLength <= 0); - noteOffset = FlxPoint.get(); + this.chartNote = songNote; + } + public function set_chartNote(songNote:ChartNote):ChartNote { + chartNote = songNote; - if (isHoldPiece) this.multAlpha = .6; + if (songNote != null) { + this.kind = songNote.kind; + this.msTime = songNote.msTime; + this.laneIndex = songNote.laneIndex; + this.strumlineIndex = songNote.strumlineIndex; + this.msLength = Math.max(songNote.msLength, 0); + + this.extraData.clear(); + if (songNote.extraData != null) { + for (k => v in songNote.extraData) + setVar(k, v); + } + + if (tail != null) + tail.chartNote = songNote; + } - loadAtlas('notes'); - reloadAnimations(); + return songNote; } - - public function reloadAnimations() { - animation.destroyAnimations(); - var dirName:String = directionNames[noteData]; - addAnimation('hit', '$dirName note', 24, false); - playAnimation('hit', true); - if (isHoldPiece) { - addAnimation('tail', '$dirName hold tail', 24, false); - addAnimation('hold', '$dirName hold piece', 24, false); - playAnimation(this.isHoldTail ? 'tail' : 'hold', true); + public function updateChartNote():Void { + chartNote.kind = kind; + chartNote.msTime = msTime; + chartNote.msLength = msLength; + chartNote.laneIndex = laneIndex; + chartNote.strumlineIndex = strumlineIndex; + } + + public function reload(?style:NoteStyle, ?lane:Lane):Void { + resetVars(); + + spriteOffset.set(); + tailOffset.set(); + blend = NORMAL; + + this.style = style; + + if (tail != null) { + tail.reload(style); + + var tailScaleMult:Float = tail.defaultScale / tail.defaultScale; + tail.scale.set(scale.x * tailScaleMult, scale.y * tailScaleMult); + + if (lane != null) + tail.renderDistance = Note.msToDistance(lane.spawnRadius, lane.scrollSpeed); } - updateHitbox(); } - public function toChartNote():funkin.backend.play.Chart.ChartNote { - return {laneIndex: noteData, msTime: msTime, kind: noteKind, msLength: msLength, player: player}; + public function updateTail():Void { + isHoldNote = (msLength > 0); + if (isHoldNote) { + if (tail == null) { + tail = new NoteTail(this); + } else { + tail.reloadNote(this); + } + } + } + public function toChartNote():ChartNote { + return chartNote ?? {laneIndex: laneIndex, msTime: msTime, kind: kind, msLength: msLength, strumlineIndex: strumlineIndex}; } - public function set_noteKind(newKind:String) { - return noteKind = newKind; + function set_kind(newKind:String) { + return kind = newKind; } - public function set_msTime(newTime:Float) { + function set_msTime(newTime:Float) { if (msTime == newTime) return newTime; - @:bypassAccessor beatTime = conductorInUse.metronome.convertMeasure(newTime, MS, BEAT); + @:bypassAccessor beatTime = conductorInUse.convertMeasure(newTime, MS, BEAT); return msTime = newTime; } - public function set_beatTime(newTime:Float) { + function set_beatTime(newTime:Float) { if (beatTime == newTime) return newTime; - @:bypassAccessor msTime = conductorInUse.metronome.convertMeasure(newTime, BEAT, MS); + @:bypassAccessor msTime = conductorInUse.convertMeasure(newTime, BEAT, MS); return beatTime = newTime; } - public function set_msLength(newLength:Float) { + function set_msLength(newLength:Float) { if (msLength == newLength) return newLength; - @:bypassAccessor beatLength = conductorInUse.metronome.convertMeasure(msTime + newLength, MS, BEAT) - beatTime; - return msLength = newLength; + msLength = newLength; + @:bypassAccessor beatLength = conductorInUse.convertMeasure(msTime + newLength, MS, BEAT) - beatTime; + updateTail(); + return newLength; } - public function set_beatLength(newLength:Float) { + function set_beatLength(newLength:Float) { if (beatLength == newLength) return newLength; - @:bypassAccessor msLength = conductorInUse.metronome.convertMeasure(beatTime + newLength, BEAT, MS) - msTime; - return beatLength = newLength; + beatLength = newLength; + @:bypassAccessor msLength = conductorInUse.convertMeasure(beatTime + newLength, BEAT, MS) - msTime; + updateTail(); + return newLength; + } + function get_endMs() + return msTime + msLength; + function get_endBeat() + return beatTime + beatLength; + + function set_style(newStyle:NoteStyle) { + if (style == newStyle) return newStyle; + + style = newStyle; + loadStyle(newStyle); + if (tail != null) + tail.style = newStyle; + + return newStyle; + } + public function set_rgbEnabled(newE:Bool) { + shader = (newE ? rgbShader.shader : null); + return rgbEnabled = newE; + } + public function loadStyle(newStyle:NoteStyleAsset) { + var oldScale:Float = defaultScale; + var style:NoteStyle = NoteStyle.fetch(newStyle); + var asset:NoteStyleAssetData = style?.data.notes; + + NoteStyleUtil.loadNoteStyleAnimations(this, asset, style?.getDirectionName(laneIndex)); + updateRGBShader = !(style?.data.general.disableRGB ?? false); + defaultScale = asset?.scale ?? 1; + defaultAlpha = asset?.alpha ?? 1; + scale.x *= (defaultScale / oldScale); + scale.y *= (defaultScale / oldScale); + + reloadAnimShader('hit', newStyle); + playAnimation('hit', true); + updateHitbox(); } - public function get_endMs() - return msTime + (isHoldPiece ? msLength : 0); - public function get_endBeat() - return beatTime + (isHoldPiece ? beatLength : 0); public static function distanceToMS(distance:Float, scrollSpeed:Float) return distance / (.45 * scrollSpeed); public static function msToDistance(ms:Float, scrollSpeed:Float) return ms * (.45 * scrollSpeed); - public dynamic function followLane(lane:Lane, scrollSpeed:Float) { - var receptor:Receptor = lane.receptor; - var speed:Float = scrollSpeed * scrollMultiplier; - var dir:Float = lane.direction + directionOffset; - - var holdOffsetX:Float = 0; - var holdOffsetY:Float = 0; - - scrollDistance = Note.msToDistance(msTime - conductorInUse.songPosition, speed); - if (isHoldPiece) { - if (isHoldTail) { - scale.y = scale.x; updateHitbox(); - scrollDistance -= height; - } else { - scrollDistance -= scale.y; + public override function followLane(lane:Lane) { + var timeDiff:Float = msTime - conductorInUse.songPosition; + var receptor:Receptor = lane?.receptor; + + copyReceptor(receptor); + + try { + scrollDistance = (customScrollDistance ?? arrowPath?.customScrollDistance ?? genericScrollDistance)(this, lane, timeDiff); + if (updateModchart) + (customModchart ?? arrowPath?.customModchart ?? genericModchart)(this, lane, scrollDistance); + } catch (e:haxe.Exception) { + Log.error('error on note modchart function -> ${e.details()}'); + + customModchart = null; + customScrollDistance = null; + + scrollDistance = genericScrollDistance(this, lane, timeDiff); + if (updateModchart) + genericModchart(this, lane, scrollDistance); + } + + if (isHoldNote && tail != null) { + tail.goodHit = goodHit; + tail.followLane(lane); + tail.updateHitbox(); + } + } + public function copyReceptor(receptor:Receptor) { + if (receptor == null) return; + + if (followScale) { // maybe this is too many variables ... + copyReceptorScale(receptor); + updateHitbox(); + } + if (followReceptor) + scrollPosition.set(receptor.x + (receptor.width - width) * .5, receptor.y + (receptor.height - height) * .5); + if (followAlpha) + alpha = receptor.alpha * multAlpha * defaultAlpha; + if (followVisible) + visible = receptor.visible; + if (followAngle) + angle = receptor.angle; + } + public function copyReceptorScale(receptor:Receptor) { + if (receptor == null) return; + + var scaleMult:Float = defaultScale / receptor.defaultScale; + scale.set(receptor.scale.x * scaleMult, receptor.scale.y * scaleMult); + } + + public override function playAnimation(anim:String, forced:Bool = false, reversed:Bool = false, frame:Int = 0) { + var overrideAnim:String = '$anim-$laneIndex'; + if (animationExists(overrideAnim)) + anim = overrideAnim; + + if (forced || this.anim.name != anim) + reloadAnimShader(anim, style); + + super.playAnimation(anim, forced, reversed, frame); + } + public function reloadAnimShader(anim:String, style:NoteStyle) { + if (updateRGBShader) { + var animData:NoteStyleAnimData = style?.getAssetAnimation(style?.data.notes, anim); + if (animData != null) { + if (animData.disableRGB) { + rgbEnabled = false; + } else { + var colors:Array = style.getDirectionColorMod(laneIndex, animData.colorMod); + rgbShader.set(colors[0], colors[1], colors[2]); + rgbEnabled = true; + } } - setOffset(); - origin.set(frameWidth * .5); - holdOffsetX = (receptor.width - frameWidth) * .5; - holdOffsetY = receptor.height * .5; - angle = dir - 90; - } else if (followAngle) { - angle = lane.receptor.angle; - } - - var xP:Float = 0; - var yP:Float = scrollDistance; - var rad:Float = dir / 180 * Math.PI; - x = receptor.x + noteOffset.x + Math.sin(rad) * xP + Math.cos(rad) * yP + holdOffsetX; - y = receptor.y + noteOffset.y + Math.sin(rad) * yP + Math.cos(rad) * xP + holdOffsetY; - alpha = lane.alpha * receptor.alpha * multAlpha; - - if (isHoldPiece) { //handle in DISTANCE to support scroll direction - if (canHit && lane.held) - clipDistance = Math.max(0, -scrollDistance); + } + } +} - var cropTop:Float = 0; - var cropBottom:Float = frameHeight; - var cropY:Float = clipDistance / scale.y; - var cropHeight:Float = frameHeight; - if (!isHoldTail) { - final holdDist:Float = Note.msToDistance(msLength, scrollSpeed); - cropTop ++; - cropHeight --; - scale.y = holdDist / (cropHeight - cropTop); - tail = parent?.tail; - if (tail != null) - cropBottom += Math.min(0, (Note.msToDistance(tail.msTime - msTime, scrollSpeed) - tail.height) / scale.y - (cropHeight - cropTop)); - // if anyone can help me figure out how to make it clip exactly to the tail id appreciate it +class NoteTail extends Note { + public var parent(default, set):Note; + + public var followParent:Bool = true; + public var renderDistance:Null = null; + + public var renderTriangles:Bool = true; // TODO + public var clipToDistance:Null = null; + public var adaptiveDirection:Bool = true; + + public var holdScale(default, null):FlxPoint; + public var tailScale(default, null):FlxPoint; + + var holdStrip:NoteTailStrip; + var tailStrip:NoteTailStrip; + + var drawData:Array = []; + var minBodyHeight:Float = 35; + var drawItems:Int = 0; + var zebra:Bool = false; // debug + + public function new(parent:Note) { + super(parent?.chartNote); + + rgbShader = new RGBSwap(); + shader = rgbShader.shader; + + holdScale = FlxPoint.get(1, 1); + tailScale = FlxPoint.get(1, 1); + + holdStrip = new NoteTailStrip(this, 'hold'); + tailStrip = new NoteTailStrip(this, 'tail'); + + this.isHoldTail = true; + this.forceDraw = true; + this.parent = parent; + } + public override function destroy() { + holdStrip?.destroy(); + tailStrip?.destroy(); + + holdScale.put(); + tailScale.put(); + + drawData = null; + drawItems = 0; + + super.destroy(); + } + + function set_parent(note:Note):Note { + if (note != null) + reloadNote(note); + return parent = note; + } + + public function reloadNote(note:Note):Void { + conductorInUse = note.conductorInUse ?? FunkinState.getCurrentConductor(); + chartNote = note.chartNote; + laneIndex = note.laneIndex; + style = note.style; + + holdStrip?.reloadTail(this); + tailStrip?.reloadTail(this); + } + public override function reload(?style:NoteStyle, ?lane:Lane):Void { + super.reload(style, lane); + + clipToDistance = null; + } + public override function updateTail():Void {} + public override function update(elapsed:Float):Void { + super.update(elapsed); + holdStrip?.update(elapsed); + tailStrip?.update(elapsed); + } + public override function draw():Void { + holdStrip?.copyNote(this); + tailStrip?.copyNote(this); + + for (camera in getCamerasLegacy()) { + if (camera.visible && camera.exists) + drawComplex(camera); + } + } + public override function drawComplex(camera:FlxCamera):Void { + updateShader(camera); + + if (renderTriangles) { + for (i in 0 ... drawItems) { + var data:NoteTailDrawData = drawData[i]; + var strip:NoteTailStrip = data?.strip; + if (strip == null) break; + + if (zebra) + strip.color = (i % 2 == 0 ? FlxColor.GRAY : FlxColor.WHITE); + strip.updateRender(data); + strip.drawToCamera(camera); + } + } + } + + public override function loadStyle(newStyle:NoteStyleAsset) { + var oldScale:Float = defaultScale; + var style:NoteStyle = NoteStyle.fetch(newStyle); + var asset:NoteStyleAssetData = style?.data.holds; + + defaultScale = asset?.scale ?? 1; + defaultAlpha = asset?.alpha ?? 1; + scale.x *= (defaultScale / oldScale); + scale.y *= (defaultScale / oldScale); + + holdStrip?.loadStyle(style); + tailStrip?.loadStyle(style); + } + + public override function followLane(lane:Lane):Void { + var receptor:Receptor = lane.receptor; + + copyNote(parent); + copyReceptor(receptor); + + copyValues(); + if (renderTriangles) { + updateTriangles(lane); + } else { + // TODO: basic tile renderer (again)? + } + } + public function copyNote(note:Note) { + if (followParent && parent != null) { + direction = parent.direction; + followAlpha = parent.followAlpha; + followVisible = parent.followVisible; + scrollMultiplier = parent.scrollMultiplier; + + scale.copyFrom(parent.scale); + } + } + public override function copyReceptor(receptor:Receptor) { + if (receptor == null) return; + + if (followScale) { + copyReceptorScale(receptor); + updateHitbox(); + } + if (followReceptor) + scrollPosition.set(receptor.x + receptor.width * .5, receptor.y + receptor.height * .5); + if (followAlpha) + alpha = receptor.alpha * parent.multAlpha * multAlpha * defaultAlpha; + if (followVisible) + visible = (parent.visible && receptor.visible); + if (followAngle) + angle = direction + parent.direction + receptor.lane?.direction ?? 0 + 90; + } + public function copyValues():Void { + alpha = multAlpha * defaultAlpha; + + if (parent != null) { + scrollFactor.copyFrom(parent.scrollFactor); + cameras = parent.cameras; + shader = parent.shader; + alpha *= parent.alpha; + color = parent.color; + } + } + public function updateTriangles(lane:Lane):Void { + if (!updateModchart) return; + + var render:NoteTailStrip = tailStrip; + renderingTail = true; + + var distFunc = (customScrollDistance ?? parent?.customScrollDistance ?? parent?.arrowPath?.customScrollDistance ?? genericScrollDistance); + var modchartFunc = (customModchart ?? parent?.customModchart ?? parent?.arrowPath?.customModchart ?? genericModchart); + var timeDiff:Float = endMs - conductorInUse.songPosition; + var receptor:Receptor = lane.receptor; + + var scrollDistance:Float = 0; + try { + scrollDistance = distFunc(this, lane, timeDiff); + modchartFunc(this, lane, scrollDistance); + } catch (e:haxe.Exception) { + Log.error('error on note modchart function -> ${e.details()}'); + + customModchart = null; + customScrollDistance = null; + modchartFunc = genericModchart; + distFunc = genericScrollDistance; + + scrollDistance = genericScrollDistance(this, lane, timeDiff); + genericModchart(this, lane, timeDiff); + } + + if (goodHit) clipToDistance = distFunc(this, lane, 0); + + var clipDistance:Float = distFunc(this, lane, msTime - conductorInUse.songPosition); + if (clipToDistance != null) clipDistance = Math.max(clipDistance, clipToDistance); + + + drawItems = 0; + var defaultAngle:Float = angle; + var prevAngle:Null = null; + var scaleY:Float = (lane?.scale.y ?? scale.y); + + while (scrollDistance > clipDistance) { + var absSY:Float = Math.abs(scale.y); + var height:Float = render.frameHeight * Math.max(absSY, renderingTail ? absSY : scaleY); // thats crazy bro + if (!renderingTail && height < minBodyHeight) height = minBodyHeight; + + var prevScale:FlxPoint = FlxPoint.weak(scale.x, scale.y); + var prevPosition:FlxPoint = FlxPoint.weak(x, y); + var prevColor:FlxColor = color; + + scrollDistance -= height; + modchartFunc(this, lane, scrollDistance); + + if (renderDistance == null || scrollDistance < renderDistance) { + var curPosition:FlxPoint = FlxPoint.weak(x, y); + var size:Float; + + if (adaptiveDirection) { + angle = (prevPosition.degreesTo(curPosition) + 180); + size = curPosition.distanceTo(prevPosition); + prevAngle ??= angle; + } else { + size = height; + prevAngle ??= defaultAngle; + } + + + var data:NoteTailDrawData = (drawData[drawItems] ?? new NoteTailDrawData()); + + data.clip = (scrollDistance <= clipDistance ? Math.abs(scrollDistance - clipDistance) / size : 0); + data.copyPosition(prevPosition, curPosition); + data.setScale(prevScale.x, scale.x); + data.setAngle(prevAngle, angle); + data.setCT(colorTransform); + data.strip = render; + + drawData[drawItems ++] = data; + + + prevAngle = angle; } - clipRect ??= new FlxRect(); - clipto(Math.max(cropTop, cropY), Math.min(cropHeight, cropBottom)); + + renderingTail = false; + render = holdStrip; + } + } +} + +class NoteTailStrip extends FunkinStrip { + public var defaultAnim:String; + public var style(default, set):NoteStyle; + public var parentTail(default, set):NoteTail; + + var topCoordOffset:Int = 0; + var topUV:Float; + + public var laneIndex:Int; + + public function new(parent:NoteTail, defaultAnim:String = 'hold') { + super(); + + this.parentTail = parent; + this.defaultAnim = defaultAnim; + + indices = new DrawData(6, true, [0, 1, 2, 1, 2, 3]); + uvtData = new DrawData(8, true, [for (i in 0 ... 8) 0]); + vertices = new DrawData(8, true, [for (i in 0 ... 8) 0]); + } + + override function set_frame(newFrame:FlxFrame):FlxFrame { + super.set_frame(newFrame); + + if (dirty) + prepareRender(); + + return newFrame; + } + public function prepareRender():Void { // we don't need to update those every call... + if (graphic == null || frame == null) + return; + + var w:Float = graphic.width; + var h:Float = graphic.height; + + var crop:Float = (antialiasing ? .5 : 0); // gets rid of blurry edges + var leftUV:Float, highUV:Float, rightUV:Float, bottomUV:Float; + + leftUV = (frame.frame.x / w); + highUV = (frame.frame.y / h); + rightUV = (leftUV + frame.frame.width / w); + bottomUV = (highUV + frame.frame.height / h); + + switch (frame.angle) { + case ANGLE_NEG_90: + topCoordOffset = 0; + uvtData[1] = uvtData[5] = highUV; // left corners uv + uvtData[3] = uvtData[7] = bottomUV; // right corners uv + uvtData[4] = uvtData[6] = (leftUV + crop / w); // bottom corners uv + topUV = (rightUV - crop / w); // top corners uv + case ANGLE_90: + topCoordOffset = 0; + uvtData[1] = uvtData[5] = bottomUV; // left corners uv + uvtData[3] = uvtData[7] = highUV; // right corners uv + uvtData[4] = uvtData[6] = (rightUV - crop / w); // bottom corners uv + topUV = (leftUV + crop / w); // top corners uv + default: + topCoordOffset = 1; + uvtData[0] = uvtData[4] = leftUV; // left corners uv + uvtData[2] = uvtData[6] = rightUV; // right corners uv + uvtData[5] = uvtData[7] = (bottomUV - crop / h); // bottom corners uv + topUV = (highUV + crop / h); // top corners uv + } // +90 untested but SHOULD work ?? + } + public function updateRender(drawData:NoteTailDrawData):Void { + if (graphic == null) return; + + var sprXOffset:Float = -(spriteOffset.x + animOffset.x); + var sprYOffset:Float = -(spriteOffset.y + animOffset.y); + var nextXOffset:Float = FlxMath.lerp(drawData.xFrom, drawData.xTo, (1 - drawData.clip)); + var nextYOffset:Float = FlxMath.lerp(drawData.yFrom, drawData.yTo, (1 - drawData.clip)); + + // update vertices + var xOffset:Float = (sprXOffset * drawData.scaleFrom); + var xOffsetTo:Float = (sprXOffset * drawData.scaleTo); + var yOffset:Float = (sprYOffset * drawData.scaleFrom); + var yOffsetTo:Float = (sprYOffset * drawData.scaleTo); + + var width:Float = (frameWidth * .5 * drawData.scaleTo); + var sin:Float = (FlxMath.fastSin(drawData.angleTo) * width); + var cos:Float = (FlxMath.fastCos(drawData.angleTo) * width); + + vertices[0] = (-sin + nextXOffset + xOffsetTo); // top left + vertices[1] = (cos + nextYOffset + yOffsetTo); + vertices[2] = (sin + nextXOffset + xOffsetTo); // top right + vertices[3] = (-cos + nextYOffset + yOffsetTo); + uvtData[0 + topCoordOffset] = uvtData[2 + topCoordOffset] = FlxMath.lerp(topUV, uvtData[4 + topCoordOffset], drawData.clip); // top corners uv (for clipping) + + width = (frameWidth * .5 * drawData.scaleFrom); + sin = (FlxMath.fastSin(drawData.angleFrom) * width); + cos = (FlxMath.fastCos(drawData.angleFrom) * width); + + vertices[4] = (-sin + xOffset + drawData.xFrom); // bottom left + vertices[5] = (cos + yOffset + drawData.yFrom); + vertices[6] = (sin + xOffset + drawData.xFrom); // bottom right + vertices[7] = (-cos + yOffset + drawData.yFrom); + + // colors[0] = colors[1] = (gradient ? drawData.colorFrom : drawData.colorTo); + // colors[2] = colors[3] = drawData.colorTo; + + if (drawData.ct != null) { + var ct:ColorTransform = drawData.ct; + colorTransform.setOffsets(ct.redOffset, ct.greenOffset, ct.blueOffset, ct.alphaOffset); + colorTransform.setMultipliers(ct.redMultiplier, ct.greenMultiplier, ct.blueMultiplier, ct.alphaMultiplier); + } + } + public inline function copyNote(note:Note):Void { + scrollFactor.copyFrom(note.scrollFactor); + initialZoom = note.initialZoom; + zoomFactor = note.zoomFactor; + shader = note.shader; + } + + function set_style(newStyle:NoteStyle) { + if (style == newStyle) return newStyle; + style = newStyle; + loadStyle(newStyle); + return newStyle; + } + function set_parentTail(note:NoteTail):NoteTail { + reloadTail(note); + return parentTail = note; + } + public function reloadTail(note:NoteTail):Void { + scale.copyFrom(note.scale); + laneIndex = note.laneIndex; + style = note.style; + } + public function loadStyle(newStyle:NoteStyleAsset) { + var style:NoteStyle = NoteStyle.fetch(newStyle); + var asset:NoteStyleAssetData = style?.data.holds; + + NoteStyleUtil.loadNoteStyleAnimations(this, asset, style?.getDirectionName(laneIndex)); + + var overrideAnim:String = '$defaultAnim-$laneIndex'; + if (animationExists(overrideAnim)) { + playAnimation(overrideAnim, true); + } else { + playAnimation(defaultAnim, true); + } + offset.set(); + } +} - clipRect = clipRect; //refresh clip rect +class NoteTailDrawData { + public var scaleFrom:Float = 1; + public var angleFrom:Float = 0; + public var xFrom:Float = 0; + public var yFrom:Float = 0; + + public var scaleTo:Float = 1; + public var angleTo:Float = 0; + public var xTo:Float = 0; + public var yTo:Float = 0; + + public var ct:ColorTransform = null; + + public var clip:Float = 0; + + public var strip:Dynamic; + + var TO_RAD:Float = (1 / 180 * Math.PI); + + public function new() {} + + public inline function setAngle(from:Float, ?to:Float):Void { + angleFrom = from * TO_RAD; + angleTo = (to ?? from) * TO_RAD; + } + public inline function setScale(from:Float, ?to:Float):Void { + scaleFrom = from; + scaleTo = to ?? from; + } + public inline function setCT(copy:ColorTransform):Void { + ct ??= new ColorTransform(); + ct.setOffsets(copy.redOffset, copy.greenOffset, copy.blueOffset, copy.alphaOffset); + ct.setMultipliers(copy.redMultiplier, copy.greenMultiplier, copy.blueMultiplier, copy.alphaMultiplier); + } + public inline function setPosition(x:Float, y:Float, ?xT:Float, ?yT:Float):Void { + xFrom = x; + yFrom = y; + if (xT != null) xTo = xT; + if (yT != null) yTo = yT; + } + public inline function copyPosition(point:FlxPoint, ?pointTo:FlxPoint):Void { + xFrom = point.x; + yFrom = point.y; + if (pointTo != null) { + xTo = pointTo.x; + yTo = pointTo.y; } } - inline function clipto(ya:Float = 0, yb:Float = 0) - clipRect.set(0, ya, frameWidth, yb - ya); } \ No newline at end of file diff --git a/source/funkin/objects/play/Strumline.hx b/source/funkin/objects/play/Strumline.hx index 14a1a03..2e4e704 100644 --- a/source/funkin/objects/play/Strumline.hx +++ b/source/funkin/objects/play/Strumline.hx @@ -1,7 +1,10 @@ package funkin.objects.play; -import funkin.backend.play.Scoring; +import funkin.objects.Character; +import funkin.objects.play.Note; import funkin.backend.play.NoteEvent; +import funkin.backend.play.NoteStyle; +import funkin.backend.play.ScoreSystem; import flixel.util.FlxAxes; import flixel.input.keyboard.FlxKey; @@ -24,137 +27,182 @@ class Strumline extends FunkinSpriteGroup { public var cpu(default, set):Bool; // todo: macro..? public var laneCount(default, set):Int; public var direction(default, set):Float; + public var style(default, set):NoteStyle; public var scrollSpeed(default, set):Float; public var oneWay(default, set):Bool = true; public var allowInput(default, set):Bool = true; - public var hitWindow(default, set):Float = Scoring.safeFrames / 60 * 1000; + public var character(default, set):ICharacter = null; + public var noteClass(default, set):Class = Note; + public var hitWindow(default, set):Float = (ScoreSystem.safeFrames * 1000 / 60); //oh dear - public function set_cpu(isCpu:Bool) { for (lane in lanes) lane.cpu = isCpu; return cpu = isCpu; } - public function set_oneWay(isOneWay:Bool) { for (lane in lanes) lane.oneWay = isOneWay; return oneWay = isOneWay; } - public function set_direction(newDir:Float) { for (lane in lanes) lane.direction = newDir; return direction = newDir; } - public function set_hitWindow(newWindow:Float) { for (lane in lanes) lane.hitWindow = newWindow; return hitWindow = newWindow; } - public function set_allowInput(isAllowed:Bool) { for (lane in lanes) lane.allowInput = isAllowed; return allowInput = isAllowed; } - public function set_scrollSpeed(newSpeed:Float) { for (lane in lanes) lane.scrollSpeed = newSpeed; return scrollSpeed = newSpeed; } - public function set_laneSpacing(newSpacing:Float) { - var i:Int = 0; - var diff:Float = newSpacing - laneSpacing; - for (lane in lanes) { - lane.x += i * diff; - i ++; - } + function set_cpu(isCpu:Bool) { for (lane in lanes) lane.cpu = isCpu; return cpu = isCpu; } + function set_oneWay(isOneWay:Bool) { for (lane in lanes) lane.oneWay = isOneWay; return oneWay = isOneWay; } + function set_style(newStyle:NoteStyle) { if (style == newStyle) return newStyle; loadStyle(newStyle); return style = newStyle; } + function set_direction(newDir:Float) { for (lane in lanes) lane.direction = newDir; return direction = newDir; } + function set_hitWindow(newWindow:Float) { for (lane in lanes) lane.hitWindow = newWindow; return hitWindow = newWindow; } + function set_allowInput(isAllowed:Bool) { for (lane in lanes) lane.allowInput = isAllowed; return allowInput = isAllowed; } + function set_character(newChara:ICharacter) { for (lane in lanes) lane.character = newChara; return character = newChara; } + function set_noteClass(newClass:Class) { for (lane in lanes) lane.noteClass = newClass; return noteClass = newClass; } + function set_scrollSpeed(newSpeed:Float) { for (lane in lanes) lane.scrollSpeed = newSpeed; return scrollSpeed = newSpeed; } + function set_laneSpacing(newSpacing:Float) { + recalculateLaneSpacing(newSpacing, laneSpacing); return laneSpacing = newSpacing; } - public function set_laneCount(newCount:Int) { + function set_laneCount(newCount:Int) { while (lanes.length > 0 && lanes.length > newCount) { - var lane = lanes.members[lanes.length - 1]; - lanes.remove(lane, true); + var lane:Lane = lanes.members.shift(); lane.destroy(); } for (i in laneCount...newCount) { - var lane:Lane = new Lane(i * laneSpacing * scale.x, 0, i); + var lane:Lane = new Lane(i * laneSpacing * scale.x, 0, i, direction, scrollSpeed, style); + + lane.allowInput = allowInput; + lane.noteClass = noteClass; + lane.hitWindow = hitWindow; + lane.character = character; lane.strumline = this; lane.selfDraw = false; + lane.oneWay = oneWay; + lane.cpu = cpu; + + lane.scale.copyFrom(scale); lanes.add(lane); } return laneCount = newCount; } - //getters - public function get_leftBound() { - var minX:Float = Math.POSITIVE_INFINITY; - for (lane in lanes) minX = Math.min(minX, lane.receptor.x); - return minX; - } - public function get_rightBound() { - var maxX:Float = Math.NEGATIVE_INFINITY; - for (lane in lanes) maxX = Math.max(maxX, lane.receptor.x + lane.receptor.width); - return maxX; - } - public function get_topBound() { - var minY:Float = Math.POSITIVE_INFINITY; - for (lane in lanes) minY = Math.min(minY, lane.receptor.y); - return minY; - } - public function get_bottomBound() { - var maxY:Float = Math.NEGATIVE_INFINITY; - for (lane in lanes) maxY = Math.max(maxY, lane.receptor.y + lane.receptor.height); - return maxY; - } - public function get_strumlineWidth() { - var minX:Float = Math.POSITIVE_INFINITY; - var maxX:Float = Math.NEGATIVE_INFINITY; + //more getters + function get_leftBound() { return findMinXHelper(); } + function get_rightBound() { return findMaxXHelper(); } + function get_topBound() { return findMinYHelper(); } + function get_bottomBound() { return findMaxYHelper(); } + function get_strumlineWidth() { return width; } + function get_strumlineHeight() { return height; } + + override function findMinX():Float { return (lanes.length > 0 ? findMinXHelper() : x); } + override function findMaxX():Float { return (lanes.length > 0 ? findMaxXHelper() : x); } + override function findMinY():Float { return (lanes.length > 0 ? findMinYHelper() : y); } + override function findMaxY():Float { return (lanes.length > 0 ? findMaxYHelper() : y); } + override function findMinXHelper():Float { + var value:Float = Math.POSITIVE_INFINITY; for (lane in lanes) { - minX = Math.min(minX, lane.receptor.x); - maxX = Math.max(maxX, lane.receptor.x + lane.receptor.width); + var minX:Float = lane.receptor.x; + if (minX < value) value = minX; } - return (maxX - minX); + return value; } - public function get_strumlineHeight() { - var minY:Float = Math.POSITIVE_INFINITY; - var maxY:Float = Math.NEGATIVE_INFINITY; + override function findMaxXHelper():Float { + var value:Float = Math.NEGATIVE_INFINITY; for (lane in lanes) { - minY = Math.min(minY, lane.receptor.y); - maxY = Math.max(maxY, lane.receptor.y + lane.receptor.height); + var maxX:Float = lane.receptor.x + lane.receptor.width; + if (maxX > value) value = maxX; } - return (maxY - minY); + return value; } - public function get_receptorWidth() { + override function findMinYHelper():Float { + var value:Float = Math.POSITIVE_INFINITY; + for (lane in lanes) { + var minY:Float = lane.receptor.y; + if (minY < value) value = minY; + } + return value; + } + override function findMaxYHelper():Float { + var value:Float = Math.NEGATIVE_INFINITY; + for (lane in lanes) { + var maxY:Float = lane.receptor.y + lane.receptor.height; + if (maxY > value) value = maxY; + } + return value; + } + + function get_receptorWidth() { var width:Float = 0; for (lane in lanes) width = Math.max(width, lane.receptor.width); return width; } - public function get_receptorHeight() { + function get_receptorHeight() { var height:Float = 0; for (lane in lanes) height = Math.max(height, lane.receptor.height); return height; } - public override function get_width() return strumlineWidth; - public override function get_height() return strumlineHeight; - public function new(laneCount:Int = 4, direction:Float = 90, scrollSpeed:Float = 1) { + public function new(laneCount:Int = 4, direction:Float = 90, scrollSpeed:Float = 1, ?style:NoteStyleAsset = 'funkin', ?noteClass:Class) { super(); this.lanes = new FunkinTypedSpriteGroup(); this.add(lanes); + this.allowInput = true; - this.laneCount = laneCount; this.direction = direction; this.scrollSpeed = scrollSpeed; + this.noteClass = noteClass ?? Note; + + this.laneCount = laneCount; + + this.style = NoteStyle.fetch(style); + } + public function loadStyle(newStyle:NoteStyleAsset) { + var style:NoteStyle = NoteStyle.fetch(newStyle); + + laneSpacing = (style?.data.general.laneSpacing ?? laneSpacing); + + for (lane in lanes) + lane.style = style; + } + public function recalculateLaneSpacing(newSpacing:Float, oldSpacing:Float) { + var i:Int = 0; + var diff:Float = newSpacing - oldSpacing; + for (lane in lanes) { + lane.startX += i * diff * scale.x; + lane.x += i * diff * scale.x; + i ++; + } + } + public function resetLanePositions():Void { + for (i => lane in lanes) { + lane.setPosition(x + i * laneSpacing * scale.x, y); + lane.startX = lane.x; + lane.startY = lane.y; + } } public function fadeIn() { var i:Int = 0; - var targetY:Float = y; for (lane in lanes) { lane.alpha = 0; - var targetX:Float = x + i * laneSpacing; var rad:Float = lane.direction / 180 * Math.PI; FlxTween.cancelTweensOf(lane); - lane.x = targetX - Math.cos(rad) * 10; - lane.y = targetY - Math.sin(rad) * 10; - FlxTween.tween(lane, {x: targetX, y: targetY, alpha: alpha}, 1, {ease: FlxEase.circOut, startDelay: .5 + i * .2}); + lane.x = lane.startX - Math.cos(rad) * 10; + lane.y = lane.startY - Math.sin(rad) * 10; + FlxTween.tween(lane, {x: lane.startX, y: lane.startY, alpha: alpha}, 1, {ease: FlxEase.circOut, startDelay: .5 + i * .2}); i ++; } visible = true; } + public function drawSelf() { super.draw(); } public override function draw() { - super.draw(); + drawSelf(); for (lane in lanes) { // draw on top if (!lane.selfDraw) - lane.drawTop(); + @:privateAccess lane.drawThing(true); } } public function forEachLane(func:Lane -> Void) { for (lane in lanes) func(lane); } - public function forEachNote(func:Note -> Void, includeQueued:Bool = false) { + public function forEachNote(func:ChartNote -> Void, includeQueued:Bool = false) { for (lane in lanes) lane.forEachNote(func, includeQueued); } + public function forEachActiveNote(func:Note -> Void) { + for (lane in lanes) + lane.forEachActiveNote(func); + } public function getAllNotes() { - var notes:Array = []; + var notes:Array = []; for (lane in lanes) { for (note in lane.getAllNotes()) notes.push(note); @@ -166,26 +214,24 @@ class Strumline extends FunkinSpriteGroup { var wRatio:Float = (targetWidth > 0 ? targetWidth / width : 1); var hRatio:Float = (targetHeight > 0 ? targetHeight / height : 1); var ratio:Float = Math.min(wRatio, hRatio); - if (ratio != 1) { - switch (center) { - case X: - x += (width - width * ratio) * .5; - case Y: - y += (height - height * ratio) * .5; - case XY: - x += (width - width * ratio) * .5; - y += (height - height * ratio) * .5; - default: - //shrug - } - for (lane in lanes) { - lane.receptor.scale.x *= ratio; - lane.receptor.scale.y *= ratio; - lane.receptor.updateHitbox(); - lane.receptor.spriteOffset.set(0, 0); - } - laneSpacing *= ratio; + if (ratio != 1) + scaleTo(scale.x * ratio); + } + public function scaleTo(x:Float, ?y:Float, center:FlxAxes = NONE) { + y ??= x; + switch (center) { + case X: + x += (width - width * x) * .5; + case Y: + y += (height - height * x) * .5; + case XY: + x += (width - width * y) * .5; + y += (height - height * y) * .5; + default: } + + recalculateLaneSpacing(laneSpacing / scale.x * x, laneSpacing); + scale.set(x, y); } public function center(axes:FlxAxes = XY) { //do Not inline that. switch (axes) { @@ -210,12 +256,21 @@ class Strumline extends FunkinSpriteGroup { } } - public function queueNote(note:Note, ?laneIndex:Int) { - laneIndex ??= note.noteData; - laneIndex = FlxMath.wrap(laneIndex, 0, lanes.length - 1); - var lane:Lane = getLane(laneIndex); - if (lane != null) - lane.queueNote(note); + public function getNoteLane(note:ChartNote):Lane { + return getLane(note.laneIndex % laneCount); + } + public inline function queueNote(note:ChartNote, ?laneIndex:Int, sort:Bool = false, checkExists:Bool = true):ChartNote { + var lane:Lane = (laneIndex == null ? getNoteLane(note) : getLane(laneIndex)); + if (lane != null) { + lane.queueNote(note, sort, checkExists); + return note; + } + + return null; + } + public function dequeueNote(note:ChartNote) { + for (lane in lanes) + lane.dequeueNote(note); } public function clearAllNotes() { for (lane in lanes) @@ -226,7 +281,7 @@ class Strumline extends FunkinSpriteGroup { lane.resetLane(); } - public function getLane(noteData:Int) return lanes.members[noteData]; + public inline function getLane(index:Int):Lane { return lanes.members[index]; } public function fireInput(key:flixel.input.keyboard.FlxKey, pressed:Bool) { var fired:Bool = false; @@ -236,4 +291,23 @@ class Strumline extends FunkinSpriteGroup { } return fired; } + + override function set_x(value:Float):Float { + if (exists && x != value) { + var diff:Float = (value - x); + transformChildren(xTransform, diff); + for (lane in lanes) + lane.startX += diff; + } + return x = value; + } + override function set_y(value:Float):Float { + if (exists && y != value) { + var diff:Float = (value - y); + transformChildren(yTransform, diff); + for (lane in lanes) + lane.startY += diff; + } + return y = value; + } } \ No newline at end of file diff --git a/source/funkin/objects/ui/SettingItem.hx b/source/funkin/objects/ui/SettingItem.hx new file mode 100644 index 0000000..7925b4e --- /dev/null +++ b/source/funkin/objects/ui/SettingItem.hx @@ -0,0 +1,75 @@ +package funkin.objects.ui; + +class CheckboxItem extends SettingItem { + public var checkbox:Checkbox; + + public function new(name:String, ?save:String) { + super(name, save); + + add(checkbox = new Checkbox(0, -30)); + checkbox.scale.set(.5, .5); + checkbox.updateHitbox(); + + text.x += 100; + value ??= false; + check(value, true); + } + + public override function confirm():Void { + setValue(value == false); + check(value); + + super.confirm(); + } + + public function check(on:Bool, instant:Bool = false) { + checkbox.check(on, instant); + } +} + +class Checkbox extends FunkinSprite { + public function new(x:Float = 0, y:Float = 0) { + super(x, y); + + loadAtlas('options/checkbox'); + addAnimation('select', 'checkbox select'); + addAnimation('unselect', 'checkbox unselect'); + setAnimationOffset('select', 12, 40); + + check(false, true); + updateHitbox(); + } + + public function check(on:Bool, instant:Bool = false) { + playAnimation(on ? 'select' : 'unselect', true); + if (instant) finishAnimation(); + } +} + +class SettingItem extends TextItem { + public var value:Dynamic = null; + + public var save(default, set):String; + + public function new(name:String, ?save:String) { + super(name); + + this.save = save; + } + + public function getValue():Dynamic { + return (save == null ? value : Reflect.getProperty(Options.data, save)); + } + + public function setValue(value:Dynamic):Void { + if (save != null) Reflect.setProperty(Options.data, save, value); + this.value = value; + } + + function set_save(now:String):String { + save = now; + value = getValue(); + + return now; + } +} \ No newline at end of file diff --git a/source/funkin/objects/ui/TextItem.hx b/source/funkin/objects/ui/TextItem.hx new file mode 100644 index 0000000..1c7f756 --- /dev/null +++ b/source/funkin/objects/ui/TextItem.hx @@ -0,0 +1,44 @@ +package funkin.objects.ui; + +class TextItem extends FunkinSpriteGroup { + public var highlighted:Bool = false; + public var onSelected:Void -> Void; + public var selected:Bool = false; + + public var startX:Float = 0; + public var startY:Float = 0; + + public var text:Alphabet; + + public function new(name:String, ?func:Void -> Void, scale:Float = .75) { + super(); + + add(text = new Alphabet(name)); + scaleTo(scale, scale); + + this.onSelected = func; + + highlight(false); + } + + public function confirm():Void { + if (onSelected != null) + onSelected(); + } + + public function highlight(on:Bool):Void { + highlighted = on; + + if (on) { + text.color = 0xffffcc66; + alpha = 1; + } else { + text.color = FlxColor.WHITE; + alpha = .65; + } + } + + function scaleTo(x:Float = 1, y:Float = 1):Void { + text.scaleTo(x, y); + } +} \ No newline at end of file diff --git a/source/funkin/objects/ui/TextItemGroup.hx b/source/funkin/objects/ui/TextItemGroup.hx new file mode 100644 index 0000000..a8e213a --- /dev/null +++ b/source/funkin/objects/ui/TextItemGroup.hx @@ -0,0 +1,62 @@ +package funkin.objects.ui; + +class TextItemGroup extends FunkinTypedSpriteGroup { + public var itemPadding:Float = 25; + public var itemDrift:Float = 0; + public var selection:Int = 0; + + public var selectedItem(get, never):TextItem; + + public function new() { + super(); + } + + public function addItem(item:TextItem):TextItem { + add(item); + repositionItems(); + + return item; + } + + public function repositionItems():Void { + var xx:Float = 0; + var yy:Float = 0; + + for (item in members) { + if (item == null || !item.exists) continue; + + item.startY = yy; + item.startX = xx; + item.setPosition(x + xx, y + yy); + + xx += itemDrift; + yy += item.text.height + itemPadding; + } + } + + public function confirm():Void { + selectedItem?.confirm(); + } + + public function select(mod:Int = 0, sound:Bool = true):Void { + if (length == 0) return; + + if (mod != 0 && sound) FunkinSound.playOnce(Paths.sound('scrollMenu'), .8); + + var prevOption:TextItem = members[selection]; + if (prevOption != null) { + prevOption.highlight(false); + } + + selection = FlxMath.wrap(selection + mod, 0, length - 1); + + var curOption:TextItem = members[selection]; + if (curOption != null) { + curOption.highlight(true); + } + } + + function get_selectedItem():TextItem { + return members[selection]; + } +} \ No newline at end of file diff --git a/source/funkin/shaders/MonoSwap.hx b/source/funkin/shaders/MonoSwap.hx index 068d010..172760d 100644 --- a/source/funkin/shaders/MonoSwap.hx +++ b/source/funkin/shaders/MonoSwap.hx @@ -5,6 +5,15 @@ class MonoSwap { public var black(default, set):FlxColor; public var shader(default, null):MonoSwapShader = new MonoSwapShader(); + public function copy(?targetShd:MonoSwap):MonoSwap { + if (targetShd != null) { + targetShd.white = white; + targetShd.black = black; + } else { + targetShd = new MonoSwap(white, black); + } + return targetShd; + } public function set_white(newC:FlxColor) { shader.white.value = [newC.redFloat, newC.greenFloat, newC.blueFloat, newC.alphaFloat]; return white = newC; @@ -13,10 +22,14 @@ class MonoSwap { shader.black.value = [newC.redFloat, newC.greenFloat, newC.blueFloat, newC.alphaFloat]; return black = newC; } - public function new(white:FlxColor = FlxColor.WHITE, black:FlxColor = FlxColor.BLACK) { + public function set(white:FlxColor = FlxColor.WHITE, black:FlxColor = FlxColor.BLACK) { this.white = white; this.black = black; } + + public function new(white:FlxColor = FlxColor.WHITE, black:FlxColor = FlxColor.BLACK) { + this.set(white, black); + } } class MonoSwapShader extends flixel.system.FlxAssets.FlxShader { diff --git a/source/funkin/shaders/RGBSwap.hx b/source/funkin/shaders/RGBSwap.hx index 7c48a99..83af0b2 100644 --- a/source/funkin/shaders/RGBSwap.hx +++ b/source/funkin/shaders/RGBSwap.hx @@ -5,7 +5,17 @@ class RGBSwap { // im coming public var blue(default, set):FlxColor; public var green(default, set):FlxColor; public var shader(default, null):RGBSwapShader = new RGBSwapShader(); - + + public function copy(?targetShd:RGBSwap):RGBSwap { + if (targetShd != null) { + targetShd.green = green; + targetShd.blue = blue; + targetShd.red = red; + } else { + targetShd = new RGBSwap(red, green, blue); + } + return targetShd; + } public function set_red(newC:FlxColor) { shader.red.value = [newC.redFloat, newC.greenFloat, newC.blueFloat]; return red = newC; @@ -18,11 +28,15 @@ class RGBSwap { // im coming shader.blue.value = [newC.redFloat, newC.greenFloat, newC.blueFloat]; return blue = newC; } - public function new(red:FlxColor = FlxColor.RED, green:FlxColor = FlxColor.LIME, blue:FlxColor = FlxColor.BLUE) { + public function set(red:FlxColor = FlxColor.RED, green:FlxColor = FlxColor.LIME, blue:FlxColor = FlxColor.BLUE) { this.red = red; this.blue = blue; this.green = green; } + + public function new(red:FlxColor = FlxColor.RED, green:FlxColor = FlxColor.LIME, blue:FlxColor = FlxColor.BLUE) { + this.set(red, green, blue); + } } class RGBSwapShader extends flixel.system.FlxAssets.FlxShader { diff --git a/source/funkin/states/CharterState.hx b/source/funkin/states/CharterState.hx index 584d7d5..ac9acac 100644 --- a/source/funkin/states/CharterState.hx +++ b/source/funkin/states/CharterState.hx @@ -1,178 +1,75 @@ package funkin.states; -import funkin.objects.Character; -import funkin.objects.play.*; -import funkin.shaders.RGBSwap; -import funkin.backend.play.Chart; -import funkin.states.FreeplaySubState.FreeplaySongText; - -import sys.thread.Thread; import openfl.display.Sprite; import openfl.display.Bitmap; -import openfl.text.TextFormat; import openfl.text.TextField; -import openfl.events.MouseEvent; -import openfl.events.KeyboardEvent; +import openfl.text.TextFormat; import flixel.util.FlxStringUtil; -import flixel.input.keyboard.FlxKey; import flixel.addons.display.FlxBackdrop; +import funkin.backend.play.Chart; +import funkin.backend.play.NoteStyle; +import funkin.objects.play.Strumline; +import funkin.objects.Character; + class CharterState extends FunkinState { - public static var genericRGB:RGBSwap; - public static var instance:CharterState; - public static var inEditor:Bool = false; public static var chart:Chart; - public var quant:Int = 4; - public var quantText:FlxText; - public var quantGraphic:FunkinSprite; - public var measureLines:FlxTypedSpriteGroup; - public var strumlines:FlxTypedSpriteGroup; - public var strumlineHighlight:FunkinSprite; - public var charterDisplay:CharterDisplay; - public var camHUD:FunkinCamera; - public var camScroll:FunkinCamera; - - public var noteKindBubble:FunkinSprite; - public var noteKindBubbleText:FreeplaySongText; - public var noteKindBubbleFocusGraphic:FlxGraphic; + public var strumlineGroup:FunkinTypedSpriteGroup; - public var selectionBox:FlxSprite; - public var selectionLeniency:Float = 55; - public var pickedNote:CharterNote = null; - public var draggingNotes:Bool = false; - var pickedLaneIndex:Int = 0; - var pickedBeat:Float = 0; - - public var keybinds:Array> = []; - - var vocalsSounds:Array = []; - public var music:FunkinSoundGroup; - public var tickSound:FlxSound; - public var hitsound:FlxSound; + public var charterDisplay:CharterDisplay; - public var scrollSpeed(default, set):Float = 1; public var songPaused(default, set):Bool; - var visualScrollSpeed(get, never):Float; - - public var fullNoteCount:Int = 0; - public var hitNoteCount:Int = 0; + public var noteCount:Int = 0; - var lastMouseY:Float = 0; - var scrolling:Bool = false; - var strumGrabY:Null = null; - var heldKeys:Array = []; - var heldKeybinds:Array = []; - var copiedNotes:Array = []; - var heldNotes:Array = []; - var quants:Array = [4, 8, 12, 16, 24, 32, 48, 64, 96, 192]; - - var undoMemory:Int = 15; - var undoActions:Array = []; - var redoActions:Array = []; + public var music:FunkinSoundGroup; + var vocalsSounds:Array = []; - public function new(?chart:Chart) { + public function new(?targetChart:Chart) { super(); - CharterState.chart = chart ?? CharterState.chart ?? Chart.loadChart('test'); + Mods.currentMod ??= ''; + + chart = targetChart ?? chart ?? Chart.loadChart('test'); } - override public function create() { super.create(); - Main.watermark.visible = false; - Main.instance.addChild(charterDisplay = new CharterDisplay(conductorInUse = new Conductor())); // wow! - beatHit.add(beatHitEvent); - barHit.add(barHitEvent); - - music = new FunkinSoundGroup(); - tickSound = FunkinSound.load(Paths.sound('beatTick')); - hitsound = FunkinSound.load(Paths.sound('hitsound')); - hitsound.volume = .7; - - genericRGB ??= new RGBSwap(0xb3a9b8, FlxColor.WHITE, 0x333333); - inEditor = true; instance = this; + conductorInUse = new Conductor(); + conductorInUse.tempoChanges = chart.tempoChanges; - FlxG.camera.zoom = .5; + charterDisplay = new CharterDisplay(conductorInUse); + Main.instance.addChild(charterDisplay); - camHUD = new FunkinCamera(); - camScroll = new FunkinCamera(); - camScroll.bgColor.alpha = 0; - camHUD.bgColor.alpha = 0; - FlxG.cameras.add(camScroll, false); - FlxG.cameras.add(camHUD, false); - - noteKindBubble = new FunkinSprite().loadTexture('charter/bubble'); - noteKindBubble.scale.set(.75, .75); - noteKindBubble.updateHitbox(); - noteKindBubble.camera = camHUD; - noteKindBubble.color = 0xff323034; - noteKindBubble.alpha = .6; - noteKindBubbleText = new FreeplaySongText(0, 0, '', 0x808080, .8); //ffe0b0d0; - noteKindBubbleText.size = 16; - noteKindBubbleText.angle = -3; - noteKindBubbleText.origin.set(0, 6); - noteKindBubbleText.camera = camHUD; - - selectionBox = new FlxSprite().makeGraphic(1, 1, FlxColor.LIME); - selectionBox.camera = camScroll; - selectionBox.visible = false; - selectionBox.blend = ADD; - selectionBox.alpha = .25; - selectionBox.origin.set(); - add(selectionBox); var background:FlxBackdrop = new FlxBackdrop(Paths.image('charter/bg')); - background.antialiasing = true; + background.antialiasing = Options.data.antialiasing; background.scale.set(.85, .85); background.velocity.set(5, 5); add(background); - var underlay:FunkinSprite = new FunkinSprite(0, 0, false).makeGraphic(1, FlxG.height, 0xff101010); - underlay.screenCenter(); - underlay.alpha = .7; - add(underlay); - - measureLines = new FlxTypedSpriteGroup(); - strumlines = new FlxTypedSpriteGroup(); - add(measureLines); - add(strumlines); - - for (key in [FlxKey.ONE, FlxKey.TWO, FlxKey.THREE, FlxKey.FOUR, FlxKey.FIVE, FlxKey.SIX, FlxKey.SEVEN, FlxKey.EIGHT]) { - keybinds.push([key]); - heldNotes.push(null); - heldKeybinds.push(false); - } + + var chartStyle:String = chart.noteStyle; + var mania:String = '${chart.keyCount}k'; + var noteStyle:String = chartStyle; + if (NoteStyle.exists('$chartStyle-$mania')) + noteStyle = '$chartStyle-$mania'; - var strumlineSpacing:Float = 150; - var xx:Float = 0; - var h:Float = 0; - for (i in 0...2) { - var strumline = new CharterStrumline(4); - strumline.x = xx; - strumline.cpu = false; - strumline.oneWay = false; - strumlines.add(strumline); - xx += strumline.strumlineWidth + strumlineSpacing; - h = Math.max(h, strumline.strumlineHeight); - for (lane in strumline.lanes) { - lane.receptor.autoReset = true; - lane.oneWay = false; - } + music = new FunkinSoundGroup(); + strumlineGroup = new FunkinTypedSpriteGroup(); + + var strumlineX:Float = 0; + for (i in 0 ... chart.getStrumlineCount()) { + var strumline:Strumline = new Strumline(chart.keyCount, 90, chart.scrollSpeed, noteStyle); + strumline.x = strumlineX; + strumline.allowInput = strumline.cpu = false; + strumline.fitToSize(0, strumline.height * .7); + + strumlineX += strumline.width + 150 * strumline.scale.x; + + strumlineGroup.add(strumline); } - strumlines.y = FlxG.height * .5 - h * .5 - 320; - strumlines.x = (FlxG.width - (xx - strumlineSpacing)) * .5; - - strumlineHighlight = new FunkinSprite().makeGraphic(1, 1, 0xffb094b0); - strumlineHighlight.setGraphicSize(strumlines.width, strumlines.height); - strumlineHighlight.updateHitbox(); - strumlineHighlight.blend = ADD; - strumlineHighlight.alpha = .25; - - FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressEvent); - FlxG.stage.addEventListener(KeyboardEvent.KEY_UP, keyReleaseEvent); - FlxG.stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveEvent); - - setWindowTitle(); + strumlineGroup.screenCenter(); + add(strumlineGroup); chart.instLoaded = false; chart.loadMusic('data/songs/${chart.path}/', false); @@ -186,54 +83,13 @@ class CharterState extends FunkinState { conductorInUse.syncTracker = chart.inst; } loadVocals(chart.path, chart.audioSuffix); - - scrollSpeed = chart.scrollSpeed; - conductorInUse.metronome.tempoChanges = chart.tempoChanges; - - for (note in chart.generateNotes(true)) - queueNote(note); - songPaused = true; - recalculateNoteCount(); - charterDisplay.songLength = findSongLength(); - - var bgPadding:Float = 50; - underlay.setGraphicSize(strumlines.width + bgPadding * 2, FlxG.height * 5); - - quantGraphic = new FunkinSprite().loadAtlas('charter/quant'); - quantGraphic.addAnimation('quant', 'new quant', 0); - quantGraphic.playAnimation('quant', true); - quantGraphic.updateHitbox(); - //quantGraphic.x = strumlines.x + xx - strumlineSpacing; - quantGraphic.y = strumlines.y + (h - quantGraphic.height) * .5; - quantGraphic.screenCenter(X); - add(quantGraphic); - - quantText = new FlxText(0, 0, 300); - quantText.setFormat(Paths.ttf('vcr'), 40, FlxColor.WHITE, CENTER, OUTLINE, FlxColor.BLACK); - quantText.borderSize = 4; - quantText.updateHitbox(); - quantText.y = strumlines.y + (h - quantText.height) * .5; - quantText.screenCenter(X); - add(quantText); - changeQuant(0); - - add(strumlineHighlight); - - var measureBeats:Array = conductorInUse.getMeasureBeats(findSongLength()); - var beatLength:Float = Math.ceil(conductorInUse.convertMeasure(findSongLength(), MS, BEAT)); - for (measure => beat in measureBeats) { - conductorInUse.beat = beat; // todo: fix mid-bar bpm changes...hehe - var beatLength:Int = Std.int((measureBeats[measure + 1] ?? beatLength) - beat); - var spacing:Float = Note.msToDistance(conductorInUse.crochet, visualScrollSpeed); - var line:MeasureLine = new MeasureLine(strumlines.x - bgPadding, strumlines.y, measure, beat, beatLength, strumlines.width + bgPadding * 2, spacing); - measureLines.add(line); - } - - conductorInUse.songPosition = 0; } - - public function loadVocals(path:String, audioSuffix:String = '') { + public function finishSong():Void { + conductorInUse.songPosition = music.length; + songPaused = true; + } + public function loadVocals(path:String, audioSuffix:String = ''):Void { vocalsSounds.resize(0); for (chara in [chart.player1, chart.player2, chart.player3]) { @@ -256,428 +112,7 @@ class CharterState extends FunkinState { music.add(sound); } } - - override public function update(elapsed:Float) { - if (FlxG.keys.justPressed.ENTER) { - var shifted:Bool = FlxG.keys.pressed.SHIFT; - chart.tempoChanges = conductorInUse.metronome.tempoChanges; - saveToChart(chart); - FlxG.switchState(() -> new PlayState(chart, shifted)); - return; - } - - // selection and dragging - var highlightedNote:CharterNote = null; - var highlightedSelNote:CharterNote = null; - var anyNoteHovered:Bool = (pickedNote != null); - forEachNote((note:Note) -> { - var charterNote:CharterNote = cast note; - if (charterNote == null) return; - - if (!note.isHoldPiece && FlxG.mouse.overlaps(note)) { - anyNoteHovered = true; - if (charterNote.selected) { - if (highlightedSelNote == null) { - highlightedSelNote = charterNote; - } else { - var mousePoint:FlxPoint = FlxG.mouse.getWorldPosition(); - if (mousePoint.distanceTo(note.getMidpoint()) < mousePoint.distanceTo(highlightedSelNote.getMidpoint())) - highlightedSelNote = charterNote; - } - if (FlxG.mouse.justPressed) { - pickedNote = highlightedSelNote; - pickedBeat = pickedNote.beatTime; - pickedLaneIndex = laneToIndex(pickedNote.lane); - } - } else { - if (highlightedNote == null) { - highlightedNote = charterNote; - } else { - var mousePoint:FlxPoint = FlxG.mouse.getWorldPosition(); - if (mousePoint.distanceTo(note.getMidpoint()) < mousePoint.distanceTo(highlightedNote.getMidpoint())) - highlightedNote = charterNote; - } - } - } - }); - - var selectedAny:Bool = false; - var isSelecting:Bool = (selectionBox.visible && Math.abs(selectionBox.scale.x) >= 12 && Math.abs(selectionBox.scale.y) >= 12); - if (FlxG.mouse.pressed && strumGrabY == null) { - if (pickedNote != null) { - var pickedLane:Lane = null; // drag and twist notes - for (strumline in strumlines) { - for (lane in strumline.lanes) { - if (FlxG.mouse.x >= lane.receptor.x && FlxG.mouse.x <= lane.receptor.x + lane.receptor.width) { - pickedLane = lane; - break; - } - } - } - var selectedNotes:Array = getSelectedNotes(); - - readjustScrollCam(); - var quantMult:Float = (quant / 4); - var cursorBeatTime:Float = Note.distanceToMS(FlxG.mouse.getWorldPosition(camScroll).y, visualScrollSpeed) / conductorInUse.crochet; - var snappedBeatTime:Float = Math.round(cursorBeatTime * quantMult) / quantMult; - var beatDiff:Float = (snappedBeatTime - pickedNote.beatTime); - - if (shiftNotes(selectedNotes, beatDiff) != 0) - draggingNotes = true; - - if (pickedLane != null && pickedLane != pickedNote.lane) { - var laneDiff:Int = laneToIndex(pickedLane) - laneToIndex(pickedNote.lane); - if (twistNotes(selectedNotes, laneDiff) != 0) - draggingNotes = true; - } - } else { - var mousePos:FlxPoint = FlxG.mouse.getWorldPosition(camScroll); - if (FlxG.mouse.justPressed || !selectionBox.visible) { - selectionBox.setPosition(mousePos.x, mousePos.y); - selectionBox.visible = true; - } - selectionBox.scale.set(mousePos.x - selectionBox.x, mousePos.y - selectionBox.y); - } - } else if (selectionBox.visible) { - //do the selection! - var selectionBounds:FlxRect = selectionBox.getScreenBounds(null, camScroll); - if (selectionBox.scale.x < 0) selectionBounds.x += selectionBox.scale.x; - if (selectionBox.scale.y < 0) selectionBounds.y += selectionBox.scale.y; - - if (isSelecting) { - forEachNote((note:Note) -> { - if (note.isHoldPiece) return; - var charterNote:CharterNote = cast note; - if (charterNote == null) return; - - charterNote.followLane(note.lane, note.lane.scrollSpeed); - - var noteBounds:FlxRect = charterNote.getScreenBounds(); - noteBounds.x += selectionLeniency; - noteBounds.y += selectionLeniency; - noteBounds.width -= selectionLeniency * 2; - noteBounds.height -= selectionLeniency * 2; - if (noteBounds.overlaps(selectionBounds)) { - charterNote.selected = true; - selectedAny = true; - } else if (!FlxG.keys.pressed.SHIFT) { - charterNote.selected = false; - } - }, true); - } - - selectionBox.visible = false; - } - if (highlightedNote == null) - highlightedNote = highlightedSelNote; - forEachNote((note:Note) -> { - var charterNote:CharterNote = cast note; - if (charterNote == null) return; - - charterNote.highlighted = (highlightedNote == note); - }); - if (FlxG.mouse.justReleased) { - if (pickedNote != null) { - final beatDiff:Float = pickedNote.beatTime - pickedBeat; - final laneDiff:Int = laneToIndex(pickedNote.lane) - pickedLaneIndex; - if (beatDiff != 0 || laneDiff != 0) - addUndo({type: SHIFTED_NOTES, notes: getSelectedNotes(), laneMod: laneDiff, beatMod: beatDiff}); - } - if (!FlxG.keys.pressed.SHIFT && !draggingNotes && !selectedAny && strumGrabY == null) { - for (note in getSelectedNotes()) - note.selected = false; - } - if (highlightedNote != null) - highlightedNote.selected = true; - draggingNotes = false; - pickedNote = null; - } - - // time shift - if (!FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.W != FlxG.keys.pressed.S) { - var msPerSec:Float = (FlxG.keys.pressed.SHIFT ? 2000 : 1000); - if (FlxG.keys.pressed.W) - msPerSec *= -1; - - if (songPaused) - songPaused = false; - conductorInUse.paused = true; - conductorInUse.songPosition += elapsed * msPerSec; - restrictConductor(); - - if (conductorInUse.songPosition <= 0 && msPerSec < 0) { - music.stop(); - } else if (msPerSec != 1000 || !scrolling || !music.playing) { - if (!music.playing) { - music.play(true, conductorInUse.songPosition); - } else { - music.time = conductorInUse.songPosition; - } - } - - scrolling = true; - } else if (scrolling) { - songPaused = true; - scrolling = false; - } - - // receptor dragging - var strumlinesHighlighted:Bool = (FlxG.mouse.overlaps(strumlineHighlight) && !isSelecting && !anyNoteHovered && !FlxG.mouse.pressedRight); - strumlineHighlight.setPosition(strumlines.x, strumlines.y); - if (FlxG.mouse.justPressed && strumlinesHighlighted) - strumGrabY = (FlxG.mouse.y - strumlines.y); - if (FlxG.mouse.justReleased) - strumGrabY = null; - strumlineHighlight.visible = (strumlinesHighlighted || strumGrabY != null); - if (FlxG.mouse.pressed && strumGrabY != null) { - var h:Float = strumlines.height; - var middle:Float = (FlxG.height - h) * .5; - var maxDist:Float = (FlxG.height - h - 25) * .5 / FlxG.camera.zoom; - strumlines.y = Util.clamp(FlxG.mouse.y - strumGrabY, -maxDist + middle, maxDist + middle); - strumlineHighlight.setPosition(strumlines.x, strumlines.y); - - quantGraphic.y = strumlines.y + (h - quantGraphic.height) * .5; - quantText.y = strumlines.y + (h - quantText.height) * .5; - } - - if (FlxG.keys.justPressed.SPACE) songPaused = !songPaused; - super.update(elapsed); - forEachNote((note:Note) -> { - var lane:Lane = note.lane; - if (!conductorInUse.paused) { - if (conductorInUse.songPosition >= note.msTime && (conductorInUse.songPosition <= note.endMs || !note.goodHit)) { - lane.receptor?.playAnimation('confirm', true); - if (!note.goodHit && !note.isHoldPiece) - hitsound.play(true); - note.goodHit = true; - } - } else { - note.goodHit = (conductorInUse.songPosition > note.msTime + 1); - } - }); - - if (songPaused) { - FlxG.camera.zoom = Util.smoothLerp(FlxG.camera.zoom, .5, elapsed * 9); - } else { - var metronome:Metronome = conductorInUse.metronome; - var beatZoom:Float = 1 - FlxEase.quintOut(metronome.beat % 1); - var barZoom:Float = 1 - FlxEase.quintOut(Math.min((metronome.bar % 1) * metronome.timeSignature.numerator, 1)); - FlxG.camera.zoom = .5 + beatZoom * .003 + barZoom * .005; - } - - readjustScrollCam(); - for (line in measureLines) { - // line.ySpacing = Note.msToDistance(conductorInUse.crochet, visualScrollSpeed); - if (Math.abs(line.startTime - conductorInUse.beat) > 16) { - if (line.alive) line.kill(); - continue; - } - - if (!line.alive) line.revive(); - line.y = strumlines.y + strumlines.height * .5 + Note.msToDistance(conductorInUse.convertMeasure(line.startTime, BEAT, MS) - conductorInUse.songPosition, visualScrollSpeed); - } - - if (!paused) - updateHolds(); - } - public function readjustScrollCam() { - camScroll.scroll.y = Note.msToDistance(conductorInUse.songPosition, visualScrollSpeed) - strumlines.y - strumlines.height * .5; - camScroll.zoom = FlxG.camera.zoom; - } - override public function updateConductor(elapsed:Float = 0) { - var prevStep:Int = curStep; - var prevBeat:Int = curBeat; - var prevBar:Int = curBar; - - conductorInUse.update(elapsed * 1000); - - curStep = Math.floor(conductorInUse.step); - curBeat = Math.floor(conductorInUse.beat); - curBar = Math.floor(conductorInUse.bar); - - if (!songPaused) { - if (prevBar != curBar) barHit.dispatch(curBar); - if (prevBeat != curBeat) beatHit.dispatch(curBeat); - if (prevStep != curStep) stepHit.dispatch(curStep); - } - } - - public function beatHitEvent(beat:Int) { - tickSound.play(true); - } - public function barHitEvent(bar:Int) {} - public function mouseMoveEvent(event:MouseEvent) { - if (FlxG.mouse.pressedRight) { - if (!songPaused) - songPaused = true; - conductorInUse.songPosition -= Note.distanceToMS((event.stageY - lastMouseY) / Util.gameScaleY / FlxG.camera.zoom, visualScrollSpeed); - restrictConductor(); - - if (event.stageY <= 5) { - lastMouseY = (FlxG.stage.window.height - 5) + event.stageY - 10; - FlxG.stage.window.warpMouse(Std.int(event.stageX), Std.int(lastMouseY)); - return; - } else if (event.stageY >= FlxG.stage.window.height - 5) { - lastMouseY = 10 + event.stageY - (FlxG.stage.window.height - 5); - FlxG.stage.window.warpMouse(Std.int(event.stageX), Std.int(lastMouseY)); - return; - } - } - lastMouseY = event.stageY; - } - public function finishSong() { - songPaused = true; - conductorInUse.songPosition = (music.syncBase?.length ?? 0); - } - public function forEachNote(func:Note -> Void, includeQueued:Bool = false) { - for (strumline in strumlines) - strumline.forEachNote(func, includeQueued); - } - public function getSelectedNotes() { - var list:Array = []; - forEachNote((note:Note) -> { - var charterNote:CharterNote = cast note; - if (charterNote == null) return; - - if (charterNote.selected) - list.push(charterNote); - }, true); - return list; - } - public function queueNote(note:Note) { - var strumline:Strumline = (note.player ? strumlines.members[0] : strumlines.members[1]); - strumline?.queueNote(note); - return note; - } - public function shiftNotes(notesArray:Array, beatMod:Float = 0):Float { - if (beatMod == 0) return 0; - var beatDiff:Float = beatMod; - var minBeat:Float = Math.POSITIVE_INFINITY; - var maxBeat:Float = Math.NEGATIVE_INFINITY; - for (note in notesArray) { - if (note.isHoldPiece) continue; - if (note.beatTime < minBeat) minBeat = note.beatTime; - if (note.beatTime > maxBeat) maxBeat = note.beatTime; - } - if (minBeat + beatDiff < 0) - beatDiff = -minBeat; - // todo max beat - if (beatDiff != 0) { - for (note in notesArray) { - if (note.isHoldPiece) continue; - shiftNote(note, note.beatTime + beatDiff); - } - } - return beatDiff; - } - public function twistNotes(notesArray:Array, laneMod:Int = 0):Int { - if (laneMod == 0) return 0; - var laneDiff:Int = laneMod; - var minLane:Int = 9999; // prevent notes from going out of bounds - var maxLane:Int = -1; - for (note in notesArray) { - var laneIdx:Int = laneToIndex(note.lane); - if (laneIdx < minLane) minLane = laneIdx; - if (laneIdx > maxLane) maxLane = laneIdx; - } - if (minLane + laneDiff < 0) - laneDiff = -minLane; - if (maxLane + laneDiff >= getNumLanes()) - laneDiff = getNumLanes() - maxLane - 1; - - if (laneDiff != 0) { - for (note in notesArray) { - if (note.isHoldPiece) continue; - var laneIdx:Int = laneToIndex(note.lane); - var nextLane:Lane = indexToLane(laneIdx + laneDiff); - if (nextLane == null) continue; - twistNote(note, nextLane); - } - } - return laneDiff; - } - public function shiftNote(note:Note, beatTime:Float) { - if (note == null) { - Log.warning('shiftNote: ???'); - return; - } - var diff:Float = beatTime - note.beatTime; - note.beatTime = beatTime; - note.goodHit = (conductorInUse.songPosition > note.msTime + 1); - for (child in note.children) { - child.beatTime += diff; - child.goodHit = (conductorInUse.songPosition > child.msTime + 1); - } - } - public function twistNote(note:Note, lane:Lane) { - if (note == null || lane == null) { - Log.warning('twistNote: ???'); - return; - } - if (Std.isOfType(note, CharterNote)) { - var charterNote:CharterNote = cast note; - if (charterNote == null) return; - - if (charterNote.useLaneRGB) - note.shader = lane.rgbShader.shader; - } - note.player = (lane.strumline == strumlines.members[0]); - note.noteData = lane.noteData; - note.reloadAnimations(); - if (note.lane.notes.members.contains(note)) { - note.lane.notes.remove(note, true); - lane.insertNote(note); - } else { - note.lane.dequeueNote(note); - lane.queueNote(note, true); - } - note.lane = lane; - - for (child in note.children) - twistNote(child, lane); - } - public function indexToLane(index:Int) { - var n:Int = -1; - for (strumline in strumlines) { - for (lane in strumline.lanes) { - n ++; - if (n == index) - return lane; - } - } - return null; - } - public function laneToIndex(laneToFind:Lane) { - var n:Int = -1; - for (strumline in strumlines) { - for (lane in strumline.lanes) { - n ++; - if (lane == laneToFind) - return n; - } - } - return -1; - } - public function getNumLanes() { - var count:Int = 0; - for (strumline in strumlines) - count += strumline.lanes.length; - return count; - } - - public function set_scrollSpeed(newSpeed:Float) { - scrollSpeed = newSpeed; - for (strumline in strumlines) { - strumline.scrollSpeed = visualScrollSpeed; - /*for (lane in strumline.lanes) - lane.spawnRadius *= 1.5;*/ - } - return newSpeed; - } - public function get_visualScrollSpeed() { - return scrollSpeed / .7; - } - public function set_songPaused(isPaused:Bool) { + function set_songPaused(isPaused:Bool):Bool { if (isPaused) { music.stop(); } else { @@ -686,9 +121,9 @@ class CharterState extends FunkinState { music.play(true, conductorInUse.songPosition); } - for (strumline in strumlines) { - FlxTween.cancelTweensOf(strumline, ['receptorAlpha']); - FlxTween.tween(strumline, {receptorAlpha: (isPaused ? .75 : 1)}, .25, {ease: FlxEase.circOut}); + for (strumline in strumlineGroup) { + FlxTween.cancelTweensOf(strumline, ['alpha']); + FlxTween.tween(strumline, {alpha: (isPaused ? .75 : 1)}, .25, {ease: FlxEase.circOut}); for (lane in strumline.lanes) { if (isPaused) lane.receptor?.playAnimation('static'); @@ -698,586 +133,28 @@ class CharterState extends FunkinState { return songPaused = isPaused; } - public function keyPressEvent(event:KeyboardEvent) { - var key:FlxKey = event.keyCode; - if (!heldKeys.contains(key)) heldKeys.push(key); - - var keybind:Int = Controls.keybindFromArray(keybinds, key); - if (keybind >= 0 && FlxG.keys.checkStatus(key, JUST_PRESSED)) - inputOn(keybind); - - var noteControlMode:Bool = FlxG.keys.pressed.CONTROL; - var scrollMod:Int = 1; - var leniency:Float = 1 / 256; - var prevBeat:Float = conductorInUse.beat; - var quantMultiplier:Float = (quant * .25); - var pauseChart:Bool = false; - if (noteControlMode) { - keyPressNoteControl(key); - } - switch (key) { - case FlxKey.DELETE: - var deletedNotes:Array = []; - for (note in getSelectedNotes()) { - note.lane.dequeueNote(note); - note.lane.killNote(note); - deletedNotes.push(note); - } - if (deletedNotes.length > 0) - addUndo({type: REMOVED_NOTES, notes: deletedNotes}); - case FlxKey.LEFT | FlxKey.RIGHT: - if (key == FlxKey.LEFT) scrollMod *= -1; - if (noteControlMode) { - twistNotes(getSelectedNotes(), scrollMod); - } else { - changeQuant(scrollMod); - } - case FlxKey.UP | FlxKey.DOWN: - if (key == FlxKey.UP) scrollMod *= -1; - if (noteControlMode) { - shiftNotes(getSelectedNotes(), scrollMod / quantMultiplier); - } else { - placeNotes(); - pauseChart = true; - var targetBeat:Float = prevBeat + scrollMod / quantMultiplier; - if (Math.abs(prevBeat - Math.round(prevBeat * quantMultiplier) / quantMultiplier) < leniency * 2) - conductorInUse.beat = Math.round(targetBeat * quantMultiplier) / quantMultiplier; - else - conductorInUse.beat = (scrollMod > 0 ? Math.floor : Math.ceil)(targetBeat * quantMultiplier) / quantMultiplier; - } - case FlxKey.PAGEUP | FlxKey.PAGEDOWN: - placeNotes(); - pauseChart = true; - if (key == FlxKey.PAGEUP) scrollMod *= -1; - if (Math.abs(conductorInUse.bar - Std.int(conductorInUse.bar)) < (1 / quant - .0006)) - conductorInUse.bar = Math.max(0, conductorInUse.bar + scrollMod); - conductorInUse.bar = (scrollMod < 0 ? Math.floor : Math.ceil)(conductorInUse.bar); - case FlxKey.HOME: - pauseChart = true; - conductorInUse.songPosition = 0; - case FlxKey.END: - pauseChart = true; - conductorInUse.songPosition = findSongLength(); - default: - } - - if (pauseChart && !songPaused) - songPaused = true; - - restrictConductor(); - updateHolds(); - } - public function undo() { - var undoAction:UndoAction = undoActions.pop(); - if (undoAction != null) { - // Sys.println('undoing ${undoAction.type}'); - redoActions.unshift(undoAction); - switch (undoAction.type) { - case PLACED_NOTES: - for (note in undoAction.notes) { - note.lane.dequeueNote(note); - note.lane.killNote(note); - } - case REMOVED_NOTES: - for (note in undoAction.notes) { - note.lane.queueNote(note); - note.selected = true; - } - case SHIFTED_NOTES: - twistNotes(undoAction.notes, -undoAction.laneMod); - shiftNotes(undoAction.notes, -undoAction.beatMod); - default: - } - charterDisplay.addMessage(undoAction.message(), true, false); - recalculateNoteCount(); - } - } - public function redo() { - var undoAction:UndoAction = redoActions.shift(); - if (undoAction != null) { - // Sys.println('redoing ${undoAction.type}'); - undoActions.push(undoAction); - switch (undoAction.type) { - case PLACED_NOTES: - for (note in undoAction.notes) { - note.lane.queueNote(note); - note.selected = true; - } - case REMOVED_NOTES: - for (note in undoAction.notes) { - note.lane.dequeueNote(note); - note.lane.killNote(note); - } - case SHIFTED_NOTES: - twistNotes(undoAction.notes, undoAction.laneMod); - shiftNotes(undoAction.notes, undoAction.beatMod); - default: - } - charterDisplay.addMessage(undoAction.message(), true, true); - recalculateNoteCount(); - } - } - public function addUndo(action:UndoAction) { - // Sys.println('added undo action ${action.type}'); - setWindowTitle(true); - undoActions.push(action); - - while (undoActions.length > undoMemory) { - var undoAction:UndoAction = undoActions.shift(); - destroyUndoAction(undoAction, false); - } - for (undoAction in redoActions) - destroyUndoAction(undoAction, true); - redoActions.resize(0); - - charterDisplay.addMessage(action.message(), false, true); - recalculateNoteCount(); - } - public function destroyUndoAction(action:UndoAction, parallel:Bool = false) { - switch (action.type) { // parallel: for redo - case PLACED_NOTES | REMOVED_NOTES: - if (parallel == (action.type == REMOVED_NOTES)) - return; - for (note in action.notes) - note.destroy(); - default: - } - } - public function recalculateNoteCount() { - fullNoteCount = 0; - hitNoteCount = 0; - for (strumline in strumlines) { - strumline.forEachNote((note:Note) -> { - if (!note.isHoldPiece) - hitNoteCount ++; - fullNoteCount ++; - }, true); - } - } - public function copySelectedNotes() { - var selectedNotes:Array = getSelectedNotes(); - if (selectedNotes.length > 0) { - copiedNotes.resize(0); - for (note in selectedNotes) { - if (note.isHoldPiece) continue; - copiedNotes.push(note.toChartNote()); - } - copiedNotes.sort(Chart.sortByTime); - } - } - public function keyPressNoteControl(key:FlxKey) { - switch (key) { - case FlxKey.Z: - undo(); - case FlxKey.Y: - redo(); - case FlxKey.C: // COPY - copySelectedNotes(); - case FlxKey.V: // PASTE - for (note in getSelectedNotes()) - note.selected = false; - if (copiedNotes.length > 0) { - var generatedNotes:Array = []; - for (note in Chart.generateNotesFromArray(copiedNotes, true)) { - var charterNote:CharterNote = cast note; - if (charterNote == null) continue; - - generatedNotes.push(charterNote); - charterNote.justCopied = true; - charterNote.selected = true; - } - var quantMult:Float = quant / 4; - var beatDiff:Float = (Math.round(conductorInUse.beat * quantMult) / quantMult - generatedNotes[0].beatTime); - for (note in generatedNotes) { - note.beatTime += beatDiff; - queueNote(note); - } - addUndo({type: PLACED_NOTES, notes: generatedNotes, pasted: true}); - } - case FlxKey.X: // CUT - copySelectedNotes(); - var deletedNotes:Array = []; - for (note in getSelectedNotes()) { - note.lane.dequeueNote(note); - note.lane.killNote(note); - deletedNotes.push(note); - } - if (deletedNotes.length > 0) - addUndo({type: REMOVED_NOTES, notes: deletedNotes}); - case FlxKey.A: // SELECT ALL - var selectedAny:Bool = false; - forEachNote((note:Note) -> { - var charterNote:CharterNote = cast note; - if (charterNote == null) return; - - if (!charterNote.selected) { - charterNote.selected = true; - selectedAny = true; - } - }, true); - - if (!selectedAny) { - forEachNote((note:Note) -> { - var charterNote:CharterNote = cast note; - if (charterNote == null) return; - - charterNote.selected = false; - }, true); - } - default: - } - } - public function restrictConductor() { - var limitTime:Float = Math.max(conductorInUse.songPosition, 0); - limitTime = Math.min(limitTime, music.syncBase?.length ?? 0); - - conductorInUse.songPosition = limitTime; - } - public function placeNotes() { - for (key => held in heldKeybinds) { - if (held && heldNotes[key] == null) - placeNote(key); + override public function update(elapsed:Float) { + if (FlxG.keys.justPressed.ESCAPE) { + FlxG.switchState(() -> new PlayState(chart)); + return; } - } - public function findSongLength() { - var length:Null = chart?.songLength; - if (length == null) // todo - length = 0; - return length; - } - public function keyReleaseEvent(event:KeyboardEvent) { - var key:FlxKey = event.keyCode; - heldKeys.remove(key); - var keybind:Int = Controls.keybindFromArray(keybinds, key); - if (keybind >= 0) inputOff(keybind); - } - - public function changeQuant(mod:Int) { - var quantIndex:Int = FlxMath.wrap(quants.indexOf(quant) + mod, 0, quants.length - 1); - quantGraphic.animation.curAnim.curFrame = Std.int(Math.min(quantIndex, quantGraphic.animation.curAnim.numFrames - 1)); - quant = quants[quantIndex]; - quantText.text = Std.string(quant); - } - public function inputOn(keybind:Int) { - heldKeybinds[keybind] = true; - placeNote(keybind); - } - public function placeNote(keybind:Int) { - var strumlineId:Int = 0; - var data:Int = keybind; - for (strumline in strumlines) { - if (data >= strumline.laneCount) { - data -= strumline.laneCount; - strumlineId ++; - } - } - var quantMultiplier:Float = (quant / 4); - var strumline:Strumline = strumlines.members[strumlineId]; - var lane = strumline.getLane(data); - var matchingNote:Null = null; - for (note in lane.notes) { - if (note.isHoldPiece) continue; - if (Math.abs(note.beatTime - conductorInUse.beat) < 1 / quantMultiplier - .0012) { - matchingNote = cast note; - break; - } - } - if (matchingNote == null) { - hitsound.play(true); - var isPlayer:Bool = (strumlineId == 0); - var snappedBeat:Float = Math.round(conductorInUse.beat * quantMultiplier) / quantMultiplier; - var note:CharterNote = new CharterNote(isPlayer, 0, data); - heldNotes[keybind] = note; - note.beatTime = snappedBeat; - note.preventDespawn = true; - note.justPlacing = true; - lane.insertNote(note); - } else { - var deletedNotes:Array = [matchingNote]; - for (child in matchingNote.children) - deletedNotes.push(cast child); - - for (note in deletedNotes) { - note.lane.dequeueNote(note); - note.lane.killNote(note); - } - addUndo({type: REMOVED_NOTES, notes: deletedNotes}); - } - } - public function inputOff(keybind:Int) { - heldKeybinds[keybind] = false; - var note:CharterNote = heldNotes[keybind]; - if (note != null) { - for (note in getSelectedNotes()) - note.selected = false; - - var addedNotes:Array = [note]; - for (child in note.children) - addedNotes.push(cast child); - - addUndo({type: PLACED_NOTES, notes: addedNotes}); - - FunkinSound.playOnce(Paths.sound('hitsoundTail'), .7); - note.justPlacing = false; - note.preventDespawn = false; - for (child in note.children) { - var charterNote:CharterNote = cast child; - if (charterNote == null) continue; - - charterNote.preventDespawn = false; - charterNote.justPlacing = false; - } - heldNotes[keybind] = null; + if (FlxG.keys.justPressed.SPACE) { + songPaused = !songPaused; } - } - public function updateHolds() { - var quantMultiplier:Float = (quant * .25); - var snappedBeat:Float = Math.round(conductorInUse.beat * quantMultiplier) / quantMultiplier; - for (note in heldNotes) { - if (note == null) continue; - - var lane:Lane = note.lane; - note.beatLength = snappedBeat - note.beatTime; - if (note.beatLength > 0) { - if (note.children.length == 0) { - var piece:CharterNote = new CharterNote(note.player, note.msTime, note.noteData, note.msLength, note.noteKind, true); - piece.justPlacing = note.justPlacing; - piece.preventDespawn = true; - note.children.push(piece); - piece.parent = note; - var tail:CharterNote = new CharterNote(note.player, note.msTime, note.noteData, 0, note.noteKind, true); - tail.justPlacing = note.justPlacing; - tail.preventDespawn = true; - note.children.push(tail); - tail.parent = note; - note.tail = tail; - - lane.insertNote(tail); - lane.insertNote(piece); - - piece.beatLength = note.beatLength; //What - tail.beatTime = snappedBeat; - } else { - var piece:Note = note.children[0]; - piece.beatLength = note.beatLength; - note.tail.beatTime = snappedBeat; - - lane.updateNote(note.tail); - lane.updateNote(piece); - } - } else { - while (note.children.length > 0) { - var child:Note = note.children.shift(); - lane.killNote(child); - child.destroy(); - } - } - } - } - public function saveToChart(chart:Chart) { - if (chart == null) return; - chart.notes.resize(0); - for (strumline in strumlines) { - for (lane in strumline.lanes) { - for (note in lane.getAllNotes()) { - if (note.isHoldPiece) continue; - chart.notes.push(note.toChartNote()); - } - } - } - chart.sort(); - chart.findSongLength(); - } - public function setWindowTitle(mod:Bool = false) { - var win:lime.ui.Window = FlxG.stage.window; - win.title = chart.name; - if (mod) - win.title += '*'; - if (chart.difficulty != '') - win.title += ' (' + chart.difficulty.toLowerCase() + ')'; - win.title += ' | ' + Main.windowTitle; + super.update(elapsed); } override public function destroy() { - instance = null; - inEditor = false; - FlxG.stage.window.title = Main.windowTitle; - FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, keyPressEvent); - FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, keyReleaseEvent); - FlxG.stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveEvent); Main.instance.removeChild(charterDisplay); - Main.watermark.visible = true; super.destroy(); + instance = null; } } -@:structInit class UndoAction { - public var notes:Array; - public var type:UndoActionType; - - public var pasted:Bool = false; - public var beatMod:Float = 0; - public var laneMod:Int = 0; - - public function message() { - var notesCount:Int = 0; - var longMatch:Bool = true; - var kindMatch:Bool = true; - var length:Null = null; - var kind:Null = null; - - for (note in notes) { - if (note.isHoldPiece) - continue; - - notesCount ++; - length ??= note.beatLength; - if ((length > 0) != (note.beatLength > 0)) - longMatch = false; - kind ??= note.noteKind; - if (kind != note.noteKind) - kindMatch = false; - } - - var notesStr:String = (longMatch && length > 0 ? 'long note' : 'note'); - if (notesCount != 1) notesStr += 's'; - if (kindMatch && kind != '') notesStr += ' ($kind)'; - if (notesCount != 1) notesStr = '$notesCount $notesStr'; - - return switch (type) { - case PLACED_NOTES: - '${pasted ? 'copied' : 'added'} $notesStr'; - case REMOVED_NOTES: - 'removed $notesStr'; - case SHIFTED_NOTES: - 'shifted $notesStr'; - } - } -} -enum UndoActionType { - PLACED_NOTES; - REMOVED_NOTES; - SHIFTED_NOTES; -} - -class MeasureLine extends FlxSpriteGroup { - public var barText:FlxText; - public var startTime:Float; - public var ySpacing(default, set):Float; - public var lineWidth(default, set):Float; - public var measureBeats(default, set):Int; - public var lines:FlxTypedSpriteGroup; //this is getting outta hand - - public function new(x:Float = 0, y:Float = 0, bar:Int = 0, time:Float = 0, beats:Int = 4, width:Float = 160, spacing:Float = 160) { - super(x, y); - lines = new FlxTypedSpriteGroup(); - measureBeats = beats; - ySpacing = spacing; - lineWidth = width; - startTime = time; - - add(lines); - - barText = new FlxText(0, 0, 400, Std.string(bar)); - barText.setFormat(Paths.font('vcr.ttf'), 40, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); - barText.y += -barText.height * .5 + 3; - barText.x = -barText.width - 24; - barText.active = false; - add(barText); - } - - public function set_measureBeats(newBeats:Int) { - if (measureBeats == newBeats) return newBeats; - - while (lines.length > 0 && lines.length >= newBeats) { - var line = lines.members[0]; - lines.remove(line, true); - line.destroy(); - } - var isBar:Bool = true; - for (i in 0...newBeats) { - var line:FunkinSprite; - if (i >= lines.length) { - line = new FunkinSprite(); - line.makeGraphic(1, 1, -1); - line.spriteOffset.y = .5; - line.active = false; - lines.add(line); - } else { - line = lines.members[i]; - } - line.y = i * ySpacing; - line.setGraphicSize(width, isBar ? 12 : 6); - line.alpha = (isBar ? .8 : .5); - line.updateHitbox(); - line.active = false; - isBar = false; - } - return measureBeats = newBeats; - } - public function set_ySpacing(newSpacing:Float) { - if (ySpacing == newSpacing) return newSpacing; - - var i:Int = 0; - for (line in lines) - line.y = (i ++) * newSpacing; - return ySpacing = newSpacing; - } - public function set_lineWidth(newWidth:Float) { - if (lineWidth == newWidth) return newWidth; - - for (line in lines) { - line.setGraphicSize(newWidth, line.height); - line.updateHitbox(); - } - return lineWidth = newWidth; - } -} +// OTHER CLASSES (move to funkin/debug/?) -class CharterPopUp extends Sprite { - public var message(default, set):String; - public var isUndo(default, set):Bool; - public var isRedo(default, set):Bool; - public var text:TextField; - public var icon:Bitmap; - - public function new() { - super(); - - icon = new Bitmap(); - icon.smoothing = true; - icon.scaleX = icon.scaleY = .65; - icon.y = 3; - - var smallTf:TextFormat = new TextFormat('_sans', 12, -1); - smallTf.letterSpacing = -1; - text = new TextField(); - text.autoSize = LEFT; - text.selectable = false; - text.mouseEnabled = false; - text.defaultTextFormat = smallTf; - addChild(text); - } - public function set_isRedo(isIt:Bool) { - icon.bitmapData = Paths.bmd('charter/' + (isIt ? 'redo' : 'undo')); - return isRedo = isIt; - } - public function set_isUndo(isIt:Bool) { - if (isIt && !contains(icon)) { - addChild(icon); - } else if (!isIt && contains(icon)) { - removeChild(icon); - } - text.x = (isIt ? 13 : 0); - return isUndo = isIt; - } - public function set_message(mes:String) { - text.text = mes; - return message = mes; - } -} class CharterDisplay extends Sprite { public var funnyQuarterNote:Bitmap; public var conductorText:TextField; @@ -1364,8 +241,7 @@ class CharterDisplay extends Sprite { repositionTexts(); }}); } - function getTextPos(i:Int) - return 35 + i * 15; + inline function getTextPos(i:Int) { return 35 + i * 15; } function repositionTexts() { for (i => pop in popUps) { if (!contains(pop)) continue; @@ -1378,7 +254,7 @@ class CharterDisplay extends Sprite { var metronomeTextT:String = ' = ${conductor.bpm}\n${conductor.timeSignature.toString()}'; var songPosTextT:String = FlxStringUtil.formatTime(conductor.songPosition * .001, true) + ' / ' + FlxStringUtil.formatTime(songLength * .001, true); var conductorTextT:String = 'Measure: ${Math.floor(conductor.bar)}\nBeat: ${Math.floor(conductor.beat)}\nStep: ${Math.floor(conductor.step)}'; - var noteInfoTextT:String = '${charter.hitNoteCount} notes (${charter.fullNoteCount} obj)'; + var noteInfoTextT:String = '${charter.noteCount} notes'; if (metronomeText.text != metronomeTextT) metronomeText.text = metronomeTextT; if (conductorText.text != conductorTextT) conductorText.text = conductorTextT; @@ -1404,189 +280,45 @@ class CharterDisplay extends Sprite { } } -class CharterStrumline extends Strumline { - public var receptorAlpha:Float = 1; - public var noteHighlight:FlxSprite; - - public function new(laneCount:Int = 4, direction:Float = 90, scrollSpeed:Float = 1) { - super(laneCount, direction, scrollSpeed); - noteHighlight = new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE); - noteHighlight.blend = ADD; - } - - public override function draw() { - for (lane in lanes) { // draw hit notes on bottom - if (!lane.selfDraw) { - if (lane.receptor != null) - lane.receptor.alpha = receptorAlpha; - for (note in lane.notes) { - var charterNote:CharterNote = cast note; - if (charterNote == null) continue; - - charterNote.drawCustom(true); - if (charterNote.wasGoodHit()) - drawHighlight(charterNote); - } - } - } - super.draw(); - for (lane in lanes) { // draw on top - if (!lane.selfDraw) { - if (lane.receptor != null) - lane.receptor.alpha = 1; - for (note in lane.notes) { - var charterNote:CharterNote = cast note; - if (charterNote == null) { - note.draw(); - continue; - } - - charterNote.drawCustom(false); - if (!charterNote.wasGoodHit()) - drawHighlight(charterNote); - } - lane.drawTop(); - } - } - } - public function drawHighlight(note:CharterNote) { - if (note.selected && !note.isHoldPiece) { - Util.copyColorTransform(noteHighlight.colorTransform, note.colorTransform); - noteHighlight.setGraphicSize(note.width, note.height); - noteHighlight.colorTransform.alphaMultiplier = .25; - noteHighlight.setPosition(note.x, note.y); - noteHighlight.updateHitbox(); - noteHighlight.draw(); - } - } -} -class CharterNote extends Note { - public var highlighted(default, set):Bool = false; - public var hitAlphaMult:Float = .7; - public var selected(default, set):Bool = false; - public var justPlacing:Bool = false; - public var justCopied:Bool = false; - public var useLaneRGB:Bool = true; - - var pointer:FlxObject; - var noteKindDecal:FunkinSprite = null; +class CharterPopUp extends Sprite { + public var message(default, set):String; + public var isUndo(default, set):Bool; + public var isRedo(default, set):Bool; + public var text:TextField; + public var icon:Bitmap; - public function new(player:Bool, msTime:Float, noteData:Int, msLength:Float = 0, type:String = '', isHoldPiece:Bool = false, ?conductor:Conductor) { - super(player, msTime, noteData, msLength, type, isHoldPiece, conductor); - pointer = new FlxObject(); - } - public function set_highlighted(isHighlighted:Bool) { - if (highlighted == isHighlighted) return isHighlighted; - for (child in children) - cast(child, CharterNote).highlighted = isHighlighted; - highlighted = isHighlighted; - updateHighlight(); - return isHighlighted; - } - public function set_selected(isSelected:Bool) { - if (selected == isSelected) return isSelected; - if (!isSelected) justCopied = false; - for (child in children) { - var charterNote:CharterNote = cast child; - charterNote.justCopied = justCopied; - charterNote.selected = isSelected; - } - selected = isSelected; - updateHighlight(); - return isSelected; - } - public function updateHighlight() { - if (selected) { - if (justCopied) { - setColorTransform(0, 1, 1); - } else { - setColorTransform(0, 1, 0); - } - } else if (highlighted) { - setColorTransform(1.6, 1.6, 1.6, 1, 32, 32, 32); - } else { - setColorTransform(); - } - } - public override function set_noteKind(newKind:String) { - if (noteKind == newKind) return newKind; - if (newKind == '') { - if (lane != null) shader = lane.rgbShader.shader; - noteKindDecal.destroy(); - noteKindDecal = null; - useLaneRGB = true; - } else { - shader = CharterState.genericRGB.shader; - useLaneRGB = false; - noteKindDecal ??= new FunkinSprite(); - noteKindDecal.loadTexture('charter/noteKinds/$newKind'); - if (noteKindDecal.graphic == null) noteKindDecal.loadTexture('charter/noteKinds/generic'); - } - return noteKind = newKind; - } - public function wasGoodHit() - return (goodHit || (parent != null && parent.goodHit)); - public function drawCustom(good:Bool = false) { - var wasGood:Bool = wasGoodHit(); - if (wasGood != good) return; + public function new() { + super(); - var prevAlpha:Float = alpha; - if (wasGood) alpha *= hitAlphaMult; - if (justPlacing) alpha *= .75; - actuallyDraw(); - alpha = prevAlpha; + icon = new Bitmap(); + icon.smoothing = true; + icon.scaleX = icon.scaleY = .65; + icon.y = 3; + + var smallTf:TextFormat = new TextFormat('_sans', 12, -1); + smallTf.letterSpacing = -1; + text = new TextField(); + text.autoSize = LEFT; + text.selectable = false; + text.mouseEnabled = false; + text.defaultTextFormat = smallTf; + addChild(text); } - public function actuallyDraw() { - super.draw(); - if (!isHoldPiece && noteKindDecal != null) { - Util.copyColorTransform(noteKindDecal.colorTransform, this.colorTransform); - noteKindDecal.scale.set(scale.x, scale.y); - noteKindDecal.updateHitbox(); - noteKindDecal.setPosition(x + (width - noteKindDecal.width) * .5, y + (height - noteKindDecal.height) * .5); - noteKindDecal.camera = camera; - noteKindDecal.draw(); - } - pointer.x = x; - pointer.y = y; - if (!isHoldPiece && noteKind != '' && highlighted && CharterState.inEditor && !FlxG.mouse.pressed) { - pointer.x += width * .7; - pointer.y += height * .3; - var charter:CharterState = CharterState.instance; - var bubblePos:FlxPoint = getPointerScreenPosition(pointer); - charter.noteKindBubble.setPosition( - bubblePos.x, - bubblePos.y - charter.noteKindBubble.height - ); - charter.noteKindBubble.draw(); - - charter.noteKindBubbleText.text = noteKind; - charter.noteKindBubbleText.setPosition( - charter.noteKindBubble.x + 46, - charter.noteKindBubble.y + 20 - ); - charter.noteKindBubbleText.draw(); - - if (noteKindDecal != null) { - noteKindDecal.camera = charter.camHUD; - noteKindDecal.setColorTransform(); - noteKindDecal.scale.set(.35, .35); - noteKindDecal.updateHitbox(); - noteKindDecal.alpha = .75; - noteKindDecal.setPosition( - charter.noteKindBubble.x - noteKindDecal.width * .5 + 25, - charter.noteKindBubble.y - noteKindDecal.height * .5 + 32, - ); - noteKindDecal.draw(); - } + public function set_isRedo(isIt:Bool) { + icon.bitmapData = Paths.bmd('charter/' + (isIt ? 'redo' : 'undo')); + return isRedo = isIt; + } + public function set_isUndo(isIt:Bool) { + if (isIt && !contains(icon)) { + addChild(icon); + } else if (!isIt && contains(icon)) { + removeChild(icon); } + text.x = (isIt ? 13 : 0); + return isUndo = isIt; } - public override function draw() {} - public function getPointerScreenPosition(pointer:FlxObject, ?camera:FlxCamera):FlxPoint { - camera ??= pointer.camera ?? FlxG.camera; - - var point:FlxPoint = pointer.getScreenPosition(camera); - point.subtract(camera.viewMarginLeft, camera.viewMarginTop); - point.scale(camera.zoom); - return point; + public function set_message(mes:String) { + text.text = mes; + return message = mes; } } \ No newline at end of file diff --git a/source/funkin/states/CharterStateOld.hx b/source/funkin/states/CharterStateOld.hx new file mode 100644 index 0000000..b8b3314 --- /dev/null +++ b/source/funkin/states/CharterStateOld.hx @@ -0,0 +1,1541 @@ +package funkin.states; + +/*import funkin.objects.Character; +import funkin.objects.play.Note; +import funkin.objects.play.*; +import funkin.shaders.RGBSwap; +import funkin.backend.play.Chart; +import funkin.states.FreeplaySubState.FreeplaySongText; + +import sys.thread.Thread; +import openfl.display.Sprite; +import openfl.display.Bitmap; +import openfl.text.TextFormat; +import openfl.text.TextField; +import openfl.events.MouseEvent; +import openfl.events.KeyboardEvent; +import flixel.util.FlxStringUtil; +import flixel.input.keyboard.FlxKey; +import flixel.addons.display.FlxBackdrop; + +class CharterState extends FunkinState { + public static var genericRGB:RGBSwap; + + public static var instance:CharterState; + public static var inEditor:Bool = false; + public static var chart:Chart; + + public var quant:Int = 4; + public var quantText:FlxText; + public var quantGraphic:FunkinSprite; + public var measureLines:FlxTypedSpriteGroup; + public var strumlines:FlxTypedSpriteGroup; + public var strumlineHighlight:FunkinSprite; + public var charterDisplay:CharterDisplay; + public var camHUD:FunkinCamera; + public var camScroll:FunkinCamera; + + public var noteKindBubble:FunkinSprite; + public var noteKindBubbleText:FreeplaySongText; + public var noteKindBubbleFocusGraphic:FlxGraphic; + + public var selectionBox:FlxSprite; + public var selectionLeniency:Float = 55; + public var pickedNote:CharterNote = null; + public var draggingNotes:Bool = false; + var pickedLaneIndex:Int = 0; + var pickedBeat:Float = 0; + + public var keybinds:Array> = []; + + var vocalsSounds:Array = []; + public var music:FunkinSoundGroup; + public var tickSound:FlxSound; + public var hitsound:FlxSound; + + public var scrollSpeed(default, set):Float = 1; + public var songPaused(default, set):Bool; + var visualScrollSpeed(get, never):Float; + + public var fullNoteCount:Int = 0; + public var hitNoteCount:Int = 0; + + var lastMouseY:Float = 0; + var scrolling:Bool = false; + var strumGrabY:Null = null; + var heldKeys:Array = []; + var heldKeybinds:Array = []; + var copiedNotes:Array = []; + var heldNotes:Array = []; + var quants:Array = [4, 8, 12, 16, 24, 32, 48, 64, 96, 192]; + + var undoMemory:Int = 15; + var undoActions:Array = []; + var redoActions:Array = []; + + public function new(?chart:Chart) { + super(); + CharterState.chart = chart ?? CharterState.chart ?? Chart.loadChart('test'); + } + + override public function create() { + super.create(); + Main.instance.addChild(charterDisplay = new CharterDisplay(conductorInUse = new Conductor())); // wow! + + beatHit.add(beatHitEvent); + barHit.add(barHitEvent); + + music = new FunkinSoundGroup(); + tickSound = FunkinSound.load(Paths.sound('beatTick')); + hitsound = FunkinSound.load(Paths.sound('hitsound')); + hitsound.volume = .7; + + genericRGB ??= new RGBSwap(0xb3a9b8, FlxColor.WHITE, 0x333333); + inEditor = true; + instance = this; + + FlxG.camera.zoom = .5; + + camHUD = new FunkinCamera(); + camScroll = new FunkinCamera(); + camScroll.bgColor.alpha = 0; + camHUD.bgColor.alpha = 0; + FlxG.cameras.add(camScroll, false); + FlxG.cameras.add(camHUD, false); + + noteKindBubble = new FunkinSprite().loadTexture('charter/bubble'); + noteKindBubble.scale.set(.75, .75); + noteKindBubble.updateHitbox(); + noteKindBubble.camera = camHUD; + noteKindBubble.color = 0xff323034; + noteKindBubble.alpha = .6; + noteKindBubbleText = new FreeplaySongText(0, 0, '', 0x808080, .8); //ffe0b0d0; + noteKindBubbleText.size = 16; + noteKindBubbleText.angle = -3; + noteKindBubbleText.origin.set(0, 6); + noteKindBubbleText.camera = camHUD; + + selectionBox = new FlxSprite().makeGraphic(1, 1, FlxColor.LIME); + selectionBox.camera = camScroll; + selectionBox.visible = false; + selectionBox.blend = ADD; + selectionBox.alpha = .25; + selectionBox.origin.set(); + add(selectionBox); + var background:FlxBackdrop = new FlxBackdrop(Paths.image('charter/bg')); + background.antialiasing = true; + background.scale.set(.85, .85); + background.velocity.set(5, 5); + add(background); + var underlay:FunkinSprite = new FunkinSprite(0, 0, false).makeGraphic(1, FlxG.height, 0xff101010); + underlay.screenCenter(); + underlay.alpha = .7; + add(underlay); + + measureLines = new FlxTypedSpriteGroup(); + strumlines = new FlxTypedSpriteGroup(); + add(measureLines); + add(strumlines); + + for (key in [FlxKey.ONE, FlxKey.TWO, FlxKey.THREE, FlxKey.FOUR, FlxKey.FIVE, FlxKey.SIX, FlxKey.SEVEN, FlxKey.EIGHT]) { + keybinds.push([key]); + heldNotes.push(null); + heldKeybinds.push(false); + } + + var strumlineSpacing:Float = 150; + var xx:Float = 0; + var h:Float = 0; + for (i in 0...2) { + var strumline = new CharterStrumline(4); + strumline.x = xx; + strumline.cpu = false; + strumline.oneWay = false; + strumlines.add(strumline); + xx += strumline.strumlineWidth + strumlineSpacing; + h = Math.max(h, strumline.strumlineHeight); + for (lane in strumline.lanes) { + lane.receptor.autoReset = true; + lane.oneWay = false; + } + } + strumlines.y = FlxG.height * .5 - h * .5 - 320; + strumlines.x = (FlxG.width - (xx - strumlineSpacing)) * .5; + + strumlineHighlight = new FunkinSprite().makeGraphic(1, 1, 0xffb094b0); + strumlineHighlight.setGraphicSize(strumlines.width, strumlines.height); + strumlineHighlight.updateHitbox(); + strumlineHighlight.blend = ADD; + strumlineHighlight.alpha = .25; + + FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressEvent); + FlxG.stage.addEventListener(KeyboardEvent.KEY_UP, keyReleaseEvent); + FlxG.stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveEvent); + + setWindowTitle(); + + chart.instLoaded = false; + chart.loadMusic('data/songs/${chart.path}/', false); + if (chart.instLoaded) { + music.add(chart.inst); + music.syncBase = chart.inst; + music.onSoundFinished.add((snd:FunkinSound) -> { + if (snd == music.syncBase) + finishSong(); + }); + conductorInUse.syncTracker = chart.inst; + } + loadVocals(chart.path, chart.audioSuffix); + + scrollSpeed = chart.scrollSpeed; + conductorInUse.metronome.tempoChanges = chart.tempoChanges; + + for (note in chart.notes) { + var targetNote:ChartNote = note.copy(); + var strumline:CharterStrumline = strumlines.members[targetNote.laneIndex]; + strumline?.queueNote(targetNote, null, false, false); + } + + songPaused = true; + recalculateNoteCount(); + charterDisplay.songLength = findSongLength(); + + var bgPadding:Float = 50; + underlay.setGraphicSize(strumlines.width + bgPadding * 2, FlxG.height * 5); + + quantGraphic = new FunkinSprite().loadAtlas('charter/quant'); + quantGraphic.addAnimation('quant', 'new quant', 0); + quantGraphic.playAnimation('quant', true); + quantGraphic.updateHitbox(); + //quantGraphic.x = strumlines.x + xx - strumlineSpacing; + quantGraphic.y = strumlines.y + (h - quantGraphic.height) * .5; + quantGraphic.screenCenter(X); + add(quantGraphic); + + quantText = new FlxText(0, 0, 300); + quantText.setFormat(Paths.ttf('vcr'), 40, FlxColor.WHITE, CENTER, OUTLINE, FlxColor.BLACK); + quantText.borderSize = 4; + quantText.updateHitbox(); + quantText.y = strumlines.y + (h - quantText.height) * .5; + quantText.screenCenter(X); + add(quantText); + changeQuant(0); + + add(strumlineHighlight); + + var measureBeats:Array = conductorInUse.getMeasureBeats(findSongLength()); + var beatLength:Float = Math.ceil(conductorInUse.convertMeasure(findSongLength(), MS, BEAT)); + for (measure => beat in measureBeats) { + conductorInUse.beat = beat; // todo: fix mid-bar bpm changes...hehe + var beatLength:Int = Std.int((measureBeats[measure + 1] ?? beatLength) - beat); + var spacing:Float = Note.msToDistance(conductorInUse.crochet, visualScrollSpeed); + var line:MeasureLine = new MeasureLine(strumlines.x - bgPadding, strumlines.y, measure, beat, beatLength, strumlines.width + bgPadding * 2, spacing); + measureLines.add(line); + } + + conductorInUse.songPosition = 0; + } + + public function loadVocals(path:String, audioSuffix:String = '') { + vocalsSounds.resize(0); + + for (chara in [chart.player1, chart.player2, chart.player3]) { + var sound:openfl.media.Sound = Character.getVocals(chart.path, chart.audioSuffix, chara); + if (sound != null) + vocalsSounds.push(FunkinSound.load(sound)); + } + if (vocalsSounds.length == 0) { + var sound:openfl.media.Sound = Character.getVocals(chart.path, chart.audioSuffix, ''); + if (sound != null) { + vocalsSounds.push(FunkinSound.load(sound)); + } else { + Log.warning('song vocals not found...'); + } + } + for (sound in vocalsSounds) { + sound.volume = 0; + sound.play().stop(); + sound.volume = 1; + music.add(sound); + } + } + + override public function update(elapsed:Float) { + if (FlxG.keys.justPressed.ENTER) { + var shifted:Bool = FlxG.keys.pressed.SHIFT; + chart.tempoChanges = conductorInUse.metronome.tempoChanges; + saveToChart(chart); + FlxG.switchState(() -> new PlayState(chart, shifted)); + return; + } + + // selection and dragging + var highlightedNote:CharterNote = null; + var highlightedSelNote:CharterNote = null; + var anyNoteHovered:Bool = (pickedNote != null); + forEachNote((note:Note) -> { + var charterNote:CharterNote = cast note; + if (charterNote == null) return; + + if (FlxG.mouse.overlaps(note)) { + anyNoteHovered = true; + if (charterNote.selected) { + if (highlightedSelNote == null) { + highlightedSelNote = charterNote; + } else { + var mousePoint:FlxPoint = FlxG.mouse.getWorldPosition(); + if (mousePoint.distanceTo(note.getMidpoint()) < mousePoint.distanceTo(highlightedSelNote.getMidpoint())) + highlightedSelNote = charterNote; + } + if (FlxG.mouse.justPressed) { + pickedNote = highlightedSelNote; + pickedBeat = pickedNote.beatTime; + pickedLaneIndex = laneToIndex(pickedNote.lane); + } + } else { + if (highlightedNote == null) { + highlightedNote = charterNote; + } else { + var mousePoint:FlxPoint = FlxG.mouse.getWorldPosition(); + if (mousePoint.distanceTo(note.getMidpoint()) < mousePoint.distanceTo(highlightedNote.getMidpoint())) + highlightedNote = charterNote; + } + } + } + }); + + var selectedAny:Bool = false; + var isSelecting:Bool = (selectionBox.visible && Math.abs(selectionBox.scale.x) >= 12 && Math.abs(selectionBox.scale.y) >= 12); + if (FlxG.mouse.pressed && strumGrabY == null) { + if (pickedNote != null) { + var pickedLane:Lane = null; // drag and twist notes + for (strumline in strumlines) { + for (lane in strumline.lanes) { + if (FlxG.mouse.x >= lane.receptor.x && FlxG.mouse.x <= lane.receptor.x + lane.receptor.width) { + pickedLane = lane; + break; + } + } + } + var selectedNotes:Array = getSelectedNotes(); + + readjustScrollCam(); + var quantMult:Float = (quant / 4); + var cursorBeatTime:Float = Note.distanceToMS(FlxG.mouse.getWorldPosition(camScroll).y, visualScrollSpeed) / conductorInUse.crochet; + var snappedBeatTime:Float = Math.round(cursorBeatTime * quantMult) / quantMult; + var beatDiff:Float = (snappedBeatTime - pickedNote.beatTime); + + if (shiftNotes(selectedNotes, beatDiff) != 0) + draggingNotes = true; + + if (pickedLane != null && pickedLane != pickedNote.lane) { + var laneDiff:Int = laneToIndex(pickedLane) - laneToIndex(pickedNote.lane); + if (twistNotes(selectedNotes, laneDiff) != 0) + draggingNotes = true; + } + } else { + var mousePos:FlxPoint = FlxG.mouse.getWorldPosition(camScroll); + if (FlxG.mouse.justPressed || !selectionBox.visible) { + selectionBox.setPosition(mousePos.x, mousePos.y); + selectionBox.visible = true; + } + selectionBox.scale.set(mousePos.x - selectionBox.x, mousePos.y - selectionBox.y); + } + } else if (selectionBox.visible) { + //do the selection! + var selectionBounds:FlxRect = selectionBox.getScreenBounds(null, camScroll); + if (selectionBox.scale.x < 0) selectionBounds.x += selectionBox.scale.x; + if (selectionBox.scale.y < 0) selectionBounds.y += selectionBox.scale.y; + + if (isSelecting) { + forEachNote((note:Note) -> { + var charterNote:CharterNote = cast note; + if (charterNote == null) return; + + charterNote.followLane(note.lane, note.lane.scrollSpeed); + + var noteBounds:FlxRect = charterNote.getScreenBounds(); + noteBounds.x += selectionLeniency; + noteBounds.y += selectionLeniency; + noteBounds.width -= selectionLeniency * 2; + noteBounds.height -= selectionLeniency * 2; + if (noteBounds.overlaps(selectionBounds)) { + charterNote.selected = true; + selectedAny = true; + } else if (!FlxG.keys.pressed.SHIFT) { + charterNote.selected = false; + } + }, true); + } + + selectionBox.visible = false; + } + if (highlightedNote == null) + highlightedNote = highlightedSelNote; + forEachNote((note:Note) -> { + var charterNote:CharterNote = cast note; + if (charterNote == null) return; + + charterNote.highlighted = (highlightedNote == note); + }); + if (FlxG.mouse.justReleased) { + if (pickedNote != null) { + final beatDiff:Float = pickedNote.beatTime - pickedBeat; + final laneDiff:Int = laneToIndex(pickedNote.laneIndex, pickedNote.strumlineIndex) - pickedLaneIndex; + if (beatDiff != 0 || laneDiff != 0) + addUndo({type: SHIFTED_NOTES, notes: getSelectedNotes(), laneMod: laneDiff, beatMod: beatDiff}); + } + if (!FlxG.keys.pressed.SHIFT && !draggingNotes && !selectedAny && strumGrabY == null) { + for (note in getSelectedNotes()) + note.selected = false; + } + if (highlightedNote != null) + highlightedNote.selected = true; + draggingNotes = false; + pickedNote = null; + } + + // time shift + if (!FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.W != FlxG.keys.pressed.S) { + var msPerSec:Float = (FlxG.keys.pressed.SHIFT ? 2000 : 1000); + if (FlxG.keys.pressed.W) + msPerSec *= -1; + + if (songPaused) + songPaused = false; + conductorInUse.paused = true; + conductorInUse.songPosition += elapsed * msPerSec; + restrictConductor(); + + if (conductorInUse.songPosition <= 0 && msPerSec < 0) { + music.stop(); + } else if (msPerSec != 1000 || !scrolling || !music.playing) { + if (!music.playing) { + music.play(true, conductorInUse.songPosition); + } else { + music.time = conductorInUse.songPosition; + } + } + + scrolling = true; + } else if (scrolling) { + songPaused = true; + scrolling = false; + } + + // receptor dragging + var strumlinesHighlighted:Bool = (FlxG.mouse.overlaps(strumlineHighlight) && !isSelecting && !anyNoteHovered && !FlxG.mouse.pressedRight); + strumlineHighlight.setPosition(strumlines.x, strumlines.y); + if (FlxG.mouse.justPressed && strumlinesHighlighted) + strumGrabY = (FlxG.mouse.y - strumlines.y); + if (FlxG.mouse.justReleased) + strumGrabY = null; + strumlineHighlight.visible = (strumlinesHighlighted || strumGrabY != null); + if (FlxG.mouse.pressed && strumGrabY != null) { + var h:Float = strumlines.height; + var middle:Float = (FlxG.height - h) * .5; + var maxDist:Float = (FlxG.height - h - 25) * .5 / FlxG.camera.zoom; + strumlines.y = Util.clamp(FlxG.mouse.y - strumGrabY, -maxDist + middle, maxDist + middle); + strumlineHighlight.setPosition(strumlines.x, strumlines.y); + + quantGraphic.y = strumlines.y + (h - quantGraphic.height) * .5; + quantText.y = strumlines.y + (h - quantText.height) * .5; + } + + if (FlxG.keys.justPressed.SPACE) songPaused = !songPaused; + super.update(elapsed); + forEachNote((note:Note) -> { + var lane:Lane = note.lane; + if (!conductorInUse.paused) { + if (conductorInUse.songPosition >= note.msTime && (conductorInUse.songPosition <= note.endMs || !note.goodHit)) { + lane.receptor?.playAnimation('confirm', true); + if (!note.goodHit) + hitsound.play(true); + note.goodHit = true; + } + } else { + note.goodHit = (conductorInUse.songPosition > note.msTime + 1); + } + }); + + if (songPaused) { + FlxG.camera.zoom = Util.smoothLerp(FlxG.camera.zoom, .5, elapsed * 9); + } else { + var metronome:Metronome = conductorInUse.metronome; + var beatZoom:Float = 1 - FlxEase.quintOut(metronome.beat % 1); + var barZoom:Float = 1 - FlxEase.quintOut(Math.min((metronome.bar % 1) * metronome.timeSignature.numerator, 1)); + FlxG.camera.zoom = .5 + beatZoom * .003 + barZoom * .005; + } + + readjustScrollCam(); + for (line in measureLines) { + // line.ySpacing = Note.msToDistance(conductorInUse.crochet, visualScrollSpeed); + if (Math.abs(line.startTime - conductorInUse.beat) > 16) { + if (line.alive) line.kill(); + continue; + } + + if (!line.alive) line.revive(); + line.y = strumlines.y + strumlines.height * .5 + Note.msToDistance(conductorInUse.convertMeasure(line.startTime, BEAT, MS) - conductorInUse.songPosition, visualScrollSpeed); + } + + if (!paused) + updateHolds(); + } + public function readjustScrollCam() { + camScroll.scroll.y = Note.msToDistance(conductorInUse.songPosition, visualScrollSpeed) - strumlines.y - strumlines.height * .5; + camScroll.zoom = FlxG.camera.zoom; + } + override public function updateConductor(elapsed:Float = 0) { + var prevStep:Int = curStep; + var prevBeat:Int = curBeat; + var prevBar:Int = curBar; + + conductorInUse.update(elapsed * 1000); + + curStep = Math.floor(conductorInUse.step); + curBeat = Math.floor(conductorInUse.beat); + curBar = Math.floor(conductorInUse.bar); + + if (!songPaused) { + if (prevBar != curBar) barHit.dispatch(curBar); + if (prevBeat != curBeat) beatHit.dispatch(curBeat); + if (prevStep != curStep) stepHit.dispatch(curStep); + } + } + + public function beatHitEvent(beat:Int) { + tickSound.play(true); + } + public function barHitEvent(bar:Int) {} + public function mouseMoveEvent(event:MouseEvent) { + if (FlxG.mouse.pressedRight) { + if (!songPaused) + songPaused = true; + conductorInUse.songPosition -= Note.distanceToMS((event.stageY - lastMouseY) / Util.gameScaleY / FlxG.camera.zoom, visualScrollSpeed); + restrictConductor(); + + if (event.stageY <= 5) { + lastMouseY = (FlxG.stage.window.height - 5) + event.stageY - 10; + FlxG.stage.window.warpMouse(Std.int(event.stageX), Std.int(lastMouseY)); + return; + } else if (event.stageY >= FlxG.stage.window.height - 5) { + lastMouseY = 10 + event.stageY - (FlxG.stage.window.height - 5); + FlxG.stage.window.warpMouse(Std.int(event.stageX), Std.int(lastMouseY)); + return; + } + } + lastMouseY = event.stageY; + } + public function finishSong() { + songPaused = true; + conductorInUse.songPosition = (music.syncBase?.length ?? 0); + } + public function forEachNote(func:Note -> Void, includeQueued:Bool = false) { + for (strumline in strumlines) + strumline.forEachNote(func, includeQueued); + } + public function getSelectedNotes() { + var list:Array = []; + forEachNote((note:Note) -> { + var charterNote:CharterNote = cast note; + if (charterNote == null) return; + + if (charterNote.selected) + list.push(charterNote); + }, true); + return list; + } + public function queueNote(note:ChartNote):ChartNote { + var strumline:Strumline = strumlines.members[note.strumlineIndex]; + strumline?.queueNote(note); + strumline?.updateQueue(); + return note; + } + public function spawnNote(note:ChartNote):CharterNote { + var strumline:Strumline = strumlines.members[note.strumlineIndex]; + var lane:Lane = strumline?.getNoteLane(note); + var note:CharterNote = null; + if (lane != null) { + note = lane.insertNote(note); + strumline?.updateQueue(); + } + return note; + } + public function shiftNotes(notesArray:Array, beatMod:Float = 0):Float { + if (beatMod == 0) return 0; + var beatDiff:Float = beatMod; + var minBeat:Float = Math.POSITIVE_INFINITY; + var maxBeat:Float = Math.NEGATIVE_INFINITY; + for (note in notesArray) { + if (note.beatTime < minBeat) minBeat = note.beatTime; + if (note.beatTime > maxBeat) maxBeat = note.beatTime; + } + if (minBeat + beatDiff < 0) + beatDiff = -minBeat; + // todo max beat + if (beatDiff != 0) { + for (note in notesArray) + shiftNote(note, note.beatTime + beatDiff); + } + return beatDiff; + } + public function twistNotes(notesArray:Array, laneMod:Int = 0):Int { + if (laneMod == 0) return 0; + + var laneDiff:Int = laneMod; + var minLane:Int = 9999; // prevent notes from going out of bounds + var maxLane:Int = -1; + for (note in notesArray) { + var laneIdx:Int = laneToIndex(note.laneIndex, note.strumlineIndex); + if (laneIdx < minLane) minLane = laneIdx; + if (laneIdx > maxLane) maxLane = laneIdx; + } + if (minLane + laneDiff < 0) + laneDiff = -minLane; + if (maxLane + laneDiff >= getNumLanes()) + laneDiff = getNumLanes() - maxLane - 1; + + if (laneDiff != 0) { + for (note in notesArray) { + var laneIdx:Int = laneToIndex(note.laneIndex, note.strumlineIndex); + var nextLane:Lane = indexToLane(laneIdx + laneDiff); + if (nextLane == null) continue; + twistNote(note, nextLane); + } + } + return laneDiff; + } + public function shiftNote(note:CharterNote, beatTime:Float) { + if (note == null) { + Log.warning('shiftNote: ???'); + return; + } + + note.msTime = conductorInUse.convertMeasure(beatTime, BEAT, MS); + note.updateChartNote(); + } + public function twistNote(note:CharterNote, lane:Lane) { + if (note == null || lane == null) { + Log.warning('twistNote: ???'); + return; + } + if (Std.isOfType(note, CharterNote)) { + var charterNote:CharterNote = cast note; + if (charterNote == null) return; + + if (charterNote.useLaneRGB) + note.shader = lane.rgbShader.shader; + } + + note.strumlineIndex = getStrumlineIndex(findLaneStrumline(lane)); + note.laneIndex = lane.laneIndex; + + note.lane.killNote(note); + queueNote(note.chartNote); + + note.lane = lane; + } + public function getStrumlineIndex(strumline:CharterStrumline):Int + return strumlines.members.indexOf(strumline); + public function findLaneStrumline(lane:Lane):Int { + for (strumline in strumlines) { + var laneIdx:Int = strumline.lanes.indexOf(lane); + if (laneIdx != -1) return laneIdx; + } + return -1; + } + public function indexToLane(index:Int) { + var n:Int = -1; + for (strumline in strumlines) { + for (lane in strumline.lanes) { + n ++; + if (n == index) + return lane; + } + } + return null; + } + public function laneToIndex(laneIndex:Int, strumlineIndex:Int) { + var n:Int = 0; + for (i => strumline in strumlines) { + if (i == strumlineIndex) return n + laneIndex; + n += strumline.laneCount; + } + return -1; + } + public function getNumLanes() { + var count:Int = 0; + for (strumline in strumlines) + count += strumline.lanes.length; + return count; + } + + public function set_scrollSpeed(newSpeed:Float) { + scrollSpeed = newSpeed; + for (strumline in strumlines) { + strumline.scrollSpeed = visualScrollSpeed; + } + return newSpeed; + } + public function get_visualScrollSpeed() { + return scrollSpeed / .7; + } + public function set_songPaused(isPaused:Bool) { + if (isPaused) { + music.stop(); + } else { + if (conductorInUse.songPosition >= (music.syncBase?.length ?? 0)) + return songPaused = true; + music.play(true, conductorInUse.songPosition); + } + + for (strumline in strumlines) { + FlxTween.cancelTweensOf(strumline, ['receptorAlpha']); + FlxTween.tween(strumline, {receptorAlpha: (isPaused ? .75 : 1)}, .25, {ease: FlxEase.circOut}); + for (lane in strumline.lanes) { + if (isPaused) + lane.receptor?.playAnimation('static'); + } + } + conductorInUse.paused = isPaused; + return songPaused = isPaused; + } + + public function keyPressEvent(event:KeyboardEvent) { + var key:FlxKey = event.keyCode; + if (!heldKeys.contains(key)) heldKeys.push(key); + + var keybind:Int = Controls.keybindFromArray(keybinds, key); + if (keybind >= 0 && FlxG.keys.checkStatus(key, JUST_PRESSED)) + inputOn(keybind); + + var noteControlMode:Bool = FlxG.keys.pressed.CONTROL; + var scrollMod:Int = 1; + var leniency:Float = 1 / 256; + var prevBeat:Float = conductorInUse.beat; + var quantMultiplier:Float = (quant * .25); + var pauseChart:Bool = false; + if (noteControlMode) { + keyPressNoteControl(key); + } + switch (key) { + case FlxKey.DELETE: + var deletedNotes:Array = []; + for (note in getSelectedNotes()) { + deletedNotes.push(note.chartNote); + note.lane.dequeueNote(note); + note.lane.killNote(note); + } + if (deletedNotes.length > 0) + addUndo({type: REMOVED_NOTES, notes: deletedNotes}); + case FlxKey.LEFT | FlxKey.RIGHT: + if (key == FlxKey.LEFT) scrollMod *= -1; + if (noteControlMode) { + twistNotes(getSelectedNotes(), scrollMod); + } else { + changeQuant(scrollMod); + } + case FlxKey.UP | FlxKey.DOWN: + if (key == FlxKey.UP) scrollMod *= -1; + if (noteControlMode) { + shiftNotes(getSelectedNotes(), scrollMod / quantMultiplier); + } else { + placeNotes(); + pauseChart = true; + var targetBeat:Float = prevBeat + scrollMod / quantMultiplier; + if (Math.abs(prevBeat - Math.round(prevBeat * quantMultiplier) / quantMultiplier) < leniency * 2) + conductorInUse.beat = Math.round(targetBeat * quantMultiplier) / quantMultiplier; + else + conductorInUse.beat = (scrollMod > 0 ? Math.floor : Math.ceil)(targetBeat * quantMultiplier) / quantMultiplier; + } + case FlxKey.PAGEUP | FlxKey.PAGEDOWN: + placeNotes(); + pauseChart = true; + if (key == FlxKey.PAGEUP) scrollMod *= -1; + if (Math.abs(conductorInUse.bar - Std.int(conductorInUse.bar)) < (1 / quant - .0006)) + conductorInUse.bar = Math.max(0, conductorInUse.bar + scrollMod); + conductorInUse.bar = (scrollMod < 0 ? Math.floor : Math.ceil)(conductorInUse.bar); + case FlxKey.HOME: + pauseChart = true; + conductorInUse.songPosition = 0; + case FlxKey.END: + pauseChart = true; + conductorInUse.songPosition = findSongLength(); + default: + } + + if (pauseChart && !songPaused) + songPaused = true; + + restrictConductor(); + updateHolds(); + } + public function undo() { + var undoAction:UndoAction = undoActions.pop(); + if (undoAction != null) { + // Sys.println('undoing ${undoAction.type}'); + redoActions.unshift(undoAction); + switch (undoAction.type) { + case PLACED_NOTES: + for (note in undoAction.notes) { + note.lane.killNote(note); + } + case REMOVED_NOTES: + for (note in undoAction.notes) { + var note:CharterNote = spawnNote(note); + note.selected = true; + } + case SHIFTED_NOTES: + twistNotes(undoAction.notes, -undoAction.laneMod); + shiftNotes(undoAction.notes, -undoAction.beatMod); + default: + } + charterDisplay.addMessage(undoAction.message(), true, false); + recalculateNoteCount(); + } + } + public function redo() { + var undoAction:UndoAction = redoActions.shift(); + if (undoAction != null) { + // Sys.println('redoing ${undoAction.type}'); + undoActions.push(undoAction); + switch (undoAction.type) { + case PLACED_NOTES: + for (note in undoAction.notes) { + note.lane.queueNote(note); + note.selected = true; + } + case REMOVED_NOTES: + for (note in undoAction.notes) { + note.lane.dequeueNote(note); + note.lane.killNote(note); + } + case SHIFTED_NOTES: + twistNotes(undoAction.notes, undoAction.laneMod); + shiftNotes(undoAction.notes, undoAction.beatMod); + default: + } + charterDisplay.addMessage(undoAction.message(), true, true); + recalculateNoteCount(); + } + } + public function addUndo(action:UndoAction) { + // Sys.println('added undo action ${action.type}'); + setWindowTitle(true); + undoActions.push(action); + + while (undoActions.length > undoMemory) { + var undoAction:UndoAction = undoActions.shift(); + destroyUndoAction(undoAction, false); + } + for (undoAction in redoActions) + destroyUndoAction(undoAction, true); + redoActions.resize(0); + + charterDisplay.addMessage(action.message(), false, true); + recalculateNoteCount(); + } + public function destroyUndoAction(action:UndoAction, parallel:Bool = false) { + switch (action.type) { // parallel: for redo + case PLACED_NOTES | REMOVED_NOTES: + if (parallel == (action.type == REMOVED_NOTES)) + return; + for (note in action.notes) + note.destroy(); + default: + } + } + public function recalculateNoteCount() { + fullNoteCount = 0; + hitNoteCount = 0; + for (strumline in strumlines) { + strumline.forEachNote((note:Note) -> { + hitNoteCount ++; + fullNoteCount ++; + }, true); + } + } + public function copySelectedNotes() { + var selectedNotes:Array = getSelectedNotes(); + if (selectedNotes.length > 0) { + copiedNotes.resize(0); + for (note in selectedNotes) + copiedNotes.push(note.chartNote); + + copiedNotes.sort(Chart.sortByTime); + } + } + public function keyPressNoteControl(key:FlxKey) { + switch (key) { + case FlxKey.Z: + undo(); + case FlxKey.Y: + redo(); + case FlxKey.C: // COPY + copySelectedNotes(); + case FlxKey.V: // PASTE + for (note in getSelectedNotes()) + note.selected = false; + if (copiedNotes.length > 0) { + var generatedNotes:Array = []; + + var quantMult:Float = quant / 4; + var beatDiff:Float = (Math.round(conductorInUse.beat * quantMult) / quantMult - generatedNotes[0].beatTime); + + for (note in generatedNotes) { + var note:Note = generateNote(note.chartNote); + + note.beatTime += beatDiff; + note.updateChartNote(); + note.justCopied = true; + note.selected = true; + } + addUndo({type: PLACED_NOTES, notes: generatedNotes, pasted: true}); + } + case FlxKey.X: // CUT + copySelectedNotes(); + var deletedNotes:Array = []; + for (note in getSelectedNotes()) { + note.lane.dequeueNote(note); + note.lane.killNote(note); + deletedNotes.push(note); + } + if (deletedNotes.length > 0) + addUndo({type: REMOVED_NOTES, notes: deletedNotes}); + case FlxKey.A: // SELECT ALL + var selectedAny:Bool = false; + forEachNote((note:Note) -> { + var charterNote:CharterNote = cast note; + if (charterNote == null) return; + + if (!charterNote.selected) { + charterNote.selected = true; + selectedAny = true; + } + }, true); + + if (!selectedAny) { + forEachNote((note:Note) -> { + var charterNote:CharterNote = cast note; + if (charterNote == null) return; + + charterNote.selected = false; + }, true); + } + default: + } + } + public function restrictConductor() { + var limitTime:Float = Math.max(conductorInUse.songPosition, 0); + limitTime = Math.min(limitTime, music.syncBase?.length ?? 0); + + conductorInUse.songPosition = limitTime; + } + public function placeNotes() { + for (key => held in heldKeybinds) { + if (held && heldNotes[key] == null) + placeNote(key); + } + } + public function findSongLength() { + var length:Null = chart?.songLength; + if (length == null) // todo + length = 0; + return length; + } + public function keyReleaseEvent(event:KeyboardEvent) { + var key:FlxKey = event.keyCode; + heldKeys.remove(key); + + var keybind:Int = Controls.keybindFromArray(keybinds, key); + if (keybind >= 0) inputOff(keybind); + } + + public function changeQuant(mod:Int) { + var quantIndex:Int = FlxMath.wrap(quants.indexOf(quant) + mod, 0, quants.length - 1); + quantGraphic.animation.curAnim.curFrame = Std.int(Math.min(quantIndex, quantGraphic.animation.curAnim.numFrames - 1)); + quant = quants[quantIndex]; + quantText.text = Std.string(quant); + } + public function inputOn(keybind:Int) { + heldKeybinds[keybind] = true; + placeNote(keybind); + } + public function placeNote(keybind:Int) { + var strumlineId:Int = 0; + var data:Int = keybind; + for (strumline in strumlines) { + if (data >= strumline.laneCount) { + data -= strumline.laneCount; + strumlineId ++; + } + } + var quantMultiplier:Float = (quant / 4); + var strumline:Strumline = strumlines.members[strumlineId]; + var lane = strumline.getLane(data); + var matchingNote:Null = null; + for (note in lane.notes) { + if (note.isHoldPiece) continue; + if (Math.abs(note.beatTime - conductorInUse.beat) < 1 / quantMultiplier - .0012) { + matchingNote = cast note; + break; + } + } + if (matchingNote == null) { + hitsound.play(true); + var isPlayer:Bool = (strumlineId == 0); + var snappedBeat:Float = Math.round(conductorInUse.beat * quantMultiplier) / quantMultiplier; + var note:CharterNote = new CharterNote(isPlayer, 0, data); + heldNotes[keybind] = note; + note.beatTime = snappedBeat; + note.preventDespawn = true; + note.justPlacing = true; + lane.insertNote(note); + } else { + var deletedNotes:Array = [matchingNote]; + + for (note in deletedNotes) { + note.lane.dequeueNote(note); + note.lane.killNote(note); + } + addUndo({type: REMOVED_NOTES, notes: deletedNotes}); + } + } + public function inputOff(keybind:Int) { + heldKeybinds[keybind] = false; + var note:CharterNote = heldNotes[keybind]; + if (note != null) { + for (note in getSelectedNotes()) + note.selected = false; + + var addedNotes:Array = [note]; + + addUndo({type: PLACED_NOTES, notes: addedNotes}); + + FunkinSound.playOnce(Paths.sound('hitsoundTail'), .7); + note.justPlacing = false; + note.preventDespawn = false; + + heldNotes[keybind] = null; + } + } + public function updateHolds() { + var quantMultiplier:Float = (quant * .25); + var snappedBeat:Float = Math.round(conductorInUse.beat * quantMultiplier) / quantMultiplier; + for (note in heldNotes) { + if (note == null) continue; + + note.beatLength = snappedBeat - note.beatTime; + note.updateChartNote(); + } + } + public function saveToChart(chart:Chart) { + if (chart == null) return; + chart.notes.resize(0); + for (strumline in strumlines) { + for (lane in strumline.lanes) { + for (note in lane.getAllNotes()) { + if (note.isHoldPiece) continue; + chart.notes.push(note.toChartNote()); + } + } + } + chart.sort(); + chart.findSongLength(); + } + public function setWindowTitle(mod:Bool = false) { + var win:lime.ui.Window = FlxG.stage.window; + win.title = chart.name; + if (mod) + win.title += '*'; + if (chart.difficulty != '') + win.title += ' (' + chart.difficulty.toLowerCase() + ')'; + + win.title += ' | ' + Main.windowTitle; + } + + override public function destroy() { + instance = null; + inEditor = false; + FlxG.stage.window.title = Main.windowTitle; + FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, keyPressEvent); + FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, keyReleaseEvent); + FlxG.stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveEvent); + Main.instance.removeChild(charterDisplay); + super.destroy(); + } +} + +@:structInit class UndoAction { + public var notes:Array; + public var type:UndoActionType; + + public var pasted:Bool = false; + public var beatMod:Float = 0; + public var laneMod:Int = 0; + + public function message() { + var notesCount:Int = 0; + var longMatch:Bool = true; + var kindMatch:Bool = true; + var length:Null = null; + var kind:Null = null; + + for (note in notes) { + notesCount ++; + length ??= note.beatLength; + if ((length > 0) != (note.beatLength > 0)) + longMatch = false; + kind ??= note.noteKind; + if (kind != note.noteKind) + kindMatch = false; + } + + var notesStr:String = (longMatch && length > 0 ? 'long note' : 'note'); + if (notesCount != 1) notesStr += 's'; + if (kindMatch && kind != '') notesStr += ' ($kind)'; + if (notesCount != 1) notesStr = '$notesCount $notesStr'; + + return switch (type) { + case PLACED_NOTES: + '${pasted ? 'copied' : 'added'} $notesStr'; + case REMOVED_NOTES: + 'removed $notesStr'; + case SHIFTED_NOTES: + 'shifted $notesStr'; + } + } +} +enum UndoActionType { + PLACED_NOTES; + REMOVED_NOTES; + SHIFTED_NOTES; +} + +class MeasureLine extends FlxSpriteGroup { + public var barText:FlxText; + public var startTime:Float; + public var ySpacing(default, set):Float; + public var lineWidth(default, set):Float; + public var measureBeats(default, set):Int; + public var lines:FlxTypedSpriteGroup; //this is getting outta hand + + public function new(x:Float = 0, y:Float = 0, bar:Int = 0, time:Float = 0, beats:Int = 4, width:Float = 160, spacing:Float = 160) { + super(x, y); + lines = new FlxTypedSpriteGroup(); + measureBeats = beats; + ySpacing = spacing; + lineWidth = width; + startTime = time; + + add(lines); + + barText = new FlxText(0, 0, 400, Std.string(bar)); + barText.setFormat(Paths.font('vcr.ttf'), 40, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); + barText.y += -barText.height * .5 + 3; + barText.x = -barText.width - 24; + barText.active = false; + add(barText); + } + + public function set_measureBeats(newBeats:Int) { + if (measureBeats == newBeats) return newBeats; + + while (lines.length > 0 && lines.length >= newBeats) { + var line = lines.members[0]; + lines.remove(line, true); + line.destroy(); + } + var isBar:Bool = true; + for (i in 0...newBeats) { + var line:FunkinSprite; + if (i >= lines.length) { + line = new FunkinSprite(); + line.makeGraphic(1, 1, -1); + line.spriteOffset.y = .5; + line.active = false; + lines.add(line); + } else { + line = lines.members[i]; + } + line.y = i * ySpacing; + line.setGraphicSize(width, isBar ? 12 : 6); + line.alpha = (isBar ? .8 : .5); + line.updateHitbox(); + line.active = false; + isBar = false; + } + return measureBeats = newBeats; + } + public function set_ySpacing(newSpacing:Float) { + if (ySpacing == newSpacing) return newSpacing; + + var i:Int = 0; + for (line in lines) + line.y = (i ++) * newSpacing; + return ySpacing = newSpacing; + } + public function set_lineWidth(newWidth:Float) { + if (lineWidth == newWidth) return newWidth; + + for (line in lines) { + line.setGraphicSize(newWidth, line.height); + line.updateHitbox(); + } + return lineWidth = newWidth; + } +} + +class CharterPopUp extends Sprite { + public var message(default, set):String; + public var isUndo(default, set):Bool; + public var isRedo(default, set):Bool; + public var text:TextField; + public var icon:Bitmap; + + public function new() { + super(); + + icon = new Bitmap(); + icon.smoothing = true; + icon.scaleX = icon.scaleY = .65; + icon.y = 3; + + var smallTf:TextFormat = new TextFormat('_sans', 12, -1); + smallTf.letterSpacing = -1; + text = new TextField(); + text.autoSize = LEFT; + text.selectable = false; + text.mouseEnabled = false; + text.defaultTextFormat = smallTf; + addChild(text); + } + public function set_isRedo(isIt:Bool) { + icon.bitmapData = Paths.bmd('charter/' + (isIt ? 'redo' : 'undo')); + return isRedo = isIt; + } + public function set_isUndo(isIt:Bool) { + if (isIt && !contains(icon)) { + addChild(icon); + } else if (!isIt && contains(icon)) { + removeChild(icon); + } + text.x = (isIt ? 13 : 0); + return isUndo = isIt; + } + public function set_message(mes:String) { + text.text = mes; + return message = mes; + } +} +class CharterDisplay extends Sprite { + public var funnyQuarterNote:Bitmap; + public var conductorText:TextField; + public var noteInfoText:TextField; + public var songPosText:TextField; + public var metronomeText:TextField; + public var background:Bitmap; + + public var conductor:Conductor; + public var songLength:Float = 0; + + public var popUps:Array = []; + // maybe put the pop ups in a container + + public function new(conductor:Conductor) { + super(); + + this.conductor = conductor; + + var metronomeTf:TextFormat = new TextFormat(Paths.ttf('vcr'), 15, -1); + metronomeTf.leading = -2; + + background = new Bitmap(new openfl.display.BitmapData(1, 1, true, FlxColor.BLACK)); + background.alpha = .6; + addChild(background); + + funnyQuarterNote = new Bitmap(Paths.bmd('charter/quarterNote'), null, true); + funnyQuarterNote.x = 13; + addChild(funnyQuarterNote); + conductorText = new TextField(); + conductorText.defaultTextFormat = metronomeTf; + noteInfoText = new TextField(); + noteInfoText.defaultTextFormat = new TextFormat(Paths.ttf('vcr'), 11, -1); + noteInfoText.defaultTextFormat.letterSpacing = -1; + noteInfoText.alpha = .75; + songPosText = new TextField(); + songPosText.defaultTextFormat = new TextFormat(Paths.ttf('vcr'), 12, -1); + metronomeText = new TextField(); + metronomeText.defaultTextFormat = new TextFormat(Paths.ttf('vcr'), 18, -1); + + for (text in [conductorText, songPosText, noteInfoText, metronomeText]) { + text.x = 10; + text.autoSize = LEFT; + text.multiline = true; + text.selectable = false; + text.mouseEnabled = false; + addChild(text); + } + } + public function addMessage(message:String, isUndo:Bool = false, isRedo:Bool = false) { + var i:Int = 0; + var toUse:CharterPopUp = null; + for (pop in popUps) { + if (!contains(pop)) { + toUse = pop; + break; + } + i ++; + } + var maxHeight:Float = FlxG.stage.window.height - 96; + if (getTextPos(0) >= maxHeight) + toUse = popUps[0]; + else if (getTextPos(i) >= maxHeight) + toUse = popUps[i - 1]; + + if (toUse == null) { + toUse = new CharterPopUp(); + popUps.push(toUse); + } + toUse.message = message; + toUse.isUndo = isUndo; + toUse.isRedo = isRedo; + toUse.alpha = .75; + toUse.x = 10; + + if (!contains(toUse)) + addChild(toUse); + repositionTexts(); + FlxTween.cancelTweensOf(toUse); + FlxTween.tween(toUse, {alpha: 0}, .5, {startDelay: .75, onComplete: (_) -> { + if (contains(toUse)) + removeChild(toUse); + popUps.push(popUps.shift()); + repositionTexts(); + }}); + } + function getTextPos(i:Int) + return 35 + i * 15; + function repositionTexts() { + for (i => pop in popUps) { + if (!contains(pop)) continue; + pop.y = getTextPos(i); + } + } + public function updateMetronomeInfo() { + var charter:CharterState = CharterState.instance; + + var metronomeTextT:String = ' = ${conductor.bpm}\n${conductor.timeSignature.toString()}'; + var songPosTextT:String = FlxStringUtil.formatTime(conductor.songPosition * .001, true) + ' / ' + FlxStringUtil.formatTime(songLength * .001, true); + var conductorTextT:String = 'Measure: ${Math.floor(conductor.bar)}\nBeat: ${Math.floor(conductor.beat)}\nStep: ${Math.floor(conductor.step)}'; + var noteInfoTextT:String = '${charter.hitNoteCount} notes (${charter.fullNoteCount} obj)'; + + if (metronomeText.text != metronomeTextT) metronomeText.text = metronomeTextT; + if (conductorText.text != conductorTextT) conductorText.text = conductorTextT; + if (noteInfoText.text != noteInfoTextT) noteInfoText.text = noteInfoTextT; + if (songPosText.text != songPosTextT) songPosText.text = songPosTextT; + } + + override function __enterFrame(deltaTime:Float) { + updateMetronomeInfo(); + + noteInfoText.x = FlxG.stage.window.width - 12 - noteInfoText.textWidth; + + var h:Float = FlxG.stage.window.height; + songPosText.y = h - songPosText.textHeight - 12; + noteInfoText.y = h - noteInfoText.textHeight - 12; + conductorText.y = h - conductorText.textHeight - 32; + var infoHeight:Float = songPosText.textHeight + conductorText.textHeight + 32; + + funnyQuarterNote.y = metronomeText.y = h - infoHeight - metronomeText.textHeight; + background.scaleX = Math.max(Math.max(songPosText.textWidth, conductorText.textWidth) + 24, 120); + background.scaleY = infoHeight + metronomeText.textHeight + 10; + background.y = h - background.scaleY; + } +} + +class CharterStrumline extends Strumline { + public var receptorAlpha:Float = 1; + public var noteHighlight:FlxSprite; + + public function new(laneCount:Int = 4, direction:Float = 90, scrollSpeed:Float = 1) { + super(laneCount, direction, scrollSpeed, CharterNote); + noteHighlight = new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE); + noteHighlight.blend = ADD; + } + + public override function draw() { + for (lane in lanes) { // draw hit notes on bottom + if (!lane.selfDraw) { + if (lane.receptor != null) + lane.receptor.alpha = receptorAlpha; + + for (note in lane.notes) { + if (!Std.isOfType(note, CharterNote)) continue; + var charterNote:CharterNote = cast note; + + charterNote.drawCustom(true); + if (charterNote.goodHit) + drawHighlight(charterNote); + } + } + } + drawSelf(); + for (lane in lanes) { // draw on top + if (!lane.selfDraw) { + if (lane.receptor != null) + lane.receptor.alpha = 1; + + for (note in lane.notes) { + var charterNote:CharterNote = cast note; + + charterNote.drawCustom(false); + if (!charterNote.goodHit) + drawHighlight(charterNote); + } + @:privateAccess lane.drawThing(true); + } + } + } + public function drawHighlight(note:CharterNote) { + if (note.selected) { + Util.copyColorTransform(noteHighlight.colorTransform, note.colorTransform); + noteHighlight.setGraphicSize(note.width, note.height); + noteHighlight.colorTransform.alphaMultiplier = .25; + noteHighlight.setPosition(note.x, note.y); + noteHighlight.updateHitbox(); + noteHighlight.draw(); + } + } +} +class CharterNote extends Note { + public var highlighted(default, set):Bool = false; + public var hitAlphaMult:Float = .7; + public var selected(default, set):Bool = false; + public var justPlacing:Bool = false; + public var justCopied:Bool = false; + public var useLaneRGB:Bool = true; + + var pointer:FlxObject; + var noteKindDecal:FunkinSprite = null; + + public function new(songNote:ChartNote, ?conductor:Conductor) { + super(songNote, conductor); + pointer = new FlxObject(); + } + public function set_highlighted(isHighlighted:Bool) { + if (highlighted == isHighlighted) return isHighlighted; + for (child in children) + cast(child, CharterNote).highlighted = isHighlighted; + highlighted = isHighlighted; + updateHighlight(); + return isHighlighted; + } + public function set_selected(isSelected:Bool) { + if (selected == isSelected) return isSelected; + if (!isSelected) justCopied = false; + for (child in children) { + var charterNote:CharterNote = cast child; + charterNote.justCopied = justCopied; + charterNote.selected = isSelected; + } + preventDespawn = isSelected; + selected = isSelected; + updateHighlight(); + return isSelected; + } + public function updateHighlight() { + if (selected) { + if (justCopied) { + setColorTransform(0, 1, 1); + } else { + setColorTransform(0, 1, 0); + } + } else if (highlighted) { + setColorTransform(1.6, 1.6, 1.6, 1, 32, 32, 32); + } else { + setColorTransform(); + } + } + public override function set_kind(newKind:String) { + if (kind == newKind) return newKind; + + if (newKind == '') { + if (lane != null) shader = lane.rgbShader.shader; + noteKindDecal.destroy(); + noteKindDecal = null; + useLaneRGB = true; + } else { + shader = CharterState.genericRGB.shader; + useLaneRGB = false; + noteKindDecal ??= new FunkinSprite(); + noteKindDecal.loadTexture('charter/noteKinds/$newKind'); + if (noteKindDecal.graphic == null) noteKindDecal.loadTexture('charter/noteKinds/generic'); + } + + return kind = newKind; + } + public function drawCustom(good:Bool = false) { + if (goodHit != good) return; + + var prevAlpha:Float = alpha; + if (wasGood) alpha *= hitAlphaMult; + if (justPlacing) alpha *= .75; + actuallyDraw(); + alpha = prevAlpha; + } + public function actuallyDraw() { + super.draw(); + if (noteKindDecal != null) { + Util.copyColorTransform(noteKindDecal.colorTransform, this.colorTransform); + noteKindDecal.scale.set(scale.x, scale.y); + noteKindDecal.updateHitbox(); + noteKindDecal.setPosition(x + (width - noteKindDecal.width) * .5, y + (height - noteKindDecal.height) * .5); + noteKindDecal.camera = camera; + noteKindDecal.draw(); + } + pointer.x = x; + pointer.y = y; + if (noteKind != '' && highlighted && CharterState.inEditor && !FlxG.mouse.pressed) { + pointer.x += width * .7; + pointer.y += height * .3; + var charter:CharterState = CharterState.instance; + var bubblePos:FlxPoint = getPointerScreenPosition(pointer); + charter.noteKindBubble.setPosition( + bubblePos.x, + bubblePos.y - charter.noteKindBubble.height + ); + charter.noteKindBubble.draw(); + + charter.noteKindBubbleText.text = noteKind; + charter.noteKindBubbleText.setPosition( + charter.noteKindBubble.x + 46, + charter.noteKindBubble.y + 20 + ); + charter.noteKindBubbleText.draw(); + + if (noteKindDecal != null) { + noteKindDecal.camera = charter.camHUD; + noteKindDecal.setColorTransform(); + noteKindDecal.scale.set(.35, .35); + noteKindDecal.updateHitbox(); + noteKindDecal.alpha = .75; + noteKindDecal.setPosition( + charter.noteKindBubble.x - noteKindDecal.width * .5 + 25, + charter.noteKindBubble.y - noteKindDecal.height * .5 + 32, + ); + noteKindDecal.draw(); + } + } + } + public override function draw() {} + public function getPointerScreenPosition(pointer:FlxObject, ?camera:FlxCamera):FlxPoint { + camera ??= pointer.camera ?? FlxG.camera; + + var point:FlxPoint = pointer.getScreenPosition(camera); + point.subtract(camera.viewMarginLeft, camera.viewMarginTop); + point.scale(camera.zoom); + return point; + } +}*/ \ No newline at end of file diff --git a/source/funkin/states/CrashState.hx b/source/funkin/states/CrashState.hx index 7e4c97c..415b547 100644 --- a/source/funkin/states/CrashState.hx +++ b/source/funkin/states/CrashState.hx @@ -30,8 +30,7 @@ class CrashState extends FlxState { super.create(); - FlxG.sound.music.stop(); - Main.watermark.visible = false; + FlxG.sound.music?.stop(); Main.instance.removeChild(Main.debugDisplay); function loadCrashAsset(sprite:FunkinSprite, ?stack:Array) { @@ -247,7 +246,6 @@ $message'; override public function destroy() { Main.instance.addChild(Main.debugDisplay); - Main.watermark.visible = true; super.destroy(); error = null; diff --git a/source/funkin/states/FreeplayState.hx b/source/funkin/states/FreeplayState.hx index e374ce4..6af9da8 100644 --- a/source/funkin/states/FreeplayState.hx +++ b/source/funkin/states/FreeplayState.hx @@ -16,8 +16,12 @@ class FreeplayState extends FunkinState { public static var selectedDifficulty:Int = 0; public static var currentVariation:String = 'default'; + var fallbackVariation:Variation = new Variation('unknown', {difficulties: ['easy', 'normal', 'hard'], suffix: '', name: 'Unknown'}); + var _hasUnknown:Bool = false; + override public function create() { Mods.currentMod = null; + super.create(); playMusic(MainMenuState.menuMusic); @@ -31,7 +35,7 @@ class FreeplayState extends FunkinState { diffText.setFormat(Paths.ttf('vcr'), 18, FlxColor.WHITE, RIGHT, OUTLINE, FlxColor.BLACK); diffText.scrollFactor.set(); add(diffText); - + displayItems = new FlxTypedGroup(); add(displayItems); @@ -44,10 +48,12 @@ class FreeplayState extends FunkinState { loadLevel('$folder/$level', path.mod); } } + if (_hasUnknown) + variationList.push(fallbackVariation); FlxG.camera.target = target = new FlxObject(); FlxG.camera.followLerp = 9 / 60; - + if (currentVariation == null) currentVariation = variationList[0].internalName; displayVariation(findVariation(currentVariation)); selectDifficulty(); @@ -56,8 +62,8 @@ class FreeplayState extends FunkinState { Main.showWatermark = true; - DiscordRPC.presence.details = 'Navigating freeplay'; - DiscordRPC.dirty = true; + DiscordRpc.presence.details = 'Navigating freeplay'; + DiscordRpc.dirty = true; } override public function update(elapsed:Float) { @@ -76,11 +82,15 @@ class FreeplayState extends FunkinState { inputEnabled = false; new FlxTimer().start(2, (timer:FlxTimer) -> { + var difficulty:Null = null; var shifted:Bool = FlxG.keys.pressed.SHIFT; var variation:Variation = findVariation(currentVariation); + + if (variation != null) difficulty = variation.difficulties[selectedDifficulty]; + var selectedItem:SongItem = displayItems.members[selection]; Mods.currentMod = selectedItem.mod ?? ''; - var chart:Chart = Chart.loadChart(selectedItem.songPath, variation.difficulties[selectedDifficulty], variation.suffix); + var chart:Chart = Chart.loadChart(selectedItem.songPath, difficulty, variation.suffix); FlxG.switchState(() -> new PlayState(chart, shifted)); }); } @@ -91,6 +101,8 @@ class FreeplayState extends FunkinState { // TODO: this doesn't work like how i wanted it to... public function selectDifficulty(mod:Int = 0) { + if (variationList.length == 0) return; + var variation:Variation = findVariation(currentVariation); var difficulties:Array = variation.difficulties; var nextVariation:Variation = variation; @@ -118,7 +130,7 @@ class FreeplayState extends FunkinState { diffText.text = 'VARIATION: ${nextVariation.name}\n${difficulties[selectedDifficulty].toUpperCase()}'; } public function select(mod:Int = 0) { - if (items.length == 0) return; + if (displayItems.length == 0) return; if (mod != 0) FunkinSound.playOnce(Paths.sound('scrollMenu'), .8); displayItems.members[selection]?.highlight(false); @@ -150,17 +162,27 @@ class FreeplayState extends FunkinState { var content:String = File.getContent(path); var levels:LevelsData = TJSON.parse(content); for (song in levels.songList) { + Mods.currentMod = mod; + if (song.showOnFreeplay != null && !song.showOnFreeplay) continue; var i:Int = items.length - 1; var item:SongItem = new SongItem(0, 0, song.displayName, song.icon); item.songPath = song.songPath; item.mod = mod ?? ''; items.push(item); + for (variationName in (song.variations ?? levels.variations)) { loadVariation(variationName, mod); var variation:Variation = findVariation(variationName); - if (variation != null) + if (variation != null) { item.variations.push(variation); + } else { + variation = variationList[0]; + variation ??= fallbackVariation; + item.variations.push(fallbackVariation); + + if (variation == fallbackVariation) _hasUnknown = true; + } } var songPath:String = 'data/songs/${song.songPath}'; @@ -172,6 +194,8 @@ class FreeplayState extends FunkinState { } catch (e:haxe.Exception) { Log.error('error loading level @ "$path" -> ${e.details()}'); } + + Mods.currentMod = null; } public function loadVariation(name:String, ?mod:String) { try { diff --git a/source/funkin/states/FreeplaySubState.hx b/source/funkin/states/FreeplaySubState.hx index 515e4d0..ebf8af4 100644 --- a/source/funkin/states/FreeplaySubState.hx +++ b/source/funkin/states/FreeplaySubState.hx @@ -33,7 +33,7 @@ class FreeplaySubState extends FunkinState { backingCard.slideIn(); add(backingCard); - conductorInUse.beatHit.add(beatHitEvent); + beatHit.add(beatHitEvent); angleMask = new AngleMask(); bg = new FunkinSprite(FlxG.width, 0).loadTexture('freeplay/freeplayBGdad'); @@ -64,7 +64,7 @@ class FreeplaySubState extends FunkinState { add(fnfFreeplay); add(ostName); - var testCaspule:FreeplayCapsule = new FreeplayCapsule(400, 400, 'TESTING!'); + var testCaspule:FreeplayCapsule = new FreeplayCapsule(400, 400, 'TESTING!', MEDIUM); add(testCaspule); playMusic('freeplayRandom'); @@ -221,12 +221,12 @@ class FreeplaySongText extends FlxSpriteGroup { public var font(default, set):Null; public var glowColor(default, set):FlxColor; - public function new(x:Float = 0, y:Float = 0, ?text:String, glowColor:FlxColor = 0xff00ccff, intensity:Float = 1) { + public function new(x:Float = 0, y:Float = 0, ?text:String, glowColor:FlxColor = 0xff00ccff, intensity:Float = 1, quality:BitmapFilterQuality = LOW) { super(x, y); blurText = new FlxText(0, 0, 'Random', 40); whiteText = new FlxText(0, 0, 'Random', 40); - textGlowFilter = new GlowFilter(glowColor, 1, intensity * 5, intensity * 5, 210, BitmapFilterQuality.LOW); - blurText.textField.filters = [new openfl.filters.BlurFilter(intensity * 3, intensity * 3, BitmapFilterQuality.LOW)]; + textGlowFilter = new GlowFilter(glowColor, 1, intensity * 5, intensity * 5, 210, quality); + blurText.textField.filters = [new openfl.filters.BlurFilter(intensity * 3, intensity * 3, quality)]; whiteText.textField.filters = [textGlowFilter]; blurText.blend = BlendMode.ADD; @@ -260,7 +260,7 @@ class FreeplaySongText extends FlxSpriteGroup { } } -class FreeplayCapsule extends FlxSpriteGroup { +class FreeplayCapsule extends FunkinSpriteGroup { public var capsule:FunkinSprite; public var bpmText:FunkinSprite; public var weekType:FunkinSprite; @@ -275,7 +275,8 @@ class FreeplayCapsule extends FlxSpriteGroup { capsule.playAnimation('unselected'); add(capsule); - songText = new FreeplaySongText(capsule.width * 0.26, 45, text); + songText = new FreeplaySongText(capsule.width * .26, 45, text); + songText.size = Std.int(songText.size * .8); add(songText); bpmText = new FunkinSprite(144, 87).loadTexture('freeplay/capsule/bpmtext'); diff --git a/source/funkin/states/GameOverSubState.hx b/source/funkin/states/GameOverSubState.hx index 3e8368d..05ec475 100644 --- a/source/funkin/states/GameOverSubState.hx +++ b/source/funkin/states/GameOverSubState.hx @@ -6,12 +6,7 @@ import funkin.objects.CharacterGroup; import openfl.media.Sound; class GameOverSubState extends FunkinState { - public var cam:FunkinCamera; - public var playState:PlayState; public var cameraZoom:Float = 1; - public var started:Bool = false; - public var confirmed:Bool = false; - public var wasInstant:Bool = false; public var character:Character = null; public var deathAnimationPostfix:String = ''; @@ -19,12 +14,21 @@ class GameOverSubState extends FunkinState { public var musicPath:String = 'gameplay/gameOver/gameOver'; public var startMusicPath:String = 'gameplay/gameOver/gameOverStart'; public var confirmMusicPath:String = 'gameplay/gameOver/gameOverEnd'; + public var musicVolume:Float = 1; public var sound:Sound; public var music:Sound; public var startMusic:Sound; public var confirmMusic:Sound; + var cam:FunkinCamera; + var playState:PlayState; + var waitTimer:FlxTimer = null; + public var started:Bool = false; + public var canStart:Bool = true; + public var confirmed:Bool = false; + public var wasInstant:Bool = false; + public function new(instant:Bool = true) { super(); @@ -34,6 +38,7 @@ class GameOverSubState extends FunkinState { FlxG.camera = cam = playState.camGame; playState.hscripts.run('death', [instant, this]); + playState.dispatchSongEvent({type: DEATH_INIT, character: character, subState: this}); if (character != null) { cameraZoom = character.deathData?.cameraZoom ?? 1; @@ -53,27 +58,14 @@ class GameOverSubState extends FunkinState { public override function create() { FlxG.state.persistentDraw = false; FlxG.state.persistentUpdate = false; + if (character != null) { add(character); focusOnCharacter(character); playState.stage.remove(character); - var aniName:String = 'firstDeath$deathAnimationPostfix'; - if (character.animationExists(aniName, true)) { - character.playAnimation(aniName); - character.onAnimationComplete.add((anim:String) -> { - if (!started && anim == aniName) - startGameOver(); - }); - } - new FlxTimer().start(2.5, (_) -> { - if (!started && character.isAnimationFinished()) - startGameOver(); - }); - } else { - new FlxTimer().start(2.5, (_) -> startGameOver()); } - if (sound != null) - FunkinSound.playOnce(sound); + + playState.dispatchSongEvent({type: DEATH_FIRST, character: character, subState: this}); playState.hscripts.run('deathCreate', [wasInstant, this]); } public override function update(elapsed:Float) { @@ -83,14 +75,8 @@ class GameOverSubState extends FunkinState { FlxG.switchState(FreeplayState.new); return; } - if (FlxG.keys.justPressed.ENTER) { - if (!confirmed) { - endGameOver(); - if (playState != null) - playState.hscripts.run('deathConfirm'); - } else { - // g - } + if (FlxG.keys.justPressed.ENTER && !confirmed) { + endGameOver(); } super.update(elapsed); @@ -112,34 +98,62 @@ class GameOverSubState extends FunkinState { } } public function startGameOver() { + playState.dispatchSongEvent({type: DEATH_START, character: character, subState: this}); + } + public function endGameOver() { + playState.dispatchSongEvent({type: DEATH_CONFIRM, character: character, subState: this}); + } + + public function firstDeathEvent() { + if (character != null) { + var aniName:String = 'firstDeath$deathAnimationPostfix'; + if (character.animationExists(aniName, true)) { + character.playAnimation(aniName); + character.onAnimationComplete.add((ani:String) -> { + if (canStart && !started && ani == aniName) + startGameOver(); + }); + } + new FlxTimer().start(2.5, (_) -> { + if (!started && canStart && character.isAnimationFinished()) + startGameOver(); + }); + } else { + new FlxTimer().start(2.5, (_) -> startGameOver()); + } + if (sound != null) + FunkinSound.playOnce(sound); + } + public function startDeathEvent() { character?.playAnimation('deathLoop$deathAnimationPostfix'); if (confirmed) return; if (startMusic != null) { - FlxG.sound.playMusic(startMusic, 1, false); + FlxG.sound.playMusic(startMusic, musicVolume, false); FlxG.sound.music.onComplete = () -> { if (music != null) - FlxG.sound.playMusic(music); + FlxG.sound.playMusic(music, musicVolume); }; } else if (music != null) { - FlxG.sound.playMusic(music); + FlxG.sound.playMusic(music, musicVolume); } started = true; playState.hscripts.run('deathStart', [this]); } - public function endGameOver() { + public function confirmDeathEvent() { + confirmed = true; if (!started) { started = true; + playState.dispatchSongEvent({type: DEATH_START, character: character, subState: this}); playState.hscripts.run('deathStart', [this]); } - confirmed = true; character?.playAnimation('deathConfirm$deathAnimationPostfix'); if (FlxG.sound.music != null) FlxG.sound.music.stop(); if (confirmMusic != null) - FlxG.sound.playMusic(confirmMusic, 1, false); + FlxG.sound.playMusic(confirmMusic, musicVolume, false); new FlxTimer().start(.7, (_) -> { FlxG.camera.fade(FlxColor.BLACK, 2, false, () -> { FlxG.resetState(); }); }); diff --git a/source/funkin/states/MainMenuState.hx b/source/funkin/states/MainMenuState.hx index efb0018..0303d33 100644 --- a/source/funkin/states/MainMenuState.hx +++ b/source/funkin/states/MainMenuState.hx @@ -43,8 +43,8 @@ class MainMenuState extends FunkinState { select(); FlxG.camera.snapToTarget(); - DiscordRPC.presence.details = 'In the main menu'; - DiscordRPC.dirty = true; + DiscordRpc.presence.details = 'In the main menu'; + DiscordRpc.dirty = true; Paths.clean(); } diff --git a/source/funkin/states/ModMenuState.hx b/source/funkin/states/ModMenuState.hx index 821bc3e..ea675d9 100644 --- a/source/funkin/states/ModMenuState.hx +++ b/source/funkin/states/ModMenuState.hx @@ -77,8 +77,8 @@ class ModMenuState extends FunkinState { } shiftCapsules(); - DiscordRPC.presence.details = 'In the mod menu'; - DiscordRPC.dirty = true; + DiscordRpc.presence.details = 'In the mod menu'; + DiscordRpc.dirty = true; } public function stepHitEvent(step:Int) { diff --git a/source/funkin/states/OptionsState.hx b/source/funkin/states/OptionsState.hx index 3527760..6ccb293 100644 --- a/source/funkin/states/OptionsState.hx +++ b/source/funkin/states/OptionsState.hx @@ -1,35 +1,37 @@ package funkin.states; -import funkin.objects.Alphabet; +import funkin.objects.ui.*; +import funkin.objects.ui.SettingItem; class OptionsState extends FunkinState { + public var bg:FunkinSprite; public var target:FlxObject; - public var items:FlxTypedGroup; + public var items:TextItemGroup; public var inputEnabled:Bool = true; - public var settingList:Array = [ - {save: 'downscroll', display: 'Downscroll'}, - {save: 'middlescroll', display: 'Middlescroll'}, - {save: 'ghostTapping', display: 'Ghost Tapping'}, - {save: 'xtendScore', display: 'Extended Score Display'} - ]; + public static var selection:Int = 0; override public function create() { super.create(); playMusic(MainMenuState.menuMusic); - var bg:FunkinSprite = new FunkinSprite().loadTexture('mainmenu/bgMagenta'); + + bg = new FunkinSprite().loadTexture('mainmenu/bgMagenta'); bg.setGraphicSize(bg.width * 1.1); bg.scrollFactor.set(); bg.updateHitbox(); bg.screenCenter(); add(bg); - items = new FlxTypedGroup(); + items = new TextItemGroup(); + items.itemDrift = 12.5; add(items); - for (i => setting in settingList) - items.add(new SettingItem(12.5 * i, 75 * i, setting.save, setting.display, setting.type)); + items.addItem(new CheckboxItem('Downscroll', 'downscroll')); + items.addItem(new CheckboxItem('Middlescroll', 'middlescroll')); + items.addItem(new CheckboxItem('Ghost Tapping', 'ghostTapping')); + items.addItem(new CheckboxItem('Extended Score Display', 'xtendScore')); + items.select(selection, false); FlxG.camera.target = target = new FlxObject(); FlxG.camera.followLerp = 9 / 60; @@ -38,8 +40,8 @@ class OptionsState extends FunkinState { Main.showWatermark = true; - DiscordRPC.presence.details = 'Navigating options'; - DiscordRPC.dirty = true; + DiscordRpc.presence.details = 'Navigating options'; + DiscordRpc.dirty = true; } override public function update(elapsed:Float) { @@ -48,101 +50,16 @@ class OptionsState extends FunkinState { if (FlxG.keys.justPressed.UP) select(-1); if (FlxG.keys.justPressed.DOWN) select(1); - if (FlxG.keys.justPressed.ENTER) { - var curSetting:SettingItem = items.members[selection]; - if (curSetting != null && curSetting.type == BOOLEAN) { - curSetting.enabled = !curSetting.enabled; - } - } - if (FlxG.keys.justPressed.ESCAPE) { - FlxG.switchState(MainMenuState.new); - } + if (FlxG.keys.justPressed.ENTER) items.confirm(); + if (FlxG.keys.justPressed.ESCAPE) FlxG.switchState(MainMenuState.new); } public function select(mod:Int = 0) { - if (items.length == 0) return; - if (mod != 0) FunkinSound.playOnce(Paths.sound('scrollMenu'), .8); + items.select(mod); - items.members[selection].highlight(false); + if (items.selectedItem != null) + target.setPosition(items.selectedItem.x + 400, items.selectedItem.getMidpoint().y); - selection = FlxMath.wrap(selection + mod, 0, items.length - 1); - var selectedItem:SettingItem = items.members[selection]; - selectedItem.highlight(); - - target.setPosition(selectedItem.x + 400, selectedItem.getMidpoint().y); + selection = items.selection; } -} - -class SettingItem extends FlxSpriteGroup { - public var text:Alphabet; - public var type:SettingType; - public var checkbox:FunkinSprite = null; - public var settingSave:Null = null; - public var settingValue(get, default):Dynamic; - public var enabled(default, set):Bool = false; - - public function new(x:Float = 0, y:Float = 0, ?save:String, name:String = 'Unknown', type:SettingType = BOOLEAN) { - super(x, y); - - settingSave = save; - text = new Alphabet(100, 0, name); - text.scaleTo(.75, .75); - add(text); - - this.type = type; - switch (type) { - case NUMBER: - case STRING: - case BOOLEAN: - checkbox = new FunkinSprite(0, -30); - checkbox.scale.set(.5, .5); - checkbox.loadAtlas('options/checkbox'); - checkbox.addAnimation('select', 'checkbox select'); - checkbox.addAnimation('unselect', 'checkbox unselect'); - checkbox.setAnimationOffset('select', 12, 40); - checkbox.playAnimation('unselect'); - checkbox.finishAnimation(); - checkbox.updateHitbox(); - - enabled = settingValue; - checkbox.finishAnimation(); - add(checkbox); - default: - } - highlight(false); - } - inline function hasSave() return (settingSave != null && Reflect.getProperty(Options.data, settingSave) != null); - public function get_settingValue() { - return Reflect.getProperty(Options.data, settingSave); - } - public function set_enabled(on:Bool) { - if (type != BOOLEAN) return on; - // trace('$settingSave -> ${hasSave()}'); - if (hasSave() && on != settingValue) Reflect.setProperty(Options.data, settingSave, on); - checkbox.playAnimation(on ? 'select' : 'unselect'); - return enabled = on; - } - public function highlight(on:Bool = true) { - if (on) { - checkbox.alpha = 1; - text.alpha = 1; - text.color = 0xffcc66; - } else { - checkbox.alpha = .65; - text.alpha = .65; - text.color = 0xffffff; - } - } -} - -typedef SettingData = { - var save:String; - var display:String; - var ?type:SettingType; -} - -enum abstract SettingType(String) to String { - var BOOLEAN = 'bool'; - var NUMBER = 'number'; - var STRING = 'string'; } \ No newline at end of file diff --git a/source/funkin/states/PauseSubState.hx b/source/funkin/states/PauseSubState.hx new file mode 100644 index 0000000..8d541cd --- /dev/null +++ b/source/funkin/states/PauseSubState.hx @@ -0,0 +1,40 @@ +package funkin.states; + +import funkin.objects.ui.*; + +class PauseSubState extends FunkinState { + public var items:TextItemGroup; + + public var playState:PlayState = null; + + public override function new(?playState:PlayState) { + super(); + this.bgColor = 0x80000000; + this.playState = playState; + } + + public override function create() { + super.create(); + + items = new TextItemGroup(); + items.addItem(new TextItem('Resume', function() close())); + items.addItem(new TextItem('Restart', function() playState?.restartSong())); + items.addItem(new TextItem('Exit to Menu', function() FlxG.switchState(FreeplayState.new))); + items.screenCenter(Y); + items.select(); + add(items); + + items.x = -items.width; + FlxTween.tween(items, {x: 75}, .35, {ease: FlxEase.expoOut}); + + camera = FunkinCamera.topCamera; + } + + public override function update(elapsed:Float):Void { + super.update(elapsed); + + if (FlxG.keys.justPressed.UP) items.select(-1); + if (FlxG.keys.justPressed.DOWN) items.select(1); + if (FlxG.keys.justPressed.ENTER) items.confirm(); + } +} \ No newline at end of file diff --git a/source/funkin/states/PlayState.hx b/source/funkin/states/PlayState.hx index fd1d4e8..f2bcfab 100644 --- a/source/funkin/states/PlayState.hx +++ b/source/funkin/states/PlayState.hx @@ -2,14 +2,19 @@ package funkin.states; import openfl.events.KeyboardEvent; import flixel.input.keyboard.FlxKey; +import flixel.util.FlxSignal.FlxTypedSignal; import funkin.backend.scripting.HScript; import funkin.backend.play.ScoreHandler; +import funkin.backend.play.ScoreSystem; +import funkin.backend.play.IPlayEvent; +import funkin.backend.play.SongEvent; +import funkin.backend.play.NoteStyle; import funkin.backend.play.NoteEvent; -import funkin.backend.play.Scoring; import funkin.backend.play.Chart; import funkin.objects.CharacterGroup; import funkin.objects.Character; +import funkin.objects.play.Note; import funkin.objects.play.*; import funkin.objects.*; @@ -27,18 +32,18 @@ class PlayState extends FunkinState { public var healthBar:Bar; public var iconP1:HealthIcon; public var iconP2:HealthIcon; - public var scoreTxt:FlxText; - public var debugTxt:FlxText; + public var scoreTxt:FunkinText; + public var uiGroup:FunkinSpriteGroup; + public var ratingGroup:FunkinTypedSpriteGroup; public var playerStrumline:Strumline; public var opponentStrumline:Strumline; - public var uiGroup:FunkinSpriteGroup; - public var ratingGroup:FunkinTypedSpriteGroup; public var strumlineGroup:FunkinTypedSpriteGroup; public var singAnimations:Array = ['LEFT', 'DOWN', 'UP', 'RIGHT']; public var keybinds:Array> = []; public var heldKeys:Array = []; + public var pauseDisabled:Bool = false; public var inputDisabled:Bool = false; public var playCountdown:Bool = true; public var autoUpdateRPC:Bool = true; @@ -47,22 +52,25 @@ class PlayState extends FunkinState { public var camGame:FunkinCamera; public var camOther:FunkinCamera; public var camFocusTarget:FlxObject; - public var spotlight:Null; + public var spotlight(default, set):Null; - public var camZoomRate:Int = -1; // 0: no bop - <0: every measure (always) + public var camLocked:Bool = false; + public var camZooming:Bool = true; public var camZoomIntensity:Float = 1; public var hudZoomIntensity:Float = 2; + public var camZoomRate:Null = null; // 0: no bop - null: every measure (always) public static var chart:Chart = null; - public var notes:Array = []; + public var notes:Array = []; public var songName:String; public var simple:Bool; - public var scoring:ScoreHandler = new ScoreHandler(EMI); + public var scoring:ScoreHandler = new ScoreHandler(new EmiScoreSystem()); @:isVar public var score(get, set):Float = 0; @:isVar public var misses(get, set):Int = 0; @:isVar public var combo(get, set):Int = 0; public var accuracy(get, null):Float = 0; + public var ghostTapping:Bool; public var maxHealth(default, set):Float = 1; public var health(default, dynamic):Float = .5; @@ -78,9 +86,45 @@ class PlayState extends FunkinState { public var godmode:Bool; public var downscroll:Bool; public var middlescroll:Bool; + public var audioOffset:Float; + public var songStarted:Bool = false; public var songFinished:Bool = false; + public var noteStyle(default, set):NoteStyleAsset; + + public var fadeNotes:Bool = true; + static var wooshNotes:Array = []; + static var restartingSong:Bool = false; + + function set_noteStyle(newStyle:NoteStyleAsset):NoteStyleAsset { + if (noteStyle == newStyle) return newStyle; + + var stylePath:String = NoteStyle.getPath(noteStyle); + hscripts.find('(notes/$stylePath) Style Script')?.kill(); + + bootStyleScript(newStyle, 'notes'); + + for (strumline in strumlineGroup) + strumline.loadStyle(newStyle); + + return noteStyle = newStyle; + } + function bootStyleScript(style:NoteStyleAsset, folder:String = 'notes') { + var stylePath:String = NoteStyle.getPath(style); + var styleScriptPath:Null = Paths.getPath('scripts/styles/$folder/$stylePath.hx'); + + if (styleScriptPath != null) { + var scriptName:String = '($folder/$stylePath) Style Script'; + var foundScript:HScript = hscripts.find(scriptName); + if (foundScript == null) { + hscripts.loadFromFile(styleScriptPath, scriptName, ['style' => NoteStyle.fetch(style)]); + } else { + foundScript.revive(); + } + } + } + public function new(chart:Chart, simple:Bool = false) { PlayState.chart = chart ?? PlayState.chart ?? new Chart(''); PlayState.chart.instLoaded = false; @@ -91,6 +135,11 @@ class PlayState extends FunkinState { comboBroken(scoring.combo); } else { popCombo(newCombo); + + if (stage != null) { + for (chara in stage.characters) + chara.playComboAnimation(newCombo); + } } }); super(); @@ -98,75 +147,131 @@ class PlayState extends FunkinState { override public function create() { super.create(); - Main.watermark.visible = false; + godmode = false; // practice mode? downscroll = Options.data.downscroll; middlescroll = Options.data.middlescroll; + ghostTapping = Options.data.ghostTapping; conductorInUse = new Conductor(); - conductorInUse.metronome.tempoChanges = chart.tempoChanges; + conductorInUse.copyTempoChanges(chart.tempoChanges); + barHit.add((t:Int) -> dispatchSongEvent({type: BAR_HIT, time: t})); + beatHit.add((t:Int) -> dispatchSongEvent({type: BEAT_HIT, time: t})); + stepHit.add((t:Int) -> dispatchSongEvent({type: STEP_HIT, time: t})); + conductorInUse.sortTempoChanges(); hitsound = FunkinSound.load(Paths.sound('gameplay/hitsounds/hitsound'), .7); music = new FunkinSoundGroup(); + audioOffset = chart.audioOffset; songName = chart.name; - var genNotes:Array = chart.generateNotes(); + @:privateAccess FlxG.cameras.defaults.resize(0); + camOther = new FunkinCamera(); + camGame = new FunkinCamera(); + camHUD = new FunkinCamera(); + camHUD.bgColor.alpha = 0; + camGame.bgColor.alpha = 0; + camOther.bgColor.alpha = 0; + FlxG.cameras.add(camGame, true); + FlxG.cameras.add(camHUD, false); + FlxG.cameras.add(camOther, false); + + camFocusTarget = new FlxObject(0, FlxG.height * .5); + camGame.follow(camFocusTarget, LOCKON, 3); + add(camFocusTarget); + + camGame.zoomFollowLerp = camHUD.zoomFollowLerp = 3; + + uiGroup = new FunkinSpriteGroup(); + uiGroup.cameras = [camHUD]; + uiGroup.zIndex = 75; + add(uiGroup); + strumlineGroup = new FunkinTypedSpriteGroup(); + strumlineGroup.cameras = [camHUD]; + strumlineGroup.zIndex = 100; + add(strumlineGroup); + ratingGroup = new FunkinTypedSpriteGroup(); + + var strumlineBound:Float = (FlxG.width - 300) * .5; + var strumlineY:Float = 50; + + var chartStyle:String = chart.noteStyle; + var mania:String = '${chart.keyCount}k'; + keybinds = Options.data.keybinds[mania] ?? Options.data.keybinds['4k']; + if (NoteStyle.exists('$chartStyle-$mania')) { + noteStyle = '$chartStyle-$mania'; + } else { + noteStyle = chartStyle; + } + + opponentStrumline = new Strumline(chart.keyCount, 90, chart.scrollSpeed); + opponentStrumline.noteEvent.add(opponentNoteEvent); + opponentStrumline.zIndex = 40; + opponentStrumline.cpu = true; + opponentStrumline.allowInput = false; + + playerStrumline = new Strumline(chart.keyCount, 90, chart.scrollSpeed); + playerStrumline.noteEvent.add(playerNoteEvent); + playerStrumline.assignKeybinds(keybinds); + playerStrumline.zIndex = 50; + + strumlineGroup.add(opponentStrumline); + strumlineGroup.add(playerStrumline); + + // the good stuff + var time:Float = Sys.time(); + + Log.minor('cloning notes'); + for (note in chart.notes) + notes.push(note.copy()); if (!simple) { var loadedEvents:Array = []; var noteKinds:Array = []; - for (note in genNotes) { - var noteKind:String = note.noteKind; - if (noteKind.trim() != '' && !noteKinds.contains(noteKind)) { - hscripts.loadFromPaths('scripts/notekinds/$noteKind.hx'); - noteKinds.push(noteKind); - } - } + for (event in chart.events) { var eventName:String = event.name; if (!loadedEvents.contains(eventName)) { loadedEvents.push(eventName); hscripts.loadFromPaths('scripts/events/$eventName.hx'); } - events.push(event); - pushedEvent(event); } hscripts.loadFromFolder('scripts/global'); hscripts.loadFromFolder('scripts/songs/${chart.path}'); + + Log.minor('loading notekinds'); + for (note in notes) { + var noteKind:String = note.kind; + if (noteKind != '' && noteKind.trim() != '' && !noteKinds.contains(noteKind)) { + hscripts.loadFromPaths('scripts/notekinds/$noteKind.hx'); + noteKinds.push(noteKind); + } + } } - stepHit.add(stepHitEvent); - beatHit.add(beatHitEvent); - barHit.add(barHitEvent); - - @:privateAccess FlxG.cameras.defaults.resize(0); - camOther = new FunkinCamera(); - camGame = new FunkinCamera(); - camHUD = new FunkinCamera(); - camHUD.bgColor.alpha = 0; - camGame.bgColor.alpha = 0; - camOther.bgColor.alpha = 0; - FlxG.cameras.add(camGame, true); - FlxG.cameras.add(camHUD, false); - FlxG.cameras.add(camOther, false); - - camFocusTarget = new FlxObject(0, FlxG.height * .5); - camGame.follow(camFocusTarget, LOCKON, 3); - add(camFocusTarget); + for (strumline in strumlineGroup) + strumline.loadStyle(noteStyle); - camGame.zoomFollowLerp = camHUD.zoomFollowLerp = 3; + opponentStrumline.fitToSize(strumlineBound, opponentStrumline.height * .7); + playerStrumline.fitToSize(strumlineBound, playerStrumline.height * .7); + opponentStrumline.setPosition(50, strumlineY); + playerStrumline.setPosition(FlxG.width - playerStrumline.width - 50 - 75, strumlineY); if (!simple) { - stage = new Stage(chart); - stage.setup(chart.stage); + stage = new Stage(chart.stage); + stage.addCharacters(chart); + stage.start(this); add(stage); - + player1 = stage.getCharacter('bf'); player2 = stage.getCharacter('dad'); player3 = stage.getCharacter('gf'); - focusOnCharacter((player3 ?? player1).current); + focusOnCharacter((player3 ?? player1)?.current); + + playerStrumline.character = player1; + opponentStrumline.character = player2; } else { camFocusTarget.setPosition(FlxG.width * .5, FlxG.height * .5); simpleBG = new FunkinSprite().loadTexture('mainmenu/bgGreen'); @@ -181,8 +286,8 @@ class PlayState extends FunkinState { camHUD.zoomTarget = 1; camGame.snapToTarget(); - var path:String = 'data/songs/${chart.path}/'; - chart.loadMusic(path, false); + chart.loadMusic('data/songs/${chart.path}/', false); + if (!chart.instLoaded) chart.loadMusic('songs/${chart.path}/', false); if (chart.instLoaded) { music.add(chart.inst); music.syncBase = chart.inst; @@ -193,50 +298,22 @@ class PlayState extends FunkinState { conductorInUse.syncTracker = chart.inst; } else { Log.warning('chart instrumental not found...'); - Log.minor('verify path:'); - Log.minor('- $path${Util.pathSuffix('Inst', chart.audioSuffix)}.ogg'); + Log.minor('verify paths:'); + Log.minor('- songs/${chart.path}/${Util.pathSuffix('Inst', chart.audioSuffix)}.ogg'); + Log.minor('- data/songs/${chart.path}/${Util.pathSuffix('Inst', chart.audioSuffix)}.ogg'); } loadVocals(chart.path, chart.audioSuffix); - uiGroup = new FunkinSpriteGroup(); - uiGroup.camera = camHUD; - add(uiGroup); - strumlineGroup = new FunkinTypedSpriteGroup(); - strumlineGroup.camera = camHUD; - add(strumlineGroup); - - var scrollDir:Float = (Options.data.downscroll ? 270 : 90); - var strumlineBound:Float = (FlxG.width - 300) * .5; - var strumlineY:Float = 50; - - keybinds = Options.data.keybinds['4k']; - - opponentStrumline = new Strumline(4, scrollDir, chart.scrollSpeed); - opponentStrumline.fitToSize(strumlineBound, opponentStrumline.height * .7); - opponentStrumline.noteEvent.add(opponentNoteEvent); - opponentStrumline.setPosition(50, strumlineY); - opponentStrumline.zIndex = 40; - opponentStrumline.cpu = true; - opponentStrumline.allowInput = false; - - playerStrumline = new Strumline(4, scrollDir, chart.scrollSpeed * 1.08); - playerStrumline.fitToSize(strumlineBound, playerStrumline.height * .7); - playerStrumline.setPosition(FlxG.width - playerStrumline.width - 50 - 75, strumlineY); - playerStrumline.noteEvent.add(playerNoteEvent); - playerStrumline.assignKeybinds(keybinds); - playerStrumline.zIndex = 50; - if (middlescroll) { playerStrumline.screenCenter(X); opponentStrumline.fitToSize(playerStrumline.leftBound - 50 - opponentStrumline.leftBound, 0, Y); } - for (note in genNotes) { - var strumline:Strumline = (note.player ? playerStrumline : opponentStrumline); - strumline.queueNote(note); - notes.push(note); + Log.minor('queueing notes'); + for (note in notes) { + var strumline:Strumline = strumlineGroup.members[note.strumlineIndex]; + strumline?.queueNote(note, null, false, false); } - ratingGroup = new FunkinTypedSpriteGroup(); ratingGroup.setPosition(player3?.getMidpoint()?.x ?? FlxG.width * .5, player3?.getMidpoint()?.y ?? FlxG.height * .5); if (stage != null) { ratingGroup.zIndex = Util.getHighestZIndex(stage.characters, 50) + 5; @@ -249,72 +326,107 @@ class PlayState extends FunkinState { } // TODO: figure out how to display the correct icons in simple mode maybe? they just display the placeholder face - healthBar = new Bar(0, FlxG.height - 50, (_) -> health, 'healthBar'); - healthBar.bounds.max = maxHealth; + healthBar = new Bar(0, FlxG.height - 50, (_) -> health, 'gameplay/healthBar', {min: 0, max: maxHealth}); healthBar.y -= healthBar.height; healthBar.screenCenter(X); healthBar.zIndex = 10; uiGroup.add(healthBar); - iconP1 = new HealthIcon(0, 0, player1?.healthIcon, player1?.current?.healthIconData?.isPixel); + iconP1 = new HealthIcon(0, 0, player1?.healthIconData, true); iconP1.origin.x = 0; - iconP1.flipX = true; // fuck you - iconP1.zIndex = 15; + iconP1.zIndex = 20; uiGroup.add(iconP1); - iconP2 = new HealthIcon(0, 0, player2?.healthIcon, player2?.current?.healthIconData?.isPixel); + iconP2 = new HealthIcon(0, 0, player2?.healthIconData, false); iconP2.origin.x = iconP2.frameWidth; iconP2.zIndex = 15; uiGroup.add(iconP2); - if (player1 != null) { - player1.onCharacterChanged.add((name:String, char:Character) -> matchHealthIcon(iconP1, char)); - player2.onCharacterChanged.add((name:String, char:Character) -> matchHealthIcon(iconP2, char)); - } + if (player1 != null) + player1.onCharacterChanged.add((name:String, char:Character) -> matchIconData(iconP1, char)); + if (player2 != null) + player2.onCharacterChanged.add((name:String, char:Character) -> matchIconData(iconP2, char)); - scoreTxt = new FlxText(0, FlxG.height - 25, FlxG.width, 'Score: idk'); + scoreTxt = new FunkinText(0, FlxG.height - 25, FlxG.width, 'Score: idk'); scoreTxt.setFormat(Paths.ttf('vcr'), 16, FlxColor.WHITE, CENTER, OUTLINE, FlxColor.BLACK); scoreTxt.y -= scoreTxt.height * .5; scoreTxt.borderSize = 1.25; + scoreTxt.zIndex = 30; uiGroup.add(scoreTxt); - updateScoreText(); - debugTxt = new FlxText(0, 12, FlxG.width, ''); - debugTxt.setFormat(Paths.ttf('vcr'), 16, FlxColor.WHITE, CENTER, OUTLINE, FlxColor.BLACK); - uiGroup.add(debugTxt); - strumlineGroup.add(playerStrumline); - strumlineGroup.add(opponentStrumline); - - if (downscroll) { - flipMembers(uiGroup); - flipMembers(strumlineGroup); - } - - for (i in 0...4) Paths.sound('gameplay/hitsounds/miss$i'); + for (i in 0...4) + Paths.sound('gameplay/hitsounds/miss$i'); Paths.sound('gameplay/hitsounds/hitsoundTail'); Paths.sound('gameplay/hitsounds/hitsoundFail'); + conductorInUse.audioOffset = audioOffset; FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressEvent); FlxG.stage.addEventListener(KeyboardEvent.KEY_UP, keyReleaseEvent); refreshRPCTitle(); + for (event in chart.events) + dispatchSongEvent({type: PUSH_EVENT, chartEvent: event}); + + if (downscroll) { + playerStrumline.direction = opponentStrumline.direction = 270; + flipUI(); + } + hscripts.run('createPost'); + + uiGroup.sortZIndex(); + stage?.sortZIndex(); + updateScoreText(); sortZIndex(); + if (restartingSong) + fadeNotes = false; + for (note in wooshNotes) { + if (!note.exists) { + Log.warning('well the note isnt real'); + continue; + } + var strumline:Strumline = strumlineGroup.members[note.strumlineIndex]; + if (strumline != null) { + var lane:Lane = strumline.getLane(note.laneIndex); + if (lane != null) lane.wooshNotes.add(note); + } + } + for (strumline in strumlineGroup) { + for (lane in strumline.lanes) + lane.woosh(); + } + wooshNotes.resize(0); + restartingSong = false; + if (playCountdown) { - for (strumline in strumlineGroup) - strumline.visible = false; + if (fadeNotes) { + for (strumline in strumlineGroup) + strumline.visible = false; + } - for (snd in ['THREE', 'TWO', 'ONE', 'GO']) - Paths.sound('gameplay/countdown/funkin/intro$snd'); - for (img in ['ready', 'set', 'go']) - Paths.image(img); + for (tick in ['THREE', 'TWO', 'ONE', 'GO']) { + Paths.sound('gameplay/countdown/funkin/intro$tick'); + Paths.image(tick); + } } - conductorInUse.beat = (playCountdown ? -5 : -1); + beginCountdown(); + update(0); + + Log.info('GAME ON! (${Math.round((Sys.time() - time) * 1000) / 1000}s)'); } - - inline function flipMembers(grp:FlxTypedSpriteGroup) { + function flipUI() { for (mem in uiGroup) mem.y = FlxG.height - mem.y - mem.height; + for (mem in strumlineGroup) + mem.y = FlxG.height - mem.y - mem.height; + } + function matchIconData(?icon:HealthIcon, ?char:Character) { + if (icon == null || char == null) return; + + icon.iconData = char.healthIconData; + + iconP1.origin.x = 0; + iconP2.origin.x = iconP2.frameWidth; } public function loadVocals(path:String, audioSuffix:String = '') { @@ -380,14 +492,15 @@ class PlayState extends FunkinState { events.resize(0); for (note in notes) { - var strumline:Strumline = (note.player ? playerStrumline : opponentStrumline); - strumline.queueNote(note); + var strumline:Strumline = strumlineGroup.members[note.strumlineIndex]; + strumline?.queueNote(note, null, false, false); } - for (event in chart.events) events.push(event); + for (event in chart.events) + events.push(event); music.pause(); - music.time = 0; + music.time = -audioOffset; resetConductor(); - conductorInUse.beat = -5; + beginCountdown(); resetScore(); refreshRPCTitle(); @@ -409,31 +522,17 @@ class PlayState extends FunkinState { syncMusic(false, true); } if (FlxG.keys.justPressed.Z) { - var strumlineY:Float = 50; downscroll = !downscroll; - Options.data.downscroll = !Options.data.downscroll; - if (Options.data.downscroll) strumlineY = FlxG.height - opponentStrumline.receptorHeight - strumlineY; - for (strumline in strumlineGroup) { + for (strumline in strumlineGroup) strumline.direction += 180; - strumline.y = strumlineY; - } - flipMembers(uiGroup); + + flipUI(); } } else if (!dead) { - if (FlxG.keys.justPressed.ENTER) { - paused = !paused; - var pauseVocals:Bool = (paused || conductorInUse.songPosition < 0); - if (pauseVocals) { - music.pause(); - } else { - music.play(true, conductorInUse.songPosition); - syncMusic(false, true); - } - FlxTimer.globalManager.forEach((timer:FlxTimer) -> { if (!timer.finished) timer.active = !paused; }); - FlxTween.globalManager.forEach((tween:FlxTween) -> { if (!tween.finished) tween.active = !paused; }); - - refreshRPCTitle(); - refreshRPCTime(); + if (FlxG.keys.justPressed.ENTER && !paused && !pauseDisabled) { + FlxTimer.globalManager.forEach((timer:FlxTimer) -> { if (!timer.finished) timer.active = false; }); + FlxTween.globalManager.forEach((tween:FlxTween) -> { if (!tween.finished) tween.active = false; }); + openSubState(new PauseSubState(this)); } if (FlxG.keys.justPressed.R && !paused) @@ -447,10 +546,14 @@ class PlayState extends FunkinState { return; } - iconP1.offset.x = 0; - iconP2.offset.x = iconP2.frameWidth; - iconP2.setPosition(healthBar.barCenter.x - 60 + iconP2.width * .5, healthBar.barCenter.y - iconP2.height * .5); - iconP1.setPosition(healthBar.barCenter.x + 60 - iconP1.width * .5, healthBar.barCenter.y - iconP1.height * .5); + if (iconP1.autoUpdatePosition) { + iconP1.offset.x = 0; + iconP1.setPosition(healthBar.barCenter.x + 60 - iconP1.width * .5, healthBar.barCenter.y - iconP1.height * .5); + } + if (iconP2.autoUpdatePosition) { + iconP2.offset.x = iconP2.frameWidth; + iconP2.setPosition(healthBar.barCenter.x - 60 + iconP2.width * .5, healthBar.barCenter.y - iconP2.height * .5); + } super.update(elapsed); syncMusic(); @@ -465,21 +568,80 @@ class PlayState extends FunkinState { super.draw(); hscripts.run('drawPost'); } - - public function finishSong() { - songFinished = true; - if (HScript.stopped(hscripts.run('finishSong'))) { - conductorInUse.paused = true; - return; + + override public function onSubStateOpened(subState):Void { + if (subState is PauseSubState) { + paused = true; + music.pause(); + + camHUD.active = camGame.active = false; + + refreshRPCTitle(); + refreshRPCTime(); + } + } + override public function onSubStateClosed(subState):Void { + if (subState is PauseSubState) { + paused = false; + music.play(true, conductorInUse.songPosition); + syncMusic(false, true); + + camHUD.active = camGame.active = true; + FlxTimer.globalManager.forEach((timer:FlxTimer) -> { if (!timer.finished) timer.active = true; }); + FlxTween.globalManager.forEach((tween:FlxTween) -> { if (!tween.finished) tween.active = true; }); + + refreshRPCTitle(); + refreshRPCTime(); + } + } + + public function stepHitEvent(step:Int) { + syncMusic(true); + hscripts.run('stepHit', [step]); + } + public function beatHitEvent(beat:Int) { + if (playCountdown) { + switch (beat) { + case -4: + dispatchSongEvent({type: START_COUNTDOWN, countdown: 'THREE'}); + dispatchSongEvent({type: TICK_COUNTDOWN, countdown: 'THREE'}); + case -3: + dispatchSongEvent({type: TICK_COUNTDOWN, countdown: 'TWO'}); + case -2: + dispatchSongEvent({type: TICK_COUNTDOWN, countdown: 'ONE'}); + case -1: + dispatchSongEvent({type: TICK_COUNTDOWN, countdown: 'GO'}); + default: + } } - FlxG.switchState(() -> new FreeplayState()); + if (beat >= 0 && !songStarted) + dispatchSongEvent({type: SONG_START}); + + if (DiscordRpc.supported && music.playing && DiscordRpc.presence.endTimestamp.toInt() == 0) + refreshRPCTime(); + if (camZoomRate != null && camZoomRate > 0 && beat % camZoomRate == 0) + bopCamera(); + + iconP1.bop(); + iconP2.bop(); + if (stage != null) + stage.beatHit(beat); + + hscripts.run('beatHit', [beat]); + } + public function barHitEvent(beat:Int) { + if (camZoomRate == null) + bopCamera(); + + hscripts.run('barHit', [beat]); } public function syncMusic(forceSongpos:Bool = false, forceTrackTime:Bool = false) { - var syncBase:FunkinSound = music.syncBase; - if (chart.instLoaded && syncBase != null && syncBase.playing && !conductorInUse.paused) { - if ((forceSongpos && conductorInUse.songPosition < syncBase.time) || Math.abs(syncBase.time - conductorInUse.songPosition) > 75) - conductorInUse.songPosition = syncBase.time; + var syncBase:FlxSound = conductorInUse.syncTracker; + if (syncBase != null && syncBase.playing && !conductorInUse.paused) { + var offsetTime:Float = syncBase.time + conductorInUse.audioOffset; + if ((forceSongpos && conductorInUse.songPosition < offsetTime) || Math.abs(offsetTime - conductorInUse.songPosition) > 75) + conductorInUse.songPosition = offsetTime; if (forceTrackTime) { if (Math.abs(music.getDisparity(syncBase.time)) > 75) music.syncToBase(); @@ -487,138 +649,7 @@ class PlayState extends FunkinState { } } } - - public function pushedEvent(event:ChartEvent) { - var params:Map = event.params; // todo: move this outside of playstate? - switch (event.name) { - case 'PlayAnimation': - var focusChara:Null = null; - switch (params['target']) { - case 'girlfriend', 'gf': focusChara = player3; - case 'boyfriend', 'bf': focusChara = player1; - case 'dad': focusChara = player2; - } if (focusChara != null) focusChara.preloadAnimAsset(params['anim']); - } - hscripts.run('eventPushed', [event]); - } - public function triggerEvent(event:ChartEvent) { - var params:Map = event.params; // todo: also move this outside of playstate - switch (event.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 = player1; - case 1: // opponent focus - focusChara = player2; - case 2: // gf focus - focusChara = player3; - } - - if (focusChara != null) { - focusOnCharacter(focusChara.current); - } else { - camFocusTarget.x = 0; - camFocusTarget.y = 0; - spotlight = null; - } - if (params.exists('x')) camFocusTarget.x += Util.parseFloat(params['x']); - if (params.exists('y')) camFocusTarget.y += Util.parseFloat(params['y']); - FlxTween.cancelTweensOf(camGame.scroll); - switch (params['ease']) { - case 'CLASSIC' | null: - camGame.pauseFollowLerp = false; - case 'INSTANT': - camGame.snapToTarget(); - camGame.pauseFollowLerp = false; - default: - var duration:Float = Util.parseFloat(params['duration'], 4) * conductorInUse.stepCrochet * .001; - if (duration <= 0) { - camGame.snapToTarget(); - 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; - } - camGame.pauseFollowLerp = true; - FlxTween.tween(camGame.scroll, {x: camFocusTarget.x - FlxG.width * .5, y: camFocusTarget.y - FlxG.height * .5}, duration, {ease: easeFunction, onComplete: (_) -> { - 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 : (stage?.zoom ?? 1)); - camGame.zoomTarget = targetZoom; - FlxTween.cancelTweensOf(camGame, ['zoom']); - switch (params['ease']) { - case 'INSTANT': - camGame.zoom = targetZoom; - camGame.pauseZoomLerp = false; - default: - var duration:Float = Util.parseFloat(params['duration'], 4) * conductorInUse.stepCrochet * .001; - if (duration <= 0) { - camGame.zoom = targetZoom; - 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; - } - camGame.pauseZoomLerp = true; - FlxTween.tween(camGame, {zoom: targetZoom}, duration, {ease: easeFunction, onComplete: (_) -> { - camGame.pauseZoomLerp = false; - }}); - } - } - case 'SetCameraBop': - var targetRate:Int = Util.parseInt(params['rate'], -1); - var targetIntensity:Float = Util.parseFloat(params['intensity'], 1); - hudZoomIntensity = targetIntensity * 2; - camZoomIntensity = targetIntensity; - 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 = player2; - case 'girlfriend' | 'gf': focus = player3; - case 'boyfriend' | 'bf' | 'player': focus = player1; - default: focus = 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)) { - chara.playAnimation(anim, forced); - chara.specialAnim = forced; - chara.animReset = 8; - } - } else if (Std.isOfType(focus, FunkinSprite)) { - var funk:FunkinSprite = cast focus; - if (funk.animationExists(anim)) { - funk.playAnimation(anim, forced); - } - } - } - } - hscripts.run('eventTriggered', [event]); - } + public function focusOnCharacter(chara:Character, center:Bool = false) { if (chara != null) { camFocusTarget.x = chara.getMidpoint().x + chara.cameraOffset.x + (center ? 0 : chara.stageCameraOffset.x); @@ -626,41 +657,41 @@ class PlayState extends FunkinState { spotlight = chara; } } - public function matchHealthIcon(icon:HealthIcon, ?chara:Character) { - if (chara != null) { - icon.icon = chara.healthIcon; - icon.isPixel = chara?.healthIconData?.isPixel; - } + function set_spotlight(?newSprite:FlxSprite):Null { + if (spotlight == newSprite) return newSprite; + + dispatchSongEvent({type: CHANGE_SPOTLIGHT, sprite: newSprite}); + return spotlight = newSprite; } - + // TODO: ok, maybe these could be in a single function public function refreshRPCTime() { - if (!autoUpdateRPC) + if (!DiscordRpc.supported || !autoUpdateRPC) return; if (music.playing) { - var beginTime:Float = Date.now().getTime() - music.time; + var beginTime:Float = Date.now().getTime() - music.time + audioOffset; var endTime:Float = beginTime + chart.songLength; - DiscordRPC.presence.endTimestamp = Std.int(endTime * .001); - DiscordRPC.presence.startTimestamp = Std.int(beginTime * .001); + DiscordRpc.presence.endTimestamp = Std.int(endTime * .001); + DiscordRpc.presence.startTimestamp = Std.int(beginTime * .001); // trace('PLAYING SONG FROM ' + Std.int(beginTime * .001) + ' to ' + Std.int(endTime * .001)); } else { - DiscordRPC.presence.startTimestamp = DiscordRPC.presence.endTimestamp = 0; + DiscordRpc.presence.startTimestamp = DiscordRpc.presence.endTimestamp = 0; } - DiscordRPC.dirty = true; + DiscordRpc.dirty = true; } public function refreshRPCTitle() { - if (!autoUpdateRPC) + if (!DiscordRpc.supported || !autoUpdateRPC) return; var detailsText:String = '${chart.name} on ${chart.difficulty.toUpperCase()}'; if (paused) detailsText += ' (Paused)'; - DiscordRPC.details = detailsText; + DiscordRpc.details = detailsText; refreshRPCDetails(); } public function refreshRPCDetails() { - if (!autoUpdateRPC) + if (!DiscordRpc.supported || !autoUpdateRPC) return; var detailsString:String; @@ -687,51 +718,34 @@ class PlayState extends FunkinState { detailsString = ''; } - DiscordRPC.state = detailsString; + DiscordRpc.state = detailsString; } - public function stepHitEvent(step:Int) { - syncMusic(true); + inline public function dispatchSongEvent(e:SongEvent) dispatchPlayEvent('songEvent', e); + public function dispatchPlayEvent(funcName:String, e:IPlayEvent) { + runAllHScript('${funcName}Pre', [e]); - hscripts.run('stepHit', [step]); - } - public function beatHitEvent(beat:Int) { - if (playCountdown) { - var folder:String = 'funkin'; - switch (beat) { - case -4: - FunkinSound.playOnce(Paths.sound('gameplay/countdown/$folder/introTHREE')); - for (strumline in strumlineGroup) - strumline.fadeIn(); - case -3: - popCountdown('ready'); - FunkinSound.playOnce(Paths.sound('gameplay/countdown/$folder/introTWO')); - case -2: - popCountdown('set'); - FunkinSound.playOnce(Paths.sound('gameplay/countdown/$folder/introONE')); - case -1: - popCountdown('go'); - FunkinSound.playOnce(Paths.sound('gameplay/countdown/$folder/introGO')); - case 0: - music.play(true); - syncMusic(true, true); - default: - } - } - if (music.playing && DiscordRPC.presence.endTimestamp.toInt() == 0) { - refreshRPCTime(); - } - if (camZoomRate > 0 && beat % camZoomRate == 0) - bopCamera(); - - iconP1.bop(); - iconP2.bop(); - if (stage != null) - stage.beatHit(beat); + try e.dispatch() + catch (e:haxe.Exception) Log.error('error dispatching event -> ${e.details()}'); - hscripts.run('beatHit', [beat]); + runAllHScript(funcName, [e]); + } + public function beginCountdown() { + songStarted = false; + conductorInUse.beat = (playCountdown ? -5 : -1); + } + public function finishSong() { + songFinished = true; + dispatchSongEvent({type: SONG_FINISH}); + } + public function restartSong() { + restartingSong = true; + FlxG.resetState(); } - public function popCountdown(image:String) { + public function popCountdown(image:String):FunkinSprite { + if (Paths.image(image) == null) + return null; + var pop = new FunkinSprite().loadTexture(image); pop.camera = camHUD; pop.screenCenter(); @@ -740,20 +754,32 @@ class PlayState extends FunkinState { remove(pop); pop.destroy(); }}); - hscripts.run('countdownPop', [image, pop]); - } - public function barHitEvent(bar:Int) { - if (camZoomRate < 0) - bopCamera(); - - hscripts.run('barHit', [bar]); + return pop; } public function bopCamera() { + if (!camZooming) + return; if (!camHUD.pauseZoomLerp) camHUD.zoom += .015 * hudZoomIntensity; if (!camGame.pauseZoomLerp) camGame.zoom += .015 * camZoomIntensity; } + function runAllHScript(func:String, ?args:Array):Bool { + var canContinue = true; + + canContinue = (canContinue && !HScript.stopped(hscripts.run(func, args))); + if (stage != null) { + for (chara in stage.characters) { + if (chara == null || !chara.exists || !chara.alive) continue; + + var current:Character = chara.current; + if (current != null) + canContinue = (canContinue && !HScript.stopped(current.hscripts.run(func, args))); + } + } + + return canContinue; + } public function keyPressEvent(event:KeyboardEvent) { var key:FlxKey = event.keyCode; @@ -764,16 +790,17 @@ class PlayState extends FunkinState { if (HScript.stopped(hscripts.run('keyPressed', [key, justPressed])) || inputDisabled || paused) return; if (justPressed) { var keybind:Int = Controls.keybindFromArray(keybinds, key); + var oldTime:Float = conductorInUse.songPosition; - var newTimeMaybe:Float = conductorInUse.syncTracker?.time ?? oldTime; if (conductorInUse.syncTracker != null && conductorInUse.syncTracker.playing) - conductorInUse.songPosition = newTimeMaybe; // too rigged? (Math.abs(newTimeMaybe) < Math.abs(oldTime) ? newTimeMaybe : oldTime); + @:privateAccess conductorInUse.songPosition = conductorInUse.syncTracker._channel.position + audioOffset; - if (keybind >= 0) { - if (!HScript.stopped(hscripts.run('keybindPressed', [keybind, key]))) { - for (strumline in strumlineGroup) - strumline.fireInput(key, true); - } + var canFireInput:Bool = true; + if (keybind >= 0) + canFireInput = !HScript.stopped(hscripts.run('keybindPressed', [keybind, key])); + if (canFireInput) { + for (strumline in strumlineGroup) + strumline.fireInput(key, true); } conductorInUse.songPosition = oldTime; @@ -784,45 +811,45 @@ class PlayState extends FunkinState { heldKeys.remove(key); if (HScript.stopped(hscripts.run('keyReleased', [key])) || inputDisabled || paused) return; + var keybind:Int = Controls.keybindFromArray(keybinds, key); - - if (keybind >= 0) { - var result:Dynamic = hscripts.run('keybindReleased', [keybind, key]); + var oldTime:Float = conductorInUse.songPosition; + var newTimeMaybe:Float = conductorInUse.syncTracker?.time ?? oldTime; + if (conductorInUse.syncTracker != null && conductorInUse.syncTracker.playing) + conductorInUse.songPosition = newTimeMaybe; + + var canFireInput:Bool = true; + if (keybind >= 0) + canFireInput = !HScript.stopped(hscripts.run('keybindReleased', [keybind, key])); + if (canFireInput) { for (strumline in strumlineGroup) strumline.fireInput(key, false); } + + conductorInUse.songPosition = oldTime; } public function playerNoteEvent(e:NoteEvent) { - e.targetCharacter = player1; - e.doSplash = true; - e.doSpark = true; + e.setup(); - if (e.type == NoteEventType.GHOST && Options.data.ghostTapping) { + e.doSplash = e.doSpark = true; + + if (e.type == NoteEventType.GHOST && ghostTapping) { e.playAnimation = false; } else { - e.playSound = true; - e.applyRating = true; + e.playSound = e.applyHealth = e.applyRating = true; } - - hscripts.run('playerNoteEventPre', [e]); - try e.dispatch() - catch (e:haxe.Exception) Log.error('error dispatching note event -> ${e.message}'); - hscripts.run('playerNoteEvent', [e]); + + dispatchPlayEvent('playerNoteEvent', e); } public function opponentNoteEvent(e:NoteEvent) { - e.targetCharacter = player2; - e.applyRating = false; - e.playSound = false; - e.doSplash = false; - e.doSpark = false; - - hscripts.run('opponentNoteEventPre', [e]); - try e.dispatch() - catch (e:haxe.Exception) Log.error('error dispatching note event -> ${e.message}'); - hscripts.run('opponentNoteEvent', [e]); + e.setup(); + + dispatchPlayEvent('opponentNoteEvent', e); } public function popCombo(combo:Int) { + if (!ratingGroup.alive) return; + var tempCombo:Int = combo; var nums:Array = []; while (tempCombo >= 1) { @@ -833,7 +860,7 @@ class PlayState extends FunkinState { var xOffset:Float = -nums.length * .5 + .5; for (i => num in nums) { - var popNum:FunkinSprite = popRating('num$num', .5, 2); + var popNum:FunkinSprite = popRating('gameplay/funkin/num$num', .5, 2); popNum.setPosition(popNum.x + (i + xOffset) * 43, popNum.y + 80); popNum.acceleration.y = FlxG.random.int(200, 300); popNum.velocity.y = -FlxG.random.int(140, 160); @@ -841,16 +868,22 @@ class PlayState extends FunkinState { } } public function popRating(ratingString:String, scale:Float = .7, beats:Float = 1) { - var rating:FunkinSprite = new FunkinSprite(0, 0); + var rating:FunkinSprite = ratingGroup.recycle(FunkinSprite, () -> new FunkinSprite()); + + if (!ratingGroup.alive) return rating; + + rating.alpha = 1; + rating.visible = true; + rating.cameras = ratingGroup.cameras; rating.loadTexture(ratingString); rating.scale.set(scale, scale); + rating.setPosition(ratingGroup.x, ratingGroup.y); rating.offset.set(rating.frameWidth * .5, rating.frameHeight * .5); - - ratingGroup.add(rating); - FlxTween.tween(rating, {alpha: 0}, .2, {onComplete: (tween:FlxTween) -> { - ratingGroup.remove(rating, true); - rating.destroy(); - }, startDelay: conductorInUse.crochet * .001 * beats}); + + rating.revive(); + ratingGroup.moveToTop(rating); + FlxTween.cancelTweensOf(rating); + FlxTween.tween(rating, {alpha: 0}, .2, {onComplete: (_) -> rating.kill(), startDelay: conductorInUse.crochet * .001 * beats}); return rating; } @@ -861,16 +894,17 @@ class PlayState extends FunkinState { return maxHealth = newHealth; } public dynamic function set_health(newHealth:Float) { - newHealth = FlxMath.bound(newHealth, 0, maxHealth); + newHealth = Util.clamp(newHealth, 0, maxHealth); switch (newHealth) { case (_ <= .15) => true: - iconP1.state = LOSING; - iconP2.state = WINNING; + if (iconP1.autoUpdateState) iconP1.state = LOSING; + if (iconP2.autoUpdateState) iconP2.state = WINNING; case (_ >= maxHealth - .15) => true: - iconP1.state = WINNING; - iconP2.state = LOSING; + if (iconP1.autoUpdateState) iconP1.state = WINNING; + if (iconP2.autoUpdateState) iconP2.state = LOSING; default: - iconP1.state = iconP2.state = NEUTRAL; + if (iconP1.autoUpdateState) iconP1.state = NEUTRAL; + if (iconP2.autoUpdateState) iconP2.state = NEUTRAL; } if (newHealth <= 0 && !godmode && !dead) die(false); @@ -890,6 +924,7 @@ class PlayState extends FunkinState { FlxTween.cancelTweensOf(camGame.scroll); FlxTween.cancelTweensOf(camGame); camGame.pauseFollowLerp = false; + camGame.zoomOffset = 0; refreshRPCDetails(); gameOver = new GameOverSubState(instant); @@ -972,16 +1007,38 @@ class PlayState extends FunkinState { } } public dynamic function comboBroken(oldCombo:Int) { - player3?.playAnimationSteps('sad', true, 8); popCombo(0); + + if (stage != null) { + for (chara in stage.characters) + chara.playComboDropAnimation(scoring.combo); + } } override public function destroy() { - DiscordRPC.details = DiscordRPC.state = ''; - DiscordRPC.presence.startTimestamp = DiscordRPC.presence.endTimestamp = 0; + if (restartingSong) { + for (strumline in strumlineGroup) { + for (lane in strumline.lanes) { + for (note in lane.notes) { + if (!note.alive) { + note.destroy(); + continue; + } + wooshNotes.push(note); + } + while (lane.notes.length > 0) // this is fucking stupid + lane.notes.remove(lane.notes.members[0], true); + } + } + } + + Paths.library = ''; + + funkin.backend.play.NoteStyle.wipe(); + DiscordRpc.details = DiscordRpc.state = ''; + DiscordRpc.presence.startTimestamp = DiscordRpc.presence.endTimestamp = 0; FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, keyPressEvent); FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, keyReleaseEvent); - Main.watermark.visible = true; conductorInUse.paused = false; super.destroy(); } diff --git a/source/funkin/states/TitleState.hx b/source/funkin/states/TitleState.hx index e78cbe6..cd44d0e 100644 --- a/source/funkin/states/TitleState.hx +++ b/source/funkin/states/TitleState.hx @@ -13,6 +13,9 @@ class TitleState extends FunkinState { public var enter:FunkinSprite; public var logo:FunkinSprite; + public var enterColors:Array = [0xff33ffff, 0xff3333cc]; + var enterColorIndex:Int = 0; + public var introTexts:Array> = []; public var currentIntroText:Array; public var titleStarted:Bool = false; @@ -42,10 +45,12 @@ class TitleState extends FunkinState { } } public override function create() { + super.create(); + preload(); - currentIntroText = FlxG.random.getObject(introTexts) ?? ['funkin', 'FOREVER']; + currentIntroText = FlxG.random.getObject(introTexts) ?? ['FUNKIN', 'FOREVER']; - conductorInUse.beatHit.add(beatHitEvent); + beatHit.add(beatHitEvent); logo = new FunkinSprite().loadAtlas('titlescreen/logo'); logo.addAnimation('bump', 'logo bumpin'); @@ -65,9 +70,9 @@ class TitleState extends FunkinState { titleGroup.add(enter); ngSpr = new FunkinSprite(); - if (FlxG.random.bool(1)) { + if (FlxG.random.bool(1)) { ngSpr.loadGraphic(Paths.image('titlescreen/ngClassic')); - } else if (FlxG.random.bool(30)) { + } else if (FlxG.random.bool(30)) { ngSpr.loadGraphic(Paths.image('titlescreen/ngAnimated'), true, 600); ngSpr.setGraphicSize(Std.int(ngSpr.width * 0.55)); ngSpr.addAnimation('idle', null, 4, true, [0, 1]); @@ -79,15 +84,16 @@ class TitleState extends FunkinState { } ngSpr.alpha = .0001; ngSpr.updateHitbox(); - ngSpr.screenCenter(X); - ngSpr.y = FlxG.height - ngSpr.height - 160; + ngSpr.screenCenter(X); + ngSpr.y = FlxG.height - ngSpr.height - 160; playMusic(MainMenuState.menuMusic, !skipIntro); conductorInUse.syncTracker = FlxG.sound.music; + conductorInUse.sync(); add(introGroup); if (!skipIntro) { - add(ngSpr); + add(ngSpr); final ySpacing:Float = 60; queueEvent(beatToMS(1), (_) -> { @@ -122,8 +128,10 @@ class TitleState extends FunkinState { showTitleScreen(true); } - DiscordRPC.presence.details = 'In the title screen'; - DiscordRPC.dirty = true; + DiscordRpc.presence.details = 'In the title screen'; + DiscordRpc.dirty = true; + + enter.color = enterColors[enterColorIndex ++]; } public override function update(elapsed:Float) { super.update(elapsed); @@ -147,19 +155,17 @@ class TitleState extends FunkinState { } } } - public override function destroy() { - conductorInUse.beatHit.remove(beatHitEvent); - super.destroy(); - } public function beatHitEvent(beat:Int) { logo.playAnimation('bump', true); if (beat % 2 == 0) { if (!confirmed) { - var to:FlxColor = 0xff3333cc; - var from:FlxColor = 0xff33ffff; + if (enterColorIndex >= enterColors.length) enterColorIndex = 0; + + var to:FlxColor = enterColors[enterColorIndex ++]; + FlxTween.cancelTweensOf(enter); - FlxTween.color(enter, conductorInUse.crochet * .001 * 2, from, to, {type: (beat % 4 < 2 ? ONESHOT : BACKWARD)}); + FlxTween.color(enter, conductorInUse.crochet * .001 * 2, enter.color, to); } } } diff --git a/source/funkin/util/MemoryUtil.hx b/source/funkin/util/MemoryUtil.hx new file mode 100644 index 0000000..22e024b --- /dev/null +++ b/source/funkin/util/MemoryUtil.hx @@ -0,0 +1,40 @@ +package funkin.util; + +#if hl import hl.Gc; +#elseif cpp import cpp.vm.Gc; +#elseif neko import neko.vm.Gc; #end + +class MemoryUtil { + public static function getMemoryUsed():#if cpp Float #else Int #end { + #if cpp + return Gc.memInfo64(Gc.MEM_INFO_CURRENT); + #else + return openfl.system.System.totalMemory; + #else + Log.warning('GC not implemented on this platform'); + #end + } + + public static function enable(yea:Bool = true):Void { + #if (cpp || hl) + Gc.enable(yea); + #else + Log.warning('GC not implemented on this platform'); + #end + } + + public static function collect(major:Bool = true):Void { + openfl.system.System.gc(); + #if (cpp || neko) + Gc.run(major); + #elseif hl + if (major) Gc.major(); + #end + } + + public static function compact():Void { + #if cpp + Gc.compact(); + #end + } +} \ No newline at end of file diff --git a/source/funkin/util/Util.hx b/source/funkin/util/Util.hx index a647ba3..6cd1ceb 100644 --- a/source/funkin/util/Util.hx +++ b/source/funkin/util/Util.hx @@ -53,9 +53,17 @@ class Util { // maybe these utils can be on their own specific purpose classes if (min != null && n < min) return min; return (max != null && n > max ? max : n); } - public static function smoothLerp(a:Float, b:Float, t:Float):Float { + public static inline function smoothLerp(a:Float, b:Float, t:Float):Float { return FlxMath.lerp(a, b, 1 - Math.exp(-t)); } + public static inline function euclideanMod(n:Float, div:Float):Float { + var mod:Float = n % div; + return (mod < 0 ? mod + Math.abs(div) : mod); + } + public static function eucMod(n:Float, div:Float):Float { return euclideanMod(n, div); } + public static inline function wrap(n:Float, min:Float, max:Float):Float { + return euclideanMod(n - min, max - min) + min; + } // idfk public static function sortZIndex(order:Int, a:FlxBasic, b:FlxBasic):Int { diff --git a/source/import.hx b/source/import.hx index 296d484..b69a0f5 100644 --- a/source/import.hx +++ b/source/import.hx @@ -36,6 +36,7 @@ import funkin.util.*; import funkin.debug.Log; import funkin.backend.Options; import funkin.backend.Controls; +import funkin.backend.FunkinText; import funkin.backend.FunkinSound; import funkin.backend.FunkinState; import funkin.backend.FunkinSprite; @@ -47,5 +48,5 @@ import funkin.backend.rhythm.*; import funkin.backend.Mods; import funkin.backend.Paths; -import funkin.backend.DiscordRpc; +import funkin.backend.api.DiscordRpc; #end \ No newline at end of file