Files
FNF-i486-Engine/source/Character.hx
JordanSantiagoYT 1638018089 revert for now pt 2
2025-04-07 06:18:32 -04:00

613 lines
15 KiB
Haxe

package;
import flixel.tweens.FlxEase;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.addons.effects.FlxTrail;
import flixel.animation.FlxBaseAnimation;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.tweens.FlxTween;
import flixel.util.FlxSort;
import flixel.util.FlxDestroyUtil;
import flixel.math.FlxRect;
import Section.SwagSection;
#if MODS_ALLOWED
import sys.io.File;
import sys.FileSystem;
#end
import openfl.utils.AssetType;
import openfl.utils.Assets;
import haxe.Json;
using StringTools;
typedef CharacterFile = {
var animations:Array<AnimArray>;
var image:String;
var scale:Float;
var sing_duration:Float;
var healthicon:String;
var position:Array<Float>;
var camera_position:Array<Float>;
var flip_x:Bool;
var no_antialiasing:Bool;
var healthbar_colors:Array<Int>;
var winning_colors:Array<Int>;
var losing_colors:Array<Int>;
var noteskin:String;
var vocals_file:String;
var health_drain:Bool;
var drain_amount:Float;
var drain_floor:Float;
var shake_screen:Bool;
var shake_intensity:Float;
var shake_duration:Float;
@:optional var _editor_isPlayer:Null<Bool>;
}
typedef AnimArray = {
var anim:String;
var name:String;
var fps:Int;
var loop:Bool;
var indices:Array<Int>;
var offsets:Array<Int>;
}
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 = DEFAULT_CHARACTER;
public var colorTween:FlxTween;
public var holdTimer:Float = 0;
public var heyTimer:Float = 0;
var playState:PlayState;
public var specialAnim:Bool = false;
public var animationNotes:Array<Dynamic> = [];
public var stunned:Bool = false;
public var singDuration:Float = 4; //Multiplier of how long a character holds the sing pose
public var idleSuffix:String = '';
public var danceIdle:Bool = false; //Character use "danceLeft" and "danceRight" instead of "idle"
public var skipDance:Bool = false;
public var healthIcon:String = 'face';
public var animationsArray:Array<AnimArray> = [];
public var noteskin:String;
public var positionArray:Array<Float> = [0, 0];
public var cameraPosition:Array<Float> = [0, 0];
public var otherCharacters:Array<Character>;
public var hasMissAnimations:Bool = false;
public var vocalsFile:String = '';
public var isDeathCharacter:Bool = false;
public var healthDrain:Bool = false;
public var drainAmount:Float = 0;
public var drainFloor:Float = 0;
public var shakeScreen:Bool = false;
public var shakeIntensity:Float = 0;
public var shakeDuration:Float = 0;
//Used on Character Editor
public var imageFile:String = '';
public var jsonScale:Float = 1;
public var noAntialiasing:Bool = false;
public var originalFlipX:Bool = false;
public var editorIsPlayer:Null<Bool> = null;
public var healthColorArray:Array<Int> = [255, 0, 0];
public var winningColorArray:Array<Int> = [255, 0, 0];
public var losingColorArray:Array<Int> = [255, 0, 0];
public static var DEFAULT_CHARACTER:String = 'bf'; //In case a character is missing, it will use BF on its place
public function new(x:Float, y:Float, ?character:String = 'bf', ?isPlayer:Bool = false, ?isDeathCharacter:Bool = false)
{
super(x, y);
animOffsets = new Map();
curCharacter = character;
this.isPlayer = isPlayer;
antialiasing = ClientPrefs.globalAntialiasing;
var library:String = null;
switch (curCharacter)
{
//case 'your character name in case you want to hardcode them instead':
default:
var characterPath:String = 'characters/' + curCharacter + '.json';
#if MODS_ALLOWED
var path:String = Paths.modFolders(characterPath);
if (!FileSystem.exists(path)) {
path = Paths.getPreloadPath(characterPath);
}
if (!FileSystem.exists(path))
#else
var path:String = Paths.getPreloadPath(characterPath);
if (!Assets.exists(path))
#end
{
path = Paths.getPreloadPath('characters/' + DEFAULT_CHARACTER + '.json'); //If a character couldn't be found, change him to BF just to prevent a crash
}
try
{
#if MODS_ALLOWED
loadCharacterFile(cast Json.parse(File.getContent(path)));
#else
loadCharacterFile(cast Json.parse(Assets.getText(path)));
#end
}
catch(e:Dynamic)
{
trace('Error loading character file of "$character": $e');
}
}
originalFlipX = flipX;
if(animOffsets.exists('singLEFTmiss') || animOffsets.exists('singDOWNmiss') || animOffsets.exists('singUPmiss') || animOffsets.exists('singRIGHTmiss')) hasMissAnimations = true;
recalculateDanceIdle();
dance();
if (isPlayer)
flipX = !flipX;
switch(curCharacter)
{
case 'pico-speaker':
skipDance = true;
loadMappedAnims();
playAnim("shoot1");
case 'pico-blazin', 'darnell-blazin':
skipDance = true;
}
}
public function loadCharacterFile(json:CharacterFile)
{
isAnimateAtlas = false;
#if flxanimate
var animToFind:String = Paths.getPath('images/' + json.image + '/Animation.json', TEXT);
if (#if MODS_ALLOWED FileSystem.exists(animToFind) || #end Assets.exists(animToFind))
isAnimateAtlas = true;
#end
scale.set(1, 1);
updateHitbox();
if(!isAnimateAtlas)
{
frames = Paths.getMultiAtlas(json.image.split(','));
}
#if flxanimate
else
{
atlas = new FlxAnimate();
atlas.showPivot = false;
try
{
Paths.loadAnimateAtlas(atlas, json.image);
}
catch(e:haxe.Exception)
{
FlxG.log.warn('Could not load atlas ${json.image}: $e');
trace(e.stack);
}
}
#end
imageFile = json.image;
noteskin = json.noteskin;
if(json.scale != 1) {
jsonScale = json.scale;
scale.set(jsonScale, jsonScale);
updateHitbox();
}
positionArray = json.position;
cameraPosition = json.camera_position;
healthIcon = json.healthicon;
singDuration = json.sing_duration;
flipX = !!json.flip_x;
if(json.no_antialiasing) {
antialiasing = false;
noAntialiasing = true;
}
healthDrain = json.health_drain;
shakeScreen = json.shake_screen;
drainAmount = json.drain_amount;
drainFloor = json.drain_floor;
shakeIntensity = (!Math.isNaN(json.shake_intensity) ? json.shake_intensity : 0.0075);
shakeDuration = (!Math.isNaN(json.shake_duration) ? json.shake_duration : 0.1);
vocalsFile = json.vocals_file != null ? json.vocals_file : '';
editorIsPlayer = json._editor_isPlayer;
if(json.healthbar_colors != null && json.healthbar_colors.length > 2)
healthColorArray = json.healthbar_colors;
if(json.winning_colors != null && json.winning_colors.length > 2)
winningColorArray = json.winning_colors;
else winningColorArray = healthColorArray;
if(json.losing_colors != null && json.losing_colors.length > 2)
losingColorArray = json.losing_colors;
else losingColorArray = healthColorArray;
antialiasing = !noAntialiasing;
if(!ClientPrefs.globalAntialiasing) antialiasing = false;
animationsArray = json.animations;
if(animationsArray != null && animationsArray.length > 0) {
for (anim in animationsArray) {
var animAnim:String = '' + anim.anim;
var animName:String = '' + anim.name;
var animFps:Int = anim.fps;
var animLoop:Bool = !!anim.loop; //Bruh
var animIndices:Array<Int> = anim.indices;
if(!isAnimateAtlas)
{
if(animIndices != null && animIndices.length > 0)
animation.addByIndices(animAnim, animName, animIndices, "", animFps, animLoop);
else
animation.addByPrefix(animAnim, animName, animFps, animLoop);
}
#if flxanimate
else
{
if(animIndices != null && animIndices.length > 0)
atlas.anim.addBySymbolIndices(animAnim, animName, animIndices, animFps, animLoop);
else
atlas.anim.addBySymbol(animAnim, animName, animFps, animLoop);
}
#end
if(anim.offsets != null && anim.offsets.length > 1) {
addOffset(anim.anim, anim.offsets[0], anim.offsets[1]);
}
}
#if flxanimate
if(isAnimateAtlas) copyAtlasValues();
#end
}
}
var anim:String;
override function update(elapsed:Float)
{
if (ClientPrefs.ffmpegMode) elapsed = 1 / ClientPrefs.targetFPS;
if(isAnimateAtlas) atlas.update(elapsed);
if(debugMode || (!isAnimateAtlas && animation.curAnim == null) || (isAnimateAtlas && atlas.anim.curSymbol == null))
{
super.update(elapsed);
return;
}
if(heyTimer > 0)
{
heyTimer -= elapsed * PlayState.instance.playbackRate;
if(heyTimer <= 0)
{
anim = getAnimationName();
if(specialAnim && (anim == 'hey' || anim == 'cheer'))
{
specialAnim = false;
dance();
}
heyTimer = 0;
}
} else if(specialAnim && isAnimationFinished())
{
specialAnim = false;
dance();
}
else if (getAnimationName().endsWith('miss') && isAnimationFinished())
{
dance();
finishAnimation();
}
switch(curCharacter)
{
case 'pico-speaker':
if(animationNotes.length > 0 && Conductor.songPosition > animationNotes[0][0])
{
var noteData:Int = 1;
if(animationNotes[0][1] > 2) noteData = 3;
noteData += FlxG.random.int(0, 1);
playAnim('shoot' + noteData, true);
animationNotes.shift();
}
if(isAnimationFinished()) playAnim(animation.curAnim.name, false, false, animation.curAnim.frames.length - 3);
}
if (!isPlayer)
{
if (!PlayState.opponentChart || curCharacter.startsWith('gf')) {
if (getAnimationName().startsWith('sing')) holdTimer += elapsed;
if (holdTimer >= Conductor.stepCrochet * (0.0011 / (FlxG.sound.music != null ? FlxG.sound.music.pitch : 1)) * singDuration * (PlayState.instance != null ? PlayState.instance.singDurMult : 1))
{
dance();
holdTimer = 0;
}
} else {
if (getAnimationName().startsWith('sing'))
{
holdTimer += elapsed;
}
else
holdTimer = 0;
if (getAnimationName().endsWith('miss') && isAnimationFinished() && !debugMode)
dance();
}
}
anim = getAnimationName();
if(isAnimationFinished() && animOffsets.exists('$anim-loop'))
playAnim('$anim-loop');
super.update(elapsed);
}
inline public function isAnimationNull():Bool
return !isAnimateAtlas ? (animation.curAnim == null) : (atlas.anim.curSymbol == null);
var _lastPlayedAnimation:String;
inline public function getAnimationName():String
{
return _lastPlayedAnimation;
}
public function isAnimationFinished():Bool
{
if(isAnimationNull()) return false;
return !isAnimateAtlas ? animation.curAnim.finished : atlas.anim.finished;
}
public function finishAnimation():Void
{
if(isAnimationNull()) return;
if(!isAnimateAtlas) animation.curAnim.finish();
else atlas.anim.curFrame = atlas.anim.length - 1;
}
public function hasAnimation(anim:String):Bool
{
return animOffsets.exists(anim);
}
public var animPaused(get, set):Bool;
private function get_animPaused():Bool
{
if(isAnimationNull()) return false;
return !isAnimateAtlas ? animation.curAnim.paused : atlas.anim.isPlaying;
}
private function set_animPaused(value:Bool):Bool
{
if(isAnimationNull()) return value;
if(!isAnimateAtlas) animation.curAnim.paused = value;
else
{
if(value) atlas.anim.pause();
else atlas.anim.resume();
}
return value;
}
public var danced:Bool = false;
/**
* FOR GF DANCING SHIT
*/
public function dance()
{
if (!debugMode && !skipDance && !specialAnim)
{
if(danceIdle)
{
danced = !danced;
if (danced)
inline playAnim('danceRight' + idleSuffix);
else
inline playAnim('danceLeft' + idleSuffix);
}
else if(animation.getByName('idle' + idleSuffix) != null) {
inline playAnim('idle' + idleSuffix);
}
}
}
var daOffset = null;
public function playAnim(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void
{
specialAnim = false;
if(!isAnimateAtlas)
{
animation.play(AnimName, Force, Reversed, Frame);
}
else
{
atlas.anim.play(AnimName, Force, Reversed, Frame);
atlas.update(0);
}
_lastPlayedAnimation = AnimName;
if (hasAnimation(AnimName))
{
daOffset = animOffsets.get(AnimName);
offset.set(daOffset[0], daOffset[1]);
}
else
offset.set(0, 0);
if (curCharacter.startsWith('gf'))
{
if (AnimName == 'singLEFT')
danced = true;
else if (AnimName == 'singRIGHT')
danced = false;
if (AnimName == 'singUP' || AnimName == 'singDOWN')
danced = !danced;
}
}
function loadMappedAnims():Void
{
var noteData:Array<SwagSection> = Song.loadFromJson('picospeaker', Paths.formatToSongPath(PlayState.SONG.song)).notes;
for (section in noteData) {
for (songNotes in section.sectionNotes) {
animationNotes.push(songNotes);
}
}
stages.objects.TankmenBG.animationNotes = animationNotes;
animationNotes.sort(sortAnims);
}
function sortAnims(Obj1:Array<Dynamic>, Obj2:Array<Dynamic>):Int
{
return FlxSort.byValues(FlxSort.ASCENDING, Obj1[0], Obj2[0]);
}
public var danceEveryNumBeats:Int = 2;
private var settingCharacterUp:Bool = true;
public function recalculateDanceIdle() {
var lastDanceIdle:Bool = danceIdle;
danceIdle = (animation.getByName('danceLeft' + idleSuffix) != null && animation.getByName('danceRight' + idleSuffix) != null);
if(settingCharacterUp)
{
danceEveryNumBeats = (danceIdle ? 1 : 2);
}
else if(lastDanceIdle != danceIdle)
{
var calc:Float = danceEveryNumBeats;
if(danceIdle)
calc /= 2;
else
calc *= 2;
danceEveryNumBeats = Math.round(Math.max(calc, 1));
}
settingCharacterUp = false;
}
public function addOffset(name:String, x:Float = 0, y:Float = 0)
{
animOffsets[name] = [x, y];
}
public function quickAnimAdd(name:String, anim:String)
{
animation.addByPrefix(name, anim, 24, false);
}
// Atlas support
// special thanks ne_eo for the references, you're the goat!!
public var isAnimateAtlas:Bool = false;
#if flxanimate
public var atlas:FlxAnimate;
public override function draw()
{
if(isAnimateAtlas)
{
copyAtlasValues();
atlas.draw();
return;
}
super.draw();
}
public function copyAtlasValues()
{
@:privateAccess
{
atlas.cameras = cameras;
atlas.scrollFactor = scrollFactor;
atlas.scale = scale;
atlas.offset = offset;
atlas.origin = origin;
atlas.x = x;
atlas.y = y;
atlas.angle = angle;
atlas.alpha = alpha;
atlas.visible = visible;
atlas.flipX = flipX;
atlas.flipY = flipY;
atlas.shader = shader;
atlas.antialiasing = antialiasing;
atlas.colorTransform = colorTransform;
atlas.color = color;
}
}
public override function destroy()
{
super.destroy();
destroyAtlas();
}
public function destroyAtlas()
{
if (atlas != null)
atlas = FlxDestroyUtil.destroy(atlas);
}
#end
}
class Boyfriend extends Character
{
public var startedDeath:Bool = false;
public function new(x:Float, y:Float, ?char:String = 'bf')
{
super(x, y, char, true);
}
override function update(elapsed:Float)
{
if (ClientPrefs.ffmpegMode) elapsed = 1 / ClientPrefs.targetFPS;
if (!debugMode && animation.curAnim != null)
{
if (animation.curAnim.name.startsWith('sing'))
holdTimer += elapsed;
else
holdTimer = 0;
if (animation.curAnim.name.endsWith('miss') && isAnimationFinished() && !debugMode)
playAnim('idle', true, false, 10);
if (animation.curAnim.name == 'firstDeath' && isAnimationFinished() && startedDeath)
playAnim('deathLoop');
}
super.update(elapsed);
}
}