Skip to content

Commit b6b1d8e

Browse files
authored
Crystal Bomb (Cavern Helper) Detonation Field (#84)
Crystal Bomb (Cavern Helper) Detonation Field
2 parents db2eabd + 645a748 commit b6b1d8e

File tree

4 files changed

+364
-0
lines changed

4 files changed

+364
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module SpringCollab2020CrystalBombDetonator
2+
3+
using ..Ahorn, Maple
4+
5+
@mapdef Entity "SpringCollab2020/crystalBombDetonator" CrystalBombDetonator(x::Integer, y::Integer, width::Integer=Maple.defaultBlockWidth, height::Integer=Maple.defaultBlockHeight)
6+
7+
const placements = Ahorn.PlacementDict(
8+
"Crystal Bomb Detonation Field (Spring Collab 2020)" => Ahorn.EntityPlacement(
9+
CrystalBombDetonator,
10+
"rectangle"
11+
),
12+
)
13+
14+
Ahorn.minimumSize(entity::CrystalBombDetonator) = 8, 8
15+
Ahorn.resizable(entity::CrystalBombDetonator) = true, true
16+
17+
function Ahorn.selection(entity::CrystalBombDetonator)
18+
x, y = Ahorn.position(entity)
19+
20+
width = Int(get(entity.data, "width", 8))
21+
height = Int(get(entity.data, "height", 8))
22+
23+
return Ahorn.Rectangle(x, y, width, height)
24+
end
25+
26+
function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::CrystalBombDetonator, room::Maple.Room)
27+
width = Int(get(entity.data, "width", 32))
28+
height = Int(get(entity.data, "height", 32))
29+
30+
Ahorn.drawRectangle(ctx, 0, 0, width, height, (0.45, 0.0, 0.45, 0.8), (0.0, 0.0, 0.0, 0.0))
31+
end
32+
33+
end

