diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml
new file mode 100644
index 000000000..008830842
--- /dev/null
+++ b/.github/actions/setup-haxeshit/action.yml
@@ -0,0 +1,18 @@
+name: setup-haxeshit
+description: "sets up haxe shit, using HMM!"
+runs:
+ using: "composite"
+ steps:
+ - uses: krdlab/setup-haxe@v1.1.6
+ with:
+ haxe-version: 4.2.4
+ - name: Config haxelib
+ run: |
+ haxelib config
+ shell: bash
+ - name: Installing Haxe lol
+ run: |
+ haxe -version
+ haxelib --global install hmm
+ haxelib --global run hmm install --quiet
+ shell: bash
\ No newline at end of file
diff --git a/.github/actions/upload-itch/action.yml b/.github/actions/upload-itch/action.yml
new file mode 100644
index 000000000..5abc31b16
--- /dev/null
+++ b/.github/actions/upload-itch/action.yml
@@ -0,0 +1,44 @@
+name: upload-itch
+description: "installs Butler, and uploads to itch.io!"
+inputs:
+ butler-key:
+ description: "Butler API secret key"
+ required: true
+ build-dir:
+ description: "Directory of the game build"
+ required: true
+ target:
+ description: "Target (html5, win, linux, mac)"
+ required: true
+runs:
+ using: "composite"
+ steps:
+ - name: Install butler Windows
+ if: runner.os == 'Windows'
+ run: |
+ curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default
+ 7z x butler.zip
+ ./butler -v
+ shell: bash
+ - name: Install butler Mac
+ if: runner.os == 'macOS'
+ run: |
+ curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default
+ unzip butler.zip
+ ./butler -V
+ shell: bash
+ - name: Install butler Linux
+ if: runner.os == 'Linux'
+ run: |
+ curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
+ unzip butler.zip
+ chmod +x butler
+ ./butler -V
+ shell: bash
+ - name: Upload game to itch.io
+ env:
+ BUTLER_API_KEY: ${{inputs.butler-key}}
+ run: |
+ ./butler login
+ ./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF##*/}
+ shell: bash
\ No newline at end of file
diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml
new file mode 100644
index 000000000..abbacc7d3
--- /dev/null
+++ b/.github/workflows/build-shit.yml
@@ -0,0 +1,63 @@
+name: build-upload
+on: [push, workflow_dispatch]
+jobs:
+ create-nightly-html5:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/setup-haxeshit
+ - name: Build game?
+ run: |
+ haxelib run lime build html5 -debug
+ ls
+ - uses: ./.github/actions/upload-itch
+ with:
+ butler-key: ${{ secrets.BUTLER_API_KEY}}
+ build-dir: export/debug/html5/bin
+ target: html5
+ create-nightly-win:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/setup-haxeshit
+ - name: Build game
+ run: |
+ haxelib run lime build windows -debug
+ dir
+ - uses: ./.github/actions/upload-itch
+ with:
+ butler-key: ${{ secrets.BUTLER_API_KEY}}
+ build-dir: export/debug/windows/bin
+ target: win
+ create-nightly-mac:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/setup-haxeshit
+ - name: Build game?
+ run: |
+ haxelib run lime build mac -debug
+ ls
+ - uses: ./.github/actions/upload-itch
+ with:
+ butler-key: ${{ secrets.BUTLER_API_KEY}}
+ build-dir: export/debug/macos/bin
+ target: mac
+ create-nightly-linux:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/setup-haxeshit
+ - name: Setting up Linux
+ run: |
+ haxelib run lime setup linux
+ - name: Build game?
+ run: |
+ haxelib run lime build linux -debug
+ ls
+ - uses: ./.github/actions/upload-itch
+ with:
+ butler-key: ${{ secrets.BUTLER_API_KEY}}
+ build-dir: export/debug/linux/bin
+ target: linux
+
diff --git a/.github/workflows/learn-github-actions.yml b/.github/workflows/learn-github-actions.yml
deleted file mode 100644
index 44702b170..000000000
--- a/.github/workflows/learn-github-actions.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: learn-github-actions
-on: [push]
-jobs:
- create-nightly-linux:
- runs-on: ubuntu-latest
- steps:
- - uses: krdlab/setup-haxe@v1.1.6
- with:
- haxe-version: 4.2.4
- - uses: actions/checkout@v2
- - name: Cache Haxe Stuff
- run: |
- haxelib config
- - name: Installing Haxe Stuff
- run: |
- haxe -version
- haxelib install openfl --quiet
- haxelib install lime --quiet
- haxelib install flixel --quiet
- haxelib install flixel-addons --quiet
- haxelib install hscript --quiet
- haxelib install flixel-ui --quiet
- haxelib install firetongue --quiet
- haxelib install hxcpp-debug-server --quiet
- haxelib install hxcpp --quiet
- haxelib git polymod https://github.com/larsiusprime/polymod develop --quiet
- haxelib run lime setup linux
- - name: Build game?
- run: |
- haxelib run lime build html5 -debug -clean
- ls
- - name: Install butler and shit
- run: |
- curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
- unzip butler.zip
- chmod +x butler
- ./butler -V
- - name: Upload game to itch.io
- env:
- BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY}}
- run: |
- ./butler login
- ./butler push export/debug/html5/bin ninja-muffin24/funkin-secret:html5
- - uses: actions/upload-artifact@v1
- with:
- name: funkinSecret-${{ runner.os }}-${{ github.sha }}
- path: export/debug/html5/bin
-
diff --git a/.gitignore b/.gitignore
index 8a4d131df..492e1d0c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@ export/
.vscode/
APIStuff.hx
.DS_STORE
-RECOVER_*.fla
\ No newline at end of file
+RECOVER_*.fla
+.haxelib/
\ No newline at end of file
diff --git a/Project.xml b/Project.xml
index 89402c2eb..81f017853 100644
--- a/Project.xml
+++ b/Project.xml
@@ -128,6 +128,7 @@
+
diff --git a/README.md b/README.md
index d868c6574..df348d7f2 100644
--- a/README.md
+++ b/README.md
@@ -39,27 +39,21 @@ First you need to install Haxe and HaxeFlixel. I'm too lazy to write and keep up
1. [Install Haxe 4.1.5](https://haxe.org/download/version/4.1.5/) (Download 4.1.5 instead of 4.2.0 because 4.2.0 is broken and is not working with gits properly...)
2. [Install HaxeFlixel](https://haxeflixel.com/documentation/install-haxeflixel/) after downloading Haxe
-Other installations you'd need is the additional libraries, a fully updated list will be in `Project.xml` in the project root. Currently, these are all of the things you need to install:
+Other installations you'd need is the additional libraries, a fully updated list will be in `hmm.json` in the project root. Currently, these are all of the things you need to install:
```
-flixel
-flixel-addons
-flixel-ui
-hscript
-newgrounds
+haxelib --global install hmm
+haxelib --global run hmm setup
+hmm install
```
-So for each of those type `haxelib install [library]` so shit like `haxelib install newgrounds`
-You'll also need to install a couple things that involve Gits. To do this, you need to do a few things first.
+
You should have everything ready for compiling the game! Follow the guide below to continue!
-At the moment, you can optionally fix the transition bug in songs with zoomed out cameras.
-- Run `haxelib git flixel-addons https://github.com/HaxeFlixel/flixel-addons` in the terminal/command-prompt.
-
### Ignored files
I gitignore the API keys for the game, so that no one can nab them and post fake highscores on the leaderboards. But because of that the game
diff --git a/example_mods/introMod/_append/data/introText.txt b/example_mods/introMod/_append/data/introText.txt
deleted file mode 100644
index 45e0c08ab..000000000
--- a/example_mods/introMod/_append/data/introText.txt
+++ /dev/null
@@ -1 +0,0 @@
-swagshit--moneymoney
\ No newline at end of file
diff --git a/example_mods/introMod/images/gfDanceTitle.png b/example_mods/introMod/images/gfDanceTitle.png
deleted file mode 100644
index 989f2a68a..000000000
Binary files a/example_mods/introMod/images/gfDanceTitle.png and /dev/null differ
diff --git a/hmm.json b/hmm.json
new file mode 100644
index 000000000..984f54166
--- /dev/null
+++ b/hmm.json
@@ -0,0 +1,74 @@
+{
+ "dependencies": [
+ {
+ "name": "discord_rpc",
+ "type": "git",
+ "dir": null,
+ "ref": "2d83fa8",
+ "url": "https://github.com/Aidan63/linc_discord-rpc"
+ },
+ {
+ "name": "firetongue",
+ "type": "git",
+ "dir": null,
+ "ref": "c5666c8",
+ "url": "https://github.com/larsiusprime/firetongue"
+ },
+ {
+ "name": "flixel",
+ "type": "git",
+ "dir": null,
+ "ref": "93a049d6",
+ "url": "https://github.com/haxeflixel/flixel"
+ },
+ {
+ "name": "flixel-addons",
+ "type": "haxelib",
+ "version": "2.11.0"
+ },
+ {
+ "name": "flixel-ui",
+ "type": "haxelib",
+ "version": "2.4.0"
+ },
+ {
+ "name": "hscript",
+ "type": "git",
+ "dir": null,
+ "ref": "a1b7f74",
+ "url": "https://github.com/mastereric/hscript"
+ },
+ {
+ "name": "hxcpp",
+ "type": "haxelib",
+ "version": "4.2.1"
+ },
+ {
+ "name": "hxcpp-debug-server",
+ "type": "haxelib",
+ "version": "1.2.4"
+ },
+ {
+ "name": "lime",
+ "type": "haxelib",
+ "version": "7.9.0"
+ },
+ {
+ "name": "openfl",
+ "type": "haxelib",
+ "version": "9.1.0"
+ },
+ {
+ "name": "polymod",
+ "type": "git",
+ "dir": null,
+ "ref": "c858b48",
+ "url": "https://github.com/larsiusprime/polymod"
+ },
+ {
+ "name": "thx.semver",
+ "type": "haxelib",
+ "version": "0.2.2"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/source/funkin/Boyfriend.hx b/source/funkin/Boyfriend.hx
deleted file mode 100644
index 12bf7553e..000000000
--- a/source/funkin/Boyfriend.hx
+++ /dev/null
@@ -1,43 +0,0 @@
-package funkin;
-
-import flixel.FlxSprite;
-import flixel.graphics.frames.FlxAtlasFrames;
-import flixel.util.FlxTimer;
-
-using StringTools;
-
-class Boyfriend extends Character
-{
- // public var stunned:Bool = false;
- public function new(x:Float, y:Float, ?char:String = 'bf')
- {
- super(x, y, char, true);
- }
-
- public var startedDeath:Bool = false;
-
- override function update(elapsed:Float)
- {
- if (!debugMode)
- {
- if (animation.curAnim.name.startsWith('sing'))
- {
- holdTimer += elapsed;
- }
- else
- holdTimer = 0;
-
- if (animation.curAnim.name.endsWith('miss') && animation.curAnim.finished && !debugMode)
- {
- playAnim('idle', true, false, 10);
- }
-
- if (animation.curAnim.name == 'firstDeath' && animation.curAnim.finished && startedDeath)
- {
- playAnim('deathLoop');
- }
- }
-
- super.update(elapsed);
- }
-}
diff --git a/source/funkin/Character.hx b/source/funkin/Character.hx
deleted file mode 100644
index a81d599a6..000000000
--- a/source/funkin/Character.hx
+++ /dev/null
@@ -1,766 +0,0 @@
-package funkin;
-
-import funkin.util.Constants;
-import funkin.Note.NoteData;
-import funkin.SongLoad.SwagSong;
-import funkin.Section.SwagSection;
-import flixel.FlxSprite;
-import flixel.animation.FlxBaseAnimation;
-import flixel.graphics.frames.FlxAtlasFrames;
-import flixel.util.FlxSort;
-import haxe.io.Path;
-import funkin.play.PlayState;
-
-using StringTools;
-
-class Character extends FlxSprite
-{
- public var animOffsets:Map>;
- public var debugMode:Bool = false;
-
- public var isPlayer:Bool = false;
- public var curCharacter:String = 'bf';
-
- public var holdTimer:Float = 0;
-
- public var animationNotes:Array = [];
-
- public function new(x:Float, y:Float, ?character:String = "bf", ?isPlayer:Bool = false)
- {
- super(x, y);
-
- animOffsets = new Map>();
- curCharacter = character;
- this.isPlayer = isPlayer;
-
- var tex:FlxAtlasFrames;
- antialiasing = true;
-
- switch (curCharacter)
- {
- case 'gf':
- // GIRLFRIEND CODE
- tex = Paths.getSparrowAtlas('characters/GF_assets');
- frames = tex;
- quickAnimAdd('cheer', 'GF Cheer');
- quickAnimAdd('singLEFT', 'GF left note');
- quickAnimAdd('singRIGHT', 'GF Right Note');
- quickAnimAdd('singUP', 'GF Up Note');
- quickAnimAdd('singDOWN', 'GF Down Note');
- animation.addByIndices('sad', 'gf sad', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "", 24, true);
- animation.addByIndices('danceLeft', 'GF Dancing Beat', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false);
- animation.addByIndices('danceRight', 'GF Dancing Beat', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24, false);
- animation.addByIndices('hairBlow', "GF Dancing Beat Hair blowing", [0, 1, 2, 3], "", 24);
- animation.addByIndices('hairFall', "GF Dancing Beat Hair Landing", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], "", 24, false);
- animation.addByPrefix('scared', 'GF FEAR', 24, true);
-
- loadOffsetFile(curCharacter);
-
- playAnim('danceRight');
-
- case 'gf-christmas':
- tex = Paths.getSparrowAtlas('characters/gfChristmas');
- frames = tex;
- quickAnimAdd('cheer', 'GF Cheer');
- quickAnimAdd('singLEFT', 'GF left note');
- quickAnimAdd('singRIGHT', 'GF Right Note');
- quickAnimAdd('singUP', 'GF Up Note');
- quickAnimAdd('singDOWN', 'GF Down Note');
- animation.addByIndices('sad', 'gf sad', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "", 24, false);
- animation.addByIndices('danceLeft', 'GF Dancing Beat', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false);
- animation.addByIndices('danceRight', 'GF Dancing Beat', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24, false);
- animation.addByIndices('hairBlow', "GF Dancing Beat Hair blowing", [0, 1, 2, 3], "", 24);
- animation.addByIndices('hairFall', "GF Dancing Beat Hair Landing", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], "", 24, false);
- animation.addByPrefix('scared', 'GF FEAR', 24, true);
-
- loadOffsetFile(curCharacter);
-
- playAnim('danceRight');
- case 'gf-tankmen':
- frames = Paths.getSparrowAtlas('characters/gfTankmen');
- animation.addByIndices('sad', 'GF Crying at Gunpoint', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "", 24, true);
- animation.addByIndices('danceLeft', 'GF Dancing at Gunpoint', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false);
- animation.addByIndices('danceRight', 'GF Dancing at Gunpoint', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24, false);
-
- loadOffsetFile('gf');
- playAnim('danceRight');
-
- case 'bf-holding-gf':
- frames = Paths.getSparrowAtlas('characters/bfAndGF');
- quickAnimAdd('idle', 'BF idle dance');
- quickAnimAdd('singDOWN', 'BF NOTE DOWN0');
- quickAnimAdd('singLEFT', 'BF NOTE LEFT0');
- quickAnimAdd('singRIGHT', 'BF NOTE RIGHT0');
- quickAnimAdd('singUP', 'BF NOTE UP0');
-
- quickAnimAdd('singDOWNmiss', 'BF NOTE DOWN MISS');
- quickAnimAdd('singLEFTmiss', 'BF NOTE LEFT MISS');
- quickAnimAdd('singRIGHTmiss', 'BF NOTE RIGHT MISS');
- quickAnimAdd('singUPmiss', 'BF NOTE UP MISS');
- quickAnimAdd('bfCatch', 'BF catches GF');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
-
- flipX = true;
-
- case 'gf-car':
- tex = Paths.getSparrowAtlas('characters/gfCar');
- frames = tex;
- animation.addByIndices('singUP', 'GF Dancing Beat Hair blowing CAR', [0], "", 24, false);
- animation.addByIndices('danceLeft', 'GF Dancing Beat Hair blowing CAR', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false);
- animation.addByIndices('danceRight', 'GF Dancing Beat Hair blowing CAR', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24,
- false);
- animation.addByIndices('idleHair', 'GF Dancing Beat Hair blowing CAR', [10, 11, 12, 25, 26, 27], "", 24, true);
-
- loadOffsetFile(curCharacter);
-
- playAnim('danceRight');
-
- case 'gf-pixel':
- tex = Paths.getSparrowAtlas('characters/gfPixel');
- frames = tex;
- animation.addByIndices('singUP', 'GF IDLE', [2], "", 24, false);
- animation.addByIndices('danceLeft', 'GF IDLE', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false);
- animation.addByIndices('danceRight', 'GF IDLE', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24, false);
-
- loadOffsetFile(curCharacter);
-
- playAnim('danceRight');
-
- setGraphicSize(Std.int(width * Constants.PIXEL_ART_SCALE));
- updateHitbox();
- antialiasing = false;
-
- case 'dad':
- // DAD ANIMATION LOADING CODE
- tex = Paths.getSparrowAtlas('characters/DADDY_DEAREST');
- frames = tex;
- quickAnimAdd('idle', 'Dad idle dance');
- quickAnimAdd('singUP', 'Dad Sing Note UP');
- quickAnimAdd('singRIGHT', 'Dad Sing Note RIGHT');
- quickAnimAdd('singDOWN', 'Dad Sing Note DOWN');
- quickAnimAdd('singLEFT', 'Dad Sing Note LEFT');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
- case 'spooky':
- tex = Paths.getSparrowAtlas('characters/spooky_kids_assets');
- frames = tex;
- quickAnimAdd('singUP', 'spooky UP NOTE');
- quickAnimAdd('singDOWN', 'spooky DOWN note');
- quickAnimAdd('singLEFT', 'note sing left');
- quickAnimAdd('singRIGHT', 'spooky sing right');
- animation.addByIndices('danceLeft', 'spooky dance idle', [0, 2, 6], "", 12, false);
- animation.addByIndices('danceRight', 'spooky dance idle', [8, 10, 12, 14], "", 12, false);
-
- loadOffsetFile(curCharacter);
-
- playAnim('danceRight');
- case 'mom':
- tex = Paths.getSparrowAtlas('characters/Mom_Assets');
- frames = tex;
-
- quickAnimAdd('idle', "Mom Idle");
- quickAnimAdd('singUP', "Mom Up Pose");
- quickAnimAdd('singDOWN', "MOM DOWN POSE");
- quickAnimAdd('singLEFT', 'Mom Left Pose');
- // ANIMATION IS CALLED MOM LEFT POSE BUT ITS FOR THE RIGHT
- // CUZ DAVE IS DUMB!
- quickAnimAdd('singRIGHT', 'Mom Pose Left');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
-
- case 'mom-car':
- tex = Paths.getSparrowAtlas('characters/momCar');
- frames = tex;
-
- quickAnimAdd('idle', "Mom Idle");
- quickAnimAdd('singUP', "Mom Up Pose");
- quickAnimAdd('singDOWN', "MOM DOWN POSE");
- quickAnimAdd('singLEFT', 'Mom Left Pose');
- // ANIMATION IS CALLED MOM LEFT POSE BUT ITS FOR THE RIGHT
- // CUZ DAVE IS DUMB!
- quickAnimAdd('singRIGHT', 'Mom Pose Left');
- animation.addByIndices('idleHair', "Mom Idle", [10, 11, 12, 13], "", 24, true);
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
- case 'monster':
- tex = Paths.getSparrowAtlas('characters/Monster_Assets');
- frames = tex;
- quickAnimAdd('idle', 'monster idle');
- quickAnimAdd('singUP', 'monster up note');
- quickAnimAdd('singDOWN', 'monster down');
- quickAnimAdd('singLEFT', 'Monster left note');
- quickAnimAdd('singRIGHT', 'Monster Right note');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
- case 'monster-christmas':
- tex = Paths.getSparrowAtlas('characters/monsterChristmas');
- frames = tex;
- quickAnimAdd('idle', 'monster idle');
- quickAnimAdd('singUP', 'monster up note');
- quickAnimAdd('singDOWN', 'monster down');
- quickAnimAdd('singLEFT', 'Monster left note');
- quickAnimAdd('singRIGHT', 'Monster Right note');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
- case 'pico':
- tex = Paths.getSparrowAtlas('characters/Pico_FNF_assetss');
- frames = tex;
- quickAnimAdd('idle', "Pico Idle Dance");
- quickAnimAdd('singUP', 'pico Up note0');
- quickAnimAdd('singDOWN', 'Pico Down Note0');
-
- // isPlayer = true;
-
- // Need to be flipped! REDO THIS LATER!
- quickAnimAdd('singLEFT', 'Pico Note Right0');
- quickAnimAdd('singRIGHT', 'Pico NOTE LEFT0');
- quickAnimAdd('singRIGHTmiss', 'Pico NOTE LEFT miss');
- quickAnimAdd('singLEFTmiss', 'Pico Note Right Miss');
-
- quickAnimAdd('singUPmiss', 'pico Up note miss');
- quickAnimAdd('singDOWNmiss', 'Pico Down Note MISS');
-
- // right now it loads a seperate offset file for pico, would be cool if could generalize it!
- var playerShit:String = "";
-
- if (isPlayer)
- playerShit += "Player";
-
- loadOffsetFile(curCharacter + playerShit);
-
- playAnim('idle');
-
- flipX = true;
-
- case 'pico-speaker':
- frames = Paths.getSparrowAtlas('characters/picoSpeaker');
-
- quickAnimAdd('shoot1', "Pico shoot 1");
- quickAnimAdd('shoot2', "Pico shoot 2");
- quickAnimAdd('shoot3', "Pico shoot 3");
- quickAnimAdd('shoot4', "Pico shoot 4");
-
- // here for now, will be replaced later for less copypaste
- loadOffsetFile(curCharacter);
- playAnim('shoot1');
-
- loadMappedAnims();
-
- case 'bf':
- var tex = Paths.getSparrowAtlas('characters/BOYFRIEND');
- frames = tex;
- quickAnimAdd('idle', 'BF idle dance');
- quickAnimAdd('singUP', 'BF NOTE UP0');
- quickAnimAdd('singLEFT', 'BF NOTE LEFT0');
- quickAnimAdd('singRIGHT', 'BF NOTE RIGHT0');
- quickAnimAdd('singDOWN', 'BF NOTE DOWN0');
- quickAnimAdd('singUPmiss', 'BF NOTE UP MISS');
- quickAnimAdd('singLEFTmiss', 'BF NOTE LEFT MISS');
- quickAnimAdd('singRIGHTmiss', 'BF NOTE RIGHT MISS');
- quickAnimAdd('singDOWNmiss', 'BF NOTE DOWN MISS');
- quickAnimAdd('preAttack', 'bf pre attack');
- quickAnimAdd('attack', 'boyfriend attack');
- quickAnimAdd('hey', 'BF HEY');
-
- quickAnimAdd('firstDeath', "BF dies");
- animation.addByPrefix('deathLoop', "BF Dead Loop", 24, true);
- quickAnimAdd('deathConfirm', "BF Dead confirm");
-
- animation.addByPrefix('scared', 'BF idle shaking', 24, true);
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
-
- flipX = true;
-
- loadOffsetFile(curCharacter);
-
- case 'bf-christmas':
- var tex = Paths.getSparrowAtlas('characters/bfChristmas');
- frames = tex;
- quickAnimAdd('idle', 'BF idle dance');
- quickAnimAdd('singUP', 'BF NOTE UP0');
- quickAnimAdd('singLEFT', 'BF NOTE LEFT0');
- quickAnimAdd('singRIGHT', 'BF NOTE RIGHT0');
- quickAnimAdd('singDOWN', 'BF NOTE DOWN0');
- quickAnimAdd('singUPmiss', 'BF NOTE UP MISS');
- quickAnimAdd('singLEFTmiss', 'BF NOTE LEFT MISS');
- quickAnimAdd('singRIGHTmiss', 'BF NOTE RIGHT MISS');
- quickAnimAdd('singDOWNmiss', 'BF NOTE DOWN MISS');
- quickAnimAdd('hey', 'BF HEY');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
-
- flipX = true;
- case 'bf-car':
- var tex = Paths.getSparrowAtlas('characters/bfCar');
- frames = tex;
- quickAnimAdd('idle', 'BF idle dance');
- quickAnimAdd('singUP', 'BF NOTE UP0');
- quickAnimAdd('singLEFT', 'BF NOTE LEFT0');
- quickAnimAdd('singRIGHT', 'BF NOTE RIGHT0');
- quickAnimAdd('singDOWN', 'BF NOTE DOWN0');
- quickAnimAdd('singUPmiss', 'BF NOTE UP MISS');
- quickAnimAdd('singLEFTmiss', 'BF NOTE LEFT MISS');
- quickAnimAdd('singRIGHTmiss', 'BF NOTE RIGHT MISS');
- quickAnimAdd('singDOWNmiss', 'BF NOTE DOWN MISS');
- animation.addByIndices('idleHair', 'BF idle dance', [10, 11, 12, 13], "", 24, true);
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
-
- flipX = true;
- case 'bf-pixel':
- frames = Paths.getSparrowAtlas('characters/bfPixel');
- quickAnimAdd('idle', 'BF IDLE');
- quickAnimAdd('singUP', 'BF UP NOTE');
- quickAnimAdd('singLEFT', 'BF LEFT NOTE');
- quickAnimAdd('singRIGHT', 'BF RIGHT NOTE');
- quickAnimAdd('singDOWN', 'BF DOWN NOTE');
- quickAnimAdd('singUPmiss', 'BF UP MISS');
- quickAnimAdd('singLEFTmiss', 'BF LEFT MISS');
- quickAnimAdd('singRIGHTmiss', 'BF RIGHT MISS');
- quickAnimAdd('singDOWNmiss', 'BF DOWN MISS');
-
- loadOffsetFile(curCharacter);
-
- setGraphicSize(Std.int(width * 6));
- updateHitbox();
-
- playAnim('idle');
-
- width -= 100;
- height -= 100;
-
- antialiasing = false;
-
- flipX = true;
- case 'bf-pixel-dead':
- frames = Paths.getSparrowAtlas('characters/bfPixelsDEAD');
- quickAnimAdd('singUP', "BF Dies pixel");
- quickAnimAdd('firstDeath', "BF Dies pixel");
- animation.addByPrefix('deathLoop', "Retry Loop", 24, true);
- quickAnimAdd('deathConfirm', "RETRY CONFIRM");
- animation.play('firstDeath');
-
- loadOffsetFile(curCharacter);
-
- playAnim('firstDeath');
- // pixel bullshit
- setGraphicSize(Std.int(width * 6));
- updateHitbox();
- antialiasing = false;
- flipX = true;
-
- case 'bf-holding-gf-dead':
- frames = Paths.getSparrowAtlas('characters/bfHoldingGF-DEAD');
- quickAnimAdd('singUP', 'BF Dead with GF Loop');
- quickAnimAdd('firstDeath', 'BF Dies with GF');
- animation.addByPrefix('deathLoop', 'BF Dead with GF Loop', 24, true);
- quickAnimAdd('deathConfirm', 'RETRY confirm holding gf');
-
- loadOffsetFile(curCharacter);
-
- playAnim('firstDeath');
-
- flipX = true;
-
- case 'senpai':
- frames = Paths.getSparrowAtlas('characters/senpai');
- quickAnimAdd('idle', 'Senpai Idle');
- // at framerate 16.8 animation plays over 2 beats at 144bpm,
- // but if the game lags or the bpm is > 144 (mods etc.)
- // he may miss his next dance
- // animation.getByName('idle').frameRate = 16.8;
-
- quickAnimAdd('singUP', 'SENPAI UP NOTE');
- quickAnimAdd('singLEFT', 'SENPAI LEFT NOTE');
- quickAnimAdd('singRIGHT', 'SENPAI RIGHT NOTE');
- quickAnimAdd('singDOWN', 'SENPAI DOWN NOTE');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
-
- setGraphicSize(Std.int(width * 6));
- updateHitbox();
-
- antialiasing = false;
- case 'senpai-angry':
- frames = Paths.getSparrowAtlas('characters/senpai');
- quickAnimAdd('idle', 'Angry Senpai Idle');
- quickAnimAdd('singUP', 'Angry Senpai UP NOTE');
- quickAnimAdd('singLEFT', 'Angry Senpai LEFT NOTE');
- quickAnimAdd('singRIGHT', 'Angry Senpai RIGHT NOTE');
- quickAnimAdd('singDOWN', 'Angry Senpai DOWN NOTE');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
-
- setGraphicSize(Std.int(width * 6));
- updateHitbox();
-
- antialiasing = false;
-
- case 'spirit':
- frames = Paths.getPackerAtlas('characters/spirit');
- quickAnimAdd('idle', "idle spirit_");
- quickAnimAdd('singUP', "up_");
- quickAnimAdd('singRIGHT', "right_");
- quickAnimAdd('singLEFT', "left_");
- quickAnimAdd('singDOWN', "spirit down_");
-
- loadOffsetFile(curCharacter);
-
- setGraphicSize(Std.int(width * 6));
- updateHitbox();
-
- playAnim('idle');
-
- antialiasing = false;
-
- case 'parents-christmas':
- frames = Paths.getSparrowAtlas('characters/mom_dad_christmas_assets');
- quickAnimAdd('idle', 'Parent Christmas Idle');
- quickAnimAdd('singUP', 'Parent Up Note Dad');
- quickAnimAdd('singDOWN', 'Parent Down Note Dad');
- quickAnimAdd('singLEFT', 'Parent Left Note Dad');
- quickAnimAdd('singRIGHT', 'Parent Right Note Dad');
-
- quickAnimAdd('singUP-alt', 'Parent Up Note Mom');
-
- quickAnimAdd('singDOWN-alt', 'Parent Down Note Mom');
- quickAnimAdd('singLEFT-alt', 'Parent Left Note Mom');
- quickAnimAdd('singRIGHT-alt', 'Parent Right Note Mom');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
- case 'tankman':
- frames = Paths.getSparrowAtlas('characters/tankmanCaptain');
-
- quickAnimAdd('idle', "Tankman Idle Dance");
-
- if (isPlayer)
- {
- quickAnimAdd('singLEFT', 'Tankman Note Left ');
- quickAnimAdd('singRIGHT', 'Tankman Right Note ');
- quickAnimAdd('singLEFTmiss', 'Tankman Note Left MISS');
- quickAnimAdd('singRIGHTmiss', 'Tankman Right Note MISS');
- }
- else
- {
- // Need to be flipped! REDO THIS LATER
- quickAnimAdd('singLEFT', 'Tankman Right Note ');
- quickAnimAdd('singRIGHT', 'Tankman Note Left ');
- quickAnimAdd('singLEFTmiss', 'Tankman Right Note MISS');
- quickAnimAdd('singRIGHTmiss', 'Tankman Note Left MISS');
- }
-
- quickAnimAdd('singUP', 'Tankman UP note ');
- quickAnimAdd('singDOWN', 'Tankman DOWN note ');
- quickAnimAdd('singUPmiss', 'Tankman UP note MISS');
- quickAnimAdd('singDOWNmiss', 'Tankman DOWN note MISS');
-
- // PRETTY GOOD tankman
- // TANKMAN UGH instanc
-
- quickAnimAdd('singDOWN-alt', 'PRETTY GOOD');
- quickAnimAdd('singUP-alt', 'TANKMAN UGH');
-
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
-
- flipX = true;
- case 'darnell':
- frames = Paths.getSparrowAtlas('characters/darnell');
-
- quickAnimAdd('idle', 'Darnell Idle');
- quickAnimAdd('singUP', "Darnell pose up");
- quickAnimAdd('singDOWN', 'Darnell Pose Down');
- quickAnimAdd('singRIGHT', 'darnell pose left');
- quickAnimAdd('singLEFT', 'Darnell pose right'); // naming is reversed for left/right for darnell!
- quickAnimAdd('laugh', 'darnell laugh');
-
- // temp
- loadOffsetFile(curCharacter);
-
- playAnim('idle');
-
- animation.finishCallback = function(animShit:String)
- {
- if (animShit.startsWith('sing'))
- {
- // loop the anim
- // this way is a little verbose, but basically sets it to the same animation, but 8 frames before finish
- playAnim(animShit, true, false, animation.getByName(animShit).frames.length - 8);
- }
- }
- case 'darnell-fighter':
- frames = Paths.getSparrowAtlas('fightDarnell');
-
- quickAnimAdd('idle', "fight idle darnell");
- quickAnimAdd('block', 'block');
- quickAnimAdd('hit high', 'hit high');
- quickAnimAdd('hit low', 'hit low');
- quickAnimAdd('punch low', 'punch low');
- quickAnimAdd('punch high', 'punch high');
- quickAnimAdd('dodge', 'dodge');
- playAnim('idle');
-
- addOffset('punch low', -90);
- addOffset('punch high', -90);
- addOffset('block', 50, 20);
- addOffset('dodge', 50, -20);
-
- case 'pico-fighter':
- frames = Paths.getSparrowAtlas('fightPico');
-
- quickAnimAdd('idle', 'fight idle pico');
- quickAnimAdd('block', 'block');
- quickAnimAdd('hit high', 'hit high');
- quickAnimAdd('hit low', 'hit low');
- quickAnimAdd('punch low', 'punch low');
- quickAnimAdd('punch high', 'punch high');
- quickAnimAdd('dodge', 'dodge');
- playAnim('idle');
-
- addOffset('punch low', 160);
- addOffset('punch high', 160);
-
- case 'nene':
- // GIRLFRIEND CODE
- tex = Paths.getSparrowAtlas('characters/Nene_assets');
- frames = tex;
-
- animation.addByIndices('danceLeft', 'nenebeforeyougetawoopin', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false);
- animation.addByIndices('danceRight', 'nenebeforeyougetawoopin', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24, false);
-
- loadOffsetFile(curCharacter);
-
- playAnim('danceRight');
- }
-
- dance();
- animation.finish();
-
- if (isPlayer)
- {
- flipX = !flipX;
-
- // Doesn't flip for BF, since his are already in the right place???
- if (!curCharacter.startsWith('bf'))
- {
- // var animArray
- var oldRight = animation.getByName('singRIGHT').frames;
- animation.getByName('singRIGHT').frames = animation.getByName('singLEFT').frames;
- animation.getByName('singLEFT').frames = oldRight;
-
- // IF THEY HAVE MISS ANIMATIONS??
- if (animation.getByName('singRIGHTmiss') != null)
- {
- var oldMiss = animation.getByName('singRIGHTmiss').frames;
- animation.getByName('singRIGHTmiss').frames = animation.getByName('singLEFTmiss').frames;
- animation.getByName('singLEFTmiss').frames = oldMiss;
- }
- }
- }
- }
-
- public function loadMappedAnims()
- {
- var swagshit:SwagSong = SongLoad.loadFromJson('stress', 'stress');
-
- var notes:Array = swagshit.noteMap.get('picospeaker');
-
- for (section in notes)
- {
- for (noteData in section.sectionNotes)
- {
- animationNotes.push(noteData);
- }
- }
-
- trace(animationNotes);
- animationNotes.sort(sortAnims);
- }
-
- function sortAnims(val1:NoteData, val2:NoteData):Int
- {
- return FlxSort.byValues(FlxSort.ASCENDING, val1.strumTime, val2.strumTime);
- }
-
- function quickAnimAdd(name:String, prefix:String)
- {
- animation.addByPrefix(name, prefix, 24, false);
- }
-
- private function loadOffsetFile(offsetCharacter:String)
- {
- var daFile:Array = CoolUtil.coolTextFile(Paths.file("images/characters/" + offsetCharacter + "Offsets.txt", TEXT, 'shared'));
-
- for (i in daFile)
- {
- var splitWords:Array = i.split(" ");
- addOffset(splitWords[0], Std.parseInt(splitWords[1]), Std.parseInt(splitWords[2]));
- }
- }
-
- override function update(elapsed:Float)
- {
- if (!curCharacter.startsWith('bf'))
- {
- if (animation.curAnim.name.startsWith('sing'))
- {
- holdTimer += elapsed;
- }
-
- var dadVar:Float = 4;
-
- if (curCharacter == 'dad')
- dadVar = 6.1;
- if (holdTimer >= Conductor.stepCrochet * dadVar * 0.001)
- {
- dance();
- holdTimer = 0;
- }
- }
-
- if (curCharacter.endsWith('-car'))
- {
- // looping hair anims after idle finished
- if (!animation.curAnim.name.startsWith('sing') && animation.curAnim.finished)
- playAnim('idleHair');
- }
-
- switch (curCharacter)
- {
- case 'gf':
- if (animation.curAnim.name == 'hairFall' && animation.curAnim.finished)
- playAnim('danceRight');
- case "pico-speaker":
- // for pico??
- if (animationNotes.length > 0)
- {
- if (Conductor.songPosition > animationNotes[0].strumTime)
- {
- trace('played shoot anim' + animationNotes[0].noteData);
-
- var shootAnim:Int = 1;
-
- if ((cast animationNotes[0].noteData) >= 2)
- shootAnim = 3;
-
- shootAnim += FlxG.random.int(0, 1);
-
- playAnim('shoot' + shootAnim, true);
- animationNotes.shift();
- }
- }
-
- if (animation.curAnim.finished)
- {
- playAnim(animation.curAnim.name, false, false, animation.curAnim.numFrames - 3);
- }
- }
-
- super.update(elapsed);
- }
-
- private var danced:Bool = false;
-
- /**
- * FOR GF DANCING SHIT
- */
- public function dance()
- {
- if (animation == null)
- return;
- if (!debugMode)
- {
- switch (curCharacter)
- {
- case 'gf' | 'gf-christmas' | 'gf-car' | 'gf-pixel' | 'gf-tankmen' | "nene":
- if (!animation.curAnim.name.startsWith('hair'))
- {
- danced = !danced;
-
- if (danced)
- playAnim('danceRight');
- else
- playAnim('danceLeft');
- }
-
- case 'tankman':
- if (!animation.curAnim.name.endsWith('DOWN-alt'))
- playAnim('idle');
-
- case 'spooky':
- danced = !danced;
-
- if (danced)
- playAnim('danceRight');
- else
- playAnim('danceLeft');
- default:
- playAnim('idle');
- }
- }
- }
-
- public function playAnim(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void
- {
- if (animation == null)
- return;
- animation.play(AnimName, Force, Reversed, Frame);
-
- var daOffset = animOffsets.get(AnimName);
- if (animOffsets.exists(AnimName))
- {
- offset.set(daOffset[0], daOffset[1]);
- }
- else
- offset.set(0, 0);
-
- if (curCharacter == 'gf')
- {
- if (AnimName == 'singLEFT')
- {
- danced = true;
- }
- else if (AnimName == 'singRIGHT')
- {
- danced = false;
- }
-
- if (AnimName == 'singUP' || AnimName == 'singDOWN')
- {
- danced = !danced;
- }
- }
- }
-
- public function addOffset(name:String, x:Float = 0, y:Float = 0)
- {
- animOffsets[name] = [x, y];
- }
-}
diff --git a/source/funkin/CoolUtil.hx b/source/funkin/CoolUtil.hx
index 60e12f343..a35364e5f 100644
--- a/source/funkin/CoolUtil.hx
+++ b/source/funkin/CoolUtil.hx
@@ -10,13 +10,13 @@ import flixel.math.FlxRect;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
+import funkin.play.PlayState;
+import funkin.shaderslmfao.ScreenWipeShader;
import haxe.Json;
import haxe.format.JsonParser;
import lime.math.Rectangle;
import lime.utils.Assets;
import openfl.filters.ShaderFilter;
-import funkin.play.PlayState;
-import funkin.shaderslmfao.ScreenWipeShader;
using StringTools;
@@ -68,18 +68,34 @@ class CoolUtil
static var oldCamPos:FlxPoint = new FlxPoint();
static var oldMousePos:FlxPoint = new FlxPoint();
- public static function mouseCamDrag():Void
+ /**
+ * Used to be for general camera middle click dragging, now generalized for any click and drag type shit!
+ * Listen I don't make the rules here
+ * @param target what you want to be dragged, defaults to CAMERA SCROLL
+ * @param jusPres the "justPressed", should be a button of some sort
+ * @param pressed the "pressed", which should be the same button as `jusPres`
+ */
+ public static function mouseCamDrag(?target:FlxPoint, ?jusPres:Bool, ?pressed:Bool):Void
{
- if (FlxG.mouse.justPressedMiddle)
+ if (target == null)
+ target = FlxG.camera.scroll;
+
+ if (jusPres == null)
+ jusPres = FlxG.mouse.justPressedMiddle;
+
+ if (pressed == null)
+ pressed = FlxG.mouse.pressedMiddle;
+
+ if (jusPres)
{
- oldCamPos.set(FlxG.camera.scroll.x, FlxG.camera.scroll.y);
+ oldCamPos.set(target.x, target.y);
oldMousePos.set(FlxG.mouse.screenX, FlxG.mouse.screenY);
}
- if (FlxG.mouse.pressedMiddle)
+ if (pressed)
{
- FlxG.camera.scroll.x = oldCamPos.x - (FlxG.mouse.screenX - oldMousePos.x);
- FlxG.camera.scroll.y = oldCamPos.y - (FlxG.mouse.screenY - oldMousePos.y);
+ target.x = oldCamPos.x - (FlxG.mouse.screenX - oldMousePos.x);
+ target.y = oldCamPos.y - (FlxG.mouse.screenY - oldMousePos.y);
}
}
@@ -118,6 +134,16 @@ class CoolUtil
FlxG.camera.setFilters([new ShaderFilter(screenWipeShit)]);
}
+ /**
+ * Just saves the json with some default values hehe
+ * @param json
+ * @return String
+ */
+ public static inline function jsonStringify(data:Dynamic):String
+ {
+ return Json.stringify(data, null, "\t");
+ }
+
/**
* Hashlink json encoding fix for some wacky bullshit
* https://github.com/HaxeFoundation/haxe/issues/6930#issuecomment-384570392
diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx
index 3706b9f9f..7f37eff18 100644
--- a/source/funkin/FreeplayState.hx
+++ b/source/funkin/FreeplayState.hx
@@ -25,6 +25,7 @@ import funkin.freeplayStuff.BGScrollingText;
import funkin.freeplayStuff.DJBoyfriend;
import funkin.freeplayStuff.FreeplayScore;
import funkin.freeplayStuff.SongMenuItem;
+import funkin.play.HealthIcon;
import funkin.play.PlayState;
import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.PureColor;
@@ -295,7 +296,7 @@ class FreeplayState extends MusicBeatSubstate
// grpSongs.add(songText);
var icon:HealthIcon = new HealthIcon(songs[i].songCharacter);
- icon.sprTracker = songText;
+ // icon.sprTracker = songText;
// using a FlxGroup is too much fuss!
iconArray.push(icon);
diff --git a/source/funkin/GameOverSubstate.hx b/source/funkin/GameOverSubstate.hx
index 40cf9c0ca..113e64b63 100644
--- a/source/funkin/GameOverSubstate.hx
+++ b/source/funkin/GameOverSubstate.hx
@@ -4,83 +4,89 @@ import flixel.FlxObject;
import flixel.system.FlxSound;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
+import funkin.modding.events.ScriptEvent;
+import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState;
+import funkin.play.character.BaseCharacter;
import funkin.ui.PreferencesMenu;
+using StringTools;
+
+/**
+ * A substate which renders over the PlayState when the player dies.
+ * Displays the player death animation, plays the music, and handles restarting the song.
+ *
+ * The newest implementation uses a substate, which prevents having to reload the song and stage each reset.
+ */
class GameOverSubstate extends MusicBeatSubstate
{
- var bf:Boyfriend;
- var camFollow:FlxObject;
+ /**
+ * The boyfriend character.
+ */
+ var boyfriend:BaseCharacter;
- var stageSuffix:String = "";
- var randomGameover:Int = 1;
+ /**
+ * The invisible object in the scene which the camera focuses on.
+ */
+ var cameraFollowPoint:FlxObject;
- var gameOverMusic:FlxSound;
+ /**
+ * The music playing in the background of the state.
+ */
+ var gameOverMusic:FlxSound = new FlxSound();
+
+ /**
+ * Whether the player has confirmed and prepared to restart the level.
+ * This means the animation and transition have already started.
+ */
+ var isEnding:Bool = false;
+
+ /**
+ * Music variant to use.
+ * TODO: De-hardcode this somehow.
+ */
+ var musicVariant:String = "";
public function new()
{
- gameOverMusic = new FlxSound();
- FlxG.sound.list.add(gameOverMusic);
-
- var daStage = PlayState.instance.currentStageId;
- var daBf:String = '';
- switch (daStage)
- {
- case 'school' | 'schoolEvil':
- stageSuffix = '-pixel';
- daBf = 'bf-pixel-dead';
- default:
- daBf = 'bf';
- }
-
- var daSong = PlayState.currentSong.song.toLowerCase();
-
- switch (daSong)
- {
- case 'stress':
- daBf = 'bf-holding-gf-dead';
- }
-
super();
+ FlxG.sound.list.add(gameOverMusic);
+ gameOverMusic.stop();
+
Conductor.songPosition = 0;
- var bfXPos = PlayState.instance.currentStage.getBoyfriend().getScreenPosition().x;
- var bfYPos = PlayState.instance.currentStage.getBoyfriend().getScreenPosition().y;
- bf = new Boyfriend(bfXPos, bfYPos, daBf);
- add(bf);
+ playBlueBalledSFX();
- camFollow = new FlxObject(bf.getGraphicMidpoint().x, bf.getGraphicMidpoint().y, 1, 1);
- add(camFollow);
-
- FlxG.sound.play(Paths.sound('fnf_loss_sfx' + stageSuffix));
- // Conductor.bpm = 100;
-
- switch (PlayState.currentSong.player1)
+ switch (PlayState.instance.currentStageId)
{
- case 'pico':
- stageSuffix = 'Pico';
+ case 'school' | 'schoolEvil':
+ musicVariant = "-pixel";
+ default:
+ if (PlayState.instance.currentStage.getBoyfriend().characterId == 'pico')
+ {
+ musicVariant = "Pico";
+ }
+ else
+ {
+ musicVariant = "";
+ }
}
- // FlxG.camera.followLerp = 1;
- // FlxG.camera.focusOn(FlxPoint.get(FlxG.width / 2, FlxG.height / 2));
+ // We have to remove boyfriend from the stage. Then we can add him back at the end.
+ boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
+ boyfriend.isDead = true;
+ boyfriend.playAnimation('firstDeath');
+ add(boyfriend);
- // commented out for now
- FlxG.camera.scroll.set();
+ cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
+ add(cameraFollowPoint);
+
+ // FlxG.camera.scroll.set();
FlxG.camera.target = null;
-
- bf.playAnim('firstDeath');
-
- var randomCensor:Array = [];
-
- if (PreferencesMenu.getPref('censor-naughty'))
- randomCensor = [1, 3, 8, 13, 17, 21];
-
- randomGameover = FlxG.random.int(1, 25, randomCensor);
+ FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
}
- var playingDeathSound:Bool = false;
-
override function update(elapsed:Float)
{
// makes the lerp non-dependant on the framerate
@@ -93,14 +99,14 @@ class GameOverSubstate extends MusicBeatSubstate
var touch = FlxG.touches.getFirst();
if (touch != null)
{
- if (touch.overlaps(bf))
- endBullshit();
+ if (touch.overlaps(boyfriend))
+ confirmDeath();
}
}
if (controls.ACCEPT)
{
- endBullshit();
+ confirmDeath();
}
if (controls.BACK)
@@ -116,74 +122,129 @@ class GameOverSubstate extends MusicBeatSubstate
FlxG.switchState(new FreeplayState());
}
- if (bf.animation.curAnim.name == 'firstDeath' && bf.animation.curAnim.curFrame == 12)
+ // Start panning the camera to BF after 12 frames.
+ // TODO: Should this be de-hardcoded?
+ if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.animation.curAnim.curFrame == 12)
{
- FlxG.camera.follow(camFollow, LOCKON, 0.01);
- }
-
- switch (PlayState.storyWeek)
- {
- case 7:
- if (bf.animation.curAnim.name == 'firstDeath' && bf.animation.curAnim.finished && !playingDeathSound)
- {
- playingDeathSound = true;
-
- bf.startedDeath = true;
- coolStartDeath(0.2);
-
- FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + randomGameover), 1, false, null, true, function()
- {
- if (!isEnding)
- {
- gameOverMusic.fadeIn(4, 0.2, 1);
- }
- // FlxG.sound.music.fadeIn(4, 0.2, 1);
- });
- }
- default:
- if (bf.animation.curAnim.name == 'firstDeath' && bf.animation.curAnim.finished)
- {
- bf.startedDeath = true;
- coolStartDeath();
- }
+ cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
+ cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
}
if (gameOverMusic.playing)
{
Conductor.songPosition = gameOverMusic.time;
}
+ else
+ {
+ switch (PlayState.storyWeek)
+ {
+ case 7:
+ if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
+ {
+ playingJeffQuote = true;
+ playJeffQuote();
+
+ startDeathMusic(0.2);
+ }
+ default:
+ if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
+ {
+ startDeathMusic();
+ }
+ }
+ }
+
+ dispatchEvent(new UpdateScriptEvent(elapsed));
}
- private function coolStartDeath(?vol:Float = 1):Void
+ override function dispatchEvent(event:ScriptEvent)
+ {
+ super.dispatchEvent(event);
+
+ ScriptEventDispatcher.callEvent(boyfriend, event);
+ }
+
+ /**
+ * Starts the death music at the appropriate volume.
+ * @param startingVolume
+ */
+ function startDeathMusic(?startingVolume:Float = 1):Void
{
if (!isEnding)
{
- gameOverMusic.loadEmbedded(Paths.music('gameOver' + stageSuffix));
- gameOverMusic.volume = vol;
+ gameOverMusic.loadEmbedded(Paths.music('gameOver' + musicVariant));
+ gameOverMusic.volume = startingVolume;
+ gameOverMusic.play();
+ }
+ else
+ {
+ gameOverMusic.loadEmbedded(Paths.music('gameOverEnd' + musicVariant));
+ gameOverMusic.volume = startingVolume;
gameOverMusic.play();
}
- // FlxG.sound.playMusic();
}
- var isEnding:Bool = false;
+ /**
+ * Play the sound effect that occurs when
+ * boyfriend's testicles get utterly annihilated.
+ */
+ function playBlueBalledSFX()
+ {
+ FlxG.sound.play(Paths.sound('fnf_loss_sfx' + musicVariant));
+ }
- function endBullshit():Void
+ var playingJeffQuote:Bool = false;
+
+ /**
+ * Week 7-specific hardcoded behavior, to play a custom death quote.
+ * TODO: Make this a module somehow.
+ */
+ function playJeffQuote()
+ {
+ var randomCensor:Array = [];
+
+ if (PreferencesMenu.getPref('censor-naughty'))
+ randomCensor = [1, 3, 8, 13, 17, 21];
+
+ FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), 1, false, null, true, function()
+ {
+ // Once the quote ends, fade in the game over music.
+ if (!isEnding && gameOverMusic != null)
+ {
+ gameOverMusic.fadeIn(4, 0.2, 1);
+ }
+ });
+ }
+
+ /**
+ * Do behavior which occurs when you confirm and move to restart the level.
+ */
+ function confirmDeath():Void
{
if (!isEnding)
{
isEnding = true;
- bf.playAnim('deathConfirm', true);
- gameOverMusic.stop();
- // FlxG.sound.music.stop();
- FlxG.sound.play(Paths.music('gameOverEnd' + stageSuffix));
+ startDeathMusic(); // isEnding changes this function's behavior.
+
+ boyfriend.playAnimation('deathConfirm', true);
+
+ // After the animation finishes...
new FlxTimer().start(0.7, function(tmr:FlxTimer)
{
+ // ...fade out the graphics. Then after that happens...
FlxG.camera.fade(FlxColor.BLACK, 2, false, function()
{
+ // ...close the GameOverSubstate.
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.needsReset = true;
+
+ // Readd Boyfriend to the stage.
+ boyfriend.isDead = false;
+ remove(boyfriend);
+ PlayState.instance.currentStage.addCharacter(boyfriend, BF);
+
+ // Close the substate.
close();
- // LoadingState.loadAndSwitchState(new PlayState());
});
});
}
diff --git a/source/funkin/HealthIcon.hx b/source/funkin/HealthIcon.hx
deleted file mode 100644
index 617af2b74..000000000
--- a/source/funkin/HealthIcon.hx
+++ /dev/null
@@ -1,88 +0,0 @@
-package funkin;
-
-import flixel.FlxSprite;
-import openfl.utils.Assets;
-import funkin.play.PlayState;
-
-using StringTools;
-
-class HealthIcon extends FlxSprite
-{
- /**
- * Used for FreeplayState! If you use it elsewhere, prob gonna annoying
- */
- public var sprTracker:FlxSprite;
-
- public var char:String = '';
-
- var isPlayer:Bool = false;
-
- public function new(char:String = 'bf', isPlayer:Bool = false)
- {
- super();
-
- this.isPlayer = isPlayer;
-
- antialiasing = true;
- changeIcon(char);
- scrollFactor.set();
- }
-
- public var isOldIcon:Bool = false;
-
- public function swapOldIcon():Void
- {
- isOldIcon = !isOldIcon;
-
- if (isOldIcon)
- changeIcon('bf-old');
- else
- changeIcon(PlayState.currentSong.player1);
- }
-
- var pixelArrayFunny:Array = CoolUtil.coolTextFile(Paths.file('images/icons/pixelIcons.txt'));
-
- public function changeIcon(newChar:String):Void
- {
- if (newChar != 'bf-pixel' && newChar != 'bf-old')
- newChar = newChar.split('-')[0].trim();
-
- if (!Assets.exists(Paths.image('icons/icon-' + newChar)))
- {
- FlxG.log.warn('No icon with data: $newChar : using default placeholder face instead!');
- newChar = "face";
- }
-
- if (newChar != char)
- {
- if (animation.getByName(newChar) == null)
- {
- var imgSize:Int = 150;
-
- if (newChar.endsWith('pixel') || pixelArrayFunny.contains(newChar))
- imgSize = 32;
-
- loadGraphic(Paths.image('icons/icon-' + newChar), true, imgSize, imgSize);
-
- animation.add(newChar, [0, 1], 0, false, isPlayer);
- }
- animation.play(newChar);
- char = newChar;
-
- if (newChar.endsWith('pixel') || pixelArrayFunny.contains(newChar))
- antialiasing = false;
- else
- antialiasing = true;
- setGraphicSize(150);
- updateHitbox();
- }
- }
-
- override function update(elapsed:Float)
- {
- super.update(elapsed);
-
- if (sprTracker != null)
- setPosition(sprTracker.x + sprTracker.width + 10, sprTracker.y - 30);
- }
-}
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index cb860a2ff..bfde8c327 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -8,11 +8,10 @@ import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.util.FlxColor;
import funkin.charting.ChartingState;
-import funkin.charting.ChartingState;
import funkin.modding.module.ModuleHandler;
import funkin.play.PicoFight;
import funkin.play.PlayState;
-import funkin.play.stage.StageData;
+import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.stage.StageData;
import funkin.ui.PreferencesMenu;
import funkin.ui.animDebugShit.DebugBoundingState;
@@ -125,7 +124,8 @@ class InitState extends FlxTransitionableState
FlxTransitionableState.skipNextTransIn = true;
StageDataParser.loadStageCache();
-
+ CharacterDataParser.loadCharacterCache();
+ ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
#if song
@@ -179,7 +179,11 @@ class InitState extends FlxTransitionableState
#elseif FIGHT
FlxG.switchState(new PicoFight());
#elseif ANIMDEBUG
+<<<<<<< HEAD
+ FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
+=======
FlxG.switchState(new DebugBoundingState());
+>>>>>>> origin/feature/scripted-modules
#elseif NETTEST
FlxG.switchState(new netTest.NetTest());
#else
diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx
index 8155caf3d..ee84e14c8 100644
--- a/source/funkin/MainMenuState.hx
+++ b/source/funkin/MainMenuState.hx
@@ -19,11 +19,13 @@ import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.module.ModuleHandler;
import funkin.shaderslmfao.ScreenWipeShader;
import funkin.ui.AtlasMenuList;
+import funkin.ui.MenuList.MenuItem;
import funkin.ui.MenuList;
import funkin.ui.OptionsState;
import funkin.ui.PreferencesMenu;
import funkin.ui.Prompt;
import funkin.util.Constants;
+import funkin.util.WindowUtil;
import lime.app.Application;
import openfl.filters.ShaderFilter;
@@ -39,7 +41,7 @@ import io.newgrounds.NG;
class MainMenuState extends MusicBeatState
{
- var menuItems:MainMenuList;
+ var menuItems:MenuTypedList;
var magenta:FlxSprite;
var camFollow:FlxObject;
@@ -87,7 +89,7 @@ class MainMenuState extends MusicBeatState
add(magenta);
// magenta.scrollFactor.set();
- menuItems = new MainMenuList();
+ menuItems = new MenuTypedList();
add(menuItems);
menuItems.onChange.add(onMenuItemChange);
menuItems.onAcceptPress.add(function(_)
@@ -103,31 +105,32 @@ class MainMenuState extends MusicBeatState
});
menuItems.enabled = true; // can move on intro
- menuItems.createItem('story mode', function() startExitState(new StoryMenuState()));
- menuItems.createItem('freeplay', function()
+ createMenuItem('storymode', 'mainmenu/storymode', function() startExitState(new StoryMenuState()));
+ createMenuItem('freeplay', 'mainmenu/freeplay', function()
{
persistentDraw = true;
persistentUpdate = false;
openSubState(new FreeplayState());
});
- // addMenuItem('options', function () startExitState(new OptionMenu()));
#if CAN_OPEN_LINKS
var hasPopupBlocker = #if web true #else false #end;
if (VideoState.seenVideo)
- menuItems.createItem('kickstarter', selectDonate, hasPopupBlocker);
+ {
+ createMenuItem('kickstarter', 'mainmenu/kickstarter', selectDonate, hasPopupBlocker);
+ }
else
- menuItems.createItem('donate', selectDonate, hasPopupBlocker);
+ {
+ createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker);
+ }
#end
- menuItems.createItem('options', function() startExitState(new OptionsState()));
- // #if newgrounds
- // if (NGio.isLoggedIn)
- // menuItems.createItem("logout", selectLogout);
- // else
- // menuItems.createItem("login", selectLogin);
- // #end
- // center vertically
+ createMenuItem('options', 'mainmenu/options', function()
+ {
+ startExitState(new OptionsState());
+ });
+
+ // Reset position of menu items.
var spacing = 160;
var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2;
for (i in 0...menuItems.length)
@@ -145,19 +148,26 @@ class MainMenuState extends MusicBeatState
// This has to come AFTER!
this.leftWatermarkText.text = Constants.VERSION;
- this.rightWatermarkText.text = "blablabla test";
-
- // var versionStr = 'v${Application.current.meta.get('version')}';
- // versionStr += ' (secret week 8 build do not leak)';
- //
- // var versionShit:FlxText = new FlxText(5, FlxG.height - 18, 0, versionStr, 12);
- // versionShit.scrollFactor.set();
- // versionShit.setFormat("VCR OSD Mono", 16, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
- // add(versionShit);
+ // this.rightWatermarkText.text = "blablabla test";
// NG.core.calls.event.logEvent('swag').send();
}
+ function createMenuItem(name:String, atlas:String, callback:Void->Void, fireInstantly:Bool = false):Void
+ {
+ var item = new AtlasMenuItem(name, Paths.getSparrowAtlas(atlas), callback);
+ item.fireInstantly = fireInstantly;
+ item.ID = menuItems.length;
+
+ item.scrollFactor.set();
+
+ // Set the offset of the item so the sprite is centered on the origin.
+ item.centered = true;
+ item.changeAnim('idle');
+
+ menuItems.addItem(name, item);
+ }
+
override function closeSubState()
{
magenta.visible = false;
@@ -185,17 +195,7 @@ class MainMenuState extends MusicBeatState
#if CAN_OPEN_LINKS
function selectDonate()
{
- #if linux
- // Sys.command('/usr/bin/xdg-open', ["https://ninja-muffin24.itch.io/funkin", "&"]);
- Sys.command('/usr/bin/xdg-open', [
- "https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/",
- "&"
- ]);
- #else
- // FlxG.openURL('https://ninja-muffin24.itch.io/funkin');
-
- FlxG.openURL('https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/');
- #end
+ WindowUtil.openURL(Constants.URL_KICKSTARTER);
}
#end
@@ -317,46 +317,3 @@ class MainMenuState extends MusicBeatState
}
}
}
-
-private class MainMenuList extends MenuTypedList
-{
- public var atlas:FlxAtlasFrames;
-
- public function new()
- {
- atlas = Paths.getSparrowAtlas('main_menu');
- super(Vertical);
- }
-
- public function createItem(x = 0.0, y = 0.0, name:String, callback, fireInstantly = false)
- {
- var item = new MainMenuItem(x, y, name, atlas, callback);
- item.fireInstantly = fireInstantly;
- item.ID = length;
-
- return addItem(name, item);
- }
-
- override function destroy()
- {
- super.destroy();
- atlas = null;
- }
-}
-
-private class MainMenuItem extends AtlasMenuItem
-{
- public function new(x = 0.0, y = 0.0, name, atlas, callback)
- {
- super(x, y, name, atlas, callback);
- scrollFactor.set();
- }
-
- override function changeAnim(anim:String)
- {
- super.changeAnim(anim);
- // position by center
- centerOrigin();
- offset.copyFrom(origin);
- }
-}
diff --git a/source/funkin/MusicBeatState.hx b/source/funkin/MusicBeatState.hx
index 0fca6fb84..5e86ea022 100644
--- a/source/funkin/MusicBeatState.hx
+++ b/source/funkin/MusicBeatState.hx
@@ -10,6 +10,8 @@ import funkin.Conductor.BPMChangeEvent;
import funkin.modding.PolymodHandler;
import funkin.modding.events.ScriptEvent;
import funkin.modding.module.ModuleHandler;
+import funkin.play.character.CharacterData.CharacterDataParser;
+import funkin.play.stage.StageData.StageDataParser;
import funkin.util.SortUtil;
/**
diff --git a/source/funkin/Note.hx b/source/funkin/Note.hx
index 98fdc2165..566b4ea49 100644
--- a/source/funkin/Note.hx
+++ b/source/funkin/Note.hx
@@ -1,11 +1,12 @@
package funkin;
-import funkin.util.Constants;
import flixel.FlxSprite;
import flixel.math.FlxMath;
+import funkin.play.PlayState;
+import funkin.play.Strumline.StrumlineStyle;
import funkin.shaderslmfao.ColorSwap;
import funkin.ui.PreferencesMenu;
-import funkin.play.PlayState;
+import funkin.util.Constants;
using StringTools;
@@ -93,7 +94,10 @@ class Note extends FlxSprite
// anything below sick threshold is sick
public static var arrowColors:Array = [1, 1, 1, 1];
- public function new(strumTime:Float = 0, noteData:NoteType, ?prevNote:Note, ?sustainNote:Bool = false)
+ // Which note asset to load?
+ public var style:StrumlineStyle = NORMAL;
+
+ public function new(strumTime:Float = 0, noteData:NoteType, ?prevNote:Note, ?sustainNote:Bool = false, ?style:StrumlineStyle = NORMAL)
{
super();
@@ -110,10 +114,15 @@ class Note extends FlxSprite
data.noteData = noteData;
+ this.style = style;
+
+ if (this.style == null)
+ this.style = StrumlineStyle.NORMAL;
+
// TODO: Make this logic more generic
- switch (PlayState.instance.currentStageId)
+ switch (this.style)
{
- case 'school' | 'schoolEvil':
+ case PIXEL:
loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17);
animation.add('greenScroll', [6]);
@@ -227,6 +236,7 @@ class Note extends FlxSprite
{
super.update(elapsed);
+ // mustPress indicates the player is the one pressing the key
if (mustPress)
{
// miss on the NEXT frame so lag doesnt make u miss notes
@@ -244,7 +254,8 @@ class Note extends FlxSprite
}
if (data.strumTime > Conductor.songPosition - HIT_WINDOW)
- { // * 0.5 if sustain note, so u have to keep holding it closer to all the way thru!
+ {
+ // * 0.5 if sustain note, so u have to keep holding it closer to all the way thru!
if (data.strumTime < Conductor.songPosition + (HIT_WINDOW * (isSustainNote ? 0.5 : 1)))
canBeHit = true;
}
@@ -281,14 +292,14 @@ typedef RawNoteData =
var strumTime:Float;
var noteData:NoteType;
var sustainLength:Float;
- var altNote:Bool;
+ var altNote:String;
var noteKind:NoteKind;
}
@:forward
abstract NoteData(RawNoteData)
{
- public function new(strumTime = 0.0, noteData:NoteType = 0, sustainLength = 0.0, altNote = false, noteKind = NORMAL)
+ public function new(strumTime = 0.0, noteData:NoteType = 0, sustainLength = 0.0, altNote = "", noteKind = NORMAL)
{
this = {
strumTime: strumTime,
@@ -455,7 +466,12 @@ enum abstract NoteColor(NoteType) from Int to Int from NoteType
enum abstract NoteKind(String) from String to String
{
+ /**
+ * The default note type.
+ */
var NORMAL = "normal";
+
+ // Testing shiz
var PYRO_LIGHT = "pyro_light";
var PYRO_KICK = "pyro_kick";
var PYRO_TOSS = "pyro_toss";
diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx
index 03a90ff9a..cf127df90 100644
--- a/source/funkin/Paths.hx
+++ b/source/funkin/Paths.hx
@@ -107,7 +107,7 @@ class Paths
return 'assets/fonts/$key';
}
- inline static public function getSparrowAtlas(key:String, ?library:String)
+ static public function getSparrowAtlas(key:String, ?library:String)
{
return FlxAtlasFrames.fromSparrow(image(key, library), file('images/$key.xml', library));
}
diff --git a/source/funkin/SongLoad.hx b/source/funkin/SongLoad.hx
index 770795602..f786d96f5 100644
--- a/source/funkin/SongLoad.hx
+++ b/source/funkin/SongLoad.hx
@@ -189,8 +189,11 @@ class SongLoad
noteStuff[sectionIndex].sectionNotes[noteIndex].strumTime = arrayDipshit[0];
noteStuff[sectionIndex].sectionNotes[noteIndex].noteData = arrayDipshit[1];
noteStuff[sectionIndex].sectionNotes[noteIndex].sustainLength = arrayDipshit[2];
- noteStuff[sectionIndex].sectionNotes[noteIndex].altNote = arrayDipshit[3];
- if (arrayDipshit.length >= 5)
+ if (arrayDipshit.length > 3)
+ {
+ noteStuff[sectionIndex].sectionNotes[noteIndex].altNote = arrayDipshit[3];
+ }
+ if (arrayDipshit.length > 4)
{
noteStuff[sectionIndex].sectionNotes[noteIndex].noteKind = arrayDipshit[4];
}
diff --git a/source/funkin/StoryMenuState.hx b/source/funkin/StoryMenuState.hx
index dff3fce14..868e62d40 100644
--- a/source/funkin/StoryMenuState.hx
+++ b/source/funkin/StoryMenuState.hx
@@ -34,6 +34,8 @@ class StoryMenuState extends MusicBeatState
];
var curDifficulty:Int = 1;
+ // TODO: This info is just hardcoded right now.
+ // We should probably make it so that weeks must be completed in order to unlock the next week.
public static var weekUnlocked:Array = [true, true, true, true, true, true, true, true];
var weekCharacters:Array = [
@@ -44,7 +46,7 @@ class StoryMenuState extends MusicBeatState
['mom', 'bf', 'gf'],
['parents-christmas', 'bf', 'gf'],
['senpai', 'bf', 'gf'],
- ['tankman', 'bf', 'gf']
+ ['tankman', 'bf', 'gf'],
];
var weekNames:Array = [
@@ -114,8 +116,6 @@ class StoryMenuState extends MusicBeatState
grpLocks = new FlxTypedGroup();
add(grpLocks);
- trace("Line 70");
-
#if discord_rpc
// Updating Discord Rich Presence
DiscordClient.changePresence("In the Menus", null);
@@ -145,8 +145,6 @@ class StoryMenuState extends MusicBeatState
}
}
- trace("Line 96");
-
for (char in 0...3)
{
var weekCharacterThing:MenuCharacter = new MenuCharacter((FlxG.width * 0.25) * (1 + char) - 150, weekCharacters[curWeek][char]);
@@ -178,8 +176,6 @@ class StoryMenuState extends MusicBeatState
difficultySelectors = new FlxGroup();
add(difficultySelectors);
- trace("Line 124");
-
leftArrow = new FlxSprite(grpWeekText.members[0].x + grpWeekText.members[0].width + 10, grpWeekText.members[0].y + 10);
leftArrow.frames = ui_tex;
leftArrow.animation.addByPrefix('idle', "arrow left");
@@ -204,8 +200,6 @@ class StoryMenuState extends MusicBeatState
rightArrow.animation.play('idle');
difficultySelectors.add(rightArrow);
- trace("Line 150");
-
add(yellowBG);
add(grpWeekCharacters);
@@ -220,8 +214,6 @@ class StoryMenuState extends MusicBeatState
updateText();
- trace("Line 165");
-
super.create();
}
diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx
index 4df29e1f3..cf8c6ea80 100644
--- a/source/funkin/TitleState.hx
+++ b/source/funkin/TitleState.hx
@@ -515,7 +515,7 @@ class TitleState extends MusicBeatState
lime.ui.Haptic.vibrate(100, 100);
- var coolText:AtlasText = new AtlasText(0, 0, text, AtlasFont.BOLD);
+ var coolText:AtlasText = new AtlasText(0, 0, text.trim(), AtlasFont.BOLD);
coolText.screenCenter(X);
coolText.y += (textGroup.length * 60) + 200;
textGroup.add(coolText);
@@ -554,7 +554,7 @@ class TitleState extends MusicBeatState
switch (i + 1)
{
case 1:
- createCoolText(['ninjamuffin99', 'phantomArcade', 'kawaisprite', 'evilsk8er']);
+ createCoolText(['ninjamuffin99', 'phantomArcade', 'kawaisprite', 'evilsk8r']);
case 3:
addMoreText('present');
case 4:
diff --git a/source/funkin/api/newgrounds/NGUnsafe.hx b/source/funkin/api/newgrounds/NGUnsafe.hx
new file mode 100644
index 000000000..2995988a9
--- /dev/null
+++ b/source/funkin/api/newgrounds/NGUnsafe.hx
@@ -0,0 +1,78 @@
+package funkin.api.newgrounds;
+
+import flixel.util.FlxSignal;
+import flixel.util.FlxTimer;
+import lime.app.Application;
+import openfl.display.Stage;
+#if newgrounds
+import io.newgrounds.NG;
+import io.newgrounds.NGLite;
+import io.newgrounds.components.ScoreBoardComponent.Period;
+import io.newgrounds.objects.Error;
+import io.newgrounds.objects.Medal;
+import io.newgrounds.objects.Score;
+import io.newgrounds.objects.ScoreBoard;
+import io.newgrounds.objects.events.Response;
+import io.newgrounds.objects.events.Result.GetCurrentVersionResult;
+import io.newgrounds.objects.events.Result.GetVersionResult;
+#end
+
+using StringTools;
+
+/**
+ * Contains any script functions which should be BLOCKED from use by modded scripts.
+ */
+class NGUnsafe
+{
+ static public function logEvent(event:String)
+ {
+ #if newgrounds
+ NG.core.calls.event.logEvent(event).send();
+ trace('should have logged: ' + event);
+ #else
+ #if debug
+ trace('event:$event - not logged, missing NG.io lib');
+ #end
+ #end
+ }
+
+ static public function unlockMedal(id:Int)
+ {
+ #if newgrounds
+ if (isLoggedIn)
+ {
+ var medal = NG.core.medals.get(id);
+ if (!medal.unlocked)
+ medal.sendUnlock();
+ }
+ #else
+ #if debug
+ trace('medal:$id - not unlocked, missing NG.io lib');
+ #end
+ #end
+ }
+
+ static public function postScore(score:Int = 0, song:String)
+ {
+ #if newgrounds
+ if (isLoggedIn)
+ {
+ for (id in NG.core.scoreBoards.keys())
+ {
+ var board = NG.core.scoreBoards.get(id);
+
+ if (song == board.name)
+ {
+ board.postScore(score, "Uhh meow?");
+ }
+
+ // trace('loaded scoreboard id:$id, name:${board.name}');
+ }
+ }
+ #else
+ #if debug
+ trace('Song:$song, Score:$score - not posted, missing NG.io lib');
+ #end
+ #end
+ }
+}
diff --git a/source/funkin/api/newgrounds/NGUtil.hx b/source/funkin/api/newgrounds/NGUtil.hx
new file mode 100644
index 000000000..8ec06b27f
--- /dev/null
+++ b/source/funkin/api/newgrounds/NGUtil.hx
@@ -0,0 +1,260 @@
+package funkin.api.newgrounds;
+
+import flixel.util.FlxSignal;
+import flixel.util.FlxTimer;
+import lime.app.Application;
+import openfl.display.Stage;
+#if newgrounds
+import io.newgrounds.NG;
+import io.newgrounds.NGLite;
+import io.newgrounds.components.ScoreBoardComponent.Period;
+import io.newgrounds.objects.Error;
+import io.newgrounds.objects.Medal;
+import io.newgrounds.objects.Score;
+import io.newgrounds.objects.ScoreBoard;
+import io.newgrounds.objects.events.Response;
+import io.newgrounds.objects.events.Result.GetCurrentVersionResult;
+import io.newgrounds.objects.events.Result.GetVersionResult;
+#end
+
+using StringTools;
+
+/**
+ * Contains any script functions which should be ALLOWD for use by modded scripts.
+ */
+class NGUtil
+{
+ #if newgrounds
+ /**
+ * True, if the saved sessionId was used in the initial login, and failed to connect.
+ * Used in MainMenuState to show a popup to establish a new connection
+ */
+ public static var savedSessionFailed(default, null):Bool = false;
+
+ public static var scoreboardsLoaded:Bool = false;
+ public static var isLoggedIn(get, never):Bool;
+
+ inline static function get_isLoggedIn()
+ {
+ return NG.core != null && NG.core.loggedIn;
+ }
+
+ public static var scoreboardArray:Array = [];
+
+ public static var ngDataLoaded(default, null):FlxSignal = new FlxSignal();
+ public static var ngScoresLoaded(default, null):FlxSignal = new FlxSignal();
+
+ public static var GAME_VER:String = "";
+
+ static public function checkVersion(callback:String->Void)
+ {
+ trace('checking NG.io version');
+ GAME_VER = "v" + Application.current.meta.get('version');
+
+ 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);
+ }).send();
+ }
+
+ static public function init()
+ {
+ var api = APIStuff.API;
+ if (api == null || api.length == 0)
+ {
+ trace("Missing Newgrounds API key, aborting connection");
+ return;
+ }
+ trace("connecting to newgrounds");
+
+ #if NG_FORCE_EXPIRED_SESSION
+ var sessionId:String = "fake_session_id";
+ function onSessionFail(error:Error)
+ {
+ trace("Forcing an expired saved session. " + "To disable, comment out NG_FORCE_EXPIRED_SESSION in Project.xml");
+ savedSessionFailed = true;
+ }
+ #else
+ var sessionId:String = NGLite.getSessionId();
+ if (sessionId != null)
+ trace("found web session id");
+
+ #if (debug)
+ if (sessionId == null && APIStuff.SESSION != null)
+ {
+ trace("using debug session id");
+ sessionId = APIStuff.SESSION;
+ }
+ #end
+
+ var onSessionFail:Error->Void = null;
+ if (sessionId == null && FlxG.save.data.sessionId != null)
+ {
+ trace("using stored session id");
+ sessionId = FlxG.save.data.sessionId;
+ onSessionFail = function(error) savedSessionFailed = true;
+ }
+ #end
+
+ NG.create(api, sessionId, #if NG_DEBUG true #else false #end, onSessionFail);
+
+ #if NG_VERBOSE
+ NG.core.verbose = true;
+ #end
+ // Set the encryption cipher/format to RC4/Base64. AES128 and Hex are not implemented yet
+ NG.core.initEncryption(APIStuff.EncKey); // Found in you NG project view
+
+ if (NG.core.attemptingLogin)
+ {
+ /* a session_id was found in the loadervars, this means the user is playing on newgrounds.com
+ * and we should login shortly. lets wait for that to happen
+ */
+ trace("attempting login");
+ NG.core.onLogin.add(onNGLogin);
+ }
+ // GK: taking out auto login, adding a login button to the main menu
+ // else
+ // {
+ // /* They are NOT playing on newgrounds.com, no session id was found. We must start one manually, if we want to.
+ // * Note: This will cause a new browser window to pop up where they can log in to newgrounds
+ // */
+ // NG.core.requestLogin(onNGLogin);
+ // }
+ }
+
+ /**
+ * Attempts to log in to newgrounds by requesting a new session ID, only call if no session ID was found automatically
+ * @param popupLauncher The function to call to open the login url, must be inside
+ * a user input event or the popup blocker will block it.
+ * @param onComplete A callback with the result of the connection.
+ */
+ static public function login(?popupLauncher:(Void->Void)->Void, onComplete:ConnectionResult->Void)
+ {
+ trace("Logging in manually");
+ var onPending:Void->Void = null;
+ if (popupLauncher != null)
+ {
+ onPending = function() popupLauncher(NG.core.openPassportUrl);
+ }
+
+ var onSuccess:Void->Void = onNGLogin;
+ var onFail:Error->Void = null;
+ var onCancel:Void->Void = null;
+ if (onComplete != null)
+ {
+ onSuccess = function()
+ {
+ onNGLogin();
+ onComplete(Success);
+ }
+ onFail = function(e) onComplete(Fail(e.message));
+ onCancel = function() onComplete(Cancelled);
+ }
+
+ NG.core.requestLogin(onSuccess, onPending, onFail, onCancel);
+ }
+
+ inline static public function cancelLogin():Void
+ {
+ NG.core.cancelLoginRequest();
+ }
+
+ static function onNGLogin():Void
+ {
+ trace('logged in! user:${NG.core.user.name}');
+ FlxG.save.data.sessionId = NG.core.sessionId;
+ FlxG.save.flush();
+ // Load medals then call onNGMedalFetch()
+ NG.core.requestMedals(onNGMedalFetch);
+
+ // Load Scoreboards hten call onNGBoardsFetch()
+ NG.core.requestScoreBoards(onNGBoardsFetch);
+
+ ngDataLoaded.dispatch();
+ }
+
+ static public function logout()
+ {
+ NG.core.logOut();
+
+ FlxG.save.data.sessionId = null;
+ FlxG.save.flush();
+ }
+
+ // --- MEDALS
+ static function onNGMedalFetch():Void
+ {
+ /*
+ // Reading medal info
+ for (id in NG.core.medals.keys())
+ {
+ var medal = NG.core.medals.get(id);
+ trace('loaded medal id:$id, name:${medal.name}, description:${medal.description}');
+ }
+
+ // Unlocking medals
+ var unlockingMedal = NG.core.medals.get(54352);// medal ids are listed in your NG project viewer
+ if (!unlockingMedal.unlocked)
+ unlockingMedal.sendUnlock();
+ */
+ }
+
+ // --- SCOREBOARDS
+ static function onNGBoardsFetch():Void
+ {
+ /*
+ // Reading medal info
+ for (id in NG.core.scoreBoards.keys())
+ {
+ var board = NG.core.scoreBoards.get(id);
+ trace('loaded scoreboard id:$id, name:${board.name}');
+ }
+ */
+ // var board = NG.core.scoreBoards.get(8004);// ID found in NG project view
+
+ // Posting a score thats OVER 9000!
+ // board.postScore(FlxG.random.int(0, 1000));
+
+ // --- To view the scores you first need to select the range of scores you want to see ---
+
+ // add an update listener so we know when we get the new scores
+ // board.onUpdate.add(onNGScoresFetch);
+ trace("shoulda got score by NOW!");
+ // board.requestScores(20);// get the best 10 scores ever logged
+ // more info on scores --- http://www.newgrounds.io/help/components/#scoreboard-getscores
+ }
+
+ static function onNGScoresFetch():Void
+ {
+ scoreboardsLoaded = true;
+
+ ngScoresLoaded.dispatch();
+ /*
+ for (score in NG.core.scoreBoards.get(8737).scores)
+ {
+ trace('score loaded user:${score.user.name}, score:${score.formatted_value}');
+
+ }
+ */
+
+ // var board = NG.core.scoreBoards.get(8004);// ID found in NG project view
+ // board.postScore(HighScore.score);
+
+ // NGUtil.scoreboardArray = NG.core.scoreBoards.get(8004).scores;
+ }
+ #end
+}
+
+enum ConnectionResult
+{
+ /** Log in successful */
+ Success;
+
+ /** Could not login */
+ Fail(msg:String);
+
+ /** User cancelled the login */
+ Cancelled;
+}
diff --git a/source/funkin/api/newgrounds/NgPrompt.hx b/source/funkin/api/newgrounds/NgPrompt.hx
new file mode 100644
index 000000000..5f08bf443
--- /dev/null
+++ b/source/funkin/api/newgrounds/NgPrompt.hx
@@ -0,0 +1,104 @@
+package funkin.api.newgrounds;
+
+#if newgrounds
+import funkin.NGio;
+import funkin.ui.Prompt;
+
+class NgPrompt extends Prompt
+{
+ public function new(text:String, style:ButtonStyle = Yes_No)
+ {
+ super(text, style);
+ }
+
+ static public function showLogin()
+ {
+ return showLoginPrompt(true);
+ }
+
+ static public function showSavedSessionFailed()
+ {
+ return showLoginPrompt(false);
+ }
+
+ static function showLoginPrompt(fromUi:Bool)
+ {
+ var prompt = new NgPrompt("Talking to server...", None);
+ prompt.openCallback = NGUtil.login.bind(function popupLauncher(openPassportUrl)
+ {
+ var choiceMsg = fromUi ? #if web "Log in to Newgrounds?" #else null #end // User-input needed to allow popups
+ : "Your session has expired.\n Please login again.";
+
+ if (choiceMsg != null)
+ {
+ prompt.setText(choiceMsg);
+ prompt.setButtons(Yes_No);
+ #if web
+ prompt.buttons.getItem("yes").fireInstantly = true;
+ #end
+ prompt.onYes = function()
+ {
+ prompt.setText("Connecting..." #if web + "\n(check your popup blocker)" #end);
+ prompt.setButtons(None);
+ openPassportUrl();
+ };
+ prompt.onNo = function()
+ {
+ prompt.close();
+ prompt = null;
+ NGio.cancelLogin();
+ };
+ }
+ else
+ {
+ prompt.setText("Connecting...");
+ openPassportUrl();
+ }
+ }, function onLoginComplete(result:ConnectionResult)
+ {
+ switch (result)
+ {
+ case Success:
+ {
+ prompt.setText("Login Successful");
+ prompt.setButtons(Ok);
+ prompt.onYes = prompt.close;
+ }
+ case Fail(msg):
+ {
+ trace("Login Error:" + msg);
+ prompt.setText("Login failed");
+ prompt.setButtons(Ok);
+ prompt.onYes = prompt.close;
+ }
+ case Cancelled:
+ {
+ if (prompt != null)
+ {
+ prompt.setText("Login cancelled by user");
+ prompt.setButtons(Ok);
+ prompt.onYes = prompt.close;
+ }
+ else
+ trace("Login cancelled via prompt");
+ }
+ }
+ });
+
+ return prompt;
+ }
+
+ static public function showLogout()
+ {
+ var user = io.newgrounds.NG.core.user.name;
+ var prompt = new NgPrompt('Log out of $user?', Yes_No);
+ prompt.onYes = function()
+ {
+ NGio.logout();
+ prompt.close();
+ };
+ prompt.onNo = prompt.close;
+ return prompt;
+ }
+}
+#end
diff --git a/source/funkin/api/newgrounds/README.md b/source/funkin/api/newgrounds/README.md
new file mode 100644
index 000000000..f61e1b0fd
--- /dev/null
+++ b/source/funkin/api/newgrounds/README.md
@@ -0,0 +1,9 @@
+# funkin.api.newgrounds
+
+This package contains two main classes:
+- `NGUtil` contains utility functions for interacting with the Newgrounds API.
+ - This includes any functions which scripts should be able to use,
+ such as retrieving achievement status.
+- `NGUnsafe` contains sensitive utility functions for interacting with the Newgrounds API.
+ - This includes any functions which scripts should not be able to use,
+ such as writing high scores or posting achievements.
\ No newline at end of file
diff --git a/source/funkin/charting/ChartingState.hx b/source/funkin/charting/ChartingState.hx
index eb4b13663..3f19edc07 100644
--- a/source/funkin/charting/ChartingState.hx
+++ b/source/funkin/charting/ChartingState.hx
@@ -23,6 +23,7 @@ import funkin.SongLoad.SwagSong;
import funkin.audiovis.ABotVis;
import funkin.audiovis.PolygonSpectogram;
import funkin.audiovis.SpectogramSprite;
+import funkin.play.HealthIcon;
import funkin.play.PlayState;
import funkin.rendering.MeshRender;
import haxe.Json;
@@ -110,6 +111,9 @@ class ChartingState extends MusicBeatState
leftIcon.setGraphicSize(0, 45);
rightIcon.setGraphicSize(0, 45);
+ leftIcon.autoUpdate = false;
+ rightIcon.autoUpdate = false;
+
add(leftIcon);
add(rightIcon);
@@ -705,7 +709,7 @@ class ChartingState extends MusicBeatState
{
if (FlxG.mouse.overlaps(leftIcon))
{
- if (leftIcon.char == _song.player1)
+ if (leftIcon.characterId == _song.player1)
{
p1Muted = !p1Muted;
leftIcon.animation.curAnim.curFrame = p1Muted ? 1 : 0;
@@ -727,7 +731,7 @@ class ChartingState extends MusicBeatState
// sloppy copypaste lol deal with it!
if (FlxG.mouse.overlaps(rightIcon))
{
- if (rightIcon.char == _song.player1)
+ if (rightIcon.characterId == _song.player1)
{
p1Muted = !p1Muted;
rightIcon.animation.curAnim.curFrame = p1Muted ? 1 : 0;
@@ -1012,7 +1016,7 @@ class ChartingState extends MusicBeatState
if (curSelectedNote != null)
{
trace('ALT NOTE SHIT');
- curSelectedNote.altNote = !curSelectedNote.altNote;
+ curSelectedNote.altNote = (curSelectedNote.altNote == "alt") ? "" : "alt";
trace(curSelectedNote.altNote);
}
}
@@ -1129,19 +1133,25 @@ class ChartingState extends MusicBeatState
{
if (check_mustHitSection.checked)
{
- leftIcon.changeIcon(_song.player1);
- rightIcon.changeIcon(_song.player2);
+ leftIcon.characterId = (_song.player1);
+ rightIcon.characterId = (_song.player2);
- leftIcon.animation.curAnim.curFrame = p1Muted ? 1 : 0;
- rightIcon.animation.curAnim.curFrame = p2Muted ? 1 : 0;
+ // leftIcon.animation.curAnim.curFrame = p1Muted ? 1 : 0;
+ // rightIcon.animation.curAnim.curFrame = p2Muted ? 1 : 0;
+
+ leftIcon.playAnimation(p1Muted ? LOSING : IDLE);
+ rightIcon.playAnimation(p2Muted ? LOSING : IDLE);
}
else
{
- leftIcon.changeIcon(_song.player2);
- rightIcon.changeIcon(_song.player1);
+ leftIcon.characterId = (_song.player2);
+ rightIcon.characterId = (_song.player1);
- leftIcon.animation.curAnim.curFrame = p2Muted ? 1 : 0;
- rightIcon.animation.curAnim.curFrame = p1Muted ? 1 : 0;
+ leftIcon.playAnimation(p2Muted ? LOSING : IDLE);
+ rightIcon.playAnimation(p1Muted ? LOSING : IDLE);
+
+ // leftIcon.animation.curAnim.curFrame = p2Muted ? 1 : 0;
+ // rightIcon.animation.curAnim.curFrame = p1Muted ? 1 : 0;
}
leftIcon.setGraphicSize(0, 45);
rightIcon.setGraphicSize(0, 45);
@@ -1348,7 +1358,7 @@ class ChartingState extends MusicBeatState
var noteStrum = getStrumTime(dummyArrow.y) + sectionStartTime();
var noteData = Math.floor(FlxG.mouse.x / GRID_SIZE);
var noteSus = 0;
- var noteAlt = false;
+ var noteAlt = "";
justPlacedNote = true;
diff --git a/source/funkin/freeplayStuff/DJBoyfriend.hx b/source/funkin/freeplayStuff/DJBoyfriend.hx
index 8ea52370d..20449c25e 100644
--- a/source/funkin/freeplayStuff/DJBoyfriend.hx
+++ b/source/funkin/freeplayStuff/DJBoyfriend.hx
@@ -23,14 +23,14 @@ class DJBoyfriend extends FlxSprite
addOffset('intro', 0, 0);
addOffset('idle', -4, -426);
- playAnim('intro');
+ playAnimation('intro');
animation.finishCallback = function(anim)
{
switch (anim)
{
case "intro":
animHITsignal.dispatch();
- playAnim('idle'); // plays idle anim after playing intro
+ playAnimation('idle'); // plays idle anim after playing intro
}
};
}
@@ -38,7 +38,7 @@ class DJBoyfriend extends FlxSprite
// playAnim stolen from Character.hx, cuz im lazy lol!
public var animOffsets:Map>;
- public function playAnim(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void
+ public function playAnimation(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void
{
animation.play(AnimName, Force, Reversed, Frame);
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index 407f3ec85..1fe2b0682 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -144,6 +144,8 @@ class PolymodHandler
// Ensure script files have merge support.
output.addType("hscript", TextFileFormat.PLAINTEXT);
output.addType("hxs", TextFileFormat.PLAINTEXT);
+ output.addType("hxc", TextFileFormat.PLAINTEXT);
+ output.addType("hx", TextFileFormat.PLAINTEXT);
// You can specify the format of a specific file, with file extension.
// output.addFile("data/introText.txt", TextFileFormat.LINES)
diff --git a/source/funkin/modding/base/ScriptedFlxSprite.hx b/source/funkin/modding/base/ScriptedFlxSprite.hx
index 9bccdc778..d13d968a8 100644
--- a/source/funkin/modding/base/ScriptedFlxSprite.hx
+++ b/source/funkin/modding/base/ScriptedFlxSprite.hx
@@ -4,7 +4,4 @@ import flixel.FlxSprite;
import funkin.modding.IHook;
@:hscriptClass
-class ScriptedFlxSprite extends FlxSprite implements IHook
-{
- // No body needed for this class, it's magic ;)
-}
+class ScriptedFlxSprite extends FlxSprite implements IHook {}
diff --git a/source/funkin/modding/base/ScriptedFlxSpriteGroup.hx b/source/funkin/modding/base/ScriptedFlxSpriteGroup.hx
index a60dfdad3..5db61b81e 100644
--- a/source/funkin/modding/base/ScriptedFlxSpriteGroup.hx
+++ b/source/funkin/modding/base/ScriptedFlxSpriteGroup.hx
@@ -4,7 +4,4 @@ import flixel.group.FlxSpriteGroup;
import funkin.modding.IHook;
@:hscriptClass
-class ScriptedFlxSpriteGroup extends FlxSpriteGroup implements IHook
-{
- // No body needed for this class, it's magic ;)
-}
+class ScriptedFlxSpriteGroup extends FlxSpriteGroup implements IHook {}
diff --git a/source/funkin/modding/module/ModuleHandler.hx b/source/funkin/modding/module/ModuleHandler.hx
index 908b5c428..e533a39fd 100644
--- a/source/funkin/modding/module/ModuleHandler.hx
+++ b/source/funkin/modding/module/ModuleHandler.hx
@@ -47,6 +47,16 @@ class ModuleHandler
trace("[MODULEHANDLER] Module cache loaded.");
}
+ public static function buildModuleCallbacks():Void
+ {
+ FlxG.signals.postStateSwitch.add(onStateSwitchComplete);
+ }
+
+ static function onStateSwitchComplete():Void
+ {
+ callEvent(new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_END, FlxG.state, true));
+ }
+
static function addToModuleCache(module:Module):Void
{
moduleCache.set(module.moduleId, module);
diff --git a/source/funkin/modding/module/ScriptedModule.hx b/source/funkin/modding/module/ScriptedModule.hx
index 31c79addb..7369707e3 100644
--- a/source/funkin/modding/module/ScriptedModule.hx
+++ b/source/funkin/modding/module/ScriptedModule.hx
@@ -3,7 +3,4 @@ package funkin.modding.module;
import funkin.modding.IHook;
@:hscriptClass
-class ScriptedModule extends Module implements IHook
-{
- // No body needed for this class, it's magic ;)
-}
+class ScriptedModule extends Module implements IHook {}
diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index 8413110ca..235db587e 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -28,19 +28,23 @@ class Countdown
* Performs the countdown.
* Pauses the song, plays the countdown graphics/sound, and then starts the song.
* This will automatically stop and restart the countdown if it is already running.
+ * @returns `false` if the countdown was cancelled by a script.
*/
- public static function performCountdown(isPixelStyle:Bool):Void
+ public static function performCountdown(isPixelStyle:Bool):Bool
{
+ countdownStep = BEFORE;
+ var cancelled:Bool = propagateCountdownEvent(countdownStep);
+ if (cancelled)
+ return false;
+
// Stop any existing countdown.
stopCountdown();
PlayState.isInCountdown = true;
Conductor.songPosition = Conductor.crochet * -5;
- countdownStep = BEFORE;
-
- var cancelled:Bool = propagateCountdownEvent(countdownStep);
- if (cancelled)
- return;
+ // Handle onBeatHit events manually
+ @:privateAccess
+ PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, 0, 0));
// The timer function gets called based on the beat of the song.
countdownTimer = new FlxTimer();
@@ -49,9 +53,9 @@ class Countdown
{
countdownStep = decrement(countdownStep);
- // Play the dance animations manually.
+ // Handle onBeatHit events manually
@:privateAccess
- PlayState.instance.danceOnBeat();
+ PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, 0, 0));
// Countdown graphic.
showCountdownGraphic(countdownStep, isPixelStyle);
@@ -69,7 +73,9 @@ class Countdown
{
stopCountdown();
}
- }, 6); // Before, 3, 2, 1, GO!, After
+ }, 5); // Before, 3, 2, 1, GO!, After
+
+ return true;
}
/**
@@ -91,11 +97,9 @@ class Countdown
return true;
}
- // Stage
- ScriptEventDispatcher.callEvent(PlayState.instance.currentStage, event);
-
- // Modules
- ModuleHandler.callEvent(event);
+ // Modules, stages, characters.
+ @:privateAccess
+ PlayState.instance.dispatchEvent(event);
return event.eventCanceled;
}
diff --git a/source/funkin/play/Fighter.hx b/source/funkin/play/Fighter.hx
index fa17b6bd7..6a25283ba 100644
--- a/source/funkin/play/Fighter.hx
+++ b/source/funkin/play/Fighter.hx
@@ -1,19 +1,22 @@
package funkin.play;
+import funkin.play.character.BaseCharacter;
import flixel.FlxSprite;
-class Fighter extends Character
+class Fighter extends BaseCharacter
{
public function new(?x:Float = 0, ?y:Float = 0, ?char:String = "pico-fighter")
{
- super(x, y, char);
+ super(char);
+ this.x = x;
+ this.y = y;
animation.finishCallback = function(anim:String)
{
switch anim
{
case "punch low" | "punch high" | "block" | 'dodge':
- dance();
+ dance(true);
}
};
}
@@ -42,20 +45,20 @@ class Fighter extends Character
function dodge()
{
- playAnim('dodge');
+ playAnimation('dodge');
curAction = DODGE;
}
public function block()
{
- playAnim('block');
+ playAnimation('block');
curAction = BLOCK;
}
public function punch()
{
curAction = PUNCH;
- playAnim('punch ' + (FlxG.random.bool() ? "low" : "high"));
+ playAnimation('punch ' + (FlxG.random.bool() ? "low" : "high"));
}
}
diff --git a/source/funkin/play/HealthIcon.hx b/source/funkin/play/HealthIcon.hx
new file mode 100644
index 000000000..083d26783
--- /dev/null
+++ b/source/funkin/play/HealthIcon.hx
@@ -0,0 +1,441 @@
+package funkin.play;
+
+import flixel.FlxSprite;
+import flixel.math.FlxMath;
+import flixel.math.FlxPoint;
+import funkin.play.character.CharacterData.CharacterDataParser;
+import openfl.utils.Assets;
+
+/**
+ * This is a rework of the health icon with the following changes:
+ * - The health icon now owns its own state logic. It queries health and updates the sprite itself,
+ * rather than relying on PlayState to command it.
+ * - The health icon now supports animations.
+ * - The health icon will now search for a SparrowV2 (XML) spritesheet, and use that for rendering if it can.
+ * - If it can't find a spritesheet, it will the old format; a two-frame 300x150 image.
+ * - If the spritesheet is found, the health icon will attempt to load and use the following animations as appropriate:
+ * - `idle`, `winning`, `losing`, `toWinning`, `fromWinning`, `toLosing`, `fromLosing`
+ * - The health icon is now easier to control via scripts.
+ * - Set `autoUpdate` to false to prevent the health icon from changing its own animations.
+ * - Once `autoUpdate` is false, you can manually call `playAnimation()` to play a specific animation.
+ * - i.e. `PlayState.instance.iconP1.playAnimation("losing")`
+ * - Scripts can also utilize all functionality that a normal FlxSprite would have access to, such as adding supplimental animations.
+ * - i.e. `PlayState.instance.iconP1.animation.addByPrefix("jumpscare", "jumpscare", 24, false);`
+ * @author MasterEric
+ */
+class HealthIcon extends FlxSprite
+{
+ /**
+ * The character this icon is representing.
+ * Setting this variable will automatically update the graphic.
+ */
+ public var characterId(default, set):String;
+
+ /**
+ * Whether this health icon should automatically update its state based on the character's health.
+ * Note that turning this off means you have to manually do the following:
+ * - Bumping the icon on the beat.
+ * - Switching between winning/losing/idle animations.
+ * - Repositioning the icon as health changes.
+ */
+ public var autoUpdate:Bool = true;
+
+ /**
+ * Since the `scale` of the sprite dynamically changes over time,
+ * this value allows you to set a relative scale for the icon.
+ * @default 1x scale
+ */
+ public var size:FlxPoint = new FlxPoint(1, 1);
+
+ /**
+ * Apply the "bump" animation once every X steps.
+ */
+ public var bumpEvery:Int = 4;
+
+ /**
+ * The player the health icon is attached to.
+ */
+ var playerId:Int = 0;
+
+ /**
+ * Whether the sprite is pixel art or not.
+ * Calculated when loading an icon.
+ */
+ var isPixel:Bool = false;
+
+ /**
+ * Whether this is a legacy icon or not.
+ */
+ var isLegacyStyle:Bool = false;
+
+ /**
+ * At this amount of health, play the Winning animation instead of the idle.
+ */
+ static final WINNING_THRESHOLD = 0.8 * 2;
+
+ /**
+ * At this amount of health, play the Losing animation instead of the idle.
+ */
+ static final LOSING_THRESHOLD = 0.2 * 2;
+
+ /**
+ * The maximum health of the player.
+ */
+ static final MAXIMUM_HEALTH = 2;
+
+ /**
+ * The size of a non-pixel icon when using the legacy format.
+ * Remember, modern icons can be any size.
+ */
+ static final LEGACY_ICON_SIZE = 150;
+
+ /**
+ * The size of a pixel icon when using the legacy format.
+ * Remember, modern icons can be any size.
+ */
+ static final LEGACY_PIXEL_SIZE = 32;
+
+ /**
+ * shitty hardcoded value for a specific positioning!!!
+ */
+ static final POSITION_OFFSET = 26;
+
+ public function new(char:String = 'bf', playerId:Int = 0)
+ {
+ super(0, 0);
+ this.playerId = playerId;
+ this.scrollFactor.set();
+
+ this.characterId = char;
+
+ this.antialiasing = !isPixel;
+
+ this.flipX = playerId == 0;
+
+ initTargetSize();
+ }
+
+ function set_characterId(value:String):String
+ {
+ if (value == characterId)
+ return value;
+
+ characterId = value;
+ loadCharacter(characterId);
+ return value;
+ }
+
+ /**
+ * Easter egg; press 9 in the PlayState to use the old player icon.
+ */
+ public function toggleOldIcon():Void
+ {
+ if (characterId == 'bf-old')
+ {
+ characterId = PlayState.currentSong.player1;
+ }
+ else
+ {
+ characterId = 'bf-old';
+ }
+ }
+
+ /**
+ * Called by Flixel every frame. Includes logic to manage the currently playing animation.
+ */
+ override function update(elapsed:Float):Void
+ {
+ super.update(elapsed);
+
+ if (PlayState.instance == null)
+ return;
+
+ // Auto-update the state of the icon based on the player's health.
+ if (autoUpdate)
+ {
+ switch (playerId)
+ {
+ case 0: // Boyfriend
+ // Update the animation based on the current state.
+ updateHealthIcon(PlayState.instance.health);
+ // Update the position to match the health bar.
+ this.x = PlayState.instance.healthBar.x
+ + (PlayState.instance.healthBar.width * (FlxMath.remapToRange(PlayState.instance.healthBar.value, 0, 2, 100, 0) * 0.01)
+ - POSITION_OFFSET);
+ case 1: // Dad
+ // Update the animation based on the current state.
+ updateHealthIcon(MAXIMUM_HEALTH - PlayState.instance.health);
+ // Update the position to match the health bar.
+ this.x = PlayState.instance.healthBar.x
+ + (PlayState.instance.healthBar.width * (FlxMath.remapToRange(PlayState.instance.healthBar.value, 0, 2, 100, 0) * 0.01))
+ - (this.width - POSITION_OFFSET);
+ }
+
+ // Lerp the health icon back to its normal size,
+ // while maintaining aspect ratio.
+ if (this.width > this.height)
+ {
+ // Apply linear interpolation while accounting for frame rate.
+ var targetSize = Std.int(CoolUtil.coolLerp(this.width, 150 * this.size.x, 0.15));
+
+ setGraphicSize(targetSize, 0);
+ }
+ else
+ {
+ var targetSize = Std.int(CoolUtil.coolLerp(this.height, 150 * this.size.y, 0.15));
+
+ setGraphicSize(0, targetSize);
+ }
+ this.updateHitbox();
+ }
+ }
+
+ public function onStepHit(curStep:Int)
+ {
+ if (curStep % bumpEvery == 0 && isLegacyStyle)
+ {
+ // Make the health icons bump (the update function causes them to lerp back down).
+ if (this.width > this.height)
+ {
+ var targetSize = Std.int(CoolUtil.coolLerp(this.width + 30, 150, 0.15));
+
+ setGraphicSize(targetSize, 0);
+ }
+ else
+ {
+ var targetSize = Std.int(CoolUtil.coolLerp(this.height + 30, 150, 0.15));
+
+ setGraphicSize(0, targetSize);
+ }
+ this.updateHitbox();
+ }
+ }
+
+ inline function initTargetSize()
+ {
+ setGraphicSize(150);
+ updateHitbox();
+ }
+
+ function updateHealthIcon(health:Float)
+ {
+ // We want to efficiently handle animation playback
+
+ // Here, we use the current animation name to track the current state
+ // of a simple state machine. Neat!
+
+ switch (getCurrentAnimation())
+ {
+ case IDLE:
+ if (health < LOSING_THRESHOLD)
+ playAnimation(TO_LOSING, LOSING);
+ else if (health > WINNING_THRESHOLD)
+ playAnimation(TO_WINNING, WINNING);
+ else
+ playAnimation(IDLE);
+ case WINNING:
+ if (health < WINNING_THRESHOLD)
+ playAnimation(FROM_WINNING, IDLE);
+ else
+ playAnimation(WINNING, IDLE);
+ case LOSING:
+ if (health > LOSING_THRESHOLD)
+ playAnimation(FROM_LOSING, IDLE);
+ else
+ playAnimation(LOSING, IDLE);
+ case TO_LOSING:
+ if (isAnimationFinished())
+ playAnimation(LOSING, IDLE);
+ case TO_WINNING:
+ if (isAnimationFinished())
+ playAnimation(WINNING, IDLE);
+ case FROM_LOSING | FROM_WINNING:
+ if (isAnimationFinished())
+ playAnimation(IDLE);
+ case "":
+ playAnimation(IDLE);
+ default:
+ playAnimation(IDLE);
+ }
+ }
+
+ /**
+ * Load
+ * @param charId
+ */
+ function loadAnimationNew(charId:String):Void
+ {
+ this.animation.addByPrefix(IDLE, IDLE, 24, true);
+ this.animation.addByPrefix(WINNING, WINNING, 24, true);
+ this.animation.addByPrefix(LOSING, LOSING, 24, true);
+ this.animation.addByPrefix(TO_WINNING, TO_WINNING, 24, true);
+ this.animation.addByPrefix(TO_LOSING, TO_LOSING, 24, true);
+ this.animation.addByPrefix(FROM_WINNING, FROM_WINNING, 24, true);
+ this.animation.addByPrefix(FROM_LOSING, FROM_LOSING, 24, true);
+ }
+
+ /**
+ * Load health icon animations using the legacy format.
+ * Simply assumes two icons, one on
+ * @param charId
+ */
+ function loadAnimationOld(charId:String):Void
+ {
+ this.animation.add(IDLE, [0], 0, false, this.playerId == 0);
+ this.animation.add(LOSING, [1], 0, false, this.playerId == 0);
+ }
+
+ function correctCharacterId(charId:String):String
+ {
+ if (!Assets.exists(Paths.image('icons/icon-$charId')))
+ {
+ FlxG.log.warn('No icon for character: $charId : using default placeholder face instead!');
+ return "face";
+ }
+
+ return charId;
+ }
+
+ function isNewSpritesheet(charId:String):Bool
+ {
+ return Assets.exists(Paths.file('images/icons/icon-$characterId.xml'));
+ }
+
+ function fetchIsPixel(charId:String):Bool
+ {
+ var charData = CharacterDataParser.fetchCharacterData(charId);
+ if (charData == null)
+ {
+ FlxG.log.warn('No character data found for character: $charId');
+ return false;
+ }
+ return charData.isPixel;
+ }
+
+ function loadCharacter(charId:String):Void
+ {
+ if (correctCharacterId(charId) != charId)
+ {
+ characterId = correctCharacterId(charId);
+ return;
+ }
+
+ isPixel = fetchIsPixel(charId);
+
+ isLegacyStyle = !isNewSpritesheet(charId);
+
+ if (!isLegacyStyle)
+ {
+ frames = Paths.getSparrowAtlas('icons/icon-$charId');
+
+ loadAnimationNew(charId);
+ }
+ else
+ {
+ loadGraphic(Paths.image('icons/icon-$charId'), true, isPixel ? LEGACY_PIXEL_SIZE : LEGACY_ICON_SIZE,
+ isPixel ? LEGACY_PIXEL_SIZE : LEGACY_ICON_SIZE);
+
+ loadAnimationOld(charId);
+ }
+ }
+
+ /**
+ * @return Name of the current animation being played by this health icon.
+ */
+ public function getCurrentAnimation():String
+ {
+ if (this.animation == null || this.animation.curAnim == null)
+ return "";
+ return this.animation.curAnim.name;
+ }
+
+ /**
+ * @return Whether this sprite posesses the given animation.
+ * Only true if the animation was successfully loaded from the XML.
+ */
+ public function hasAnimation(id:String):Bool
+ {
+ if (this.animation == null)
+ return false;
+
+ return this.animation.getByName(id) != null;
+ }
+
+ /**
+ * @return Whether the current animation is in the finished state.
+ */
+ public function isAnimationFinished():Bool
+ {
+ return this.animation.finished;
+ }
+
+ /**
+ * Plays the animation with the given name.
+ * @param name The name of the animation to play.
+ * @param fallback The fallback animation to play if the given animation is not found.
+ * @param restart Whether to forcibly restart the animation if it is already playing.
+ */
+ public function playAnimation(name:String, fallback:String = null, restart = false):Void
+ {
+ // Attempt to play the animation
+ if (hasAnimation(name))
+ {
+ this.animation.play(name, restart, false, 0);
+ return;
+ }
+
+ // Play the fallback animation if the requested animation was not found
+ if (fallback != null && hasAnimation(fallback))
+ {
+ this.animation.play(fallback, restart, false, 0);
+ return;
+ }
+
+ // If we don't have an animation, we're done.
+ }
+}
+
+enum abstract HealthIconState(String) to String from String
+{
+ /**
+ * Indicates the health icon is in the default animation.
+ * Plays as long as health is between 20% and 80%.
+ */
+ var IDLE = "idle";
+
+ /**
+ * Indicates the health icon is playing the Winning animation.
+ * Plays as long as health is above 80%.
+ */
+ var WINNING = "winning";
+
+ /**
+ * Indicates the health icon is playing the Losing animation.
+ * Plays as long as health is below 20%.
+ */
+ var LOSING = "losing";
+
+ /**
+ * Indicates that the health icon is transitioning between `idle` and `winning`.
+ * The next animation will play once the current animation finishes.
+ */
+ var TO_WINNING = "toWinning";
+
+ /**
+ * Indicates that the health icon is transitioning between `idle` and `losing`.
+ * The next animation will play once the current animation finishes.
+ */
+ var TO_LOSING = "toLosing";
+
+ /**
+ * Indicates that the health icon is transitioning between `winning` and `idle`.
+ * The next animation will play once the current animation finishes.
+ */
+ var FROM_WINNING = "fromWinning";
+
+ /**
+ * Indicates that the health icon is transitioning between `losing` and `idle`.
+ * The next animation will play once the current animation finishes.
+ */
+ var FROM_LOSING = "fromLosing";
+}
diff --git a/source/funkin/play/PicoFight.hx b/source/funkin/play/PicoFight.hx
index f37bcb08f..a5dddac3a 100644
--- a/source/funkin/play/PicoFight.hx
+++ b/source/funkin/play/PicoFight.hx
@@ -178,7 +178,7 @@ class PicoFight extends MusicBeatState
pico.punch();
}
if (controls.NOTE_LEFT_R)
- pico.playAnim('idle');
+ pico.playAnimation('idle');
super.update(elapsed);
}
@@ -190,8 +190,10 @@ class PicoFight extends MusicBeatState
override function beatHit():Bool
{
+ // super.beatHit() returns false if a module cancelled the event.
if (!super.beatHit())
return false;
+
funnyWave.thickness = 10;
funnyWave.waveAmplitude = 300;
funnyWave.realtimeVisLenght = 0.1;
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index a127e70b2..1b4e652ec 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -6,6 +6,7 @@ import flixel.FlxSprite;
import flixel.FlxState;
import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
+import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
@@ -18,18 +19,29 @@ import flixel.util.FlxColor;
import flixel.util.FlxSort;
import flixel.util.FlxTimer;
import funkin.Note;
+import funkin.Note;
import funkin.Section.SwagSection;
+import funkin.Section.SwagSection;
+import funkin.SongLoad.SwagSong;
import funkin.SongLoad.SwagSong;
import funkin.charting.ChartingState;
import funkin.modding.IHook;
+import funkin.modding.IHook;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
+import funkin.modding.module.ModuleHandler;
+import funkin.play.HealthIcon;
+import funkin.play.Strumline.StrumlineArrow;
import funkin.play.Strumline.StrumlineArrow;
import funkin.play.Strumline.StrumlineStyle;
+import funkin.play.Strumline.StrumlineStyle;
+import funkin.play.character.BaseCharacter;
+import funkin.play.character.CharacterData;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData;
import funkin.ui.PopUpStuff;
import funkin.ui.PreferencesMenu;
+import funkin.ui.stageBuildShit.StageOffsetSubstate;
import funkin.util.Constants;
import funkin.util.SortUtil;
import lime.ui.Haptic;
@@ -80,6 +92,12 @@ class PlayState extends MusicBeatState implements IHook
*/
public static var isInCountdown:Bool = false;
+ /**
+ * Gets set to true when the PlayState needs to reset (player opted to restart or died).
+ * Gets disabled once resetting happens.
+ */
+ public static var needsReset:Bool = false;
+
/**
* The current "Blueball Counter" to display in the pause menu.
* Resets when you beat a song or go back to the main menu.
@@ -119,11 +137,20 @@ class PlayState extends MusicBeatState implements IHook
*/
public var health:Float = 1;
+ /**
+ * The player's current score.
+ */
+ public var songScore:Int = 0;
+
/**
* An empty FlxObject contained in the scene.
* The current gameplay camera will be centered on this object. Tween its position to move the camera smoothly.
+ *
+ * This is an FlxSprite for two reasons:
+ * 1. It needs to be an object in the scene for the camera to be configured to follow it.
+ * 2. It needs to be an FlxSprite to allow a graphic (optionally, for debug purposes) to be drawn on it.
*/
- public var cameraFollowPoint:FlxObject;
+ public var cameraFollowPoint:FlxSprite = new FlxSprite(0, 0);
/**
* PRIVATE INSTANCE VARIABLES
@@ -165,7 +192,7 @@ class PlayState extends MusicBeatState implements IHook
* The bar which displays the player's health.
* Dynamically updated based on the value of `healthLerp` (which is based on `health`).
*/
- private var healthBar:FlxBar;
+ public var healthBar:FlxBar;
/**
* The background image used for the health bar.
@@ -173,6 +200,16 @@ class PlayState extends MusicBeatState implements IHook
*/
public var healthBarBG:FlxSprite;
+ /**
+ * The health icon representing the player.
+ */
+ public var iconP1:HealthIcon;
+
+ /**
+ * The health icon representing the opponent.
+ */
+ public var iconP2:HealthIcon;
+
/**
* The sprite group containing active player's strumline notes.
*/
@@ -216,7 +253,6 @@ class PlayState extends MusicBeatState implements IHook
public static var storyWeek:Int = 0;
public static var storyPlaylist:Array = [];
public static var storyDifficulty:Int = 1;
- public static var needsReset:Bool = false;
public static var seenCutscene:Bool = false;
public static var campaignScore:Int = 0;
@@ -228,15 +264,11 @@ class PlayState extends MusicBeatState implements IHook
private var combo:Int = 0;
private var generatedMusic:Bool = false;
private var startingSong:Bool = false;
- private var iconP1:HealthIcon;
- private var iconP2:HealthIcon;
var dialogue:Array;
var talking:Bool = true;
- var songScore:Int = 0;
var doof:DialogueBox;
var grpNoteSplashes:FlxTypedGroup;
- var camPos:FlxPoint;
var comboPopUps:PopUpStuff;
var perfectMode:Bool = false;
var previousFrameTime:Int = 0;
@@ -258,6 +290,12 @@ class PlayState extends MusicBeatState implements IHook
instance = this;
+ // Displays the camera follow point as a sprite for debug purposes.
+ // TODO: Put this on a toggle?
+ cameraFollowPoint.makeGraphic(8, 8, 0xFF00FF00);
+ cameraFollowPoint.visible = false;
+ cameraFollowPoint.zIndex = 1000000;
+
// Reduce physics accuracy (who cares!!!) to improve animation quality.
FlxG.fixedTimestep = false;
@@ -305,12 +343,32 @@ class PlayState extends MusicBeatState implements IHook
// Once the song is loaded, we can continue and initialize the stage.
+ var healthBarYPos:Float = PreferencesMenu.getPref('downscroll') ? FlxG.height * 0.1 : FlxG.height * 0.9;
+ healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
+ healthBarBG.screenCenter(X);
+ healthBarBG.scrollFactor.set(0, 0);
+ add(healthBarBG);
+
+ healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this,
+ 'healthLerp', 0, 2);
+ healthBar.scrollFactor.set();
+ healthBar.createFilledBar(Constants.HEALTH_BAR_RED, Constants.HEALTH_BAR_GREEN);
+ add(healthBar);
+
initStage();
initCharacters();
#if discord_rpc
initDiscord();
#end
+ // Configure camera follow point.
+ if (previousCameraFollowPoint != null)
+ {
+ cameraFollowPoint.setPosition(previousCameraFollowPoint.x, previousCameraFollowPoint.y);
+ previousCameraFollowPoint = null;
+ }
+ add(cameraFollowPoint);
+
comboPopUps = new PopUpStuff();
add(comboPopUps);
@@ -324,45 +382,15 @@ class PlayState extends MusicBeatState implements IHook
generateSong();
- cameraFollowPoint = new FlxObject(0, 0, 1, 1);
- cameraFollowPoint.setPosition(camPos.x, camPos.y);
-
- if (previousCameraFollowPoint != null)
- {
- cameraFollowPoint = previousCameraFollowPoint;
- previousCameraFollowPoint = null;
- }
-
- add(cameraFollowPoint);
resetCamera();
FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height);
- var healthBarYPos:Float = PreferencesMenu.getPref('downscroll') ? FlxG.height * 0.1 : FlxG.height * 0.9;
- healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
- healthBarBG.screenCenter(X);
- healthBarBG.scrollFactor.set(0, 0);
- add(healthBarBG);
-
- healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this,
- 'healthLerp', 0, 2);
- healthBar.scrollFactor.set();
- healthBar.createFilledBar(Constants.HEALTH_BAR_RED, Constants.HEALTH_BAR_GREEN);
- add(healthBar);
-
scoreText = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, "", 20);
scoreText.setFormat(Paths.font("vcr.ttf"), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
scoreText.scrollFactor.set();
add(scoreText);
- iconP1 = new HealthIcon(currentSong.player1, true);
- iconP1.y = healthBar.y - (iconP1.height / 2);
- add(iconP1);
-
- iconP2 = new HealthIcon(currentSong.player2, false);
- iconP2.y = healthBar.y - (iconP2.height / 2);
- add(iconP2);
-
// Attach the groups to the HUD camera so they are rendered independent of the stage.
grpNoteSplashes.cameras = [camHUD];
activeNotes.cameras = [camHUD];
@@ -371,6 +399,8 @@ class PlayState extends MusicBeatState implements IHook
iconP1.cameras = [camHUD];
iconP2.cameras = [camHUD];
scoreText.cameras = [camHUD];
+ leftWatermarkText.cameras = [camHUD];
+ rightWatermarkText.cameras = [camHUD];
// if (SONG.song == 'South')
// FlxG.camera.alpha = 0.7;
@@ -454,6 +484,9 @@ class PlayState extends MusicBeatState implements IHook
currentStageId = 'schoolEvil';
case 'guns' | 'stress' | 'ugh':
currentStageId = 'tankmanBattlefield';
+ case 'experimental-phase' | 'perfection':
+ // SERIOUSLY REVAMP THE CHART FORMAT ALREADY
+ currentStageId = "breakout";
default:
currentStageId = "mainStage";
}
@@ -463,7 +496,19 @@ class PlayState extends MusicBeatState implements IHook
function initCharacters()
{
- // all dis is shitty, redo later for stage shit
+ iconP1 = new HealthIcon(currentSong.player1, 0);
+ iconP1.y = healthBar.y - (iconP1.height / 2);
+ add(iconP1);
+
+ iconP2 = new HealthIcon(currentSong.player2, 1);
+ iconP2.y = healthBar.y - (iconP2.height / 2);
+ add(iconP2);
+
+ //
+ // GIRLFRIEND
+ //
+
+ // TODO: Tie the GF version to the song data, not the stage ID or the current player.
var gfVersion:String = 'gf';
switch (currentStageId)
@@ -476,102 +521,84 @@ class PlayState extends MusicBeatState implements IHook
gfVersion = 'gf-pixel';
case 'tankmanBattlefield':
gfVersion = 'gf-tankmen';
+ case 'breakout':
+ // SERIOUSLY PUT THIS SHIT IN THE CHART
+ gfVersion = '';
}
if (currentSong.player1 == "pico")
- {
gfVersion = "nene";
- }
if (currentSong.song.toLowerCase() == 'stress')
gfVersion = 'pico-speaker';
- var gf = new Character(400, 130, gfVersion);
- gf.scrollFactor.set(0.95, 0.95);
+ if (currentSong.song.toLowerCase() == 'tutorial')
+ gfVersion = '';
- switch (gfVersion)
+ //
+ // GIRLFRIEND
+ //
+ var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(gfVersion);
+
+ if (girlfriend != null)
{
- case 'pico-speaker':
- gf.x -= 50;
- gf.y -= 200;
+ girlfriend.characterType = CharacterType.GF;
+ girlfriend.scrollFactor.set(0.95, 0.95);
+ if (gfVersion == 'pico-speaker')
+ {
+ girlfriend.x -= 50;
+ girlfriend.y -= 200;
+ }
+ }
+ else if (gfVersion != '')
+ {
+ trace('WARNING: Could not load girlfriend character with ID ${gfVersion}, skipping...');
}
- var dad = new Character(100, 100, currentSong.player2);
+ //
+ // DAD
+ //
+ var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentSong.player2);
- camPos = new FlxPoint(dad.getGraphicMidpoint().x, dad.getGraphicMidpoint().y);
+ if (dad != null)
+ {
+ dad.characterType = CharacterType.DAD;
+ }
switch (currentSong.player2)
{
case 'gf':
- dad.setPosition(gf.x, gf.y);
- gf.visible = false;
if (isStoryMode)
{
- camPos.x += 600;
+ cameraFollowPoint.x += 600;
tweenCamIn();
}
- case "spooky":
- dad.y += 200;
- case "monster":
- dad.y += 100;
- case 'monster-christmas':
- dad.y += 130;
- case 'dad':
- camPos.x += 400;
- case 'pico':
- camPos.x += 600;
- dad.y += 300;
- case 'parents-christmas':
- dad.x -= 500;
- case 'senpai' | 'senpai-angry':
- dad.x += 150;
- dad.y += 360;
- camPos.set(dad.getGraphicMidpoint().x + 300, dad.getGraphicMidpoint().y);
- case 'spirit':
- dad.x -= 150;
- dad.y += 100;
- camPos.set(dad.getGraphicMidpoint().x + 300, dad.getGraphicMidpoint().y);
- case 'tankman':
- dad.y += 180;
}
- var boyfriend = new Boyfriend(770, 450, currentSong.player1);
+ //
+ // BOYFRIEND
+ //
+ var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentSong.player1);
- // REPOSITIONING PER STAGE
- switch (currentStageId)
+ if (boyfriend != null)
{
- case "tank":
- gf.y += 10;
- gf.x -= 30;
- boyfriend.x += 40;
- boyfriend.y += 0;
- dad.y += 60;
- dad.x -= 80;
-
- if (gfVersion != 'pico-speaker')
- {
- gf.x -= 170;
- gf.y -= 75;
- }
+ boyfriend.characterType = CharacterType.BF;
}
if (currentStage != null)
{
// We're using Eric's stage handler.
// Characters get added to the stage, not the main scene.
- currentStage.addCharacter(gf, GF);
+ currentStage.addCharacter(girlfriend, GF);
currentStage.addCharacter(boyfriend, BF);
currentStage.addCharacter(dad, DAD);
+ // Camera starts at dad.
+ cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
+
// Redo z-indexes.
currentStage.refresh();
}
- else
- {
- add(gf);
- add(dad);
- add(boyfriend);
- }
}
/**
@@ -598,6 +625,15 @@ class PlayState extends MusicBeatState implements IHook
super.debug_refreshModules();
}
+ /**
+ * Pauses music and vocals easily.
+ */
+ public function pauseMusic()
+ {
+ FlxG.sound.music.pause();
+ vocals.pause();
+ }
+
/**
* Loads stage data from cache, assembles the props,
* and adds it to the state.
@@ -614,7 +650,7 @@ class PlayState extends MusicBeatState implements IHook
ScriptEventDispatcher.callEvent(currentStage, event);
// Apply camera zoom.
- defaultCameraZoom *= currentStage.camZoom;
+ defaultCameraZoom = currentStage.camZoom;
// Add the stage to the scene.
this.add(currentStage);
@@ -665,8 +701,6 @@ class PlayState extends MusicBeatState implements IHook
senpaiEvil.screenCenter();
senpaiEvil.x += senpaiEvil.width / 5;
- cameraFollowPoint.setPosition(camPos.x, camPos.y);
-
if (currentSong.song.toLowerCase() == 'roses' || currentSong.song.toLowerCase() == 'thorns')
{
remove(black);
@@ -828,7 +862,18 @@ class PlayState extends MusicBeatState implements IHook
else
oldNote = null;
- var swagNote:Note = new Note(daStrumTime, daNoteData, oldNote);
+ var strumlineStyle:StrumlineStyle = NORMAL;
+
+ // TODO: Put this in the chart or something?
+ switch (currentStageId)
+ {
+ case 'school':
+ strumlineStyle = PIXEL;
+ case 'schoolEvil':
+ strumlineStyle = PIXEL;
+ }
+
+ var swagNote:Note = new Note(daStrumTime, daNoteData, oldNote, false, strumlineStyle);
// swagNote.data = songNotes;
swagNote.data.sustainLength = songNotes.sustainLength;
swagNote.data.altNote = songNotes.altNote;
@@ -843,7 +888,8 @@ class PlayState extends MusicBeatState implements IHook
{
oldNote = inactiveNotes[Std.int(inactiveNotes.length - 1)];
- var sustainNote:Note = new Note(daStrumTime + (Conductor.stepCrochet * susNote) + Conductor.stepCrochet, daNoteData, oldNote, true);
+ var sustainNote:Note = new Note(daStrumTime + (Conductor.stepCrochet * susNote) + Conductor.stepCrochet, daNoteData, oldNote, true,
+ strumlineStyle);
sustainNote.scrollFactor.set();
inactiveNotes.push(sustainNote);
@@ -915,6 +961,11 @@ class PlayState extends MusicBeatState implements IHook
{
super.update(elapsed);
+ if (FlxG.keys.justPressed.U)
+ {
+ openSubState(new StageOffsetSubstate());
+ }
+
updateHealthBar();
updateScoreText();
@@ -1032,32 +1083,11 @@ class PlayState extends MusicBeatState implements IHook
FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
if (FlxG.keys.justPressed.NINE)
- iconP1.swapOldIcon();
-
- iconP1.setGraphicSize(Std.int(CoolUtil.coolLerp(iconP1.width, 150, 0.15)));
- iconP2.setGraphicSize(Std.int(CoolUtil.coolLerp(iconP2.width, 150, 0.15)));
-
- iconP1.updateHitbox();
- iconP2.updateHitbox();
-
- var iconOffset:Int = 26;
-
- iconP1.x = healthBar.x + (healthBar.width * (FlxMath.remapToRange(healthBar.value, 0, 2, 100, 0) * 0.01) - iconOffset);
- iconP2.x = healthBar.x + (healthBar.width * (FlxMath.remapToRange(healthBar.value, 0, 2, 100, 0) * 0.01)) - (iconP2.width - iconOffset);
+ iconP1.toggleOldIcon();
if (health > 2)
health = 2;
- if (healthBar.percent < 20)
- iconP1.animation.curAnim.curFrame = 1;
- else
- iconP1.animation.curAnim.curFrame = 0;
-
- if (healthBar.percent > 80)
- iconP2.animation.curAnim.curFrame = 1;
- else
- iconP2.animation.curAnim.curFrame = 0;
-
#if debug
if (FlxG.keys.justPressed.ONE)
endSong();
@@ -1068,14 +1098,7 @@ class PlayState extends MusicBeatState implements IHook
changeSection(-1);
#end
- if (generatedMusic && SongLoad.getSong()[Std.int(curStep / 16)] != null)
- {
- cameraRightSide = SongLoad.getSong()[Std.int(curStep / 16)].mustHitSection;
-
- cameraMovement();
- }
-
- if (camZooming)
+ if (camZooming && subState == null)
{
FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
camHUD.zoom = FlxMath.lerp(1 * FlxCamera.defaultZoom, camHUD.zoom, 0.95);
@@ -1083,6 +1106,7 @@ class PlayState extends MusicBeatState implements IHook
FlxG.watch.addQuick("beatShit", curBeat);
FlxG.watch.addQuick("stepShit", curStep);
+ FlxG.watch.addQuick("songPos", Conductor.songPosition);
if (currentSong.song == 'Fresh')
{
@@ -1127,6 +1151,8 @@ class PlayState extends MusicBeatState implements IHook
deathCounter += 1;
+ dispatchEvent(new ScriptEvent(ScriptEvent.GAME_OVER));
+
openSubState(new GameOverSubstate());
#if discord_rpc
@@ -1144,7 +1170,7 @@ class PlayState extends MusicBeatState implements IHook
inactiveNotes.shift();
}
- if (generatedMusic)
+ if (generatedMusic && playerStrumline != null)
{
activeNotes.forEachAlive(function(daNote:Note)
{
@@ -1195,35 +1221,28 @@ class PlayState extends MusicBeatState implements IHook
}
}
- if (!daNote.mustPress && daNote.wasGoodHit)
+ if (!daNote.mustPress && daNote.wasGoodHit && !daNote.tooLate)
{
if (currentSong.song != 'Tutorial')
camZooming = true;
- var altAnim:String = "";
+ var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, true);
+ dispatchEvent(event);
- if (SongLoad.getSong()[Math.floor(curStep / 16)] != null)
+ // Calling event.cancelEvent() in a module should force the CPU to miss the note.
+ // This is useful for cool shit, including but not limited to:
+ // - Making the AI ignore notes which are hazardous.
+ // - Making the AI miss notes on purpose for aesthetic reasons.
+ if (event.eventCanceled)
{
- if (SongLoad.getSong()[Math.floor(curStep / 16)].altAnim)
- altAnim = '-alt';
+ daNote.tooLate = true;
}
-
- if (daNote.data.altNote)
- altAnim = '-alt';
-
- if (!daNote.isSustainNote)
+ else
{
- currentStage.getDad().playAnim('sing' + daNote.dirNameUpper + altAnim, true);
+ // Volume of DAD.
+ if (currentSong.needsVoices)
+ vocals.volume = 1;
}
-
- currentStage.getDad().holdTimer = 0;
-
- if (currentSong.needsVoices)
- vocals.volume = 1;
-
- daNote.kill();
- activeNotes.remove(daNote, true);
- daNote.destroy();
}
// WIP interpolation shit? Need to fix the pause issue
@@ -1248,20 +1267,8 @@ class PlayState extends MusicBeatState implements IHook
daNote.destroy();
}
}
- else if (daNote.tooLate || daNote.wasGoodHit)
+ if (daNote.wasGoodHit)
{
- // TODO: Why the hell is the noteMiss logic in two different places?
- if (daNote.tooLate)
- {
- var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, daNote, true);
- dispatchEvent(event);
-
- // lose less health on sustain notes!
- health -= 0.0775 * (daNote.isSustainNote ? 0.2 : 1); // if it's sustain, multiply it by 0.2 (not checked for balence yet), else keep it same (multiply by 1)
- vocals.volume = 0;
- killCombo();
- }
-
daNote.active = false;
daNote.visible = false;
@@ -1269,6 +1276,11 @@ class PlayState extends MusicBeatState implements IHook
activeNotes.remove(daNote, true);
daNote.destroy();
}
+
+ if (daNote.tooLate)
+ {
+ noteMiss(daNote);
+ }
});
}
@@ -1300,8 +1312,10 @@ class PlayState extends MusicBeatState implements IHook
function killCombo():Void
{
- if (combo > 5 && currentStage.getGirlfriend().animOffsets.exists('sad'))
- currentStage.getGirlfriend().playAnim('sad');
+ // Girlfriend gets sad if you combo break after hitting 5 notes.
+ if (currentStage != null && currentStage.getGirlfriend() != null)
+ if (combo > 5 && currentStage.getGirlfriend().hasAnimation('sad'))
+ currentStage.getGirlfriend().playAnimation('sad');
if (combo != 0)
{
@@ -1432,7 +1446,7 @@ class PlayState extends MusicBeatState implements IHook
private function popUpScore(strumtime:Float, daNote:Note):Void
{
var noteDiff:Float = Math.abs(strumtime - Conductor.songPosition);
- // boyfriend.playAnim('hey');
+ // boyfriend.playAnimation('hey');
vocals.volume = 1;
var score:Int = 350;
@@ -1465,15 +1479,6 @@ class PlayState extends MusicBeatState implements IHook
health += healthMulti;
- // TODO: Redo note hit logic to make sure this always gets called
- var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, true);
- dispatchEvent(event);
-
- if (event.eventCanceled)
- {
- // TODO: Do a thing!
- }
-
if (isSick)
{
var noteSplash:NoteSplash = grpNoteSplashes.recycle(NoteSplash);
@@ -1492,61 +1497,44 @@ class PlayState extends MusicBeatState implements IHook
comboPopUps.displayCombo(combo);
}
- function cameraMovement()
+ function controlCamera()
{
if (currentStage == null)
return;
- if (cameraFollowPoint.x != currentStage.getDad().getMidpoint().x + 150 && !cameraRightSide)
+ var isFocusedOnDad = cameraFollowPoint.x == currentStage.getDad().cameraFocusPoint.x;
+ var isFocusedOnBF = cameraFollowPoint.x == currentStage.getBoyfriend().cameraFocusPoint.x;
+
+ if (cameraRightSide && !isFocusedOnBF)
{
- cameraFollowPoint.setPosition(currentStage.getDad().getMidpoint().x + 150, currentStage.getDad().getMidpoint().y - 100);
- // camFollow.setPosition(lucky.getMidpoint().x - 120, lucky.getMidpoint().y + 210);
+ // Focus the camera on the player.
+ cameraFollowPoint.setPosition(currentStage.getBoyfriend().cameraFocusPoint.x, currentStage.getBoyfriend().cameraFocusPoint.y);
- switch (currentStage.getDad().curCharacter)
+ // TODO: Un-hardcode this.
+ if (currentSong.song.toLowerCase() == 'tutorial')
+ FlxTween.tween(FlxG.camera, {zoom: 1 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut});
+ }
+ else if (!cameraRightSide && !isFocusedOnDad)
+ {
+ // Focus the camera on the opponent.
+ cameraFollowPoint.setPosition(currentStage.getDad().cameraFocusPoint.x, currentStage.getDad().cameraFocusPoint.y);
+
+ // TODO: Un-hardcode this stuff.
+ if (currentStage.getDad().characterId == 'mom')
{
- case 'mom':
- cameraFollowPoint.y = currentStage.getDad().getMidpoint().y;
- case 'senpai' | 'senpai-angry':
- cameraFollowPoint.y = currentStage.getDad().getMidpoint().y - 430;
- cameraFollowPoint.x = currentStage.getDad().getMidpoint().x - 100;
- }
-
- if (currentStage.getDad().curCharacter == 'mom')
vocals.volume = 1;
+ }
if (currentSong.song.toLowerCase() == 'tutorial')
tweenCamIn();
}
-
- if (cameraRightSide && cameraFollowPoint.x != currentStage.getBoyfriend().getMidpoint().x - 100)
- {
- cameraFollowPoint.setPosition(currentStage.getBoyfriend().getMidpoint().x - 100, currentStage.getBoyfriend().getMidpoint().y - 100);
-
- switch (currentStageId)
- {
- case 'limo':
- cameraFollowPoint.x = currentStage.getBoyfriend().getMidpoint().x - 300;
- case 'mall':
- cameraFollowPoint.y = currentStage.getBoyfriend().getMidpoint().y - 200;
- case 'school' | 'schoolEvil':
- cameraFollowPoint.x = currentStage.getBoyfriend().getMidpoint().x - 200;
- cameraFollowPoint.y = currentStage.getBoyfriend().getMidpoint().y - 200;
- }
-
- if (currentSong.song.toLowerCase() == 'tutorial')
- FlxTween.tween(FlxG.camera, {zoom: 1 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut});
- }
}
- public var test:(PlayState) -> Void = function(instance:PlayState)
- {
- trace('test');
- trace(instance.currentStageId);
- };
-
- @:hookable
public function keyShit(test:Bool):Void
{
+ if (PlayState.instance == null)
+ return;
+
// control arrays, order L D R U
var holdArray:Array = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT];
var pressArray:Array = [
@@ -1630,7 +1618,7 @@ class PlayState extends MusicBeatState implements IHook
for (shit in 0...pressArray.length)
{ // if a direction is hit that shouldn't be
if (pressArray[shit] && !directionList.contains(shit))
- PlayState.instance.noteMiss(shit);
+ PlayState.instance.ghostNoteMiss(shit);
}
for (coolNote in possibleNotes)
{
@@ -1640,26 +1628,20 @@ class PlayState extends MusicBeatState implements IHook
}
else
{
+ // HNGGG I really want to add an option for ghost tapping
for (shit in 0...pressArray.length)
if (pressArray[shit])
- PlayState.instance.noteMiss(shit);
+ PlayState.instance.ghostNoteMiss(shit, false);
}
}
if (PlayState.instance == null || PlayState.instance.currentStage == null)
return;
- if (PlayState.instance.currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true))
- {
- if (PlayState.instance.currentStage.getBoyfriend().animation != null
- && PlayState.instance.currentStage.getBoyfriend().animation.curAnim.name.startsWith('sing')
- && !PlayState.instance.currentStage.getBoyfriend().animation.curAnim.name.endsWith('miss'))
- {
- PlayState.instance.currentStage.getBoyfriend().playAnim('idle');
- }
- }
for (keyId => isPressed in pressArray)
{
+ if (playerStrumline == null)
+ continue;
var arrow:StrumlineArrow = PlayState.instance.playerStrumline.getArrow(keyId);
if (isPressed && arrow.animation.curAnim.name != 'confirm')
@@ -1673,33 +1655,78 @@ class PlayState extends MusicBeatState implements IHook
}
}
- function noteMiss(direction:NoteDir = 1):Void
+ /**
+ * Called when a player presses a key with no note present.
+ * Scripts can modify the amount of health/score lost, whether player animations or sounds are used,
+ * or even cancel the event entirely.
+ *
+ * @param direction
+ * @param hasPossibleNotes
+ */
+ function ghostNoteMiss(direction:NoteType = 1, hasPossibleNotes:Bool = true):Void
{
- // whole function used to be encased in if (!boyfriend.stunned)
- health -= 0.07;
- killCombo();
+ var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in.
+ hasPossibleNotes, // Whether there was a note you could have hit.
+ - 0.035 * 2, // How much health to add (negative).
+ - 10 // Amount of score to add (negative).
+ );
+ dispatchEvent(event);
+
+ // Calling event.cancelEvent() skips animations and penalties. Neat!
+ if (event.eventCanceled)
+ return;
+
+ health += event.healthChange;
+ if (!isPracticeMode)
+ songScore += event.scoreChange;
+
+ if (event.playSound)
+ {
+ vocals.volume = 0;
+ FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
+ }
+ }
+
+ function noteMiss(note:Note):Void
+ {
+ var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, true);
+ dispatchEvent(event);
+ // Calling event.cancelEvent() skips all the other logic! Neat!
+ if (event.eventCanceled)
+ return;
+
+ health -= 0.0775;
if (!isPracticeMode)
songScore -= 10;
-
vocals.volume = 0;
- FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
+ killCombo();
- currentStage.getBoyfriend().playAnim('sing' + direction.nameUpper + 'miss', true);
+ note.active = false;
+ note.visible = false;
+
+ note.kill();
+ activeNotes.remove(note, true);
+ note.destroy();
}
function goodNoteHit(note:Note):Void
{
if (!note.wasGoodHit)
{
+ var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, true);
+ dispatchEvent(event);
+
+ // Calling event.cancelEvent() skips all the other logic! Neat!
+ if (event.eventCanceled)
+ return;
+
if (!note.isSustainNote)
{
combo += 1;
popUpScore(note.data.strumTime, note);
}
- currentStage.getBoyfriend().playAnim('sing' + note.dirNameUpper, true);
-
playerStrumline.getArrow(note.data.noteData).playAnimation('confirm', true);
note.wasGoodHit = true;
@@ -1726,7 +1753,8 @@ class PlayState extends MusicBeatState implements IHook
resyncVocals();
}
- dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep));
+ iconP1.onStepHit(curStep);
+ iconP2.onStepHit(curStep);
return true;
}
@@ -1742,6 +1770,13 @@ class PlayState extends MusicBeatState implements IHook
activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
}
+ // Moving this code into the `beatHit` function allows for scripts and modules to control the camera better.
+ if (generatedMusic && SongLoad.getSong()[Std.int(curStep / 16)] != null)
+ {
+ cameraRightSide = SongLoad.getSong()[Std.int(curStep / 16)].mustHitSection;
+ controlCamera();
+ }
+
if (SongLoad.getSong()[Math.floor(curStep / 16)] != null)
{
if (SongLoad.getSong()[Math.floor(curStep / 16)].changeBPM)
@@ -1768,11 +1803,31 @@ class PlayState extends MusicBeatState implements IHook
}
}
- // Make the health icons bump (the update function causes them to lerp back down).
- iconP1.setGraphicSize(Std.int(iconP1.width + 30));
- iconP2.setGraphicSize(Std.int(iconP2.width + 30));
- iconP1.updateHitbox();
- iconP2.updateHitbox();
+ // That combo counter that got spoiled that one time.
+ // Comes with NEAT visual and audio effects.
+
+ // bruh this var is bonkers i thot it was a function lmfaooo
+
+ var shouldShowComboText:Bool = (curBeat % 8 == 7) // End of measure. TODO: Is this always the correct time?
+ && (SongLoad.getSong()[Std.int(curStep / 16)].mustHitSection) // Current section is BF's.
+ && (combo > 5) // Don't want to show on small combos.
+ && ((SongLoad.getSong().length < Std.int(curStep / 16)) // Show at the end of the song.
+ || (!SongLoad.getSong()[Std.int(curStep / 16) + 1].mustHitSection) // Or when the next section is Dad's.
+ );
+
+ if (shouldShowComboText)
+ {
+ var animShit:ComboCounter = new ComboCounter(-100, 300, combo);
+ animShit.scrollFactor.set(0.6, 0.6);
+ add(animShit);
+
+ var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
+
+ new FlxTimer().start(((Conductor.crochet / 1000) * 1.25) - frameShit, function(tmr)
+ {
+ animShit.forceFinish();
+ });
+ }
// Make the characters dance on the beat
danceOnBeat();
@@ -1790,35 +1845,19 @@ class PlayState extends MusicBeatState implements IHook
if (currentStage == null)
return;
- if (curBeat % gfSpeed == 0)
- currentStage.getGirlfriend().dance();
-
- if (curBeat % 2 == 0)
- {
- if (currentStage.getBoyfriend().animation != null && !currentStage.getBoyfriend().animation.curAnim.name.startsWith("sing"))
- currentStage.getBoyfriend().playAnim('idle');
- if (currentStage.getDad().animation != null && !currentStage.getDad().animation.curAnim.name.startsWith("sing"))
- currentStage.getDad().dance();
- }
- else if (currentStage.getDad().curCharacter == 'spooky')
- {
- if (!currentStage.getDad().animation.curAnim.name.startsWith("sing"))
- currentStage.getDad().dance();
- }
-
if (curBeat % 8 == 7 && currentSong.song == 'Bopeebo')
{
- currentStage.getBoyfriend().playAnim('hey', true);
+ currentStage.getBoyfriend().playAnimation('hey', true);
}
if (curBeat % 16 == 15
&& currentSong.song == 'Tutorial'
- && currentStage.getDad().curCharacter == 'gf'
+ && currentStage.getDad().characterId == 'gf'
&& curBeat > 16
&& curBeat < 48)
{
- currentStage.getBoyfriend().playAnim('hey', true);
- currentStage.getDad().playAnim('cheer', true);
+ currentStage.getBoyfriend().playAnimation('hey', true);
+ currentStage.getDad().playAnimation('cheer', true);
}
}
@@ -1911,7 +1950,7 @@ class PlayState extends MusicBeatState implements IHook
if (event.eventCanceled)
return;
- if (FlxG.sound.music != null && !startingSong)
+ if (FlxG.sound.music != null && !startingSong && !isInCutscene)
resyncVocals();
// Resume the countdown.
@@ -1935,13 +1974,15 @@ class PlayState extends MusicBeatState implements IHook
*/
function startCountdown():Void
{
+ var result = Countdown.performCountdown(currentStageId.startsWith('school'));
+ if (!result)
+ return;
+
isInCutscene = false;
camHUD.visible = true;
talking = false;
buildStrumlines();
-
- Countdown.performCountdown(currentStageId.startsWith('school'));
}
override function dispatchEvent(event:ScriptEvent):Void
@@ -1955,6 +1996,10 @@ class PlayState extends MusicBeatState implements IHook
// Dispatch event to stage script.
ScriptEventDispatcher.callEvent(currentStage, event);
+ // Dispatch event to character script(s).
+ if (currentStage != null)
+ currentStage.dispatchToCharacters(event);
+
// TODO: Dispatch event to song script
}
@@ -1978,9 +2023,10 @@ class PlayState extends MusicBeatState implements IHook
/**
* Resets the camera's zoom level and focus point.
*/
- function resetCamera():Void
+ public function resetCamera():Void
{
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04);
+ FlxG.camera.targetOffset.set();
FlxG.camera.zoom = defaultCameraZoom;
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
}
@@ -2009,11 +2055,17 @@ class PlayState extends MusicBeatState implements IHook
/**
* This function is called whenever Flixel switches switching to a new FlxState.
+ * @return Whether to actually switch to the new state.
*/
override function switchTo(nextState:FlxState):Bool
{
- performCleanup();
+ var result = super.switchTo(nextState);
- return super.switchTo(nextState);
+ if (result)
+ {
+ performCleanup();
+ }
+
+ return result;
}
}
diff --git a/source/funkin/play/VanillaCutscenes.hx b/source/funkin/play/VanillaCutscenes.hx
index 6985f90b4..71587eeb9 100644
--- a/source/funkin/play/VanillaCutscenes.hx
+++ b/source/funkin/play/VanillaCutscenes.hx
@@ -2,6 +2,7 @@ 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;
@@ -9,7 +10,7 @@ import flixel.util.FlxTimer;
/**
* Static methods for playing cutscenes in the PlayState.
- * TODO: Softcode this shit!!!!!1!
+ * TODO: Un-hardcode this shit!!!!!1!
*/
class VanillaCutscenes
{
@@ -64,7 +65,7 @@ class VanillaCutscenes
@:privateAccess
PlayState.instance.startCountdown();
@:privateAccess
- PlayState.instance.cameraMovement();
+ PlayState.instance.controlCamera();
}
public static function playHorrorStartCutscene()
diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
new file mode 100644
index 000000000..22b95bc06
--- /dev/null
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -0,0 +1,446 @@
+package funkin.play.character;
+
+import flixel.math.FlxPoint;
+import funkin.Note.NoteDir;
+import funkin.modding.events.ScriptEvent;
+import funkin.play.character.CharacterData.CharacterDataParser;
+import funkin.play.stage.Bopper;
+
+using StringTools;
+
+/**
+ * A Character is a stage prop which bops to the music as well as controlled by the strumlines.
+ *
+ * Remember: The character's origin is at its FEET. (horizontal center, vertical bottom)
+ */
+class BaseCharacter extends Bopper
+{
+ // Metadata about a character.
+ public var characterId(default, null):String;
+ public var characterName(default, null):String;
+
+ /**
+ * Whether the player is an active character (Boyfriend) or not.
+ */
+ public var characterType:CharacterType = OTHER;
+
+ /**
+ * Tracks how long, in seconds, the character has been playing the current `sing` animation.
+ * This is used to ensure that characters play the `sing` animations for at least one beat,
+ * preventing them from reverting to the `idle` animation between notes.
+ */
+ public var holdTimer:Float = 0;
+
+ public var isDead:Bool = false;
+ public var debugMode:Bool = false;
+
+ final _data:CharacterData;
+ final singTimeCrochet:Float;
+
+ /**
+ * The offset between the corner of the sprite and the origin of the sprite (at the character's feet).
+ * cornerPosition = stageData - characterOrigin
+ */
+ public var characterOrigin(get, null):FlxPoint;
+
+ function get_characterOrigin():FlxPoint
+ {
+ var xPos = (width / 2); // Horizontal center
+ var yPos = (height); // Vertical bottom
+ return new FlxPoint(xPos, yPos);
+ }
+
+ /**
+ * The absolute position of the top-left of the character.
+ * @return
+ */
+ public var cornerPosition(get, null):FlxPoint;
+
+ function get_cornerPosition():FlxPoint
+ {
+ return new FlxPoint(x, y);
+ }
+
+ /**
+ * The absolute position of the character's feet, at the bottom-center of the sprite.
+ */
+ public var feetPosition(get, null):FlxPoint;
+
+ function get_feetPosition():FlxPoint
+ {
+ return new FlxPoint(x + characterOrigin.x, y + characterOrigin.y);
+ }
+
+ /**
+ * Returns the point the camera should focus on.
+ * Should be approximately centered on the character, and should not move based on the current animation.
+ *
+ * Set the position of this rather than reassigning it, so that anything referencing it will not be affected.
+ */
+ public var cameraFocusPoint(default, null):FlxPoint = new FlxPoint(0, 0);
+
+ override function set_animOffsets(value:Array)
+ {
+ if (animOffsets == null)
+ animOffsets = [0, 0];
+ if (animOffsets == value)
+ return value;
+
+ var xDiff = animOffsets[0] - value[0];
+ var yDiff = animOffsets[1] - value[1];
+
+ // Call the super function so that camera focus point is not affected.
+ super.set_x(this.x + xDiff);
+ super.set_y(this.y + yDiff);
+
+ return animOffsets = value;
+ }
+
+ /**
+ * If the x position changes, other than via changing the animation offset,
+ * then we need to update the camera focus point.
+ */
+ override function set_x(value:Float):Float
+ {
+ if (value == this.x)
+ return value;
+
+ var xDiff = value - this.x;
+ this.cameraFocusPoint.x += xDiff;
+
+ return super.set_x(value);
+ }
+
+ /**
+ * If the y position changes, other than via changing the animation offset,
+ * then we need to update the camera focus point.
+ */
+ override function set_y(value:Float):Float
+ {
+ if (value == this.y)
+ return value;
+
+ var yDiff = value - this.y;
+ this.cameraFocusPoint.y += yDiff;
+
+ return super.set_y(value);
+ }
+
+ public function new(id:String)
+ {
+ super();
+ this.characterId = id;
+
+ _data = CharacterDataParser.fetchCharacterData(this.characterId);
+ if (_data == null)
+ {
+ throw 'Could not find character data for characterId: $characterId';
+ }
+ else
+ {
+ this.characterName = _data.name;
+ this.singTimeCrochet = _data.singTime;
+ this.globalOffsets = _data.offsets;
+ this.flipX = _data.flipX;
+ }
+
+ shouldBop = false;
+ }
+
+ /**
+ * Set the sprite scale to the appropriate value.
+ * @param scale
+ */
+ function setScale(scale:Null):Void
+ {
+ if (scale == null)
+ scale = 1.0;
+
+ var feetPos:FlxPoint = feetPosition;
+ this.scale.x = scale;
+ this.scale.y = scale;
+ this.updateHitbox();
+ // Reposition with newly scaled sprite.
+ this.x = feetPos.x - characterOrigin.x + globalOffsets[0];
+ this.y = feetPos.y - characterOrigin.y + globalOffsets[1];
+ }
+
+ /**
+ * The per-character camera offset.
+ */
+ var characterCameraOffsets(get, null):Array;
+
+ function get_characterCameraOffsets():Array
+ {
+ return _data.cameraOffsets;
+ }
+
+ override function onCreate(event:ScriptEvent):Void
+ {
+ // Camera focus point
+ var charCenterX = this.x + this.width / 2;
+ var charCenterY = this.y + this.height / 2;
+ this.cameraFocusPoint = new FlxPoint(charCenterX + _data.cameraOffsets[0], charCenterY + _data.cameraOffsets[1]);
+ super.onCreate(event);
+ }
+
+ public function initHealthIcon(isOpponent:Bool):Void
+ {
+ if (!isOpponent)
+ {
+ PlayState.instance.iconP1.characterId = _data.healthIcon.id;
+ PlayState.instance.iconP1.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
+ PlayState.instance.iconP1.offset.x = _data.healthIcon.offsets[0];
+ PlayState.instance.iconP1.offset.y = _data.healthIcon.offsets[1];
+ }
+ else
+ {
+ PlayState.instance.iconP2.characterId = _data.healthIcon.id;
+ PlayState.instance.iconP2.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
+ PlayState.instance.iconP2.offset.x = _data.healthIcon.offsets[0];
+ PlayState.instance.iconP2.offset.y = _data.healthIcon.offsets[1];
+ }
+ }
+
+ public override function onUpdate(event:UpdateScriptEvent):Void
+ {
+ super.onUpdate(event);
+
+ // Reset hold timer for each note pressed.
+ if (justPressedNote() && this.characterType == BF)
+ {
+ holdTimer = 0;
+ }
+
+ if (isDead)
+ {
+ playDeathAnimation();
+ }
+
+ if (hasAnimation('idle-end') && getCurrentAnimation() == "idle" && isAnimationFinished())
+ playAnimation('idle-end');
+ if (hasAnimation('singLEFT-end') && getCurrentAnimation() == "singLEFT" && isAnimationFinished())
+ playAnimation('singLEFT-end');
+ if (hasAnimation('singDOWN-end') && getCurrentAnimation() == "singDOWN" && isAnimationFinished())
+ playAnimation('singDOWN-end');
+ if (hasAnimation('singUP-end') && getCurrentAnimation() == "singUP" && isAnimationFinished())
+ playAnimation('singUP-end');
+ if (hasAnimation('singRIGHT-end') && getCurrentAnimation() == "singRIGHT" && isAnimationFinished())
+ playAnimation('singRIGHT-end');
+
+ // Handle character note hold time.
+ if (getCurrentAnimation().startsWith("sing"))
+ {
+ 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
+
+ // 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.
+ var shouldStopSinging:Bool = (this.characterType == BF) ? !isHoldingNote() : true;
+
+ FlxG.watch.addQuick('singTimeMs-${characterId}', singTimeMs);
+ if (holdTimer > singTimeMs && shouldStopSinging)
+ {
+ trace(getCurrentAnimation());
+ // trace('holdTimer reached ${holdTimer}sec (> ${singTimeMs}), stopping sing animation');
+ holdTimer = 0;
+ dance(true);
+ }
+ }
+ else
+ {
+ holdTimer = 0;
+ // super.onBeatHit handles the regular `dance()` calls.
+ }
+ FlxG.watch.addQuick('holdTimer-${characterId}', holdTimer);
+ }
+
+ /**
+ * Since no `onBeatHit` or `dance` calls happen in GameOverSubstate,
+ * this regularly gets called instead.
+ */
+ public function playDeathAnimation(force:Bool = false):Void
+ {
+ if (force || (getCurrentAnimation().startsWith("firstDeath") && isAnimationFinished()))
+ {
+ playAnimation("deathLoop");
+ }
+ }
+
+ override function dance(force:Bool = false)
+ {
+ // Prevent default dancing behavior.
+ if (debugMode)
+ return;
+
+ if (!force)
+ {
+ 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;
+ }
+
+ // Otherwise, fallback to the super dance() method, which handles playing the idle animation.
+ super.dance();
+ }
+
+ /**
+ * Returns true if the player just pressed a note.
+ * Used when determing whether a the player character should revert to the `idle` animation.
+ * On non-player characters, this should be ignored.
+ */
+ function justPressedNote(player:Int = 1):Bool
+ {
+ // Returns true if at least one of LEFT, DOWN, UP, or RIGHT is being held.
+ switch (player)
+ {
+ case 1:
+ return [
+ PlayerSettings.player1.controls.NOTE_LEFT_P,
+ PlayerSettings.player1.controls.NOTE_DOWN_P,
+ PlayerSettings.player1.controls.NOTE_UP_P,
+ PlayerSettings.player1.controls.NOTE_RIGHT_P,
+ ].contains(true);
+ case 2:
+ return [
+ PlayerSettings.player2.controls.NOTE_LEFT_P,
+ PlayerSettings.player2.controls.NOTE_DOWN_P,
+ PlayerSettings.player2.controls.NOTE_UP_P,
+ PlayerSettings.player2.controls.NOTE_RIGHT_P,
+ ].contains(true);
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the player is holding a note.
+ * Used when determing whether a the player character should revert to the `idle` animation.
+ * On non-player characters, this should be ignored.
+ */
+ function isHoldingNote(player:Int = 1):Bool
+ {
+ // Returns true if at least one of LEFT, DOWN, UP, or RIGHT is being held.
+ switch (player)
+ {
+ case 1:
+ return [
+ PlayerSettings.player1.controls.NOTE_LEFT,
+ PlayerSettings.player1.controls.NOTE_DOWN,
+ PlayerSettings.player1.controls.NOTE_UP,
+ PlayerSettings.player1.controls.NOTE_RIGHT,
+ ].contains(true);
+ case 2:
+ return [
+ PlayerSettings.player2.controls.NOTE_LEFT,
+ PlayerSettings.player2.controls.NOTE_DOWN,
+ PlayerSettings.player2.controls.NOTE_UP,
+ PlayerSettings.player2.controls.NOTE_RIGHT,
+ ].contains(true);
+ }
+ return false;
+ }
+
+ /**
+ * Every time a note is hit, check if the note is from the same strumline.
+ * If it is, then play the sing animation.
+ */
+ public override function onNoteHit(event:NoteScriptEvent)
+ {
+ super.onNoteHit(event);
+
+ if (event.note.mustPress && characterType == BF)
+ {
+ // If the note is from the same strumline, play the sing animation.
+ this.playSingAnimation(event.note.data.dir, false, event.note.data.altNote);
+ holdTimer = 0;
+ }
+ else if (!event.note.mustPress && characterType == DAD)
+ {
+ // If the note is from the same strumline, play the sing animation.
+ this.playSingAnimation(event.note.data.dir, false, event.note.data.altNote);
+ holdTimer = 0;
+ }
+ }
+
+ /**
+ * Every time a note is missed, check if the note is from the same strumline.
+ * If it is, then play the sing animation.
+ */
+ public override function onNoteMiss(event:NoteScriptEvent)
+ {
+ super.onNoteMiss(event);
+
+ if (event.note.mustPress && characterType == BF)
+ {
+ // If the note is from the same strumline, play the sing animation.
+ this.playSingAnimation(event.note.data.dir, true, event.note.data.altNote);
+ }
+ else if (!event.note.mustPress && characterType == DAD)
+ {
+ // If the note is from the same strumline, play the sing animation.
+ this.playSingAnimation(event.note.data.dir, true, event.note.data.altNote);
+ }
+ }
+
+ /**
+ * Every time a wrong key is pressed, play the miss animation if we are Boyfriend.
+ */
+ public override function onNoteGhostMiss(event:GhostMissNoteScriptEvent)
+ {
+ super.onNoteGhostMiss(event);
+
+ if (event.eventCanceled || !event.playAnim)
+ {
+ // Skipping...
+ return;
+ }
+
+ if (characterType == BF)
+ {
+ trace('Playing ghost miss animation...');
+ // If the note is from the same strumline, play the sing animation.
+ this.playSingAnimation(event.dir, true, null);
+ }
+ }
+
+ public override function onDestroy(event:ScriptEvent):Void
+ {
+ this.characterType = OTHER;
+ }
+
+ /**
+ * Play the appropriate singing animation, for the given note direction.
+ * @param dir The direction of the note.
+ * @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
+ {
+ 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);
+ }
+}
+
+enum CharacterType
+{
+ BF;
+ DAD;
+ GF;
+ OTHER;
+}
diff --git a/source/funkin/play/character/Character.hx b/source/funkin/play/character/Character.hx
deleted file mode 100644
index c5ae1014a..000000000
--- a/source/funkin/play/character/Character.hx
+++ /dev/null
@@ -1,9 +0,0 @@
-package funkin.play.character;
-
-enum CharacterType
-{
- BF;
- GF;
- DAD;
- OTHER;
-}
diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
new file mode 100644
index 000000000..5fa55ada4
--- /dev/null
+++ b/source/funkin/play/character/CharacterData.hx
@@ -0,0 +1,591 @@
+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.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 haxe.Json;
+import openfl.utils.Assets;
+
+using StringTools;
+
+class CharacterDataParser
+{
+ /**
+ * The current version string for the stage data format.
+ * Handle breaking changes by incrementing this value
+ * and adding migration to the `migrateStageData()` function.
+ */
+ 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";
+
+ static final characterCache:Map = new Map();
+ static final characterScriptedClass:Map = new Map();
+
+ static final DEFAULT_CHAR_ID:String = 'UNKNOWN';
+
+ /**
+ * Parses and preloads the game's stage data and scripts when the game starts.
+ *
+ * If you want to force stages to be reloaded, you can just call this function again.
+ */
+ public static function loadCharacterCache():Void
+ {
+ // Clear any stages that are cached if there were any.
+ clearCharacterCache();
+ trace("[CHARDATA] Loading character cache...");
+
+ //
+ // UNSCRIPTED CHARACTERS
+ //
+ var charIdList:Array = DataAssets.listDataFilesInPath('characters/');
+ var unscriptedCharIds:Array = charIdList.filter(function(charId:String):Bool
+ {
+ return !characterCache.exists(charId);
+ });
+ trace(' Fetching data for ${unscriptedCharIds.length} characters...');
+ for (charId in unscriptedCharIds)
+ {
+ try
+ {
+ var charData:CharacterData = parseCharacterData(charId);
+ if (charData != null)
+ {
+ trace(' Loaded character data: ${charId}');
+ characterCache.set(charId, charData);
+ }
+ }
+ catch (e)
+ {
+ // Assume error was already logged.
+ continue;
+ }
+ }
+
+ //
+ // SCRIPTED CHARACTERS
+ //
+
+ // Fuck I wish scripted classes supported static functions.
+
+ var scriptedCharClassNames1:Array = ScriptedSparrowCharacter.listScriptClasses();
+ if (scriptedCharClassNames1.length > 0)
+ {
+ trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
+ for (charCls in scriptedCharClassNames1)
+ {
+ var character = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
+ characterScriptedClass.set(character.characterId, charCls);
+ }
+ }
+
+ var scriptedCharClassNames2:Array = ScriptedPackerCharacter.listScriptClasses();
+ if (scriptedCharClassNames2.length > 0)
+ {
+ trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
+ for (charCls in scriptedCharClassNames2)
+ {
+ var character = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID);
+ characterScriptedClass.set(character.characterId, charCls);
+ }
+ }
+
+ var scriptedCharClassNames3:Array = ScriptedMultiSparrowCharacter.listScriptClasses();
+ if (scriptedCharClassNames3.length > 0)
+ {
+ trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
+ for (charCls in scriptedCharClassNames3)
+ {
+ var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
+ if (character == null)
+ {
+ trace(' Failed to instantiate scripted character: ${charCls}');
+ continue;
+ }
+ 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
+ {
+ return !(scriptedCharClassNames1.contains(charCls)
+ || scriptedCharClassNames2.contains(charCls)
+ || scriptedCharClassNames3.contains(charCls));
+ });
+
+ if (scriptedCharClassNames.length > 0)
+ {
+ trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
+ for (charCls in scriptedCharClassNames)
+ {
+ var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
+ if (character == null)
+ {
+ trace(' Failed to instantiate scripted character: ${charCls}');
+ continue;
+ }
+ else
+ {
+ trace(' Successfully instantiated scripted character: ${charCls}');
+ characterScriptedClass.set(character.characterId, charCls);
+ }
+ }
+ }
+
+ trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
+ }
+
+ public static function fetchCharacter(charId:String):Null
+ {
+ if (charId == null || charId == '')
+ {
+ // Gracefully handle songs that don't use this character.
+ return null;
+ }
+
+ if (characterCache.exists(charId))
+ {
+ var charData:CharacterData = characterCache.get(charId);
+ var charScriptClass:String = characterScriptedClass.get(charId);
+
+ var char:BaseCharacter;
+
+ if (charScriptClass != null)
+ {
+ 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);
+ }
+ }
+ 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('[CHARDATA] 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('[CHARDATA] Failed to build character, not found in cache: ${charId}');
+ return null;
+ }
+ }
+
+ public static function fetchCharacterData(charId:String):Null
+ {
+ if (characterCache.exists(charId))
+ {
+ return characterCache.get(charId);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ static function clearCharacterCache():Void
+ {
+ if (characterCache != null)
+ {
+ characterCache.clear();
+ }
+ if (characterScriptedClass != null)
+ {
+ characterScriptedClass.clear();
+ }
+ }
+
+ /**
+ * Load a character's JSON file, parse its data, and return it.
+ *
+ * @param charId The character to load.
+ * @return The character data, or null if validation failed.
+ */
+ public static function parseCharacterData(charId:String):Null
+ {
+ var rawJson:String = loadCharacterFile(charId);
+
+ var charData:CharacterData = migrateCharacterData(rawJson, charId);
+
+ return validateCharacterData(charId, charData);
+ }
+
+ static function loadCharacterFile(charPath:String):String
+ {
+ var charFilePath:String = Paths.json('characters/${charPath}');
+ var rawJson = StringTools.trim(Assets.getText(charFilePath));
+
+ while (!StringTools.endsWith(rawJson, "}"))
+ {
+ rawJson = rawJson.substr(0, rawJson.length - 1);
+ }
+
+ return rawJson;
+ }
+
+ static function migrateCharacterData(rawJson:String, charId:String)
+ {
+ // If you update the character data format in a breaking way,
+ // handle migration here by checking the `version` value.
+
+ try
+ {
+ var charData:CharacterData = cast Json.parse(rawJson);
+ return charData;
+ }
+ catch (e)
+ {
+ trace(' Error parsing data for character: ${charId}');
+ trace(' ${e}');
+ return null;
+ }
+ }
+
+ /**
+ * The default time the character should sing for, in beats.
+ * Values that are too low will cause the character to stop singing between notes.
+ * Originally, this value was set to 1, but it was changed to 2 because that became
+ * too low after some other code changes.
+ */
+ static final DEFAULT_SINGTIME:Float = 2.0;
+
+ static final DEFAULT_DANCEEVERY:Int = 1;
+ static final DEFAULT_FLIPX:Bool = false;
+ static final DEFAULT_FLIPY:Bool = false;
+ 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_OFFSETS:Array = [0, 0];
+ static final DEFAULT_HEALTHICON_OFFSETS:Array = [0, 25];
+ 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";
+
+ /**
+ * Set unspecified parameters to their defaults.
+ * If the parameter is mandatory, print an error message.
+ * @param id
+ * @param input
+ * @return The validated character data
+ */
+ static function validateCharacterData(id:String, input:CharacterData):Null
+ {
+ if (input == null)
+ {
+ // trace('[CHARDATA] ERROR: Could not parse character data for "${id}".');
+ return null;
+ }
+
+ if (input.version == null)
+ {
+ trace('[CHARDATA] WARN: No semantic version specified for character data file "$id", assuming ${CHARACTER_DATA_VERSION}');
+ input.version = CHARACTER_DATA_VERSION;
+ }
+
+ if (!VersionUtil.validateVersion(input.version, CHARACTER_DATA_VERSION_RULE))
+ {
+ trace('[CHARDATA] ERROR: Could not load character data for "$id": bad version (got ${input.version}, expected ${CHARACTER_DATA_VERSION_RULE})');
+ return null;
+ }
+
+ if (input.name == null)
+ {
+ trace('[CHARDATA] WARN: Character data for "$id" missing name');
+ input.name = DEFAULT_NAME;
+ }
+
+ if (input.renderType == null)
+ {
+ input.renderType = DEFAULT_RENDERTYPE;
+ }
+
+ if (input.assetPath == null)
+ {
+ trace('[CHARDATA] ERROR: Could not load character data for "$id": missing assetPath');
+ return null;
+ }
+
+ if (input.offsets == null)
+ {
+ input.offsets = DEFAULT_OFFSETS;
+ }
+
+ if (input.cameraOffsets == null)
+ {
+ input.cameraOffsets = DEFAULT_OFFSETS;
+ }
+
+ if (input.healthIcon == null)
+ {
+ input.healthIcon = {
+ id: null,
+ scale: null,
+ offsets: null
+ };
+ }
+
+ if (input.healthIcon.id == null)
+ {
+ input.healthIcon.id = id;
+ }
+
+ if (input.healthIcon.scale == null)
+ {
+ input.healthIcon.scale = DEFAULT_SCALE;
+ }
+
+ if (input.healthIcon.offsets == null)
+ {
+ input.healthIcon.offsets = DEFAULT_OFFSETS;
+ }
+
+ if (input.startingAnimation == null)
+ {
+ input.startingAnimation = DEFAULT_STARTINGANIM;
+ }
+
+ if (input.scale == null)
+ {
+ input.scale = DEFAULT_SCALE;
+ }
+
+ if (input.isPixel == null)
+ {
+ input.isPixel = DEFAULT_ISPIXEL;
+ }
+
+ if (input.danceEvery == null)
+ {
+ input.danceEvery = DEFAULT_DANCEEVERY;
+ }
+
+ if (input.singTime == null)
+ {
+ input.singTime = DEFAULT_SINGTIME;
+ }
+
+ if (input.animations == null || input.animations.length == 0)
+ {
+ trace('[CHARDATA] ERROR: Could not load character data for "$id": missing animations');
+ input.animations = [];
+ }
+
+ if (input.flipX == null)
+ {
+ input.flipX = DEFAULT_FLIPX;
+ }
+
+ if (input.animations.length == 0 && input.startingAnimation != null)
+ {
+ return null;
+ }
+
+ for (inputAnimation in input.animations)
+ {
+ if (inputAnimation.name == null)
+ {
+ trace('[CHARDATA] ERROR: Could not load character data for "$id": missing animation name for prop "${input.name}"');
+ return null;
+ }
+
+ if (inputAnimation.frameRate == null)
+ {
+ inputAnimation.frameRate = DEFAULT_FRAMERATE;
+ }
+
+ if (inputAnimation.offsets == null)
+ {
+ inputAnimation.offsets = DEFAULT_OFFSETS;
+ }
+
+ if (inputAnimation.looped == null)
+ {
+ inputAnimation.looped = DEFAULT_LOOP;
+ }
+
+ if (inputAnimation.flipX == null)
+ {
+ inputAnimation.flipX = DEFAULT_FLIPX;
+ }
+
+ if (inputAnimation.flipY == null)
+ {
+ inputAnimation.flipY = DEFAULT_FLIPY;
+ }
+ }
+
+ // All good!
+ return input;
+ }
+}
+
+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
+}
+
+typedef CharacterData =
+{
+ /**
+ * The sematic version number of the character data JSON format.
+ */
+ var version:String;
+
+ /**
+ * The readable name of the character.
+ */
+ var name:String;
+
+ /**
+ * The type of rendering system to use for the character.
+ * @default sparrow
+ */
+ var renderType:CharacterRenderType;
+
+ /**
+ * Behavior varies by render type:
+ * - SPARROW: Path to retrieve both the spritesheet and the XML data from.
+ * - PACKER: Path to retrieve both the spritsheet and the TXT data from.
+ */
+ var assetPath:String;
+
+ /**
+ * The scale of the graphic as a float.
+ * Pro tip: On pixel-art levels, save the sprites small and set this value to 6 or so to save memory.
+ * @default 1
+ */
+ var scale:Null;
+
+ /**
+ * Optional data about the health icon for the character.
+ */
+ var healthIcon:Null;
+
+ /**
+ * The global offset to the character's position, in pixels.
+ * @default [0, 0]
+ */
+ var offsets:Null>;
+
+ /**
+ * The amount to offset the camera by while focusing on this character.
+ * Default value focuses on the character directly.
+ * @default [0, 0]
+ */
+ var cameraOffsets:Array;
+
+ /**
+ * Setting this to true disables anti-aliasing for the character.
+ * @default false
+ */
+ var isPixel:Null;
+
+ /**
+ * The frequency at which the character will play its idle animation, in beats.
+ * Increasing this number will make the character dance less often.
+ *
+ * @default 1
+ */
+ var danceEvery:Null;
+
+ /**
+ * The minimum duration that a character will play a note animation for, in beats.
+ * If this number is too low, you may see the character start playing the idle animation between notes.
+ * If this number is too high, you may see the the character play the sing animation for too long after the notes are gone.
+ *
+ * Examples:
+ * - Daddy Dearest uses a value of `1.525`.
+ * @default 1.0
+ */
+ var singTime:Null;
+
+ /**
+ * An optional array of animations which the character can play.
+ */
+ var animations:Array;
+
+ /**
+ * If animations are used, this is the name of the animation to play first.
+ * @default idle
+ */
+ var startingAnimation:Null;
+
+ /**
+ * Whether or not the whole ass sprite is flipped by default.
+ * Useful for characters that could also be played (Pico)
+ *
+ * @default false
+ */
+ var flipX:Null;
+};
+
+typedef HealthIconData =
+{
+ /**
+ * The ID to use for the health icon.
+ * @default The character's ID
+ */
+ var id:Null;
+
+ /**
+ * The scale of the health icon.
+ */
+ var scale:Null;
+
+ /**
+ * The offset of the health icon, in pixels.
+ * @default [0, 25]
+ */
+ var offsets:Null>;
+}
diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx
new file mode 100644
index 000000000..160bc1c5d
--- /dev/null
+++ b/source/funkin/play/character/MultiSparrowCharacter.hx
@@ -0,0 +1,212 @@
+package funkin.play.character;
+
+import flixel.graphics.frames.FlxFramesCollection;
+import funkin.modding.events.ScriptEvent;
+import funkin.util.assets.FlxAnimationUtil;
+
+/**
+ * For some characters which use Sparrow atlases, the spritesheets need to be split
+ * into multiple files. This character renderer handles by showing the appropriate sprite.
+ *
+ * Examples in base game include BF Holding GF (most of the sprites are in one file
+ * but the death animation is in a separate file).
+ * Only example I can think of in mods is Tricky (which has a separate file for each animation).
+ *
+ * BaseCharacter has game logic, SparrowCharacter has only rendering logic.
+ * KEEP THEM SEPARATE!
+ */
+class MultiSparrowCharacter extends BaseCharacter
+{
+ /**
+ * The actual group which holds all spritesheets this character uses.
+ */
+ private var members:Map = new Map();
+
+ /**
+ * A map between animation names and what frame collection the animation should use.
+ */
+ private var animAssetPath:Map = new Map();
+
+ /**
+ * The current frame collection being used.
+ */
+ private var activeMember:String;
+
+ public function new(id:String)
+ {
+ super(id);
+ }
+
+ override function onCreate(event:ScriptEvent):Void
+ {
+ trace('Creating MULTI SPARROW CHARACTER: ' + this.characterId);
+
+ buildSprites();
+ super.onCreate(event);
+ }
+
+ function buildSprites()
+ {
+ buildSpritesheets();
+ buildAnimations();
+
+ playAnimation(_data.startingAnimation);
+
+ if (_data.isPixel)
+ {
+ this.antialiasing = false;
+ }
+ else
+ {
+ this.antialiasing = true;
+ }
+ }
+
+ function buildSpritesheets()
+ {
+ // Build the list of asset paths to use.
+ // Ignore nulls and duplicates.
+ var assetList = [_data.assetPath];
+ for (anim in _data.animations)
+ {
+ if (anim.assetPath != null && !assetList.contains(anim.assetPath))
+ {
+ assetList.push(anim.assetPath);
+ }
+ animAssetPath.set(anim.name, anim.assetPath);
+ }
+
+ // Load the Sparrow atlas for each path and store them in the members map.
+ for (asset in assetList)
+ {
+ var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared');
+ // If we don't do this, the unused textures will be removed as soon as they're loaded.
+
+ if (texture == null)
+ {
+ trace('Multi-Sparrow atlas could not load texture: ${asset}');
+ }
+ else
+ {
+ trace('Adding multi-sparrow atlas: ${asset}');
+ texture.parent.destroyOnNoUse = false;
+ members.set(asset, texture);
+ }
+ }
+
+ // Use the default frame collection to start.
+ loadFramesByAssetPath(_data.assetPath);
+ }
+
+ /**
+ * Replace this sprite's animation frames with the ones at this asset path.
+ */
+ function loadFramesByAssetPath(assetPath:String):Void
+ {
+ if (_data.assetPath == null)
+ {
+ trace('[ERROR] Multi-Sparrow character has no default asset path!');
+ return;
+ }
+ if (assetPath == null)
+ {
+ // trace('Asset path is null, falling back to default. This is normal!');
+ loadFramesByAssetPath(_data.assetPath);
+ return;
+ }
+
+ if (this.activeMember == assetPath)
+ {
+ // trace('Already using this asset path: ${assetPath}');
+ return;
+ }
+
+ if (members.exists(assetPath))
+ {
+ // Switch to a new set of sprites.
+ trace('Loading frames from asset path: ${assetPath}');
+ this.frames = members.get(assetPath);
+ this.activeMember = assetPath;
+ this.setScale(_data.scale);
+ }
+ else
+ {
+ trace('[WARN] MultiSparrow character ${characterId} could not find asset path: ${assetPath}');
+ }
+ }
+
+ /**
+ * Replace this sprite's animation frames with the ones needed to play this animation.
+ */
+ function loadFramesByAnimName(animName)
+ {
+ if (animAssetPath.exists(animName))
+ {
+ loadFramesByAssetPath(animAssetPath.get(animName));
+ }
+ else
+ {
+ trace('[WARN] MultiSparrow character ${characterId} could not find animation: ${animName}');
+ }
+ }
+
+ function buildAnimations()
+ {
+ trace('[MULTISPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
+
+ // We need to swap to the proper frame collection before adding the animations, I think?
+ for (anim in _data.animations)
+ {
+ loadFramesByAnimName(anim.name);
+ FlxAnimationUtil.addAtlasAnimation(this, anim);
+
+ if (anim.offsets == null)
+ {
+ setAnimationOffsets(anim.name, 0, 0);
+ }
+ else
+ {
+ setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
+ }
+ }
+
+ var animNames = this.animation.getNameList();
+ trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
+ }
+
+ public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void
+ {
+ loadFramesByAnimName(name);
+ super.playAnimation(name, restart, ignoreOther);
+ }
+
+ override function set_frames(value:FlxFramesCollection):FlxFramesCollection
+ {
+ // DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK
+ // WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM
+ // if (animation != null)
+ // {
+ // animation.destroyAnimations();
+ // }
+
+ if (value != null)
+ {
+ graphic = value.parent;
+ this.frames = value;
+ this.frame = value.getByIndex(0);
+ this.numFrames = value.numFrames;
+ resetHelpers();
+ this.bakedRotationAngle = 0;
+ this.animation.frameIndex = 0;
+ graphicLoaded();
+ }
+ else
+ {
+ this.frames = null;
+ this.frame = null;
+ this.graphic = null;
+ }
+
+ return this.frames;
+ }
+}
diff --git a/source/funkin/play/character/PackerCharacter.hx b/source/funkin/play/character/PackerCharacter.hx
new file mode 100644
index 000000000..b7282423e
--- /dev/null
+++ b/source/funkin/play/character/PackerCharacter.hx
@@ -0,0 +1,77 @@
+package funkin.play.character;
+
+import funkin.modding.events.ScriptEvent;
+import flixel.graphics.frames.FlxFramesCollection;
+import funkin.util.assets.FlxAnimationUtil;
+import funkin.play.character.BaseCharacter.CharacterType;
+
+/**
+ * A PackerCharacter is a Character which is rendered by
+ * displaying an animation derived from a Packer spritesheet file.
+ */
+class PackerCharacter extends BaseCharacter
+{
+ public function new(id:String)
+ {
+ super(id);
+ }
+
+ override function onCreate(event:ScriptEvent):Void
+ {
+ trace('Creating PACKER CHARACTER: ' + this.characterId);
+
+ loadSpritesheet();
+ loadAnimations();
+
+ playAnimation(_data.startingAnimation);
+
+ super.onCreate(event);
+ }
+
+ function loadSpritesheet()
+ {
+ trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
+
+ var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath, 'shared');
+ if (tex == null)
+ {
+ trace('Could not load Packer sprite: ${_data.assetPath}');
+ return;
+ }
+
+ this.frames = tex;
+
+ if (_data.isPixel)
+ {
+ this.antialiasing = false;
+ }
+ else
+ {
+ this.antialiasing = true;
+ }
+
+ this.setScale(_data.scale);
+ }
+
+ function loadAnimations()
+ {
+ trace('[PACKERCHAR] Loading ${_data.animations.length} animations for ${characterId}');
+
+ FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
+
+ for (anim in _data.animations)
+ {
+ if (anim.offsets == null)
+ {
+ setAnimationOffsets(anim.name, 0, 0);
+ }
+ else
+ {
+ setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
+ }
+ }
+
+ var animNames = this.animation.getNameList();
+ trace('[PACKERCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
+ }
+}
diff --git a/source/funkin/play/character/ScriptedCharacter.hx b/source/funkin/play/character/ScriptedCharacter.hx
new file mode 100644
index 000000000..1ce8f7f93
--- /dev/null
+++ b/source/funkin/play/character/ScriptedCharacter.hx
@@ -0,0 +1,23 @@
+package funkin.play.character;
+
+import funkin.play.character.PackerCharacter;
+import funkin.play.character.SparrowCharacter;
+import funkin.play.character.MultiSparrowCharacter;
+import funkin.modding.IHook;
+
+/**
+ * Note: Making a scripted class extending BaseCharacter is not recommended.
+ * Do so ONLY if are handling all the character rendering yourself,
+ * and can't use one of the built-in render modes.
+ */
+@:hscriptClass
+class ScriptedBaseCharacter extends BaseCharacter implements IHook {}
+
+@:hscriptClass
+class ScriptedSparrowCharacter extends SparrowCharacter implements IHook {}
+
+@:hscriptClass
+class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements IHook {}
+
+@:hscriptClass
+class ScriptedPackerCharacter extends PackerCharacter implements IHook {}
diff --git a/source/funkin/play/character/SparrowCharacter.hx b/source/funkin/play/character/SparrowCharacter.hx
new file mode 100644
index 000000000..e8191940c
--- /dev/null
+++ b/source/funkin/play/character/SparrowCharacter.hx
@@ -0,0 +1,79 @@
+package funkin.play.character;
+
+import funkin.modding.events.ScriptEvent;
+import funkin.util.assets.FlxAnimationUtil;
+import flixel.graphics.frames.FlxFramesCollection;
+
+/**
+ * A SparrowCharacter is a Character which is rendered by
+ * displaying an animation derived from a SparrowV2 atlas spritesheet file.
+ *
+ * BaseCharacter has game logic, SparrowCharacter has only rendering logic.
+ * KEEP THEM SEPARATE!
+ */
+class SparrowCharacter extends BaseCharacter
+{
+ public function new(id:String)
+ {
+ super(id);
+ }
+
+ override function onCreate(event:ScriptEvent):Void
+ {
+ trace('Creating SPARROW CHARACTER: ' + this.characterId);
+
+ loadSpritesheet();
+ loadAnimations();
+
+ playAnimation(_data.startingAnimation);
+
+ super.onCreate(event);
+ }
+
+ function loadSpritesheet()
+ {
+ trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
+
+ var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath, 'shared');
+ if (tex == null)
+ {
+ trace('Could not load Sparrow sprite: ${_data.assetPath}');
+ return;
+ }
+
+ this.frames = tex;
+
+ if (_data.isPixel)
+ {
+ this.antialiasing = false;
+ }
+ else
+ {
+ this.antialiasing = true;
+ }
+
+ this.setScale(_data.scale);
+ }
+
+ function loadAnimations()
+ {
+ trace('[SPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
+
+ FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
+
+ for (anim in _data.animations)
+ {
+ if (anim.offsets == null)
+ {
+ setAnimationOffsets(anim.name, 0, 0);
+ }
+ else
+ {
+ setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
+ }
+ }
+
+ var animNames = this.animation.getNameList();
+ trace('[SPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
+ }
+}
diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index 1c8fdce5d..525ab0f91 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -1,5 +1,6 @@
package funkin.play.stage;
+import flixel.FlxG;
import flixel.FlxSprite;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.events.ScriptEvent;
@@ -36,6 +37,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
*/
public var idleSuffix(default, set):String = "";
+ /**
+ * Whether this bopper should bop every beat. By default it's true, but when used
+ * for characters/players, it should be false so it doesn't cut off their animations!!!!!
+ */
+ public var shouldBop:Bool = true;
+
+ public var finishCallbackMap:MapVoid> = new MapVoid>();
+
function set_idleSuffix(value:String):String
{
this.idleSuffix = value;
@@ -76,6 +85,11 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
{
super();
this.danceEvery = danceEvery;
+ this.animation.finishCallback = function(name)
+ {
+ if (finishCallbackMap[name] != null)
+ finishCallbackMap[name]();
+ };
}
function update_shouldAlternate():Void
@@ -93,14 +107,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
{
if (danceEvery > 0 && event.beat % danceEvery == 0)
{
- dance(true);
+ dance(shouldBop);
}
}
/**
* Called every `danceEvery` beats of the song.
*/
- public function dance(force:Bool = false):Void
+ public function dance(forceRestart:Bool = false):Void
{
if (this.animation == null)
{
@@ -116,17 +130,17 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
{
if (hasDanced)
{
- playAnimation('danceRight$idleSuffix', true);
+ playAnimation('danceRight$idleSuffix', forceRestart);
}
else
{
- playAnimation('danceLeft$idleSuffix', true);
+ playAnimation('danceLeft$idleSuffix', forceRestart);
}
hasDanced = !hasDanced;
}
else
{
- playAnimation('idle$idleSuffix', true);
+ playAnimation('idle$idleSuffix', forceRestart);
}
}
@@ -173,18 +187,39 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
}
}
+ public var canPlayOtherAnims:Bool = true;
+
/**
* @param name The name 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(name:String, restart:Bool = false):Void
+ public function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void
{
+ if (ignoreOther == null)
+ ignoreOther = false;
+
+ if (!canPlayOtherAnims)
+ return;
+
var correctName = correctAnimationName(name);
if (correctName == null)
return;
this.animation.play(correctName, restart, false, 0);
+ if (ignoreOther)
+ {
+ canPlayOtherAnims = false;
+
+ // doing it with this funny map, since overriding the animation.finishCallback is a bit messier IMO
+ finishCallbackMap[name] = function()
+ {
+ canPlayOtherAnims = true;
+ finishCallbackMap[name] = null;
+ };
+ }
+
applyAnimationOffsets(correctName);
}
diff --git a/source/funkin/play/stage/ScriptedBopper.hx b/source/funkin/play/stage/ScriptedBopper.hx
index a344b0428..405ef57f6 100644
--- a/source/funkin/play/stage/ScriptedBopper.hx
+++ b/source/funkin/play/stage/ScriptedBopper.hx
@@ -3,8 +3,4 @@ package funkin.play.stage;
import funkin.modding.IHook;
@:hscriptClass
-@:keep
-class ScriptedBopper extends Bopper implements IHook
-{
- // No body needed for this class, it's magic ;)
-}
+class ScriptedBopper extends Bopper implements IHook {}
diff --git a/source/funkin/play/stage/ScriptedStage.hx b/source/funkin/play/stage/ScriptedStage.hx
index 114afb1d5..e6a92d8b8 100644
--- a/source/funkin/play/stage/ScriptedStage.hx
+++ b/source/funkin/play/stage/ScriptedStage.hx
@@ -3,7 +3,4 @@ package funkin.play.stage;
import funkin.modding.IHook;
@:hscriptClass
-class ScriptedStage extends Stage implements IHook
-{
- // No body needed for this class, it's magic ;)
-}
+class ScriptedStage extends Stage implements IHook {}
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index 6a0cce769..768938608 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -2,12 +2,14 @@ package funkin.play.stage;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
+import flixel.math.FlxPoint;
import flixel.util.FlxSort;
import funkin.modding.IHook;
import funkin.modding.IScriptedClass;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
-import funkin.play.character.Character.CharacterType;
+import funkin.play.character.BaseCharacter;
+import funkin.play.stage.StageData.StageDataCharacter;
import funkin.play.stage.StageData.StageDataParser;
import funkin.util.SortUtil;
import funkin.util.assets.FlxAnimationUtil;
@@ -27,7 +29,7 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public var camZoom:Float = 1.0;
var namedProps:Map = new Map();
- var characters:Map = new Map();
+ var characters:Map = new Map();
var boppers:Array = new Array();
/**
@@ -158,6 +160,14 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
}
}
+ if (Std.isOfType(propSprite, Bopper))
+ {
+ for (propAnim in dataProp.animations)
+ {
+ cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
+ }
+ }
+
if (dataProp.startingAnimation != null)
{
propSprite.animation.play(dataProp.startingAnimation);
@@ -206,7 +216,6 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public function refresh()
{
sort(SortUtil.byZIndex, FlxSort.ASCENDING);
- trace('Stage sorted by z-index');
}
/**
@@ -232,53 +241,116 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
/**
* Used by the PlayState to add a character to the stage.
*/
- public function addCharacter(character:Character, charType:CharacterType)
+ public function addCharacter(character:BaseCharacter, charType:CharacterType)
{
+ if (character == null)
+ return;
+
+ #if debug
+ // Temporary marker that shows where the character's location is relative to.
+ // Should display at the stage position of the character (before any offsets).
+ // TODO: Make this a toggle? It's useful to turn on from time to time.
+ var debugIcon:FlxSprite = new FlxSprite(0, 0);
+ debugIcon.makeGraphic(8, 8, 0xffff00ff);
+ debugIcon.visible = false;
+ debugIcon.zIndex = 1000000;
+ #end
+
// Apply position and z-index.
+ var charData:StageDataCharacter = null;
switch (charType)
{
case BF:
this.characters.set("bf", character);
- character.zIndex = _data.characters.bf.zIndex;
- character.x = _data.characters.bf.position[0];
- character.y = _data.characters.bf.position[1];
+ charData = _data.characters.bf;
+ character.flipX = !character.flipX;
+ // flip offsets if flipX
+ character.initHealthIcon(false);
case GF:
this.characters.set("gf", character);
- character.zIndex = _data.characters.gf.zIndex;
- character.x = _data.characters.gf.position[0];
- character.y = _data.characters.gf.position[1];
+ charData = _data.characters.gf;
case DAD:
this.characters.set("dad", character);
- character.zIndex = _data.characters.dad.zIndex;
- character.x = _data.characters.dad.position[0];
- character.y = _data.characters.dad.position[1];
+ charData = _data.characters.dad;
+ // flip offsets if flipX
+ character.initHealthIcon(true);
default:
- this.characters.set(character.curCharacter, character);
+ this.characters.set(character.characterId, character);
+ }
+ if (charData != null)
+ {
+ character.zIndex = charData.zIndex;
+
+ // Start with the per-stage character position.
+ // Subtracting the origin ensures characters are positioned relative to their feet.
+ // Subtracting the global offset allows positioning on a per-character basis.
+ character.x = charData.position[0] - character.characterOrigin.x + character.globalOffsets[0];
+ character.y = charData.position[1] - character.characterOrigin.y + character.globalOffsets[1];
+
+ character.cameraFocusPoint.x += charData.cameraOffsets[0];
+ character.cameraFocusPoint.y += charData.cameraOffsets[1];
+
+ // Draw the debug icon at the character's feet.
+ debugIcon.x = charData.position[0];
+ debugIcon.y = charData.position[1];
}
// Add the character to the scene.
this.add(character);
+ this.add(debugIcon);
+ }
+
+ public inline function getGirlfriendPosition():FlxPoint
+ {
+ return new FlxPoint(_data.characters.gf.position[0], _data.characters.gf.position[1]);
+ }
+
+ public inline function getBoyfriendPosition():FlxPoint
+ {
+ return new FlxPoint(_data.characters.bf.position[0], _data.characters.bf.position[1]);
+ }
+
+ public inline function getDadPosition():FlxPoint
+ {
+ return new FlxPoint(_data.characters.dad.position[0], _data.characters.dad.position[1]);
}
/**
* Retrieves a given character from the stage.
*/
- public function getCharacter(id:String):Character
+ public function getCharacter(id:String):BaseCharacter
{
return this.characters.get(id);
}
- public function getBoyfriend():Character
+ /**
+ * Retrieve the Boyfriend character.
+ * @param pop If true, the character will be removed from the stage as well.
+ */
+ public function getBoyfriend(?pop:Bool = false):BaseCharacter
{
- return getCharacter('bf');
+ if (pop)
+ {
+ var boyfriend:BaseCharacter = getCharacter("bf");
+
+ // Remove the character from the stage.
+ this.remove(boyfriend);
+ this.characters.remove("bf");
+
+ return boyfriend;
+ }
+ else
+ {
+ return getCharacter('bf');
+ }
}
- public function getGirlfriend():Character
+ public function getGirlfriend():BaseCharacter
{
return getCharacter('gf');
}
- public function getDad():Character
+ public function getDad():BaseCharacter
{
return getCharacter('dad');
}
@@ -309,6 +381,32 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
return result;
}
+ /**
+ * Dispatch an event to all the characters in the stage.
+ * @param event The script event to dispatch.
+ */
+ public function dispatchToCharacters(event:ScriptEvent):Void
+ {
+ for (characterId in characters.keys())
+ {
+ dispatchToCharacter(characterId, event);
+ }
+ }
+
+ /**
+ * Dispatch an event to a specific character.
+ * @param characterId The ID of the character to dispatch to.
+ * @param event The script event to dispatch.
+ */
+ public function dispatchToCharacter(characterId:String, event:ScriptEvent):Void
+ {
+ var character:BaseCharacter = getCharacter(characterId);
+ if (character != null)
+ {
+ ScriptEventDispatcher.callEvent(character, event);
+ }
+ }
+
/**
* onDestroy gets called when the player is leaving the PlayState,
* and is used to clean up any objects that need to be destroyed.
diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx
index dc5538bac..e4d1904fd 100644
--- a/source/funkin/play/stage/StageData.hx
+++ b/source/funkin/play/stage/StageData.hx
@@ -175,6 +175,8 @@ class StageDataParser
static final DEFAULT_ISPIXEL:Bool = false;
static final DEFAULT_NAME:String = "Untitled Stage";
static final DEFAULT_OFFSETS:Array = [0, 0];
+ static final DEFAULT_CAMERA_OFFSETS_BF:Array = [-100, -100];
+ static final DEFAULT_CAMERA_OFFSETS_DAD:Array = [150, -100];
static final DEFAULT_POSITION:Array = [0, 0];
static final DEFAULT_SCALE:Float = 1.0;
static final DEFAULT_SCROLL:Array = [0, 0];
@@ -339,10 +341,12 @@ class StageDataParser
if (input.characters.bf == null)
{
input.characters.bf = DEFAULT_CHARACTER_DATA;
+ input.characters.bf.cameraOffsets = DEFAULT_CAMERA_OFFSETS_BF;
}
if (input.characters.dad == null)
{
input.characters.dad = DEFAULT_CHARACTER_DATA;
+ input.characters.dad.cameraOffsets = DEFAULT_CAMERA_OFFSETS_DAD;
}
if (input.characters.gf == null)
{
@@ -361,7 +365,14 @@ class StageDataParser
}
if (inputCharacter.cameraOffsets == null || inputCharacter.cameraOffsets.length != 2)
{
- inputCharacter.cameraOffsets = [0, 0];
+ if (inputCharacter == input.characters.bf)
+ inputCharacter.cameraOffsets = DEFAULT_CAMERA_OFFSETS_BF;
+ else if (inputCharacter == input.characters.dad)
+ inputCharacter.cameraOffsets = DEFAULT_CAMERA_OFFSETS_DAD;
+ else
+ {
+ inputCharacter.cameraOffsets = [0, 0];
+ }
}
}
@@ -484,7 +495,7 @@ typedef StageDataCharacter =
/**
* The camera offsets to apply when focusing on the character on this stage.
- * @default [0, 0]
+ * @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
*/
cameraOffsets:Array,
};
diff --git a/source/funkin/ui/AtlasMenuList.hx b/source/funkin/ui/AtlasMenuList.hx
index 7a217a44c..922ef9369 100644
--- a/source/funkin/ui/AtlasMenuList.hx
+++ b/source/funkin/ui/AtlasMenuList.hx
@@ -5,6 +5,9 @@ import flixel.graphics.frames.FlxAtlasFrames;
typedef AtlasAsset = flixel.util.typeLimit.OneOfTwo;
+/**
+ * A menulist whose items share a single texture atlas.
+ */
class AtlasMenuList extends MenuTypedList
{
public var atlas:FlxAtlasFrames;
@@ -33,11 +36,16 @@ class AtlasMenuList extends MenuTypedList
}
}
+/**
+ * A menu list item which uses single texture atlas.
+ */
class AtlasMenuItem extends MenuItem
{
var atlas:FlxAtlasFrames;
- public function new(x = 0.0, y = 0.0, name:String, atlas:FlxAtlasFrames, callback)
+ public var centered:Bool = false;
+
+ public function new(x = 0.0, y = 0.0, name:String, atlas, callback)
{
this.atlas = atlas;
super(x, y, name, callback);
@@ -52,10 +60,17 @@ class AtlasMenuItem extends MenuItem
super.setData(name, callback);
}
- function changeAnim(animName:String)
+ public function changeAnim(animName:String)
{
animation.play(animName);
updateHitbox();
+
+ if (centered)
+ {
+ // position by center
+ centerOrigin();
+ offset.copyFrom(origin);
+ }
}
override function idle()
diff --git a/source/funkin/ui/animDebugShit/DebugBoundingState.hx b/source/funkin/ui/animDebugShit/DebugBoundingState.hx
index 9f49f5cc0..19012a500 100644
--- a/source/funkin/ui/animDebugShit/DebugBoundingState.hx
+++ b/source/funkin/ui/animDebugShit/DebugBoundingState.hx
@@ -1,11 +1,13 @@
package funkin.ui.animDebugShit;
-import flixel.FlxCamera;
-import flixel.FlxSprite;
-import flixel.FlxState;
+import funkin.play.character.CharacterData.CharacterDataParser;
+import funkin.play.character.SparrowCharacter;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.ui.FlxInputText;
import flixel.addons.ui.FlxUIDropDownMenu;
+import flixel.FlxCamera;
+import flixel.FlxSprite;
+import flixel.FlxState;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFrame;
import flixel.group.FlxGroup;
@@ -15,6 +17,7 @@ import flixel.text.FlxText;
import flixel.util.FlxColor;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
+import funkin.play.character.BaseCharacter;
import lime.utils.Assets as LimeAssets;
import openfl.Assets;
import openfl.events.Event;
@@ -249,7 +252,7 @@ class DebugBoundingState extends FlxState
swagChar.offset.x = (FlxG.mouse.x - mouseOffset.x) * -1;
swagChar.offset.y = (FlxG.mouse.y - mouseOffset.y) * -1;
- swagChar.animOffsets.set(animDropDownMenu.selectedLabel, [swagChar.offset.x, swagChar.offset.y]);
+ swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, [Std.int(swagChar.offset.x), Std.int(swagChar.offset.y)]);
txtOffsetShit.text = 'Offset: ' + swagChar.offset;
}
@@ -391,9 +394,9 @@ class DebugBoundingState extends FlxState
if (FlxG.keys.justPressed.RIGHT || FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.UP || FlxG.keys.justPressed.DOWN)
{
var animName = animDropDownMenu.selectedLabel;
- var coolValues:Array = swagChar.animOffsets.get(animName);
+ var coolValues:Array = swagChar.animationOffsets.get(animName);
- var multiplier:Float = 5;
+ var multiplier:Int = 5;
if (FlxG.keys.pressed.CONTROL)
multiplier = 1;
@@ -410,8 +413,8 @@ class DebugBoundingState extends FlxState
else if (FlxG.keys.justPressed.DOWN)
coolValues[1] -= 1 * multiplier;
- swagChar.animOffsets.set(animDropDownMenu.selectedLabel, coolValues);
- swagChar.playAnim(animName);
+ swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, coolValues);
+ swagChar.playAnimation(animName);
txtOffsetShit.text = 'Offset: ' + coolValues;
@@ -422,9 +425,9 @@ class DebugBoundingState extends FlxState
{
var outputString:String = "";
- for (i in swagChar.animOffsets.keys())
+ for (i in swagChar.animationOffsets.keys())
{
- outputString += i + " " + swagChar.animOffsets.get(i)[0] + " " + swagChar.animOffsets.get(i)[1] + "\n";
+ outputString += i + " " + swagChar.animationOffsets.get(i)[0] + " " + swagChar.animationOffsets.get(i)[1] + "\n";
}
outputString.trim();
@@ -432,7 +435,7 @@ class DebugBoundingState extends FlxState
}
}
- var swagChar:Character;
+ var swagChar:BaseCharacter;
function loadAnimShit(char:String)
{
@@ -442,8 +445,10 @@ class DebugBoundingState extends FlxState
swagChar.destroy();
}
- swagChar = new Character(100, 100, char);
- swagChar.debugMode = true;
+ swagChar = CharacterDataParser.fetchCharacter(char);
+ swagChar.x = 100;
+ swagChar.y = 100;
+ // swagChar.debugMode = true;
offsetView.add(swagChar);
generateOutlines(swagChar.frames.frames);
@@ -451,11 +456,11 @@ class DebugBoundingState extends FlxState
var animThing:Array = [];
- for (i in swagChar.animOffsets.keys())
+ for (i in swagChar.animationOffsets.keys())
{
animThing.push(i);
trace(i);
- trace(swagChar.animOffsets[i]);
+ trace(swagChar.animationOffsets[i]);
}
animDropDownMenu.setData(FlxUIDropDownMenu.makeStrIdLabelArray(animThing, true));
@@ -468,8 +473,8 @@ class DebugBoundingState extends FlxState
onionSkinChar.alpha = 0.6;
var animName = animThing[Std.parseInt(str)];
- swagChar.playAnim(animName, true); // trace();
- trace(swagChar.animOffsets.get(animName));
+ swagChar.playAnimation(animName, true); // trace();
+ trace(swagChar.animationOffsets.get(animName));
txtOffsetShit.text = 'Offset: ' + swagChar.offset;
};
@@ -486,7 +491,7 @@ class DebugBoundingState extends FlxState
_file.addEventListener(Event.COMPLETE, onSaveComplete);
_file.addEventListener(Event.CANCEL, onSaveCancel);
_file.addEventListener(IOErrorEvent.IO_ERROR, onSaveError);
- _file.save(saveString, swagChar.curCharacter + "Offsets.txt");
+ _file.save(saveString, swagChar.characterId + "Offsets.txt");
}
}
diff --git a/source/funkin/ui/stageBuildShit/StageOffsetSubstate.hx b/source/funkin/ui/stageBuildShit/StageOffsetSubstate.hx
new file mode 100644
index 000000000..1196e8cf1
--- /dev/null
+++ b/source/funkin/ui/stageBuildShit/StageOffsetSubstate.hx
@@ -0,0 +1,103 @@
+package funkin.ui.stageBuildShit;
+
+import flixel.math.FlxPoint;
+import flixel.ui.FlxButton;
+import funkin.play.PlayState;
+import funkin.play.character.BaseCharacter;
+import funkin.play.stage.StageData;
+import haxe.Json;
+import openfl.Assets;
+
+class StageOffsetSubstate extends MusicBeatSubstate
+{
+ public function new()
+ {
+ super();
+ FlxG.mouse.visible = true;
+ PlayState.instance.pauseMusic();
+ FlxG.camera.target = null;
+
+ var btn:FlxButton = new FlxButton(200, 10, "SAVE COMPILE", function()
+ {
+ var stageLol:StageData = StageDataParser.parseStageData(PlayState.instance.currentStageId);
+
+ var bfPos = PlayState.instance.currentStage.getBoyfriend().feetPosition;
+ stageLol.characters.bf.position[0] = Std.int(bfPos.x);
+ stageLol.characters.bf.position[1] = Std.int(bfPos.y);
+
+ var dadPos = PlayState.instance.currentStage.getDad().feetPosition;
+
+ stageLol.characters.dad.position[0] = Std.int(dadPos.x);
+ stageLol.characters.dad.position[1] = Std.int(dadPos.y);
+
+ var GF_FEET_SNIIIIIIIIIIIIIFFFF = PlayState.instance.currentStage.getGirlfriend().feetPosition;
+ stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x);
+ stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y);
+
+ var outputJson = CoolUtil.jsonStringify(stageLol);
+
+ #if sys
+ // save "local" to the current export.
+ sys.io.File.saveContent('./assets/data/stages/' + PlayState.instance.currentStageId + '.json', outputJson);
+
+ // save to the dev version
+ sys.io.File.saveContent('../../../../assets/preload/data/stages/' + PlayState.instance.currentStageId + '.json', outputJson);
+ #end
+ // trace(dipshitJson);
+
+ // put character position data to a file of some sort
+ });
+ btn.scrollFactor.set();
+ add(btn);
+ btn.cameras = [PlayState.instance.camHUD];
+ }
+
+ var mosPosOld:FlxPoint = new FlxPoint();
+ var sprOld:FlxPoint = new FlxPoint();
+
+ var char:BaseCharacter = null;
+
+ override function update(elapsed:Float)
+ {
+ super.update(elapsed);
+
+ CoolUtil.mouseCamDrag();
+
+ if (FlxG.keys.pressed.CONTROL)
+ CoolUtil.mouseWheelZoom();
+
+ if (FlxG.mouse.pressed)
+ {
+ if (FlxG.mouse.justPressed)
+ {
+ for (thing in PlayState.instance.currentStage)
+ {
+ if (FlxG.mouse.overlaps(thing) && Std.isOfType(thing, BaseCharacter))
+ char = cast thing;
+ }
+
+ if (char != null)
+ {
+ sprOld.x = char.x;
+ sprOld.y = char.y;
+ }
+
+ mosPosOld.x = FlxG.mouse.x;
+ mosPosOld.y = FlxG.mouse.y;
+ }
+
+ if (char != null)
+ {
+ char.x = sprOld.x - (mosPosOld.x - FlxG.mouse.x);
+ char.y = sprOld.y - (mosPosOld.y - FlxG.mouse.y);
+ }
+ }
+
+ if (FlxG.keys.justPressed.Y)
+ {
+ PlayState.instance.resetCamera();
+ FlxG.mouse.visible = false;
+ close();
+ }
+ }
+}
diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx
index b0bd798dc..6e92d6b68 100644
--- a/source/funkin/util/SortUtil.hx
+++ b/source/funkin/util/SortUtil.hx
@@ -13,6 +13,8 @@ class SortUtil
*/
public static inline function byZIndex(Order:Int, Obj1:FlxBasic, Obj2:FlxBasic):Int
{
+ if (Obj1 == null || Obj2 == null)
+ return 0;
return FlxSort.byValues(Order, Obj1.zIndex, Obj2.zIndex);
}
diff --git a/source/funkin/util/assets/DataAssets.hx b/source/funkin/util/assets/DataAssets.hx
index d00703068..ed4805276 100644
--- a/source/funkin/util/assets/DataAssets.hx
+++ b/source/funkin/util/assets/DataAssets.hx
@@ -21,7 +21,10 @@ class DataAssets
{
var pathNoSuffix = textPath.substring(0, textPath.length - ext.length);
var pathNoPrefix = pathNoSuffix.substring(queryPath.length);
- results.push(pathNoPrefix);
+
+ // No duplicates! Why does this happen?
+ if (!results.contains(pathNoPrefix))
+ results.push(pathNoPrefix);
}
}