From e490ec6c12536e92d1199d0e1542a1c4677e4313 Mon Sep 17 00:00:00 2001 From: maddie480 <52103563+maddie480@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:11:17 +0100 Subject: [PATCH] Add Respawning Bounce Jellyfish --- ...maxHelpingHandRespawningBounceJellyfish.jl | 32 ++++ Ahorn/lang/en_gb.lang | 7 + Entities/RespawningBounceJellyfish.cs | 57 ++++++ Entities/RespawningJellyfish.cs | 140 +------------- Entities/RespawningJellyfishGeneric.cs | 174 ++++++++++++++++++ Loenn/entities/movingFlagTouchSwitch.lua | 2 +- Loenn/entities/respawningBounceJellyfish.lua | 80 ++++++++ Loenn/entities/saveFileStrawberryGate.lua | 2 +- Loenn/entities/shatterFlagSwitchGate.lua | 2 +- Loenn/lang/en_gb.lang | 13 ++ .../activateTimedTouchSwitchesTimer.lua | 2 +- MaddieHelpingHand.csproj | 4 + Module/MaxHelpingHandModule.cs | 23 ++- everest.yaml | 4 +- lib-stripped/BounceHelper.dll | Bin 0 -> 44032 bytes 15 files changed, 400 insertions(+), 142 deletions(-) create mode 100644 Ahorn/entities/maxHelpingHandRespawningBounceJellyfish.jl create mode 100644 Entities/RespawningBounceJellyfish.cs create mode 100644 Entities/RespawningJellyfishGeneric.cs create mode 100644 Loenn/entities/respawningBounceJellyfish.lua create mode 100644 lib-stripped/BounceHelper.dll diff --git a/Ahorn/entities/maxHelpingHandRespawningBounceJellyfish.jl b/Ahorn/entities/maxHelpingHandRespawningBounceJellyfish.jl new file mode 100644 index 0000000..5f72c2d --- /dev/null +++ b/Ahorn/entities/maxHelpingHandRespawningBounceJellyfish.jl @@ -0,0 +1,32 @@ +module MaxHelpingHandRespawningBounceJellyfish + +using ..Ahorn, Maple + +@mapdef Entity "MaxHelpingHand/RespawningBounceJellyfish" RespawningBounceJellyfish(x::Integer, y::Integer, platform::Bool=true, soulBound::Bool=true, baseDashCount::Integer=1, + respawnTime::Number=2.0, spriteDirectory::String="objects/MaxHelpingHand/glider") + +const placements = Ahorn.PlacementDict( + "Respawning Bounce Jellyfish (Bounce Helper + Maddie's Helping Hand)" => Ahorn.EntityPlacement( + RespawningBounceJellyfish + ) +) + +function Ahorn.selection(entity::RespawningBounceJellyfish) + x, y = Ahorn.position(entity) + sprite = get(entity, "spriteDirectory", "objects/MaxHelpingHand/glider") * "/idle0" + + return Ahorn.getSpriteRectangle(sprite, x, y - 4) +end + +function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::RespawningBounceJellyfish, room::Maple.Room) + sprite = get(entity, "spriteDirectory", "objects/MaxHelpingHand/glider") * "/idle0" + Ahorn.drawSprite(ctx, sprite, 0, -4) + + if get(entity, "platform", false) + curve = Ahorn.SimpleCurve((-7, -1), (7, -1), (0, -6)) + Ahorn.drawSimpleCurve(ctx, curve, (1.0, 1.0, 1.0, 1.0), thickness=1) + end +end + + +end \ No newline at end of file diff --git a/Ahorn/lang/en_gb.lang b/Ahorn/lang/en_gb.lang index c49af72..d615e47 100644 --- a/Ahorn/lang/en_gb.lang +++ b/Ahorn/lang/en_gb.lang @@ -152,6 +152,13 @@ placements.entities.MaxHelpingHand/RespawningJellyfish.tooltips.tutorial=Whether placements.entities.MaxHelpingHand/RespawningJellyfish.tooltips.respawnTime=The jellyfish will respawn at its starting location after this amount of time (in seconds) after being destroyed. placements.entities.MaxHelpingHand/RespawningJellyfish.tooltips.spriteDirectory=The directory containing all the sprites for the respawning jellyfish.\nTo make your own, copy Graphics/Atlases/Gameplay/objects/MaxHelpingHand/glider from the mod zip to Mods/yourmod/Graphics/Atlases/Gameplay/MyMap/myglider and type MyMap/myglider in this field. +# Respawning Bounce Jellyfish +placements.entities.MaxHelpingHand/RespawningBounceJellyfish.tooltips.platform=Whether the jelly starts floating. +placements.entities.MaxHelpingHand/RespawningBounceJellyfish.tooltips.soulBound=Whether the jelly kills the player on death and can't be left behind. +placements.entities.MaxHelpingHand/RespawningBounceJellyfish.tooltips.baseDashCount=How many dashes the jelly will start with and refill to. +placements.entities.MaxHelpingHand/RespawningBounceJellyfish.tooltips.respawnTime=The jellyfish will respawn at its starting location after this amount of time (in seconds) after being destroyed. +placements.entities.MaxHelpingHand/RespawningBounceJellyfish.tooltips.spriteDirectory=The directory containing all the sprites for the respawning jellyfish.\nTo make your own, copy Graphics/Atlases/Gameplay/objects/MaxHelpingHand/glider from the mod zip to Mods/yourmod/Graphics/Atlases/Gameplay/MyMap/myglider and type MyMap/myglider in this field. + # Grouped Trigger Spikes placements.entities.MaxHelpingHand/GroupedTriggerSpikesUp.tooltips.type=Changes the visual appearance of the spikes. placements.entities.MaxHelpingHand/GroupedTriggerSpikesDown.tooltips.type=Changes the visual appearance of the spikes. diff --git a/Entities/RespawningBounceJellyfish.cs b/Entities/RespawningBounceJellyfish.cs new file mode 100644 index 0000000..9cd5da3 --- /dev/null +++ b/Entities/RespawningBounceJellyfish.cs @@ -0,0 +1,57 @@ +using Celeste.Mod.BounceHelper; +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using System.Collections; +using Monocle; +using System.Collections.Generic; +using System; + +namespace Celeste.Mod.MaxHelpingHand.Entities { + [CustomEntity("MaxHelpingHand/RespawningJellyfish")] + public class RespawningBounceJellyfish : BounceJellyfish { + public static void LoadBounceHelper() { + RespawningJellyfishGeneric.Load(); + Everest.Events.Level.OnLoadEntity += onLoadEntity; + On.Monocle.Tracker.Initialize += onTrackerInitialize; + } + + public static void UnloadBounceHelper() { + RespawningJellyfishGeneric.Unload(); + Everest.Events.Level.OnLoadEntity -= onLoadEntity; + On.Monocle.Tracker.Initialize -= onTrackerInitialize; + } + + private static bool onLoadEntity(Level level, LevelData levelData, Vector2 offset, EntityData entityData) { + if (entityData.Name == "MaxHelpingHand/RespawningBounceJellyfish") { + level.Add(new RespawningBounceJellyfish(entityData, offset)); + return true; + } + + return false; + } + + private static void onTrackerInitialize(On.Monocle.Tracker.orig_Initialize orig) { + orig(); + Tracker.TrackedEntityTypes[typeof(RespawningBounceJellyfish)] = new List { typeof(BounceJellyfish) }; + } + + + private RespawningJellyfishGeneric manager; + + public RespawningBounceJellyfish(EntityData data, Vector2 offset) : base(data, offset) { + manager = new RespawningJellyfishGeneric(this, data, () => Speed, speed => Speed = speed); + } + + public override void Update() { + manager.Update(base.Update); + } + + protected override void OnSquish(CollisionData data) { + manager.OnSquish(base.OnSquish, data); + } + + private IEnumerator destroyThenRespawnRoutine() { + return manager.destroyThenRespawnRoutine(); + } + } +} diff --git a/Entities/RespawningJellyfish.cs b/Entities/RespawningJellyfish.cs index df90c23..d7ee9d3 100644 --- a/Entities/RespawningJellyfish.cs +++ b/Entities/RespawningJellyfish.cs @@ -1,156 +1,26 @@ using Celeste.Mod.Entities; using Microsoft.Xna.Framework; -using Mono.Cecil.Cil; -using Monocle; -using MonoMod.Cil; -using MonoMod.Utils; -using System; using System.Collections; namespace Celeste.Mod.MaxHelpingHand.Entities { [CustomEntity("MaxHelpingHand/RespawningJellyfish")] public class RespawningJellyfish : Glider { - public static void Load() { - IL.Celeste.Glider.Update += modGliderUpdate; - } - - public static void Unload() { - IL.Celeste.Glider.Update -= modGliderUpdate; - } - - private static ParticleType P_NotGlow; - - private DynData self; - - private float respawnTime; - private bool bubble; - - private Sprite sprite; - - private Vector2 initialPosition; - private bool respawning; - - private bool shouldRespawn = true; + private RespawningJellyfishGeneric manager; public RespawningJellyfish(EntityData data, Vector2 offset) : base(data, offset) { - if (P_NotGlow == null) { - // P_NotGlow is a transparent particle. - P_NotGlow = new ParticleType(P_Glow) { - Color = Color.Transparent, - Color2 = Color.Transparent - }; - } - - respawnTime = data.Float("respawnTime"); - bubble = data.Bool("bubble"); - initialPosition = Position; - respawning = false; - - // get the sprite, and replace it depending on the path in entity properties. - self = new DynData(this); - sprite = self.Get("sprite"); - new DynData(sprite)["atlas"] = GFX.Game; - sprite.Path = data.Attr("spriteDirectory", defaultValue: "objects/MaxHelpingHand/glider") + "/"; - sprite.Stop(); - sprite.ClearAnimations(); - sprite.AddLoop("idle", "idle", 0.1f); - sprite.AddLoop("held", "held", 0.1f); - sprite.Add("fall", "fall", 0.06f, "fallLoop"); - sprite.AddLoop("fallLoop", "fallLoop", 0.06f); - sprite.Add("death", "death", 0.06f); - sprite.Add("respawn", "respawn", 0.03f, "idle"); - sprite.Play("idle"); - - // make the jelly go invisible when the death animation is done. - sprite.OnFinish += anim => { - if (anim == "death") { - Visible = false; - } - }; - - // listen for transitions: if the jelly is carried to another screen, it should not respawn anymore. - Add(new TransitionListener() { - OnOutBegin = () => shouldRespawn = false - }); + manager = new RespawningJellyfishGeneric(this, data, () => Speed, speed => Speed = speed); } public override void Update() { - if (shouldRespawn && !respawning && Top + Speed.Y * Engine.DeltaTime > (SceneAs().Bounds.Bottom + 16)) { - // the jellyfish glided off-screen. - removeAndRespawn(); - } - - // if the jelly is invisible, "disable" the particles (actually make them invisible). - ParticleType vanillaGlow = P_Glow; - if (!Visible) P_Glow = P_NotGlow; - - base.Update(); - - P_Glow = vanillaGlow; - } - - private static void modGliderUpdate(ILContext il) { - ILCursor cursor = new ILCursor(il); - - while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchNewobj())) { - Logger.Log("MaxHelpingHand/RespawningJellyfish", $"Replacing coroutine to make jellyfish respawn at {cursor.Index} in IL for Glider.Update"); - - cursor.Emit(OpCodes.Ldarg_0); - cursor.EmitDelegate>((orig, self) => { - if (self is RespawningJellyfish jelly) { - return new Coroutine(jelly.destroyThenRespawnRoutine()); - } - - return orig; - }); - } + manager.Update(base.Update); } protected override void OnSquish(CollisionData data) { - if (shouldRespawn) { - if (!TrySquishWiggle(data)) { - // the jellyfish was squished. - removeAndRespawn(); - } - } else { - // vanilla behavior - base.OnSquish(data); - } - } - - private void removeAndRespawn() { - Collidable = false; - Visible = false; - self["destroyed"] = true; - Add(new Coroutine(respawnRoutine())); + manager.OnSquish(base.OnSquish, data); } private IEnumerator destroyThenRespawnRoutine() { - // do like vanilla, but instead of removing the jelly, wait then have it respawn. - Audio.Play("event:/new_content/game/10_farewell/glider_emancipate", Position); - sprite.Play("death"); - - return respawnRoutine(); - } - - private IEnumerator respawnRoutine() { - respawning = true; - - // wait for the respawn time - yield return respawnTime; - - // then respawn at the initial position - Visible = true; - Position = initialPosition; - Speed = Vector2.Zero; - sprite.Play("respawn"); - - yield return 0.24f; - - respawning = false; - self["destroyed"] = false; - self["bubble"] = bubble; - Collidable = true; + return manager.destroyThenRespawnRoutine(); } } } diff --git a/Entities/RespawningJellyfishGeneric.cs b/Entities/RespawningJellyfishGeneric.cs new file mode 100644 index 0000000..1ea3787 --- /dev/null +++ b/Entities/RespawningJellyfishGeneric.cs @@ -0,0 +1,174 @@ +using Microsoft.Xna.Framework; +using Mono.Cecil.Cil; +using Monocle; +using MonoMod.Cil; +using MonoMod.RuntimeDetour; +using MonoMod.Utils; +using System; +using System.Collections; +using System.Reflection; + +namespace Celeste.Mod.MaxHelpingHand.Entities { + internal static class RespawningJellyfishCache { + internal static MethodInfo trySquishWiggle = typeof(Actor).GetMethod("TrySquishWiggle", BindingFlags.NonPublic | BindingFlags.Instance); + } + public class RespawningJellyfishGeneric where T : Actor, U where U : Actor { + private static ILHook hookGliderUpdate = null; + + public static void Load() { + hookGliderUpdate = new ILHook(typeof(U).GetMethod("Update"), modGliderUpdate); + } + + public static void Unload() { + hookGliderUpdate?.Dispose(); + hookGliderUpdate = null; + } + + private static ParticleType P_NotGlow; + + private T self; + private DynData selfData; + + private float respawnTime; + private bool bubble; + + private Sprite sprite; + + private Vector2 initialPosition; + private bool respawning; + + private bool shouldRespawn = true; + + private Func getSpeed; + private Action setSpeed; + + public RespawningJellyfishGeneric(T self, EntityData data, Func getSpeed, Action setSpeed) { + this.self = self; + this.getSpeed = getSpeed; + this.setSpeed = setSpeed; + + if (P_NotGlow == null) { + // P_NotGlow is a transparent particle. + P_NotGlow = new ParticleType(Glider.P_Glow) { + Color = Color.Transparent, + Color2 = Color.Transparent + }; + } + + respawnTime = data.Float("respawnTime"); + bubble = data.Bool("bubble") || data.Bool("platform"); + initialPosition = self.Position; + respawning = false; + + // get the sprite, and replace it depending on the path in entity properties. + selfData = new DynData(self); + sprite = selfData.Get("sprite"); + new DynData(sprite)["atlas"] = GFX.Game; + sprite.Path = data.Attr("spriteDirectory", defaultValue: "objects/MaxHelpingHand/glider") + "/"; + sprite.Stop(); + sprite.ClearAnimations(); + foreach (string suffix in new string[] { "", "B", "R", "RH", "P", "PH", "F" }) { + sprite.AddLoop("idle" + suffix, "idle", 0.1f); + sprite.AddLoop("held" + suffix, "held", 0.1f); + sprite.Add("fall" + suffix, "fall", 0.06f, "fallLoop" + suffix); + sprite.AddLoop("fallLoop" + suffix, "fallLoop", 0.06f); + sprite.Add("death" + suffix, "death", 0.06f); + sprite.Add("respawn" + suffix, "respawn", 0.03f, "idle" + suffix); + } + sprite.Play("idle"); + + // make the jelly go invisible when the death animation is done. + sprite.OnFinish += anim => { + if (anim == "death") { + self.Visible = false; + } + }; + + // listen for transitions: if the jelly is carried to another screen, it should not respawn anymore. + self.Add(new TransitionListener() { + OnOutBegin = () => shouldRespawn = false + }); + } + + public void Update(Action baseUpdate) { + if (shouldRespawn && !respawning && self.Top + getSpeed().Y * Engine.DeltaTime > (self.SceneAs().Bounds.Bottom + 16)) { + // the jellyfish glided off-screen. + removeAndRespawn(); + } + + // if the jelly is invisible, "disable" the particles (actually make them invisible). + ParticleType vanillaGlow = Glider.P_Glow; + if (!self.Visible) Glider.P_Glow = P_NotGlow; + + baseUpdate(); + + Glider.P_Glow = vanillaGlow; + } + + private static void modGliderUpdate(ILContext il) { + ILCursor cursor = new ILCursor(il); + + if (cursor.TryGotoNext(MoveType.After, instr => instr.MatchNewobj())) { + MethodInfo method = typeof(T).GetMethod("destroyThenRespawnRoutine", BindingFlags.NonPublic | BindingFlags.Instance); + Logger.Log("MaxHelpingHand/RespawningJellyfish", $"Replacing coroutine to make jellyfish respawn at {cursor.Index} in IL for {cursor.Method.FullName}, calling {method.DeclaringType.FullName}.{method.Name}"); + + cursor.Emit(OpCodes.Ldarg_0); + cursor.EmitDelegate>((orig, self) => { + if (self is T) { + return new Coroutine((IEnumerator) method.Invoke(self, new object[0])); + } + + return orig; + }); + } + } + + public void OnSquish(Action baseOnSquish, CollisionData data) { + if (shouldRespawn) { + if (!((bool) RespawningJellyfishCache.trySquishWiggle.Invoke(self, new object[] { data }))) { + // the jellyfish was squished. + removeAndRespawn(); + } + } else { + // vanilla behavior + baseOnSquish(data); + } + } + + private void removeAndRespawn() { + self.Collidable = false; + self.Visible = false; + selfData["destroyed"] = true; + self.Add(new Coroutine(respawnRoutine())); + } + + internal IEnumerator destroyThenRespawnRoutine() { + // do like vanilla, but instead of removing the jelly, wait then have it respawn. + Audio.Play("event:/new_content/game/10_farewell/glider_emancipate", self.Position); + sprite.Play("death"); + + return respawnRoutine(); + } + + private IEnumerator respawnRoutine() { + respawning = true; + + // wait for the respawn time + yield return respawnTime; + + // then respawn at the initial position + self.Visible = true; + self.Position = initialPosition; + setSpeed(Vector2.Zero); + sprite.Play("respawn"); + + yield return 0.24f; + + respawning = false; + selfData["destroyed"] = false; + selfData["bubble"] = bubble; + selfData["platform"] = bubble; + self.Collidable = true; + } + } +} diff --git a/Loenn/entities/movingFlagTouchSwitch.lua b/Loenn/entities/movingFlagTouchSwitch.lua index 10f645a..1381854 100644 --- a/Loenn/entities/movingFlagTouchSwitch.lua +++ b/Loenn/entities/movingFlagTouchSwitch.lua @@ -7,7 +7,7 @@ touchSwitch.name = "MaxHelpingHand/MovingFlagTouchSwitch" touchSwitch.depth = 2000 touchSwitch.nodeLimits = {0, -1} touchSwitch.nodeLineRenderType = "line" -touchSwitch.associatedMods = { "OutbackHelper" } +touchSwitch.associatedMods = { "MaxHelpingHand", "OutbackHelper" } touchSwitch.placements = { { name = "touch_switch", diff --git a/Loenn/entities/respawningBounceJellyfish.lua b/Loenn/entities/respawningBounceJellyfish.lua new file mode 100644 index 0000000..f147073 --- /dev/null +++ b/Loenn/entities/respawningBounceJellyfish.lua @@ -0,0 +1,80 @@ +local drawableSprite = require("structs.drawable_sprite") +local drawableLine = require("structs.drawable_line") +local drawing = require("utils.drawing") + +local respawningBounceJellyfish = {} + +respawningBounceJellyfish.name = "MaxHelpingHand/RespawningBounceJellyfish" +respawningBounceJellyfish.depth = -5 +respawningBounceJellyfish.associatedMods = { "MaxHelpingHand", "BounceHelper" } +local respawningBounceJellyfishDashCounts = { + 0, 1, 2 +} + +respawningBounceJellyfish.fieldInformation = { + baseDashCount = { + options = respawningBounceJellyfishDashCounts, + editable = false + } +} +respawningBounceJellyfish.placements = { + { + name = "normal", + data = { + platform = true, + soulBound = false, + baseDashCount = 0, + respawnTime = 2.0, + spriteDirectory = "objects/MaxHelpingHand/glider" + } + }, + { + name = "single", + data = { + platform = true, + soulBound = false, + baseDashCount = 1, + respawnTime = 2.0, + spriteDirectory = "objects/MaxHelpingHand/glider" + } + }, + { + name = "double", + data = { + platform = true, + soulBound = false, + baseDashCount = 2, + respawnTime = 2.0, + spriteDirectory = "objects/MaxHelpingHand/glider" + } + } +} + +function respawningBounceJellyfish.sprite(room, entity) + local bubble = entity.platform + local texture = entity.spriteDirectory .. "/idle0" + + if entity.bubble then + local x, y = entity.x or 0, entity.y or 0 + local points = drawing.getSimpleCurve({x - 11, y - 1}, {x + 11, y - 1}, {x - 0, y - 6}) + local lineSprites = drawableLine.fromPoints(points):getDrawableSprite() + local jellySprite = drawableSprite.fromTexture(texture, entity) + jellySprite:setOffset(jellySprite.meta.width / 2, jellySprite.meta.height / 2 + 4) + + table.insert(lineSprites, 1, jellySprite) + + return lineSprites + + else + return drawableSprite.fromTexture(texture, entity) + end +end + +function respawningBounceJellyfish.rectangle(room, entity) + local texture = entity.spriteDirectory .. "/idle0" + local sprite = drawableSprite.fromTexture(texture, entity) + + return sprite:getRectangle() +end + +return respawningBounceJellyfish \ No newline at end of file diff --git a/Loenn/entities/saveFileStrawberryGate.lua b/Loenn/entities/saveFileStrawberryGate.lua index 1f850cf..5d064db 100644 --- a/Loenn/entities/saveFileStrawberryGate.lua +++ b/Loenn/entities/saveFileStrawberryGate.lua @@ -9,7 +9,7 @@ heartDoor.name = "MaxHelpingHand/SaveFileStrawberryGate" heartDoor.depth = 0 heartDoor.nodeLimits = {0, 1} heartDoor.minimumSize = {40, 8} -heartDoor.associatedMods = { "LunaticHelper" } +heartDoor.associatedMods = { "MaxHelpingHand", "LunaticHelper" } heartDoor.fieldInformation = { requires = { diff --git a/Loenn/entities/shatterFlagSwitchGate.lua b/Loenn/entities/shatterFlagSwitchGate.lua index 2e9990f..4426f9f 100644 --- a/Loenn/entities/shatterFlagSwitchGate.lua +++ b/Loenn/entities/shatterFlagSwitchGate.lua @@ -6,7 +6,7 @@ local switchGate = {} switchGate.name = "MaxHelpingHand/ShatterFlagSwitchGate" switchGate.depth = 0 switchGate.minimumSize = {16, 16} -switchGate.associatedMods = { "VortexHelper" } +switchGate.associatedMods = { "MaxHelpingHand", "VortexHelper" } switchGate.placements = {} local textures = { diff --git a/Loenn/lang/en_gb.lang b/Loenn/lang/en_gb.lang index 79c61c9..8009d5d 100644 --- a/Loenn/lang/en_gb.lang +++ b/Loenn/lang/en_gb.lang @@ -644,6 +644,19 @@ entities.MaxHelpingHand/RespawningJellyfish.attributes.description.tutorial=Whet entities.MaxHelpingHand/RespawningJellyfish.attributes.description.respawnTime=The jellyfish will respawn at its starting location after this amount of time (in seconds) after being destroyed. entities.MaxHelpingHand/RespawningJellyfish.attributes.description.spriteDirectory=The directory containing all the sprites for the respawning jellyfish.\nTo make your own, copy Graphics/Atlases/Gameplay/objects/MaxHelpingHand/glider from the mod zip to Mods/yourmod/Graphics/Atlases/Gameplay/MyMap/myglider and type MyMap/myglider in this field. +# Respawning Bounce Jellyfish +entities.MaxHelpingHand/RespawningBounceJellyfish.placements.name.normal=Respawning Bounce Jellyfish +entities.MaxHelpingHand/RespawningBounceJellyfish.placements.name.single=Respawning Bounce Jellyfish (One Dash) +entities.MaxHelpingHand/RespawningBounceJellyfish.placements.name.double=Respawning Bounce Jellyfish (Two Dashes) +entities.MaxHelpingHand/RespawningBounceJellyfish.placements.description.normal=A Jellyfish compatible with Bounce Mode. +entities.MaxHelpingHand/RespawningBounceJellyfish.placements.description.single=Bounce Mode exclusive jellyfish that can dash. +entities.MaxHelpingHand/RespawningBounceJellyfish.placements.description.double=Bounce Mode exclusive jellyfish that can dash twice. +entities.MaxHelpingHand/RespawningBounceJellyfish.attributes.description.platform=Whether the jelly starts floating. +entities.MaxHelpingHand/RespawningBounceJellyfish.attributes.description.soulBound=Whether the jelly kills the player on death and can't be left behind. +entities.MaxHelpingHand/RespawningBounceJellyfish.attributes.description.baseDashCount=How many dashes the jelly will start with and refill to. +entities.MaxHelpingHand/RespawningBounceJellyfish.attributes.description.respawnTime=The jellyfish will respawn at its starting location after this amount of time (in seconds) after being destroyed. +entities.MaxHelpingHand/RespawningBounceJellyfish.attributes.description.spriteDirectory=The directory containing all the sprites for the respawning jellyfish.\nTo make your own, copy Graphics/Atlases/Gameplay/objects/MaxHelpingHand/glider from the mod zip to Mods/yourmod/Graphics/Atlases/Gameplay/MyMap/myglider and type MyMap/myglider in this field. + # Rotating Bumper entities.MaxHelpingHand/RotatingBumper.placements.name.bumper=Bumper (Rotating) entities.MaxHelpingHand/RotatingBumper.attributes.description.speed=The rotating speed in degrees per second. Use a negative number for counterclockwise rotation. diff --git a/Loenn/triggers/activateTimedTouchSwitchesTimer.lua b/Loenn/triggers/activateTimedTouchSwitchesTimer.lua index 54c7223..ff045d2 100644 --- a/Loenn/triggers/activateTimedTouchSwitchesTimer.lua +++ b/Loenn/triggers/activateTimedTouchSwitchesTimer.lua @@ -1,7 +1,7 @@ local trigger = {} trigger.name = "MaxHelpingHand/ActivateTimedTouchSwitchesTimerTrigger" -trigger.associatedMods = { "OutbackHelper" } +trigger.associatedMods = { "MaxHelpingHand", "OutbackHelper" } trigger.placements = { name = "trigger" } diff --git a/MaddieHelpingHand.csproj b/MaddieHelpingHand.csproj index f8865ef..156ce7c 100644 --- a/MaddieHelpingHand.csproj +++ b/MaddieHelpingHand.csproj @@ -7,6 +7,10 @@ preview + + lib-stripped\BounceHelper.dll + False + lib-stripped\Celeste.exe False diff --git a/Module/MaxHelpingHandModule.cs b/Module/MaxHelpingHandModule.cs index 7cda1ca..1dc0984 100644 --- a/Module/MaxHelpingHandModule.cs +++ b/Module/MaxHelpingHandModule.cs @@ -11,6 +11,7 @@ namespace Celeste.Mod.MaxHelpingHand.Module { public class MaxHelpingHandModule : EverestModule { private bool hookedSineParallax = false; + private bool bounceHelperLoaded = false; private static FieldInfo contentLoaded = typeof(Everest).GetField("_ContentLoaded", BindingFlags.NonPublic | BindingFlags.Static); @@ -84,7 +85,7 @@ public override void Load() { ReskinnableCrystalHeart.Load(); SetFlagOnButtonPressController.Load(); FlagPickup.Load(); - RespawningJellyfish.Load(); + RespawningJellyfishGeneric.Load(); SetFlagOnSpawnTrigger.Load(); CustomSeekerBarrier.Load(); MoreCustomNPC.Load(); @@ -163,7 +164,7 @@ public override void Unload() { SetFlagOnButtonPressController.Unload(); FlagPickup.Unload(); AvBdaySpeechBubbleFixup.Unload(); - RespawningJellyfish.Unload(); + RespawningJellyfishGeneric.Unload(); ReskinnableStarRotateSpinner.Unload(); ReskinnableStarTrackSpinner.Unload(); SetFlagOnSpawnTrigger.Unload(); @@ -191,6 +192,10 @@ public override void Unload() { if (hookedSineParallax) { unhookSineParallax(); } + + if (bounceHelperLoaded) { + unloadBounceHelper(); + } } public override void LoadContent(bool firstLoad) { @@ -231,6 +236,10 @@ private void HookMods() { if (!hookedSineParallax && Everest.Loader.DependencyLoaded(new EverestModuleMetadata() { Name = "FlaglinesAndSuch", Version = new Version(1, 4, 17) })) { hookSineParallax(); } + + if (!bounceHelperLoaded && Everest.Loader.DependencyLoaded(new EverestModuleMetadata { Name = "BounceHelper", Version = new Version(1, 8, 0) })) { + loadBounceHelper(); + } } private void hookSineParallax() { @@ -243,6 +252,16 @@ private void unhookSineParallax() { hookedSineParallax = false; } + private void loadBounceHelper() { + RespawningBounceJellyfish.LoadBounceHelper(); + bounceHelperLoaded = true; + } + + private void unloadBounceHelper() { + RespawningBounceJellyfish.UnloadBounceHelper(); + bounceHelperLoaded = false; + } + private Backdrop onLoadBackdrop(MapData map, BinaryPacker.Element child, BinaryPacker.Element above) { if (child.Name.Equals("MaxHelpingHand/HeatWaveNoColorGrade", StringComparison.OrdinalIgnoreCase)) { return new HeatWaveNoColorGrade(child.AttrBool("controlColorGradeWhenActive"), child.AttrBool("renderParticles", defaultValue: true)); diff --git a/everest.yaml b/everest.yaml index df50067..015c5c2 100644 --- a/everest.yaml +++ b/everest.yaml @@ -1,6 +1,6 @@ # The mod used to be known as "max480's Helping Hand", and wasn't renamed for compatibility reasons - Name: MaxHelpingHand - Version: 1.28.7 + Version: 1.28.8 DLL: bin/Release/net452/MaxHelpingHand.dll Dependencies: - Name: Everest @@ -14,3 +14,5 @@ Version: 1.1.1 - Name: OutbackHelper Version: 1.7.2 + - Name: BounceHelper + Version: 1.8.0 diff --git a/lib-stripped/BounceHelper.dll b/lib-stripped/BounceHelper.dll new file mode 100644 index 0000000000000000000000000000000000000000..5ff6786e27b52eb0fd53e2ac25f6d6d069836703 GIT binary patch literal 44032 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 literal 0 HcmV?d00001