Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Each effect follows this structure:
2. A dedicated `MonoBehaviour` runner (nested in the same file or a sibling file) spawned via `EffectRuntimeController.Instance.SpawnRunner<T>(...)` inside `TrySchedule`.
3. The runner handles timing (start/end beat), per-frame updates in `LateUpdate`, and cleanup in `Stop`/`OnDisable`.
4. Registration via `EffectDefinitionRegistry.Initialize(...)` using `Register(new YourEffect())`.
5. Documentation media at `docs/effects/<effect-id>/preview.gif` and `docs/effects/<effect-id>/demo.mp4`, and an entry in `docs/effects/README.md`.
5. A short `.mp4` preview video (uploaded to GitHub, not stored in the repository) showing the event selected with its properties and the effect being applied in the editor, with the link included in `docs/effects/README.md` under the **Preview** section. Since agents cannot run the game to record this, include the following placeholder instead and leave it for the PR author to replace: `<!-- TODO: record preview .mp4 in-game, upload to GitHub, and replace this placeholder with the link -->`.

**Handling effect conflicts and concurrent instances:**
- Before implementing a new effect, consider how it interacts with all existing effects when running simultaneously.
Expand Down
2 changes: 0 additions & 2 deletions BopVisualEffects/Core/EffectDefinitionRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
using BopVisualEffects.Effects.Vignette;
using BopVisualEffects.Effects.ZoomIn;
using BopVisualEffects.Effects.ZoomOut;
using BopVisualEffects.Effects.ZoomPulse;

namespace BopVisualEffects.Core;

Expand Down Expand Up @@ -70,7 +69,6 @@ public static void Initialize(string pluginGuid, ClassLogger log, ConfigFile con
registry.Register(new VignetteEffect());
registry.Register(new ZoomInEffect());
registry.Register(new ZoomOutEffect());
registry.Register(new ZoomPulseEffect());
_instance = registry;
}

Expand Down
53 changes: 53 additions & 0 deletions BopVisualEffects/Core/EntityExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using UnityEngine;

namespace BopVisualEffects.Core;

/// <summary>
/// Extension methods for reading typed values from <see cref="Entity"/> properties.
/// </summary>
internal static class EntityExtensions
{
/// <summary>
/// Reads a <see cref="Color"/> from an entity property by <paramref name="key"/>.
/// </summary>
/// <remarks>
/// Handles two cases, in order:
/// <list type="number">
/// <item>The value is already a <see cref="Color"/> struct (in-memory, before a save/load cycle).</item>
/// <item>
/// The value is a <see cref="Dictionary{TKey,TValue}"/> with r/g/b/a sub-keys
/// (produced when the game JSON-deserializes a stored <see cref="Color"/> without type info).
/// </item>
/// </list>
/// Returns <paramref name="fallback"/> (default: <see cref="Color.white"/>) when neither applies.
/// </remarks>
/// <param name="entity">The entity to read from.</param>
/// <param name="key">Property key holding the color value.</param>
/// <param name="fallback">Value returned when the property is absent or unrecognised.</param>
public static Color GetColor(this Entity entity, string key, Color? fallback = null)
{
if (!entity.dynamicData.TryGetValue(key, out var value) || value is null) return fallback ?? Color.white;
return value switch
{
Color color => color,
Dictionary<string, object> dict => new Color(GetSubFloat(dict, "r"), GetSubFloat(dict, "g"),
GetSubFloat(dict, "b"), GetSubFloat(dict, "a", 1f)),
Comment thread
Brollyy marked this conversation as resolved.
_ => fallback ?? Color.white
};
}

private static float GetSubFloat(Dictionary<string, object> dict, string key, float defaultValue = 0f)
{
if (!dict.TryGetValue(key, out object? value) || value is null)
return defaultValue;

return value switch
{
float f => f,
double d => (float)d,
int i => i,
_ => defaultValue
};
}
}
44 changes: 13 additions & 31 deletions BopVisualEffects/Effects/ColorTint/ColorTintEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
resizable = true,
properties = new Dictionary<string, object>
{
["r"] = 1.0f,
["g"] = 0.0f,
["b"] = 0.0f,
["alpha"] = 0.25f
["color"] = new MixtapeEventTemplates.ColorField(new Color(1.0f, 0.0f, 0.0f, 0.25f))
}
Comment thread
Brollyy marked this conversation as resolved.
};
}
Expand All @@ -45,10 +42,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
{
var log = ClassLogger.GetForClass<ColorTintEffect>();
var durationBeats = Mathf.Max(0.01f, entity.length);
var r = entity.GetFloat("r");
var g = entity.GetFloat("g");
var b = entity.GetFloat("b");
var alpha = entity.GetFloat("alpha");
var color = entity.GetColor("color");
var startBeat = entity.beat;
Comment thread
Brollyy marked this conversation as resolved.
var endBeat = startBeat + durationBeats;

Expand All @@ -59,17 +53,14 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner<ColorTintRunner>(runner =>
runner.Initialize(loader, loader.jukebox, startBeat, endBeat, r, g, b, alpha));
runner.Initialize(loader, loader.jukebox, startBeat, endBeat, color));
}
}

