From a471e5a4ef946d7a091bfb10d405c79d9b62fe86 Mon Sep 17 00:00:00 2001 From: bigkahuna443 <13278973+bigkahuna443@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:12:36 -0500 Subject: [PATCH 1/3] Add Boom Box Zips from SSC23 Ported this over with Viv + xoli's permission. Things to note: - added an activationId field to bring it in line with other FH entities - added a Loenn plugin - zip mover functionality copied from vanilla with a fix to move the explosion collider entity with it --- Ahorn/entities/boomBoxZip.jl | 69 +++++++++++ Ahorn/lang/en_gb.lang | 5 + Code/Entities/BoomBox.cs | 4 +- Code/Entities/BoomBoxZip.cs | 201 ++++++++++++++++++++++++++++++++ Loenn/entities/boom_box_zip.lua | 105 +++++++++++++++++ Loenn/lang/en_gb.lang | 6 + 6 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 Ahorn/entities/boomBoxZip.jl create mode 100644 Code/Entities/BoomBoxZip.cs create mode 100644 Loenn/entities/boom_box_zip.lua diff --git a/Ahorn/entities/boomBoxZip.jl b/Ahorn/entities/boomBoxZip.jl new file mode 100644 index 0000000..2cfb54b --- /dev/null +++ b/Ahorn/entities/boomBoxZip.jl @@ -0,0 +1,69 @@ +module FactoryHelperBoomBoxZip +using ..Ahorn, Maple + +@mapdef Entity "FactoryHelper/BoomBoxZip" BoomBoxZip(x::Integer, y::Integer, activationId::String="", initialDelay::Number=0.0, startActive::Bool=false) + +const placements = Ahorn.PlacementDict( + "Boom Box Zip (Factory Helper)" => Ahorn.EntityPlacement( + BoomBoxZip, + "point", + Dict{String, Any}( + "startActive" => true + ), + function(entity) + entity.data["nodes"] = [(Int(entity.data["x"]) + 32, Int(entity.data["y"]))] + end + ), +) +activeSprite = "objects/FactoryHelper/boomBox/active00" + +Ahorn.nodeLimits(entity::BoomBoxZip) = 1,1 + +ropeColor = (77, 60, 34) ./ 255 +function Ahorn.renderAbs(ctx::Ahorn.Cairo.CairoContext, entity::BoomBoxZip, room::Maple.Room) + x, y = Ahorn.position(entity) + + Ahorn.drawSprite(ctx, activeSprite, x + 12, y + 12) + + nx, ny = Int.(entity.data["nodes"][1]) + + cx, cy = x + 12, y + 12 + cnx,cny = nx + 12, ny + 12 + length = sqrt((x - nx)^2 + (y - ny)^2) + theta = atan(cny - cy, cnx - cx) + Ahorn.Cairo.save(ctx) + + Ahorn.translate(ctx, cx, cy) + Ahorn.rotate(ctx, theta) + + Ahorn.setSourceColor(ctx, ropeColor) + Ahorn.set_antialias(ctx, 1) + Ahorn.set_line_width(ctx, 1); + + # Offset for rounding errors + Ahorn.move_to(ctx, 0, 4 + (theta <= 0)) + Ahorn.line_to(ctx, length, 4 + (theta <= 0)) + + Ahorn.move_to(ctx, 0, -4 - (theta > 0)) + Ahorn.line_to(ctx, length, -4 - (theta > 0)) + + Ahorn.stroke(ctx) + + Ahorn.Cairo.restore(ctx) + + Ahorn.drawSprite(ctx, "objects/zipmover/cog", cnx, cny) + +end + +function Ahorn.selection(entity::BoomBoxZip) + x, y = Ahorn.position(entity) + nx, ny = Int.(entity.data["nodes"][1]) + + width = Int(get(entity.data, "width", 8)) + height = Int(get(entity.data, "height", 8)) + + return [Ahorn.Rectangle(x, y, 24, 24), Ahorn.Rectangle(nx + floor(Int, width / 2) - 5, ny + floor(Int, height / 2) - 5, 10, 10)] +end + + +end \ No newline at end of file diff --git a/Ahorn/lang/en_gb.lang b/Ahorn/lang/en_gb.lang index e85491c..a23bfb6 100644 --- a/Ahorn/lang/en_gb.lang +++ b/Ahorn/lang/en_gb.lang @@ -7,6 +7,11 @@ placements.entities.FactoryHelper/BoomBox.tooltips.activationId=String value of placements.entities.FactoryHelper/BoomBox.tooltips.initialDelay=The time it takes before the box starts turning on in seconds after activated. placements.entities.FactoryHelper/BoomBox.tooltips.startActive=If `true` the boom box will initially be active. +# Boom Box Zip +placements.entities.FactoryHelper/BoomBoxZip.tooltips.activationId=String value of the entity's activator ID. When a factory activator with this ID is turned on, the entity will toggle state. +placements.entities.FactoryHelper/BoomBoxZip.tooltips.initialDelay=The time it takes before the box starts turning on in seconds after activated. +placements.entities.FactoryHelper/BoomBoxZip.tooltips.startActive=If `true` the boom box will initially be active. + # Conveyor Belt placements.entities.FactoryHelper/Conveyor.tooltips.activationId=String value of the entity's activator ID. When a factory activator with this ID is turned on, the entity will toggle state. placements.entities.FactoryHelper/Conveyor.tooltips.startLeft=If `true` the conveyor will initially be moving left. diff --git a/Code/Entities/BoomBox.cs b/Code/Entities/BoomBox.cs index 8c634b8..9f600b2 100644 --- a/Code/Entities/BoomBox.cs +++ b/Code/Entities/BoomBox.cs @@ -25,7 +25,7 @@ public class BoomBox : Solid { private readonly float _initialDelay; private readonly Sprite _sprite; private readonly Sprite _boomSprite; - private readonly BoomCollider _boomCollider; + protected BoomCollider _boomCollider; private readonly SoundSource _sfx; private readonly float _startupTime = 1.5f; private float _angryResetTimer = 0f; @@ -227,7 +227,7 @@ private void Explode() { Collidable = true; } - private class BoomCollider : Entity { + protected class BoomCollider : Entity { public BoomCollider(Vector2 position) : base(position) { Collider = new Circle(40f, 0, 0); } diff --git a/Code/Entities/BoomBoxZip.cs b/Code/Entities/BoomBoxZip.cs new file mode 100644 index 0000000..abac6b8 --- /dev/null +++ b/Code/Entities/BoomBoxZip.cs @@ -0,0 +1,201 @@ +using Celeste; +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System; +using System.Collections; + +namespace FactoryHelper.Entities { + [CustomEntity("FactoryHelper/BoomBoxZip")] + public class BoomBoxZip : BoomBox { + public float percent; + public Vector2 start, target; + private ZipMoverPathRenderer pathRenderer; + + public BoomBoxZip(EntityData data, Vector2 offset) + : this(data.Position + offset, data.Attr("activationId", ""), data.Float("initialDelay", 0f), data.Bool("startActive", false), data.Nodes[0] + offset) { + } + + public BoomBoxZip(Vector2 position, string activationId, float initialDelay, bool startActive, Vector2 target) + : base(position, activationId, initialDelay, startActive) { + Add(new Coroutine(ZipMoverSequence())); + this.start = this.Position; + this.target = target; + } + + public override void Added(Scene scene) { + base.Added(scene); + scene.Add(pathRenderer = new ZipMoverPathRenderer(this)); + } + + private IEnumerator ZipMoverSequence() { + start = Position; + while (true) { + if (!HasPlayerRider()) { + yield return null; + continue; + } + Input.Rumble(RumbleStrength.Medium, RumbleLength.Short); + StartShaking(0.1f); + yield return 0.1f; + StopPlayerRunIntoAnimation = false; + float at2 = 0f; + while (at2 < 1f) { + yield return null; + at2 = Calc.Approach(at2, 1f, 2f * Engine.DeltaTime); + percent = Ease.SineIn(at2); + Vector2 vector = Vector2.Lerp(start, target, percent); + ScrapeParticlesCheck(vector); + if (Scene.OnInterval(0.1f)) { + pathRenderer.CreateSparks(); + } + + MoveTo(vector); + _boomCollider.Position = vector + new Vector2(Width / 2, Height / 2); + } + + StartShaking(0.2f); + Input.Rumble(RumbleStrength.Strong, RumbleLength.Medium); + SceneAs().Shake(); + StopPlayerRunIntoAnimation = true; + yield return 0.5f; + StopPlayerRunIntoAnimation = false; + at2 = 0f; + while (at2 < 1f) { + yield return null; + at2 = Calc.Approach(at2, 1f, 0.5f * Engine.DeltaTime); + percent = 1f - Ease.SineIn(at2); + Vector2 position = Vector2.Lerp(target, start, Ease.SineIn(at2)); + MoveTo(position); + _boomCollider.Position = position + new Vector2(Width / 2, Height / 2); + } + + StopPlayerRunIntoAnimation = true; + StartShaking(0.2f); + yield return 0.5f; + } + } + + private void ScrapeParticlesCheck(Vector2 to) { + if (!base.Scene.OnInterval(0.03f)) { + return; + } + + bool flag = to.Y != base.ExactPosition.Y; + bool flag2 = to.X != base.ExactPosition.X; + if (flag && !flag2) { + int num = Math.Sign(to.Y - base.ExactPosition.Y); + Vector2 value = (num != 1) ? base.TopLeft : base.BottomLeft; + int num2 = 4; + if (num == 1) { + num2 = Math.Min((int)base.Height - 12, 20); + } + + int num3 = (int)base.Height; + if (num == -1) { + num3 = Math.Max(16, (int)base.Height - 16); + } + + if (base.Scene.CollideCheck(value + new Vector2(-2f, num * -2))) { + for (int i = num2; i < num3; i += 8) { + SceneAs().ParticlesFG.Emit(ZipMover.P_Scrape, base.TopLeft + new Vector2(0f, (float)i + (float)num * 2f), (num == 1) ? (-(float)Math.PI / 4f) : ((float)Math.PI / 4f)); + } + } + + if (base.Scene.CollideCheck(value + new Vector2(base.Width + 2f, num * -2))) { + for (int j = num2; j < num3; j += 8) { + SceneAs().ParticlesFG.Emit(ZipMover.P_Scrape, base.TopRight + new Vector2(-1f, (float)j + (float)num * 2f), (num == 1) ? ((float)Math.PI * -3f / 4f) : ((float)Math.PI * 3f / 4f)); + } + } + } else { + if (!flag2 || flag) { + return; + } + + int num4 = Math.Sign(to.X - base.ExactPosition.X); + Vector2 value2 = (num4 != 1) ? base.TopLeft : base.TopRight; + int num5 = 4; + if (num4 == 1) { + num5 = Math.Min((int)base.Width - 12, 20); + } + + int num6 = (int)base.Width; + if (num4 == -1) { + num6 = Math.Max(16, (int)base.Width - 16); + } + + if (base.Scene.CollideCheck(value2 + new Vector2(num4 * -2, -2f))) { + for (int k = num5; k < num6; k += 8) { + SceneAs().ParticlesFG.Emit(ZipMover.P_Scrape, base.TopLeft + new Vector2((float)k + (float)num4 * 2f, -1f), (num4 == 1) ? ((float)Math.PI * 3f / 4f) : ((float)Math.PI / 4f)); + } + } + + if (base.Scene.CollideCheck(value2 + new Vector2(num4 * -2, base.Height + 2f))) { + for (int l = num5; l < num6; l += 8) { + SceneAs().ParticlesFG.Emit(ZipMover.P_Scrape, base.BottomLeft + new Vector2((float)l + (float)num4 * 2f, 0f), (num4 == 1) ? ((float)Math.PI * -3f / 4f) : (-(float)Math.PI / 4f)); + } + } + } + } + + private class ZipMoverPathRenderer : Entity { + private static readonly Color ropeColor = Calc.HexToColor("4d3c22"); + private static readonly Color ropeLightColor = Calc.HexToColor("766c49"); + + public BoomBoxZip zipMover; + + private MTexture cog; + private Vector2 from; + private Vector2 to; + private Vector2 sparkAdd; + private float sparkDirFromA; + private float sparkDirFromB; + private float sparkDirToA; + private float sparkDirToB; + + public ZipMoverPathRenderer(BoomBoxZip zipMover) { + base.Depth = 5000; + this.zipMover = zipMover; + from = this.zipMover.start + new Vector2(this.zipMover.Width / 2f, this.zipMover.Height / 2f); + to = this.zipMover.target + new Vector2(this.zipMover.Width / 2f, this.zipMover.Height / 2f); + sparkAdd = (from - to).SafeNormalize(5f).Perpendicular(); + float num = (from - to).Angle(); + sparkDirFromA = num + (float)Math.PI / 8f; + sparkDirFromB = num - (float)Math.PI / 8f; + sparkDirToA = num + (float)Math.PI - (float)Math.PI / 8f; + sparkDirToB = num + (float)Math.PI + (float)Math.PI / 8f; + cog = GFX.Game["objects/zipmover/cog"]; + } + + public void CreateSparks() { + SceneAs().ParticlesBG.Emit(ZipMover.P_Sparks, from + sparkAdd + Calc.Random.Range(-Vector2.One, Vector2.One), sparkDirFromA); + SceneAs().ParticlesBG.Emit(ZipMover.P_Sparks, from - sparkAdd + Calc.Random.Range(-Vector2.One, Vector2.One), sparkDirFromB); + SceneAs().ParticlesBG.Emit(ZipMover.P_Sparks, to + sparkAdd + Calc.Random.Range(-Vector2.One, Vector2.One), sparkDirToA); + SceneAs().ParticlesBG.Emit(ZipMover.P_Sparks, to - sparkAdd + Calc.Random.Range(-Vector2.One, Vector2.One), sparkDirToB); + } + + public override void Render() { + DrawCogs(Vector2.UnitY, Color.Black); + DrawCogs(Vector2.Zero); + } + + private void DrawCogs(Vector2 offset, Color? colorOverride = null) { + Vector2 vector = (to - from).SafeNormalize(); + Vector2 value = vector.Perpendicular() * 3f; + Vector2 value2 = -vector.Perpendicular() * 4f; + float rotation = zipMover.percent * (float)Math.PI * 2f; + Draw.Line(from + value + offset, to + value + offset, colorOverride.HasValue ? colorOverride.Value : ropeColor); + Draw.Line(from + value2 + offset, to + value2 + offset, colorOverride.HasValue ? colorOverride.Value : ropeColor); + for (float num = 4f - zipMover.percent * (float)Math.PI * 8f % 4f; num < (to - from).Length(); num += 4f) { + Vector2 value3 = from + value + vector.Perpendicular() + vector * num; + Vector2 value4 = to + value2 - vector * num; + Draw.Line(value3 + offset, value3 + vector * 2f + offset, colorOverride.HasValue ? colorOverride.Value : ropeLightColor); + Draw.Line(value4 + offset, value4 - vector * 2f + offset, colorOverride.HasValue ? colorOverride.Value : ropeLightColor); + } + + cog.DrawCentered(from + offset, colorOverride.HasValue ? colorOverride.Value : Color.White, 1f, rotation); + cog.DrawCentered(to + offset, colorOverride.HasValue ? colorOverride.Value : Color.White, 1f, rotation); + } + } + } +} diff --git a/Loenn/entities/boom_box_zip.lua b/Loenn/entities/boom_box_zip.lua new file mode 100644 index 0000000..c6f0b1c --- /dev/null +++ b/Loenn/entities/boom_box_zip.lua @@ -0,0 +1,105 @@ +local drawableSprite = require("structs.drawable_sprite") +local drawableLine = require("structs.drawable_line") +local utils = require("utils") + +local boomBoxZip = {} + +boomBoxZip.name = "FactoryHelper/BoomBoxZip" +boomBoxZip.nodeVisibility = "never" +boomBoxZip.nodeLimits = {1, 1} +boomBoxZip.canResize = {false, false} + +-- width added as a hack to fix default node placements +boomBoxZip.ignoredFields = {"_name", "_id", "width"} + +boomBoxZip.fieldInformation = { + initialDelay = { + minimumValue = 0.0 + } +} + +boomBoxZip.placements = { + name = "active", + data = { + width = 24, + activationId = "", + initialDelay = 0.0, + startActive = true + } +} + +local inactiveTexture = "objects/FactoryHelper/boomBox/idle00" +local activeTexture = "objects/FactoryHelper/boomBox/active00" +local cogTexture = "objects/zipmover/cog" +local ropeColor = {77 / 255, 60 / 255, 34 / 255} + +local function addNodeSprites(sprites, entity, centerX, centerY, centerNodeX, centerNodeY) + local nodeCogSprite = drawableSprite.fromTexture(cogTexture, entity) + nodeCogSprite:setPosition(centerNodeX, centerNodeY) + nodeCogSprite:setJustification(0.5, 0.5) + + local points = {centerX, centerY, centerNodeX, centerNodeY} + local leftLine = drawableLine.fromPoints(points, ropeColor, 1) + local rightLine = drawableLine.fromPoints(points, ropeColor, 1) + + leftLine:setOffset(0, 4.5) + rightLine:setOffset(0, -4.5) + + leftLine.depth = 5000 + rightLine.depth = 5000 + + for _, sprite in ipairs(leftLine:getDrawableSprite()) do + table.insert(sprites, sprite) + end + + for _, sprite in ipairs(rightLine:getDrawableSprite()) do + table.insert(sprites, sprite) + end + + table.insert(sprites, nodeCogSprite) +end + +function boomBoxZip.sprite(room, entity) + local sprites = {} + + local x, y = entity.x or 0, entity.y or 0 + local nodes = entity.nodes or {{x = 0, y = 0}} + + -- Draw cog + rope + local halfWidth = 12 + local centerX = x + halfWidth + local centerY = y + halfWidth + local centerNodeX = nodes[1].x + halfWidth + local centerNodeY = nodes[1].y + halfWidth + addNodeSprites(sprites, entity, centerX, centerY, centerNodeX, centerNodeY) + + -- Draw boom box + local boomBoxTex = entity.startActive and activeTexture or inactiveTexture + local boomBoxSprite = drawableSprite.fromTexture(boomBoxTex, entity) + boomBoxSprite:setPosition(centerX, centerY) + boomBoxSprite:setJustification(0.5, 0.5) + table.insert(sprites, boomBoxSprite) + + return sprites +end + +function boomBoxZip.selection(room, entity) + local x, y = entity.x or 0, entity.y or 0 + local nodes = entity.nodes or {{x = 0, y = 0}} + + local halfWidth = 12 + local centerX = x + halfWidth + local centerY = y + halfWidth + local centerNodeX = nodes[1].x + halfWidth + local centerNodeY = nodes[1].y + halfWidth + + local cogSprite = drawableSprite.fromTexture(cogTexture, entity) + local cogWidth, cogHeight = cogSprite.meta.width, cogSprite.meta.height + + local mainRectangle = utils.rectangle(x, y, 24, 24) + local nodeRectangle = utils.rectangle(centerNodeX - math.floor(cogWidth / 2), centerNodeY - math.floor(cogHeight / 2), cogWidth, cogHeight) + + return mainRectangle, {nodeRectangle} +end + +return boomBoxZip diff --git a/Loenn/lang/en_gb.lang b/Loenn/lang/en_gb.lang index af1cb67..4b94f82 100644 --- a/Loenn/lang/en_gb.lang +++ b/Loenn/lang/en_gb.lang @@ -14,6 +14,12 @@ entities.FactoryHelper/BoomBox.attributes.description.activationId=String value entities.FactoryHelper/BoomBox.attributes.description.initialDelay=The time it takes before the box starts turning on in seconds after activated. entities.FactoryHelper/BoomBox.attributes.description.startActive=If `true` the boom box will initially be active. +# Boom Box Zip +entities.FactoryHelper/BoomBoxZip.placements.name.active=Boom Box Zip +entities.FactoryHelper/BoomBoxZip.attributes.description.activationId=String value of the entity's activator ID. When a factory activator with this ID is turned on, the entity will toggle state. +entities.FactoryHelper/BoomBoxZip.attributes.description.initialDelay=The time it takes before the box starts turning on in seconds after activated. +entities.FactoryHelper/BoomBoxZip.attributes.description.startActive=If `true` the boom box will initially be active. + # Conveyor entities.FactoryHelper/Conveyor.placements.name.start_left=Conveyor (Left) entities.FactoryHelper/Conveyor.placements.name.start_right=Conveyor (Right) From d9f331e1cf45c151d5a66201d16c9e5e91dca80d Mon Sep 17 00:00:00 2001 From: bigkahuna443 <13278973+bigkahuna443@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:40:22 -0500 Subject: [PATCH 2/3] Change BoomCollider into a Circle Mostly just makes it a bit easier to tell what's happening/make future changes if needed --- Code/Entities/BoomBox.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Code/Entities/BoomBox.cs b/Code/Entities/BoomBox.cs index 9f600b2..57c5dc5 100644 --- a/Code/Entities/BoomBox.cs +++ b/Code/Entities/BoomBox.cs @@ -22,10 +22,11 @@ public class BoomBox : Solid { SpeedMax = 24f }; + protected Circle _boomCollider; + private readonly float _initialDelay; private readonly Sprite _sprite; private readonly Sprite _boomSprite; - protected BoomCollider _boomCollider; private readonly SoundSource _sfx; private readonly float _startupTime = 1.5f; private float _angryResetTimer = 0f; @@ -63,7 +64,7 @@ public BoomBox(Vector2 position, string activationId, float initialDelay, bool s _boomSprite.CenterOrigin(); _boomSprite.Position = new Vector2(Width / 2, Height / 2); - _boomCollider = new BoomCollider(position + new Vector2(Width / 2, Height / 2)); + _boomCollider = new Circle(40f, X + Width / 2, Y + Height / 2); Add(_sfx = new SoundSource()); _sfx.Position = new Vector2(Width / 2, Height / 2); Add(new LightOcclude(0.2f)); @@ -145,7 +146,6 @@ private IEnumerator WindDownSequence() { public override void Added(Scene scene) { base.Added(scene); - scene.Add(_boomCollider); Activator.HandleStartup(scene); } @@ -172,6 +172,11 @@ public override void Update() { } } + public override void DebugRender(Camera camera) { + base.DebugRender(camera); + _boomCollider.Render(camera, Collidable ? Color.Red : Color.DarkRed); + } + private void HandleAngryMode() { CheckForAngryMode(); if (_angryMode) { @@ -216,7 +221,7 @@ private void Explode() { (Scene as Level).Displacement.AddBurst(Center, 0.35f, 4f, 64f, 0.5f); Player player = Scene.Tracker.GetEntity(); Collidable = false; - if (player != null && player.CollideCheck(_boomCollider) && !Scene.CollideCheck(player.Center, Center)) { + if (player != null && _boomCollider.Collide(player) && !Scene.CollideCheck(player.Center, Center)) { if (player.Bottom < Top && player.Top > Bottom) { player.ExplodeLaunch(Center, false, true); } else { @@ -226,11 +231,5 @@ private void Explode() { Collidable = true; } - - protected class BoomCollider : Entity { - public BoomCollider(Vector2 position) : base(position) { - Collider = new Circle(40f, 0, 0); - } - } } } From 90d6643fe4396260a320d222b097babfaadf642a Mon Sep 17 00:00:00 2001 From: bigkahuna443 <13278973+bigkahuna443@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:02:19 -0500 Subject: [PATCH 3/3] Updated debug render Used the color of the player collider for the blast zone since it makes more sense + we only render it when the boom box is on --- Code/Entities/BoomBox.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Code/Entities/BoomBox.cs b/Code/Entities/BoomBox.cs index 57c5dc5..9be1184 100644 --- a/Code/Entities/BoomBox.cs +++ b/Code/Entities/BoomBox.cs @@ -174,7 +174,9 @@ public override void Update() { public override void DebugRender(Camera camera) { base.DebugRender(camera); - _boomCollider.Render(camera, Collidable ? Color.Red : Color.DarkRed); + if (Activator.IsOn) { + _boomCollider.Render(camera, Color.HotPink); + } } private void HandleAngryMode() {