diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml index 3931e62a7..5844499b7 100644 --- a/.github/actions/setup-haxeshit/action.yml +++ b/.github/actions/setup-haxeshit/action.yml @@ -1,10 +1,20 @@ name: setup-haxeshit -description: "sets up haxe shit, using lix!" +description: "sets up haxe shit, using HMM!" runs: using: "composite" steps: - - uses: lix-pm/setup-lix@1.0.0 + - uses: krdlab/setup-haxe@v1.4.0 with: - lix-version: 15.8.9 # optional - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' + haxe-version: 4.2.5 + - name: Config haxelib + run: | + haxelib config + shell: bash + - name: Installing Haxe lol + run: | + haxe -version + haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git + haxelib version + haxelib --global install hmm + haxelib --global run hmm install --quiet + shell: bash diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index 182ee2af6..574f1c3ef 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -55,15 +55,10 @@ jobs: export/debug/windows/haxe/ export/debug/windows/obj/ - - name: lix stuff - run: | - npm i -g lix - lix download - lix +lib lime - lix run lime setup + - uses: ./.github/actions/setup-haxeshit - name: Build game run: | - lix run lime build windows + haxelib run lime build windows dir - uses: ./.github/actions/upload-itch with: diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c46be2c9c..483db9ea9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -5,6 +5,7 @@ "vshaxe.haxe-checkstyle", // Haxe code style and conventions "vshaxe.hxcpp-debugger", // CPP debugging "openfl.lime-vscode-extension", // Lime integration - "esbenp.prettier-vscode" // JSON formatting + "esbenp.prettier-vscode", // JSON formatting + "redhat.vscode-xml" // XML formatting ] } diff --git a/CODESTYLE.md b/CODESTYLE.md index 9fb08eef3..2641febfa 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -13,7 +13,7 @@ Code Quality is handled by the `vshaxe.haxe-checkstyle` extension, which include * Checks can be disabled by setting the severity to `IGNORE`. * `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the intentation is. * `CommentedOutCode` check is in place because old code should be retrieved via Git history. -* TODO items: +* TODO items: Enable these one-by-one and fix them to improve the overall code quality. - Reconfigure `MethodLength` - Reconfigure `CyclomaticComplexity` - Re-enable `MagicNumber` diff --git a/Project.xml b/Project.xml index a251ccdbf..c2f03f83b 100644 --- a/Project.xml +++ b/Project.xml @@ -115,21 +115,21 @@ - - + + + - - - - + + - - - - - + + + + + + diff --git a/checkstyle.json b/checkstyle.json index 69a86dbfe..d9200ea12 100644 --- a/checkstyle.json +++ b/checkstyle.json @@ -41,7 +41,9 @@ "type": "AvoidStarImport" }, { - "props": {}, + "props": { + "severity": "IGNORE" + }, "type": "AvoidTernaryOperator" }, { @@ -215,7 +217,8 @@ }, { "props": { - "character": " " + "character": " ", + "severity": "IGNORE" }, "type": "Indentation" }, @@ -229,7 +232,8 @@ }, { "props": { - "ignoreReturnAssignments": false + "ignoreReturnAssignments": false, + "severity": "WARNING" }, "type": "InnerAssignment" }, @@ -275,11 +279,25 @@ }, "type": "MagicNumber" }, + { + "props": { + "format": "^[A-Z][a-zA-Z0-9]*$", + "tokens": ["ABSTRACT"] + }, + "type": "MemberName" + }, + { + "props": { + "format": "^[_a-zA-Z][a-z][a-zA-Z0-9]*$", + "tokens": ["ABSTRACT"] + }, + "type": "MemberName" + }, { "props": { "ignoreExtern": true, - "format": "^[a-z][a-zA-Z0-9]*$", - "tokens": [] + "format": "^[_a-z][a-zA-Z0-9]*$", + "tokens": ["PUBLIC", "PRIVATE", "CLASS", "TYPEDEF"] }, "type": "MemberName" }, @@ -310,10 +328,10 @@ { "props": { "modifiers": [ - "MACRO", - "OVERRIDE", "PUBLIC_PRIVATE", "STATIC", + "MACRO", + "OVERRIDE", "INLINE", "DYNAMIC", "FINAL" @@ -342,13 +360,15 @@ }, { "props": { - "max": 3 + "max": 3, + "severity": "IGNORE" }, "type": "NestedControlFlow" }, { "props": { - "max": 1 + "max": 1, + "severity": "IGNORE" }, "type": "NestedForDepth" }, @@ -366,7 +386,7 @@ }, { "props": { - "option": "questionMark" + "option": "nullDefault" }, "type": "NullableParameter" }, @@ -390,7 +410,6 @@ { "props": { "tokens": [ - "=", "+", "-", "*", @@ -426,6 +445,13 @@ "++", "--" ], + "option": "nl" + }, + "type": "OperatorWrap" + }, + { + "props": { + "tokens": ["="], "option": "eol" }, "type": "OperatorWrap" @@ -446,7 +472,9 @@ "type": "ParameterNumber" }, { - "props": {}, + "props": { + "severity": "WARNING" + }, "type": "PublicAccessor" }, { @@ -480,7 +508,8 @@ { "props": { "ignoreFormat": "^$", - "max": 2 + "max": 2, + "severity": "IGNORE" }, "type": "ReturnCount" }, diff --git a/hmm.json b/hmm.json index 087fd5fef..9edc42cdd 100644 --- a/hmm.json +++ b/hmm.json @@ -1,108 +1,116 @@ { - "dependencies": [{ - "name": "discord_rpc", - "type": "git", - "dir": null, - "ref": "2d83fa8", - "url": "https://github.com/Aidan63/linc_discord-rpc" - }, - { - "name": "flixel", - "type": "git", - "dir": null, - "ref": "d6100cc8", - "url": "https://github.com/EliteMasterEric/flixel" - }, - { - "name": "flixel-addons", - "type": "git", - "dir": null, - "ref": "f107166", - "url": "https://github.com/EliteMasterEric/flixel-addons" - }, - { - "name": "flixel-ui", - "type": "haxelib", - "version": "2.4.0" - }, - { - "name": "flxanimate", - "type": "git", - "dir": null, - "ref": "18b2060", - "url": "https://github.com/Dot-Stuff/flxanimate" - }, - { - "name": "format", - "type": "haxelib", - "version": "3.5.0" - }, - { - "name": "haxeui-core", - "type": "git", - "dir": null, - "ref": "e5cf78d", - "url": "https://github.com/haxeui/haxeui-core/" - }, - { - "name": "haxeui-flixel", - "type": "git", - "dir": null, - "ref": "f03bb6d", - "url": "https://github.com/haxeui/haxeui-flixel" - }, - { - "name": "hmm", - "type": "git", - "dir": null, - "ref": "3ef9522", - "url": "https://github.com/steviegt6/hmm" - }, - { - "name": "hscript", - "type": "haxelib", - "version": "2.5.0" - }, - { - "name": "hxcpp", - "type": "haxelib", - "version": "4.2.1" - }, - { - "name": "hxcpp-debug-server", - "type": "haxelib", - "version": "1.2.4" - }, - { - "name": "hxp", - "type": "haxelib", - "version": null - }, - { - "name": "lime", - "type": "git", - "dir": null, - "ref": "afadf5f", - "url": "https://github.com/openfl/lime" - }, - { - "name": "openfl", - "type": "git", - "dir": null, - "ref": "b2c18513", - "url": "https://github.com/EliteMasterEric/openfl" - }, - { - "name": "polymod", - "type": "git", - "dir": null, - "ref": "4e5b4b3", - "url": "https://github.com/larsiusprime/polymod" - }, - { - "name": "thx.semver", - "type": "haxelib", - "version": "0.2.2" - } - ] -} \ No newline at end of file + "dependencies": [ + { + "name": "discord_rpc", + "type": "git", + "dir": null, + "ref": "2d83fa8", + "url": "https://github.com/Aidan63/linc_discord-rpc" + }, + { + "name": "flixel", + "type": "git", + "dir": null, + "ref": "d6100cc8", + "url": "https://github.com/EliteMasterEric/flixel" + }, + { + "name": "flixel-addons", + "type": "git", + "dir": null, + "ref": "f107166", + "url": "https://github.com/EliteMasterEric/flixel-addons" + }, + { + "name": "flixel-ui", + "type": "haxelib", + "version": "2.4.0" + }, + { + "name": "flxanimate", + "type": "git", + "dir": null, + "ref": "18b2060", + "url": "https://github.com/Dot-Stuff/flxanimate" + }, + { + "name": "format", + "type": "haxelib", + "version": "3.5.0" + }, + { + "name": "haxeui-core", + "type": "git", + "dir": null, + "ref": "e5cf78d", + "url": "https://github.com/haxeui/haxeui-core/" + }, + { + "name": "haxeui-flixel", + "type": "git", + "dir": null, + "ref": "f03bb6d", + "url": "https://github.com/haxeui/haxeui-flixel" + }, + { + "name": "hmm", + "type": "git", + "dir": null, + "ref": "3ef9522", + "url": "https://github.com/steviegt6/hmm" + }, + { + "name": "hscript", + "type": "haxelib", + "version": "2.5.0" + }, + { + "name": "hxcodec", + "type": "git", + "dir": null, + "ref": "master", + "url": "https://github.com/EliteMasterEric/hxCodec" + }, + { + "name": "hxcpp", + "type": "haxelib", + "version": "4.2.1" + }, + { + "name": "hxcpp-debug-server", + "type": "haxelib", + "version": "1.2.4" + }, + { + "name": "hxp", + "type": "haxelib", + "version": null + }, + { + "name": "lime", + "type": "git", + "dir": null, + "ref": "afadf5f", + "url": "https://github.com/openfl/lime" + }, + { + "name": "openfl", + "type": "git", + "dir": null, + "ref": "b2c18513", + "url": "https://github.com/EliteMasterEric/openfl" + }, + { + "name": "polymod", + "type": "git", + "dir": null, + "ref": "4e5b4b3", + "url": "https://github.com/larsiusprime/polymod" + }, + { + "name": "thx.semver", + "type": "haxelib", + "version": "0.2.2" + } + ] +} diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index f7f662b7b..fea8899d2 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -211,7 +211,7 @@ class InitState extends FlxTransitionableState #elseif FREEPLAY FlxG.switchState(new FreeplayState()); #elseif ANIMATE - FlxG.switchState(new funkin.animate.dotstuff.DotStuffTestStage()); + FlxG.switchState(new funkin.ui.animDebugShit.FlxAnimateTest()); #elseif CHARTING FlxG.switchState(new ChartingState()); #elseif STAGEBUILD diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index f961a049f..3a1c65285 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -51,6 +51,11 @@ class Paths return getPath(file, type, library); } + public static inline function animateAtlas(path:String, library:String) + { + return getLibraryPathForce('images/$path', library); + } + inline static public function txt(key:String, ?library:String) { return getPath('data/$key.txt', TEXT, library); diff --git a/source/funkin/animate/AnimTestStage.hx b/source/funkin/animate/AnimTestStage.hx deleted file mode 100644 index 0b2291ee4..000000000 --- a/source/funkin/animate/AnimTestStage.hx +++ /dev/null @@ -1,36 +0,0 @@ -package funkin.animate; - -import flixel.FlxSprite; -import flixel.FlxState; -import flixel.addons.display.FlxGridOverlay; - -class AnimTestStage extends FlxState -{ - var tl:AnimateTimeline; - var swag:FlxAnimate; - - override function create() - { - var bg:FlxSprite = FlxGridOverlay.create(32, 32); - add(bg); - bg.scrollFactor.set(); - - swag = new FlxAnimate(200, 200); - add(swag); - - tl = new AnimateTimeline(Paths.file('images/tightBarsLol/Animation.json')); - add(tl); - - super.create(); - } - - override function update(elapsed:Float) - { - tl.curFrame = swag.daFrame; - - CoolUtil.mouseWheelZoom(); - CoolUtil.mouseCamDrag(); - - super.update(elapsed); - } -} diff --git a/source/funkin/animate/AnimateTimeline.hx b/source/funkin/animate/AnimateTimeline.hx deleted file mode 100644 index f5831d199..000000000 --- a/source/funkin/animate/AnimateTimeline.hx +++ /dev/null @@ -1,74 +0,0 @@ -package funkin.animate; - -import flixel.FlxCamera; -import flixel.FlxSprite; -import flixel.group.FlxGroup.FlxTypedGroup; -import flixel.group.FlxGroup; -import flixel.text.FlxText; -import flixel.util.FlxColor; -import haxe.Json; -import lime.utils.Assets; - -class AnimateTimeline extends FlxTypedGroup -{ - // var coolParsed:Parsed; - var playhead:FlxSprite; - - public var curFrame(default, set):Int; - - function set_curFrame(frm:Int):Int - { - if (playhead != null) playhead.x = 5 + (frm * 12) + (12 * 5); - return frm; - } - - var hudCamShit:FlxCamera; - - public function new(parsed:String) - { - super(); - - /* hudCamShit = new FlxCamera(); - hudCamShit.bgColor = FlxColor.TRANSPARENT; - FlxG.cameras.add(hudCamShit, false); - - playhead = new FlxSprite(0, -12).makeGraphic(2, 10, FlxColor.MAGENTA); - add(playhead); - - hudCamShit.follow(playhead); - hudCamShit.setScrollBounds(0, null, -14, null); - - curFrame = 0; - - coolParsed = cast Json.parse(Assets.getText(parsed)); - - var layerNum:Int = 0; - for (layer in coolParsed.AN.TL.L) - { - var frameNum:Int = 0; - - for (frame in layer.FR) - { - var coolFrame:TimelineFrame = new TimelineFrame((frame.I * 12) + 12 * 5, layerNum * 12, frame.DU, frame); - add(coolFrame); - frameNum++; - } - - var layerName:FlxText = new FlxText(0, layerNum * 12, 0, layer.LN, 10); - layerName.color = FlxColor.PURPLE; - layerName.scrollFactor.x = 0; - - var layerBG:FlxSprite = new FlxSprite(0, layerNum * 12).makeGraphic(12 * 4, 12); - layerBG.scrollFactor.x = 0; - - add(layerBG); - add(layerName); - - layerNum++; - } - - - this.cameras = [hudCamShit]; - */ - } -} diff --git a/source/funkin/animate/FlxAnimate.hx b/source/funkin/animate/FlxAnimate.hx deleted file mode 100644 index 1a6d3dbad..000000000 --- a/source/funkin/animate/FlxAnimate.hx +++ /dev/null @@ -1,285 +0,0 @@ -package funkin.animate; - -import funkin.animate.ParseAnimate.AnimJson; -import funkin.animate.ParseAnimate.Sprite; -import funkin.animate.ParseAnimate.Spritemap; -import flixel.FlxCamera; -import flixel.FlxSprite; -import flixel.graphics.FlxGraphic; -import flixel.graphics.frames.FlxAtlasFrames; -import flixel.graphics.frames.FlxFrame.FlxFrameAngle; -import flixel.group.FlxGroup; -import flixel.math.FlxMatrix; -import flixel.math.FlxPoint; -import flixel.math.FlxRect; -import flixel.system.FlxAssets.FlxGraphicAsset; -import haxe.format.JsonParser; -import openfl.Assets; -import openfl.display.BitmapData; -import openfl.geom.Matrix; -import openfl.geom.Rectangle; - -class FlxAnimate extends FlxSymbol -{ - // var myAnim:Animation; - // var animBitmap:BitmapData; - var jsonAnim:AnimJson; - - var sprGrp:FlxTypedGroup; - - public function new(x:Float, y:Float) - { - super(x, y); - - sprGrp = new FlxTypedGroup(); - - var tests:Array = ['tightBarsLol', 'tightestBars']; - - var folder:String = tests[1]; - - frames = FlxAnimate.fromAnimate(Paths.file('images/' + folder + "/spritemap1.png"), Paths.file('images/$folder/spritemap1.json')); - - jsonAnim = cast CoolUtil.coolJSON(Assets.getText(Paths.file('images/$folder/Animation.json'))); - ParseAnimate.generateSymbolmap(jsonAnim.SD.S); - ParseAnimate.resetFrameList(); - - ParseAnimate.parseTimeline(jsonAnim.AN.TL, 0, 0); - - generateSpriteShit(); - - /* var folder:String = 'tightestBars'; - coolParse = cast Json.parse(Assets.getText(Paths.file('images/' + folder + '/Animation.json'))); - - // reverses the layers, for proper rendering! - coolParse.AN.TL.L.reverse(); - super(x, y, coolParse); - - frames = FlxAnimate.fromAnimate(Paths.file('images/' + folder + '/spritemap1.png'), Paths.file('images/' + folder + '/spritemap1.json')); - */ - - // frames - } - - override function draw() - { - // having this commented out fixes some wacky scaling bullshit? - // or fixes drawing it twice? - // super.draw(); - - // renderFrame(coolParse.AN.TL, coolParse, true); - - actualFrameRender(); - } - - /** - * Puts all the needed sprites into a FlxTypedGroup, and properly recycles them? - **/ - function generateSpriteShit() - { - sprGrp.kill(); // kills group, maybe dont need to do this one so broadly? ehh whatev - - for (frameSorted in ParseAnimate.frameList) - { - for (i in frameSorted) - { - // instead of making them every frame, regenerate when needed? - var spr:FlxSymbol = sprGrp.recycle(FlxSymbol); // redo this to recycle from a list later - spr.frames = frames; - spr.frame = spr.frames.getByName(i.frameName); // this one is fine - spr.updateHitbox(); - - // move this? wont work here! - if (FlxG.keys.justPressed.I) - { - trace(i.frameName); - trace(i.depthString); - // trace("random lol: " + i.randomLol); - } - - // cuz its in group, gets a lil fuckie when animated, need to go thru and properly reset each thing for shit like matrix! - // merely resets the matrix to normal ass one! - spr.transformMatrix.identity(); - spr.setPosition(); - - /* for (swagMatrix in i.matrixArray) - { - var alsoSwag:FlxMatrix = new FlxMatrix(swagMatrix[0], swagMatrix[1], swagMatrix[4], swagMatrix[5], swagMatrix[12], swagMatrix[13]); - spr.matrixExposed = true; - spr.transformMatrix.concat(alsoSwag); - }*/ - - // i.fullMatrix.concat - - spr.matrixExposed = true; - - // trace(i.fullMatrix); - - if (i.fullMatrix.a < 0) - { - trace('negative?'); - trace(i.fullMatrix); - } - - spr.transformMatrix.concat(i.fullMatrix); - - if (i.fullMatrix.a < 0) - { - trace('negative?'); - trace(i.fullMatrix); - trace(spr.transformMatrix); - } - - // trace(spr.transformMatrix); - - spr.origin.set(); - - /* for (trpShit in i.trpArray) - { - spr.origin.x -= trpShit[0]; - spr.origin.y -= trpShit[1]; - } - */ - // spr.alpha = 0.3; - - spr.antialiasing = true; - sprGrp.add(spr); - spr.alpha = 0.5; - - /* if (i == "0225") - { - trace('FUNNY MATRIX!'); - trace(spr._matrix); - trace("\n\n MATRIX MAP"); - for (m in ParseAnimate.matrixMap.get("0225")) - { - trace(m); - } - - trace('\n\n'); - }*/ - } - } - - // trace(sprGrp.length); - } - - // fix render order of ALL layers! - // seperate frameList into layers - // go thru animate file to see how it should all be ordered - // per frame symbol stuff to fix lip sync (in ParseAnimate?) - // definitely need to dig through Animate.json stuff - // something with TRP stuff, look through tighterBars (GF scene) - // redo map stuff incase there's multiple assets - // ONE CENTRAL THING FOR THIS DUMBASS BULLSHIT - // sorted framelist put it all in there, then make i actually mean something - - function actualFrameRender() - { - sprGrp.draw(); - } - - // notes to self - // account for different layers - var playingAnim:Bool = false; - var frameTickTypeShit:Float = 0; - var animFrameRate:Int = 24; - - // redo all the matrix animation stuff - - override function update(elapsed:Float) - { - super.update(elapsed); - - if (FlxG.keys.justPressed.SPACE) playingAnim = !playingAnim; - - if (playingAnim) - { - frameTickTypeShit += elapsed; - - // prob fix this framerate thing for higher framerates? - if (frameTickTypeShit >= 1 / 24) - { - changeFrame(1); - frameTickTypeShit = 0; - ParseAnimate.resetFrameList(); - ParseAnimate.parseTimeline(jsonAnim.AN.TL, 0, daFrame); - - generateSpriteShit(); - } - } - - if (FlxG.keys.justPressed.RIGHT) - { - changeFrame(1); - - ParseAnimate.resetFrameList(); - ParseAnimate.parseTimeline(jsonAnim.AN.TL, 0, daFrame); - - generateSpriteShit(); - } - if (FlxG.keys.justPressed.LEFT) changeFrame(-1); - } - - /** - * PARSES THE 'spritemap1.png' or whatever into a FlxAtlasFrames!!! - */ - public static function fromAnimate(Source:FlxGraphicAsset, Description:String):FlxAtlasFrames - { - var graphic:FlxGraphic = FlxG.bitmap.add(Source); - if (graphic == null) return null; - - var frames:FlxAtlasFrames = FlxAtlasFrames.findFrame(graphic); - if (frames != null) return frames; - - if (graphic == null || Description == null) return null; - - frames = new FlxAtlasFrames(graphic); - - var data:Spritemap; - - var json:String = Description; - - // trace(json); - - var funnyJson:Dynamic = {}; - if (Assets.exists(json)) funnyJson = JaySon.parseFile(json); - - // trace(json); - - // data = c - - data = cast funnyJson; - - for (sprite in data.ATLAS.SPRITES) - { - // probably nicer way to do this? Oh well - var swagSprite:Sprite = sprite.SPRITE; - - var rect = FlxRect.get(swagSprite.x, swagSprite.y, swagSprite.w, swagSprite.h); - - var size = new Rectangle(0, 0, rect.width, rect.height); - - var offset = FlxPoint.get(-size.left, -size.top); - var sourceSize = FlxPoint.get(size.width, size.height); - - frames.addAtlasFrame(rect, sourceSize, offset, swagSprite.name); - } - - return frames; - } -} - -// handy json function that has some hashlink fix, see the thing in CoolUtils file to see the link / where i stole it from -class JaySon -{ - public static function parseFile(name:String) - { - var cont = Assets.getText(name); - function is(n:Int, what:Int) - return cont.charCodeAt(n) == what; - return JsonParser.parse(cont.substr(if (is(0, 65279)) /// looks like a HL target, skipping only first character here: - 1 else if (is(0, 239) && is(1, 187) && is(2, 191)) /// it seems to be Neko or PHP, start from position 3: - 3 else /// all other targets, that prepare the UTF string correctly - 0)); - } -} diff --git a/source/funkin/animate/FlxSymbol.hx b/source/funkin/animate/FlxSymbol.hx deleted file mode 100644 index cd01b4937..000000000 --- a/source/funkin/animate/FlxSymbol.hx +++ /dev/null @@ -1,81 +0,0 @@ -package funkin.animate; - -import funkin.animate.ParseAnimate.AnimJson; -import funkin.animate.ParseAnimate.Animation; -import funkin.animate.ParseAnimate.Frame; -import funkin.animate.ParseAnimate.Sprite; -import funkin.animate.ParseAnimate.Spritemap; -import funkin.animate.ParseAnimate.SymbolDictionary; -import funkin.animate.ParseAnimate.Timeline; -import flixel.FlxCamera; -import flixel.FlxSprite; -import flixel.graphics.frames.FlxFrame.FlxFrameAngle; -import flixel.math.FlxAngle; -import flixel.math.FlxMath; -import flixel.math.FlxMatrix; -import flixel.math.FlxPoint; -import lime.system.System; -import openfl.Assets; -import openfl.geom.Matrix; - -class FlxSymbol extends FlxSprite -{ - // Loop types shit - public static inline var LOOP:String = 'LP'; - public static inline var PLAY_ONCE:String = 'PO'; - public static inline var SINGLE_FRAME:String = 'SF'; - - public var transformMatrix:Matrix = new Matrix(); - public var daLoopType:String = 'LP'; // LP by default, is set below!!! - - /** - * Bool flag showing whether transformMatrix is used for rendering or not. - * False by default, which means that transformMatrix isn't used for rendering - */ - public var matrixExposed:Bool = true; - - public function new(x:Float, y:Float) - { - super(x, y); - } - - public var daFrame:Int = 0; - - function changeFrame(frameChange:Int = 0):Void - { - daFrame += frameChange; - } - - /** - * custom "homemade" (nabbed from FlxSkewSprite) draw function, to make having a matrix transform slightly - * less painful - */ - override function drawComplex(camera:FlxCamera):Void - { - _frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY()); - _matrix.translate(-origin.x, -origin.y); - _matrix.scale(scale.x, scale.y); - - if (matrixExposed) - { - _matrix.concat(transformMatrix); - } - - if (bakedRotationAngle <= 0) - { - updateTrig(); - - if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle); - } - - _point.addPoint(origin); - _matrix.translate(_point.x, _point.y); - - if (isPixelPerfectRender(camera)) - { - _matrix.tx = Math.floor(_matrix.tx); - _matrix.ty = Math.floor(_matrix.ty); - } - camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader); - } -} diff --git a/source/funkin/animate/ParseAnimate.hx b/source/funkin/animate/ParseAnimate.hx deleted file mode 100644 index 9375ed47b..000000000 --- a/source/funkin/animate/ParseAnimate.hx +++ /dev/null @@ -1,517 +0,0 @@ -package funkin.animate; - -import haxe.format.JsonParser; -import openfl.Assets; -import openfl.geom.Matrix3D; -import openfl.geom.Matrix; -#if sys -import sys.io.File; -#end - -/** - * Generally designed / written in a way that can be easily taken out of FNF and used elsewhere - * I don't think it even has ties to OpenFL? Could probably just use it for ANY haxe - * project if needed, DOES NEED A LOT OF CLEANUP THOUGH! - */ -class ParseAnimate -{ - // make list of frames needed to render (with ASI) - // make GIANT list of all the frames ever and have them in order? - public static var symbolMap:Map = new Map(); - public static var actualSprites:Map = new Map(); - - var _atlas:Map; - var _symbolData:Map; - var _defaultSymbolName:String; - - public function new(data:AnimJson, atlas:Spritemap) - { - // bitmap data could prob be instead - // this code is mostly nabbed from https://github.com/miltoncandelero/OpenFLAnimateAtlas/blob/master/Source/animateatlas/displayobject/SpriteAnimationLibrary.hx - parseAnimationData(data); - parseAtlasData(atlas); - } - - function parseAnimationData(data:AnimJson):Void - { - _symbolData = new Map(); - - var symbols = data.SD.S; - for (symbol in symbols) - _symbolData[symbol.SN] = preprocessSymbolData(symbol); - - var defaultSymbol:Symbol = preprocessSymbolData(data.AN); - _defaultSymbolName = defaultSymbol.SN; - _symbolData.set(_defaultSymbolName, defaultSymbol); - } - - // at little redundant, does exactly the same thing as genSpritemap() - function parseAtlasData(atlas:Spritemap):Void - { - _atlas = new Map(); - if (atlas.ATLAS != null && atlas.ATLAS.SPRITES != null) - { - for (s in atlas.ATLAS.SPRITES) - _atlas.set(s.SPRITE.name, s.SPRITE); - } - } - - /** - * Not used, was used for testing stuff though! - */ - public static function init() - { - // Main.gids - var folder:String = 'tightestBars'; - - // var spritemap:Spritemap = - // var spritemap:Spritemap = genSpritemap('test/$folder/spritemap1.json'); - - actualSprites = genSpritemap('test/$folder/spritemap1.json'); - - var animation:AnimJson = cast CoolUtil.coolJSON(Assets.getText('src/$folder/Animation.json')); - - generateSymbolmap(animation.SD.S); - - trace("\n\nANIMATION SHIT\n"); - - var timelineLength:Int = 0; - for (lyr in animation.AN.TL.L) - timelineLength = Std.int(Math.max(lyr.FR.length, timelineLength)); - - var content:String = animation.AN.TL.L[0].LN; - content += "TOTAL FRAMES NEEDED: " + timelineLength + "\n"; - - for (frm in 0...timelineLength) - { - trace('FRAME NUMBER ' + frm); - try - { - parseTimeline(animation.AN.TL, 1, frm); - content += 'Good write on frame: ' + frm + "\n"; - } - catch (e) - { - content += "BAD WRITE : " + frm + "\n"; - content += "\t" + e + "\n"; - trace(e); - } - - // File.saveContent("output.txt", content); - } - - parseTimeline(animation.AN.TL, 1, 0); - trace(actualSprites); - } - - /** - * a MAP of SPRITES, not to be confused with Spritemap... lol - */ - public static function genSpritemap(json:String):Map - { - var sprShitty:Spritemap = cast CoolUtil.coolJSON(json); - var sprMap:Map = new Map(); - - for (spr in sprShitty.ATLAS.SPRITES) - sprMap.set(spr.SPRITE.name, spr.SPRITE); - return sprMap; - } - - // should change dis to all private? - public static function generateSymbolmap(symbols:Array) - { - for (symbol in symbols) - { - // trace(symbol.SN + "has: " + symbol.TL.L.length + " LAYERS"); - - symbolMap.set(symbol.SN, symbol); - // parseTimeline(symbol.TL); - } - } - - public static function preprocessSymbolData(anim:Symbol):Symbol - { - var timelineData:Timeline = anim.TL; - var layerData:Array = timelineData.L; - - if (!timelineData.sortedForRender) - { - timelineData.sortedForRender = true; - layerData.reverse(); - } - - for (layerStuff in layerData) - { - var frames:Array = layerStuff.FR; - - for (frame in frames) - { - var elements:Array = frame.E; - for (e in 0...elements.length) - { - var element:Element = elements[e]; - if (element.ASI != null) - { - element = elements[e] = - { - SI: - { - SN: "ATLAS_SYMBOL_SPRITE", - LP: "LP", - TRP: {x: 0, y: 0}, - M3D: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], - FF: 0, - ST: "G", - ASI: element.ASI - } - } - } - } - } - } - - return anim; - } - - public static var curLoopType:String; - - /** - * Stuff for debug parsing - */ - public static var depthTypeBeat:String = ""; - - /** - * Array of bullshit that will eventually be RENDERED by whoever wanna use it! - */ - public static var frameList:Array> = []; - - // for loop stuf - - /** - * Similar to frameList, keeps track of shit according to framess? - * That amount of arrays within arrays is fuckin dumb - * but innermost array is basically just x and y value, cuz im dum - */ - public static var matrixHelp:Array>> = []; - - public static var trpHelpIDK:Array>> = []; - - public static var loopedFrameShit:Int = 0; - - public static var funnyMatrix:Matrix = new Matrix(); - public static var matrixFlipper:Array = []; - - // clean up all the crazy ass arrays - - public static function resetFrameList() - { - // funnyMatrix.identity(); - - frameList = []; - frameList.push([]); - matrixHelp = []; - matrixHelp.push([]); - - trpHelpIDK = []; - trpHelpIDK.push([]); - } - - public static var isFlipped:Bool = false; - - public static function parseTimeline(TL:Timeline, tabbed:Int = 0, ?frameInput:Int) - { - var strTab:String = ""; - for (i in 0...tabbed) - strTab += '\t'; - - for (layer in TL.L) - { - var frameArray:Array = []; - var frameMap:Map = new Map(); - - for (frms in layer.FR) - { - for (i in 0...frms.DU) - frameArray.push(frms.I); - - frameMap.set(frms.I, frms); - } - - if (frameInput == null) frameInput = 0; - - var oldFrm:Int = frameInput; - /* - if (curLoopType == "SF") - { - trace(layer.LN); - - trace(frameArray); - trace(frameInput); - trace(curLoopType); - }*/ - - if (curLoopType == "LP") frameInput = frameArray[frameInput % frameArray.length]; - else if (curLoopType == "SF") - { - frameInput = frameArray[loopedFrameShit]; - - // see what happens when something has more than 2 layer? - // single frame stuff isn't fully implemented - } - else - frameInput = frameArray[frameInput]; - - // trace(frameMap.get(frameInput)); - - var frame:Frame = frameMap.get(frameInput); - - // get somethin sorted per element list, which would essentially be per symbol things properly sorted - // seperate data types if symbol or atlassymbolinstance? would probably be maybe slightly less memory intensive? i dunno - - // goes thru each layer, and then each element - // after it gets thru each element it adds to the layer frame stuff. - // make somethin that works recursively, maybe thats the symbol dictionary type shit? - - for (element in frame.E) - { - if (Reflect.hasField(element, "ASI")) - { - matrixHelp[matrixHelp.length - 1].push(element.ASI.M3D); - - var m3D = element.ASI.M3D; - var lilMatrix:Matrix = new Matrix(m3D[0], m3D[1], m3D[4], m3D[5], m3D[12], m3D[13]); - matrixFlipper.push(lilMatrix); - - // matrixFlipper.reverse(); - - // funnyMatrix.identity(); - - // for (m in matrixFlipper) - // funnyMatrix.concat(m); - - if (isFlipped) - { - trace("MORE FLIPPED SHIT"); - trace("MORE FLIPPED SHIT"); - trace("MORE FLIPPED SHIT"); - trace(funnyMatrix); - trace(matrixFlipper); - } - - // trace(funnyMatrix); - - funnyMatrix.concat(lilMatrix); - // trace(funnyMatrix); - - frameList[frameList.length - 1].push( - { - frameName: element.ASI.N, - depthString: depthTypeBeat, - matrixArray: matrixHelp[matrixHelp.length - 1], - trpArray: trpHelpIDK[trpHelpIDK.length - 1], - fullMatrix: funnyMatrix.clone() - }); - - // flips the matrix once?? I cant remember exactly why it needs to be flipped - // matrixHelp[matrixHelp.length - 1].reverse(); - - // trpHelpIDK = []; - - // push the matrix array after each symbol? - - funnyMatrix.identity(); - matrixFlipper = []; - - depthTypeBeat = ""; - curLoopType = ""; - loopedFrameShit = 0; - - isFlipped = false; - } - else - { - var m3D = element.SI.M3D; - var lilMatrix:Matrix = new Matrix(m3D[0], m3D[1], m3D[4], m3D[5], m3D[12], m3D[13]); - - if (lilMatrix.a == -1) - { - isFlipped = true; - - trace('IS THE NEGATIVE ONE'); - } - - if (isFlipped) trace(lilMatrix); - - funnyMatrix.concat(lilMatrix); - matrixFlipper.push(lilMatrix); - // trace(funnyMatrix); - - matrixHelp[matrixHelp.length - 1].push(element.SI.M3D); - trpHelpIDK[trpHelpIDK.length - 1].push([element.SI.TRP.x, element.SI.TRP.y]); // trpHelpIDK.push(); - depthTypeBeat += "->" + element.SI.SN; - curLoopType = element.SI.LP; - - var inputFrame:Int = element.SI.FF; - - // JANKY FIX, MAY NOT ACCOUNT FOR ALL SCENARIOS OF SINGLE FRAME ANIMATIONS!! - if (curLoopType == "SF") - { - // trace("LOOP SHIT: " + inputFrame); - loopedFrameShit = inputFrame; - } - - // condense the animation code, so it automatically already fills up animation shit per symbol - - parseTimeline(symbolMap.get(element.SI.SN).TL, tabbed + 1, inputFrame); - } - - // idk if this should go per layer or per element / object? - - matrixHelp.push([]); - trpHelpIDK.push([]); - } - - if (tabbed == 0) - { - frameList[frameList.length - 1].reverse(); - frameList.push([]); // new layer essentially - } - } - - frameList.reverse(); - } -} - -typedef VALIDFRAME = -{ - frameName:String, - depthString:String, - matrixArray:Array>, - trpArray:Array>, - fullMatrix:Matrix -} - -typedef AnimJson = -{ - AN:Animation, - SD:SymbolDictionary, - MD:MetaData -} - -typedef Animation = -{ - N:String, - SN:String, - TL:Timeline -} - -typedef SymbolDictionary = -{ - S:Array -} - -typedef Symbol = -{ - /**Symbol name*/ - SN:String, - - TL:Timeline -} - -typedef Timeline = -{ - ?sortedForRender:Bool, - L:Array -} - -typedef Layer = -{ - LN:String, - FR:Array -} - -typedef Frame = -{ - E:Array, - I:Int, - DU:Int - // maybe need to implement names if it has frame labels? -} - -typedef Element = -{ - SI:SymbolInstance, - ?ASI:AlsoSymbolInstance - // lmfao idk what ASI stands for lmfaoo, i dont think its "also" -} - -typedef SymbolInstance = -{ - SN:String, - ASI:AlsoSymbolInstance, - - /**Symbol type, prob either G (graphic), or movie clip?*/ ST:String, - - /**First frame*/ FF:Int, - - /**Loop type, loop ping pong, etc.*/ LP:String, - - /**3D matrix*/ M3D:Array, - - TRP: - { - x:Float, y:Float - } -} - -typedef AlsoSymbolInstance = -{ - N:String, - M3D:Array -} - -typedef MetaData = -{ - /** - * Framerate - */ - FRT:Int -} - -// SPRITEMAP BULLSHIT -typedef Spritemap = -{ - ATLAS: - { - SPRITES:Array - }, - meta:Meta -} - -typedef SpriteBullshit = -{ - SPRITE:Sprite -} - -typedef Sprite = -{ - name:String, - x:Int, - y:Int, - w:Int, - h:Int, - rotated:Bool -} - -typedef Meta = -{ - app:String, - verstion:String, - image:String, - format:String, - size: - { - w:Int, h:Float - }, - resolution:Float -} diff --git a/source/funkin/animate/TimelineFrame.hx b/source/funkin/animate/TimelineFrame.hx deleted file mode 100644 index 530859443..000000000 --- a/source/funkin/animate/TimelineFrame.hx +++ /dev/null @@ -1,63 +0,0 @@ -package funkin.animate; - -import flixel.FlxSprite; -import flixel.input.mouse.FlxMouseEvent; -import flixel.util.FlxColor; -import funkin.animate.ParseAnimate.Frame; - -class TimelineFrame extends FlxSprite -{ - public var data:Frame; - - public function new(x:Float, y:Float, length:Int = 0, data:Frame) - { - super(x, y); - - this.data = data; - - makeGraphic((10 * length) + (2 * (length - 1)), 10, FlxColor.RED); - - FlxMouseEvent.add(this, null, null, function(spr:TimelineFrame) - { - alpha = 0.5; - }, function(spr:TimelineFrame) - { - alpha = 1; - }, false, true, true); - } - - override function update(elapsed:Float) - { - // if (FlxG.mouse.overlaps(this, cameras[1])) - // alpha = 0.6; - // else - // alpha = 1; - - if (FlxG.mouse.overlaps(this, cameras[0]) && FlxG.mouse.justPressed) - { - trace("\nFRAME DATA - \n\tFRAME NUM: " + data.I + "\n\tFRAME DURATION: " + data.DU); - - for (e in data.E) - { - var elementOutput:String = "\n"; - - if (Reflect.hasField(e, 'ASI')) - { - elementOutput += "ELEMENT IS ASI!"; - - elementOutput += "\n\t"; - elementOutput += "FRAME NAME: " + e.ASI.N; - } - else - { - elementOutput += "ELEMENT IS SYMBOL INSTANCE!"; - elementOutput += "\n\tSYMBOL NAME: " + e.SI.SN; - } - - trace(elementOutput); - } - } - - super.update(elapsed); - } -} diff --git a/source/funkin/api/newgrounds/NGUtil.hx b/source/funkin/api/newgrounds/NGUtil.hx index 9d6f02680..88db899d1 100644 --- a/source/funkin/api/newgrounds/NGUtil.hx +++ b/source/funkin/api/newgrounds/NGUtil.hx @@ -49,8 +49,7 @@ class NGUtil trace('checking NG.io version'); GAME_VER = "v" + Application.current.meta.get('version'); - NG.core.calls.app.getCurrentVersion(GAME_VER).addDataHandler(function(response) - { + NG.core.calls.app.getCurrentVersion(GAME_VER).addDataHandler(function(response) { GAME_VER = response.result.data.currentVersion; trace('CURRENT NG VERSION: ' + GAME_VER); callback(GAME_VER); @@ -141,8 +140,7 @@ class NGUtil var onCancel:Void->Void = null; if (onComplete != null) { - onSuccess = function() - { + onSuccess = function() { onNGLogin(); onComplete(Success); } diff --git a/source/funkin/audio/FlxAudioGroup.hx b/source/funkin/audio/FlxAudioGroup.hx index 66044b3cc..b506eaed9 100644 --- a/source/funkin/audio/FlxAudioGroup.hx +++ b/source/funkin/audio/FlxAudioGroup.hx @@ -29,8 +29,7 @@ class FlxAudioGroup extends FlxTypedGroup function set_time(time:Float):Float { - forEachAlive(function(sound:FlxSound) - { + forEachAlive(function(sound:FlxSound) { // account for different offsets per sound? sound.time = time; }); @@ -52,8 +51,7 @@ class FlxAudioGroup extends FlxTypedGroup function set_volume(volume:Float):Float { - forEachAlive(function(sound:FlxSound) - { + forEachAlive(function(sound:FlxSound) { sound.volume = volume; }); @@ -80,8 +78,7 @@ class FlxAudioGroup extends FlxTypedGroup { #if FLX_PITCH trace('Setting audio pitch to ' + val); - forEachAlive(function(sound:FlxSound) - { + forEachAlive(function(sound:FlxSound) { sound.pitch = val; }); #end @@ -96,8 +93,7 @@ class FlxAudioGroup extends FlxTypedGroup function set_autoDestroyMembers(value:Bool):Bool { autoDestroyMembers = value; - forEachAlive(function(sound:FlxSound) - { + forEachAlive(function(sound:FlxSound) { sound.autoDestroy = value; }); return value; @@ -131,8 +127,7 @@ class FlxAudioGroup extends FlxTypedGroup */ public function pause() { - forEachAlive(function(sound:FlxSound) - { + forEachAlive(function(sound:FlxSound) { sound.pause(); }); } @@ -142,8 +137,7 @@ class FlxAudioGroup extends FlxTypedGroup */ public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float) { - forEachAlive(function(sound:FlxSound) - { + forEachAlive(function(sound:FlxSound) { sound.play(forceRestart, startTime, endTime); }); } @@ -153,8 +147,7 @@ class FlxAudioGroup extends FlxTypedGroup */ public function resume() { - forEachAlive(function(sound:FlxSound) - { + forEachAlive(function(sound:FlxSound) { sound.resume(); }); } @@ -164,8 +157,7 @@ class FlxAudioGroup extends FlxTypedGroup */ public function stop() { - forEachAlive(function(sound:FlxSound) - { + forEachAlive(function(sound:FlxSound) { sound.stop(); }); } @@ -188,8 +180,7 @@ class FlxAudioGroup extends FlxTypedGroup { var deviation:Float = 0; - forEachAlive(function(sound:FlxSound) - { + forEachAlive(function(sound:FlxSound) { if (targetTime == null) targetTime = sound.time; else { diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx new file mode 100644 index 000000000..1f1dc239e --- /dev/null +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -0,0 +1,174 @@ +package funkin.graphics.adobeanimate; + +import flixel.util.FlxSignal.FlxTypedSignal; +import flxanimate.FlxAnimate; +import flxanimate.FlxAnimate.Settings; +import flixel.math.FlxPoint; + +/** + * A sprite which provides convenience functions for rendering a texture atlas with animations. + */ +class FlxAtlasSprite extends FlxAnimate +{ + static final SETTINGS:Settings = + { + // ?ButtonSettings:Map, + FrameRate: 24.0, + Reversed: false, + // ?OnComplete:Void -> Void, + ShowPivot: true, + Antialiasing: true, + ScrollFactor: new FlxPoint(1, 1), + // Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset + }; + + /** + * Signal dispatched when an animation finishes playing. + */ + public var onAnimationFinish:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + + var currentAnimation:String; + + var canPlayOtherAnims:Bool = true; + + public function new(x:Float, y:Float, path:String) + { + super(x, y, path); + + if (this.anim.curInstance == null) + { + throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?'; + } + + this.antialiasing = true; + + onAnimationFinish.add(cleanupAnimation); + + // This defaults the sprite to play the first animation in the atlas, + // then pauses it. This ensures symbols are intialized properly. + this.anim.play(''); + this.anim.pause(); + } + + /** + * @return A list of all the animations this sprite has available. + */ + public function listAnimations():Array + { + return this.anim.getFrameLabels(); + } + + /** + * @param id A string ID of the animation. + * @return Whether the animation was found on this sprite. + */ + public function hasAnimation(id:String):Bool + { + return getLabelIndex(id) != -1; + } + + /** + * @return The current animation being played. + */ + public function getCurrentAnimation():String + { + return this.currentAnimation; + } + + /** + * Plays an animation. + * @param id A string ID of the animation to play. + * @param restart Whether to restart the animation if it is already playing. + * @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing + */ + public function playAnimation(id:String, ?restart:Bool = false, ?ignoreOther:Bool = false):Void + { + // Skip if not allowed to play animations. + if ((!canPlayOtherAnims && !ignoreOther)) return; + + if (id == null || id == '') id = this.currentAnimation; + + if (this.currentAnimation == id && !restart) + { + if (anim.isPlaying) + { + // Skip if animation is already playing. + return; + } + else + { + // Resume animation if it's paused. + anim.play('', false, false); + } + } + + // Skip if the animation doesn't exist + if (!hasAnimation(id)) + { + trace('Animation ' + id + ' not found'); + return; + } + + // Stop the current animation if it is playing. + // This includes removing existing frame callbacks. + if (this.currentAnimation != null) this.stopAnimation(); + + // Add a callback to ensure `onAnimationFinish` is dispatched. + addFrameCallback(getNextFrameLabel(id), function() { + trace('Animation finished: ' + id); + onAnimationFinish.dispatch(id); + }); + + // Prevent other animations from playing if `ignoreOther` is true. + if (ignoreOther) canPlayOtherAnims = false; + + // Move to the first frame of the animation. + goToFrameLabel(id); + this.currentAnimation = id; + } + + /** + * Stops the current animation. + */ + public function stopAnimation():Void + { + if (this.currentAnimation == null) return; + + this.anim.removeAllCallbacksFrom(getNextFrameLabel(this.currentAnimation)); + + goToFrameIndex(0); + } + + function addFrameCallback(label:String, callback:Void->Void):Void + { + var frameLabel = this.anim.getFrameLabel(label); + frameLabel.add(callback); + } + + inline function goToFrameLabel(label:String):Void + { + this.anim.goToFrameLabel(label); + } + + inline function getNextFrameLabel(label:String):String + { + return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length]; + } + + inline function getLabelIndex(label:String):Int + { + return listAnimations().indexOf(label); + } + + inline function goToFrameIndex(index:Int):Void + { + this.anim.curFrame = index; + } + + public function cleanupAnimation(_:String):Void + { + canPlayOtherAnims = true; + this.currentAnimation = null; + this.anim.stop(); + } +} diff --git a/source/funkin/FlxVideo.hx b/source/funkin/graphics/video/FlxVideo.hx similarity index 93% rename from source/funkin/FlxVideo.hx rename to source/funkin/graphics/video/FlxVideo.hx index 9349164b2..db95c3d95 100644 --- a/source/funkin/FlxVideo.hx +++ b/source/funkin/graphics/video/FlxVideo.hx @@ -1,4 +1,4 @@ -package funkin; +package funkin.graphics.video; import flixel.FlxBasic; import flixel.FlxSprite; @@ -7,6 +7,9 @@ import openfl.media.Video; import openfl.net.NetConnection; import openfl.net.NetStream; +/** + * Plays a video via a NetStream. Only works on HTML5. + */ class FlxVideo extends FlxBasic { var video:Video; diff --git a/source/funkin/play/Fighter.hx b/source/funkin/play/Fighter.hx index 42e044874..691d21b83 100644 --- a/source/funkin/play/Fighter.hx +++ b/source/funkin/play/Fighter.hx @@ -7,12 +7,11 @@ class Fighter extends BaseCharacter { public function new(?x:Float = 0, ?y:Float = 0, ?char:String = "pico-fighter") { - super(char); + super(char, Custom); this.x = x; this.y = y; - animation.finishCallback = function(anim:String) - { + animation.finishCallback = function(anim:String) { switch anim { case "punch low" | "punch high" | "block" | 'dodge': diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index b2e0bba12..61d2dc509 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -6,7 +6,8 @@ import flixel.FlxSprite; import flixel.FlxState; import flixel.FlxSubState; import flixel.addons.transition.FlxTransitionableState; -import flixel.group.FlxGroup; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.input.keyboard.FlxKey; import flixel.math.FlxMath; import flixel.math.FlxRect; import flixel.text.FlxText; @@ -23,12 +24,12 @@ import funkin.SongLoad.SwagSong; import funkin.charting.ChartingState; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; -import funkin.play.GameOverSubstate; -import funkin.play.HealthIcon; import funkin.play.Strumline.StrumlineArrow; import funkin.play.Strumline.StrumlineStyle; import funkin.play.character.BaseCharacter; +import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.CharacterData; +import funkin.play.cutscene.VanillaCutscenes; import funkin.play.event.SongEvent.SongEventParser; import funkin.play.scoring.Scoring; import funkin.play.song.Song; @@ -37,7 +38,7 @@ import funkin.play.song.SongData.SongNoteData; import funkin.play.song.SongData.SongPlayableChar; import funkin.play.song.SongValidator; import funkin.play.stage.Stage; -import funkin.play.stage.StageData; +import funkin.play.stage.StageData.StageDataParser; import funkin.ui.PopUpStuff; import funkin.ui.PreferencesMenu; import funkin.ui.stageBuildShit.StageOffsetSubstate; @@ -48,6 +49,9 @@ import lime.ui.Haptic; import Discord.DiscordClient; #end +/** + * The gameplay state, where all the rhythm gaming happens. + */ class PlayState extends MusicBeatState { /** @@ -81,7 +85,7 @@ class PlayState extends MusicBeatState public static var isPracticeMode:Bool = false; /** - * Whether the game is currently in a cutscene, and gameplay should be stopped. + * Whether the game is currently in an animated cutscene, and gameplay should be stopped. */ public static var isInCutscene:Bool = false; @@ -90,6 +94,11 @@ class PlayState extends MusicBeatState */ public static var disableKeys:Bool = false; + /* + * Whether the game is currently in dialog, and gameplay should be stopped. + */ + public static var isInDialog:Bool = false; + /** * Whether the game is currently in the countdown before the song resumes. */ @@ -188,6 +197,12 @@ class PlayState extends MusicBeatState */ var criticalFailure:Bool = false; + /** + * How many beats between camera zooms. + * @default One camera zoom per four beats. + */ + var camZoomRate:Int = 4; + /** * RENDER OBJECTS */ @@ -243,6 +258,11 @@ class PlayState extends MusicBeatState */ public var camGame:FlxCamera; + /** + * The camera which contains, and controls visibility of, a video cutscene. + */ + public var camCutscene:FlxCamera; + /** * PROPERTIES */ @@ -273,9 +293,7 @@ class PlayState extends MusicBeatState var vocals:VoicesGroup; var vocalsFinished:Bool = false; - var camZooming:Bool = false; var gfSpeed:Int = 1; - // private var combo:Int = 0; var generatedMusic:Bool = false; var startingSong:Bool = false; @@ -290,14 +308,14 @@ class PlayState extends MusicBeatState #if discord_rpc // Discord RPC variables - var storyDifficultyText:String = ""; - var iconRPC:String = ""; + var storyDifficultyText:String = ''; + var iconRPC:String = ''; var songLength:Float = 0; - var detailsText:String = ""; - var detailsPausedText:String = ""; + var detailsText:String = ''; + var detailsPausedText:String = ''; #end - override public function create() + override public function create():Void { super.create(); @@ -458,18 +476,18 @@ class PlayState extends MusicBeatState leftWatermarkText.cameras = [camHUD]; rightWatermarkText.cameras = [camHUD]; - // if (SONG.song == 'South') - // FlxG.camera.alpha = 0.7; - // UI_camera.zoom = 1; - - // cameras = [FlxG.cameras.list[1]]; + // Starting song! startingSong = true; + // TODO: Softcode cutscenes. + // TODO: Alternatively: make a song script that allows startCountdown to be called, + // then cancels the countdown, hides the UI, plays the cutscene, + // then calls PlayState.startCountdown later? if (isStoryMode && !seenCutscene) { seenCutscene = true; - switch (currentSong.song.toLowerCase()) + switch (currentSong_NEW.songId.toLowerCase()) { case "winter-horrorland": VanillaCutscenes.playHorrorStartCutscene(); @@ -483,9 +501,6 @@ class PlayState extends MusicBeatState VanillaCutscenes.playGunsCutscene(); default: // VanillaCutscenes will call startCountdown later. - // TODO: Alternatively: make a song script that allows startCountdown to be called, - // then cancels the countdown, hides the strumline, plays the cutscene, - // then calls Countdown.performCountdown() startCountdown(); } } @@ -497,6 +512,10 @@ class PlayState extends MusicBeatState #if debug this.rightWatermarkText.text = Constants.VERSION; #end + + #if debug + FlxG.console.registerObject('playState', this); + #end } function get_currentChart():SongDifficulty @@ -508,7 +527,7 @@ class PlayState extends MusicBeatState /** * Initializes the game and HUD cameras. */ - function initCameras() + function initCameras():Void { // Configure the default camera zoom level. defaultCameraZoom = FlxCamera.defaultZoom * 1.05; @@ -516,12 +535,15 @@ class PlayState extends MusicBeatState camGame = new SwagCamera(); camHUD = new FlxCamera(); camHUD.bgColor.alpha = 0; + camCutscene = new FlxCamera(); + camCutscene.bgColor.alpha = 0; FlxG.cameras.reset(camGame); FlxG.cameras.add(camHUD, false); + FlxG.cameras.add(camCutscene, false); } - function initStage() + function initStage():Void { if (currentSong_NEW != null) { @@ -533,23 +555,21 @@ class PlayState extends MusicBeatState switch (currentSong.song.toLowerCase()) { case 'spookeez' | 'monster' | 'south': - currentStageId = "spookyMansion"; + currentStageId = 'spookyMansion'; case 'pico' | 'blammed' | 'philly': currentStageId = 'phillyTrain'; - case "milf" | 'satin-panties' | 'high': + case 'milf' | 'satin-panties' | 'high': currentStageId = 'limoRide'; - case "cocoa" | 'eggnog': + case 'cocoa' | 'eggnog': currentStageId = 'mallXmas'; case 'winter-horrorland': currentStageId = 'mallEvil'; case 'senpai' | 'roses': currentStageId = 'school'; - case "darnell" | "lit-up" | "2hot": + case 'darnell' | 'lit-up' | '2hot': currentStageId = 'phillyStreets'; - // currentStageId = 'pyro'; - case "blazin": + case 'blazin': currentStageId = 'phillyBlazin'; - // currentStageId = 'pyro'; case 'pyro': currentStageId = 'pyro'; case 'thorns': @@ -557,13 +577,13 @@ class PlayState extends MusicBeatState case 'guns' | 'stress' | 'ugh': currentStageId = 'tankmanBattlefield'; default: - currentStageId = "mainStage"; + currentStageId = 'mainStage'; } // Loads the relevant stage based on its ID. loadStage(currentStageId); } - function initStage_NEW() + function initStage_NEW():Void { if (currentChart == null) { @@ -800,11 +820,19 @@ class PlayState extends MusicBeatState if (girlfriend != null) { currentStage.addCharacter(girlfriend, GF); + + #if debug + FlxG.console.registerObject('gf', girlfriend); + #end } if (boyfriend != null) { currentStage.addCharacter(boyfriend, BF); + + #if debug + FlxG.console.registerObject('bf', boyfriend); + #end } if (dad != null) @@ -812,6 +840,10 @@ class PlayState extends MusicBeatState currentStage.addCharacter(dad, DAD); // Camera starts at dad. cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y); + + #if debug + FlxG.console.registerObject('dad', dad); + #end } // Rearrange by z-indexes. @@ -872,6 +904,10 @@ class PlayState extends MusicBeatState // Add the stage to the scene. this.add(currentStage); + + #if debug + FlxG.console.registerObject('stage', currentStage); + #end } } @@ -941,7 +977,7 @@ class PlayState extends MusicBeatState { if (dialogueBox != null) { - isInCutscene = true; + isInDialog = true; if (currentSong.song.toLowerCase() == 'thorns') { @@ -1016,7 +1052,7 @@ class PlayState extends MusicBeatState function generateSong():Void { - // FlxG.log.add(ChartParser.parse()); + trace('===WARNING=== Song uses old chart format!!!!!'); Conductor.forceBPM(currentSong.bpm); @@ -1030,8 +1066,6 @@ class PlayState extends MusicBeatState vocalsFinished = true; }; - trace(vocals); - activeNotes = new FlxTypedGroup(); activeNotes.zIndex = 1000; add(activeNotes); @@ -1383,10 +1417,15 @@ class PlayState extends MusicBeatState inputSpitter = []; + // Reset music properly. + FlxG.sound.music.pause(); vocals.pause(); - FlxG.sound.music.time = 0; + vocals.time = 0; + + FlxG.sound.music.volume = 1; + vocals.volume = 1; currentStage.resetStage(); @@ -1523,7 +1562,7 @@ class PlayState extends MusicBeatState if (health > 2.0) health = 2.0; if (health < 0.0) health = 0.0; - if (camZooming && subState == null) + if (subState == null) { FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95); camHUD.zoom = FlxMath.lerp(1 * FlxCamera.defaultZoom, camHUD.zoom, 0.95); @@ -1542,7 +1581,6 @@ class PlayState extends MusicBeatState switch (Conductor.currentBeat) { case 16: - camZooming = true; gfSpeed = 2; case 48: gfSpeed = 1; @@ -1553,7 +1591,7 @@ class PlayState extends MusicBeatState } } - if ((!isInCutscene && !disableKeys) && !_exiting) + if (!isInCutscene && !isInDialog && !disableKeys && !_exiting) { // RESET = Quick Game Over Screen if (controls.RESET) @@ -1666,8 +1704,6 @@ class PlayState extends MusicBeatState if (!daNote.mustPress && daNote.wasGoodHit && !daNote.tooLate) { - if (currentSong != null && currentSong.song != 'Tutorial') camZooming = true; - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, Highscore.tallies.combo, true); dispatchEvent(event); @@ -1739,14 +1775,25 @@ class PlayState extends MusicBeatState } } - if (!isInCutscene && !disableKeys) keyShit(true); + if (!isInCutscene && !isInDialog && !disableKeys) keyShit(true); + if (isInCutscene && !disableKeys) handleCutsceneKeys(); + } + + static final CUTSCENE_KEYS:Array = [SPACE, ESCAPE, ENTER]; + + function handleCutsceneKeys():Void + { + if (FlxG.keys.anyJustPressed(CUTSCENE_KEYS)) + { + VanillaCutscenes.finishCutscene(); + } } function applyClipRect(daNote:Note):Void { // clipRect is applied to graphic itself so use frame Heights var swagRect:FlxRect = new FlxRect(0, 0, daNote.frameWidth, daNote.frameHeight); - var strumLineMid = playerStrumline.y + Note.swagWidth / 2; + var strumLineMid:Float = playerStrumline.y + Note.swagWidth / 2; if (PreferencesMenu.getPref('downscroll')) { @@ -1903,7 +1950,7 @@ class PlayState extends MusicBeatState trace('WENT TO RESULTS SCREEN!'); // unloadAssets(); - camZooming = false; + camZoomRate = 0; FlxG.camera.follow(PlayState.instance.currentStage.getGirlfriend(), null, 0.05); FlxG.camera.targetOffset.y -= 350; @@ -2310,25 +2357,22 @@ class PlayState extends MusicBeatState } } - // Manage the camera focus, if necessary. - // controlCamera(); - - // HARDCODING FOR MILF ZOOMS! - if (PreferencesMenu.getPref('camera-zoom')) { + // TODO: Move this into a song script. if (currentSong != null && currentSong.song.toLowerCase() == 'milf' && Conductor.currentBeat >= 168 - && Conductor.currentBeat < 200 - && camZooming - && FlxG.camera.zoom < 1.35) + && Conductor.currentBeat < 200) { - FlxG.camera.zoom += 0.015 * FlxCamera.defaultZoom; - camHUD.zoom += 0.03; + camZoomRate = 1; + } + if (currentSong != null && currentSong.song.toLowerCase() == 'milf' && Conductor.currentBeat >= 200) + { + camZoomRate = 4; } - if (camZooming && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) && Conductor.currentBeat % 4 == 0) + if (FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) && camZoomRate > 0 && Conductor.currentBeat % camZoomRate == 0) { FlxG.camera.zoom += 0.015 * FlxCamera.defaultZoom; camHUD.zoom += 0.03; @@ -2446,7 +2490,7 @@ class PlayState extends MusicBeatState * Function called before opening a new substate. * @param subState The substate to open. */ - override function openSubState(subState:FlxSubState) + public override function openSubState(subState:FlxSubState) { // If there is a substate which requires the game to continue, // then make this a condition. @@ -2472,7 +2516,7 @@ class PlayState extends MusicBeatState * Function called before closing the current substate. * @param subState */ - override function closeSubState() + public override function closeSubState() { if (isGamePaused) { @@ -2482,7 +2526,7 @@ class PlayState extends MusicBeatState if (event.eventCanceled) return; - if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals(); + if (FlxG.sound.music != null && !startingSong && !isInCutscene && !isInDialog) resyncVocals(); // Resume the countdown. Countdown.resumeCountdown(); @@ -2502,12 +2546,14 @@ class PlayState extends MusicBeatState * Prepares to start the countdown. * Ends any running cutscenes, creates the strumlines, and starts the countdown. */ - function startCountdown():Void + public function startCountdown():Void { - var result = Countdown.performCountdown(currentStageId.startsWith('school')); + // If Countdown.performCountdown returns false, then the countdown was canceled by a script. + var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school')); if (!result) return; isInCutscene = false; + isInDialog = false; camHUD.visible = true; talking = false; @@ -2529,6 +2575,8 @@ class PlayState extends MusicBeatState if (currentStage != null) currentStage.dispatchToCharacters(event); // TODO: Dispatch event to song script + + // TODO: Dispatch event to note script } /** diff --git a/source/funkin/play/VanillaCutscenes.hx b/source/funkin/play/VanillaCutscenes.hx deleted file mode 100644 index 020612470..000000000 --- a/source/funkin/play/VanillaCutscenes.hx +++ /dev/null @@ -1,105 +0,0 @@ -package funkin.play; - -import flixel.FlxSprite; -import flixel.tweens.FlxEase; -import flixel.tweens.FlxEase; -import flixel.tweens.FlxTween; -import flixel.tweens.FlxTween; -import flixel.util.FlxColor; -import flixel.util.FlxTimer; - -/** - * Static methods for playing cutscenes in the PlayState. - * TODO: Un-hardcode this shit!!!!!1! - */ -class VanillaCutscenes -{ - public static function playUghCutscene():Void - { - playVideoCutscene('music/ughCutscene.mp4'); - } - - public static function playGunsCutscene():Void - { - playVideoCutscene('music/gunsCutscene.mp4'); - } - - public static function playStressCutscene():Void - { - playVideoCutscene('music/stressCutscene.mp4'); - } - - static var blackScreen:FlxSprite; - - /** - * Plays a cutscene from a video file, then starts the countdown once the video is done. - * TODO: Cutscene is currently skipped on native platforms. - */ - static function playVideoCutscene(path:String):Void - { - PlayState.isInCutscene = true; - PlayState.instance.camHUD.visible = false; - - blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); - blackScreen.scrollFactor.set(0, 0); - PlayState.instance.add(blackScreen); - - #if html5 - var vid:FlxVideo = new FlxVideo(path); - vid.finishCallback = finishVideoCutscene; - #else - finishVideoCutscene(); - #end - } - - /** - * Does the cleanup to start the countdown after the video is done. - * Gets called immediately if the video can't be played. - */ - static function finishVideoCutscene():Void - { - PlayState.instance.remove(blackScreen); - blackScreen = null; - - FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut}); - @:privateAccess - PlayState.instance.startCountdown(); - // @:privateAccess - // PlayState.instance.controlCamera(); - } - - public static function playHorrorStartCutscene() - { - PlayState.isInCutscene = true; - PlayState.instance.camHUD.visible = false; - - blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); - blackScreen.scrollFactor.set(0, 0); - PlayState.instance.add(blackScreen); - - new FlxTimer().start(0.1, function(tmr:FlxTimer) - { - PlayState.instance.remove(blackScreen); - FlxG.sound.play(Paths.sound('Lights_Turn_On')); - PlayState.instance.cameraFollowPoint.y = -2050; - PlayState.instance.cameraFollowPoint.x += 200; - FlxG.camera.focusOn(PlayState.instance.cameraFollowPoint.getPosition()); - FlxG.camera.zoom = 1.5; - - new FlxTimer().start(0.8, function(tmr:FlxTimer) - { - PlayState.instance.camHUD.visible = true; - PlayState.instance.remove(blackScreen); - blackScreen = null; - FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, 2.5, - { - ease: FlxEase.quadInOut, - onComplete: function(twn:FlxTween) - { - Countdown.performCountdown(false); - } - }); - }); - }); - } -} diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx new file mode 100644 index 000000000..339662704 --- /dev/null +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -0,0 +1,722 @@ +package funkin.play.character; + +import flixel.animation.FlxAnimationController; +import flixel.FlxCamera; +import flixel.FlxSprite; +import flixel.graphics.frames.FlxFrame; +import flixel.graphics.frames.FlxFramesCollection; +import flixel.math.FlxMath; +import flixel.math.FlxPoint.FlxCallbackPoint; +import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.system.FlxAssets.FlxGraphicAsset; +import flixel.util.FlxColor; +import flixel.util.FlxDestroyUtil; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.modding.events.ScriptEvent; +import funkin.play.character.CharacterData.CharacterRenderType; +import openfl.display.BitmapData; +import openfl.display.BlendMode; + +/** + * Individual animation data for an AnimateAtlasCharacter. + */ +typedef AnimateAtlasAnimation = +{ + name:String, + prefix:String, + offset:Null>, + loop:Bool, +} + +/** + * An AnimateAtlasCharacter is a Character which is rendered by + * displaying an animation derived from an Adobe Animate texture atlas spritesheet file. + * + * BaseCharacter has game logic, AnimateAtlasCharacter has only rendering logic. + * KEEP THEM SEPARATE! + */ +class AnimateAtlasCharacter extends BaseCharacter +{ + // BaseCharacter extends FlxSprite but we can't make it also extend FlxAtlasSprite UGH + // I basically copied the code from FlxSpriteGroup to make the FlxAtlasSprite a "child" of this class + var mainSprite:FlxAtlasSprite; + + var _skipTransformChildren:Bool = false; + + var animations:Map = new Map(); + var currentAnimation:String; + + public function new(id:String) + { + super(id, CharacterRenderType.AnimateAtlas); + } + + override function initVars():Void + { + // this.flixelType = SPRITEGROUP; + + // TODO: Make `animation` a stub that redirects calls to `mainSprite`? + animation = new FlxAnimationController(this); + + offset = new FlxCallbackPoint(offsetCallback); + origin = new FlxCallbackPoint(originCallback); + scale = new FlxCallbackPoint(scaleCallback); + scrollFactor = new FlxCallbackPoint(scrollFactorCallback); + + scale.set(1, 1); + scrollFactor.set(1, 1); + + initMotionVars(); + } + + override function onCreate(event:ScriptEvent):Void + { + trace('Creating Animate Atlas character: ' + this.characterId); + + var atlasSprite:FlxAtlasSprite = loadAtlasSprite(); + setSprite(atlasSprite); + loadAnimations(); + + super.onCreate(event); + } + + public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void + { + if ((!canPlayOtherAnims && !ignoreOther)) return; + + currentAnimation = name; + var prefix:String = getAnimationData(name).prefix; + if (prefix == null) prefix = name; + this.mainSprite.playAnimation(prefix, restart, ignoreOther); + } + + function loadAtlasSprite():FlxAtlasSprite + { + trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.'); + + var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas(_data.assetPath, 'shared')); + + sprite.onAnimationFinish.removeAll(); + sprite.onAnimationFinish.add(this.onAnimationFinished); + + return sprite; + } + + override function onAnimationFinished(prefix:String):Void + { + super.onAnimationFinished(prefix); + + if (getAnimationData() != null && getAnimationData().loop) + { + playAnimation(prefix, true, false); + } + else + { + this.mainSprite.cleanupAnimation(prefix); + } + } + + function setSprite(sprite:FlxAtlasSprite):Void + { + trace('[ATLASCHAR] Applying sprite properties to ${characterId}'); + + this.mainSprite = sprite; + + var feetPos:FlxPoint = feetPosition; + this.updateHitbox(); + + sprite.x = this.x; + sprite.y = this.y; + sprite.alpha *= alpha; + sprite.flipX = flipX; + sprite.flipY = flipY; + sprite.scrollFactor.copyFrom(scrollFactor); + sprite.cameras = _cameras; // _cameras instead of cameras because get_cameras() will not return null + + if (clipRect != null) clipRectTransform(sprite, clipRect); + } + + function loadAnimations():Void + { + trace('[ATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}'); + + var animData:Array = cast _data.animations; + + for (anim in animData) + { + animations.set(anim.name, anim); + } + } + + public override function getCurrentAnimation():String + { + return this.mainSprite.getCurrentAnimation(); + } + + function getAnimationData(name:String = null):AnimateAtlasAnimation + { + if (name == null) name = getCurrentAnimation(); + return animations.get(name); + } + + // + // + // Code copied from FlxSpriteGroup + // + // + + /** + * Handy function that allows you to quickly transform one property of sprites in this group at a time. + * + * @param callback Function to transform the sprites. Example: + * `function(sprite, v:Dynamic) { s.acceleration.x = v; s.makeGraphic(10,10,0xFF000000); }` + * @param value Value which will passed to lambda function. + */ + @:generic + public function transformChildren(callback:FlxAtlasSprite->V->Void, value:V):Void + { + if (_skipTransformChildren || this.mainSprite == null) return; + + callback(this.mainSprite, value); + } + + /** + * Calls `kill()` on the group's members and then on the group itself. + * You can revive this group later via `revive()` after this. + */ + public override function kill():Void + { + _skipTransformChildren = true; + super.kill(); + _skipTransformChildren = false; + this.mainSprite.kill(); + } + + /** + * Revives the group. + */ + public override function revive():Void + { + _skipTransformChildren = true; + super.revive(); // calls set_exists and set_alive + _skipTransformChildren = false; + this.mainSprite.revive(); + } + + /** + * **WARNING:** A destroyed `FlxBasic` can't be used anymore. + * It may even cause crashes if it is still part of a group or state. + * You may want to use `kill()` instead if you want to disable the object temporarily only and `revive()` it later. + * + * This function is usually not called manually (Flixel calls it automatically during state switches for all `add()`ed objects). + * + * Override this function to `null` out variables manually or call `destroy()` on class members if necessary. + * Don't forget to call `super.destroy()`! + */ + public override function destroy():Void + { + // normally don't have to destroy FlxPoints, but these are FlxCallbackPoints! + offset = FlxDestroyUtil.destroy(offset); + origin = FlxDestroyUtil.destroy(origin); + scale = FlxDestroyUtil.destroy(scale); + scrollFactor = FlxDestroyUtil.destroy(scrollFactor); + + this.mainSprite = FlxDestroyUtil.destroy(this.mainSprite); + + super.destroy(); + } + + /** + * Check and see if any sprite in this group is currently on screen. + * + * @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera. + * @return Whether the object is on screen or not. + */ + public override function isOnScreen(?camera:FlxCamera):Bool + { + if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible && this.mainSprite.isOnScreen(camera)) return true; + + return false; + } + + /** + * Checks to see if a point in 2D world space overlaps any `FlxSprite` object from this group. + * + * @param Point The point in world space you want to check. + * @param InScreenSpace Whether to take scroll factors into account when checking for overlap. + * @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera. + * @return Whether or not the point overlaps this group. + */ + public override function overlapsPoint(point:FlxPoint, inScreenSpace:Bool = false, camera:FlxCamera = null):Bool + { + var result:Bool = false; + result = this.mainSprite.overlapsPoint(point, inScreenSpace, camera); + return result; + } + + /** + * Checks to see if a point in 2D world space overlaps any of FlxSprite object's current displayed pixels. + * This check is ALWAYS made in screen space, and always takes scroll factors into account. + * + * @param Point The point in world space you want to check. + * @param Mask Used in the pixel hit test to determine what counts as solid. + * @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera. + * @return Whether or not the point overlaps this object. + */ + public override function pixelsOverlapPoint(point:FlxPoint, Mask:Int = 0xFF, Camera:FlxCamera = null):Bool + { + var result:Bool = false; + if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible) + { + result = this.mainSprite.pixelsOverlapPoint(point, Mask, Camera); + } + return result; + } + + public override function update(elapsed:Float):Void + { + this.mainSprite.update(elapsed); + + if (moves) updateMotion(elapsed); + } + + public override function draw():Void + { + this.mainSprite.draw(); + + #if FLX_DEBUG + if (FlxG.debugger.drawDebug) drawDebug(); + #end + } + + inline function xTransform(sprite:FlxSprite, x:Float):Void + sprite.x += x; // addition + + inline function yTransform(sprite:FlxSprite, y:Float):Void + sprite.y += y; // addition + + inline function angleTransform(sprite:FlxSprite, angle:Float):Void + sprite.angle += angle; // addition + + inline function alphaTransform(sprite:FlxSprite, alpha:Float):Void + { + if (sprite.alpha != 0 || alpha == 0) + { + sprite.alpha *= alpha; // multiplication + } + else + { + sprite.alpha = 1 / alpha; // direct set to avoid stuck sprites + } + } + + inline function directAlphaTransform(sprite:FlxSprite, alpha:Float):Void + sprite.alpha = alpha; // direct set + + inline function facingTransform(sprite:FlxSprite, facing:Int):Void + sprite.facing = facing; + + inline function flipXTransform(sprite:FlxSprite, flipX:Bool):Void + sprite.flipX = flipX; + + inline function flipYTransform(sprite:FlxSprite, flipY:Bool):Void + sprite.flipY = flipY; + + inline function movesTransform(sprite:FlxSprite, moves:Bool):Void + sprite.moves = moves; + + inline function pixelPerfectTransform(sprite:FlxSprite, pixelPerfect:Bool):Void + sprite.pixelPerfectRender = pixelPerfect; + + inline function gColorTransform(sprite:FlxSprite, color:Int):Void + sprite.color = color; + + inline function blendTransform(sprite:FlxSprite, blend:BlendMode):Void + sprite.blend = blend; + + inline function immovableTransform(sprite:FlxSprite, immovable:Bool):Void + sprite.immovable = immovable; + + inline function visibleTransform(sprite:FlxSprite, visible:Bool):Void + sprite.visible = visible; + + inline function activeTransform(sprite:FlxSprite, active:Bool):Void + sprite.active = active; + + inline function solidTransform(sprite:FlxSprite, solid:Bool):Void + sprite.solid = solid; + + inline function aliveTransform(sprite:FlxSprite, alive:Bool):Void + sprite.alive = alive; + + inline function existsTransform(sprite:FlxSprite, exists:Bool):Void + sprite.exists = exists; + + inline function cameraTransform(sprite:FlxSprite, camera:FlxCamera):Void + sprite.camera = camera; + + inline function camerasTransform(sprite:FlxSprite, cameras:Array):Void + sprite.cameras = cameras; + + inline function offsetTransform(sprite:FlxSprite, offset:FlxPoint):Void + sprite.offset.copyFrom(offset); + + inline function originTransform(sprite:FlxSprite, origin:FlxPoint):Void + sprite.origin.copyFrom(origin); + + inline function scaleTransform(sprite:FlxSprite, scale:FlxPoint):Void + sprite.scale.copyFrom(scale); + + inline function scrollFactorTransform(sprite:FlxSprite, scrollFactor:FlxPoint):Void + sprite.scrollFactor.copyFrom(scrollFactor); + + inline function clipRectTransform(sprite:FlxSprite, clipRect:FlxRect):Void + { + if (clipRect == null) + { + sprite.clipRect = null; + } + else + { + sprite.clipRect = FlxRect.get(clipRect.x - sprite.x + x, clipRect.y - sprite.y + y, clipRect.width, clipRect.height); + } + } + + inline function offsetCallback(offset:FlxPoint):Void + transformChildren(offsetTransform, offset); + + inline function originCallback(origin:FlxPoint):Void + transformChildren(originTransform, origin); + + inline function scaleCallback(scale:FlxPoint):Void + transformChildren(scaleTransform, scale); + + inline function scrollFactorCallback(scrollFactor:FlxPoint):Void + transformChildren(scrollFactorTransform, scrollFactor); + + override function set_camera(value:FlxCamera):FlxCamera + { + if (camera != value) transformChildren(cameraTransform, value); + return super.set_camera(value); + } + + override function set_cameras(value:Array):Array + { + if (cameras != value) transformChildren(camerasTransform, value); + return super.set_cameras(value); + } + + override function set_exists(value:Bool):Bool + { + if (exists != value) transformChildren(existsTransform, value); + return super.set_exists(value); + } + + override function set_visible(value:Bool):Bool + { + if (exists && visible != value) transformChildren(visibleTransform, value); + return super.set_visible(value); + } + + override function set_active(value:Bool):Bool + { + if (exists && active != value) transformChildren(activeTransform, value); + return super.set_active(value); + } + + override function set_alive(value:Bool):Bool + { + if (alive != value) transformChildren(aliveTransform, value); + return super.set_alive(value); + } + + override function set_x(value:Float):Float + { + if (!exists || x == value) return x; // early return (no need to transform) + + transformChildren(xTransform, value - x); // offset + x = value; + return x; + } + + override function set_y(value:Float):Float + { + if (exists && y != value) transformChildren(yTransform, value - y); // offset + y = value; + return y; + } + + override function set_angle(value:Float):Float + { + if (exists && angle != value) transformChildren(angleTransform, value - angle); // offset + angle = value; + return angle; + } + + override function set_alpha(value:Float):Float + { + value = FlxMath.bound(value, 0, 1); + + if (exists && alpha != value) + { + transformChildren(directAlphaTransform, value); + } + alpha = value; + return alpha; + } + + override function set_facing(value:Int):Int + { + if (exists && facing != value) transformChildren(facingTransform, value); + facing = value; + return facing; + } + + override function set_flipX(value:Bool):Bool + { + if (exists && flipX != value) transformChildren(flipXTransform, value); + flipX = value; + return flipX; + } + + override function set_flipY(value:Bool):Bool + { + if (exists && flipY != value) transformChildren(flipYTransform, value); + flipY = value; + return flipY; + } + + override function set_moves(value:Bool):Bool + { + if (exists && moves != value) transformChildren(movesTransform, value); + moves = value; + return moves; + } + + override function set_immovable(value:Bool):Bool + { + if (exists && immovable != value) transformChildren(immovableTransform, value); + immovable = value; + return immovable; + } + + override function set_solid(value:Bool):Bool + { + if (exists && solid != value) transformChildren(solidTransform, value); + return super.set_solid(value); + } + + override function set_color(value:Int):Int + { + if (exists && color != value) transformChildren(gColorTransform, value); + color = value; + return color; + } + + override function set_blend(value:BlendMode):BlendMode + { + if (exists && blend != value) transformChildren(blendTransform, value); + blend = value; + return blend; + } + + override function set_clipRect(rect:FlxRect):FlxRect + { + if (exists) transformChildren(clipRectTransform, rect); + return super.set_clipRect(rect); + } + + override function set_pixelPerfectRender(value:Bool):Bool + { + if (exists && pixelPerfectRender != value) transformChildren(pixelPerfectTransform, value); + return super.set_pixelPerfectRender(value); + } + + override function set_width(value:Float):Float + { + return value; + } + + override function get_width():Float + { + if (this.mainSprite == null) return 0; + + return this.mainSprite.width; + } + + /** + * Returns the left-most position of the left-most member. + * If there are no members, x is returned. + * + * @since 5.0.0 + * @return the left-most position of the left-most member + */ + public function findMinX():Float + { + return this.mainSprite == null ? x : findMinXHelper(); + } + + function findMinXHelper():Float + { + return this.mainSprite.x; + } + + /** + * Returns the right-most position of the right-most member. + * If there are no members, x is returned. + * + * @since 5.0.0 + * @return the right-most position of the right-most member + */ + public function findMaxX():Float + { + return this.mainSprite == null ? x : findMaxXHelper(); + } + + function findMaxXHelper():Float + { + return this.mainSprite.x + this.mainSprite.width; + } + + /** + * This functionality isn't supported in SpriteGroup + */ + override function set_height(value:Float):Float + { + return value; + } + + override function get_height():Float + { + if (this.mainSprite == null) return 0; + + return this.mainSprite.height; + } + + /** + * Returns the top-most position of the top-most member. + * If there are no members, y is returned. + * + * @since 5.0.0 + * @return the top-most position of the top-most member + */ + public function findMinY():Float + { + return this.mainSprite == null ? y : findMinYHelper(); + } + + function findMinYHelper():Float + { + return this.mainSprite.y; + } + + /** + * Returns the top-most position of the top-most member. + * If there are no members, y is returned. + * + * @since 5.0.0 + * @return the bottom-most position of the bottom-most member + */ + public function findMaxY():Float + { + return this.mainSprite == null ? y : findMaxYHelper(); + } + + function findMaxYHelper():Float + { + return this.mainSprite.y + this.mainSprite.height; + } + + /** + * This functionality isn't supported in SpriteGroup + * @return this sprite group + */ + public override function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite + { + #if FLX_DEBUG + throw "This function is not supported in FlxSpriteGroup"; + #end + return this; + } + + /** + * This functionality isn't supported in SpriteGroup + * @return this sprite group + */ + public override function loadGraphic(Graphic:FlxGraphicAsset, Animated:Bool = false, Width:Int = 0, Height:Int = 0, Unique:Bool = false, + ?Key:String):FlxSprite + { + return this; + } + + /** + * This functionality isn't supported in SpriteGroup + * @return this sprite group + */ + public override function loadRotatedGraphic(Graphic:FlxGraphicAsset, Rotations:Int = 16, Frame:Int = -1, AntiAliasing:Bool = false, AutoBuffer:Bool = false, + ?Key:String):FlxSprite + { + #if FLX_DEBUG + throw "This function is not supported in FlxSpriteGroup"; + #end + return this; + } + + /** + * This functionality isn't supported in SpriteGroup + * @return this sprite group + */ + public override function makeGraphic(Width:Int, Height:Int, Color:Int = FlxColor.WHITE, Unique:Bool = false, ?Key:String):FlxSprite + { + #if FLX_DEBUG + throw "This function is not supported in FlxSpriteGroup"; + #end + return this; + } + + override function set_pixels(value:BitmapData):BitmapData + { + return value; + } + + override function set_frame(value:FlxFrame):FlxFrame + { + return value; + } + + override function get_pixels():BitmapData + { + return null; + } + + /** + * Internal function to update the current animation frame. + * + * @param RunOnCpp Whether the frame should also be recalculated if we're on a non-flash target + */ + override inline function calcFrame(RunOnCpp:Bool = false):Void + { + // Nothing to do here + } + + /** + * This functionality isn't supported in SpriteGroup + */ + override inline function resetHelpers():Void {} + + /** + * This functionality isn't supported in SpriteGroup + */ + public override inline function stamp(Brush:FlxSprite, X:Int = 0, Y:Int = 0):Void {} + + override function set_frames(Frames:FlxFramesCollection):FlxFramesCollection + { + return Frames; + } + + /** + * This functionality isn't supported in SpriteGroup + */ + override inline function updateColorTransform():Void {} +} diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index bd12b5f2e..01156dfab 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -4,6 +4,7 @@ import flixel.math.FlxPoint; import funkin.modding.events.ScriptEvent; import funkin.noteStuff.NoteBasic.NoteDir; import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.play.character.CharacterData.CharacterRenderType; import funkin.play.stage.Bopper; /** @@ -62,13 +63,27 @@ class BaseCharacter extends Bopper * The absolute position of the top-left of the character. * @return */ - public var cornerPosition(get, null):FlxPoint; + public var cornerPosition(get, set):FlxPoint; function get_cornerPosition():FlxPoint { return new FlxPoint(x, y); } + function set_cornerPosition(value:FlxPoint):FlxPoint + { + var xDiff:Float = value.x - this.x; + var yDiff:Float = value.y - this.y; + + this.cameraFocusPoint.x += xDiff; + this.cameraFocusPoint.y += yDiff; + + super.set_x(value.x); + super.set_y(value.y); + + return value; + } + /** * The absolute position of the character's feet, at the bottom-center of the sprite. */ @@ -131,7 +146,7 @@ class BaseCharacter extends Bopper return super.set_y(value); } - public function new(id:String) + public function new(id:String, renderType:CharacterRenderType) { super(); this.characterId = id; @@ -141,6 +156,10 @@ class BaseCharacter extends Bopper { throw 'Could not find character data for characterId: $characterId'; } + else if (_data.renderType != renderType) + { + throw 'Render type mismatch for character ($characterId): expected ${renderType}, got ${_data.renderType}'; + } else { this.characterName = _data.name; @@ -235,6 +254,8 @@ class BaseCharacter extends Bopper override function onCreate(event:ScriptEvent):Void { + super.onCreate(event); + // Make sure we are playing the idle animation... this.dance(); // ...then update the hitbox so that this.width and this.height are correct. @@ -307,14 +328,16 @@ class BaseCharacter extends Bopper return; } - if (hasAnimation('idle-hold') && getCurrentAnimation() == "idle" && isAnimationFinished()) playAnimation('idle-hold'); - if (hasAnimation('singLEFT-hold') && getCurrentAnimation() == "singLEFT" && isAnimationFinished()) playAnimation('singLEFT-hold'); - if (hasAnimation('singDOWN-hold') && getCurrentAnimation() == "singDOWN" && isAnimationFinished()) playAnimation('singDOWN-hold'); - if (hasAnimation('singUP-hold') && getCurrentAnimation() == "singUP" && isAnimationFinished()) playAnimation('singUP-hold'); - if (hasAnimation('singRIGHT-hold') && getCurrentAnimation() == "singRIGHT" && isAnimationFinished()) playAnimation('singRIGHT-hold'); + // This logic turns the idle animation into a "lead-in" animation. + if (hasAnimation('idle-hold') && getCurrentAnimation() == 'idle' && isAnimationFinished()) playAnimation('idle-hold'); + + if (hasAnimation('singLEFT-hold') && getCurrentAnimation() == 'singLEFT' && isAnimationFinished()) playAnimation('singLEFT-hold'); + if (hasAnimation('singDOWN-hold') && getCurrentAnimation() == 'singDOWN' && isAnimationFinished()) playAnimation('singDOWN-hold'); + if (hasAnimation('singUP-hold') && getCurrentAnimation() == 'singUP' && isAnimationFinished()) playAnimation('singUP-hold'); + if (hasAnimation('singRIGHT-hold') && getCurrentAnimation() == 'singRIGHT' && isAnimationFinished()) playAnimation('singRIGHT-hold'); // Handle character note hold time. - if (getCurrentAnimation().startsWith("sing")) + if (getCurrentAnimation().startsWith('sing')) { // TODO: Rework this code (and all character animations ugh) // such that the hold time is handled by padding frames, @@ -324,7 +347,7 @@ class BaseCharacter extends Bopper holdTimer += event.elapsed; var singTimeMs:Float = singTimeCrochet * (Conductor.crochet * 0.001); // x beats, to ms. - if (getCurrentAnimation().endsWith("miss")) singTimeMs *= 2; // makes it feel more awkward when you miss + if (getCurrentAnimation().endsWith('miss')) singTimeMs *= 2; // makes it feel more awkward when you miss // Without this check here, the player character would only play the `sing` animation // for one beat, as opposed to holding it as long as the player is holding the button. @@ -349,39 +372,31 @@ class BaseCharacter extends Bopper /** * Since no `onBeatHit` or `dance` calls happen in GameOverSubstate, * this regularly gets called instead. + * + * @param force Force the deathLoop animation to play, even if `firstDeath` is still playing. */ public function playDeathAnimation(force:Bool = false):Void { - if (force || (getCurrentAnimation().startsWith("firstDeath") && isAnimationFinished())) + if (force || (getCurrentAnimation().startsWith('firstDeath') && isAnimationFinished())) { - playAnimation("deathLoop" + GameOverSubstate.animationSuffix); + playAnimation('deathLoop' + GameOverSubstate.animationSuffix); } } - override function dance(force:Bool = false) + override function dance(force:Bool = false):Void { // Prevent default dancing behavior. - if (debugMode) return; - - if (isDead) return; + if (debugMode || isDead) return; if (!force) { - if (getCurrentAnimation().startsWith("sing")) - { - return; - } - if (["hey", "cheer"].contains(getCurrentAnimation()) && !isAnimationFinished()) - { - return; - } + if (getCurrentAnimation().startsWith('sing')) return; + + if (['hey', 'cheer'].contains(getCurrentAnimation()) && !isAnimationFinished()) return; } // Prevent dancing while another animation is playing. - if (!force && getCurrentAnimation().startsWith("sing")) - { - return; - } + if (!force && getCurrentAnimation().startsWith('sing')) return; // Otherwise, fallback to the super dance() method, which handles playing the idle animation. super.dance(); @@ -538,15 +553,18 @@ class BaseCharacter extends Bopper * @param miss If true, play the miss animation instead of the sing animation. * @param suffix A suffix to append to the animation name, like `alt`. */ - public function playSingAnimation(dir:NoteDir, miss:Bool = false, suffix:String = ""):Void + public function playSingAnimation(dir:NoteDir, ?miss:Bool = false, ?suffix:String = ''):Void { - var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != "" ? '-${suffix}' : ''}'; + var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != '' ? '-${suffix}' : ''}'; // restart even if already playing, because the character might sing the same note twice. playAnimation(anim, true); } } +/** + * The type of a given character sprite. Defines its default behaviors. + */ enum CharacterType { /** @@ -563,7 +581,8 @@ enum CharacterType * - At idle, dances with `danceLeft` and `danceRight` if available, or `idle` if not. * - When the CPU hits a note, plays the appropriate `singDIR` animation until DAD is done singing. * - If there is a `singDIR-end` animation, the `singDIR` animation will play once before looping the `singDIR-end` animation until DAD is done singing. - * - When the CPU misses a note (NOTE: This only happens via script, not by default), plays the appropriate `singDIR-miss` animation until DAD is done singing. + * - When the CPU misses a note (NOTE: This only happens via script, not by default), + * plays the appropriate `singDIR-miss` animation until DAD is done singing. */ DAD; diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index 87cbd078b..7b0ac8981 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -1,18 +1,14 @@ package funkin.play.character; -import flixel.util.typeLimit.OneOfTwo; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; -import funkin.play.character.BaseCharacter; -import funkin.play.character.MultiSparrowCharacter; -import funkin.play.character.PackerCharacter; +import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter; import funkin.play.character.ScriptedCharacter.ScriptedBaseCharacter; import funkin.play.character.ScriptedCharacter.ScriptedMultiSparrowCharacter; import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter; import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter; -import funkin.play.character.SparrowCharacter; -import funkin.util.VersionUtil; import funkin.util.assets.DataAssets; +import funkin.util.VersionUtil; import haxe.Json; import openfl.utils.Assets; @@ -23,12 +19,12 @@ class CharacterDataParser * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ - public static final CHARACTER_DATA_VERSION:String = "1.0.0"; + public static final CHARACTER_DATA_VERSION:String = '1.0.0'; /** * The current version rule check for the stage data format. */ - public static final CHARACTER_DATA_VERSION_RULE:String = "1.0.x"; + public static final CHARACTER_DATA_VERSION_RULE:String = '1.0.x'; static final characterCache:Map = new Map(); static final characterScriptedClass:Map = new Map(); @@ -44,14 +40,13 @@ class CharacterDataParser { // Clear any stages that are cached if there were any. clearCharacterCache(); - trace("Loading character cache..."); + trace('Loading character cache...'); // // UNSCRIPTED CHARACTERS // var charIdList:Array = DataAssets.listDataFilesInPath('characters/'); - var unscriptedCharIds:Array = charIdList.filter(function(charId:String):Bool - { + var unscriptedCharIds:Array = charIdList.filter(function(charId:String):Bool { return !characterCache.exists(charId); }); trace(' Fetching data for ${unscriptedCharIds.length} characters...'); @@ -85,8 +80,16 @@ class CharacterDataParser trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...'); for (charCls in scriptedCharClassNames1) { - var character = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID); - characterScriptedClass.set(character.characterId, charCls); + try + { + var character:SparrowCharacter = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID); + characterScriptedClass.set(character.characterId, charCls); + } + catch (e) + { + trace(' FAILED to instantiate scripted Sparrow character: ${charCls}'); + trace(e); + } } } @@ -96,8 +99,16 @@ class CharacterDataParser trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...'); for (charCls in scriptedCharClassNames2) { - var character = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID); - characterScriptedClass.set(character.characterId, charCls); + try + { + var character:PackerCharacter = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID); + characterScriptedClass.set(character.characterId, charCls); + } + catch (e) + { + trace(' FAILED to instantiate scripted Packer character: ${charCls}'); + trace(e); + } } } @@ -107,24 +118,46 @@ class CharacterDataParser trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...'); for (charCls in scriptedCharClassNames3) { - var character = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID); - if (character == null) + try { - trace(' Failed to instantiate scripted character: ${charCls}'); - continue; + var character:MultiSparrowCharacter = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID); + characterScriptedClass.set(character.characterId, charCls); + } + catch (e) + { + trace(' FAILED to instantiate scripted Multi-Sparrow character: ${charCls}'); + trace(e); + } + } + } + + var scriptedCharClassNames4:Array = ScriptedAnimateAtlasCharacter.listScriptClasses(); + if (scriptedCharClassNames4.length > 0) + { + trace(' Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...'); + for (charCls in scriptedCharClassNames4) + { + try + { + var character:AnimateAtlasCharacter = ScriptedAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID); + characterScriptedClass.set(character.characterId, charCls); + } + catch (e) + { + trace(' FAILED to instantiate scripted Animate Atlas character: ${charCls}'); + trace(e); } - characterScriptedClass.set(character.characterId, charCls); } } // NOTE: Only instantiate the ones not populated above. // ScriptedBaseCharacter.listScriptClasses() will pick up scripts extending the other classes. var scriptedCharClassNames:Array = ScriptedBaseCharacter.listScriptClasses(); - scriptedCharClassNames = scriptedCharClassNames.filter(function(charCls:String):Bool - { + scriptedCharClassNames = scriptedCharClassNames.filter(function(charCls:String):Bool { return !(scriptedCharClassNames1.contains(charCls) || scriptedCharClassNames2.contains(charCls) - || scriptedCharClassNames3.contains(charCls)); + || scriptedCharClassNames3.contains(charCls) + || scriptedCharClassNames4.contains(charCls)); }); if (scriptedCharClassNames.length > 0) @@ -132,7 +165,7 @@ class CharacterDataParser trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...'); for (charCls in scriptedCharClassNames) { - var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID); + var character:BaseCharacter = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID, Custom); if (character == null) { trace(' Failed to instantiate scripted character: ${charCls}'); @@ -149,83 +182,101 @@ class CharacterDataParser trace(' Successfully loaded ${Lambda.count(characterCache)} stages.'); } + /** + * Fetches data for a character and returns a BaseCharacter instance, + * ready to be added to the scene. + * @param charId The character ID to fetch. + * @return The character instance, or null if the character was not found. + */ public static function fetchCharacter(charId:String):Null { - if (charId == null || charId == '') + if (charId == null || charId == '' || !characterCache.exists(charId)) { - // Gracefully handle songs that don't use this character. + // Gracefully handle songs that don't use this character, + // or throw an error if the character is missing. + + if (charId != null && charId != '') trace('Failed to build character, not found in cache: ${charId}'); return null; } - if (characterCache.exists(charId)) + var charData:CharacterData = characterCache.get(charId); + var charScriptClass:String = characterScriptedClass.get(charId); + + var char:BaseCharacter; + + if (charScriptClass != null) { - var charData:CharacterData = characterCache.get(charId); - var charScriptClass:String = characterScriptedClass.get(charId); - - var char:BaseCharacter; - - if (charScriptClass != null) + switch (charData.renderType) { - switch (charData.renderType) - { - case CharacterRenderType.MULTISPARROW: - char = ScriptedMultiSparrowCharacter.init(charScriptClass, charId); - case CharacterRenderType.SPARROW: - char = ScriptedSparrowCharacter.init(charScriptClass, charId); - case CharacterRenderType.PACKER: - char = ScriptedPackerCharacter.init(charScriptClass, charId); - default: - // We're going to assume that the script class does the rendering. - char = ScriptedBaseCharacter.init(charScriptClass, charId); - } + case CharacterRenderType.AnimateAtlas: + char = ScriptedAnimateAtlasCharacter.init(charScriptClass, charId); + case CharacterRenderType.MultiSparrow: + char = ScriptedMultiSparrowCharacter.init(charScriptClass, charId); + case CharacterRenderType.Sparrow: + char = ScriptedSparrowCharacter.init(charScriptClass, charId); + case CharacterRenderType.Packer: + char = ScriptedPackerCharacter.init(charScriptClass, charId); + default: + // We're going to assume that the script class does the rendering. + char = ScriptedBaseCharacter.init(charScriptClass, charId, CharacterRenderType.Custom); } - else - { - switch (charData.renderType) - { - case CharacterRenderType.MULTISPARROW: - char = new MultiSparrowCharacter(charId); - case CharacterRenderType.SPARROW: - char = new SparrowCharacter(charId); - case CharacterRenderType.PACKER: - char = new PackerCharacter(charId); - default: - trace('[WARN] Creating character with undefined renderType ${charData.renderType}'); - char = new BaseCharacter(charId); - } - } - - trace('Successfully instantiated character: ${charId}'); - - // Call onCreate only in the fetchCharacter() function, not at application initialization. - ScriptEventDispatcher.callEvent(char, new ScriptEvent(ScriptEvent.CREATE)); - - return char; } else { - trace('Failed to build character, not found in cache: ${charId}'); + switch (charData.renderType) + { + case CharacterRenderType.AnimateAtlas: + char = new AnimateAtlasCharacter(charId); + case CharacterRenderType.MultiSparrow: + char = new MultiSparrowCharacter(charId); + case CharacterRenderType.Sparrow: + char = new SparrowCharacter(charId); + case CharacterRenderType.Packer: + char = new PackerCharacter(charId); + default: + trace('[WARN] Creating character with undefined renderType ${charData.renderType}'); + char = new BaseCharacter(charId, CharacterRenderType.Custom); + } + } + + if (char == null) + { + trace('Failed to instantiate character: ${charId}'); return null; } + + trace('Successfully instantiated character: ${charId}'); + + // Call onCreate only in the fetchCharacter() function, not at application initialization. + ScriptEventDispatcher.callEvent(char, new ScriptEvent(ScriptEvent.CREATE)); + + return char; } + /** + * Fetches just the character data for a character. + * @param charId The character ID to fetch. + * @return The character data, or null if the character was not found. + */ public static function fetchCharacterData(charId:String):Null { - if (characterCache.exists(charId)) - { - return characterCache.get(charId); - } - else - { - return null; - } + if (characterCache.exists(charId)) return characterCache.get(charId); + + return null; } + /** + * Lists all the valid character IDs. + * @return An array of character IDs. + */ public static function listCharacterIds():Array { return characterCache.keys().array(); } + /** + * Clears the character data cache. + */ static function clearCharacterCache():Void { if (characterCache != null) @@ -239,7 +290,7 @@ class CharacterDataParser } /** - * Load a character's JSON file, parse its data, and return it. + * Load a character's JSON file and parse its data. * * @param charId The character to load. * @return The character data, or null if validation failed. @@ -258,7 +309,7 @@ class CharacterDataParser var charFilePath:String = Paths.json('characters/${charPath}'); var rawJson = Assets.getText(charFilePath).trim(); - while (!StringTools.endsWith(rawJson, "}")) + while (!StringTools.endsWith(rawJson, '}')) { rawJson = rawJson.substr(0, rawJson.length - 1); } @@ -266,7 +317,7 @@ class CharacterDataParser return rawJson; } - static function migrateCharacterData(rawJson:String, charId:String) + static function migrateCharacterData(rawJson:String, charId:String):Null { // If you update the character data format in a breaking way, // handle migration here by checking the `version` value. @@ -298,13 +349,13 @@ class CharacterDataParser static final DEFAULT_FRAMERATE:Int = 24; static final DEFAULT_ISPIXEL:Bool = false; static final DEFAULT_LOOP:Bool = false; - static final DEFAULT_NAME:String = "Untitled Character"; + static final DEFAULT_NAME:String = 'Untitled Character'; static final DEFAULT_OFFSETS:Array = [0, 0]; static final DEFAULT_HEALTHICON_OFFSETS:Array = [0, 25]; - static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.SPARROW; + static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.Sparrow; static final DEFAULT_SCALE:Float = 1; static final DEFAULT_SCROLL:Array = [0, 0]; - static final DEFAULT_STARTINGANIM:String = "idle"; + static final DEFAULT_STARTINGANIM:String = 'idle'; /** * Set unspecified parameters to their defaults. @@ -317,7 +368,7 @@ class CharacterDataParser { if (input == null) { - // trace('ERROR: Could not parse character data for "${id}".'); + trace('ERROR: Could not parse character data for "${id}".'); return null; } @@ -471,20 +522,40 @@ class CharacterDataParser } } +/** + * Describes the available rendering types for a character. + */ enum abstract CharacterRenderType(String) from String to String { - var SPARROW = 'sparrow'; - var PACKER = 'packer'; - var MULTISPARROW = 'multisparrow'; - // TODO: FlxSpine? - // https://api.haxeflixel.com/flixel/addons/editors/spine/FlxSpine.html - // TODO: Aseprite? - // https://lib.haxe.org/p/openfl-aseprite/ - // TODO: Animate? - // https://lib.haxe.org/p/flxanimate - // TODO: REDACTED + /** + * Renders the character using a single spritesheet and XML data. + */ + public var Sparrow = 'sparrow'; + + /** + * Renders the character using a single spritesheet and TXT data. + */ + public var Packer = 'packer'; + + /** + * Renders the character using multiple spritesheets and XML data. + */ + public var MultiSparrow = 'multisparrow'; + + /** + * Renders the character using a spritesheet of symbols and JSON data. + */ + public var AnimateAtlas = 'animateatlas'; + + /** + * Renders the character using a custom method. + */ + public var Custom = 'custom'; } +/** + * The JSON data schema used to define a character. + */ typedef CharacterData = { /** @@ -580,6 +651,9 @@ typedef CharacterData = var flipX:Null; }; +/** + * The JSON data schema used to define the health icon for a character. + */ typedef HealthIconData = { /** diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx index 98dda41d0..974a1c431 100644 --- a/source/funkin/play/character/MultiSparrowCharacter.hx +++ b/source/funkin/play/character/MultiSparrowCharacter.hx @@ -3,6 +3,7 @@ package funkin.play.character; import flixel.graphics.frames.FlxFramesCollection; import funkin.modding.events.ScriptEvent; import funkin.util.assets.FlxAnimationUtil; +import funkin.play.character.CharacterData.CharacterRenderType; /** * For some characters which use Sparrow atlases, the spritesheets need to be split @@ -37,7 +38,7 @@ class MultiSparrowCharacter extends BaseCharacter public function new(id:String) { - super(id); + super(id, CharacterRenderType.MultiSparrow); } override function onCreate(event:ScriptEvent):Void @@ -48,7 +49,7 @@ class MultiSparrowCharacter extends BaseCharacter super.onCreate(event); } - function buildSprites() + function buildSprites():Void { buildSpritesheets(); buildAnimations(); @@ -63,8 +64,11 @@ class MultiSparrowCharacter extends BaseCharacter } } - function buildSpritesheets() + function buildSpritesheets():Void { + // TODO: This currently works by creating like 5 frame collections and switching between them. + // It would be better to refactor this to simply concatenate the frame collections together. + // Build the list of asset paths to use. // Ignore nulls and duplicates. var assetList = [_data.assetPath]; diff --git a/source/funkin/play/character/PackerCharacter.hx b/source/funkin/play/character/PackerCharacter.hx index 00469964f..3ee276eb1 100644 --- a/source/funkin/play/character/PackerCharacter.hx +++ b/source/funkin/play/character/PackerCharacter.hx @@ -1,9 +1,9 @@ package funkin.play.character; -import funkin.modding.events.ScriptEvent; import flixel.graphics.frames.FlxFramesCollection; +import funkin.modding.events.ScriptEvent; +import funkin.play.character.CharacterData.CharacterRenderType; import funkin.util.assets.FlxAnimationUtil; -import funkin.play.character.BaseCharacter.CharacterType; /** * A PackerCharacter is a Character which is rendered by @@ -13,7 +13,7 @@ class PackerCharacter extends BaseCharacter { public function new(id:String) { - super(id); + super(id, CharacterRenderType.Packer); } override function onCreate(event:ScriptEvent):Void @@ -26,7 +26,7 @@ class PackerCharacter extends BaseCharacter super.onCreate(event); } - function loadSpritesheet() + function loadSpritesheet():Void { trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}'); @@ -51,7 +51,7 @@ class PackerCharacter extends BaseCharacter this.setScale(_data.scale); } - function loadAnimations() + function loadAnimations():Void { trace('[PACKERCHAR] Loading ${_data.animations.length} animations for ${characterId}'); diff --git a/source/funkin/play/character/ScriptedCharacter.hx b/source/funkin/play/character/ScriptedCharacter.hx index 2760cb42c..3d9ad7ed4 100644 --- a/source/funkin/play/character/ScriptedCharacter.hx +++ b/source/funkin/play/character/ScriptedCharacter.hx @@ -1,10 +1,5 @@ package funkin.play.character; -import funkin.play.character.MultiSparrowCharacter; -import funkin.play.character.PackerCharacter; -import funkin.play.character.SparrowCharacter; -import polymod.hscript.HScriptedClass; - /** * A script that can be tied to a BaseCharacter, which persists across states. * Create a scripted class that extends BaseCharacter to use this. @@ -13,7 +8,7 @@ import polymod.hscript.HScriptedClass; * and can't use one of the built-in render modes. */ @:hscriptClass -class ScriptedBaseCharacter extends BaseCharacter implements HScriptedClass {} +class ScriptedBaseCharacter extends BaseCharacter implements polymod.hscript.HScriptedClass {} /** * A script that can be tied to a SparrowCharacter, which persists across states. @@ -21,7 +16,7 @@ class ScriptedBaseCharacter extends BaseCharacter implements HScriptedClass {} * then call `super('charId')` in the constructor to use this. */ @:hscriptClass -class ScriptedSparrowCharacter extends SparrowCharacter implements HScriptedClass {} +class ScriptedSparrowCharacter extends SparrowCharacter implements polymod.hscript.HScriptedClass {} /** * A script that can be tied to a MultiSparrowCharacter, which persists across states. @@ -29,7 +24,7 @@ class ScriptedSparrowCharacter extends SparrowCharacter implements HScriptedClas * then call `super('charId')` in the constructor to use this. */ @:hscriptClass -class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements HScriptedClass {} +class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements polymod.hscript.HScriptedClass {} /** * A script that can be tied to a PackerCharacter, which persists across states. @@ -37,4 +32,12 @@ class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements HSc * then call `super('charId')` in the constructor to use this. */ @:hscriptClass -class ScriptedPackerCharacter extends PackerCharacter implements HScriptedClass {} +class ScriptedPackerCharacter extends PackerCharacter implements polymod.hscript.HScriptedClass {} + +/** + * A script that can be tied to an AnimateAtlasCharacter, which persists across states. + * Create a scripted class that extends AnimateAtlasCharacter, + * then call `super('charId')` in the constructor to use this. + */ +@:hscriptClass +class ScriptedAnimateAtlasCharacter extends AnimateAtlasCharacter implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/play/character/SparrowCharacter.hx b/source/funkin/play/character/SparrowCharacter.hx index e9e9cf423..1db14fe23 100644 --- a/source/funkin/play/character/SparrowCharacter.hx +++ b/source/funkin/play/character/SparrowCharacter.hx @@ -3,6 +3,7 @@ package funkin.play.character; import funkin.modding.events.ScriptEvent; import funkin.util.assets.FlxAnimationUtil; import flixel.graphics.frames.FlxFramesCollection; +import funkin.play.character.CharacterData.CharacterRenderType; /** * A SparrowCharacter is a Character which is rendered by @@ -15,7 +16,7 @@ class SparrowCharacter extends BaseCharacter { public function new(id:String) { - super(id); + super(id, CharacterRenderType.Sparrow); } override function onCreate(event:ScriptEvent):Void diff --git a/source/funkin/play/cutscene/VanillaCutscenes.hx b/source/funkin/play/cutscene/VanillaCutscenes.hx new file mode 100644 index 000000000..99d0bd0d8 --- /dev/null +++ b/source/funkin/play/cutscene/VanillaCutscenes.hx @@ -0,0 +1,125 @@ +package funkin.play.cutscene; + +import hxcodec.flixel.FlxVideoSprite; +import hxcodec.flixel.FlxCutsceneState; +import flixel.FlxSprite; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxColor; +import flixel.util.FlxTimer; + +/** + * Static methods for playing cutscenes in the PlayState. + * TODO: Un-hardcode this shit!!!!!1! + */ +class VanillaCutscenes +{ + /** + * Well, well, well, what have we got here? + */ + public static function playUghCutscene():Void + { + playVideoCutscene('music/ughCutscene.mp4'); + } + + /** + * Nice bars for an ugly, boring teenager! + */ + public static function playGunsCutscene():Void + { + playVideoCutscene('music/gunsCutscene.mp4'); + } + + /** + * Don't you have a school to shoot up? + */ + public static function playStressCutscene():Void + { + playVideoCutscene('music/stressCutscene.mp4'); + } + + static var blackScreen:FlxSprite; + + /** + * Plays a cutscene from a video file, then starts the countdown once the video is done. + * TODO: Cutscene is currently skipped on native platforms. + */ + static function playVideoCutscene(path:String):Void + { + // Tell PlayState to stop the song until the video is done. + PlayState.isInCutscene = true; + PlayState.instance.camHUD.visible = false; + + // Display a black screen to hide the game while the video is playing. + blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); + blackScreen.scrollFactor.set(0, 0); + blackScreen.cameras = [PlayState.instance.camCutscene]; + PlayState.instance.add(blackScreen); + + #if html5 + // Video displays OVER the FlxState. + vid = new FlxVideo(path); + vid.finishCallback = finishCutscene; + #else + // Video displays OVER the FlxState. + vid = new FlxVideoSprite(0, 0); + + vid.cameras = [PlayState.instance.camCutscene]; + + PlayState.instance.add(vid); + + vid.playVideo(Paths.file(path), false); + vid.onEndReached.add(finishCutscene.bind(0.5)); + #end + } + + static var vid:#if html5 FlxVideo #else FlxVideoSprite #end; + + /** + * Does the cleanup to start the countdown after the video is done. + * Gets called immediately if the video can't be played. + */ + public static function finishCutscene(?transitionTime:Float = 2.5):Void + { + trace('ALERT: Finish cutscene called!'); + + #if html5 + #else + vid.stop(); + PlayState.instance.remove(vid); + #end + + PlayState.instance.camHUD.visible = true; + + FlxTween.tween(blackScreen, {alpha: 0}, transitionTime, + { + ease: FlxEase.quadInOut, + onComplete: function(twn:FlxTween) { + PlayState.instance.remove(blackScreen); + blackScreen = null; + } + }); + FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, transitionTime, + { + ease: FlxEase.quadInOut, + onComplete: function(twn:FlxTween) { + PlayState.instance.startCountdown(); + } + }); + } + + /** + * FNF corruption mod??? + */ + public static function playHorrorStartCutscene():Void + { + PlayState.isInCutscene = true; + PlayState.instance.camHUD.visible = false; + + blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); + blackScreen.scrollFactor.set(0, 0); + PlayState.instance.add(blackScreen); + + new FlxTimer().start(0.1, _ -> finishCutscene(2.5)); + } +} diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index e674a1e24..29d2e80e0 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -47,17 +47,17 @@ class FocusCameraSongEvent extends SongEvent super('FocusCamera'); } - public override function handleEvent(data:SongEventData) + public override function handleEvent(data:SongEventData):Void { // Does nothing if there is no PlayState camera or stage. if (PlayState.instance == null || PlayState.instance.currentStage == null) return; - var posX = data.getFloat('x'); + var posX:Null = data.getFloat('x'); if (posX == null) posX = 0.0; - var posY = data.getFloat('y'); + var posY:Null = data.getFloat('y'); if (posY == null) posY = 0.0; - var char = data.getInt('char'); + var char:Null = data.getInt('char'); if (char == null) char = cast data.value; @@ -65,29 +65,45 @@ class FocusCameraSongEvent extends SongEvent { case -1: // Position trace('Focusing camera on static position.'); - var xTarget = posX; - var yTarget = posY; + var xTarget:Float = posX; + var yTarget:Float = posY; PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); case 0: // Boyfriend // Focus the camera on the player. + if (PlayState.instance.currentStage.getBoyfriend() == null) + { + trace('No BF to focus on.'); + return; + } trace('Focusing camera on player.'); - var xTarget = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX; - var yTarget = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY; + var xTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX; + var yTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY; PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); case 1: // Dad // Focus the camera on the dad. + if (PlayState.instance.currentStage.getDad() == null) + { + trace('No dad to focus on.'); + return; + } trace('Focusing camera on dad.'); - var xTarget = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX; - var yTarget = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY; + trace(PlayState.instance.currentStage.getDad()); + var xTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX; + var yTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY; PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); case 2: // Girlfriend // Focus the camera on the girlfriend. + if (PlayState.instance.currentStage.getGirlfriend() == null) + { + trace('No GF to focus on.'); + return; + } trace('Focusing camera on girlfriend.'); - var xTarget = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX; - var yTarget = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY; + var xTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX; + var yTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY; PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); default: @@ -97,7 +113,7 @@ class FocusCameraSongEvent extends SongEvent public override function getTitle():String { - return "Focus Camera"; + return 'Focus Camera'; } /** diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index 1e2ff28bb..75d002cb5 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -1,5 +1,8 @@ package funkin.play.scoring; +/** + * Which system to use when scoring and judging notes. + */ enum abstract ScoringSystem(String) { /** @@ -19,9 +22,6 @@ enum abstract ScoringSystem(String) * Scores the player based on the offset based on timing, represented by a sigmoid function. */ var PBOT1; - - // WIFE1 - // WIFE3 } /** @@ -35,161 +35,229 @@ class Scoring * @param scoringSystem The scoring system to use. * @return The score the note receives. */ - public static function scoreNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1) + public static function scoreNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1):Int { - switch (scoringSystem) + return switch (scoringSystem) { - case LEGACY: - return scoreNote_LEGACY(msTiming); - case WEEK7: - return scoreNote_WEEK7(msTiming); - case PBOT1: - return scoreNote_PBOT1(msTiming); + case LEGACY: scoreNoteLEGACY(msTiming); + case WEEK7: scoreNoteWEEK7(msTiming); + case PBOT1: scoreNotePBOT1(msTiming); default: trace('ERROR: Unknown scoring system: ' + scoringSystem); - return 0; + 0; } } /** * Determine the judgement a note receives under a given scoring system. * @param msTiming The difference between the note's time and when it was hit. + * @param scoringSystem The scoring system to use. * @return The judgement the note receives. */ public static function judgeNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1):String { - switch (scoringSystem) + return switch (scoringSystem) { - case LEGACY: - return judgeNote_LEGACY(msTiming); - case WEEK7: - return judgeNote_WEEK7(msTiming); - case PBOT1: - return judgeNote_PBOT1(msTiming); + case LEGACY: judgeNoteLEGACY(msTiming); + case WEEK7: judgeNoteWEEK7(msTiming); + case PBOT1: judgeNotePBOT1(msTiming); default: trace('ERROR: Unknown scoring system: ' + scoringSystem); - return 'miss'; + 'miss'; } } /** - * The maximum score received. + * The maximum score a note can receive. */ - public static var PBOT1_MAX_SCORE = 350; + public static final PBOT1_MAX_SCORE:Int = 500; /** - * The minimum score received. + * The offset of the sigmoid curve for the scoring function. */ - public static var PBOT1_MIN_SCORE = 0; + public static final PBOT1_SCORING_OFFSET:Float = 54.99; + + /** + * The slope of the sigmoid curve for the scoring function. + */ + public static final PBOT1_SCORING_SLOPE:Float = 0.080; + + /** + * The minimum score a note can receive while still being considered a hit. + */ + public static final PBOT1_MIN_SCORE:Float = 9.0; + + /** + * The score a note receives when it is missed. + */ + public static final PBOT1_MISS_SCORE:Int = 0; /** * The threshold at which a note hit is considered perfect and always given the max score. - **/ - public static var PBOT1_PERFECT_THRESHOLD = 5.0; // 5ms. + */ + public static final PBOT1_PERFECT_THRESHOLD:Float = 5.0; // 5ms /** - * The threshold at which a note hit is considered missed and always given the min score. - **/ - public static var PBOT1_MISS_THRESHOLD = (10 / 60) * 1000; // ~166ms + * The threshold at which a note hit is considered missed. + * `160ms` + */ + public static final PBOT1_MISS_THRESHOLD:Float = 160.0; - // Magic numbers used to tweak the shape of the scoring function. - public static var PBOT1_SCORING_SLOPE:Float = 0.052; - public static var PBOT1_SCORING_OFFSET:Float = 80.0; + /** + * The time within which a note is considered to have been hit with the Killer judgement. + * `~7.5% of the hit window, or 12.5ms` + */ + public static final PBOT1_KILLER_THRESHOLD:Float = 12.5; - static function scoreNote_PBOT1(msTiming:Float):Int + /** + * The time within which a note is considered to have been hit with the Sick judgement. + * `~25% of the hit window, or 45ms` + */ + public static final PBOT1_SICK_THRESHOLD:Float = 45.0; + + /** + * The time within which a note is considered to have been hit with the Good judgement. + * `~55% of the hit window, or 90ms` + */ + public static final PBOT1_GOOD_THRESHOLD:Float = 90.0; + + /** + * The time within which a note is considered to have been hit with the Bad judgement. + * `~85% of the hit window, or 135ms` + */ + public static final PBOT1_BAD_THRESHOLD:Float = 135.0; + + /** + * The time within which a note is considered to have been hit with the Shit judgement. + * `100% of the hit window, or 160ms` + */ + public static final PBOT1_SHIT_THRESHOLD:Float = 160.0; + + static function scoreNotePBOT1(msTiming:Float):Int { // Absolute value because otherwise late hits are always given the max score. - var absTiming = Math.abs(msTiming); - if (absTiming > PBOT1_MISS_THRESHOLD) - { - return PBOT1_MIN_SCORE; - } - else if (absTiming < PBOT1_PERFECT_THRESHOLD) - { - return PBOT1_MAX_SCORE; - } - else - { - // Calculate the score based on the timing using a sigmoid function. - var factor:Float = 1.0 - (1.0 / (1.0 + Math.exp(-PBOT1_SCORING_SLOPE * (absTiming - PBOT1_SCORING_OFFSET)))); + var absTiming:Float = Math.abs(msTiming); - var score = Std.int(PBOT1_MAX_SCORE * factor); + return switch (absTiming) + { + case(_ > PBOT1_MISS_THRESHOLD) => true: + PBOT1_MISS_SCORE; + case(_ < PBOT1_PERFECT_THRESHOLD) => true: + PBOT1_MAX_SCORE; + default: + var factor:Float = 1.0 - (1.0 / (1.0 + Math.exp(-PBOT1_SCORING_SLOPE * (absTiming - PBOT1_SCORING_OFFSET)))); + var score:Int = Std.int(PBOT1_MAX_SCORE * factor + PBOT1_MIN_SCORE); - return score; + score; } } - static function judgeNote_PBOT1(msTiming:Float):String + static function judgeNotePBOT1(msTiming:Float):String { - return judgeNote_WEEK7(msTiming); + var absTiming:Float = Math.abs(msTiming); + + return switch (absTiming) + { + case(_ < PBOT1_KILLER_THRESHOLD) => true: + 'killer'; + case(_ < PBOT1_SICK_THRESHOLD) => true: + 'sick'; + case(_ < PBOT1_GOOD_THRESHOLD) => true: + 'good'; + case(_ < PBOT1_BAD_THRESHOLD) => true: + 'bad'; + case(_ < PBOT1_SHIT_THRESHOLD) => true: + 'shit'; + default: + 'miss'; + } } /** * The window of time in which a note is considered to be hit, on the Funkin Legacy scoring system. * Currently equal to 10 frames at 60fps, or ~166ms. */ - public static var LEGACY_HIT_WINDOW:Float = (10 / 60) * 1000; // 166.67 ms hit window (10 frames at 60fps) + public static final LEGACY_HIT_WINDOW:Float = (10 / 60) * 1000; // 166.67 ms hit window (10 frames at 60fps) /** - * The threshold at which a note is considered a "Bad" hit rather than a "Shit" hit. + * The threshold at which a note is considered a "Sick" hit rather than another judgement. * Represented as a percentage of the total hit window. */ - public static var LEGACY_BAD_THRESHOLD:Float = 0.9; + public static final LEGACY_SICK_THRESHOLD:Float = 0.2; - public static var LEGACY_GOOD_THRESHOLD:Float = 0.75; - public static var LEGACY_SICK_THRESHOLD:Float = 0.2; - public static var LEGACY_SHIT_SCORE = 50; - public static var LEGACY_BAD_SCORE = 100; - public static var LEGACY_GOOD_SCORE = 200; - public static var LEGACY_SICK_SCORE = 350; + /** + * The threshold at which a note is considered a "Good" hit rather than another judgement. + * Represented as a percentage of the total hit window. + */ + public static final LEGACY_GOOD_THRESHOLD:Float = 0.75; - static function scoreNote_LEGACY(msTiming:Float):Int + /** + * The threshold at which a note is considered a "Bad" hit rather than another judgement. + * Represented as a percentage of the total hit window. + */ + public static final LEGACY_BAD_THRESHOLD:Float = 0.9; + + /** + * The score a note receives when hit within the Shit threshold, rather than a miss. + * Represented as a percentage of the total hit window. + */ + public static final LEGACY_SHIT_THRESHOLD:Float = 1.0; + + /** + * The score a note receives when hit within the Sick threshold. + */ + public static final LEGACY_SICK_SCORE:Int = 350; + + /** + * The score a note receives when hit within the Good threshold. + */ + public static final LEGACY_GOOD_SCORE:Int = 200; + + /** + * The score a note receives when hit within the Bad threshold. + */ + public static final LEGACY_BAD_SCORE:Int = 100; + + /** + * The score a note receives when hit within the Shit threshold. + */ + public static final LEGACY_SHIT_SCORE:Int = 50; + + static function scoreNoteLEGACY(msTiming:Float):Int { - var absTiming = Math.abs(msTiming); - if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) + var absTiming:Float = Math.abs(msTiming); + + return switch (absTiming) { - return LEGACY_SICK_SCORE; - } - else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) - { - return LEGACY_GOOD_SCORE; - } - else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) - { - return LEGACY_BAD_SCORE; - } - else if (absTiming < LEGACY_HIT_WINDOW) - { - return LEGACY_SHIT_SCORE; - } - else - { - return 0; + case(_ < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) => true: + LEGACY_SICK_SCORE; + case(_ < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) => true: + LEGACY_GOOD_SCORE; + case(_ < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) => true: + LEGACY_BAD_SCORE; + case(_ < LEGACY_HIT_WINDOW * LEGACY_SHIT_THRESHOLD) => true: + LEGACY_SHIT_SCORE; + default: + 0; } } - static function judgeNote_LEGACY(msTiming:Float):String + static function judgeNoteLEGACY(msTiming:Float):String { - var absTiming = Math.abs(msTiming); - if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) + var absTiming:Float = Math.abs(msTiming); + + return switch (absTiming) { - return 'sick'; - } - else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) - { - return 'good'; - } - else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) - { - return 'bad'; - } - else if (absTiming < LEGACY_HIT_WINDOW) - { - return 'shit'; - } - else - { - return 'miss'; + case(_ < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) => true: + 'sick'; + case(_ < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) => true: + 'good'; + case(_ < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) => true: + 'bad'; + case(_ < LEGACY_HIT_WINDOW * LEGACY_SHIT_THRESHOLD) => true: + 'shit'; + default: + 'miss'; } } @@ -197,19 +265,34 @@ class Scoring * The window of time in which a note is considered to be hit, on the Funkin Classic scoring system. * Same as L 10 frames at 60fps, or ~166ms. */ - public static var WEEK7_HIT_WINDOW = LEGACY_HIT_WINDOW; + public static final WEEK7_HIT_WINDOW:Float = LEGACY_HIT_WINDOW; - public static var WEEK7_BAD_THRESHOLD = 0.8; // 80% of the hit window, or ~125ms - public static var WEEK7_GOOD_THRESHOLD = 0.55; // 55% of the hit window, or ~91ms - public static var WEEK7_SICK_THRESHOLD = 0.2; // 20% of the hit window, or ~33ms - public static var WEEK7_SHIT_SCORE = 50; - public static var WEEK7_BAD_SCORE = 100; - public static var WEEK7_GOOD_SCORE = 200; - public static var WEEK7_SICK_SCORE = 350; + public static final WEEK7_BAD_THRESHOLD:Float = 0.8; // 80% of the hit window, or ~125ms + public static final WEEK7_GOOD_THRESHOLD:Float = 0.55; // 55% of the hit window, or ~91ms + public static final WEEK7_SICK_THRESHOLD:Float = 0.2; // 20% of the hit window, or ~33ms + public static final WEEK7_SHIT_SCORE:Int = 50; + public static final WEEK7_BAD_SCORE:Int = 100; + public static final WEEK7_GOOD_SCORE:Int = 200; + public static final WEEK7_SICK_SCORE:Int = 350; - static function scoreNote_WEEK7(msTiming:Float):Int + static function scoreNoteWEEK7(msTiming:Float):Int { - var absTiming = Math.abs(msTiming); + var absTiming:Float = Math.abs(msTiming); + + return switch (absTiming) + { + case(_ < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD) => true: + LEGACY_SICK_SCORE; + case(_ < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD) => true: + LEGACY_GOOD_SCORE; + case(_ < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD) => true: + LEGACY_BAD_SCORE; + case(_ < WEEK7_HIT_WINDOW) => true: + LEGACY_SHIT_SCORE; + default: + 0; + } + if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD) { return WEEK7_SICK_SCORE; @@ -232,7 +315,7 @@ class Scoring } } - static function judgeNote_WEEK7(msTiming:Float):String + static function judgeNoteWEEK7(msTiming:Float):String { var absTiming = Math.abs(msTiming); if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD) diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index 3666885bd..60ae32ec1 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -18,9 +18,9 @@ class SongDataParser */ static final songCache:Map = new Map(); - static final DEFAULT_SONG_ID = 'UNKNOWN'; - static final SONG_DATA_PATH = 'songs/'; - static final SONG_DATA_SUFFIX = '-metadata.json'; + static final DEFAULT_SONG_ID:String = 'UNKNOWN'; + static final SONG_DATA_PATH:String = 'songs/'; + static final SONG_DATA_SUFFIX:String = '-metadata.json'; /** * Parses and preloads the game's song metadata and scripts when the game starts. @@ -30,7 +30,7 @@ class SongDataParser public static function loadSongCache():Void { clearSongCache(); - trace("Loading song cache..."); + trace('Loading song cache...'); // // SCRIPTED SONGS @@ -54,12 +54,10 @@ class SongDataParser // // UNSCRIPTED SONGS // - var songIdList:Array = DataAssets.listDataFilesInPath(SONG_DATA_PATH, SONG_DATA_SUFFIX).map(function(songDataPath:String):String - { + var songIdList:Array = DataAssets.listDataFilesInPath(SONG_DATA_PATH, SONG_DATA_SUFFIX).map(function(songDataPath:String):String { return songDataPath.split('/')[0]; }); - var unscriptedSongIds:Array = songIdList.filter(function(songId:String):Bool - { + var unscriptedSongIds:Array = songIdList.filter(function(songId:String):Bool { return !songCache.exists(songId); }); trace(' Instantiating ${unscriptedSongIds.length} non-scripted songs...'); @@ -67,7 +65,7 @@ class SongDataParser { try { - var song = new Song(songId); + var song:Song = new Song(songId); if (song != null) { trace(' Loaded song data: ${song.songId}'); @@ -88,6 +86,8 @@ class SongDataParser /** * Retrieves a particular song from the cache. + * @param songId The ID of the song to retrieve. + * @return The song, or null if it was not found. */ public static function fetchSong(songId:String):Null { @@ -331,7 +331,7 @@ typedef RawSongNoteData = abstract SongNoteData(RawSongNoteData) { - public function new(time:Float, data:Int, length:Float = 0, kind:String = "") + public function new(time:Float, data:Int, length:Float = 0, kind:String = '') { this = { diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 757893d7f..1c03cb7bc 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -40,7 +40,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass * Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations) * that this bopper will play. */ - public var idleSuffix(default, set):String = ""; + public var idleSuffix(default, set):String = ''; /** * Whether this bopper should bop every beat. By default it's true, but when used @@ -60,7 +60,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass */ public var globalOffsets(default, set):Array = [0, 0]; - function set_globalOffsets(value:Array) + function set_globalOffsets(value:Array):Array { if (globalOffsets == null) globalOffsets = [0, 0]; if (globalOffsets == value) return value; @@ -70,14 +70,15 @@ class Bopper extends StageProp implements IPlayStateScriptedClass this.x += xDiff; this.y += yDiff; - return animOffsets = value; + globalOffsets = value; + return value; } var animOffsets(default, set):Array = [0, 0]; public var originalPosition:FlxPoint = new FlxPoint(0, 0); - function set_animOffsets(value:Array) + function set_animOffsets(value:Array):Array { if (animOffsets == null) animOffsets = [0, 0]; if (animOffsets == value) return value; @@ -102,8 +103,11 @@ class Bopper extends StageProp implements IPlayStateScriptedClass super(); this.danceEvery = danceEvery; - this.animation.callback = this.onAnimationFrame; - this.animation.finishCallback = this.onAnimationFinished; + if (this.animation != null) + { + this.animation.callback = this.onAnimationFrame; + this.animation.finishCallback = this.onAnimationFinished; + } } /** diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 3dd167fd6..e58a9fa84 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -114,7 +114,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass * The default stage construction routine. Called when the stage is going to be played in. * Instantiates each prop and adds it to the stage, while setting its parameters. */ - function buildStage() + function buildStage():Void { trace('Building stage for display: ${this.stageId}'); @@ -143,9 +143,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass // Initalize sprite frames. switch (dataProp.animType) { - case "packer": + case 'packer': propSprite.frames = Paths.getPackerAtlas(dataProp.assetPath); - default: // "sparrow" + default: // 'sparrow' propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath); } } @@ -189,7 +189,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass switch (dataProp.animType) { - case "packer": + case 'packer': for (propAnim in dataProp.animations) { propSprite.animation.add(propAnim.name, propAnim.frameIndices); @@ -199,7 +199,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]); } } - default: // "sparrow" + default: // 'sparrow' FlxAnimationUtil.addAtlasAnimations(propSprite, dataProp.animations); if (Std.isOfType(propSprite, Bopper)) { @@ -302,7 +302,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass /** * Used by the PlayState to add a character to the stage. */ - public function addCharacter(character:BaseCharacter, charType:CharacterType) + public function addCharacter(character:BaseCharacter, charType:CharacterType):Void { if (character == null) return; @@ -325,16 +325,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass switch (charType) { case BF: - this.characters.set("bf", character); + this.characters.set('bf', character); charData = _data.characters.bf; character.flipX = !character.getDataFlipX(); character.initHealthIcon(false); case GF: - this.characters.set("gf", character); + this.characters.set('gf', character); charData = _data.characters.gf; character.flipX = character.getDataFlipX(); case DAD: - this.characters.set("dad", character); + this.characters.set('dad', character); charData = _data.characters.dad; character.flipX = character.getDataFlipX(); character.initHealthIcon(true); @@ -414,11 +414,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass { if (pop) { - var boyfriend:BaseCharacter = getCharacter("bf"); + var boyfriend:BaseCharacter = getCharacter('bf'); // Remove the character from the stage. this.remove(boyfriend); - this.characters.remove("bf"); + this.characters.remove('bf'); return boyfriend; } diff --git a/source/funkin/ui/animDebugShit/FlxAnimateTest.hx b/source/funkin/ui/animDebugShit/FlxAnimateTest.hx new file mode 100644 index 000000000..738e109ef --- /dev/null +++ b/source/funkin/ui/animDebugShit/FlxAnimateTest.hx @@ -0,0 +1,48 @@ +package funkin.ui.animDebugShit; + +import flixel.FlxG; +import funkin.graphics.adobeanimate.FlxAtlasSprite; + +/** + * A simple test of FlxAnimate. + * Delete this later? + */ +class FlxAnimateTest extends MusicBeatState +{ + var sprite:FlxAtlasSprite; + + public function new() + { + super(); + this.bgColor = 0xFF999999; + } + + public override function create():Void + { + super.create(); + + sprite = new FlxAtlasSprite(0, 0, 'shared:assets/shared/images/characters/tankman'); + add(sprite); + + sprite.playAnimation('idle'); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (FlxG.keys.justPressed.SPACE) sprite.playAnimation('idle'); + + if (FlxG.keys.justPressed.W) sprite.playAnimation('singUP'); + + if (FlxG.keys.justPressed.A) sprite.playAnimation('singLEFT'); + + if (FlxG.keys.justPressed.S) sprite.playAnimation('singDOWN'); + + if (FlxG.keys.justPressed.D) sprite.playAnimation('singRIGHT'); + + if (FlxG.keys.justPressed.J) sprite.playAnimation('hehPrettyGood'); + + if (FlxG.keys.justPressed.K) sprite.playAnimation('ugh'); + } +}