private sealed class ColorTintRunner : MonoBehaviour
{
private bool _initialized;
private float _r;
private float _g;
private float _b;
private float _maxAlpha;
private Color _color;
private float _startBeat;
private float _endBeat;
private MixtapeLoaderCustom? _loader;
Expand All @@ -79,16 +70,13 @@ private sealed class ColorTintRunner : MonoBehaviour
/// <summary>
/// Initializes this runner with effect parameters.
/// </summary>
public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float r, float g, float b, float alpha)
public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, Color color)
{
_loader = loader;
_jukebox = jukebox;
_startBeat = startBeat;
_endBeat = endBeat;
_r = Mathf.Clamp01(r);
_g = Mathf.Clamp01(g);
_b = Mathf.Clamp01(b);
_maxAlpha = Mathf.Clamp01(alpha);
_color = new Color(Mathf.Clamp01(color.r), Mathf.Clamp01(color.g), Mathf.Clamp01(color.b), Mathf.Clamp01(color.a));
InitializeOverlay();
}

Expand Down Expand Up @@ -134,7 +122,7 @@ private void LateUpdate()
envelope = 1f;

if (_overlay != null)
_overlay.SetColor(_r, _g, _b, _maxAlpha * envelope);
_overlay.SetColor(new Color(_color.r, _color.g, _color.b, _color.a * envelope));
}

private void OnDisable()
Expand All @@ -149,7 +137,7 @@ private void InitializeOverlay()
return;

_overlay = camera.gameObject.AddComponent<ColorTintOverlay>();
_overlay.SetColor(_r, _g, _b, _maxAlpha);
_overlay.SetColor(_color);
_initialized = true;
}

Expand All @@ -170,20 +158,14 @@ private void RemoveOverlay()
private sealed class ColorTintOverlay : MonoBehaviour
{
private static Material? _material;
private float _r;
private float _g;
private float _b;
private float _alpha;
private Color _color;

/// <summary>
/// Updates the overlay color and opacity.
/// </summary>
public void SetColor(float r, float g, float b, float alpha)
public void SetColor(Color color)
{
_r = r;
_g = g;
_b = b;
_alpha = alpha;
_color = color;
}

private static Material? GetMaterial()
Expand All @@ -207,7 +189,7 @@ public void SetColor(float r, float g, float b, float alpha)
// Unity calls this on the camera's GameObject after it finishes rendering the scene.
private void OnPostRender()
{
if (_alpha <= 0f)
if (_color.a <= 0f)
return;

var mat = GetMaterial();
Expand All @@ -219,7 +201,7 @@ private void OnPostRender()
GL.PushMatrix();
GL.LoadOrtho();
GL.Begin(GL.QUADS);
GL.Color(new Color(_r, _g, _b, _alpha));
GL.Color(_color);
GL.Vertex3(0f, 0f, 0f);
GL.Vertex3(0f, 1f, 0f);
GL.Vertex3(1f, 1f, 0f);
Expand Down
49 changes: 15 additions & 34 deletions BopVisualEffects/Effects/Fog/FogEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
resizable = true,
properties = new Dictionary<string, object>
{
["r"] = 0.8f,
["g"] = 0.8f,
["b"] = 0.9f,
["alpha"] = 0.6f,
["color"] = new MixtapeEventTemplates.ColorField(new Color(0.8f, 0.8f, 0.9f, 0.6f)),
["height"] = 0.5f
}
Comment thread
Brollyy marked this conversation as resolved.
};
Expand All @@ -46,10 +43,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
{
var log = ClassLogger.GetForClass<FogEffect>();
var durationBeats = Mathf.Max(0.01f, entity.length);
var r = entity.GetFloat("r");
var g = entity.GetFloat("g");
var b = entity.GetFloat("b");
var alpha = entity.GetFloat("alpha");
var color = entity.GetColor("color");
Comment thread
Brollyy marked this conversation as resolved.
var height = entity.GetFloat("height");
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
Expand All @@ -61,17 +55,14 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner<FogRunner>(runner =>
runner.Initialize(loader, loader.jukebox, startBeat, endBeat, r, g, b, alpha, height));
runner.Initialize(loader, loader.jukebox, startBeat, endBeat, color, height));
}
}

