Files
CodenameEngine/source/funkin/backend/FunkinSprite.hx
T
2024-04-13 22:34:57 +02:00

356 lines
9.0 KiB
Haxe

package funkin.backend;
import flixel.addons.effects.FlxSkewedSprite;
import flixel.math.FlxMatrix;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.FlxAssets.FlxGraphicAsset;
import funkin.backend.scripting.events.PlayAnimContext;
import funkin.backend.system.interfaces.IBeatReceiver;
import funkin.backend.system.interfaces.IOffsetCompatible;
import funkin.backend.utils.XMLUtil.AnimData;
import funkin.backend.utils.XMLUtil.BeatAnim;
import funkin.backend.utils.XMLUtil.IXMLEvents;
import haxe.io.Path;
enum abstract XMLAnimType(Int)
{
var NONE = 0;
var BEAT = 1;
var LOOP = 2;
public static function fromString(str:String, def:XMLAnimType = XMLAnimType.NONE)
{
return switch (StringTools.trim(str).toLowerCase())
{
case "none": NONE;
case "beat" | "onbeat": BEAT;
case "loop": LOOP;
default: def;
}
}
}
class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements IOffsetCompatible implements IXMLEvents
{
public var extra:Map<String, Dynamic> = [];
public var spriteAnimType:XMLAnimType = NONE;
public var beatAnims:Array<BeatAnim> = [];
public var name:String;
public var zoomFactor:Float = 1;
public var debugMode:Bool = false;
public var animDatas:Map<String, AnimData> = [];
public var animEnabled:Bool = true;
public var zoomFactorEnabled:Bool = true;
/**
* ODD interval -> asynced; EVEN interval -> synced
*/
public var beatInterval(default, set):Int = 2;
public var beatOffset:Int = 0;
public var skipNegativeBeats:Bool = false;
public var animateAtlas:FlxAnimate;
@:noCompletion public var atlasPlayingAnim:String;
@:noCompletion public var atlasPath:String;
public function new(?X:Float = 0, ?Y:Float = 0, ?SimpleGraphic:FlxGraphicAsset)
{
super(X, Y);
if (SimpleGraphic != null)
{
if (SimpleGraphic is String)
loadSprite(cast SimpleGraphic);
else
loadGraphic(SimpleGraphic);
}
moves = false;
}
public static function copyFrom(source:FunkinSprite)
{
var spr = new FunkinSprite();
@:privateAccess {
spr.setPosition(source.x, source.y);
spr.frames = source.frames;
if (source.animateAtlas != null && source.atlasPath != null)
spr.loadSprite(source.atlasPath);
spr.animation.copyFrom(source.animation);
spr.visible = source.visible;
spr.alpha = source.alpha;
spr.antialiasing = source.antialiasing;
spr.scale.set(source.scale.x, source.scale.y);
spr.scrollFactor.set(source.scrollFactor.x, source.scrollFactor.y);
spr.skew.set(source.skew.x, source.skew.y);
spr.transformMatrix = source.transformMatrix;
spr.matrixExposed = source.matrixExposed;
spr.zoomFactor = source.zoomFactor;
spr.animOffsets = source.animOffsets.copy();
}
return spr;
}
public override function update(elapsed:Float)
{
super.update(elapsed);
if (animateAtlas != null)
animateAtlas.update(elapsed);
}
public function loadSprite(path:String, Unique:Bool = false, Key:String = null)
{
var noExt = Path.withoutExtension(path);
if (Assets.exists('$noExt/Animation.json'))
{
atlasPath = noExt;
animateAtlas = new FlxAnimate(x, y, noExt);
}
else
{
frames = Paths.getFrames(path, true);
}
}
public function onPropertySet(property:String, value:Dynamic) {
if (property.startsWith("velocity") || property.startsWith("acceleration"))
moves = true;
}
private var countedBeat = 0;
public function beatHit(curBeat:Int)
{
if(!animEnabled) return;
if (beatAnims.length > 0 && (curBeat + beatOffset) % beatInterval == 0)
{
if(skipNegativeBeats && curBeat < 0) return;
// TODO: find a solution without countedBeat
var anim = beatAnims[FlxMath.wrap(countedBeat++, 0, beatAnims.length - 1)];
if (anim.name != null && anim.name != "null" && anim.name != "none")
playAnim(anim.name, anim.forced);
}
}
public function stepHit(curBeat:Int)
{
}
public function measureHit(curMeasure:Int)
{
}
// ANIMATE ATLAS DRAWING
#if REGION
public override function draw()
{
if (animateAtlas != null)
{
copyAtlasValues();
animateAtlas.draw();
}
else
{
super.draw();
}
}
public function copyAtlasValues()
{
@:privateAccess {
animateAtlas.cameras = cameras;
animateAtlas.scrollFactor = scrollFactor;
animateAtlas.scale = scale;
animateAtlas.offset = offset;
animateAtlas.frameOffset = frameOffset;
animateAtlas.x = x;
animateAtlas.y = y;
animateAtlas.angle = angle;
animateAtlas.alpha = alpha;
animateAtlas.visible = visible;
animateAtlas.flipX = flipX;
animateAtlas.flipY = flipY;
animateAtlas.shader = shader;
animateAtlas.antialiasing = antialiasing;
animateAtlas.skew = skew;
animateAtlas.transformMatrix = transformMatrix;
animateAtlas.matrixExposed = matrixExposed;
}
}
public override function destroy()
{
animateAtlas = FlxDestroyUtil.destroy(animateAtlas);
if (animOffsets != null) {
for (key in animOffsets.keys()) {
final point = animOffsets[key];
animOffsets.remove(key);
if (point != null)
point.put();
}
animOffsets = null;
}
super.destroy();
}
#end
// ZOOM FACTOR
private inline function __shouldDoZoomFactor()
return zoomFactorEnabled && zoomFactor != 1;
public override function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect
{
if (camera == null)
camera = FlxG.camera;
var r = super.getScreenBounds(newRect, camera);
if(__shouldDoZoomFactor()) {
r.x -= camera.width / 2;
r.y -= camera.height / 2;
var ratio = (camera.zoom > 0 ? Math.max : Math.min)(0, FlxMath.lerp(1 / camera.zoom, 1, zoomFactor));
r.x *= ratio;
r.y *= ratio;
r.width *= ratio;
r.height *= ratio;
r.x += camera.width / 2;
r.y += camera.height / 2;
}
return r;
}
override public function isOnScreen(?camera:FlxCamera):Bool
{
if (forceIsOnScreen)
return true;
if (camera == null)
camera = FlxG.camera;
var bounds = getScreenBounds(_rect, camera);
if (bounds.width == 0 && bounds.height == 0)
return false;
return camera.containsRect(bounds);
}
// ZOOM FACTOR RENDERING
public override function doAdditionalMatrixStuff(matrix:FlxMatrix, camera:FlxCamera)
{
super.doAdditionalMatrixStuff(matrix, camera);
if(__shouldDoZoomFactor()) {
matrix.translate(-camera.width / 2, -camera.height / 2);
var requestedZoom = (camera.zoom >= 0 ? Math.max : Math.min)(FlxMath.lerp(1, camera.zoom, zoomFactor), 0);
var diff = requestedZoom / camera.zoom;
matrix.scale(diff, diff);
matrix.translate(camera.width / 2, camera.height / 2);
}
}
// OFFSETTING
#if REGION
public var animOffsets:Map<String, FlxPoint> = new Map<String, FlxPoint>();
public function addOffset(name:String, x:Float = 0, y:Float = 0)
{
animOffsets[name] = FlxPoint.get(x, y);
}
public function switchOffset(anim1:String, anim2:String)
{
var old = animOffsets[anim1];
animOffsets[anim1] = animOffsets[anim2];
animOffsets[anim2] = old;
}
#end
// PLAYANIM
#if REGION
public var lastAnimContext:PlayAnimContext = DANCE;
public function playAnim(AnimName:String, Force:Bool = false, Context:PlayAnimContext = NONE, Reversed:Bool = false, Frame:Int = 0):Void
{
if (AnimName == null)
return;
if (animateAtlas != null)
{
@:privateAccess
// if (!animateAtlas.anim.animsMap.exists(AnimName) && !animateAtlas.anim.symbolDictionary.exists(AnimName)) return;
animateAtlas.anim.play(AnimName, Force, Reversed, Frame);
atlasPlayingAnim = AnimName;
}
else
{
if (!animation.exists(AnimName) && !debugMode)
return;
animation.play(AnimName, Force, Reversed, Frame);
}
var daOffset = getAnimOffset(AnimName);
frameOffset.set(daOffset.x, daOffset.y);
daOffset.putWeak();
lastAnimContext = Context;
}
public inline function getAnimOffset(name:String)
{
if (animOffsets.exists(name))
return animOffsets[name];
return FlxPoint.weak(0, 0);
}
public inline function hasAnimation(AnimName:String):Bool @:privateAccess
return animateAtlas != null ? (animateAtlas.anim.animsMap.exists(AnimName)
|| animateAtlas.anim.symbolDictionary.exists(AnimName)) : animation.exists(AnimName);
public inline function getAnimName() {
return (animateAtlas != null) ? atlasPlayingAnim : animation.name;
}
public inline function removeAnimation(name:String) {
if (animateAtlas != null)
@:privateAccess animateAtlas.anim.animsMap.remove(name);
else
animation.remove(name);
}
public inline function getNameList():Array<String> {
if (animateAtlas != null)
return [for (name in @:privateAccess animateAtlas.anim.animsMap.keys()) name];
else
return animation.getNameList();
}
public inline function stopAnimation() {
if (animateAtlas != null)
animateAtlas.anim.pause();
else
animation.stop();
}
public inline function isAnimFinished() {
return animateAtlas != null ? (animateAtlas.anim.finished) : (animation.curAnim != null ? animation.curAnim.finished : true);
}
override function updateAnimation(elapsed:Float) {
if (animEnabled)
super.updateAnimation(elapsed);
}
#end
// Getter / Setters
@:noCompletion private function set_beatInterval(v:Int) {
if (v < 1)
v = 1;
return beatInterval = v;
}
}