1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-23 02:19:46 +00:00

Merge pull request #14 from ninjamuffin99/feature/scripted-characters

YOLO MERRRRRRRRRRRGE
Polymod - Scripted Characters
This commit is contained in:
Cameron Taylor 2022-05-31 00:00:20 -04:00 committed by GitHub
commit bc86724393
58 changed files with 3518 additions and 1556 deletions

View file

@ -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

44
.github/actions/upload-itch/action.yml vendored Normal file
View file

@ -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

63
.github/workflows/build-shit.yml vendored Normal file
View file

@ -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

View file

@ -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

3
.gitignore vendored
View file

@ -2,4 +2,5 @@ export/
.vscode/
APIStuff.hx
.DS_STORE
RECOVER_*.fla
RECOVER_*.fla
.haxelib/

View file

@ -128,6 +128,7 @@
<!--haxelib name="newgrounds" unless="switch"/> -->
<haxelib name="faxe" if='switch' />
<haxelib name="polymod" />
<haxelib name="thx.semver" />
<!-- <haxelib name="colyseus"/> -->

View file

@ -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'll also need to install a couple things that involve Gits. To do this, you need to do a few things first.
1. Download [git-scm](https://git-scm.com/downloads). Works for Windows, Mac, and Linux, just select your build.
2. Follow instructions to install the application properly.
3. Run `haxelib git polymod https://github.com/larsiusprime/polymod.git` to install Polymod.
4. Run `haxelib git discord_rpc https://github.com/Aidan63/linc_discord-rpc` to install Discord RPC.
4. Run `haxelib git discord_rpc https://github.com/Aidan63/linc_discord-rpc` to install Discord RPC. -->
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

View file

@ -1 +0,0 @@
swagshit--moneymoney

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

74
hmm.json Normal file
View file

@ -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"
}
]
}

View file

@ -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);
}
}

View file

@ -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<String, Array<Dynamic>>;
public var debugMode:Bool = false;
public var isPlayer:Bool = false;
public var curCharacter:String = 'bf';
public var holdTimer:Float = 0;
public var animationNotes:Array<NoteData> = [];
public function new(x:Float, y:Float, ?character:String = "bf", ?isPlayer:Bool = false)
{
super(x, y);
animOffsets = new Map<String, Array<Dynamic>>();
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<SwagSection> = 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<String> = CoolUtil.coolTextFile(Paths.file("images/characters/" + offsetCharacter + "Offsets.txt", TEXT, 'shared'));
for (i in daFile)
{
var splitWords:Array<String> = 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];
}
}

View file

@ -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

View file

@ -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);

View file

@ -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<Int> = [];
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<Int> = [];
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());
});
});
}

View file

@ -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<String> = 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);
}
}

View file

@ -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

View file

@ -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<AtlasMenuItem>;
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<AtlasMenuItem>();
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<MainMenuItem>
{
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);
}
}

View file