Entities/CrystalBombDetonator.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using Celeste.Mod.Entities;
5+
using Microsoft.Xna.Framework;
6+
using Monocle;
7+
8+
namespace Celeste.Mod.SpringCollab2020.Entities {
9+
[Tracked(false)]
10+
[CustomEntity("SpringCollab2020/crystalBombDetonator")]
11+
public class CrystalBombDetonator : Solid {
12+
// Duration of flashing anim
13+
public float Flash = 0f;
14+
15+
// Used in the shape of the "wave" on the renderer
16+
public float Solidify = 0f;
17+
private float solidifyDelay = 0f;
18+
19+
// Used to propagate state
20+
private List<CrystalBombDetonator> adjacent = new List<CrystalBombDetonator>();
21+
22+
// If the field is currently in its flashing state (used to propagate Flashing state to adjacent fields)
23+
public bool Flashing = false;
24+
25+
// Particle array and a list of speeds
26+
private List<Vector2> particles = new List<Vector2>();
27+
private readonly static float[] speeds = new float[] {
28+
12f,
29+
20f,
30+
40f
31+
};
32+
33+
// Cached method reference, first calculated in Update
34+
private static MethodInfo bombExplosionMethod;
35+
36+
public CrystalBombDetonator(Vector2 position, float width, float height) : base(position, width, height, false) {
37+
Collidable = false;
38+
for (int num = 0; (float) num < Width * Height / 16f; num++)
39+
particles.Add(new Vector2(Calc.Random.NextFloat(Width - 1f), Calc.Random.NextFloat(Height - 1f)));
40+
}
41+
42+
public CrystalBombDetonator(EntityData data, Vector2 offset) : this(data.Position + offset, (float) data.Width, (float) data.Height) { }
43+
44+
public override void Added(Scene scene) {
45+
base.Added(scene);
46+
scene.Tracker.GetEntity<CrystalBombDetonatorRenderer>().Track(this);
47+
}
48+
49+
public override void Removed(Scene scene) {
50+
base.Removed(scene);
51+
scene.Tracker.GetEntity<CrystalBombDetonatorRenderer>().Untrack(this);
52+
}
53+
54+
public override void Update() {
55+
if (Flashing) {
56+
Flash = Calc.Approach(Flash, 0f, Engine.DeltaTime * 4f);
57+
if (Flash <= 0f) {
58+
Flashing = false;
59+
}
60+
} else {
61+
if (solidifyDelay > 0f)
62+
solidifyDelay -= Engine.DeltaTime;
63+
else if (Solidify > 0f)
64+
Solidify = Calc.Approach(Solidify, 0f, Engine.DeltaTime);
65+
}
66+
67+
for (int i = 0; i < particles.Count; i++) {
68+
Vector2 newPosition = particles[i] + Vector2.UnitY * speeds[i % speeds.Length] * Engine.DeltaTime;
69+
newPosition.Y %= Height - 1f;
70+
particles[i] = newPosition;
71+
}
72+
base.Update();
73+
74+
CheckForBombs();
75+
}
76+
77+
public void OnTriggerDetonation() {
78+
Flash = 1f;
79+
Solidify = 1f;
80+
solidifyDelay = 1f;
81+
Flashing = true;
82+
Scene.CollideInto<CrystalBombDetonator>(new Rectangle((int) X, (int) Y - 2, (int) Width, (int) Height + 4), adjacent);
83+
Scene.CollideInto<CrystalBombDetonator>(new Rectangle((int) X - 2, (int) Y, (int) Width + 4, (int) Height), adjacent);
84+
foreach (CrystalBombDetonator crystalBombDetonator in adjacent) {
85+
if(!crystalBombDetonator.Flashing)
86+
crystalBombDetonator.OnTriggerDetonation();
87+
}
88+
adjacent.Clear();
89+
}
90+
91+
public override void Render() {
92+
Color color = Color.Yellow * 0.6f;
93+
foreach (Vector2 value in particles) {
94+
Draw.Pixel.Draw(Position + value, Vector2.Zero, color);
95+
}
96+
if (Flashing)
97+
Draw.Rect(Collider, Color.Purple * Flash * 0.5f);
98+
}
99+
100+
private void CheckForBombs() {
101+
foreach (Entity bomb in CollideAll<Actor>()) {
102+
if (bomb.GetType().ToString().Contains("CrystalBomb")) {
103+
if (bombExplosionMethod == null)
104+
bombExplosionMethod = bomb.GetType().GetMethod("Explode", BindingFlags.Instance | BindingFlags.NonPublic);
105+
bombExplosionMethod.Invoke(bomb, null);
106+
if (!Flashing)
107+
OnTriggerDetonation();
108+
}
109+
}
110+
}
111+
}
112+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.Xna.Framework;
4+
using Monocle;
5+
6+
namespace Celeste.Mod.SpringCollab2020.Entities {
7+
[Tracked(false)]
8+
public class CrystalBombDetonatorRenderer : Entity {
9+
10+
private List<CrystalBombDetonator> trackedFields = new List<CrystalBombDetonator>();
11+
private List<CrystalBombDetonatorRenderer.Edge> fieldEdges = new List<CrystalBombDetonatorRenderer.Edge>();
12+
private VirtualMap<bool> tiles;
13+
private Rectangle levelTileBounds;
14+
private bool dirty;
15+
16+
public CrystalBombDetonatorRenderer() {
17+
Tag = (Tags.Global | Tags.TransitionUpdate);
18+
Depth = 0;
19+
Add(new CustomBloom(OnRenderBloom));
20+
}
21+
22+
// Hooks to add ourself to the level at the very start
23+
public static void Load() {
24+
On.Celeste.LevelLoader.LoadingThread += LevelLoader_LoadingThread;
25+
}
26+
27+
public static void Unload() {
28+
On.Celeste.LevelLoader.LoadingThread -= LevelLoader_LoadingThread;
29+
}
30+
31+
static void LevelLoader_LoadingThread(On.Celeste.LevelLoader.orig_LoadingThread orig, LevelLoader self) {
32+
self.Level.Add(new CrystalBombDetonatorRenderer());
33+
orig(self);
34+
}
35+
36+
// Add a field to the tracker
37+
public void Track(CrystalBombDetonator block) {
38+
trackedFields.Add(block);
39+
if (tiles == null) {
40+
levelTileBounds = (Scene as Level).TileBounds;
41+
tiles = new VirtualMap<bool>(levelTileBounds.Width, levelTileBounds.Height, false);
42+
}
43+
44+
for (int xTile = (int)block.X / 8; xTile < (block.Right / 8f); xTile++)
45+
for (int yTile = (int)block.Y / 8; yTile < (block.Bottom / 8f); yTile++)
46+
tiles[xTile - levelTileBounds.X, yTile - levelTileBounds.Y] = true;
47+
48+
dirty = true;
49+
}
50+
51+
public void Untrack(CrystalBombDetonator block) {
52+
trackedFields.Remove(block);
53+
if (trackedFields.Count <= 0)
54+
tiles = null;
55+
else
56+
for (int xTile = (int)block.X / 8; xTile < (block.Right / 8f); xTile++)
57+
for (int yTile = (int)block.Y / 8; yTile < (block.Bottom / 8f); yTile++)
58+
tiles[xTile - levelTileBounds.X, yTile - levelTileBounds.Y] = false;
59+
60+
dirty = true;
61+
}
62+
63+
public override void Update() {
64+
if (dirty)
65+
RebuildEdges();
66+
UpdateEdges();
67+
}
68+
69+
public void UpdateEdges() {
70+
Camera camera = (Scene as Level).Camera;
71+
Rectangle cameraRect = new Rectangle((int) camera.Left - 4, (int) camera.Top - 4, (int) (camera.Right - camera.Left) + 8, (int) (camera.Bottom - camera.Top) + 8);
72+
for (int i = 0; i < fieldEdges.Count; i++) {
73+
if (fieldEdges[i].Visible && Scene.OnInterval(0.25f, i * 0.01f) && !fieldEdges[i].InView(ref cameraRect))
74+
fieldEdges[i].Visible = false;
75+
else
76+
if(Scene.OnInterval(0.05f, i * 0.01f) && fieldEdges[i].InView(ref cameraRect))
77+
fieldEdges[i].Visible = true;
78+
79+
if(fieldEdges[i].Visible && (Scene.OnInterval(0.05f, i * 0.01f) || fieldEdges[i].Wave == null))
80+
fieldEdges[i].UpdateWave(Scene.TimeActive * 3f);
81+
}
82+
}
83+
84+
private void RebuildEdges() {
85+
dirty = false;
86+
fieldEdges.Clear();
87+
if (trackedFields.Count > 0) {
88+
Level level = Scene as Level;
89+
int left = level.TileBounds.Left;
90+
int top = level.TileBounds.Top;
91+
int right = level.TileBounds.Right;
92+
int bottom = level.TileBounds.Bottom;
93+
Point[] array = new Point[]
94+
{
95+
new Point(0, -1),
96+
new Point(0, 1),
97+
new Point(-1, 0),
98+
new Point(1, 0)
99+
};
100+
101+
// personal note: I'm having trouble sorting out this code right now, what the hell is all of this and why are we using Points suddenly
102+
foreach (CrystalBombDetonator crystalBombDetonator in trackedFields) {
103+
for (int xTile = (int) crystalBombDetonator.X / 8; xTile < (crystalBombDetonator.Right / 8f); xTile++)
104+
for (int yTile = (int) crystalBombDetonator.Y / 8; yTile < (crystalBombDetonator.Bottom / 8f); yTile++)
105+
foreach (Point point in array) {
106+
Point point2 = new Point(-point.Y, point.X);
107+
if (!Inside(xTile + point.X, yTile + point.Y) && (!Inside(xTile - point2.X, yTile - point2.Y) || Inside(xTile + point.X - point2.X, yTile + point.Y - point2.Y))) {
108+
Point point3 = new Point(xTile, yTile);
109+
Point point4 = new Point(xTile + point2.X, yTile + point2.Y);
110+
Vector2 value = new Vector2(4f) + new Vector2((float) (point.X - point2.X), (float) (point.Y - point2.Y)) * 4f;
111+
while (Inside(point4.X, point4.Y) && !Inside(point4.X + point.X, point4.Y + point.Y)) {
112+
point4.X += point2.X;
113+
point4.Y += point2.Y;
114+
}
115+
Vector2 a = new Vector2((float) point3.X, (float) point3.Y) * 8f + value - crystalBombDetonator.Position;
116+
Vector2 b = new Vector2((float) point4.X, (float) point4.Y) * 8f + value - crystalBombDetonator.Position;
117+
fieldEdges.Add(new CrystalBombDetonatorRenderer.Edge(crystalBombDetonator, a, b));
118+
}
119+
}
120+
}
121+
}
122+
}
123+
124+
private bool Inside(int tx, int ty) {
125+
return tiles[tx - levelTileBounds.X, ty - levelTileBounds.Y];
126+
}
127+
128+
private void OnRenderBloom() {
129+
foreach (CrystalBombDetonator crystalBombDetonator in trackedFields) {
130+
if (crystalBombDetonator.Visible)
131+
Draw.Rect(crystalBombDetonator.X, crystalBombDetonator.Y, crystalBombDetonator.Width, crystalBombDetonator.Height, Color.Purple);
132+
}
133+
foreach (CrystalBombDetonatorRenderer.Edge edge in fieldEdges) {
134+
if (edge.Visible) {
135+
Vector2 value = edge.Parent.Position + edge.A;
136+
Vector2 vector = edge.Parent.Position + edge.B;
137+
for (int num = 0; num <= edge.Length; num++) {
138+
Vector2 vector2 = value + edge.Normal * num;
139+
Draw.Line(vector2, vector2 + edge.Perpendicular * edge.Wave[num], Color.Purple);
140+
}
141+
}
142+
}
143+
}
144+
145+
public override void Render() {
146+
if (trackedFields.Count <= 0)
147+
return;
148+
149+
Color color = Color.Purple * 0.45f;
150+
Color value = Color.Purple * 0.55f;
151+
foreach (CrystalBombDetonator crystalBombDetonator in trackedFields) {
152+
if (crystalBombDetonator.Visible)
153+
Draw.Rect(crystalBombDetonator.Collider, color);
154+
}
155+
if (fieldEdges.Count > 0)
156+
foreach (CrystalBombDetonatorRenderer.Edge edge in fieldEdges) {
157+
if (edge.Visible) {
158+
Vector2 value2 = edge.Parent.Position + edge.A;
159+
Vector2 vector = edge.Parent.Position + edge.B;
160+
Color color2 = Color.Lerp(value, Color.Purple, edge.Parent.Flash);
161+
for (int num = 0; num <= edge.Length; num++) {
162+
Vector2 vector2 = value2 + edge.Normal * num;
163+
Draw.Line(vector2, vector2 + edge.Perpendicular * edge.Wave[num], color);
164+
}
165+
}
166+
}
167+
}
168+
169+
private class Edge {
170+
public CrystalBombDetonator Parent;
171+
public bool Visible = true;
172+
public Vector2 A;
173+
public Vector2 B;
174+
public Vector2 Min;
175+
public Vector2 Max;
176+
public Vector2 Normal;
177+
public Vector2 Perpendicular;
178+
public float[] Wave;
179+
public float Length;
180+
181+
public Edge(CrystalBombDetonator parent, Vector2 a, Vector2 b) {
182+
Parent = parent;
183+
A = a;
184+
B = b;
185+
Min = new Vector2(Math.Min(A.X, B.X), Math.Min(A.Y, B.Y));
186+
Max = new Vector2(Math.Max(A.X, B.X), Math.Max(A.Y, B.Y));
187+
Normal = (B - A).SafeNormalize();
188+
Perpendicular = -Normal.Perpendicular();
189+
Length = (A - B).Length();
190+
}
191+
192+
public void UpdateWave(float time) {
193+
if (Wave == null || (float) Wave.Length <= Length)
194+
Wave = new float[(int) Length + 2];
195+
196+
for (int num = 0; num <= Length; num++)
197+
Wave[num] = GetWaveAt(time, num, Length);
198+
}
199+
200+
private float GetWaveAt(float offset, float along, float length) {
201+
if (along <= 1f || along >= length - 1f)
202+
return 0f;
203+
204+
if (Parent.Solidify >= 1f)
205+
return 0f;
206+
207+
float num = offset + along * 0.25f;
208+
float num2 = (float) (Math.Sin((double) num) * 2.0 + Math.Sin((double) (num * 0.25f)));
209+
return (1f + num2 * Ease.SineInOut(Calc.YoYo(along / length))) * (1f - Parent.Solidify);
210+
}
211+
212+
public bool InView(ref Rectangle view) {
213+
return (float) view.Left < Parent.X + Max.X && (float) view.Right > Parent.X + Min.X && (float) view.Top < Parent.Y + Max.Y && (float) view.Bottom > Parent.Y + Min.Y;
214+
}
215+
}
216+
}
217+
}

SpringCollab2020Module.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public override void Load() {
2020
UpsideDownJumpThru.Load();
2121
BubbleReturnBerry.Load();
2222
SidewaysJumpThru.Load();
23+
CrystalBombDetonatorRenderer.Load();
2324
}
2425

2526
public override void LoadContent(bool firstLoad) {
@@ -38,6 +39,7 @@ public override void Unload() {
3839
UpsideDownJumpThru.Unload();
3940
BubbleReturnBerry.Unload();
4041
SidewaysJumpThru.Unload();
42+
CrystalBombDetonatorRenderer.Unload();
4143
}
4244
}
4345
}

0 commit comments

Comments
 (0)