613 lines
15 KiB
Haxe
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);
|
|
}
|
|
} |