@ -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;
/**

View file

@ -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<Float> = [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";

View file

@ -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));
}

View file

@ -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];
}

View file

@ -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<Bool> = [true, true, true, true, true, true, true, true];
var weekCharacters:Array<Dynamic> = [
@ -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<String> = [
@ -114,8 +116,6 @@ class StoryMenuState extends MusicBeatState
grpLocks = new FlxTypedGroup<FlxSprite>();
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();
}

View file

@ -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:

View file

@ -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
}
}

View file

@ -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<Score> = [];
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;
}

View file

@ -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

View file

@ -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.

View file

@ -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;

View file

@ -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<String, Array<Dynamic>>;
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);

View file

@ -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)

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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);

View file

@ -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 {}

View file

@ -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;
}

View file

@ -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"));
}
}

View file

@ -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";
}

View file

@ -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;

File diff suppressed because it is too large Load diff

View file

@ -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()

View file

@ -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<Float>)
{
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<Float>):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<Float>;
function get_characterCameraOffsets():Array<Float>
{
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;
}

View file

@ -1,9 +0,0 @@
package funkin.play.character;
enum CharacterType
{
BF;
GF;
DAD;
OTHER;
}

View file

@ -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<String, CharacterData> = new Map<String, CharacterData>();
static final characterScriptedClass:Map<String, String> = new Map<String, String>();
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<String> = DataAssets.listDataFilesInPath('characters/');
var unscriptedCharIds:Array<String> = 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<String> = 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<String> = 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<String> = 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<String> = 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<BaseCharacter>
{
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<CharacterData>
{
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<CharacterData>
{
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<Float> = [0, 0];
static final DEFAULT_HEALTHICON_OFFSETS:Array<Int> = [0, 25];
static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.SPARROW;
static final DEFAULT_SCALE:Float = 1;
static final DEFAULT_SCROLL:Array<Float> = [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<CharacterData>
{
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<Float>;
/**
* Optional data about the health icon for the character.
*/
var healthIcon:Null<HealthIconData>;
/**
* The global offset to the character's position, in pixels.
* @default [0, 0]
*/
var offsets:Null<Array<Float>>;
/**
* 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<Float>;
/**
* Setting this to true disables anti-aliasing for the character.
* @default false
*/
var isPixel:Null<Bool>;
/**
* 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<Int>;
/**
* 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<Float>;
/**
* An optional array of animations which the character can play.
*/
var animations:Array<AnimationData>;
/**
* If animations are used, this is the name of the animation to play first.
* @default idle
*/
var startingAnimation:Null<String>;
/**
* 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<Bool>;
};
typedef HealthIconData =
{
/**
* The ID to use for the health icon.
* @default The character's ID
*/
var id:Null<String>;
/**
* The scale of the health icon.
*/
var scale:Null<Float>;
/**
* The offset of the health icon, in pixels.
* @default [0, 25]
*/
var offsets:Null<Array<Float>>;
}

View file

@ -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<String, FlxFramesCollection> = new Map<String, FlxFramesCollection>();
/**
* A map between animation names and what frame collection the animation should use.
*/
private var animAssetPath:Map<String, String> = new Map<String, String>();
/**
* 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;
}
}

View file

@ -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}');
}
}

View file

@ -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 {}

View file

@ -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}');
}
}

View file

@ -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:Map<String, Void->Void> = new Map<String, Void->Void>();
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);
}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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<String, FlxSprite> = new Map<String, FlxSprite>();
var characters:Map<String, Character> = new Map<String, Character>();
var characters:Map<String, BaseCharacter> = new Map<String, BaseCharacter>();
var boppers:Array<Bopper> = new Array<Bopper>();
/**
@ -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.

View file

@ -175,6 +175,8 @@ class StageDataParser
static final DEFAULT_ISPIXEL:Bool = false;
static final DEFAULT_NAME:String = "Untitled Stage";
static final DEFAULT_OFFSETS:Array<Float> = [0, 0];
static final DEFAULT_CAMERA_OFFSETS_BF:Array<Float> = [-100, -100];
static final DEFAULT_CAMERA_OFFSETS_DAD:Array<Float> = [150, -100];
static final DEFAULT_POSITION:Array<Float> = [0, 0];
static final DEFAULT_SCALE:Float = 1.0;
static final DEFAULT_SCROLL:Array<Float> = [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<Float>,
};

View file

@ -5,6 +5,9 @@ import flixel.graphics.frames.FlxAtlasFrames;
typedef AtlasAsset = flixel.util.typeLimit.OneOfTwo<String, FlxAtlasFrames>;
/**
* A menulist whose items share a single texture atlas.
*/
class AtlasMenuList extends MenuTypedList<AtlasMenuItem>
{
public var atlas:FlxAtlasFrames;
@ -33,11 +36,16 @@ class AtlasMenuList extends MenuTypedList<AtlasMenuItem>
}
}
/**
* 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()

View file

@ -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<Dynamic> = swagChar.animOffsets.get(animName);
var coolValues:Array<Float> = 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<String> = [];
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");
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}

View file

@ -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);
}
}