|
| 1 | +using Celeste.Mod.Entities; |
| 2 | +using Microsoft.Xna.Framework; |
| 3 | +using Monocle; |
| 4 | +using System.Collections.Generic; |
| 5 | +using System.Linq; |
| 6 | +using System.Reflection; |
| 7 | + |
| 8 | +namespace Celeste.Mod.SpringCollab2020.Entities { |
| 9 | + /// <summary> |
| 10 | + /// Almost a copypaste of the vanilla SandwichLava class, but: |
| 11 | + /// - plays nicely with vertical rooms |
| 12 | + /// - can be made one-way |
| 13 | + /// - has customizable speed and gap. |
| 14 | + /// </summary> |
| 15 | + [CustomEntity("SpringCollab2020/CustomSandwichLava")] |
| 16 | + [Tracked] |
| 17 | + class CustomSandwichLava : Entity { |
| 18 | + private static FieldInfo lavaBlockerTriggerEnabled = typeof(LavaBlockerTrigger).GetField("enabled", BindingFlags.NonPublic | BindingFlags.Instance); |
| 19 | + |
| 20 | + // if the player collides with one of those, lava should be forced into waiting. |
| 21 | + private List<LavaBlockerTrigger> lavaBlockerTriggers; |
| 22 | + |
| 23 | + public enum DirectionMode { |
| 24 | + AlwaysUp, AlwaysDown, CoreModeBased |
| 25 | + }; |
| 26 | + |
| 27 | + private const float TopOffset = -160f; |
| 28 | + |
| 29 | + // parameters |
| 30 | + private float startX; |
| 31 | + public DirectionMode Direction; |
| 32 | + public float Speed; |
| 33 | + // the extra pixels each side has to be shifted towards each other compared to vanilla |
| 34 | + // to comply with the "sandwichGap" setting. |
| 35 | + private float sandwichDisplacement; |
| 36 | + |
| 37 | + // state |
| 38 | + private bool iceMode; |
| 39 | + private float lerp; |
| 40 | + public bool Waiting; |
| 41 | + private bool leaving = false; |
| 42 | + private float delay = 0f; |
| 43 | + private bool persistent; |
| 44 | + private bool entering = true; |
| 45 | + |
| 46 | + // during a transition, those hold the Y positions of lava parts from the beginning of the transition. |
| 47 | + private float transitionStartY; |
| 48 | + private float transitionStartTopRectY; |
| 49 | + private float transitionStartBottomRectY; |
| 50 | + |
| 51 | + private LavaRect bottomRect; |
| 52 | + private LavaRect topRect; |
| 53 | + |
| 54 | + private SoundSource loopSfx; |
| 55 | + |
| 56 | + // in vanilla, this is SceneAs<Level>().Bounds.Bottom. This is bad with vertical rooms. |
| 57 | + private float centerY => SceneAs<Level>().Camera.Bottom - 10f; |
| 58 | + |
| 59 | + public CustomSandwichLava(EntityData data, Vector2 offset) { |
| 60 | + startX = data.Position.X + offset.X; |
| 61 | + Direction = data.Enum("direction", DirectionMode.CoreModeBased); |
| 62 | + Speed = data.Float("speed", 20f); |
| 63 | + |
| 64 | + // vanilla is 160. so, setting sandwichGap to 120 requires each side to be shifted by 20 pixels towards the other ((160 - 120) / 2). |
| 65 | + sandwichDisplacement = (160f - data.Float("sandwichGap", 160f)) / 2; |
| 66 | + |
| 67 | + Depth = -1000000; |
| 68 | + Collider = new ColliderList(new Hitbox(340f, 120f, 0f, -sandwichDisplacement), new Hitbox(340f, 120f, 0f, -280f + sandwichDisplacement)); |
| 69 | + Visible = false; |
| 70 | + |
| 71 | + Add(loopSfx = new SoundSource()); |
| 72 | + Add(new PlayerCollider(OnPlayer)); |
| 73 | + Add(new CoreModeListener(OnChangeMode)); |
| 74 | + |
| 75 | + Add(bottomRect = new LavaRect(400f, 200f, 4)); |
| 76 | + bottomRect.Position = new Vector2(-40f, 0f); |
| 77 | + bottomRect.OnlyMode = LavaRect.OnlyModes.OnlyTop; |
| 78 | + bottomRect.SmallWaveAmplitude = 2f; |
| 79 | + |
| 80 | + Add(topRect = new LavaRect(400f, 200f, 4)); |
| 81 | + topRect.Position = new Vector2(-40f, -360f); |
| 82 | + topRect.OnlyMode = LavaRect.OnlyModes.OnlyBottom; |
| 83 | + topRect.SmallWaveAmplitude = 2f; |
| 84 | + topRect.BigWaveAmplitude = (bottomRect.BigWaveAmplitude = 2f); |
| 85 | + topRect.CurveAmplitude = (bottomRect.CurveAmplitude = 4f); |
| 86 | + |
| 87 | + Add(new TransitionListener { |
| 88 | + OnOutBegin = () => { |
| 89 | + // save the Y positions |
| 90 | + transitionStartY = Y; |
| 91 | + transitionStartTopRectY = topRect.Position.Y; |
| 92 | + transitionStartBottomRectY = bottomRect.Position.Y; |
| 93 | + |
| 94 | + if (persistent && Scene != null && Scene.Entities.FindAll<CustomSandwichLava>().Count <= 1) { |
| 95 | + // no lava in the next room: leave |
| 96 | + Leave(); |
| 97 | + } else { |
| 98 | + // look up for all lava blocker triggers in the next room. |
| 99 | + lavaBlockerTriggers = Scene.Entities.OfType<LavaBlockerTrigger>().ToList(); |
| 100 | + } |
| 101 | + }, |
| 102 | + OnOut = progress => { |
| 103 | + if (Scene != null) { |
| 104 | + X = (Scene as Level).Camera.X; |
| 105 | + if (!leaving) { |
| 106 | + // make the lava elements transition smoothly to their expected positions. |
| 107 | + Y = MathHelper.Lerp(transitionStartY, centerY, progress); |
| 108 | + topRect.Position.Y = MathHelper.Lerp(transitionStartTopRectY, TopOffset - topRect.Height + sandwichDisplacement, progress); |
| 109 | + bottomRect.Position.Y = MathHelper.Lerp(transitionStartBottomRectY, -sandwichDisplacement, progress); |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + if ((progress > 0.95f) && leaving) { |
| 114 | + // lava is leaving, transition is over soon => remove it |
| 115 | + RemoveSelf(); |
| 116 | + } |
| 117 | + }, |
| 118 | + OnInEnd = () => { |
| 119 | + if (entering) { |
| 120 | + // transition is over. grab the camera position now since it's done moving. |
| 121 | + Y = centerY; |
| 122 | + entering = false; |
| 123 | + } |
| 124 | + } |
| 125 | + }); |
| 126 | + } |
| 127 | + |
| 128 | + public override void Added(Scene scene) { |
| 129 | + base.Added(scene); |
| 130 | + X = SceneAs<Level>().Bounds.Left - 10; |
| 131 | + Y = centerY; |
| 132 | + iceMode = (SceneAs<Level>().Session.CoreMode == Session.CoreModes.Cold); |
| 133 | + } |
| 134 | + |
| 135 | + public override void Awake(Scene scene) { |
| 136 | + base.Awake(scene); |
| 137 | + |
| 138 | + Player entity = Scene.Tracker.GetEntity<Player>(); |
| 139 | + if (entity != null && (entity.JustRespawned || entity.X < startX)) { |
| 140 | + Waiting = true; |
| 141 | + } |
| 142 | + |
| 143 | + List<CustomSandwichLava> existingLavas = Scene.Entities.FindAll<CustomSandwichLava>(); |
| 144 | + |
| 145 | + bool removedSelf = false; |
| 146 | + if (!persistent && existingLavas.Count >= 2) { |
| 147 | + // another lava entity already exists. |
| 148 | + CustomSandwichLava sandwichLava = (existingLavas[0] == this) ? existingLavas[1] : existingLavas[0]; |
| 149 | + if (!sandwichLava.leaving) { |
| 150 | + // transfer new settings to the existing lava. |
| 151 | + sandwichLava.startX = startX; |
| 152 | + sandwichLava.Waiting = true; |
| 153 | + sandwichLava.Direction = Direction; |
| 154 | + sandwichLava.Speed = Speed; |
| 155 | + sandwichLava.sandwichDisplacement = sandwichDisplacement; |
| 156 | + sandwichLava.Collider = Collider; |
| 157 | + entering = false; |
| 158 | + RemoveSelf(); |
| 159 | + removedSelf = true; |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + if (!removedSelf) { |
| 164 | + // turn the lava persistent, so that it animates during room transitions |
| 165 | + // and deals smoothly with successive rooms with sandwich lava. |
| 166 | + persistent = true; |
| 167 | + Tag = Tags.Persistent; |
| 168 | + |
| 169 | + if ((scene as Level).LastIntroType != Player.IntroTypes.Respawn) { |
| 170 | + // throw the two lava parts off-screen. |
| 171 | + topRect.Position.Y -= 60f + sandwichDisplacement; |
| 172 | + bottomRect.Position.Y += 60f + sandwichDisplacement; |
| 173 | + } else { |
| 174 | + Visible = true; |
| 175 | + } |
| 176 | + |
| 177 | + loopSfx.Play("event:/game/09_core/rising_threat", "room_state", iceMode ? 1 : 0); |
| 178 | + loopSfx.Position = new Vector2(Width / 2f, 0f); |
| 179 | + |
| 180 | + // look up for all lava blocker triggers in the room. |
| 181 | + lavaBlockerTriggers = scene.Entities.OfType<LavaBlockerTrigger>().ToList(); |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + private void OnChangeMode(Session.CoreModes mode) { |
| 186 | + iceMode = (mode == Session.CoreModes.Cold); |
| 187 | + loopSfx.Param("room_state", iceMode ? 1 : 0); |
| 188 | + } |
| 189 | + |
| 190 | + private void OnPlayer(Player player) { |
| 191 | + if (Waiting) { |
| 192 | + return; |
| 193 | + } |
| 194 | + if (SaveData.Instance.Assists.Invincible) { |
| 195 | + if (delay <= 0f) { |
| 196 | + int direction = (player.Y > Y + bottomRect.Position.Y - 32f) ? 1 : -1; |
| 197 | + |
| 198 | + float from = Y; |
| 199 | + float to = Y + (direction * 48); |
| 200 | + player.Speed.Y = -direction * 200; |
| 201 | + |
| 202 | + if (direction > 0) { |
| 203 | + player.RefillDash(); |
| 204 | + } |
| 205 | + |
| 206 | + Tween.Set(this, Tween.TweenMode.Oneshot, 0.4f, Ease.CubeOut, tween => { |
| 207 | + Y = MathHelper.Lerp(from, to, tween.Eased); |
| 208 | + }); |
| 209 | + |
| 210 | + delay = 0.5f; |
| 211 | + loopSfx.Param("rising", 0f); |
| 212 | + Audio.Play("event:/game/general/assist_screenbottom", player.Position); |
| 213 | + } |
| 214 | + } else { |
| 215 | + player.Die(-Vector2.UnitY); |
| 216 | + } |
| 217 | + } |
| 218 | + |
| 219 | + public void Leave() { |
| 220 | + AddTag(Tags.TransitionUpdate); |
| 221 | + leaving = true; |
| 222 | + Collidable = false; |
| 223 | + Alarm.Set(this, 2f, delegate { |
| 224 | + RemoveSelf(); |
| 225 | + }); |
| 226 | + } |
| 227 | + |
| 228 | + public override void Update() { |
| 229 | + if (entering) { |
| 230 | + // set the Y position again on the first Update call, since the camera is finished being set up. |
| 231 | + Y = centerY; |
| 232 | + entering = false; |
| 233 | + } |
| 234 | + |
| 235 | + Level level = Scene as Level; |
| 236 | + X = level.Camera.X; |
| 237 | + delay -= Engine.DeltaTime; |
| 238 | + base.Update(); |
| 239 | + Visible = true; |
| 240 | + |
| 241 | + Player player = Scene.Tracker.GetEntity<Player>(); |
| 242 | + if (player != null) { |
| 243 | + LavaBlockerTrigger collidedTrigger = lavaBlockerTriggers.Find(trigger => player.CollideCheck(trigger)); |
| 244 | + |
| 245 | + if (collidedTrigger != null && (bool) lavaBlockerTriggerEnabled.GetValue(collidedTrigger)) { |
| 246 | + // player is in a lava blocker trigger and it is enabled; block the lava. |
| 247 | + Waiting = true; |
| 248 | + } |
| 249 | + } |
| 250 | + |
| 251 | + if (Waiting) { |
| 252 | + Y = Calc.Approach(Y, centerY, 128f * Engine.DeltaTime); |
| 253 | + |
| 254 | + loopSfx.Param("rising", 0f); |
| 255 | + if (player != null && player.X >= startX && !player.JustRespawned && player.StateMachine.State != 11) { |
| 256 | + Waiting = false; |
| 257 | + } |
| 258 | + } else if (!leaving && delay <= 0f) { |
| 259 | + loopSfx.Param("rising", 1f); |
| 260 | + if (Direction == DirectionMode.AlwaysDown || (Direction == DirectionMode.CoreModeBased && iceMode)) { |
| 261 | + Y += Speed * Engine.DeltaTime; |
| 262 | + } else { |
| 263 | + Y -= Speed * Engine.DeltaTime; |
| 264 | + } |
| 265 | + } |
| 266 | + |
| 267 | + topRect.Position.Y = Calc.Approach(topRect.Position.Y, TopOffset - topRect.Height + (leaving ? (-512) : sandwichDisplacement), (leaving ? 256 : 64) * Engine.DeltaTime); |
| 268 | + bottomRect.Position.Y = Calc.Approach(bottomRect.Position.Y, leaving ? 512 : -sandwichDisplacement, (leaving ? 256 : 64) * Engine.DeltaTime); |
| 269 | + |
| 270 | + lerp = Calc.Approach(lerp, iceMode ? 1 : 0, Engine.DeltaTime * 4f); |
| 271 | + |
| 272 | + bottomRect.SurfaceColor = Color.Lerp(RisingLava.Hot[0], RisingLava.Cold[0], lerp); |
| 273 | + bottomRect.EdgeColor = Color.Lerp(RisingLava.Hot[1], RisingLava.Cold[1], lerp); |
| 274 | + bottomRect.CenterColor = Color.Lerp(RisingLava.Hot[2], RisingLava.Cold[2], lerp); |
| 275 | + bottomRect.Spikey = lerp * 5f; |
| 276 | + bottomRect.UpdateMultiplier = (1f - lerp) * 2f; |
| 277 | + bottomRect.Fade = (iceMode ? 128 : 32); |
| 278 | + |
| 279 | + topRect.SurfaceColor = bottomRect.SurfaceColor; |
| 280 | + topRect.EdgeColor = bottomRect.EdgeColor; |
| 281 | + topRect.CenterColor = bottomRect.CenterColor; |
| 282 | + topRect.Spikey = bottomRect.Spikey; |
| 283 | + topRect.UpdateMultiplier = bottomRect.UpdateMultiplier; |
| 284 | + topRect.Fade = bottomRect.Fade; |
| 285 | + } |
| 286 | + } |
| 287 | +} |
0 commit comments