private sealed class FogRunner : MonoBehaviour
{
private bool _initialized;
private float _r;
private float _g;
private float _b;
private float _maxAlpha;
private Color _color;
private float _height;
private float _startBeat;
private float _endBeat;
Expand All @@ -82,16 +73,13 @@ private sealed class FogRunner : MonoBehaviour
/// <summary>
/// Initializes this runner with effect parameters.
/// </summary>
public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float r, float g, float b, float alpha, float height)
public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, Color color, float height)
{
_loader = loader;
_jukebox = jukebox;
_startBeat = startBeat;
_endBeat = endBeat;
_r = Mathf.Clamp01(r);
_g = Mathf.Clamp01(g);
_b = Mathf.Clamp01(b);
_maxAlpha = Mathf.Clamp01(alpha);
_color = new Color(Mathf.Clamp01(color.r), Mathf.Clamp01(color.g), Mathf.Clamp01(color.b), Mathf.Clamp01(color.a));
_height = Mathf.Clamp01(height);
InitializeOverlay();
}
Expand Down Expand Up @@ -138,7 +126,7 @@ private void LateUpdate()
envelope = 1f;

if (_overlay != null)
_overlay.SetParams(_r, _g, _b, _maxAlpha * envelope, _height);
_overlay.SetParams(new Color(_color.r, _color.g, _color.b, _color.a * envelope), _height);
}

private void OnDisable()
Expand All @@ -153,7 +141,7 @@ private void InitializeOverlay()
return;

_overlay = camera.gameObject.AddComponent<FogOverlay>();
_overlay.SetParams(_r, _g, _b, _maxAlpha, _height);
_overlay.SetParams(_color, _height);
_initialized = true;
}

Expand All @@ -175,21 +163,15 @@ private void RemoveOverlay()
private sealed class FogOverlay : MonoBehaviour
{
private static Material? _material;
private float _r;
private float _g;
private float _b;
private float _alpha;
private Color _color;
private float _height;

/// <summary>
/// Updates the overlay parameters.
/// </summary>
public void SetParams(float r, float g, float b, float alpha, float height)
public void SetParams(Color color, float height)
{
_r = r;
_g = g;
_b = b;
_alpha = alpha;
_color = color;
_height = height;
}

Expand All @@ -214,7 +196,7 @@ public void SetParams(float r, float g, float b, float alpha, float height)
// Unity calls this on the camera's GameObject after it finishes rendering the scene.
private void OnPostRender()
{
if (_alpha <= 0f || _height <= 0f)
if (_color.a <= 0f || _height <= 0f)
return;

var mat = GetMaterial();
Expand All @@ -224,18 +206,17 @@ private void OnPostRender()
mat.SetPass(0);

// Ground fog: opaque at y=0 (bottom), fades to transparent at y=_height.
var fogColor = new Color(_r, _g, _b, _alpha);
var clear = new Color(_r, _g, _b, 0f);
var clear = new Color(_color.r, _color.g, _color.b, 0f);

GL.PushMatrix();
GL.LoadOrtho();
GL.Begin(GL.QUADS);

// Bottom-left → top-left → top-right → bottom-right
GL.Color(fogColor); GL.Vertex3(0f, 0f, 0f);
GL.Color(_color); GL.Vertex3(0f, 0f, 0f);
GL.Color(clear); GL.Vertex3(0f, _height, 0f);
GL.Color(clear); GL.Vertex3(1f, _height, 0f);
GL.Color(fogColor); GL.Vertex3(1f, 0f, 0f);
GL.Color(_color); GL.Vertex3(1f, 0f, 0f);

GL.End();
GL.PopMatrix();
Expand Down
Loading