Commit pending working-tree changes (local edits, line endings)
This commit is contained in:
1 parent
d9c83db4f0
commit
2c3d12e798
9 files changed
+487
-487
No files matched your search
+37
-37
@@ -1,37 +1,37 @@
|
|||||||
# Godot 4.x
|
# Godot 4.x
|
||||||
.godot/
|
.godot/
|
||||||
.idea/
|
.idea/
|
||||||
sdk/
|
sdk/
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
|
||||||
#ai
|
#ai
|
||||||
.omo/
|
.omo/
|
||||||
.opencode/
|
.opencode/
|
||||||
|
|
||||||
gen/
|
gen/
|
||||||
obj/
|
obj/
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
*.tres~
|
*.tres~
|
||||||
*.tscn~
|
*.tscn~
|
||||||
|
|
||||||
# GDScript LSP
|
# GDScript LSP
|
||||||
.godot/
|
.godot/
|
||||||
|
|
||||||
# Export
|
# Export
|
||||||
*.exe
|
*.exe
|
||||||
*.dmg
|
*.dmg
|
||||||
*.apk
|
*.apk
|
||||||
*.aab
|
*.aab
|
||||||
*.pck
|
*.pck
|
||||||
*.zip
|
*.zip
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
*.import
|
*.import
|
||||||
*.godot.uid
|
*.godot.uid
|
||||||
+1
-1
@@ -10,4 +10,4 @@
|
|||||||
],
|
],
|
||||||
"depends": [],
|
"depends": [],
|
||||||
"ui": []
|
"ui": []
|
||||||
}
|
}
|
||||||
@@ -1,161 +1,161 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Cthangover.Core.UI.Event;
|
using Cthangover.Core.UI.Event;
|
||||||
using Cthangover.Core.Utils;
|
using Cthangover.Core.Utils;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.Mods.Effects.Animation
|
namespace Cthangover.Mods.Effects.Animation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Frame-by-frame sprite animation via crossfade between two TextureRect nodes.
|
/// Frame-by-frame sprite animation via crossfade between two TextureRect nodes.
|
||||||
/// Rather than using a spritesheet, it keeps a "current" and "next" texture and
|
/// Rather than using a spritesheet, it keeps a "current" and "next" texture and
|
||||||
/// lerps their modulate between transparent and opaque, creating smooth transitions.
|
/// lerps their modulate between transparent and opaque, creating smooth transitions.
|
||||||
/// A built-in wait/waitPercent mechanism holds the "next" frame fully visible for
|
/// A built-in wait/waitPercent mechanism holds the "next" frame fully visible for
|
||||||
/// a configurable interval before advancing, producing a paced flip-book effect.
|
/// a configurable interval before advancing, producing a paced flip-book effect.
|
||||||
/// Exposes NextFrame/NextCycle events so external systems can sequence or react
|
/// Exposes NextFrame/NextCycle events so external systems can sequence or react
|
||||||
/// to animation milestones without polling.
|
/// to animation milestones without polling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class AnimationController : Control, IOnUpdateEvent
|
public partial class AnimationController : Control, IOnUpdateEvent
|
||||||
{
|
{
|
||||||
|
|
||||||
[Export] private TextureRect current;
|
[Export] private TextureRect current;
|
||||||
[Export] private TextureRect next;
|
[Export] private TextureRect next;
|
||||||
[Export] private Texture2D empty;
|
[Export] private Texture2D empty;
|
||||||
|
|
||||||
[Export] private bool isLoop = true;
|
[Export] private bool isLoop = true;
|
||||||
[Export] private float speed = 1f;
|
[Export] private float speed = 1f;
|
||||||
[Export] private float nextFrameSpeed = 0.2f;
|
[Export] private float nextFrameSpeed = 0.2f;
|
||||||
|
|
||||||
/// <summary>Fires when the current frame transition completes and the next frame texture is assigned.</summary>
|
/// <summary>Fires when the current frame transition completes and the next frame texture is assigned.</summary>
|
||||||
public event Action NextFrame;
|
public event Action NextFrame;
|
||||||
/// <summary>Fires when the animation wraps around (looping) or reaches the final frame (non-looping).</summary>
|
/// <summary>Fires when the animation wraps around (looping) or reaches the final frame (non-looping).</summary>
|
||||||
public event Action NextCycle;
|
public event Action NextCycle;
|
||||||
|
|
||||||
[Export] private Color clearColor = new(1f, 1f, 1f, 0f);
|
[Export] private Color clearColor = new(1f, 1f, 1f, 0f);
|
||||||
[Export] private Color completeColor = new(1f, 1f, 1f, 1f);
|
[Export] private Color completeColor = new(1f, 1f, 1f, 1f);
|
||||||
|
|
||||||
private int currentFrame;
|
private int currentFrame;
|
||||||
private double timestamp;
|
private double timestamp;
|
||||||
private float percent;
|
private float percent;
|
||||||
private bool wait;
|
private bool wait;
|
||||||
private float waitPercent;
|
private float waitPercent;
|
||||||
private bool isStarted;
|
private bool isStarted;
|
||||||
|
|
||||||
/// <summary>When true, the animation wraps from the last frame back to the first. When false, playback stops at the final frame.</summary>
|
/// <summary>When true, the animation wraps from the last frame back to the first. When false, playback stops at the final frame.</summary>
|
||||||
public bool IsLoop { get => isLoop; set => isLoop = value; }
|
public bool IsLoop { get => isLoop; set => isLoop = value; }
|
||||||
/// <summary>Crossfade speed multiplier between frames. Higher values produce faster transitions.</summary>
|
/// <summary>Crossfade speed multiplier between frames. Higher values produce faster transitions.</summary>
|
||||||
public float Speed { get => speed; set => speed = value; }
|
public float Speed { get => speed; set => speed = value; }
|
||||||
/// <summary>Hold duration multiplier for the "wait" phase after a frame transition completes.</summary>
|
/// <summary>Hold duration multiplier for the "wait" phase after a frame transition completes.</summary>
|
||||||
public float NextFrameSpeed { get => nextFrameSpeed; set => nextFrameSpeed = value; }
|
public float NextFrameSpeed { get => nextFrameSpeed; set => nextFrameSpeed = value; }
|
||||||
/// <summary>Whether playback is currently active. False on startup and after <see cref="Pause"/> or <see cref="Clear"/>.</summary>
|
/// <summary>Whether playback is currently active. False on startup and after <see cref="Pause"/> or <see cref="Clear"/>.</summary>
|
||||||
public bool IsStarted => isStarted;
|
public bool IsStarted => isStarted;
|
||||||
|
|
||||||
/// <summary>Ordered list of textures to cycle through. Set externally before calling <see cref="Play"/>.</summary>
|
/// <summary>Ordered list of textures to cycle through. Set externally before calling <see cref="Play"/>.</summary>
|
||||||
public List<Texture2D> FrameSet { get; set; }
|
public List<Texture2D> FrameSet { get; set; }
|
||||||
|
|
||||||
/// <summary>Resets to idle state: stops playback, clears frame set, and blanks both texture nodes to transparent.</summary>
|
/// <summary>Resets to idle state: stops playback, clears frame set, and blanks both texture nodes to transparent.</summary>
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
isStarted = false;
|
isStarted = false;
|
||||||
FrameSet = null;
|
FrameSet = null;
|
||||||
current.Texture = empty;
|
current.Texture = empty;
|
||||||
current.Modulate = Colors.Transparent;
|
current.Modulate = Colors.Transparent;
|
||||||
next.Texture = empty;
|
next.Texture = empty;
|
||||||
next.Modulate = Colors.Transparent;
|
next.Modulate = Colors.Transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Stops playback without clearing textures, freezing on the current frame.</summary>
|
/// <summary>Stops playback without clearing textures, freezing on the current frame.</summary>
|
||||||
public void Pause()
|
public void Pause()
|
||||||
{
|
{
|
||||||
isStarted = false;
|
isStarted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Starts or restarts playback from the first frame in <see cref="FrameSet"/>.</summary>
|
/// <summary>Starts or restarts playback from the first frame in <see cref="FrameSet"/>.</summary>
|
||||||
public void Play()
|
public void Play()
|
||||||
{
|
{
|
||||||
currentFrame = 0;
|
currentFrame = 0;
|
||||||
isStarted = true;
|
isStarted = true;
|
||||||
timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||||
|
|
||||||
current.Texture = empty;
|
current.Texture = empty;
|
||||||
next.Modulate = clearColor;
|
next.Modulate = clearColor;
|
||||||
next.Texture = FrameSet[currentFrame];
|
next.Texture = FrameSet[currentFrame];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Update priority. Lower numbers run earlier in the frame. Default 1.</summary>
|
/// <summary>Update priority. Lower numbers run earlier in the frame. Default 1.</summary>
|
||||||
public int Priority => 1;
|
public int Priority => 1;
|
||||||
/// <summary>Advances the crossfade each frame. Handles both the transition phase (lerping modulate) and the wait phase (holding the next frame visible).</summary>
|
/// <summary>Advances the crossfade each frame. Handles both the transition phase (lerping modulate) and the wait phase (holding the next frame visible).</summary>
|
||||||
public void OnUpdate()
|
public void OnUpdate()
|
||||||
{
|
{
|
||||||
if(!isStarted)
|
if(!isStarted)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (Lists.IsEmpty(FrameSet))
|
if (Lists.IsEmpty(FrameSet))
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double now = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
double now = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||||
if (!wait)
|
if (!wait)
|
||||||
{
|
{
|
||||||
float delta = (float)(now - timestamp) * speed;
|
float delta = (float)(now - timestamp) * speed;
|
||||||
percent = Mathf.Min(percent + delta, 1f);
|
percent = Mathf.Min(percent + delta, 1f);
|
||||||
current.Modulate = completeColor.Lerp(Colors.Transparent, percent);
|
current.Modulate = completeColor.Lerp(Colors.Transparent, percent);
|
||||||
next.Modulate = Colors.Transparent.Lerp(completeColor, percent);
|
next.Modulate = Colors.Transparent.Lerp(completeColor, percent);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
float delta = (float)(now - timestamp) * nextFrameSpeed;
|
float delta = (float)(now - timestamp) * nextFrameSpeed;
|
||||||
waitPercent = Mathf.Min(waitPercent + delta, 1f);
|
waitPercent = Mathf.Min(waitPercent + delta, 1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
timestamp = now;
|
timestamp = now;
|
||||||
|
|
||||||
if (percent >= 1f && !wait)
|
if (percent >= 1f && !wait)
|
||||||
{
|
{
|
||||||
wait = true;
|
wait = true;
|
||||||
SwitchNextFrame();
|
SwitchNextFrame();
|
||||||
percent = 0f;
|
percent = 0f;
|
||||||
}
|
}
|
||||||
if (waitPercent >= 1f && wait)
|
if (waitPercent >= 1f && wait)
|
||||||
{
|
{
|
||||||
wait = false;
|
wait = false;
|
||||||
waitPercent = 0f;
|
waitPercent = 0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SwitchNextFrame()
|
private void SwitchNextFrame()
|
||||||
{
|
{
|
||||||
NextFrame?.Invoke();
|
NextFrame?.Invoke();
|
||||||
if (Lists.IsEmpty(FrameSet))
|
if (Lists.IsEmpty(FrameSet))
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var needNextCycle = false;
|
var needNextCycle = false;
|
||||||
currentFrame++;
|
currentFrame++;
|
||||||
if (FrameSet.Count <= currentFrame)
|
if (FrameSet.Count <= currentFrame)
|
||||||
{
|
{
|
||||||
if (isLoop)
|
if (isLoop)
|
||||||
currentFrame = 0;
|
currentFrame = 0;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
currentFrame--;
|
currentFrame--;
|
||||||
Pause();
|
Pause();
|
||||||
}
|
}
|
||||||
needNextCycle = true;
|
needNextCycle = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
current.Modulate = completeColor;
|
current.Modulate = completeColor;
|
||||||
current.Texture = next.Texture;
|
current.Texture = next.Texture;
|
||||||
|
|
||||||
next.Modulate = clearColor;
|
next.Modulate = clearColor;
|
||||||
next.Texture = FrameSet[currentFrame];
|
next.Texture = FrameSet[currentFrame];
|
||||||
|
|
||||||
if(needNextCycle)
|
if(needNextCycle)
|
||||||
NextCycle?.Invoke();
|
NextCycle?.Invoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,84 +1,84 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.Mods.Effects.Animation
|
namespace Cthangover.Mods.Effects.Animation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lightweight, code-only alternative to EffectAnimator that does NOT require
|
/// Lightweight, code-only alternative to EffectAnimator that does NOT require
|
||||||
/// a shader. Constructs an AtlasTexture and shifts its Region rect each frame
|
/// a shader. Constructs an AtlasTexture and shifts its Region rect each frame
|
||||||
/// to scroll through a horizontal spritesheet. Designed for runtime spawning
|
/// to scroll through a horizontal spritesheet. Designed for runtime spawning
|
||||||
/// via the static SpawnOn factory — it anchors to fill the target Control and
|
/// via the static SpawnOn factory — it anchors to fill the target Control and
|
||||||
/// self-destructs when playback finishes, requiring no scene file setup.
|
/// self-destructs when playback finishes, requiring no scene file setup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class EffectAnimNode : TextureRect
|
public partial class EffectAnimNode : TextureRect
|
||||||
{
|
{
|
||||||
private AtlasTexture atlas;
|
private AtlasTexture atlas;
|
||||||
private int frameCount;
|
private int frameCount;
|
||||||
private float frameInterval;
|
private float frameInterval;
|
||||||
private float elapsed;
|
private float elapsed;
|
||||||
private int currentFrame = -1;
|
private int currentFrame = -1;
|
||||||
private int frameWidth;
|
private int frameWidth;
|
||||||
private float totalDuration;
|
private float totalDuration;
|
||||||
|
|
||||||
/// <summary>Factory method that creates the node, attaches it to <paramref name="target"/> filling the full rect, and initializes the spritesheet animation. Returns null if target or sheet is null.</summary>
|
/// <summary>Factory method that creates the node, attaches it to <paramref name="target"/> filling the full rect, and initializes the spritesheet animation. Returns null if target or sheet is null.</summary>
|
||||||
/// <param name="target">Parent Control to anchor the animation to.</param>
|
/// <param name="target">Parent Control to anchor the animation to.</param>
|
||||||
/// <param name="sheet">Horizontal spritesheet texture. Frame width = sheet width / frameCount.</param>
|
/// <param name="sheet">Horizontal spritesheet texture. Frame width = sheet width / frameCount.</param>
|
||||||
/// <param name="frameCount">Number of frames in the spritesheet.</param>
|
/// <param name="frameCount">Number of frames in the spritesheet.</param>
|
||||||
/// <param name="totalDuration">Total playback time in seconds; frameInterval = totalDuration / frameCount.</param>
|
/// <param name="totalDuration">Total playback time in seconds; frameInterval = totalDuration / frameCount.</param>
|
||||||
public static EffectAnimNode SpawnOn(Control target, Texture2D sheet, int frameCount, float totalDuration)
|
public static EffectAnimNode SpawnOn(Control target, Texture2D sheet, int frameCount, float totalDuration)
|
||||||
{
|
{
|
||||||
if (target == null || sheet == null)
|
if (target == null || sheet == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var node = new EffectAnimNode();
|
var node = new EffectAnimNode();
|
||||||
target.AddChild(node);
|
target.AddChild(node);
|
||||||
node.AnchorLeft = 0;
|
node.AnchorLeft = 0;
|
||||||
node.AnchorTop = 0;
|
node.AnchorTop = 0;
|
||||||
node.AnchorRight = 1;
|
node.AnchorRight = 1;
|
||||||
node.AnchorBottom = 1;
|
node.AnchorBottom = 1;
|
||||||
node.OffsetLeft = 0;
|
node.OffsetLeft = 0;
|
||||||
node.OffsetTop = 0;
|
node.OffsetTop = 0;
|
||||||
node.OffsetRight = 0;
|
node.OffsetRight = 0;
|
||||||
node.OffsetBottom = 0;
|
node.OffsetBottom = 0;
|
||||||
node.MouseFilter = MouseFilterEnum.Ignore;
|
node.MouseFilter = MouseFilterEnum.Ignore;
|
||||||
node.ZIndex = 100;
|
node.ZIndex = 100;
|
||||||
node.Init(sheet, frameCount, totalDuration);
|
node.Init(sheet, frameCount, totalDuration);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Init(Texture2D sheet, int frameCount, float totalDuration)
|
private void Init(Texture2D sheet, int frameCount, float totalDuration)
|
||||||
{
|
{
|
||||||
this.frameCount = frameCount;
|
this.frameCount = frameCount;
|
||||||
this.totalDuration = totalDuration;
|
this.totalDuration = totalDuration;
|
||||||
frameInterval = totalDuration / frameCount;
|
frameInterval = totalDuration / frameCount;
|
||||||
frameWidth = sheet.GetWidth() / frameCount;
|
frameWidth = sheet.GetWidth() / frameCount;
|
||||||
|
|
||||||
atlas = new AtlasTexture
|
atlas = new AtlasTexture
|
||||||
{
|
{
|
||||||
Atlas = sheet,
|
Atlas = sheet,
|
||||||
Region = new Rect2(0, 0, frameWidth, sheet.GetHeight()),
|
Region = new Rect2(0, 0, frameWidth, sheet.GetHeight()),
|
||||||
};
|
};
|
||||||
Texture = atlas;
|
Texture = atlas;
|
||||||
ExpandMode = ExpandModeEnum.IgnoreSize;
|
ExpandMode = ExpandModeEnum.IgnoreSize;
|
||||||
StretchMode = StretchModeEnum.KeepAspectCentered;
|
StretchMode = StretchModeEnum.KeepAspectCentered;
|
||||||
SetFrame(0);
|
SetFrame(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Steps the atlas region forward per frame interval and frees the node once the total duration has played.</summary>
|
/// <summary>Steps the atlas region forward per frame interval and frees the node once the total duration has played.</summary>
|
||||||
public override void _Process(double delta)
|
public override void _Process(double delta)
|
||||||
{
|
{
|
||||||
elapsed += (float)delta;
|
elapsed += (float)delta;
|
||||||
var frame = Mathf.Min((int)(elapsed / frameInterval), frameCount - 1);
|
var frame = Mathf.Min((int)(elapsed / frameInterval), frameCount - 1);
|
||||||
if (frame != currentFrame)
|
if (frame != currentFrame)
|
||||||
SetFrame(frame);
|
SetFrame(frame);
|
||||||
|
|
||||||
if (elapsed >= totalDuration)
|
if (elapsed >= totalDuration)
|
||||||
QueueFree();
|
QueueFree();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetFrame(int frame)
|
private void SetFrame(int frame)
|
||||||
{
|
{
|
||||||
currentFrame = frame;
|
currentFrame = frame;
|
||||||
atlas.Region = new Rect2(frame * frameWidth, 0, frameWidth, atlas.Region.Size.Y);
|
atlas.Region = new Rect2(frame * frameWidth, 0, frameWidth, atlas.Region.Size.Y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,71 +1,71 @@
|
|||||||
using Cthangover.Core.UI.Event;
|
using Cthangover.Core.UI.Event;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.Mods.Effects.Animation
|
namespace Cthangover.Mods.Effects.Animation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shader-driven frame animation: drives _FrameIndex and _FrameCount uniforms
|
/// Shader-driven frame animation: drives _FrameIndex and _FrameCount uniforms
|
||||||
/// on the TextureRect's ShaderMaterial rather than swapping textures. This means
|
/// on the TextureRect's ShaderMaterial rather than swapping textures. This means
|
||||||
/// all animation frames live in a single GPU texture atlas and the shader selects
|
/// all animation frames live in a single GPU texture atlas and the shader selects
|
||||||
/// the visible frame. Auto-destroys (QueueFree) after totalDuration elapses,
|
/// the visible frame. Auto-destroys (QueueFree) after totalDuration elapses,
|
||||||
/// making it suitable for fire-and-forget VFX that don't need external cleanup.
|
/// making it suitable for fire-and-forget VFX that don't need external cleanup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class EffectAnimator : Control, IOnUpdateEvent
|
public partial class EffectAnimator : Control, IOnUpdateEvent
|
||||||
{
|
{
|
||||||
|
|
||||||
[Export] private TextureRect image;
|
[Export] private TextureRect image;
|
||||||
[Export] private float totalDuration = 5f;
|
[Export] private float totalDuration = 5f;
|
||||||
[Export] private int frameCount = 5;
|
[Export] private int frameCount = 5;
|
||||||
|
|
||||||
private ShaderMaterial materialInstance;
|
private ShaderMaterial materialInstance;
|
||||||
private double startTime;
|
private double startTime;
|
||||||
private float frameInterval;
|
private float frameInterval;
|
||||||
private int currentFrame;
|
private int currentFrame;
|
||||||
|
|
||||||
private static readonly StringName FrameIndexId = "_FrameIndex";
|
private static readonly StringName FrameIndexId = "_FrameIndex";
|
||||||
private static readonly StringName FrameCountId = "_FrameCount";
|
private static readonly StringName FrameCountId = "_FrameCount";
|
||||||
|
|
||||||
/// <summary>Precomputes the frame interval and pushes the frame count uniform into the shader material.</summary>
|
/// <summary>Precomputes the frame interval and pushes the frame count uniform into the shader material.</summary>
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
frameInterval = totalDuration / frameCount;
|
frameInterval = totalDuration / frameCount;
|
||||||
materialInstance = image.Material as ShaderMaterial;
|
materialInstance = image.Material as ShaderMaterial;
|
||||||
if (materialInstance != null)
|
if (materialInstance != null)
|
||||||
materialInstance.SetShaderParameter(FrameCountId, frameCount);
|
materialInstance.SetShaderParameter(FrameCountId, frameCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Intentionally empty — the animator drives frames through the event controller instead of engine callbacks.</summary>
|
/// <summary>Intentionally empty — the animator drives frames through the event controller instead of engine callbacks.</summary>
|
||||||
public override void _EnterTree()
|
public override void _EnterTree()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Starts the shader animation from frame 0, resetting the elapsed timer.</summary>
|
/// <summary>Starts the shader animation from frame 0, resetting the elapsed timer.</summary>
|
||||||
public void Play()
|
public void Play()
|
||||||
{
|
{
|
||||||
startTime = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
startTime = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||||
currentFrame = 0;
|
currentFrame = 0;
|
||||||
if (materialInstance != null)
|
if (materialInstance != null)
|
||||||
materialInstance.SetShaderParameter(FrameIndexId, 0f);
|
materialInstance.SetShaderParameter(FrameIndexId, 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Update priority. Default 1.</summary>
|
/// <summary>Update priority. Default 1.</summary>
|
||||||
public int Priority => 1;
|
public int Priority => 1;
|
||||||
|
|
||||||
/// <summary>Advances the shader's _FrameIndex uniform each frame based on elapsed time. Self-destructs when <c>totalDuration</c> elapses.</summary>
|
/// <summary>Advances the shader's _FrameIndex uniform each frame based on elapsed time. Self-destructs when <c>totalDuration</c> elapses.</summary>
|
||||||
public void OnUpdate()
|
public void OnUpdate()
|
||||||
{
|
{
|
||||||
double now = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
double now = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||||
var elapsed = now - startTime;
|
var elapsed = now - startTime;
|
||||||
var newFrame = Mathf.Min((int)(elapsed / frameInterval), frameCount - 1);
|
var newFrame = Mathf.Min((int)(elapsed / frameInterval), frameCount - 1);
|
||||||
|
|
||||||
if (newFrame != currentFrame && materialInstance != null)
|
if (newFrame != currentFrame && materialInstance != null)
|
||||||
{
|
{
|
||||||
currentFrame = newFrame;
|
currentFrame = newFrame;
|
||||||
materialInstance.SetShaderParameter(FrameIndexId, currentFrame);
|
materialInstance.SetShaderParameter(FrameIndexId, currentFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elapsed >= totalDuration)
|
if (elapsed >= totalDuration)
|
||||||
QueueFree();
|
QueueFree();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
namespace Cthangover.Mods.Effects.Animation
|
namespace Cthangover.Mods.Effects.Animation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specialisation of AnimationController where Pause() also calls Clear(),
|
/// Specialisation of AnimationController where Pause() also calls Clear(),
|
||||||
/// treating "pause" as a hard reset. This is intentional for one-shot effects
|
/// treating "pause" as a hard reset. This is intentional for one-shot effects
|
||||||
/// that should clean up instantly rather than freezing on their last frame.
|
/// that should clean up instantly rather than freezing on their last frame.
|
||||||
/// The "new" keyword on Pause() shadows the base implementation on purpose.
|
/// The "new" keyword on Pause() shadows the base implementation on purpose.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class EffectController : AnimationController
|
public partial class EffectController : AnimationController
|
||||||
{
|
{
|
||||||
/// <summary>Shadows the base Pause() and calls <see cref="AnimationController.Clear"/> instead, instantly resetting one-shot effects.</summary>
|
/// <summary>Shadows the base Pause() and calls <see cref="AnimationController.Clear"/> instead, instantly resetting one-shot effects.</summary>
|
||||||
public new void Pause()
|
public new void Pause()
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+82
-82
@@ -1,82 +1,82 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Cthangover.Core.Factories;
|
using Cthangover.Core.Factories;
|
||||||
using Cthangover.Core.Mods;
|
using Cthangover.Core.Mods;
|
||||||
using Cthangover.Core.Mods.Resolvers;
|
using Cthangover.Core.Mods.Resolvers;
|
||||||
using Cthangover.Core.Utils;
|
using Cthangover.Core.Utils;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.Mods.Effects
|
namespace Cthangover.Mods.Effects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory for <c>PackedScene</c> visual effects. Overrides
|
/// Factory for <c>PackedScene</c> visual effects. Overrides
|
||||||
/// <c>LoadFromMods</c> to use <c>ResourceLoader.Load</c> from the file
|
/// <c>LoadFromMods</c> to use <c>ResourceLoader.Load</c> from the file
|
||||||
/// system instead of <c>ConvertFromBytes</c>, because Godot's scene
|
/// system instead of <c>ConvertFromBytes</c>, because Godot's scene
|
||||||
/// format is a proprietary binary blob that cannot be reconstructed
|
/// format is a proprietary binary blob that cannot be reconstructed
|
||||||
/// from raw bytes in managed code. This imposes a hard constraint:
|
/// from raw bytes in managed code. This imposes a hard constraint:
|
||||||
/// effect scenes are only loadable from <b>folder mods</b> — zip mods
|
/// effect scenes are only loadable from <b>folder mods</b> — zip mods
|
||||||
/// skip them with a warning — so mod authors distributing visual effects
|
/// skip them with a warning — so mod authors distributing visual effects
|
||||||
/// must ship them unpacked.
|
/// must ship them unpacked.
|
||||||
/// <para/>
|
/// <para/>
|
||||||
/// The single instance is a self-contained lazy singleton owned by this
|
/// The single instance is a self-contained lazy singleton owned by this
|
||||||
/// mod assembly; consumers resolve it through <see cref="Instance"/>.
|
/// mod assembly; consumers resolve it through <see cref="Instance"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EffectFactory : PrefabFactory<PackedScene>
|
public class EffectFactory : PrefabFactory<PackedScene>
|
||||||
{
|
{
|
||||||
private static readonly Lazy<EffectFactory> instance = new(() => new EffectFactory());
|
private static readonly Lazy<EffectFactory> instance = new(() => new EffectFactory());
|
||||||
|
|
||||||
/// <summary>Process-wide instance; laziness matches the former AppServices slot.</summary>
|
/// <summary>Process-wide instance; laziness matches the former AppServices slot.</summary>
|
||||||
public static EffectFactory Instance => instance.Value;
|
public static EffectFactory Instance => instance.Value;
|
||||||
|
|
||||||
private EffectFactory() : base("effects", 64) { }
|
private EffectFactory() : base("effects", 64) { }
|
||||||
|
|
||||||
/// <summary>Mod asset group scanned by this factory — files live under <c>mods/*/effects</c>.</summary>
|
/// <summary>Mod asset group scanned by this factory — files live under <c>mods/*/effects</c>.</summary>
|
||||||
public override string GroupName => "effects";
|
public override string GroupName => "effects";
|
||||||
|
|
||||||
/// <summary>Scene file extensions accepted during the mod scan.</summary>
|
/// <summary>Scene file extensions accepted during the mod scan.</summary>
|
||||||
protected override List<string> Extensions { get; } = new() { ".tscn", ".scn" };
|
protected override List<string> Extensions { get; } = new() { ".tscn", ".scn" };
|
||||||
|
|
||||||
/// <summary>Always fails with a log — PackedScene is a proprietary Godot blob; use <see cref="LoadFromMods"/> (folder mods only).</summary>
|
/// <summary>Always fails with a log — PackedScene is a proprietary Godot blob; use <see cref="LoadFromMods"/> (folder mods only).</summary>
|
||||||
protected override PackedScene ConvertFromBytes(string id, byte[] data, string extension)
|
protected override PackedScene ConvertFromBytes(string id, byte[] data, string extension)
|
||||||
{
|
{
|
||||||
GameLogger.Log("FACTORY", $"PackedScene cannot be constructed from bytes; use folder mods for '{id}'", LogLevel.Error);
|
GameLogger.Log("FACTORY", $"PackedScene cannot be constructed from bytes; use folder mods for '{id}'", LogLevel.Error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads the scene via <c>ResourceLoader</c> from the mod's real file
|
/// Loads the scene via <c>ResourceLoader</c> from the mod's real file
|
||||||
/// system path — which only folder mods can provide; zip mods are
|
/// system path — which only folder mods can provide; zip mods are
|
||||||
/// skipped with a warning.
|
/// skipped with a warning.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected override PackedScene LoadFromMods(string id, string extension)
|
protected override PackedScene LoadFromMods(string id, string extension)
|
||||||
{
|
{
|
||||||
var files = Files.Collect(GroupName);
|
var files = Files.Collect(GroupName);
|
||||||
|
|
||||||
string matchedKey = null;
|
string matchedKey = null;
|
||||||
foreach (var key in files.Keys)
|
foreach (var key in files.Keys)
|
||||||
{
|
{
|
||||||
if (key == id || key.Equals(id + extension, StringComparison.OrdinalIgnoreCase))
|
if (key == id || key.Equals(id + extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
matchedKey = key;
|
matchedKey = key;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedKey == null || !files.TryGetValue(matchedKey, out var entry))
|
if (matchedKey == null || !files.TryGetValue(matchedKey, out var entry))
|
||||||
{
|
{
|
||||||
GameLogger.Log("FACTORY", $"effect '{id}' - not found in group '{GroupName}'!", LogLevel.Error);
|
GameLogger.Log("FACTORY", $"effect '{id}' - not found in group '{GroupName}'!", LogLevel.Error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fsPath = ModManager.Instance.GetFileSystemPath(entry.ModId, entry.FullPath);
|
var fsPath = ModManager.Instance.GetFileSystemPath(entry.ModId, entry.FullPath);
|
||||||
if (fsPath == null)
|
if (fsPath == null)
|
||||||
{
|
{
|
||||||
GameLogger.Log("FACTORY", $"effect '{id}' - only folder mods support PackedScene (zip mod '{entry.ModId}' skipped)", LogLevel.Warning);
|
GameLogger.Log("FACTORY", $"effect '{id}' - only folder mods support PackedScene (zip mod '{entry.ModId}' skipped)", LogLevel.Warning);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResourceLoader.Load<PackedScene>(fsPath);
|
return ResourceLoader.Load<PackedScene>(fsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
using Cthangover.Core.Mods;
|
using Cthangover.Core.Mods;
|
||||||
|
|
||||||
namespace Cthangover.Mods.Effects
|
namespace Cthangover.Mods.Effects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Engine-mod initializer for the effects subsystem. The factory is a
|
/// Engine-mod initializer for the effects subsystem. The factory is a
|
||||||
/// self-contained lazy singleton resolved by consumers through
|
/// self-contained lazy singleton resolved by consumers through
|
||||||
/// <c>EffectFactory.Instance</c>, so there is no core binding to
|
/// <c>EffectFactory.Instance</c>, so there is no core binding to
|
||||||
/// re-establish on hot reload — the initializer exists to register the
|
/// re-establish on hot reload — the initializer exists to register the
|
||||||
/// assembly with the mod pipeline and keep the migration shape uniform
|
/// assembly with the mod pipeline and keep the migration shape uniform
|
||||||
/// with the other engine mods.
|
/// with the other engine mods.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EffectsModInitializer : IModInitializer
|
public class EffectsModInitializer : IModInitializer
|
||||||
{
|
{
|
||||||
/// <summary>No per-mod setup is required.</summary>
|
/// <summary>No per-mod setup is required.</summary>
|
||||||
public void OnModLoaded(string modId) { }
|
public void OnModLoaded(string modId) { }
|
||||||
|
|
||||||
/// <summary>Nothing to bind: the factory lazy self-initializes on first use.</summary>
|
/// <summary>Nothing to bind: the factory lazy self-initializes on first use.</summary>
|
||||||
public void OnModResourcesReady() { }
|
public void OnModResourcesReady() { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
namespace Cthangover.Mods.Effects
|
namespace Cthangover.Mods.Effects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Migration marker for the coverage pipeline (<c>SubsystemsBridge</c>
|
/// Migration marker for the coverage pipeline (<c>SubsystemsBridge</c>
|
||||||
/// reflection scan): declares the effects subsystem fully moved out of
|
/// reflection scan): declares the effects subsystem fully moved out of
|
||||||
/// the core assembly into this engine mod. The id must match the
|
/// the core assembly into this engine mod. The id must match the
|
||||||
/// <c>DeclaredTests</c> entry in <c>SubsystemsCoverageTestSetup</c>.
|
/// <c>DeclaredTests</c> entry in <c>SubsystemsCoverageTestSetup</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SubsystemRegistration
|
public static class SubsystemRegistration
|
||||||
{
|
{
|
||||||
public static string SubsystemId => "effects";
|
public static string SubsystemId => "effects";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user