Skip to content

Commit 238613b

Browse files
authored
Merge pull request #57 from EverestAPI/sideways_jumpthrus
Sideways jumpthrus
2 parents 776ec4b + 2df3169 commit 238613b

File tree

4 files changed

+386
-1
lines changed

4 files changed

+386
-1
lines changed

Ahorn/entities/sidewaysJumpThru.jl

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
module SpringCollab2020SidewaysJumpThru
2+
3+
using ..Ahorn, Maple
4+
5+
@mapdef Entity "SpringCollab2020/SidewaysJumpThru" SidewaysJumpThru(x::Integer, y::Integer, height::Integer=Maple.defaultBlockHeight,
6+
left::Bool=true, texture::String="wood")
7+
8+
textures = ["wood", "dream", "temple", "templeB", "cliffside", "reflection", "core", "moon"]
9+
const placements = Ahorn.PlacementDict()
10+
11+
for texture in textures
12+
placements["Sideways Jump Through ($(uppercasefirst(texture)), Left) (Spring Collab 2020)"] = Ahorn.EntityPlacement(
13+
SidewaysJumpThru,
14+
"rectangle",
15+
Dict{String, Any}(
16+
"texture" => texture,
17+
"left" => true
18+
)
19+
)
20+
placements["Sideways Jump Through ($(uppercasefirst(texture)), Right) (Spring Collab 2020)"] = Ahorn.EntityPlacement(
21+
SidewaysJumpThru,
22+
"rectangle",
23+
Dict{String, Any}(
24+
"texture" => texture,
25+
"left" => false
26+
)
27+
)
28+
end
29+
30+
quads = Tuple{Integer, Integer, Integer, Integer}[
31+
(0, 0, 8, 7) (8, 0, 8, 7) (16, 0, 8, 7);
32+
(0, 8, 8, 5) (8, 8, 8, 5) (16, 8, 8, 5)
33+
]
34+
35+
Ahorn.editingOptions(entity::SidewaysJumpThru) = Dict{String, Any}(
36+
"texture" => textures
37+
)
38+
39+
Ahorn.minimumSize(entity::SidewaysJumpThru) = 0, 8
40+
Ahorn.resizable(entity::SidewaysJumpThru) = false, true
41+
42+
function Ahorn.selection(entity::SidewaysJumpThru)
43+
x, y = Ahorn.position(entity)
44+
height = Int(get(entity.data, "height", 8))
45+
46+
return Ahorn.Rectangle(x, y, 8, height)
47+
end
48+
49+
function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::SidewaysJumpThru, room::Maple.Room)
50+
texture = get(entity.data, "texture", "wood")
51+
texture = texture == "default" ? "wood" : texture
52+
53+
# Values need to be system specific integer
54+
x = Int(get(entity.data, "x", 0))
55+
y = Int(get(entity.data, "y", 0))
56+
57+
height = Int(get(entity.data, "height", 8))
58+
left = get(entity.data, "left", true)
59+
60+
startX = div(x, 8) + 1
61+
startY = div(y, 8) + 1
62+
stopY = startY + div(height, 8) - 1
63+
64+
Ahorn.Cairo.save(ctx)
65+
66+
Ahorn.rotate(ctx, pi / 2)
67+
68+
if left
69+
Ahorn.scale(ctx, 1, -1)
70+
end
71+
72+
len = stopY - startY
73+
for i in 0:len
74+
connected = false
75+
qx = 2
76+
if i == 0
77+
connected = get(room.fgTiles.data, (startY - 1, startX), false) != '0'
78+
qx = 1
79+
80+
elseif i == len
81+
connected = get(room.fgTiles.data, (stopY + 1, startX), false) != '0'
82+
qx = 3
83+
end
84+
85+
quad = quads[2 - connected, qx]
86+
Ahorn.drawImage(ctx, "objects/jumpthru/$(texture)", 8 * i, left ? 0 : -8, quad...)
87+
end
88+
89+
Ahorn.Cairo.restore(ctx)
90+
end
91+
92+
end

