2023-06-16 21:37:56 +00:00
|
|
|
package funkin.play.cutscene.dialogue;
|
|
|
|
|
|
|
|
import flixel.FlxSprite;
|
2024-02-07 23:45:13 +00:00
|
|
|
import funkin.data.IRegistryEntry;
|
2023-06-16 21:37:56 +00:00
|
|
|
import funkin.modding.events.ScriptEvent;
|
|
|
|
import flixel.graphics.frames.FlxFramesCollection;
|
|
|
|
import funkin.util.assets.FlxAnimationUtil;
|
|
|
|
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
2024-03-23 19:34:37 +00:00
|
|
|
import funkin.data.dialogue.speaker.SpeakerData;
|
|
|
|
import funkin.data.dialogue.speaker.SpeakerRegistry;
|
2023-06-16 21:37:56 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The character sprite which displays during dialogue.
|
2023-08-31 22:47:23 +00:00
|
|
|
*
|
2023-06-16 21:37:56 +00:00
|
|
|
* Most conversations have two speakers, with one being flipped.
|
|
|
|
*/
|
2024-02-07 23:45:13 +00:00
|
|
|
class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRegistryEntry<SpeakerData>
|
2023-06-16 21:37:56 +00:00
|
|
|
{
|
|
|
|
/**
|
|
|
|
* The internal ID for this speaker.
|
|
|
|
*/
|
2024-02-07 23:45:13 +00:00
|
|
|
public final id:String;
|
2023-06-16 21:37:56 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The full data for a speaker.
|
|
|
|
*/
|
2024-02-07 23:45:13 +00:00
|
|
|
public final _data:SpeakerData;
|
2023-06-16 21:37:56 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A readable name for this speaker.
|
|
|
|
*/
|
2023-08-31 22:47:23 +00:00
|
|
|
public var speakerName(get, never):String;
|
2023-06-16 21:37:56 +00:00
|
|
|
|
|
|
|
function get_speakerName():String
|
|
|
|
{
|
2024-02-07 23:45:13 +00:00
|
|
|
return _data.name;
|
2023-06-16 21:37:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Offset the speaker's sprite by this much when playing each animation.
|
|
|
|
*/
|
|
|
|
var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The current animation offset being used.
|
|
|
|
*/
|
|
|
|
var animOffsets(default, set):Array<Float> = [0, 0];
|
|
|
|
|
|
|
|
function set_animOffsets(value:Array<Float>):Array<Float>
|
|
|
|
{
|
|
|
|
if (animOffsets == null) animOffsets = [0, 0];
|
|
|
|
if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value;
|
|
|
|
|
|
|
|
var xDiff:Float = value[0] - animOffsets[0];
|
|
|
|
var yDiff:Float = value[1] - animOffsets[1];
|
|
|
|
|
|
|
|
this.x += xDiff;
|
|
|
|
this.y += yDiff;
|
|
|
|
|
|
|
|
return animOffsets = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The offset of the speaker overall.
|
|
|
|
*/
|
|
|
|
public var globalOffsets(default, set):Array<Float> = [0, 0];
|
|
|
|
|
|
|
|
function set_globalOffsets(value:Array<Float>):Array<Float>
|
|
|
|
{
|
|
|
|
if (globalOffsets == null) globalOffsets = [0, 0];
|
|
|
|
if (globalOffsets == value) return value;
|
|
|
|
|
|
|
|
var xDiff:Float = value[0] - globalOffsets[0];
|
|
|
|
var yDiff:Float = value[1] - globalOffsets[1];
|
|
|
|
|
|
|
|
this.x += xDiff;
|
|
|
|
this.y += yDiff;
|
|
|
|
return globalOffsets = value;
|
|
|
|
}
|
|
|
|
|
2024-02-07 23:45:13 +00:00
|
|
|
public function new(id:String)
|
2023-06-16 21:37:56 +00:00
|
|
|
{
|
|
|
|
super();
|
|
|
|
|
2024-02-07 23:45:13 +00:00
|
|
|
this.id = id;
|
|
|
|
this._data = _fetchData(id);
|
2023-06-16 21:37:56 +00:00
|
|
|
|
2024-02-07 23:45:13 +00:00
|
|
|
if (_data == null)
|
|
|
|
{
|
|
|
|
throw 'Could not parse speaker data for id: $id';
|
|
|
|
}
|
2023-06-16 21:37:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when speaker is being created.
|
|
|
|
* @param event The script event.
|
|
|
|
*/
|
|
|
|
public function onCreate(event:ScriptEvent):Void
|
|
|
|
{
|
|
|
|
this.globalOffsets = [0, 0];
|
|
|
|
this.x = 0;
|
|
|
|
this.y = 0;
|
|
|
|
this.alpha = 1;
|
|
|
|
|
|
|
|
loadSpritesheet();
|
|
|
|
loadAnimations();
|
|
|
|
}
|
|
|
|
|
2024-02-28 05:19:08 +00:00
|
|
|
/**
|
|
|
|
* Calls `kill()` on the group's members and then on the group itself.
|
|
|
|
* You can revive this group later via `revive()` after this.
|
|
|
|
*/
|
|
|
|
public override function kill():Void
|
|
|
|
{
|
|
|
|
super.kill();
|
|
|
|
}
|
|
|
|
|
|
|
|
public override function revive():Void
|
|
|
|
{
|
|
|
|
super.revive();
|
|
|
|
|
2024-02-28 08:01:20 +00:00
|
|
|
this.visible = true;
|
|
|
|
this.alpha = 1.0;
|
|
|
|
|
2024-02-28 05:19:08 +00:00
|
|
|
loadSpritesheet();
|
|
|
|
loadAnimations();
|
|
|
|
}
|
|
|
|
|
2023-06-16 21:37:56 +00:00
|
|
|
function loadSpritesheet():Void
|
|
|
|
{
|
2024-02-07 23:45:13 +00:00
|
|
|
trace('[SPEAKER] Loading spritesheet ${_data.assetPath} for ${id}');
|
2023-06-16 21:37:56 +00:00
|
|
|
|
2024-02-07 23:45:13 +00:00
|
|
|
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
|
2023-06-16 21:37:56 +00:00
|
|
|
if (tex == null)
|
|
|
|
{
|
2024-02-07 23:45:13 +00:00
|
|
|
trace('Could not load Sparrow sprite: ${_data.assetPath}');
|
2023-06-16 21:37:56 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.frames = tex;
|
|
|
|
|
2024-02-07 23:45:13 +00:00
|
|
|
if (_data.isPixel)
|
2023-06-16 21:37:56 +00:00
|
|
|
{
|
|
|
|
this.antialiasing = false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
this.antialiasing = true;
|
|
|
|
}
|
|
|
|
|
2024-02-07 23:45:13 +00:00
|
|
|
this.flipX = _data.flipX;
|
|
|
|
this.flipY = _data.flipY;
|
|
|
|
this.globalOffsets = _data.offsets;
|
|
|
|
this.setScale(_data.scale);
|
2023-06-16 21:37:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the sprite scale to the appropriate value.
|
2023-08-31 22:47:23 +00:00
|
|
|
* @param scale
|
2023-06-16 21:37:56 +00:00
|
|
|
*/
|
|
|
|
public function setScale(scale:Null<Float>):Void
|
|
|
|
{
|
|
|
|
if (scale == null) scale = 1.0;
|
|
|
|
this.scale.x = scale;
|
|
|
|
this.scale.y = scale;
|
|
|
|
this.updateHitbox();
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadAnimations():Void
|
|
|
|
{
|
2024-02-07 23:45:13 +00:00
|
|
|
trace('[SPEAKER] Loading ${_data.animations.length} animations for ${id}');
|
2023-06-16 21:37:56 +00:00
|
|
|
|
2024-02-07 23:45:13 +00:00
|
|
|
FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
|
2023-06-16 21:37:56 +00:00
|
|
|
|
2024-02-07 23:45:13 +00:00
|
|
|
for (anim in _data.animations)
|
2023-06-16 21:37:56 +00:00
|
|
|
{
|
|
|
|
if (anim.offsets == null)
|
|
|
|
{
|
|
|
|
setAnimationOffsets(anim.name, 0, 0);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var animNames:Array<String> = this.animation.getNameList();
|
2024-02-07 23:45:13 +00:00
|
|
|
trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${id}');
|
2023-06-16 21:37:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param name The name of the animation to play.
|
|
|
|
* @param restart Whether to restart the animation if it is already playing.
|
|
|
|
*/
|
|
|
|
public function playAnimation(name:String, restart:Bool = false):Void
|
|
|
|
{
|
|
|
|
var correctName:String = correctAnimationName(name);
|
|
|
|
if (correctName == null) return;
|
|
|
|
|
|
|
|
this.animation.play(correctName, restart, false, 0);
|
|
|
|
|
|
|
|
applyAnimationOffsets(correctName);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getCurrentAnimation():String
|
|
|
|
{
|
|
|
|
if (this.animation == null || this.animation.curAnim == null) return "";
|
|
|
|
return this.animation.curAnim.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensure that a given animation exists before playing it.
|
|
|
|
* Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
|
2023-08-31 22:47:23 +00:00
|
|
|
* @param name
|
2023-06-16 21:37:56 +00:00
|
|
|
*/
|
|
|
|
function correctAnimationName(name:String):String
|
|
|
|
{
|
|
|
|
// If the animation exists, we're good.
|
|
|
|
if (hasAnimation(name)) return name;
|
|
|
|
|
2024-03-01 13:13:06 +00:00
|
|
|
FlxG.log.notice('Speaker tried to play animation "$name" that does not exist, stripping suffixes...');
|
2023-06-16 21:37:56 +00:00
|
|
|
|
|
|
|
// Attempt to strip a `-alt` suffix, if it exists.
|
|
|
|
if (name.lastIndexOf('-') != -1)
|
|
|
|
{
|
|
|
|
var correctName = name.substring(0, name.lastIndexOf('-'));
|
2024-03-01 13:13:06 +00:00
|
|
|
FlxG.log.notice('Speaker tried to play animation "$name" that does not exist, stripping suffixes...');
|
2023-06-16 21:37:56 +00:00
|
|
|
return correctAnimationName(correctName);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (name != 'idle')
|
|
|
|
{
|
2024-03-01 13:13:06 +00:00
|
|
|
FlxG.log.warn('Speaker tried to play animation "$name" that does not exist, fallback to idle...');
|
2023-06-16 21:37:56 +00:00
|
|
|
return correctAnimationName('idle');
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-03-01 13:13:06 +00:00
|
|
|
FlxG.log.error('Speaker tried to play animation "idle" that does not exist! This is bad!');
|
2023-06-16 21:37:56 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function hasAnimation(id:String):Bool
|
|
|
|
{
|
|
|
|
if (this.animation == null) return false;
|
|
|
|
|
|
|
|
return this.animation.getByName(id) != null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Define the animation offsets for a specific animation.
|
|
|
|
*/
|
|
|
|
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
|
|
|
|
{
|
|
|
|
animationOffsets.set(name, [xOffset, yOffset]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve an apply the animation offsets for a specific animation.
|
|
|
|
*/
|
|
|
|
function applyAnimationOffsets(name:String):Void
|
|
|
|
{
|
|
|
|
var offsets:Array<Float> = animationOffsets.get(name);
|
|
|
|
if (offsets != null && !(offsets[0] == 0 && offsets[1] == 0))
|
|
|
|
{
|
|
|
|
this.animOffsets = offsets;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
this.animOffsets = [0, 0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function onDialogueStart(event:DialogueScriptEvent):Void {}
|
|
|
|
|
|
|
|
public function onDialogueCompleteLine(event:DialogueScriptEvent):Void {}
|
|
|
|
|
|
|
|
public function onDialogueLine(event:DialogueScriptEvent):Void {}
|
|
|
|
|
|
|
|
public function onDialogueSkip(event:DialogueScriptEvent):Void {}
|
|
|
|
|
|
|
|
public function onDialogueEnd(event:DialogueScriptEvent):Void {}
|
|
|
|
|
|
|
|
public function onUpdate(event:UpdateScriptEvent):Void {}
|
|
|
|
|
|
|
|
public function onDestroy(event:ScriptEvent):Void
|
|
|
|
{
|
|
|
|
frames = null;
|
|
|
|
|
|
|
|
this.x = 0;
|
|
|
|
this.y = 0;
|
|
|
|
this.globalOffsets = [0, 0];
|
|
|
|
this.alpha = 0;
|
|
|
|
|
|
|
|
this.kill();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function onScriptEvent(event:ScriptEvent):Void {}
|
2024-02-07 23:45:13 +00:00
|
|
|
|
|
|
|
public override function toString():String
|
|
|
|
{
|
|
|
|
return 'Speaker($id)';
|
|
|
|
}
|
|
|
|
|
|
|
|
static function _fetchData(id:String):Null<SpeakerData>
|
|
|
|
{
|
|
|
|
return SpeakerRegistry.instance.parseEntryDataWithMigration(id, SpeakerRegistry.instance.fetchEntryVersion(id));
|
|
|
|
}
|
2023-06-16 21:37:56 +00:00
|
|
|
}
|