Ahorn/lang/en_gb.lang

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,11 @@ placements.entities.SpringCollab2020/dashSpring.tooltips.playerCanUse=Determines
4040

4141
# Customizable Glass Block Controller
4242
placements.entities.SpringCollab2020/CustomizableGlassBlockController.tooltips.starColors=A comma-separated list of all star colours visible in glass blocks in the room.
43-
placements.entities.SpringCollab2020/CustomizableGlassBlockController.tooltips.bgColor=The background colour for glass blocks in the room.
43+
placements.entities.SpringCollab2020/CustomizableGlassBlockController.tooltips.bgColor=The background colour for glass blocks in the room.
44+
45+
# Upside Down Jump Thru
46+
placements.entities.SpringCollab2020/UpsideDownJumpThru.tooltips.texture=Changes the appearance of the platform.
47+
48+
# Sideways Jump Thru
49+
placements.entities.SpringCollab2020/SidewaysJumpThru.tooltips.texture=Changes the appearance of the platform.
50+
placements.entities.SpringCollab2020/SidewaysJumpThru.tooltips.left=Whether the solid side of the jumpthru is the left side.

Entities/SidewaysJumpThru.cs

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
using Celeste.Mod.Entities;
2+
using Microsoft.Xna.Framework;
3+
using Mono.Cecil;
4+
using Mono.Cecil.Cil;
5+
using Monocle;
6+
using MonoMod.Cil;
7+
using MonoMod.RuntimeDetour;
8+
using System;
9+
using System.Reflection;
10+
11+
namespace Celeste.Mod.SpringCollab2020.Entities {
12+
[CustomEntity("SpringCollab2020/SidewaysJumpThru")]
13+
[Tracked]
14+
class SidewaysJumpThru : Entity {
15+
16+
private static FieldInfo actorMovementCounter = typeof(Actor).GetField("movementCounter", BindingFlags.Instance | BindingFlags.NonPublic);
17+
18+
public static void Load() {
19+
// implement the basic collision between actors/platforms and sideways jumpthrus.
20+
using (new DetourContext { Before = { "*" } }) { // these don't always call the orig methods, better apply them first.
21+
On.Celeste.Actor.MoveHExact += onActorMoveHExact;
22+
On.Celeste.Platform.MoveHExactCollideSolids += onPlatformMoveHExactCollideSolids;
23+
}
24+
25+
// block "climb hopping" on top of sideways jumpthrus, because this just looks weird.
26+
On.Celeste.Player.ClimbHopBlockedCheck += onPlayerClimbHopBlockedCheck;
27+
28+
// mod collide checks to include sideways jumpthrus, so that the player behaves with them like with walls.
29+
IL.Celeste.Player.WallJumpCheck += modCollideChecks; // allow player to walljump off them
30+
IL.Celeste.Player.ClimbCheck += modCollideChecks; // allow player to climb on them
31+
IL.Celeste.Player.ClimbBegin += modCollideChecks; // if not applied, the player will clip through jumpthrus if trying to climb on them
32+
IL.Celeste.Player.ClimbUpdate += modCollideChecks; // when climbing, jumpthrus are handled like walls
33+
IL.Celeste.Player.SlipCheck += modCollideChecks; // make climbing on jumpthrus not slippery
34+
IL.Celeste.Player.UpdateSprite += modCollideChecks; // have the push animation when Madeline runs against a jumpthru for example
35+
IL.Celeste.Player.NormalUpdate += modCollideChecks; // get the wall slide effect
36+
IL.Celeste.Player.OnCollideH += modCollideChecks; // handle dashes against jumpthrus properly, without "shifting" down
37+
38+
// one extra hook that kills the player momentum when hitting a jumpthru so that they don't get "stuck" on them.
39+
On.Celeste.Player.NormalUpdate += onPlayerNormalUpdate;
40+
}
41+
42+
public static void Unload() {
43+
On.Celeste.Actor.MoveHExact -= onActorMoveHExact;
44+
On.Celeste.Platform.MoveHExactCollideSolids -= onPlatformMoveHExactCollideSolids;
45+
46+
On.Celeste.Player.ClimbHopBlockedCheck -= onPlayerClimbHopBlockedCheck;
47+
48+
IL.Celeste.Player.WallJumpCheck -= modCollideChecks;
49+
IL.Celeste.Player.ClimbCheck -= modCollideChecks;
50+
IL.Celeste.Player.ClimbBegin -= modCollideChecks;
51+
IL.Celeste.Player.ClimbUpdate -= modCollideChecks;
52+
IL.Celeste.Player.SlipCheck -= modCollideChecks;
53+
IL.Celeste.Player.UpdateSprite -= modCollideChecks;
54+
IL.Celeste.Player.NormalUpdate -= modCollideChecks;
55+
IL.Celeste.Player.OnCollideH -= modCollideChecks;
56+
57+
On.Celeste.Player.NormalUpdate -= onPlayerNormalUpdate;
58+
}
59+
60+
private static bool onActorMoveHExact(On.Celeste.Actor.orig_MoveHExact orig, Actor self, int moveH, Collision onCollide, Solid pusher) {
61+
// fall back to vanilla if no sideways jumpthru is in the room.
62+
if (self.SceneAs<Level>().Tracker.CountEntities<SidewaysJumpThru>() == 0)
63+
return orig(self, moveH, onCollide, pusher);
64+
65+
Vector2 targetPosition = self.Position + Vector2.UnitX * moveH;
66+
int moveDirection = Math.Sign(moveH);
67+
int moveAmount = 0;
68+
bool movingLeftToRight = moveH > 0;
69+
while (moveH != 0) {
70+
bool didCollide = false;
71+
72+
// check if colliding with a solid
73+
Solid solid = self.CollideFirst<Solid>(self.Position + Vector2.UnitX * moveDirection);
74+
if (solid != null) {
75+
didCollide = true;
76+
} else {
77+
// check if colliding with a sideways jumpthru
78+
SidewaysJumpThru jumpThru = self.CollideFirstOutside<SidewaysJumpThru>(self.Position + Vector2.UnitX * moveDirection);
79+
if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) {
80+
// there is a sideways jump-thru and we are moving in the opposite direction => collision
81+
didCollide = true;
82+
}
83+
}
84+
85+
if (didCollide) {
86+
Vector2 movementCounter = (Vector2) actorMovementCounter.GetValue(self);
87+
movementCounter.X = 0f;
88+
onCollide?.Invoke(new CollisionData {
89+
Direction = Vector2.UnitX * moveDirection,
90+
Moved = Vector2.UnitX * moveAmount,
91+
TargetPosition = targetPosition,
92+
Hit = solid,
93+
Pusher = pusher
94+
});
95+
return true;
96+
}
97+
98+
// continue moving
99+
moveAmount += moveDirection;
100+
moveH -= moveDirection;
101+
self.X += moveDirection;
102+
}
103+
return false;
104+
}
105+
106+
private static bool onPlatformMoveHExactCollideSolids(On.Celeste.Platform.orig_MoveHExactCollideSolids orig, Platform self,
107+
int moveH, bool thruDashBlocks, Action<Vector2, Vector2, Platform> onCollide) {
108+
// fall back to vanilla if no sideways jumpthru is in the room.
109+
if (self.SceneAs<Level>().Tracker.CountEntities<SidewaysJumpThru>() == 0)
110+
return orig(self, moveH, thruDashBlocks, onCollide);
111+
112+
float x = self.X;
113+
int moveDirection = Math.Sign(moveH);
114+
int moveAmount = 0;
115+
Solid solid = null;
116+
bool movingLeftToRight = moveH > 0;
117+
bool collidedWithJumpthru = false;
118+
while (moveH != 0) {
119+
if (thruDashBlocks) {
120+
// check if we have dash blocks to break on our way.
121+
foreach (DashBlock entity in self.Scene.Tracker.GetEntities<DashBlock>()) {
122+
if (self.CollideCheck(entity, self.Position + Vector2.UnitX * moveDirection)) {
123+
entity.Break(self.Center, Vector2.UnitX * moveDirection, true, true);
124+
self.SceneAs<Level>().Shake(0.2f);
125+
Input.Rumble(RumbleStrength.Medium, RumbleLength.Medium);
126+
}
127+
}
128+
}
129+
130+
// check for collision with a solid
131+
solid = self.CollideFirst<Solid>(self.Position + Vector2.UnitX * moveDirection);
132+
133+
// check for collision with a sideways jumpthru
134+
SidewaysJumpThru jumpThru = self.CollideFirstOutside<SidewaysJumpThru>(self.Position + Vector2.UnitX * moveDirection);
135+
if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) {
136+
// there is a sideways jump-thru and we are moving in the opposite direction => collision
137+
collidedWithJumpthru = true;
138+
}
139+
140+
if (solid != null || collidedWithJumpthru) {
141+
break;
142+
}
143+
144+
// continue moving
145+
moveAmount += moveDirection;
146+
moveH -= moveDirection;
147+
self.X += moveDirection;
148+
}
149+
150+
// actually move and call the collision callback if any
151+
self.X = x;
152+
self.MoveHExact(moveAmount);
153+
if (solid != null && onCollide != null) {
154+
onCollide(Vector2.UnitX * moveDirection, Vector2.UnitX * moveAmount, solid);
155+
}
156+
return solid != null || collidedWithJumpthru;
157+
}
158+
159+
private static bool onPlayerClimbHopBlockedCheck(On.Celeste.Player.orig_ClimbHopBlockedCheck orig, Player self) {
160+
bool vanillaCheck = orig(self);
161+
if (vanillaCheck)
162+
return vanillaCheck;
163+
164+
// block climb hops on jumpthrus because those look weird
165+
return self.CollideCheckOutside<SidewaysJumpThru>(self.Position + Vector2.UnitX * (int) self.Facing);
166+
}
167+
168+
private static void modCollideChecks(ILContext il) {
169+
ILCursor cursor = new ILCursor(il);
170+
171+
while (cursor.Next != null) {
172+
Instruction next = cursor.Next;
173+
174+
// we want to replace all CollideChecks with solids here.
175+
if (next.OpCode == OpCodes.Call && (next.Operand as MethodReference)?.FullName == "System.Boolean Monocle.Entity::CollideCheck<Celeste.Solid>(Microsoft.Xna.Framework.Vector2)") {
176+
Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Patching Entity.CollideCheck to include sideways jumpthrus at {cursor.Index} in IL for {il.Method.Name}");
177+
178+
cursor.Remove();
179+
cursor.EmitDelegate<Func<Entity, Vector2, bool>>((self, checkAtPosition) => {
180+
// we still want to check for solids...
181+
if (self.CollideCheck<Solid>(checkAtPosition))
182+
return true;
183+
184+
// if we are not checking a side, this certainly has nothing to do with jumpthrus.
185+
if (self.Position.X == checkAtPosition.X)
186+
return false;
187+
188+
// our entity also collides if this is with a jumpthru and we are colliding with the solid side of it.
189+
// we are in this case if the jumpthru is left to right (the "solid" side of it is the right one)
190+
// and we are checking the collision on the left side of the player for example.
191+
bool collideOnLeftSideOfPlayer = (self.Position.X > checkAtPosition.X);
192+
SidewaysJumpThru jumpthru = self.CollideFirstOutside<SidewaysJumpThru>(checkAtPosition);
193+
return jumpthru != null && self is Player player && (jumpthru.AllowLeftToRight == collideOnLeftSideOfPlayer);
194+
});
195+
}
196+
197+
if (next.OpCode == OpCodes.Callvirt && (next.Operand as MethodReference)?.FullName == "System.Boolean Monocle.Scene::CollideCheck<Celeste.Solid>(Microsoft.Xna.Framework.Vector2)") {
198+
Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Patching Scene.CollideCheck to include sideways jumpthrus at {cursor.Index} in IL for {il.Method.Name}");
199+
200+
cursor.Remove();
201+
cursor.EmitDelegate<Func<Scene, Vector2, bool>>((self, vector) => self.CollideCheck<Solid>(vector) || self.CollideCheck<SidewaysJumpThru>(vector));
202+
}
203+
204+
cursor.Index++;
205+
}
206+
}
207+
208+
private static int onPlayerNormalUpdate(On.Celeste.Player.orig_NormalUpdate orig, Player self) {
209+
int result = orig(self);
210+
211+
// kill speed if player is going towards a jumpthru.
212+
if (self.Speed.X != 0) {
213+
bool movingLeftToRight = self.Speed.X > 0;
214+
SidewaysJumpThru jumpThru = self.CollideFirstOutside<SidewaysJumpThru>(self.Position + Vector2.UnitX * Math.Sign(self.Speed.X));
215+
if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) {
216+
self.Speed.X = 0;
217+
}
218+
}
219+
220+
return result;
221+
}
222+
223+
// ======== Begin of entity code ========
224+
225+
private int lines;
226+
private string overrideTexture;
227+
228+
public bool AllowLeftToRight;
229+
230+
public SidewaysJumpThru(Vector2 position, int height, bool allowLeftToRight, string overrideTexture)
231+
: base(position) {
232+
233+
lines = height / 8;
234+
AllowLeftToRight = allowLeftToRight;
235+
Depth = -60;
236+
this.overrideTexture = overrideTexture;
237+
238+
float hitboxOffset = 0f;
239+
if (AllowLeftToRight)
240+
hitboxOffset = 3f;
241+
242+
Collider = new Hitbox(5f, height, hitboxOffset, 0);
243+
}
244+
245+
public SidewaysJumpThru(EntityData data, Vector2 offset)
246+
: this(data.Position + offset, data.Height, !data.Bool("left"), data.Attr("texture", "default")) {
247+
}
248+
249+
public override void Awake(Scene scene) {
250+
AreaData areaData = AreaData.Get(scene);
251+
string jumpthru = areaData.Jumpthru;
252+
if (!string.IsNullOrEmpty(overrideTexture) && !overrideTexture.Equals("default")) {
253+
jumpthru = overrideTexture;
254+
}
255+
256+
MTexture mTexture = GFX.Game["objects/jumpthru/" + jumpthru];
257+
int num = mTexture.Width / 8;
258+
for (int i = 0; i < lines; i++) {
259+
int xTilePosition;
260+
int yTilePosition;
261+
if (i == 0) {
262+
xTilePosition = 0;
263+
yTilePosition = ((!CollideCheck<Solid>(Position + new Vector2(0f, -1f))) ? 1 : 0);
264+
} else if (i == lines - 1) {
265+
xTilePosition = num - 1;
266+
yTilePosition = ((!CollideCheck<Solid>(Position + new Vector2(0f, 1f))) ? 1 : 0);
267+
} else {
268+
xTilePosition = 1 + Calc.Random.Next(num - 2);
269+
yTilePosition = Calc.Random.Choose(0, 1);
270+
}
271+
Image image = new Image(mTexture.GetSubtexture(xTilePosition * 8, yTilePosition * 8, 8, 8));
272+
image.Y = i * 8;
273+
image.Rotation = (float) (Math.PI / 2);
274+
275+
if (AllowLeftToRight)
276+
image.X = 8;
277+
else
278+
image.Scale.Y = -1;
279+
280+
Add(image);
281+
}
282+
}
283+
}
284+
}

0 commit comments

Comments
 (0)