From b119a11902149ed8d9a1919ce74b69da65ff01fa Mon Sep 17 00:00:00 2001 From: max4805 Date: Sat, 3 Dec 2022 18:27:49 +0100 Subject: [PATCH] Add some lint tests --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .github/workflows/autobuild.yml | 15 + Ahorn/effects/blackholeCustomColors.jl | 28 +- Ahorn/effects/customSnow.jl | 26 +- Ahorn/effects/heatWaveNoColorGrade.jl | 26 +- Ahorn/entities/animatedJumpthruPlatform.jl | 2 +- Ahorn/entities/attachedIceWall.jl | 154 ++-- Ahorn/entities/bubblePushField.jl | 26 +- Ahorn/entities/bubbleReturnBerry.jl | 74 +- Ahorn/entities/cassetteFriendlyStrawberry.jl | 4 +- Ahorn/entities/crystalBombDetonator.jl | 4 +- Ahorn/entities/customBirdTutorial.jl | 2 +- Ahorn/entities/customRespawnTimeRefill.jl | 2 +- Ahorn/entities/customSandwichLava.jl | 76 +- Ahorn/entities/customizableGlassBlock.jl | 58 +- .../customizableGlassBlockController.jl | 44 +- Ahorn/entities/dashSpring.jl | 2 +- Ahorn/entities/diagonalWingedStrawberry.jl | 14 +- Ahorn/entities/flagSwitchGate.jl | 6 +- Ahorn/entities/flagToggleStarRotateSpinner.jl | 2 +- Ahorn/entities/flagToggleStarTrackSpinner.jl | 2 +- Ahorn/entities/flagToggleWater.jl | 2 +- Ahorn/entities/flagToggleWaterfall.jl | 214 ++--- Ahorn/entities/flagTouchSwitch.jl | 6 +- .../entities/foregroundReflectionTentacles.jl | 2 +- Ahorn/entities/glassBerry.jl | 62 +- .../entities/horizontalRoomWrapController.jl | 42 +- Ahorn/entities/invisibleLightSource.jl | 22 +- Ahorn/entities/moveBlockBarrier.jl | 4 +- Ahorn/entities/moveBlockCustomSpeed.jl | 2 +- Ahorn/entities/multiNodeMovingPlatform.jl | 14 +- Ahorn/entities/multiRoomStrawberry.jl | 2 +- Ahorn/entities/multiRoomStrawberrySeed.jl | 10 +- Ahorn/entities/negativeSummitCheckpoint.jl | 2 +- Ahorn/entities/noDashRefillSpring.jl | 2 +- Ahorn/entities/nonBadelineMovingBlock.jl | 4 +- Ahorn/entities/nonCoreModeWallBooster.jl | 2 +- .../rainbowSpinnerColorAreaController.jl | 54 +- .../entities/rainbowSpinnerColorController.jl | 44 +- Ahorn/entities/respawningJellyfish.jl | 2 +- Ahorn/entities/safeRespawnCrumble.jl | 2 +- Ahorn/entities/seekerCustomColors.jl | 4 +- Ahorn/entities/seekerStatueCustomColors.jl | 4 +- Ahorn/entities/sidewaysJumpThru.jl | 14 +- Ahorn/entities/sidewaysLava.jl | 6 +- Ahorn/entities/spikeJumpThruController.jl | 4 +- Ahorn/entities/staticPuffer.jl | 2 +- Ahorn/entities/strawberryIgnoringLighting.jl | 2 +- Ahorn/entities/trollStrawberry.jl | 2 +- Ahorn/entities/underwaterSwitchController.jl | 42 +- Ahorn/entities/upsideDownJumpThru.jl | 6 +- .../badelineBounceDirectionTrigger.jl | 30 +- Ahorn/triggers/cameraCatchupSpeed.jl | 30 +- .../cancelLightningRemoveRoutineTrigger.jl | 28 +- Ahorn/triggers/changeThemeTrigger.jl | 30 +- Ahorn/triggers/colorGradeFadeTrigger.jl | 48 +- Ahorn/triggers/customBirdTutorial.jl | 30 +- Ahorn/triggers/customSandwichLavaSettings.jl | 40 +- Ahorn/triggers/disableIcePhysics.jl | 28 +- .../triggers/flagToggleCameraTargetTrigger.jl | 58 +- .../flagToggleSmoothCameraOffsetTrigger.jl | 42 +- Ahorn/triggers/leaveTheoBehindTrigger.jl | 28 +- Ahorn/triggers/lightningStrikeTrigger.jl | 2 +- Ahorn/triggers/madelineSilhouetteTrigger.jl | 28 +- Ahorn/triggers/musicAreaChangeTrigger.jl | 2 +- Ahorn/triggers/noRefillField.jl | 26 +- Ahorn/triggers/pauseBadelineBossesTrigger.jl | 28 +- Ahorn/triggers/removeLightSourcesTrigger.jl | 2 +- Ahorn/triggers/smoothCameraOffsetTrigger.jl | 40 +- Effects/BlackholeCustomColors.cs | 148 ++-- Effects/CustomSnow.cs | 54 +- Effects/HeatWaveNoColorGrade.cs | 104 +-- Entities/AnimatedJumpthruPlatform.cs | 84 +- Entities/AttachedIceWall.cs | 38 +- Entities/BubblePushField.cs | 8 +- Entities/BubbleReturnBerry.cs | 42 +- Entities/CassetteFriendlyStrawberry.cs | 48 +- Entities/CassetteFriendlyStrawberrySeed.cs | 84 +- Entities/CaveWall.cs | 224 ++--- Entities/CrystalBombDetonator.cs | 2 +- Entities/CrystalBombDetonatorRenderer.cs | 8 +- Entities/CustomBirdTutorial.cs | 240 ++--- Entities/CustomRespawnTimeRefill.cs | 50 +- Entities/CustomSandwichLava.cs | 582 ++++++------ Entities/CustomizableGlassBlock.cs | 168 ++-- Entities/CustomizableGlassBlockController.cs | 564 ++++++------ Entities/DashSpring.cs | 152 ++-- Entities/DiagonalWingedStrawberry.cs | 4 +- Entities/FlagSwitchGate.cs | 484 +++++----- Entities/FlagToggleComponent.cs | 90 +- Entities/FlagToggleStarRotateSpinner.cs | 58 +- Entities/FlagToggleStarTrackSpinner.cs | 58 +- Entities/FlagToggleWater.cs | 72 +- Entities/FlagToggleWaterfall.cs | 168 ++-- Entities/FlagTouchSwitch.cs | 554 ++++++------ Entities/FloatierSpaceBlock.cs | 4 +- Entities/ForegroundReflectionTentacles.cs | 76 +- Entities/GlassBerry.cs | 6 +- Entities/GroupedTriggerSpikes.cs | 638 +++++++------- Entities/HorizontalRoomWrapController.cs | 56 +- Entities/InstantFallingBlock.cs | 24 +- Entities/InvisibleLightSource.cs | 4 +- Entities/LightningDashSwitch.cs | 2 +- Entities/LitBlueTorch.cs | 36 +- Entities/MoveBlockBarrier.cs | 4 +- Entities/MoveBlockBarrierRenderer.cs | 2 +- Entities/MoveBlockCustomSpeed.cs | 106 +-- Entities/MultiNodeMovingPlatform.cs | 486 +++++------ Entities/MultiRoomStrawberry.cs | 144 +-- Entities/MultiRoomStrawberrySeed.cs | 392 ++++----- Entities/NegativeSummitCheckpoint.cs | 44 +- Entities/NoDashRefillSpring.cs | 172 ++-- Entities/NonBadelineMovingBlock.cs | 14 +- Entities/NonCoreModeWallBooster.cs | 26 +- Entities/RainbowSpinnerColorAreaController.cs | 140 +-- Entities/RainbowSpinnerColorController.cs | 258 +++--- Entities/RespawningJellyfish.cs | 214 ++--- Entities/RetractSpinner.cs | 236 ++--- Entities/SafeRespawnCrumble.cs | 72 +- Entities/SeekerCustomColors.cs | 186 ++-- Entities/SeekerStatueCustomColors.cs | 72 +- Entities/SidewaysJumpThru.cs | 682 +++++++-------- Entities/SidewaysLava.cs | 756 ++++++++-------- Entities/SidewaysLavaRect.cs | 576 ++++++------ Entities/SpikeJumpThroughController.cs | 2 +- Entities/StaticPuffer.cs | 96 +- Entities/StrawberryIgnoringLighting.cs | 112 +-- Entities/TrollStrawberry.cs | 160 ++-- Entities/UnderwaterSwitchController.cs | 140 +-- Entities/UpsideDownJumpThru.cs | 826 +++++++++--------- Entities/VariableCrumbleBlock.cs | 112 +-- GrandmasterHeartSideHelper.cs | 330 +++---- Graphics/SpringCollab2020/GlassBerry.xml | 2 +- README.md | 2 +- SpringCollab2020MapDataProcessor.cs | 216 ++--- SpringCollab2020Module.cs | 300 +++---- SpringCollab2020SaveData.cs | 16 +- SpringCollab2020Session.cs | 64 +- Triggers/BadelineBounceDirectionTrigger.cs | 66 +- Triggers/BlockJellySpawnTrigger.cs | 66 +- Triggers/CameraCatchupSpeedTrigger.cs | 112 +-- .../CancelLightningRemoveRoutineTrigger.cs | 58 +- Triggers/ChangeThemeTrigger.cs | 256 +++--- Triggers/ColorGradeFadeTrigger.cs | 112 +-- Triggers/CustomBirdTutorialTrigger.cs | 60 +- Triggers/CustomSandwichLavaSettingsTrigger.cs | 66 +- Triggers/DisableIcePhysicsTrigger.cs | 90 +- Triggers/FlagToggleCameraTargetTrigger.cs | 24 +- .../FlagToggleSmoothCameraOffsetTrigger.cs | 24 +- Triggers/LeaveTheoBehindTrigger.cs | 138 +-- Triggers/LightningStrikeTrigger.cs | 4 +- Triggers/MadelineSilhouetteTrigger.cs | 230 ++--- Triggers/MusicAreaChangeTrigger.cs | 106 +-- Triggers/MusicLayerFadeTrigger.cs | 82 +- Triggers/NoRefillField.cs | 88 +- Triggers/PauseBadelineBossesTrigger.cs | 76 +- Triggers/RemoveLightSourcesTrigger.cs | 164 ++-- Triggers/SmoothCameraOffsetTrigger.cs | 70 +- Triggers/SpeedBasedMusicParamTrigger.cs | 156 ++-- Triggers/StrawberryCollectionField.cs | 132 +-- Triggers/SwapDashTrigger.cs | 58 +- 161 files changed, 7909 insertions(+), 7894 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5fcc48c..b1db409 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -33,8 +33,8 @@ assignees: '' **Information:** - - Operating System: - - Celeste Version: + - Operating System: + - Celeste Version: - Everest Version: diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index be1b0cd..f9c9149 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -8,6 +8,21 @@ jobs: steps: - uses: actions/checkout@v1 + - name: Check for tabs + run: | + find "(" -name "*.cs" -or -name "*.lua" -or -name "*.jl" -or -name "*.xml" -or -name "*.yaml" -or -name "*.yml" -or -name "*.txt" -or -name "*.md" ")" -exec grep -Pl "\t" {} \; > matches.txt + grep "" matches.txt && exit 1 || echo "No tab found!" + + - name: Check for CRLF + run: | + find "(" -name "*.cs" -or -name "*.lua" -or -name "*.jl" -or -name "*.xml" -or -name "*.yaml" -or -name "*.yml" -or -name "*.txt" -or -name "*.md" ")" -exec grep -Plz "\r\n" {} \; > matches.txt + grep "" matches.txt && exit 1 || echo "No CRLF found!" + + - name: Check for trailing spaces + run: | + find "(" -name "*.cs" -or -name "*.lua" -or -name "*.jl" -or -name "*.xml" -or -name "*.yaml" -or -name "*.yml" -or -name "*.txt" -or -name "*.md" ")" -exec grep -Pl " $" {} \; > matches.txt + grep "" matches.txt && exit 1 || echo "No trailing space found!" + - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: diff --git a/Ahorn/effects/blackholeCustomColors.jl b/Ahorn/effects/blackholeCustomColors.jl index fe301ea..8f3c3e1 100755 --- a/Ahorn/effects/blackholeCustomColors.jl +++ b/Ahorn/effects/blackholeCustomColors.jl @@ -1,14 +1,14 @@ -module SpringCollab2020BlackholeCustomColors - -using ..Ahorn, Maple - -@mapdef Effect "SpringCollab2020/BlackholeCustomColors" BlackholeCustomColors(only::String="*", exclude::String="", colorsMild::String="6e3199,851f91,3026b0", colorsWild::String="ca4ca7,b14cca,ca4ca7", - bgColorInner::String="000000", bgColorOuterMild::String="512a8b", bgColorOuterWild::String="bd2192", alpha::Number=1.0) - -placements = BlackholeCustomColors - -function Ahorn.canFgBg(effect::BlackholeCustomColors) - return true, true -end - -end +module SpringCollab2020BlackholeCustomColors + +using ..Ahorn, Maple + +@mapdef Effect "SpringCollab2020/BlackholeCustomColors" BlackholeCustomColors(only::String="*", exclude::String="", colorsMild::String="6e3199,851f91,3026b0", colorsWild::String="ca4ca7,b14cca,ca4ca7", + bgColorInner::String="000000", bgColorOuterMild::String="512a8b", bgColorOuterWild::String="bd2192", alpha::Number=1.0) + +placements = BlackholeCustomColors + +function Ahorn.canFgBg(effect::BlackholeCustomColors) + return true, true +end + +end diff --git a/Ahorn/effects/customSnow.jl b/Ahorn/effects/customSnow.jl index faab5f3..7c91d78 100755 --- a/Ahorn/effects/customSnow.jl +++ b/Ahorn/effects/customSnow.jl @@ -1,13 +1,13 @@ -module SpringCollab2020CustomSnow - -using ..Ahorn, Maple - -@mapdef Effect "SpringCollab2020/CustomSnow" CustomSnow(only::String="*", exclude::String="", colors::String="FFFFFF,FFFFFF") - -placements = CustomSnow - -function Ahorn.canFgBg(effect::CustomSnow) - return true, true -end - -end +module SpringCollab2020CustomSnow + +using ..Ahorn, Maple + +@mapdef Effect "SpringCollab2020/CustomSnow" CustomSnow(only::String="*", exclude::String="", colors::String="FFFFFF,FFFFFF") + +placements = CustomSnow + +function Ahorn.canFgBg(effect::CustomSnow) + return true, true +end + +end diff --git a/Ahorn/effects/heatWaveNoColorGrade.jl b/Ahorn/effects/heatWaveNoColorGrade.jl index da0f4f3..fd1e641 100644 --- a/Ahorn/effects/heatWaveNoColorGrade.jl +++ b/Ahorn/effects/heatWaveNoColorGrade.jl @@ -1,13 +1,13 @@ -module SpringCollab2020HeatWaveNoColorGrade - -using ..Ahorn, Maple - -@mapdef Effect "SpringCollab2020/HeatWaveNoColorGrade" HeatWaveNoColorGrade(only::String="*", exclude::String="") - -placements = HeatWaveNoColorGrade - -function Ahorn.canFgBg(effect::HeatWaveNoColorGrade) - return true, true -end - -end +module SpringCollab2020HeatWaveNoColorGrade + +using ..Ahorn, Maple + +@mapdef Effect "SpringCollab2020/HeatWaveNoColorGrade" HeatWaveNoColorGrade(only::String="*", exclude::String="") + +placements = HeatWaveNoColorGrade + +function Ahorn.canFgBg(effect::HeatWaveNoColorGrade) + return true, true +end + +end diff --git a/Ahorn/entities/animatedJumpthruPlatform.jl b/Ahorn/entities/animatedJumpthruPlatform.jl index 6be5390..53e59bd 100644 --- a/Ahorn/entities/animatedJumpthruPlatform.jl +++ b/Ahorn/entities/animatedJumpthruPlatform.jl @@ -1,4 +1,4 @@ -module SpringCollab2020AnimatedJumpthruPlatform +module SpringCollab2020AnimatedJumpthruPlatform using ..Ahorn, Maple diff --git a/Ahorn/entities/attachedIceWall.jl b/Ahorn/entities/attachedIceWall.jl index a435f71..918e5c5 100755 --- a/Ahorn/entities/attachedIceWall.jl +++ b/Ahorn/entities/attachedIceWall.jl @@ -1,77 +1,77 @@ -module SpringCollab2020AttachedIceWall - -using ..Ahorn, Maple - -@mapdef Entity "SpringCollab2020/AttachedIceWall" AttachedIceWall(x::Integer, y::Integer, height::Integer=8, left::Bool=false) - -const placements = Ahorn.PlacementDict( - "Attached Ice Wall (Right) (Spring Collab 2020)" => Ahorn.EntityPlacement( - AttachedIceWall, - "rectangle", - Dict{String, Any}( - "left" => true - ) - ), - "Attached Ice Wall (Left) (Spring Collab 2020)" => Ahorn.EntityPlacement( - AttachedIceWall, - "rectangle", - Dict{String, Any}( - "left" => false - ) - ) -) - -Ahorn.minimumSize(entity::AttachedIceWall) = 0, 8 -Ahorn.resizable(entity::AttachedIceWall) = false, true - -function Ahorn.selection(entity::AttachedIceWall) - x, y = Ahorn.position(entity) - height = Int(get(entity.data, "height", 8)) - - return Ahorn.Rectangle(x, y, 8, height) -end - -function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::AttachedIceWall, room::Maple.Room) - left = get(entity.data, "left", false) - - # Values need to be system specific integer - x = Int(get(entity.data, "x", 0)) - y = Int(get(entity.data, "y", 0)) - - height = Int(get(entity.data, "height", 8)) - tileHeight = div(height, 8) - - if left - for i in 2:tileHeight - 1 - Ahorn.drawImage(ctx, "objects/wallBooster/iceMid00", 0, (i - 1) * 8) - end - - Ahorn.drawImage(ctx, "objects/wallBooster/iceTop00", 0, 0) - Ahorn.drawImage(ctx, "objects/wallBooster/iceBottom00", 0, (tileHeight - 1) * 8) - - else - Ahorn.Cairo.save(ctx) - Ahorn.scale(ctx, -1, 1) - - for i in 2:tileHeight - 1 - Ahorn.drawImage(ctx, "objects/wallBooster/iceMid00", -8, (i - 1) * 8) - end - - Ahorn.drawImage(ctx, "objects/wallBooster/iceTop00", -8, 0) - Ahorn.drawImage(ctx, "objects/wallBooster/iceBottom00", -8, (tileHeight - 1) * 8) - - Ahorn.restore(ctx) - end -end - -# Offset X position so it flips in place -function Ahorn.flipped(entity::AttachedIceWall, horizontal::Bool) - if horizontal - entity.left = !entity.left - entity.x += entity.left ? 8 : -8 - - return entity - end -end - -end +module SpringCollab2020AttachedIceWall + +using ..Ahorn, Maple + +@mapdef Entity "SpringCollab2020/AttachedIceWall" AttachedIceWall(x::Integer, y::Integer, height::Integer=8, left::Bool=false) + +const placements = Ahorn.PlacementDict( + "Attached Ice Wall (Right) (Spring Collab 2020)" => Ahorn.EntityPlacement( + AttachedIceWall, + "rectangle", + Dict{String, Any}( + "left" => true + ) + ), + "Attached Ice Wall (Left) (Spring Collab 2020)" => Ahorn.EntityPlacement( + AttachedIceWall, + "rectangle", + Dict{String, Any}( + "left" => false + ) + ) +) + +Ahorn.minimumSize(entity::AttachedIceWall) = 0, 8 +Ahorn.resizable(entity::AttachedIceWall) = false, true + +function Ahorn.selection(entity::AttachedIceWall) + x, y = Ahorn.position(entity) + height = Int(get(entity.data, "height", 8)) + + return Ahorn.Rectangle(x, y, 8, height) +end + +function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::AttachedIceWall, room::Maple.Room) + left = get(entity.data, "left", false) + + # Values need to be system specific integer + x = Int(get(entity.data, "x", 0)) + y = Int(get(entity.data, "y", 0)) + + height = Int(get(entity.data, "height", 8)) + tileHeight = div(height, 8) + + if left + for i in 2:tileHeight - 1 + Ahorn.drawImage(ctx, "objects/wallBooster/iceMid00", 0, (i - 1) * 8) + end + + Ahorn.drawImage(ctx, "objects/wallBooster/iceTop00", 0, 0) + Ahorn.drawImage(ctx, "objects/wallBooster/iceBottom00", 0, (tileHeight - 1) * 8) + + else + Ahorn.Cairo.save(ctx) + Ahorn.scale(ctx, -1, 1) + + for i in 2:tileHeight - 1 + Ahorn.drawImage(ctx, "objects/wallBooster/iceMid00", -8, (i - 1) * 8) + end + + Ahorn.drawImage(ctx, "objects/wallBooster/iceTop00", -8, 0) + Ahorn.drawImage(ctx, "objects/wallBooster/iceBottom00", -8, (tileHeight - 1) * 8) + + Ahorn.restore(ctx) + end +end + +# Offset X position so it flips in place +function Ahorn.flipped(entity::AttachedIceWall, horizontal::Bool) + if horizontal + entity.left = !entity.left + entity.x += entity.left ? 8 : -8 + + return entity + end +end + +end diff --git a/Ahorn/entities/bubblePushField.jl b/Ahorn/entities/bubblePushField.jl index fbc91af..8e14fa5 100644 --- a/Ahorn/entities/bubblePushField.jl +++ b/Ahorn/entities/bubblePushField.jl @@ -1,20 +1,20 @@ -module SpringCollab2020BubblePushField +module SpringCollab2020BubblePushField using ..Ahorn, Maple @mapdef Entity "SpringCollab2020/bubblePushField" BubblePushField(x::Integer, y::Integer, width::Integer=Maple.defaultBlockWidth, height::Integer=Maple.defaultBlockHeight, - strength::Number=1.0, upwardStrength::Number=1.0, direction::String="Right", water::Bool=true, flag::String="bubble_push_field", activationMode::String="Always") + strength::Number=1.0, upwardStrength::Number=1.0, direction::String="Right", water::Bool=true, flag::String="bubble_push_field", activationMode::String="Always") const placements = Ahorn.PlacementDict( - "Bubble Column (Spring Collab 2020)" => Ahorn.EntityPlacement( - BubblePushField, - "rectangle" - ) + "Bubble Column (Spring Collab 2020)" => Ahorn.EntityPlacement( + BubblePushField, + "rectangle" + ) ) Ahorn.editingOptions(entity::BubblePushField) = Dict{String,Any}( - "direction" => ["Up", "Down", "Left", "Right"], - "activationMode" => ["Always", "OnlyWhenFlagActive", "OnlyWhenFlagInactive"] + "direction" => ["Up", "Down", "Left", "Right"], + "activationMode" => ["Always", "OnlyWhenFlagActive", "OnlyWhenFlagInactive"] ) Ahorn.minimumSize(entity::BubblePushField) = 8, 8 @@ -23,13 +23,13 @@ Ahorn.resizable(entity::BubblePushField) = true, true Ahorn.selection(entity::BubblePushField) = Ahorn.getEntityRectangle(entity) function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::BubblePushField, room::Maple.Room) - x = Int(get(entity.data, "x", 0)) - y = Int(get(entity.data, "y", 0)) + x = Int(get(entity.data, "x", 0)) + y = Int(get(entity.data, "y", 0)) - width = Int(get(entity.data, "width", 32)) - height = Int(get(entity.data, "height", 32)) + width = Int(get(entity.data, "width", 32)) + height = Int(get(entity.data, "height", 32)) - Ahorn.drawRectangle(ctx, 0, 0, width, height, (0.7, 0.28, 0.0, 0.34), (1.0, 1.0, 1.0, 0.5)) + Ahorn.drawRectangle(ctx, 0, 0, width, height, (0.7, 0.28, 0.0, 0.34), (1.0, 1.0, 1.0, 0.5)) end end diff --git a/Ahorn/entities/bubbleReturnBerry.jl b/Ahorn/entities/bubbleReturnBerry.jl index 0c920c0..40f86e5 100644 --- a/Ahorn/entities/bubbleReturnBerry.jl +++ b/Ahorn/entities/bubbleReturnBerry.jl @@ -1,21 +1,21 @@ -module SpringCollab2020BubbleReturnBerry +module SpringCollab2020BubbleReturnBerry using ..Ahorn, Maple @mapdef Entity "SpringCollab2020/returnBerry" ReturnBerry(x::Integer, y::Integer, order::Integer=-1, checkpointID::Integer=-1, winged::Bool=false, - nodes::Array{Tuple{Integer, Integer}, 1}=Tuple{Integer, Integer}[], SpringCollab2020_ignoreLighting::Bool=false) + nodes::Array{Tuple{Integer, Integer}, 1}=Tuple{Integer, Integer}[], SpringCollab2020_ignoreLighting::Bool=false) const placements = Ahorn.PlacementDict( - "Strawberry (With Return) (Spring Collab 2020)" => Ahorn.EntityPlacement( - ReturnBerry - ), - "Strawberry (Winged, With Return) (Spring Collab 2020)" => Ahorn.EntityPlacement( - ReturnBerry, - "point", - Dict{String,Any}( - "winged" => true - ) - ) + "Strawberry (With Return) (Spring Collab 2020)" => Ahorn.EntityPlacement( + ReturnBerry + ), + "Strawberry (Winged, With Return) (Spring Collab 2020)" => Ahorn.EntityPlacement( + ReturnBerry, + "point", + Dict{String,Any}( + "winged" => true + ) + ) ) seedSprite = "collectables/strawberry/seed00" @@ -23,50 +23,50 @@ seedSprite = "collectables/strawberry/seed00" Ahorn.nodeLimits(entity::ReturnBerry) = 0, -1 function getSpriteName(entity::ReturnBerry) - winged = get(entity.data, "winged", false) + winged = get(entity.data, "winged", false) - if winged - return "collectables/strawberry/wings01" - end + if winged + return "collectables/strawberry/wings01" + end - return "collectables/strawberry/normal00" + return "collectables/strawberry/normal00" end function Ahorn.selection(entity::ReturnBerry) - x, y = Ahorn.position(entity) - nodes = get(entity.data, "nodes", ()) + x, y = Ahorn.position(entity) + nodes = get(entity.data, "nodes", ()) - res = Ahorn.Rectangle[Ahorn.getSpriteRectangle(getSpriteName(entity), x, y)] + res = Ahorn.Rectangle[Ahorn.getSpriteRectangle(getSpriteName(entity), x, y)] - for node in nodes - nx, ny = node - push!(res, Ahorn.getSpriteRectangle(seedSprite, nx, ny)) - end + for node in nodes + nx, ny = node + push!(res, Ahorn.getSpriteRectangle(seedSprite, nx, ny)) + end - return res + return res end function Ahorn.renderSelectedAbs(ctx::Ahorn.Cairo.CairoContext, entity::ReturnBerry) - x, y = Ahorn.position(entity) + x, y = Ahorn.position(entity) - for node in get(entity.data, "nodes", ()) - nx, ny = node + for node in get(entity.data, "nodes", ()) + nx, ny = node - Ahorn.drawLines(ctx, Tuple{Number, Number}[(x, y), (nx, ny)], Ahorn.colors.selection_selected_fc) - end + Ahorn.drawLines(ctx, Tuple{Number, Number}[(x, y), (nx, ny)], Ahorn.colors.selection_selected_fc) + end end function Ahorn.renderAbs(ctx::Ahorn.Cairo.CairoContext, entity::ReturnBerry, room::Maple.Room) - x, y = Ahorn.position(entity) - nodes = get(entity.data, "nodes", ()) + x, y = Ahorn.position(entity) + nodes = get(entity.data, "nodes", ()) - for node in nodes - nx, ny = node + for node in nodes + nx, ny = node - Ahorn.drawSprite(ctx, seedSprite, nx, ny) - end + Ahorn.drawSprite(ctx, seedSprite, nx, ny) + end - Ahorn.drawSprite(ctx, getSpriteName(entity), x, y) + Ahorn.drawSprite(ctx, getSpriteName(entity), x, y) end end diff --git a/Ahorn/entities/cassetteFriendlyStrawberry.jl b/Ahorn/entities/cassetteFriendlyStrawberry.jl index ae6d9ec..c430ed6 100644 --- a/Ahorn/entities/cassetteFriendlyStrawberry.jl +++ b/Ahorn/entities/cassetteFriendlyStrawberry.jl @@ -1,4 +1,4 @@ -module SpringCollab2020CassetteFriendlyStrawberry +module SpringCollab2020CassetteFriendlyStrawberry using ..Ahorn, Maple @@ -39,7 +39,7 @@ function Ahorn.selection(entity::CassetteFriendlyStrawberry) sprite = sprites[(winged, hasPips, moon)] res = Ahorn.Rectangle[Ahorn.getSpriteRectangle(sprite, x, y)] - + for node in nodes nx, ny = node diff --git a/Ahorn/entities/crystalBombDetonator.jl b/Ahorn/entities/crystalBombDetonator.jl index 88c3ba4..631db60 100644 --- a/Ahorn/entities/crystalBombDetonator.jl +++ b/Ahorn/entities/crystalBombDetonator.jl @@ -1,4 +1,4 @@ -module SpringCollab2020CrystalBombDetonator +module SpringCollab2020CrystalBombDetonator using ..Ahorn, Maple @@ -26,7 +26,7 @@ end function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::CrystalBombDetonator, room::Maple.Room) width = Int(get(entity.data, "width", 32)) height = Int(get(entity.data, "height", 32)) - + Ahorn.drawRectangle(ctx, 0, 0, width, height, (0.45, 0.0, 0.45, 0.8), (0.0, 0.0, 0.0, 0.0)) end diff --git a/Ahorn/entities/customBirdTutorial.jl b/Ahorn/entities/customBirdTutorial.jl index c65b2e0..01d435c 100644 --- a/Ahorn/entities/customBirdTutorial.jl +++ b/Ahorn/entities/customBirdTutorial.jl @@ -26,7 +26,7 @@ end function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::CustomBirdTutorial, room::Maple.Room) scaleX = get(entity.data, "faceLeft", true) ? -1 : 1 - + Ahorn.drawSprite(ctx, sprite, 0, 0, sx=scaleX, jx=0.5, jy=1.0) end diff --git a/Ahorn/entities/customRespawnTimeRefill.jl b/Ahorn/entities/customRespawnTimeRefill.jl index 8a8a67c..efa6f5f 100644 --- a/Ahorn/entities/customRespawnTimeRefill.jl +++ b/Ahorn/entities/customRespawnTimeRefill.jl @@ -1,4 +1,4 @@ -module SpringCollab2020CustomRespawnTimeRefill +module SpringCollab2020CustomRespawnTimeRefill using ..Ahorn, Maple diff --git a/Ahorn/entities/customSandwichLava.jl b/Ahorn/entities/customSandwichLava.jl index b8d2a83..f6802ba 100644 --- a/Ahorn/entities/customSandwichLava.jl +++ b/Ahorn/entities/customSandwichLava.jl @@ -1,38 +1,38 @@ -module SpringCollab2020CustomSandwichLava - -using ..Ahorn, Maple - -@mapdef Entity "SpringCollab2020/CustomSandwichLava" CustomSandwichLava(x::Integer, y::Integer, - direction::String="CoreModeBased", speed::Number=20.0, sandwichGap::Number=160.0) - -const directions = String["AlwaysUp", "AlwaysDown", "CoreModeBased"] - -const placements = Ahorn.PlacementDict( - "Custom Sandwich Lava (Spring Collab 2020)" => Ahorn.EntityPlacement( - CustomSandwichLava - ) -) - -Ahorn.editingOptions(entity::CustomSandwichLava) = Dict{String, Any}( - "direction" => directions -) - -function Ahorn.selection(entity::CustomSandwichLava) - x, y = Ahorn.position(entity) - - return Ahorn.Rectangle(x - 12, y - 12, 24, 24) -end - -function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::CustomSandwichLava, room::Maple.Room) - direction = get(entity.data, "direction", "CoreModeBased") - - if direction == "AlwaysUp" - Ahorn.drawSprite(ctx, "ahorn/SpringCollab2020/lava_sandwich_up", 0, 0) - elseif direction == "AlwaysDown" - Ahorn.drawSprite(ctx, "ahorn/SpringCollab2020/lava_sandwich_down", 0, 0) - else - Ahorn.drawImage(ctx, Ahorn.Assets.lavaSanwitch, -12, -12) - end -end - -end +module SpringCollab2020CustomSandwichLava + +using ..Ahorn, Maple + +@mapdef Entity "SpringCollab2020/CustomSandwichLava" CustomSandwichLava(x::Integer, y::Integer, + direction::String="CoreModeBased", speed::Number=20.0, sandwichGap::Number=160.0) + +const directions = String["AlwaysUp", "AlwaysDown", "CoreModeBased"] + +const placements = Ahorn.PlacementDict( + "Custom Sandwich Lava (Spring Collab 2020)" => Ahorn.EntityPlacement( + CustomSandwichLava + ) +) + +Ahorn.editingOptions(entity::CustomSandwichLava) = Dict{String, Any}( + "direction" => directions +) + +function Ahorn.selection(entity::CustomSandwichLava) + x, y = Ahorn.position(entity) + + return Ahorn.Rectangle(x - 12, y - 12, 24, 24) +end + +function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::CustomSandwichLava, room::Maple.Room) + direction = get(entity.data, "direction", "CoreModeBased") + + if direction == "AlwaysUp" + Ahorn.drawSprite(ctx, "ahorn/SpringCollab2020/lava_sandwich_up", 0, 0) + elseif direction == "AlwaysDown" + Ahorn.drawSprite(ctx, "ahorn/SpringCollab2020/lava_sandwich_down", 0, 0) + else + Ahorn.drawImage(ctx, Ahorn.Assets.lavaSanwitch, -12, -12) + end +end + +end diff --git a/Ahorn/entities/customizableGlassBlock.jl b/Ahorn/entities/customizableGlassBlock.jl index 37faec3..071cc9d 100644 --- a/Ahorn/entities/customizableGlassBlock.jl +++ b/Ahorn/entities/customizableGlassBlock.jl @@ -1,29 +1,29 @@ -module SpringCollab2020CustomizableGlassBlock - -using ..Ahorn, Maple - -@mapdef Entity "SpringCollab2020/CustomizableGlassBlock" CustomizableGlassBlock(x::Integer, y::Integer, width::Integer=Maple.defaultBlockWidth, height::Integer=Maple.defaultBlockHeight, behindFgTiles::Bool=false) - -const placements = Ahorn.PlacementDict( - "Customizable Glass Block (Spring Collab 2020)" => Ahorn.EntityPlacement( - CustomizableGlassBlock, - "rectangle" - ) -) - -Ahorn.minimumSize(entity::CustomizableGlassBlock) = 8, 8 -Ahorn.resizable(entity::CustomizableGlassBlock) = true, true - -Ahorn.selection(entity::CustomizableGlassBlock) = Ahorn.getEntityRectangle(entity) - -function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::CustomizableGlassBlock, room::Maple.Room) - x = Int(get(entity.data, "x", 0)) - y = Int(get(entity.data, "y", 0)) - - width = Int(get(entity.data, "width", 32)) - height = Int(get(entity.data, "height", 32)) - - Ahorn.drawRectangle(ctx, 0, 0, width, height, (1.0, 1.0, 1.0, 0.5), (1.0, 1.0, 1.0, 0.5)) -end - -end +module SpringCollab2020CustomizableGlassBlock + +using ..Ahorn, Maple + +@mapdef Entity "SpringCollab2020/CustomizableGlassBlock" CustomizableGlassBlock(x::Integer, y::Integer, width::Integer=Maple.defaultBlockWidth, height::Integer=Maple.defaultBlockHeight, behindFgTiles::Bool=false) + +const placements = Ahorn.PlacementDict( + "Customizable Glass Block (Spring Collab 2020)" => Ahorn.EntityPlacement( + CustomizableGlassBlock, + "rectangle" + ) +) + +Ahorn.minimumSize(entity::CustomizableGlassBlock) = 8, 8 +Ahorn.resizable(entity::CustomizableGlassBlock) = true, true + +Ahorn.selection(entity::CustomizableGlassBlock) = Ahorn.getEntityRectangle(entity) + +function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::CustomizableGlassBlock, room::Maple.Room) + x = Int(get(entity.data, "x", 0)) + y = Int(get(entity.data, "y", 0)) + + width = Int(get(entity.data, "width", 32)) + height = Int(get(entity.data, "height", 32)) + + Ahorn.drawRectangle(ctx, 0, 0, width, height, (1.0, 1.0, 1.0, 0.5), (1.0, 1.0, 1.0, 0.5)) +end + +end diff --git a/Ahorn/entities/customizableGlassBlockController.jl b/Ahorn/entities/customizableGlassBlockController.jl index 2226a5e..fcabac1 100644 --- a/Ahorn/entities/customizableGlassBlockController.jl +++ b/Ahorn/entities/customizableGlassBlockController.jl @@ -1,22 +1,22 @@ -module SpringCollab2020CustomizableGlassBlockController - -using ..Ahorn, Maple - -@mapdef Entity "SpringCollab2020/CustomizableGlassBlockController" CustomizableGlassBlockController(x::Integer, y::Integer, - starColors::String="ff7777,77ff77,7777ff,ff77ff,77ffff,ffff77", bgColor::String="302040") - -const placements = Ahorn.PlacementDict( - "Customizable Glass Block Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( - CustomizableGlassBlockController - ) -) - -function Ahorn.selection(entity::CustomizableGlassBlockController) - x, y = Ahorn.position(entity) - - return Ahorn.Rectangle(x - 12, y - 12, 24, 24) -end - -Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::CustomizableGlassBlockController, room::Maple.Room) = Ahorn.drawImage(ctx, Ahorn.Assets.northernLights, -12, -12) - -end +module SpringCollab2020CustomizableGlassBlockController + +using ..Ahorn, Maple + +@mapdef Entity "SpringCollab2020/CustomizableGlassBlockController" CustomizableGlassBlockController(x::Integer, y::Integer, + starColors::String="ff7777,77ff77,7777ff,ff77ff,77ffff,ffff77", bgColor::String="302040") + +const placements = Ahorn.PlacementDict( + "Customizable Glass Block Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( + CustomizableGlassBlockController + ) +) + +function Ahorn.selection(entity::CustomizableGlassBlockController) + x, y = Ahorn.position(entity) + + return Ahorn.Rectangle(x - 12, y - 12, 24, 24) +end + +Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::CustomizableGlassBlockController, room::Maple.Room) = Ahorn.drawImage(ctx, Ahorn.Assets.northernLights, -12, -12) + +end diff --git a/Ahorn/entities/dashSpring.jl b/Ahorn/entities/dashSpring.jl index bba860d..7aad8f3 100644 --- a/Ahorn/entities/dashSpring.jl +++ b/Ahorn/entities/dashSpring.jl @@ -1,4 +1,4 @@ -module SpringCollab2020DashSpring +module SpringCollab2020DashSpring using ..Ahorn, Maple diff --git a/Ahorn/entities/diagonalWingedStrawberry.jl b/Ahorn/entities/diagonalWingedStrawberry.jl index f59a275..bb657c9 100644 --- a/Ahorn/entities/diagonalWingedStrawberry.jl +++ b/Ahorn/entities/diagonalWingedStrawberry.jl @@ -5,21 +5,21 @@ using ..Ahorn, Maple @mapdef Entity "SpringCollab2020/diagonalWingedStrawberry" DiagonalWingedStrawberry(x::Integer, y::Integer, order::Integer=-1, checkpointID::Integer=-1) const placements = Ahorn.PlacementDict( - "Diagonal Winged Strawberry (Spring Collab 2020)" => Ahorn.EntityPlacement( - DiagonalWingedStrawberry - ) + "Diagonal Winged Strawberry (Spring Collab 2020)" => Ahorn.EntityPlacement( + DiagonalWingedStrawberry + ) ) function Ahorn.selection(entity::DiagonalWingedStrawberry) - x, y = Ahorn.position(entity) + x, y = Ahorn.position(entity) - return Ahorn.getSpriteRectangle("collectables/strawberry/wings01", x, y) + return Ahorn.getSpriteRectangle("collectables/strawberry/wings01", x, y) end function Ahorn.renderAbs(ctx::Ahorn.Cairo.CairoContext, entity::DiagonalWingedStrawberry, room::Maple.Room) - x, y = Ahorn.position(entity) + x, y = Ahorn.position(entity) - Ahorn.drawSprite(ctx, "collectables/strawberry/wings01", x, y) + Ahorn.drawSprite(ctx, "collectables/strawberry/wings01", x, y) end end diff --git a/Ahorn/entities/flagSwitchGate.jl b/Ahorn/entities/flagSwitchGate.jl index 3a3cc83..c45ad5a 100644 --- a/Ahorn/entities/flagSwitchGate.jl +++ b/Ahorn/entities/flagSwitchGate.jl @@ -1,4 +1,4 @@ -module SpringCollab2020FlagSwitchGate +module SpringCollab2020FlagSwitchGate using ..Ahorn, Maple @@ -52,14 +52,14 @@ end function renderGateSwitch(ctx::Ahorn.Cairo.CairoContext, entity::FlagSwitchGate, x::Number, y::Number, width::Number, height::Number, sprite::String) icon = get(entity.data, "icon", "vanilla") - + iconResource = "objects/switchgate/icon00" if icon != "vanilla" iconResource = "objects/SpringCollab2020/flagSwitchGate/$(icon)/icon00" end iconSprite = Ahorn.getSprite(iconResource, "Gameplay") - + tilesWidth = div(width, 8) tilesHeight = div(height, 8) diff --git a/Ahorn/entities/flagToggleStarRotateSpinner.jl b/Ahorn/entities/flagToggleStarRotateSpinner.jl index 5ce664d..d524fe6 100644 --- a/Ahorn/entities/flagToggleStarRotateSpinner.jl +++ b/Ahorn/entities/flagToggleStarRotateSpinner.jl @@ -1,4 +1,4 @@ -module SpringCollab2020FlagToggleStarRotateSpinner +module SpringCollab2020FlagToggleStarRotateSpinner using ..Ahorn, Maple diff --git a/Ahorn/entities/flagToggleStarTrackSpinner.jl b/Ahorn/entities/flagToggleStarTrackSpinner.jl index 7dc48fd..ece0c8b 100644 --- a/Ahorn/entities/flagToggleStarTrackSpinner.jl +++ b/Ahorn/entities/flagToggleStarTrackSpinner.jl @@ -1,4 +1,4 @@ -module SpringCollab2020FlagToggleStarTrackSpinner +module SpringCollab2020FlagToggleStarTrackSpinner using ..Ahorn, Maple diff --git a/Ahorn/entities/flagToggleWater.jl b/Ahorn/entities/flagToggleWater.jl index d22d336..6e481ba 100644 --- a/Ahorn/entities/flagToggleWater.jl +++ b/Ahorn/entities/flagToggleWater.jl @@ -1,4 +1,4 @@ -module SpringCollab2020FlagToggleWater +module SpringCollab2020FlagToggleWater using ..Ahorn, Maple diff --git a/Ahorn/entities/flagToggleWaterfall.jl b/Ahorn/entities/flagToggleWaterfall.jl index 94e23ab..8a935a8 100644 --- a/Ahorn/entities/flagToggleWaterfall.jl +++ b/Ahorn/entities/flagToggleWaterfall.jl @@ -1,108 +1,108 @@ -module SpringCollab2020FlagToggleWaterfall - -using ..Ahorn, Maple - -@mapdef Entity "SpringCollab2020/FlagToggleWaterfall" FlagToggleWaterfall(x::Integer, y::Integer, flag::String="flag_toggle_waterfall", inverted::Bool=false) - -const fillColor = Ahorn.XNAColors.LightBlue .* 0.3 -const surfaceColor = Ahorn.XNAColors.LightBlue .* 0.8 - -const waterSegmentMatrix = [ - 1 0 0 0 0 0 0 1 1 1; - 1 0 0 0 0 0 0 1 1 1; - 1 0 0 0 0 0 0 1 1 1; - 1 0 0 0 0 0 0 1 1 1; - 1 0 0 0 0 0 0 1 1 1; - 1 0 0 0 0 0 0 1 1 1; - 1 0 0 0 0 0 0 1 1 1; - 1 0 0 0 0 0 0 1 1 1; - 1 1 0 0 0 0 0 0 1 1; - 1 1 0 0 0 0 0 0 1 1; - 1 1 1 0 0 0 0 0 0 1; - 1 1 1 0 0 0 0 0 0 1; - 1 1 1 0 0 0 0 0 0 1; - 1 1 1 0 0 0 0 0 0 1; - 1 1 1 0 0 0 0 0 0 1; - 1 1 1 0 0 0 0 0 0 1; - 1 1 1 0 0 0 0 0 0 1; - 1 1 1 0 0 0 0 0 0 1; - 1 1 0 0 0 0 0 0 1 1; - 1 1 0 0 0 0 0 0 1 1; - 1 1 0 0 0 0 0 0 1 1; -] - -const waterSegment = Ahorn.matrixToSurface( - waterSegmentMatrix, - [ - fillColor, - surfaceColor - ] -) - -function getHeight(entity::FlagToggleWaterfall, room::Maple.Room) - waterEntities = filter(e -> e.name == "water" || e.name == "SpringCollab2020/FlagToggleWater", room.entities) - waterRects = Ahorn.Rectangle[ - Ahorn.Rectangle( - Int(get(e.data, "x", 0)), - Int(get(e.data, "y", 0)), - Int(get(e.data, "width", 8)), - Int(get(e.data, "height", 8)) - ) for e in waterEntities - ] - - width, height = room.size - x, y = Int(get(entity.data, "x", 0)), Int(get(entity.data, "y", 0)) - tx, ty = floor(Int, x / 8) + 1, floor(Int, y / 8) + 1 - - wantedHeight = 8 - y % 8 - while wantedHeight < height - y - rect = Ahorn.Rectangle(x, y + wantedHeight, 8, 8) - - if any(Ahorn.checkCollision.(waterRects, Ref(rect))) - break - end - - if get(room.fgTiles.data, (ty + 1, tx), '0') != '0' - break - end - - wantedHeight += 8 - ty += 1 - end - - return wantedHeight -end - -const placements = Ahorn.PlacementDict( - "Flag Toggle Waterfall (Spring Collab 2020)" => Ahorn.EntityPlacement( - FlagToggleWaterfall - ) -) - -function Ahorn.selection(entity::FlagToggleWaterfall, room::Maple.Room) - x, y = Ahorn.position(entity) - height = getHeight(entity, room) - - return Ahorn.Rectangle(x, y, size(waterSegmentMatrix, 2), height) -end - -function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::FlagToggleWaterfall, room::Maple.Room) - x = Int(get(entity.data, "x", 0)) - y = Int(get(entity.data, "y", 0)) - - height = getHeight(entity, room) - segmentHeight, segmentWidth = size(waterSegmentMatrix) - - Ahorn.Cairo.save(ctx) - - Ahorn.rectangle(ctx, 0, 0, segmentWidth, height) - Ahorn.clip(ctx) - - for i in 0:segmentHeight:ceil(Int, height / segmentHeight) * segmentHeight - Ahorn.drawImage(ctx, waterSegment, 0, i) - end - - Ahorn.restore(ctx) -end - +module SpringCollab2020FlagToggleWaterfall + +using ..Ahorn, Maple + +@mapdef Entity "SpringCollab2020/FlagToggleWaterfall" FlagToggleWaterfall(x::Integer, y::Integer, flag::String="flag_toggle_waterfall", inverted::Bool=false) + +const fillColor = Ahorn.XNAColors.LightBlue .* 0.3 +const surfaceColor = Ahorn.XNAColors.LightBlue .* 0.8 + +const waterSegmentMatrix = [ + 1 0 0 0 0 0 0 1 1 1; + 1 0 0 0 0 0 0 1 1 1; + 1 0 0 0 0 0 0 1 1 1; + 1 0 0 0 0 0 0 1 1 1; + 1 0 0 0 0 0 0 1 1 1; + 1 0 0 0 0 0 0 1 1 1; + 1 0 0 0 0 0 0 1 1 1; + 1 0 0 0 0 0 0 1 1 1; + 1 1 0 0 0 0 0 0 1 1; + 1 1 0 0 0 0 0 0 1 1; + 1 1 1 0 0 0 0 0 0 1; + 1 1 1 0 0 0 0 0 0 1; + 1 1 1 0 0 0 0 0 0 1; + 1 1 1 0 0 0 0 0 0 1; + 1 1 1 0 0 0 0 0 0 1; + 1 1 1 0 0 0 0 0 0 1; + 1 1 1 0 0 0 0 0 0 1; + 1 1 1 0 0 0 0 0 0 1; + 1 1 0 0 0 0 0 0 1 1; + 1 1 0 0 0 0 0 0 1 1; + 1 1 0 0 0 0 0 0 1 1; +] + +const waterSegment = Ahorn.matrixToSurface( + waterSegmentMatrix, + [ + fillColor, + surfaceColor + ] +) + +function getHeight(entity::FlagToggleWaterfall, room::Maple.Room) + waterEntities = filter(e -> e.name == "water" || e.name == "SpringCollab2020/FlagToggleWater", room.entities) + waterRects = Ahorn.Rectangle[ + Ahorn.Rectangle( + Int(get(e.data, "x", 0)), + Int(get(e.data, "y", 0)), + Int(get(e.data, "width", 8)), + Int(get(e.data, "height", 8)) + ) for e in waterEntities + ] + + width, height = room.size + x, y = Int(get(entity.data, "x", 0)), Int(get(entity.data, "y", 0)) + tx, ty = floor(Int, x / 8) + 1, floor(Int, y / 8) + 1 + + wantedHeight = 8 - y % 8 + while wantedHeight < height - y + rect = Ahorn.Rectangle(x, y + wantedHeight, 8, 8) + + if any(Ahorn.checkCollision.(waterRects, Ref(rect))) + break + end + + if get(room.fgTiles.data, (ty + 1, tx), '0') != '0' + break + end + + wantedHeight += 8 + ty += 1 + end + + return wantedHeight +end + +const placements = Ahorn.PlacementDict( + "Flag Toggle Waterfall (Spring Collab 2020)" => Ahorn.EntityPlacement( + FlagToggleWaterfall + ) +) + +function Ahorn.selection(entity::FlagToggleWaterfall, room::Maple.Room) + x, y = Ahorn.position(entity) + height = getHeight(entity, room) + + return Ahorn.Rectangle(x, y, size(waterSegmentMatrix, 2), height) +end + +function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::FlagToggleWaterfall, room::Maple.Room) + x = Int(get(entity.data, "x", 0)) + y = Int(get(entity.data, "y", 0)) + + height = getHeight(entity, room) + segmentHeight, segmentWidth = size(waterSegmentMatrix) + + Ahorn.Cairo.save(ctx) + + Ahorn.rectangle(ctx, 0, 0, segmentWidth, height) + Ahorn.clip(ctx) + + for i in 0:segmentHeight:ceil(Int, height / segmentHeight) * segmentHeight + Ahorn.drawImage(ctx, waterSegment, 0, i) + end + + Ahorn.restore(ctx) +end + end \ No newline at end of file diff --git a/Ahorn/entities/flagTouchSwitch.jl b/Ahorn/entities/flagTouchSwitch.jl index 2fae51e..e960456 100644 --- a/Ahorn/entities/flagTouchSwitch.jl +++ b/Ahorn/entities/flagTouchSwitch.jl @@ -1,8 +1,8 @@ -module SpringCollab2020FlagTouchSwitch +module SpringCollab2020FlagTouchSwitch using ..Ahorn, Maple -@mapdef Entity "SpringCollab2020/FlagTouchSwitch" FlagTouchSwitch(x::Integer, y::Integer, +@mapdef Entity "SpringCollab2020/FlagTouchSwitch" FlagTouchSwitch(x::Integer, y::Integer, flag::String="flag_touch_switch", icon::String="vanilla", persistent::Bool=false, inactiveColor::String="5FCDE4", activeColor::String="FFFFFF", finishColor::String="F141DF") const bundledIcons = String["vanilla", "tall", "triangle", "circle"] @@ -27,7 +27,7 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::FlagTouchSwitch, ro Ahorn.drawSprite(ctx, "objects/touchswitch/container.png", 0, 0) icon = get(entity.data, "icon", "vanilla") - + iconPath = "objects/touchswitch/icon00.png" if icon != "vanilla" iconPath = "objects/SpringCollab2020/flagTouchSwitch/$(icon)/icon00.png" diff --git a/Ahorn/entities/foregroundReflectionTentacles.jl b/Ahorn/entities/foregroundReflectionTentacles.jl index 09adb1f..fe9aeb5 100644 --- a/Ahorn/entities/foregroundReflectionTentacles.jl +++ b/Ahorn/entities/foregroundReflectionTentacles.jl @@ -35,7 +35,7 @@ function Ahorn.selection(entity::ForegroundReflectionTentacles) x, y = Ahorn.position(entity) res = Ahorn.Rectangle[Ahorn.Rectangle(x - 12, y - 12, 24, 24)] - + for node in nodes nx, ny = Int.(node) diff --git a/Ahorn/entities/glassBerry.jl b/Ahorn/entities/glassBerry.jl index 827b3b2..1778d18 100644 --- a/Ahorn/entities/glassBerry.jl +++ b/Ahorn/entities/glassBerry.jl @@ -1,64 +1,64 @@ -module SpringCollab2020GlassBerryModule +module SpringCollab2020GlassBerryModule using ..Ahorn, Maple @mapdef Entity "SpringCollab2020/glassBerry" GlassBerry(x::Integer, y::Integer, checkpointID::Integer=-1, order::Integer=-1, nodes::Array{Tuple{Integer, Integer}, 1}=Tuple{Integer, Integer}[]) const placements = Ahorn.PlacementDict( - "Glass Strawberry (Spring Collab 2020)" => Ahorn.EntityPlacement( - GlassBerry, - "point" - ), + "Glass Strawberry (Spring Collab 2020)" => Ahorn.EntityPlacement( + GlassBerry, + "point" + ), ) Ahorn.nodeLimits(entity::GlassBerry) = 0, -1 function Ahorn.selection(entity::GlassBerry) - x, y = Ahorn.position(entity) + x, y = Ahorn.position(entity) - nodes = get(entity.data, "nodes", ()) - hasPips = length(nodes) > 0 + nodes = get(entity.data, "nodes", ()) + hasPips = length(nodes) > 0 - sprite = "collectables/SpringCollab2020/glassBerry/idle00" - seedSprite = "collectables/strawberry/seed00" + sprite = "collectables/SpringCollab2020/glassBerry/idle00" + seedSprite = "collectables/strawberry/seed00" - res = Ahorn.Rectangle[Ahorn.getSpriteRectangle(sprite, x, y)] + res = Ahorn.Rectangle[Ahorn.getSpriteRectangle(sprite, x, y)] - for node in nodes - nx, ny = node + for node in nodes + nx, ny = node - push!(res, Ahorn.getSpriteRectangle(seedSprite, nx, ny)) - end + push!(res, Ahorn.getSpriteRectangle(seedSprite, nx, ny)) + end - return res + return res end function Ahorn.renderSelectedAbs(ctx::Ahorn.Cairo.CairoContext, entity::GlassBerry) - x, y = Ahorn.position(entity) + x, y = Ahorn.position(entity) - for node in get(entity.data, "nodes", ()) - nx, ny = node + for node in get(entity.data, "nodes", ()) + nx, ny = node - Ahorn.drawLines(ctx, Tuple{Number, Number}[(x, y), (nx, ny)], Ahorn.colors.selection_selected_fc) - end + Ahorn.drawLines(ctx, Tuple{Number, Number}[(x, y), (nx, ny)], Ahorn.colors.selection_selected_fc) + end end function Ahorn.renderAbs(ctx::Ahorn.Cairo.CairoContext, entity::GlassBerry, room::Maple.Room) - x, y = Ahorn.position(entity) + x, y = Ahorn.position(entity) - nodes = get(entity.data, "nodes", ()) - hasPips = length(nodes) > 0 + nodes = get(entity.data, "nodes", ()) + hasPips = length(nodes) > 0 - sprite = "collectables/SpringCollab2020/glassBerry/idle00" - seedSprite = "collectables/strawberry/seed00" + sprite = "collectables/SpringCollab2020/glassBerry/idle00" + seedSprite = "collectables/strawberry/seed00" - for node in nodes - nx, ny = node + for node in nodes + nx, ny = node - Ahorn.drawSprite(ctx, seedSprite, nx, ny) - end + Ahorn.drawSprite(ctx, seedSprite, nx, ny) + end - Ahorn.drawSprite(ctx, sprite, x, y) + Ahorn.drawSprite(ctx, sprite, x, y) end end diff --git a/Ahorn/entities/horizontalRoomWrapController.jl b/Ahorn/entities/horizontalRoomWrapController.jl index 7d5924d..8676eec 100644 --- a/Ahorn/entities/horizontalRoomWrapController.jl +++ b/Ahorn/entities/horizontalRoomWrapController.jl @@ -1,21 +1,21 @@ -module SpringCollab2020HorizontalRoomWrapController - -using ..Ahorn, Maple - -@mapdef Entity "SpringCollab2020/HorizontalRoomWrapController" HorizontalRoomWrapController(x::Integer, y::Integer) - -const placements = Ahorn.PlacementDict( - "Horizontal Room Wrap Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( - HorizontalRoomWrapController - ) -) - -function Ahorn.selection(entity::HorizontalRoomWrapController) - x, y = Ahorn.position(entity) - - return Ahorn.Rectangle(x - 12, y - 12, 24, 24) -end - -Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::HorizontalRoomWrapController, room::Maple.Room) = Ahorn.drawSprite(ctx, "ahorn/SpringCollab2020/horizontal_room_wrap", 0, 0) - -end +module SpringCollab2020HorizontalRoomWrapController + +using ..Ahorn, Maple + +@mapdef Entity "SpringCollab2020/HorizontalRoomWrapController" HorizontalRoomWrapController(x::Integer, y::Integer) + +const placements = Ahorn.PlacementDict( + "Horizontal Room Wrap Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( + HorizontalRoomWrapController + ) +) + +function Ahorn.selection(entity::HorizontalRoomWrapController) + x, y = Ahorn.position(entity) + + return Ahorn.Rectangle(x - 12, y - 12, 24, 24) +end + +Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::HorizontalRoomWrapController, room::Maple.Room) = Ahorn.drawSprite(ctx, "ahorn/SpringCollab2020/horizontal_room_wrap", 0, 0) + +end diff --git a/Ahorn/entities/invisibleLightSource.jl b/Ahorn/entities/invisibleLightSource.jl index f97ffa2..9280720 100644 --- a/Ahorn/entities/invisibleLightSource.jl +++ b/Ahorn/entities/invisibleLightSource.jl @@ -1,4 +1,4 @@ -module SpringCollab2020InvisibleLightSource +module SpringCollab2020InvisibleLightSource using ..Ahorn, Maple @@ -7,26 +7,26 @@ using ..Ahorn, Maple const colors = sort(collect(keys(Ahorn.XNAColors.colors))) const placements = Ahorn.PlacementDict( - "Light Source (Spring Collab 2020)" => Ahorn.EntityPlacement( - InvisibleLightSource - ) + "Light Source (Spring Collab 2020)" => Ahorn.EntityPlacement( + InvisibleLightSource + ) ) Ahorn.editingOptions(entity::InvisibleLightSource) = Dict{String,Any}( - "color" => colors + "color" => colors ) function Ahorn.selection(entity::InvisibleLightSource) - x, y = Ahorn.position(entity) + x, y = Ahorn.position(entity) - return Ahorn.Rectangle(x - 4, y - 4, 7, 8) + return Ahorn.Rectangle(x - 4, y - 4, 7, 8) end function Ahorn.renderAbs(ctx::Ahorn.Cairo.CairoContext, entity::InvisibleLightSource, room::Maple.Room) - x, y = Ahorn.position(entity) - sprite = Ahorn.getTextureSprite("objects/hanginglamp", "Gameplay") - - Ahorn.drawImage(ctx, sprite, x - 4, y - 4, 0, 16, 7, 8, alpha=0.7) + x, y = Ahorn.position(entity) + sprite = Ahorn.getTextureSprite("objects/hanginglamp", "Gameplay") + + Ahorn.drawImage(ctx, sprite, x - 4, y - 4, 0, 16, 7, 8, alpha=0.7) end end \ No newline at end of file diff --git a/Ahorn/entities/moveBlockBarrier.jl b/Ahorn/entities/moveBlockBarrier.jl index 2bf0a7f..72e7bb4 100644 --- a/Ahorn/entities/moveBlockBarrier.jl +++ b/Ahorn/entities/moveBlockBarrier.jl @@ -1,4 +1,4 @@ -module SpringCollab2020MoveBlockBarrier +module SpringCollab2020MoveBlockBarrier using ..Ahorn, Maple @@ -26,7 +26,7 @@ end function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::MoveBlockBarrier, room::Maple.Room) width = Int(get(entity.data, "width", 32)) height = Int(get(entity.data, "height", 32)) - + Ahorn.drawRectangle(ctx, 0, 0, width, height, (0.45, 0.0, 0.45, 0.8), (0.0, 0.0, 0.0, 0.0)) end diff --git a/Ahorn/entities/moveBlockCustomSpeed.jl b/Ahorn/entities/moveBlockCustomSpeed.jl index 2eacd6c..3e5c2ce 100644 --- a/Ahorn/entities/moveBlockCustomSpeed.jl +++ b/Ahorn/entities/moveBlockCustomSpeed.jl @@ -1,4 +1,4 @@ -module SpringCollab2020MoveBlockCustomSpeed +module SpringCollab2020MoveBlockCustomSpeed using ..Ahorn, Maple diff --git a/Ahorn/entities/multiNodeMovingPlatform.jl b/Ahorn/entities/multiNodeMovingPlatform.jl index b723773..ff6ce25 100644 --- a/Ahorn/entities/multiNodeMovingPlatform.jl +++ b/Ahorn/entities/multiNodeMovingPlatform.jl @@ -42,7 +42,7 @@ function Ahorn.selection(entity::MultiNodeMovingPlatform) nodes = get(entity.data, "nodes", ()) startX, startY = Int(entity.data["x"]), Int(entity.data["y"]) rectangles = Ahorn.Rectangle[Ahorn.Rectangle(startX, startY, width, 8)] - + for node in nodes nodeX, nodeY = Int.(node) push!(rectangles, Ahorn.Rectangle(nodeX, nodeY, width, 8)) @@ -102,12 +102,12 @@ end function Ahorn.renderAbs(ctx::Ahorn.Cairo.CairoContext, entity::MultiNodeMovingPlatform, room::Maple.Room) width = Int(get(entity.data, "width", 8)) mode = get(entity.data, "mode", "Loop") - + firstNodeX, firstNodeY = Int(entity.data["x"]), Int(entity.data["y"]) previousNodeX, previousNodeY = firstNodeX, firstNodeY - + texture = get(entity.data, "texture", "default") - + nodes = get(entity.data, "nodes", ()) for node in nodes nodeX, nodeY = Int.(node) @@ -125,12 +125,12 @@ end function Ahorn.renderSelectedAbs(ctx::Ahorn.Cairo.CairoContext, entity::MultiNodeMovingPlatform, room::Maple.Room) width = Int(get(entity.data, "width", 8)) mode = get(entity.data, "mode", "Loop") - + firstNodeX, firstNodeY = Int(entity.data["x"]), Int(entity.data["y"]) previousNodeX, previousNodeY = firstNodeX, firstNodeY texture = get(entity.data, "texture", "default") - + nodes = get(entity.data, "nodes", ()) for node in nodes nodeX, nodeY = Int.(node) @@ -138,7 +138,7 @@ function Ahorn.renderSelectedAbs(ctx::Ahorn.Cairo.CairoContext, entity::MultiNod Ahorn.drawArrow(ctx, previousNodeX + width / 2, previousNodeY, nodeX + width / 2, nodeY, Ahorn.colors.selection_selected_fc, headLength=6) previousNodeX, previousNodeY = nodeX, nodeY end - + if mode == "Loop" || mode == "LoopNoPause" Ahorn.drawArrow(ctx, previousNodeX + width / 2, previousNodeY, firstNodeX + width / 2, firstNodeY, Ahorn.colors.selection_selected_fc, headLength=6) end diff --git a/Ahorn/entities/multiRoomStrawberry.jl b/Ahorn/entities/multiRoomStrawberry.jl index 5305e29..6186d4f 100644 --- a/Ahorn/entities/multiRoomStrawberry.jl +++ b/Ahorn/entities/multiRoomStrawberry.jl @@ -1,4 +1,4 @@ -module SpringCollab2020MultiRoomStrawberry +module SpringCollab2020MultiRoomStrawberry using ..Ahorn, Maple diff --git a/Ahorn/entities/multiRoomStrawberrySeed.jl b/Ahorn/entities/multiRoomStrawberrySeed.jl index fbdab75..7a9f577 100644 --- a/Ahorn/entities/multiRoomStrawberrySeed.jl +++ b/Ahorn/entities/multiRoomStrawberrySeed.jl @@ -1,10 +1,10 @@ -module SpringCollab2020MultiRoomStrawberrySeed +module SpringCollab2020MultiRoomStrawberrySeed using ..Ahorn, Maple @mapdef Entity "SpringCollab2020/MultiRoomStrawberrySeed" MultiRoomStrawberrySeed(x::Integer, y::Integer, strawberryName::String="multi_room_strawberry", sprite::String="strawberry/seed", ghostSprite::String="ghostberry/seed", index::Int=-1, ignoreLighting::Bool=false) - + const bundledSprites = String["strawberry/seed", "SpringCollab2020/miniberry/miniberry"] const bundledGhostSprites = String["ghostberry/seed", "SpringCollab2020/miniberry/ghostminiberry"] @@ -20,16 +20,16 @@ Ahorn.editingOptions(entity::MultiRoomStrawberrySeed) = Dict{String,Any}( "ghostSprite" => bundledGhostSprites ) -function Ahorn.selection(entity::MultiRoomStrawberrySeed) +function Ahorn.selection(entity::MultiRoomStrawberrySeed) x, y = Ahorn.position(entity) sprite = "collectables/" * get(entity.data, "sprite", "strawberry/seed") * "00" return Ahorn.getSpriteRectangle(sprite, x, y) -end +end function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::MultiRoomStrawberrySeed, room::Maple.Room) sprite = "collectables/" * get(entity.data, "sprite", "strawberry/seed") * "00" - + Ahorn.drawSprite(ctx, sprite, 0, 0) end diff --git a/Ahorn/entities/negativeSummitCheckpoint.jl b/Ahorn/entities/negativeSummitCheckpoint.jl index 19c3d68..ab21693 100644 --- a/Ahorn/entities/negativeSummitCheckpoint.jl +++ b/Ahorn/entities/negativeSummitCheckpoint.jl @@ -1,4 +1,4 @@ -module SpringCollab2020NegativeSummitCheckpoint +module SpringCollab2020NegativeSummitCheckpoint using ..Ahorn, Maple diff --git a/Ahorn/entities/noDashRefillSpring.jl b/Ahorn/entities/noDashRefillSpring.jl index 0251c05..2dbf3f3 100644 --- a/Ahorn/entities/noDashRefillSpring.jl +++ b/Ahorn/entities/noDashRefillSpring.jl @@ -1,4 +1,4 @@ -module SpringCollab2020NoDashRefillSpring +module SpringCollab2020NoDashRefillSpring using ..Ahorn, Maple diff --git a/Ahorn/entities/nonBadelineMovingBlock.jl b/Ahorn/entities/nonBadelineMovingBlock.jl index 144f98e..f4404e2 100644 --- a/Ahorn/entities/nonBadelineMovingBlock.jl +++ b/Ahorn/entities/nonBadelineMovingBlock.jl @@ -1,4 +1,4 @@ -module SpringCollab2020NonBadelineMovingBlock +module SpringCollab2020NonBadelineMovingBlock using ..Ahorn, Maple @@ -46,7 +46,7 @@ function Ahorn.renderSelectedAbs(ctx::Ahorn.Cairo.CairoContext, entity::NonBadel width = Int(get(entity.data, "width", 8)) height = Int(get(entity.data, "height", 8)) - + if !isempty(nodes) nx, ny = Int.(nodes[1]) cox, coy = floor(Int, width / 2), floor(Int, height / 2) diff --git a/Ahorn/entities/nonCoreModeWallBooster.jl b/Ahorn/entities/nonCoreModeWallBooster.jl index 47d2e8d..5fbb1d3 100755 --- a/Ahorn/entities/nonCoreModeWallBooster.jl +++ b/Ahorn/entities/nonCoreModeWallBooster.jl @@ -1,4 +1,4 @@ -module SpringCollab2020NonCoreModeWallBooster +module SpringCollab2020NonCoreModeWallBooster using ..Ahorn, Maple diff --git a/Ahorn/entities/rainbowSpinnerColorAreaController.jl b/Ahorn/entities/rainbowSpinnerColorAreaController.jl index 78706ec..b9aa2c4 100644 --- a/Ahorn/entities/rainbowSpinnerColorAreaController.jl +++ b/Ahorn/entities/rainbowSpinnerColorAreaController.jl @@ -1,27 +1,27 @@ -module SpringCollab2020RainbowSpinnerColorAreaController - -using ..Ahorn, Maple - -@mapdef Entity "SpringCollab2020/RainbowSpinnerColorAreaController" RainbowSpinnerColorAreaController(x::Integer, y::Integer, width::Integer=Maple.defaultBlockWidth, height::Integer=Maple.defaultBlockHeight, - colors::String="89E5AE,88E0E0,87A9DD,9887DB,D088E2", gradientSize::Number=280.0) - -const placements = Ahorn.PlacementDict( - "Rainbow Spinner Colour Area Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( - RainbowSpinnerColorAreaController, - "rectangle" - ) -) - -Ahorn.minimumSize(entity::RainbowSpinnerColorAreaController) = 8, 8 -Ahorn.resizable(entity::RainbowSpinnerColorAreaController) = true, true - -Ahorn.selection(entity::RainbowSpinnerColorAreaController) = Ahorn.getEntityRectangle(entity) - -function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::RainbowSpinnerColorAreaController, room::Maple.Room) - width = Int(get(entity.data, "width", 32)) - height = Int(get(entity.data, "height", 32)) - - Ahorn.drawRectangle(ctx, 0, 0, width, height, (0.4, 0.4, 1.0, 0.4), (0.4, 0.4, 1.0, 1.0)) -end - -end +module SpringCollab2020RainbowSpinnerColorAreaController + +using ..Ahorn, Maple + +@mapdef Entity "SpringCollab2020/RainbowSpinnerColorAreaController" RainbowSpinnerColorAreaController(x::Integer, y::Integer, width::Integer=Maple.defaultBlockWidth, height::Integer=Maple.defaultBlockHeight, + colors::String="89E5AE,88E0E0,87A9DD,9887DB,D088E2", gradientSize::Number=280.0) + +const placements = Ahorn.PlacementDict( + "Rainbow Spinner Colour Area Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( + RainbowSpinnerColorAreaController, + "rectangle" + ) +) + +Ahorn.minimumSize(entity::RainbowSpinnerColorAreaController) = 8, 8 +Ahorn.resizable(entity::RainbowSpinnerColorAreaController) = true, true + +Ahorn.selection(entity::RainbowSpinnerColorAreaController) = Ahorn.getEntityRectangle(entity) + +function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::RainbowSpinnerColorAreaController, room::Maple.Room) + width = Int(get(entity.data, "width", 32)) + height = Int(get(entity.data, "height", 32)) + + Ahorn.drawRectangle(ctx, 0, 0, width, height, (0.4, 0.4, 1.0, 0.4), (0.4, 0.4, 1.0, 1.0)) +end + +end diff --git a/Ahorn/entities/rainbowSpinnerColorController.jl b/Ahorn/entities/rainbowSpinnerColorController.jl index 6498104..638595d 100644 --- a/Ahorn/entities/rainbowSpinnerColorController.jl +++ b/Ahorn/entities/rainbowSpinnerColorController.jl @@ -1,22 +1,22 @@ -module SpringCollab2020RainbowSpinnerColorController - -using ..Ahorn, Maple - -@mapdef Entity "SpringCollab2020/RainbowSpinnerColorController" RainbowSpinnerColorController(x::Integer, y::Integer, - colors::String="89E5AE,88E0E0,87A9DD,9887DB,D088E2", gradientSize::Number=280.0) - -const placements = Ahorn.PlacementDict( - "Rainbow Spinner Colour Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( - RainbowSpinnerColorController - ) -) - -function Ahorn.selection(entity::RainbowSpinnerColorController) - x, y = Ahorn.position(entity) - - return Ahorn.Rectangle(x - 12, y - 12, 24, 24) -end - -Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::RainbowSpinnerColorController, room::Maple.Room) = Ahorn.drawImage(ctx, Ahorn.Assets.northernLights, -12, -12) - -end +module SpringCollab2020RainbowSpinnerColorController + +using ..Ahorn, Maple + +@mapdef Entity "SpringCollab2020/RainbowSpinnerColorController" RainbowSpinnerColorController(x::Integer, y::Integer, + colors::String="89E5AE,88E0E0,87A9DD,9887DB,D088E2", gradientSize::Number=280.0) + +const placements = Ahorn.PlacementDict( + "Rainbow Spinner Colour Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( + RainbowSpinnerColorController + ) +) + +function Ahorn.selection(entity::RainbowSpinnerColorController) + x, y = Ahorn.position(entity) + + return Ahorn.Rectangle(x - 12, y - 12, 24, 24) +end + +Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::RainbowSpinnerColorController, room::Maple.Room) = Ahorn.drawImage(ctx, Ahorn.Assets.northernLights, -12, -12) + +end diff --git a/Ahorn/entities/respawningJellyfish.jl b/Ahorn/entities/respawningJellyfish.jl index bbb18c7..0962d6f 100644 --- a/Ahorn/entities/respawningJellyfish.jl +++ b/Ahorn/entities/respawningJellyfish.jl @@ -1,4 +1,4 @@ -module SpringCollab2020RespawningJellyfish +module SpringCollab2020RespawningJellyfish using ..Ahorn, Maple diff --git a/Ahorn/entities/safeRespawnCrumble.jl b/Ahorn/entities/safeRespawnCrumble.jl index 2524a00..fe5267e 100644 --- a/Ahorn/entities/safeRespawnCrumble.jl +++ b/Ahorn/entities/safeRespawnCrumble.jl @@ -1,4 +1,4 @@ -module SpringCollab2020SafeRespawnCrumble +module SpringCollab2020SafeRespawnCrumble using ..Ahorn, Maple diff --git a/Ahorn/entities/seekerCustomColors.jl b/Ahorn/entities/seekerCustomColors.jl index b2ce81b..4e2fac6 100644 --- a/Ahorn/entities/seekerCustomColors.jl +++ b/Ahorn/entities/seekerCustomColors.jl @@ -1,4 +1,4 @@ -module SpringCollab2020SeekerCustomColors +module SpringCollab2020SeekerCustomColors using ..Ahorn, Maple @@ -23,7 +23,7 @@ function Ahorn.selection(entity::SeekerCustomColors) x, y = Ahorn.position(entity) res = Ahorn.Rectangle[Ahorn.getSpriteRectangle(sprite, x, y)] - + for node in nodes nx, ny = node diff --git a/Ahorn/entities/seekerStatueCustomColors.jl b/Ahorn/entities/seekerStatueCustomColors.jl index 1cf6c04..c8b141d 100644 --- a/Ahorn/entities/seekerStatueCustomColors.jl +++ b/Ahorn/entities/seekerStatueCustomColors.jl @@ -1,4 +1,4 @@ -module SpringCollab2020SeekerStatueCustomColors +module SpringCollab2020SeekerStatueCustomColors using ..Ahorn, Maple @@ -28,7 +28,7 @@ function Ahorn.selection(entity::SeekerStatueCustomColors) x, y = Ahorn.position(entity) res = Ahorn.Rectangle[Ahorn.getSpriteRectangle(statueSprite, x, y)] - + for node in nodes nx, ny = node diff --git a/Ahorn/entities/sidewaysJumpThru.jl b/Ahorn/entities/sidewaysJumpThru.jl index d5750fb..6a88952 100644 --- a/Ahorn/entities/sidewaysJumpThru.jl +++ b/Ahorn/entities/sidewaysJumpThru.jl @@ -2,14 +2,14 @@ module SpringCollab2020SidewaysJumpThru using ..Ahorn, Maple -@mapdef Entity "SpringCollab2020/SidewaysJumpThru" SidewaysJumpThru(x::Integer, y::Integer, height::Integer=Maple.defaultBlockHeight, +@mapdef Entity "SpringCollab2020/SidewaysJumpThru" SidewaysJumpThru(x::Integer, y::Integer, height::Integer=Maple.defaultBlockHeight, left::Bool=true, texture::String="wood", animationDelay::Number=0.0) textures = ["wood", "dream", "temple", "templeB", "cliffside", "reflection", "core", "moon"] const placements = Ahorn.PlacementDict() for texture in textures - placements["Sideways Jump Through ($(uppercasefirst(texture)), Left) (Spring Collab 2020)"] = Ahorn.EntityPlacement( + placements["Sideways Jump Through ($(uppercasefirst(texture)), Left) (Spring Collab 2020)"] = Ahorn.EntityPlacement( SidewaysJumpThru, "rectangle", Dict{String, Any}( @@ -56,16 +56,16 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::SidewaysJumpThru, r height = Int(get(entity.data, "height", 8)) left = get(entity.data, "left", true) - + startX = div(x, 8) + 1 startY = div(y, 8) + 1 stopY = startY + div(height, 8) - 1 animated = Number(get(entity.data, "animationDelay", 0)) > 0 - + Ahorn.Cairo.save(ctx) - + Ahorn.rotate(ctx, pi / 2) - + if left Ahorn.scale(ctx, 1, -1) end @@ -90,7 +90,7 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::SidewaysJumpThru, r Ahorn.drawImage(ctx, "objects/jumpthru/$(texture)", 8 * i, left ? 0 : -8, quad...) end end - + Ahorn.Cairo.restore(ctx) end diff --git a/Ahorn/entities/sidewaysLava.jl b/Ahorn/entities/sidewaysLava.jl index 498c794..ef61592 100644 --- a/Ahorn/entities/sidewaysLava.jl +++ b/Ahorn/entities/sidewaysLava.jl @@ -24,12 +24,12 @@ end function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::SidewaysLava, room::Maple.Room) Ahorn.Cairo.save(ctx) - + lavaMode = get(entity.data, "lavaMode", "LeftToRight") - + Ahorn.rotate(ctx, (lavaMode == "RightToLeft" ? -1 : 1) * pi / 2) Ahorn.drawImage(ctx, (lavaMode == "Sandwich" ? Ahorn.Assets.lavaSanwitch : Ahorn.Assets.risingLava), -12, -12) - + Ahorn.Cairo.restore(ctx); end diff --git a/Ahorn/entities/spikeJumpThruController.jl b/Ahorn/entities/spikeJumpThruController.jl index 5a656db..77348d3 100644 --- a/Ahorn/entities/spikeJumpThruController.jl +++ b/Ahorn/entities/spikeJumpThruController.jl @@ -1,4 +1,4 @@ -module SpringCollab2020SpikeJumpThroughController +module SpringCollab2020SpikeJumpThroughController using ..Ahorn, Maple @@ -20,7 +20,7 @@ function Ahorn.selection(entity::SpikeJumpThroughController) end function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::SpikeJumpThroughController, room::Maple.Room) - Ahorn.drawSprite(ctx, sprite, 0, 0) + Ahorn.drawSprite(ctx, sprite, 0, 0) end end diff --git a/Ahorn/entities/staticPuffer.jl b/Ahorn/entities/staticPuffer.jl index b73773c..a20249f 100644 --- a/Ahorn/entities/staticPuffer.jl +++ b/Ahorn/entities/staticPuffer.jl @@ -18,7 +18,7 @@ const placements = Ahorn.PlacementDict( Dict{String, Any}( "right" => false ) - ) + ) ) sprite = "objects/puffer/idle00" diff --git a/Ahorn/entities/strawberryIgnoringLighting.jl b/Ahorn/entities/strawberryIgnoringLighting.jl index bc58f6b..ad8accd 100644 --- a/Ahorn/entities/strawberryIgnoringLighting.jl +++ b/Ahorn/entities/strawberryIgnoringLighting.jl @@ -1,4 +1,4 @@ -module SpringCollab2020StrawberryIgnoringLighting +module SpringCollab2020StrawberryIgnoringLighting using ..Ahorn, Maple diff --git a/Ahorn/entities/trollStrawberry.jl b/Ahorn/entities/trollStrawberry.jl index 1386fbd..70a93a2 100644 --- a/Ahorn/entities/trollStrawberry.jl +++ b/Ahorn/entities/trollStrawberry.jl @@ -1,4 +1,4 @@ -module SpringCollab2020TrollStrawberry +module SpringCollab2020TrollStrawberry using ..Ahorn, Maple diff --git a/Ahorn/entities/underwaterSwitchController.jl b/Ahorn/entities/underwaterSwitchController.jl index 97cc68c..c81ba0a 100644 --- a/Ahorn/entities/underwaterSwitchController.jl +++ b/Ahorn/entities/underwaterSwitchController.jl @@ -1,21 +1,21 @@ -module SpringCollab2020UnderwaterSwitchController - -using ..Ahorn, Maple - -@mapdef Entity "SpringCollab2020/UnderwaterSwitchController" UnderwaterSwitchController(x::Integer, y::Integer, flag::String="underwater_switch") - -const placements = Ahorn.PlacementDict( - "Underwater Switch Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( - UnderwaterSwitchController - ) -) - -function Ahorn.selection(entity::UnderwaterSwitchController) - x, y = Ahorn.position(entity) - - return Ahorn.Rectangle(x - 12, y - 12, 24, 24) -end - -Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::UnderwaterSwitchController, room::Maple.Room) = Ahorn.drawSprite(ctx, "ahorn/SpringCollab2020/underwater_switch", 0, 0) - -end +module SpringCollab2020UnderwaterSwitchController + +using ..Ahorn, Maple + +@mapdef Entity "SpringCollab2020/UnderwaterSwitchController" UnderwaterSwitchController(x::Integer, y::Integer, flag::String="underwater_switch") + +const placements = Ahorn.PlacementDict( + "Underwater Switch Controller (Spring Collab 2020)" => Ahorn.EntityPlacement( + UnderwaterSwitchController + ) +) + +function Ahorn.selection(entity::UnderwaterSwitchController) + x, y = Ahorn.position(entity) + + return Ahorn.Rectangle(x - 12, y - 12, 24, 24) +end + +Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::UnderwaterSwitchController, room::Maple.Room) = Ahorn.drawSprite(ctx, "ahorn/SpringCollab2020/underwater_switch", 0, 0) + +end diff --git a/Ahorn/entities/upsideDownJumpThru.jl b/Ahorn/entities/upsideDownJumpThru.jl index 5c8dd25..a4da2c9 100644 --- a/Ahorn/entities/upsideDownJumpThru.jl +++ b/Ahorn/entities/upsideDownJumpThru.jl @@ -48,9 +48,9 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::UpsideDownJumpThru, stopX = startX + div(width, 8) - 1 startY = div(y, 8) + 1 animated = Number(get(entity.data, "animationDelay", 0)) > 0 - + Ahorn.Cairo.save(ctx) - + Ahorn.scale(ctx, 1, -1) len = stopX - startX @@ -73,7 +73,7 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::UpsideDownJumpThru, Ahorn.drawImage(ctx, "objects/jumpthru/$(texture)", 8 * i, -8, quad...) end end - + Ahorn.Cairo.restore(ctx) end diff --git a/Ahorn/triggers/badelineBounceDirectionTrigger.jl b/Ahorn/triggers/badelineBounceDirectionTrigger.jl index 8a72c71..eba19cc 100755 --- a/Ahorn/triggers/badelineBounceDirectionTrigger.jl +++ b/Ahorn/triggers/badelineBounceDirectionTrigger.jl @@ -1,15 +1,15 @@ -module SpringCollab2020BadelineBounceDirectionTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/BadelineBounceDirectionTrigger" BadelineBounceDirectionTrigger(x::Integer, y::Integer, - width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, bounceLeft::Bool=false) - -const placements = Ahorn.PlacementDict( - "Badeline Bounce Direction (Spring Collab 2020)" => Ahorn.EntityPlacement( - BadelineBounceDirectionTrigger, - "rectangle", - ), -) - -end +module SpringCollab2020BadelineBounceDirectionTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/BadelineBounceDirectionTrigger" BadelineBounceDirectionTrigger(x::Integer, y::Integer, + width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, bounceLeft::Bool=false) + +const placements = Ahorn.PlacementDict( + "Badeline Bounce Direction (Spring Collab 2020)" => Ahorn.EntityPlacement( + BadelineBounceDirectionTrigger, + "rectangle", + ), +) + +end diff --git a/Ahorn/triggers/cameraCatchupSpeed.jl b/Ahorn/triggers/cameraCatchupSpeed.jl index 1b343b1..15499b1 100644 --- a/Ahorn/triggers/cameraCatchupSpeed.jl +++ b/Ahorn/triggers/cameraCatchupSpeed.jl @@ -1,15 +1,15 @@ -module SpringCollab2020CameraCatchupSpeedTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/CameraCatchupSpeedTrigger" CameraCatchupSpeedTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, - catchupSpeed::Number=1.0) - -const placements = Ahorn.PlacementDict( - "Camera Catchup Speed (Spring Collab 2020)" => Ahorn.EntityPlacement( - CameraCatchupSpeedTrigger, - "rectangle" - ) -) - -end +module SpringCollab2020CameraCatchupSpeedTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/CameraCatchupSpeedTrigger" CameraCatchupSpeedTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, + catchupSpeed::Number=1.0) + +const placements = Ahorn.PlacementDict( + "Camera Catchup Speed (Spring Collab 2020)" => Ahorn.EntityPlacement( + CameraCatchupSpeedTrigger, + "rectangle" + ) +) + +end diff --git a/Ahorn/triggers/cancelLightningRemoveRoutineTrigger.jl b/Ahorn/triggers/cancelLightningRemoveRoutineTrigger.jl index a6ee79e..d500e04 100755 --- a/Ahorn/triggers/cancelLightningRemoveRoutineTrigger.jl +++ b/Ahorn/triggers/cancelLightningRemoveRoutineTrigger.jl @@ -1,14 +1,14 @@ -module SpringCollab2020CancelLightningRemoveRoutineTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/CancelLightningRemoveRoutineTrigger" CancelLightningRemoveRoutineTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight) - -const placements = Ahorn.PlacementDict( - "Cancel Lightning Remove Routine Trigger (Spring Collab 2020)" => Ahorn.EntityPlacement( - CancelLightningRemoveRoutineTrigger, - "rectangle", - ), -) - -end +module SpringCollab2020CancelLightningRemoveRoutineTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/CancelLightningRemoveRoutineTrigger" CancelLightningRemoveRoutineTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight) + +const placements = Ahorn.PlacementDict( + "Cancel Lightning Remove Routine Trigger (Spring Collab 2020)" => Ahorn.EntityPlacement( + CancelLightningRemoveRoutineTrigger, + "rectangle", + ), +) + +end diff --git a/Ahorn/triggers/changeThemeTrigger.jl b/Ahorn/triggers/changeThemeTrigger.jl index e91f4b3..8f3e567 100755 --- a/Ahorn/triggers/changeThemeTrigger.jl +++ b/Ahorn/triggers/changeThemeTrigger.jl @@ -1,15 +1,15 @@ -module SpringCollab2020ChangeThemeTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/ChangeThemeTrigger" ChangeThemeTrigger(x::Integer, y::Integer, - width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, enable::Bool=true) - -const placements = Ahorn.PlacementDict( - "Change Theme Trigger (Spring Collab 2020)" => Ahorn.EntityPlacement( - ChangeThemeTrigger, - "rectangle", - ), -) - -end +module SpringCollab2020ChangeThemeTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/ChangeThemeTrigger" ChangeThemeTrigger(x::Integer, y::Integer, + width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, enable::Bool=true) + +const placements = Ahorn.PlacementDict( + "Change Theme Trigger (Spring Collab 2020)" => Ahorn.EntityPlacement( + ChangeThemeTrigger, + "rectangle", + ), +) + +end diff --git a/Ahorn/triggers/colorGradeFadeTrigger.jl b/Ahorn/triggers/colorGradeFadeTrigger.jl index c44b879..a8cbb70 100644 --- a/Ahorn/triggers/colorGradeFadeTrigger.jl +++ b/Ahorn/triggers/colorGradeFadeTrigger.jl @@ -1,25 +1,25 @@ -module SpringCollab2020ColorGradeFadeTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/ColorGradeFadeTrigger" ColorGradeFadeTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, - colorGradeA::String="none", colorGradeB::String="none", direction::String="LeftToRight") - -const placements = Ahorn.PlacementDict( - "Color Grade Fade (Spring Collab 2020)" => Ahorn.EntityPlacement( - ColorGradeFadeTrigger, - "rectangle", - ), -) - -const colorGrades = String["none", "oldsite", "panicattack", "templevoid", "reflection", "credits", "cold", "hot", "feelingdown", "golden"] - -function Ahorn.editingOptions(trigger::ColorGradeFadeTrigger) - return Dict{String, Any}( - "direction" => String["LeftToRight", "TopToBottom"], - "colorGradeA" => colorGrades, - "colorGradeB" => colorGrades - ) -end - +module SpringCollab2020ColorGradeFadeTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/ColorGradeFadeTrigger" ColorGradeFadeTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, + colorGradeA::String="none", colorGradeB::String="none", direction::String="LeftToRight") + +const placements = Ahorn.PlacementDict( + "Color Grade Fade (Spring Collab 2020)" => Ahorn.EntityPlacement( + ColorGradeFadeTrigger, + "rectangle", + ), +) + +const colorGrades = String["none", "oldsite", "panicattack", "templevoid", "reflection", "credits", "cold", "hot", "feelingdown", "golden"] + +function Ahorn.editingOptions(trigger::ColorGradeFadeTrigger) + return Dict{String, Any}( + "direction" => String["LeftToRight", "TopToBottom"], + "colorGradeA" => colorGrades, + "colorGradeB" => colorGrades + ) +end + end \ No newline at end of file diff --git a/Ahorn/triggers/customBirdTutorial.jl b/Ahorn/triggers/customBirdTutorial.jl index 05d0c8d..f5f118d 100644 --- a/Ahorn/triggers/customBirdTutorial.jl +++ b/Ahorn/triggers/customBirdTutorial.jl @@ -1,15 +1,15 @@ -module SpringCollab2020CustomBirdTutorialTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/CustomBirdTutorialTrigger" CustomBirdTutorialTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, - birdId::String="birdId", showTutorial::Bool=true) - -const placements = Ahorn.PlacementDict( - "Custom Bird Tutorial (Spring Collab 2020)" => Ahorn.EntityPlacement( - CustomBirdTutorialTrigger, - "rectangle" - ) -) - -end +module SpringCollab2020CustomBirdTutorialTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/CustomBirdTutorialTrigger" CustomBirdTutorialTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, + birdId::String="birdId", showTutorial::Bool=true) + +const placements = Ahorn.PlacementDict( + "Custom Bird Tutorial (Spring Collab 2020)" => Ahorn.EntityPlacement( + CustomBirdTutorialTrigger, + "rectangle" + ) +) + +end diff --git a/Ahorn/triggers/customSandwichLavaSettings.jl b/Ahorn/triggers/customSandwichLavaSettings.jl index 9437a42..8102ee9 100644 --- a/Ahorn/triggers/customSandwichLavaSettings.jl +++ b/Ahorn/triggers/customSandwichLavaSettings.jl @@ -1,21 +1,21 @@ -module SpringCollab2020CustomSandwichLavaSettingsTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/CustomSandwichLavaSettingsTrigger" CustomSandwichLavaSettingsTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, - onlyOnce::Bool=false, direction::String="CoreModeBased", speed::Number=20.0) - -const directions = String["AlwaysUp", "AlwaysDown", "CoreModeBased"] - -const placements = Ahorn.PlacementDict( - "Custom Sandwich Lava Settings (Spring Collab 2020)" => Ahorn.EntityPlacement( - CustomSandwichLavaSettingsTrigger, - "rectangle", - ), -) - -Ahorn.editingOptions(entity::CustomSandwichLavaSettingsTrigger) = Dict{String, Any}( - "direction" => directions -) - +module SpringCollab2020CustomSandwichLavaSettingsTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/CustomSandwichLavaSettingsTrigger" CustomSandwichLavaSettingsTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, + onlyOnce::Bool=false, direction::String="CoreModeBased", speed::Number=20.0) + +const directions = String["AlwaysUp", "AlwaysDown", "CoreModeBased"] + +const placements = Ahorn.PlacementDict( + "Custom Sandwich Lava Settings (Spring Collab 2020)" => Ahorn.EntityPlacement( + CustomSandwichLavaSettingsTrigger, + "rectangle", + ), +) + +Ahorn.editingOptions(entity::CustomSandwichLavaSettingsTrigger) = Dict{String, Any}( + "direction" => directions +) + end \ No newline at end of file diff --git a/Ahorn/triggers/disableIcePhysics.jl b/Ahorn/triggers/disableIcePhysics.jl index 903ffdf..56a7b80 100644 --- a/Ahorn/triggers/disableIcePhysics.jl +++ b/Ahorn/triggers/disableIcePhysics.jl @@ -1,15 +1,15 @@ -module SpringCollab2020DisableIcePhysicsTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/DisableIcePhysicsTrigger" DisableIcePhysicsTrigger(x::Integer, y::Integer, - width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, disableIcePhysics::Bool=true) - -const placements = Ahorn.PlacementDict( - "Disable Ice Physics (Spring Collab 2020)" => Ahorn.EntityPlacement( - DisableIcePhysicsTrigger, - "rectangle" - ) -) - +module SpringCollab2020DisableIcePhysicsTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/DisableIcePhysicsTrigger" DisableIcePhysicsTrigger(x::Integer, y::Integer, + width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, disableIcePhysics::Bool=true) + +const placements = Ahorn.PlacementDict( + "Disable Ice Physics (Spring Collab 2020)" => Ahorn.EntityPlacement( + DisableIcePhysicsTrigger, + "rectangle" + ) +) + end \ No newline at end of file diff --git a/Ahorn/triggers/flagToggleCameraTargetTrigger.jl b/Ahorn/triggers/flagToggleCameraTargetTrigger.jl index a690454..e96fe32 100644 --- a/Ahorn/triggers/flagToggleCameraTargetTrigger.jl +++ b/Ahorn/triggers/flagToggleCameraTargetTrigger.jl @@ -1,30 +1,30 @@ -module SpringCollab2020FlagToggleCameraTargetTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/FlagToggleCameraTargetTrigger" FlagToggleCameraTargetTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, - lerpStrength::Number=0.0, positionMode::String="NoEffect", xOnly::Bool=false, yOnly::Bool=false, deleteFlag::String="", nodes::Array{Tuple{Integer, Integer}, 1}=Tuple{Integer, Integer}[], - flag::String="flag_toggle_camera_target", inverted::Bool=false) - -const placements = Ahorn.PlacementDict( - "Flag Toggle Camera Target (Spring Collab 2020)" => Ahorn.EntityPlacement( - FlagToggleCameraTargetTrigger, - "rectangle", - Dict{String, Any}(), - function(trigger) - trigger.data["nodes"] = [(Int(trigger.data["x"]) + Int(trigger.data["width"]) + 8, Int(trigger.data["y"]))] - end - ) -) - -function Ahorn.editingOptions(trigger::FlagToggleCameraTargetTrigger) - return Dict{String, Any}( - "positionMode" => Maple.trigger_position_modes - ) -end - -function Ahorn.nodeLimits(trigger::FlagToggleCameraTargetTrigger) - return 1, 1 -end - +module SpringCollab2020FlagToggleCameraTargetTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/FlagToggleCameraTargetTrigger" FlagToggleCameraTargetTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, + lerpStrength::Number=0.0, positionMode::String="NoEffect", xOnly::Bool=false, yOnly::Bool=false, deleteFlag::String="", nodes::Array{Tuple{Integer, Integer}, 1}=Tuple{Integer, Integer}[], + flag::String="flag_toggle_camera_target", inverted::Bool=false) + +const placements = Ahorn.PlacementDict( + "Flag Toggle Camera Target (Spring Collab 2020)" => Ahorn.EntityPlacement( + FlagToggleCameraTargetTrigger, + "rectangle", + Dict{String, Any}(), + function(trigger) + trigger.data["nodes"] = [(Int(trigger.data["x"]) + Int(trigger.data["width"]) + 8, Int(trigger.data["y"]))] + end + ) +) + +function Ahorn.editingOptions(trigger::FlagToggleCameraTargetTrigger) + return Dict{String, Any}( + "positionMode" => Maple.trigger_position_modes + ) +end + +function Ahorn.nodeLimits(trigger::FlagToggleCameraTargetTrigger) + return 1, 1 +end + end \ No newline at end of file diff --git a/Ahorn/triggers/flagToggleSmoothCameraOffsetTrigger.jl b/Ahorn/triggers/flagToggleSmoothCameraOffsetTrigger.jl index e5089bf..00e8e06 100644 --- a/Ahorn/triggers/flagToggleSmoothCameraOffsetTrigger.jl +++ b/Ahorn/triggers/flagToggleSmoothCameraOffsetTrigger.jl @@ -1,21 +1,21 @@ -module SpringCollab2020FlagToggleSmoothCameraOffsetTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/FlagToggleSmoothCameraOffsetTrigger" FlagToggleSmoothCameraOffsetTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, - offsetXFrom::Number=0.0, offsetXTo::Number=0.0, offsetYFrom::Number=0.0, offsetYTo::Number=0.0, positionMode::String="NoEffect", onlyOnce::Bool=false, flag::String="flag_toggle_smooth_camera_offset", inverted::Bool=false) - -const placements = Ahorn.PlacementDict( - "Flag Toggle Smooth Camera Offset (Spring Collab 2020)" => Ahorn.EntityPlacement( - FlagToggleSmoothCameraOffsetTrigger, - "rectangle", - ), -) - -function Ahorn.editingOptions(trigger::FlagToggleSmoothCameraOffsetTrigger) - return Dict{String, Any}( - "positionMode" => Maple.trigger_position_modes - ) -end - -end +module SpringCollab2020FlagToggleSmoothCameraOffsetTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/FlagToggleSmoothCameraOffsetTrigger" FlagToggleSmoothCameraOffsetTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, + offsetXFrom::Number=0.0, offsetXTo::Number=0.0, offsetYFrom::Number=0.0, offsetYTo::Number=0.0, positionMode::String="NoEffect", onlyOnce::Bool=false, flag::String="flag_toggle_smooth_camera_offset", inverted::Bool=false) + +const placements = Ahorn.PlacementDict( + "Flag Toggle Smooth Camera Offset (Spring Collab 2020)" => Ahorn.EntityPlacement( + FlagToggleSmoothCameraOffsetTrigger, + "rectangle", + ), +) + +function Ahorn.editingOptions(trigger::FlagToggleSmoothCameraOffsetTrigger) + return Dict{String, Any}( + "positionMode" => Maple.trigger_position_modes + ) +end + +end diff --git a/Ahorn/triggers/leaveTheoBehindTrigger.jl b/Ahorn/triggers/leaveTheoBehindTrigger.jl index 05bf6ed..f3adade 100644 --- a/Ahorn/triggers/leaveTheoBehindTrigger.jl +++ b/Ahorn/triggers/leaveTheoBehindTrigger.jl @@ -1,14 +1,14 @@ -module SpringCollab2020LeaveTheoBehindTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/LeaveTheoBehindTrigger" LeaveTheoBehindTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight) - -const placements = Ahorn.PlacementDict( - "Leave Theo Behind (Spring Collab 2020)" => Ahorn.EntityPlacement( - LeaveTheoBehindTrigger, - "rectangle", - ), -) - -end +module SpringCollab2020LeaveTheoBehindTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/LeaveTheoBehindTrigger" LeaveTheoBehindTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight) + +const placements = Ahorn.PlacementDict( + "Leave Theo Behind (Spring Collab 2020)" => Ahorn.EntityPlacement( + LeaveTheoBehindTrigger, + "rectangle", + ), +) + +end diff --git a/Ahorn/triggers/lightningStrikeTrigger.jl b/Ahorn/triggers/lightningStrikeTrigger.jl index eb90ae2..736bf1a 100644 --- a/Ahorn/triggers/lightningStrikeTrigger.jl +++ b/Ahorn/triggers/lightningStrikeTrigger.jl @@ -1,4 +1,4 @@ -module SpringCollab2020LightningStrikeTrigger +module SpringCollab2020LightningStrikeTrigger using ..Ahorn, Maple diff --git a/Ahorn/triggers/madelineSilhouetteTrigger.jl b/Ahorn/triggers/madelineSilhouetteTrigger.jl index 6055211..505e31f 100644 --- a/Ahorn/triggers/madelineSilhouetteTrigger.jl +++ b/Ahorn/triggers/madelineSilhouetteTrigger.jl @@ -1,15 +1,15 @@ -module SpringCollab2020MadelineSilhouetteTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/MadelineSilhouetteTrigger" MadelineSilhouetteTrigger(x::Integer, y::Integer, - width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, enable::Bool=true) - -const placements = Ahorn.PlacementDict( - "Madeline Silhouette (Spring Collab 2020)" => Ahorn.EntityPlacement( - MadelineSilhouetteTrigger, - "rectangle" - ) -) - +module SpringCollab2020MadelineSilhouetteTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/MadelineSilhouetteTrigger" MadelineSilhouetteTrigger(x::Integer, y::Integer, + width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, enable::Bool=true) + +const placements = Ahorn.PlacementDict( + "Madeline Silhouette (Spring Collab 2020)" => Ahorn.EntityPlacement( + MadelineSilhouetteTrigger, + "rectangle" + ) +) + end \ No newline at end of file diff --git a/Ahorn/triggers/musicAreaChangeTrigger.jl b/Ahorn/triggers/musicAreaChangeTrigger.jl index e1263d3..5c3e54f 100644 --- a/Ahorn/triggers/musicAreaChangeTrigger.jl +++ b/Ahorn/triggers/musicAreaChangeTrigger.jl @@ -3,7 +3,7 @@ module SpringCollab2020MusicAreaChangeTrigger using ..Ahorn, Maple @mapdef Trigger "SpringCollab2020/MusicAreaChangeTrigger" MusicAreaChangeTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, - musicParam1::String="", musicParam2::String="", musicParam3::String="", enterValue::Number=1.0, exitValue::Number=0.0) + musicParam1::String="", musicParam2::String="", musicParam3::String="", enterValue::Number=1.0, exitValue::Number=0.0) const placements = Ahorn.PlacementDict( "Music Area Change Trigger (Spring Collab 2020)" => Ahorn.EntityPlacement( diff --git a/Ahorn/triggers/noRefillField.jl b/Ahorn/triggers/noRefillField.jl index 33e8399..fb9f812 100644 --- a/Ahorn/triggers/noRefillField.jl +++ b/Ahorn/triggers/noRefillField.jl @@ -1,14 +1,14 @@ -module SpringCollab2020NoRefillField - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/NoRefillField" NoRefillField(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight) - -const placements = Ahorn.PlacementDict( - "No Refill Field (Spring Collab 2020)" => Ahorn.EntityPlacement( - NoRefillField, - "rectangle", - ), -) - +module SpringCollab2020NoRefillField + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/NoRefillField" NoRefillField(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight) + +const placements = Ahorn.PlacementDict( + "No Refill Field (Spring Collab 2020)" => Ahorn.EntityPlacement( + NoRefillField, + "rectangle", + ), +) + end \ No newline at end of file diff --git a/Ahorn/triggers/pauseBadelineBossesTrigger.jl b/Ahorn/triggers/pauseBadelineBossesTrigger.jl index 80f2b87..50c46da 100644 --- a/Ahorn/triggers/pauseBadelineBossesTrigger.jl +++ b/Ahorn/triggers/pauseBadelineBossesTrigger.jl @@ -1,14 +1,14 @@ -module SpringCollab2020PauseBadelineBossesTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/PauseBadelineBossesTrigger" PauseBadelineBossesTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight) - -const placements = Ahorn.PlacementDict( - "Pause Badeline Bosses (Spring Collab 2020)" => Ahorn.EntityPlacement( - PauseBadelineBossesTrigger, - "rectangle", - ), -) - -end +module SpringCollab2020PauseBadelineBossesTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/PauseBadelineBossesTrigger" PauseBadelineBossesTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight) + +const placements = Ahorn.PlacementDict( + "Pause Badeline Bosses (Spring Collab 2020)" => Ahorn.EntityPlacement( + PauseBadelineBossesTrigger, + "rectangle", + ), +) + +end diff --git a/Ahorn/triggers/removeLightSourcesTrigger.jl b/Ahorn/triggers/removeLightSourcesTrigger.jl index 6f2c5cc..9490297 100644 --- a/Ahorn/triggers/removeLightSourcesTrigger.jl +++ b/Ahorn/triggers/removeLightSourcesTrigger.jl @@ -1,4 +1,4 @@ -module SpringCollab2020RemoveLightSources +module SpringCollab2020RemoveLightSources using ..Ahorn, Maple diff --git a/Ahorn/triggers/smoothCameraOffsetTrigger.jl b/Ahorn/triggers/smoothCameraOffsetTrigger.jl index 51813a8..2b105fc 100644 --- a/Ahorn/triggers/smoothCameraOffsetTrigger.jl +++ b/Ahorn/triggers/smoothCameraOffsetTrigger.jl @@ -1,21 +1,21 @@ -module SpringCollab2020SmoothCameraOffsetTrigger - -using ..Ahorn, Maple - -@mapdef Trigger "SpringCollab2020/SmoothCameraOffsetTrigger" SmoothCameraOffsetTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, - offsetXFrom::Number=0.0, offsetXTo::Number=0.0, offsetYFrom::Number=0.0, offsetYTo::Number=0.0, positionMode::String="NoEffect", onlyOnce::Bool=false) - -const placements = Ahorn.PlacementDict( - "Smooth Camera Offset Trigger (Spring Collab 2020)" => Ahorn.EntityPlacement( - SmoothCameraOffsetTrigger, - "rectangle", - ), -) - -function Ahorn.editingOptions(trigger::SmoothCameraOffsetTrigger) - return Dict{String, Any}( - "positionMode" => Maple.trigger_position_modes - ) -end - +module SpringCollab2020SmoothCameraOffsetTrigger + +using ..Ahorn, Maple + +@mapdef Trigger "SpringCollab2020/SmoothCameraOffsetTrigger" SmoothCameraOffsetTrigger(x::Integer, y::Integer, width::Integer=Maple.defaultTriggerWidth, height::Integer=Maple.defaultTriggerHeight, + offsetXFrom::Number=0.0, offsetXTo::Number=0.0, offsetYFrom::Number=0.0, offsetYTo::Number=0.0, positionMode::String="NoEffect", onlyOnce::Bool=false) + +const placements = Ahorn.PlacementDict( + "Smooth Camera Offset Trigger (Spring Collab 2020)" => Ahorn.EntityPlacement( + SmoothCameraOffsetTrigger, + "rectangle", + ), +) + +function Ahorn.editingOptions(trigger::SmoothCameraOffsetTrigger) + return Dict{String, Any}( + "positionMode" => Maple.trigger_position_modes + ) +end + end \ No newline at end of file diff --git a/Effects/BlackholeCustomColors.cs b/Effects/BlackholeCustomColors.cs index 50f469c..c1f760a 100755 --- a/Effects/BlackholeCustomColors.cs +++ b/Effects/BlackholeCustomColors.cs @@ -1,75 +1,75 @@ -using Microsoft.Xna.Framework; -using Mono.Cecil.Cil; -using Monocle; -using MonoMod.Cil; -using MonoMod.Utils; -using System; - -namespace Celeste.Mod.SpringCollab2020.Effects { - /** - * Port of the blackhole with custom colors from max480's Helping Hand: - * https://github.com/max4805/MaxHelpingHand/blob/master/Effects/BlackholeCustomColors.cs - */ - static class BlackholeCustomColors { - private static Color[] colorsMild; - - public static void Load() { - IL.Celeste.BlackholeBG.ctor += onBlackholeConstructor; - } - - public static void Unload() { - IL.Celeste.BlackholeBG.ctor -= onBlackholeConstructor; - } - - private static void onBlackholeConstructor(ILContext il) { - ILCursor cursor = new ILCursor(il); - - // we want to insert code at the beginning of the constructor, but after the variable initialization code, - // because colorsMild is used right in the constructor. - // the "first line" of the constructor is: bgTexture = GFX.Game["objects/temple/portal/portal"]; - if (cursor.TryGotoNext(MoveType.Before, - instr => instr.MatchLdarg(0), - instr => instr.MatchLdsfld(typeof(GFX), "Game"))) { - - Logger.Log("SpringCollab2020/BlackholeCustomColors", $"Replacing colorsMild at {cursor.Index} in IL code for BlackholeBG constructor"); - cursor.Emit(OpCodes.Ldarg_0); - cursor.EmitDelegate>(self => { - if (colorsMild != null) { - new DynData(self)["colorsMild"] = colorsMild; - colorsMild = null; - } - }); - } - } - - public static BlackholeBG CreateBlackholeWithCustomColors(BinaryPacker.Element effectData) { - // set up colorsMild for the hook above. we can't use DynData to pass this over, since the object does not exist yet! - colorsMild = parseColors(effectData.Attr("colorsMild", "6e3199,851f91,3026b0")); - for (int i = 0; i < colorsMild.Length; i++) { - colorsMild[i] *= 0.8f; - } - - // build the blackhole: the hook will take care of setting colorsMild. - BlackholeBG blackhole = new BlackholeBG(); - - // ... now we've got to set everything else. - DynData blackholeData = new DynData(blackhole); - blackholeData["colorsWild"] = parseColors(effectData.Attr("colorsWild", "ca4ca7,b14cca,ca4ca7")); - blackholeData["bgColorInner"] = Calc.HexToColor(effectData.Attr("bgColorInner", "000000")); - blackholeData["bgColorOuterMild"] = Calc.HexToColor(effectData.Attr("bgColorOuterMild", "512a8b")); - blackholeData["bgColorOuterWild"] = Calc.HexToColor(effectData.Attr("bgColorOuterWild", "bd2192")); - blackhole.Alpha = effectData.AttrFloat("alpha", 1f); - - return blackhole; - } - - private static Color[] parseColors(string input) { - string[] colorsAsStrings = input.Split(','); - Color[] colors = new Color[colorsAsStrings.Length]; - for (int i = 0; i < colors.Length; i++) { - colors[i] = Calc.HexToColor(colorsAsStrings[i]); - } - return colors; - } - } +using Microsoft.Xna.Framework; +using Mono.Cecil.Cil; +using Monocle; +using MonoMod.Cil; +using MonoMod.Utils; +using System; + +namespace Celeste.Mod.SpringCollab2020.Effects { + /** + * Port of the blackhole with custom colors from max480's Helping Hand: + * https://github.com/max4805/MaxHelpingHand/blob/master/Effects/BlackholeCustomColors.cs + */ + static class BlackholeCustomColors { + private static Color[] colorsMild; + + public static void Load() { + IL.Celeste.BlackholeBG.ctor += onBlackholeConstructor; + } + + public static void Unload() { + IL.Celeste.BlackholeBG.ctor -= onBlackholeConstructor; + } + + private static void onBlackholeConstructor(ILContext il) { + ILCursor cursor = new ILCursor(il); + + // we want to insert code at the beginning of the constructor, but after the variable initialization code, + // because colorsMild is used right in the constructor. + // the "first line" of the constructor is: bgTexture = GFX.Game["objects/temple/portal/portal"]; + if (cursor.TryGotoNext(MoveType.Before, + instr => instr.MatchLdarg(0), + instr => instr.MatchLdsfld(typeof(GFX), "Game"))) { + + Logger.Log("SpringCollab2020/BlackholeCustomColors", $"Replacing colorsMild at {cursor.Index} in IL code for BlackholeBG constructor"); + cursor.Emit(OpCodes.Ldarg_0); + cursor.EmitDelegate>(self => { + if (colorsMild != null) { + new DynData(self)["colorsMild"] = colorsMild; + colorsMild = null; + } + }); + } + } + + public static BlackholeBG CreateBlackholeWithCustomColors(BinaryPacker.Element effectData) { + // set up colorsMild for the hook above. we can't use DynData to pass this over, since the object does not exist yet! + colorsMild = parseColors(effectData.Attr("colorsMild", "6e3199,851f91,3026b0")); + for (int i = 0; i < colorsMild.Length; i++) { + colorsMild[i] *= 0.8f; + } + + // build the blackhole: the hook will take care of setting colorsMild. + BlackholeBG blackhole = new BlackholeBG(); + + // ... now we've got to set everything else. + DynData blackholeData = new DynData(blackhole); + blackholeData["colorsWild"] = parseColors(effectData.Attr("colorsWild", "ca4ca7,b14cca,ca4ca7")); + blackholeData["bgColorInner"] = Calc.HexToColor(effectData.Attr("bgColorInner", "000000")); + blackholeData["bgColorOuterMild"] = Calc.HexToColor(effectData.Attr("bgColorOuterMild", "512a8b")); + blackholeData["bgColorOuterWild"] = Calc.HexToColor(effectData.Attr("bgColorOuterWild", "bd2192")); + blackhole.Alpha = effectData.AttrFloat("alpha", 1f); + + return blackhole; + } + + private static Color[] parseColors(string input) { + string[] colorsAsStrings = input.Split(','); + Color[] colors = new Color[colorsAsStrings.Length]; + for (int i = 0; i < colors.Length; i++) { + colors[i] = Calc.HexToColor(colorsAsStrings[i]); + } + return colors; + } + } } \ No newline at end of file diff --git a/Effects/CustomSnow.cs b/Effects/CustomSnow.cs index ec39b34..fff87be 100755 --- a/Effects/CustomSnow.cs +++ b/Effects/CustomSnow.cs @@ -1,27 +1,27 @@ -using Microsoft.Xna.Framework; -using MonoMod.Utils; -using System; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Effects { - public class CustomSnow : Snow { - private static MethodInfo particleInit = typeof(Snow).GetNestedType("Particle", BindingFlags.NonPublic).GetMethod("Init"); - - public CustomSnow(Color[] colors, bool foreground) : base(foreground) { - DynData selfData = new DynData(this); - - // redo the same operations as the vanilla constructor, but with our custom set of colors. - selfData["colors"] = colors; - selfData["blendedColors"] = new Color[colors.Length]; - Array particles = selfData.Get("particles"); - int speedMin = foreground ? 120 : 40; - int speedMax = foreground ? 300 : 100; - for (int i = 0; i < particles.Length; i++) { - // Particle is a private struct, so getting it gets a copy that we should set back afterwards. - object particle = particles.GetValue(i); - particleInit.Invoke(particle, new object[] { colors.Length, speedMin, speedMax }); - particles.SetValue(particle, i); - } - } - } -} +using Microsoft.Xna.Framework; +using MonoMod.Utils; +using System; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Effects { + public class CustomSnow : Snow { + private static MethodInfo particleInit = typeof(Snow).GetNestedType("Particle", BindingFlags.NonPublic).GetMethod("Init"); + + public CustomSnow(Color[] colors, bool foreground) : base(foreground) { + DynData selfData = new DynData(this); + + // redo the same operations as the vanilla constructor, but with our custom set of colors. + selfData["colors"] = colors; + selfData["blendedColors"] = new Color[colors.Length]; + Array particles = selfData.Get("particles"); + int speedMin = foreground ? 120 : 40; + int speedMax = foreground ? 300 : 100; + for (int i = 0; i < particles.Length; i++) { + // Particle is a private struct, so getting it gets a copy that we should set back afterwards. + object particle = particles.GetValue(i); + particleInit.Invoke(particle, new object[] { colors.Length, speedMin, speedMax }); + particles.SetValue(particle, i); + } + } + } +} diff --git a/Effects/HeatWaveNoColorGrade.cs b/Effects/HeatWaveNoColorGrade.cs index b76bbca..df45c23 100644 --- a/Effects/HeatWaveNoColorGrade.cs +++ b/Effects/HeatWaveNoColorGrade.cs @@ -1,52 +1,52 @@ -using Monocle; -using MonoMod.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Celeste.Mod.SpringCollab2020.Effects { - /// - /// A heatwave effect that does not affect the colorgrade when it is hidden. - /// - class HeatWaveNoColorGrade : HeatWave { - private DynData self = new DynData(); - - public HeatWaveNoColorGrade() : base() { - self = new DynData(this); - } - - public override void Update(Scene scene) { - Level level = scene as Level; - bool show = (IsVisible(level) && level.CoreMode != Session.CoreModes.None); - - if (!show) { - // if not fading out, the heatwave is invisible, so don't even bother updating it. - if (self.Get("fade") > 0) { - // be sure to lock color grading to prevent it from becoming "none". - DynData levelData = new DynData(level); - - float colorGradeEase = levelData.Get("colorGradeEase"); - float colorGradeEaseSpeed = levelData.Get("colorGradeEaseSpeed"); - string colorGrade = level.Session.ColorGrade; - - base.Update(scene); - - levelData["colorGradeEase"] = colorGradeEase; - levelData["colorGradeEaseSpeed"] = colorGradeEaseSpeed; - level.Session.ColorGrade = colorGrade; - - if (self.Get("heat") <= 0) { - // the heat hit 0, we should now restore the water sine direction - // ... because if we don't, waterfalls will flow backwards - Distort.WaterSineDirection = 1f; - } - } - } else { - // heat wave is visible: update as usual. - base.Update(scene); - } - } - } -} +using Monocle; +using MonoMod.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Celeste.Mod.SpringCollab2020.Effects { + /// + /// A heatwave effect that does not affect the colorgrade when it is hidden. + /// + class HeatWaveNoColorGrade : HeatWave { + private DynData self = new DynData(); + + public HeatWaveNoColorGrade() : base() { + self = new DynData(this); + } + + public override void Update(Scene scene) { + Level level = scene as Level; + bool show = (IsVisible(level) && level.CoreMode != Session.CoreModes.None); + + if (!show) { + // if not fading out, the heatwave is invisible, so don't even bother updating it. + if (self.Get("fade") > 0) { + // be sure to lock color grading to prevent it from becoming "none". + DynData levelData = new DynData(level); + + float colorGradeEase = levelData.Get("colorGradeEase"); + float colorGradeEaseSpeed = levelData.Get("colorGradeEaseSpeed"); + string colorGrade = level.Session.ColorGrade; + + base.Update(scene); + + levelData["colorGradeEase"] = colorGradeEase; + levelData["colorGradeEaseSpeed"] = colorGradeEaseSpeed; + level.Session.ColorGrade = colorGrade; + + if (self.Get("heat") <= 0) { + // the heat hit 0, we should now restore the water sine direction + // ... because if we don't, waterfalls will flow backwards + Distort.WaterSineDirection = 1f; + } + } + } else { + // heat wave is visible: update as usual. + base.Update(scene); + } + } + } +} diff --git a/Entities/AnimatedJumpthruPlatform.cs b/Entities/AnimatedJumpthruPlatform.cs index becc008..0fcacbc 100644 --- a/Entities/AnimatedJumpthruPlatform.cs +++ b/Entities/AnimatedJumpthruPlatform.cs @@ -1,42 +1,42 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/AnimatedJumpthruPlatform")] - class AnimatedJumpthruPlatform : JumpthruPlatform { - private string animationPath; - private float animationDelay; - private int columns; - - public AnimatedJumpthruPlatform(EntityData data, Vector2 offset) : base(data, offset) { - columns = data.Width / 8; - animationPath = data.Attr("animationPath"); - animationDelay = data.Float("animationDelay"); - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - // clean up all visuals for the jumpthru - List componentsToRemove = new List(); - foreach (Component component in this) { - if (component is Image) { - componentsToRemove.Add(component); - } - } - foreach (Component toRemove in componentsToRemove) { - toRemove.RemoveSelf(); - } - - for (int i = 0; i < columns; i++) { - Sprite jumpthruSprite = new Sprite(GFX.Game, "objects/jumpthru/" + animationPath); - jumpthruSprite.AddLoop("idle", "", animationDelay); - jumpthruSprite.X = i * 8; - jumpthruSprite.Play("idle"); - Add(jumpthruSprite); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/AnimatedJumpthruPlatform")] + class AnimatedJumpthruPlatform : JumpthruPlatform { + private string animationPath; + private float animationDelay; + private int columns; + + public AnimatedJumpthruPlatform(EntityData data, Vector2 offset) : base(data, offset) { + columns = data.Width / 8; + animationPath = data.Attr("animationPath"); + animationDelay = data.Float("animationDelay"); + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + // clean up all visuals for the jumpthru + List componentsToRemove = new List(); + foreach (Component component in this) { + if (component is Image) { + componentsToRemove.Add(component); + } + } + foreach (Component toRemove in componentsToRemove) { + toRemove.RemoveSelf(); + } + + for (int i = 0; i < columns; i++) { + Sprite jumpthruSprite = new Sprite(GFX.Game, "objects/jumpthru/" + animationPath); + jumpthruSprite.AddLoop("idle", "", animationDelay); + jumpthruSprite.X = i * 8; + jumpthruSprite.Play("idle"); + Add(jumpthruSprite); + } + } + } +} diff --git a/Entities/AttachedIceWall.cs b/Entities/AttachedIceWall.cs index 42184b8..b1d3f72 100755 --- a/Entities/AttachedIceWall.cs +++ b/Entities/AttachedIceWall.cs @@ -1,19 +1,19 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/AttachedIceWall")] - static class AttachedIceWall { - public static Entity Load(Level level, LevelData levelData, Vector2 offset, EntityData entityData) { - // an attached ice wall is just like a regular "not core mode" wall booster, but with a different static mover hitbox. - bool left = entityData.Bool("left"); - WallBooster iceWall = new WallBooster(entityData.Position + offset, entityData.Height, left, notCoreMode: true); - StaticMover staticMover = iceWall.Get(); - staticMover.SolidChecker = solid => iceWall.CollideCheck(solid, iceWall.Position + (left ? -2 : 2) * Vector2.UnitX); - staticMover.OnAttach = platform => iceWall.Depth = platform.Depth + 1; - iceWall.Get().RemoveSelf(); - return iceWall; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/AttachedIceWall")] + static class AttachedIceWall { + public static Entity Load(Level level, LevelData levelData, Vector2 offset, EntityData entityData) { + // an attached ice wall is just like a regular "not core mode" wall booster, but with a different static mover hitbox. + bool left = entityData.Bool("left"); + WallBooster iceWall = new WallBooster(entityData.Position + offset, entityData.Height, left, notCoreMode: true); + StaticMover staticMover = iceWall.Get(); + staticMover.SolidChecker = solid => iceWall.CollideCheck(solid, iceWall.Position + (left ? -2 : 2) * Vector2.UnitX); + staticMover.OnAttach = platform => iceWall.Depth = platform.Depth + 1; + iceWall.Get().RemoveSelf(); + return iceWall; + } + } +} diff --git a/Entities/BubblePushField.cs b/Entities/BubblePushField.cs index 168dc94..345cfc5 100644 --- a/Entities/BubblePushField.cs +++ b/Entities/BubblePushField.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Celeste.Mod.Entities; using System; using System.Collections.Generic; @@ -82,7 +82,7 @@ public override void Update() { base.Update(); Session session = SceneAs().Session; - if ((activationMode == ActivationMode.OnlyWhenFlagActive && !session.GetFlag(flag)) + if ((activationMode == ActivationMode.OnlyWhenFlagActive && !session.GetFlag(flag)) || (activationMode == ActivationMode.OnlyWhenFlagInactive && session.GetFlag(flag))) { // the bubble push field is currently turned off by a session flag. @@ -96,7 +96,7 @@ public override void Update() { Add(new BubbleParticle(true, true)); } - foreach (WindMover mover in Scene.Tracker.GetComponents()) { + foreach (WindMover mover in Scene.Tracker.GetComponents()) { if(mover.Entity.CollideCheck(this)) { if (WindMovers.ContainsKey(mover)) WindMovers[mover] = Calc.Approach(WindMovers[mover], Strength, Engine.DeltaTime / .6f); @@ -137,7 +137,7 @@ public override void Update() { Player tempPlayer = (Player) mover.Entity; if (tempPlayer.Holding != null || tempPlayer.Dead) return; - + mover.Move(new Vector2(0f, -UpwardStrength)); } diff --git a/Entities/BubbleReturnBerry.cs b/Entities/BubbleReturnBerry.cs index 22bcfbb..fc6a02d 100644 --- a/Entities/BubbleReturnBerry.cs +++ b/Entities/BubbleReturnBerry.cs @@ -1,7 +1,7 @@ -using Celeste.Mod.Entities; +using Celeste.Mod.Entities; using Microsoft.Xna.Framework; using Monocle; -using MonoMod.RuntimeDetour; +using MonoMod.RuntimeDetour; using System; using System.Collections; @@ -22,25 +22,25 @@ public static void Load() { public static void Unload() { On.Celeste.Player.ctor -= onPlayerConstructor; - } - - private static void onPlayerConstructor(On.Celeste.Player.orig_ctor orig, Player self, Vector2 position, PlayerSpriteMode spriteMode) { - orig(self, position, spriteMode); - - Collision originalSquishCallback = self.SquishCallback; - - Collision patchedSquishCallback = collisionData => { - // State 21 is the state where the player is located within the Cassette Bubble. - // They shouldn't be squished by moving blocks inside of it, so we prevent that. - if (self.StateMachine.State == 21) - return; - - originalSquishCallback(collisionData); - }; - - self.SquishCallback = patchedSquishCallback; - } - + } + + private static void onPlayerConstructor(On.Celeste.Player.orig_ctor orig, Player self, Vector2 position, PlayerSpriteMode spriteMode) { + orig(self, position, spriteMode); + + Collision originalSquishCallback = self.SquishCallback; + + Collision patchedSquishCallback = collisionData => { + // State 21 is the state where the player is located within the Cassette Bubble. + // They shouldn't be squished by moving blocks inside of it, so we prevent that. + if (self.StateMachine.State == 21) + return; + + originalSquishCallback(collisionData); + }; + + self.SquishCallback = patchedSquishCallback; + } + public new void OnPlayer(Player player) { base.OnPlayer(player); diff --git a/Entities/CassetteFriendlyStrawberry.cs b/Entities/CassetteFriendlyStrawberry.cs index 257277d..ec5b178 100644 --- a/Entities/CassetteFriendlyStrawberry.cs +++ b/Entities/CassetteFriendlyStrawberry.cs @@ -1,24 +1,24 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// Just a strawberry with cassette-friendly strawberry seeds. - /// - [CustomEntity("SpringCollab2020/CassetteFriendlyStrawberry")] - [RegisterStrawberry(true, false)] - class CassetteFriendlyStrawberry : Strawberry { - public CassetteFriendlyStrawberry(EntityData data, Vector2 offset, EntityID gid) : base(data, offset, gid) { - bool isGhostBerry = SaveData.Instance.CheckStrawberry(ID); - - // we just want to create cassette-friendly strawberry seeds to replace vanilla seeds. - if (data.Nodes != null && data.Nodes.Length != 0) { - Seeds = new List(); - for (int i = 0; i < data.Nodes.Length; i++) { - Seeds.Add(new CassetteFriendlyStrawberrySeed(this, offset + data.Nodes[i], i, isGhostBerry)); - } - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// Just a strawberry with cassette-friendly strawberry seeds. + /// + [CustomEntity("SpringCollab2020/CassetteFriendlyStrawberry")] + [RegisterStrawberry(true, false)] + class CassetteFriendlyStrawberry : Strawberry { + public CassetteFriendlyStrawberry(EntityData data, Vector2 offset, EntityID gid) : base(data, offset, gid) { + bool isGhostBerry = SaveData.Instance.CheckStrawberry(ID); + + // we just want to create cassette-friendly strawberry seeds to replace vanilla seeds. + if (data.Nodes != null && data.Nodes.Length != 0) { + Seeds = new List(); + for (int i = 0; i < data.Nodes.Length; i++) { + Seeds.Add(new CassetteFriendlyStrawberrySeed(this, offset + data.Nodes[i], i, isGhostBerry)); + } + } + } + } +} diff --git a/Entities/CassetteFriendlyStrawberrySeed.cs b/Entities/CassetteFriendlyStrawberrySeed.cs index 046decc..c4f926a 100644 --- a/Entities/CassetteFriendlyStrawberrySeed.cs +++ b/Entities/CassetteFriendlyStrawberrySeed.cs @@ -1,42 +1,42 @@ -using Microsoft.Xna.Framework; -using Monocle; -using System.Linq; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// Strawberry seeds that deal nicer with being hidden in cassette blocks. - /// - Their depth is adjusted to appear in front of disabled cassette blocks, but behind enabled ones. - /// - They are not "attached", meaning they won't disappear when the cassette block disappears. - /// - class CassetteFriendlyStrawberrySeed : StrawberrySeed { - - private bool isInCassetteBlock; - - public CassetteFriendlyStrawberrySeed(Strawberry strawberry, Vector2 position, int index, bool ghost) - : base(strawberry, position, index, ghost) { } - - public override void Added(Scene scene) { - if (scene.Entities.OfType().Any(block => block.Collider.Bounds.Contains(Collider.Bounds))) { - // our seed is entirely inside a cassette block: look for the static mover. - foreach (Component component in this) { - if (component is StaticMover mover) { - Remove(mover); // get rid of behavior like "disappear with cassette block" or "get double-size hitbox" - Depth = 11; // display just below active cassette blocks - isInCassetteBlock = true; - break; - } - } - } - - base.Added(scene); - } - - public override void Update() { - base.Update(); - - if (isInCassetteBlock && !Visible) { - Depth = 11; // reset depth when losing the seed - } - } - } -} +using Microsoft.Xna.Framework; +using Monocle; +using System.Linq; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// Strawberry seeds that deal nicer with being hidden in cassette blocks. + /// - Their depth is adjusted to appear in front of disabled cassette blocks, but behind enabled ones. + /// - They are not "attached", meaning they won't disappear when the cassette block disappears. + /// + class CassetteFriendlyStrawberrySeed : StrawberrySeed { + + private bool isInCassetteBlock; + + public CassetteFriendlyStrawberrySeed(Strawberry strawberry, Vector2 position, int index, bool ghost) + : base(strawberry, position, index, ghost) { } + + public override void Added(Scene scene) { + if (scene.Entities.OfType().Any(block => block.Collider.Bounds.Contains(Collider.Bounds))) { + // our seed is entirely inside a cassette block: look for the static mover. + foreach (Component component in this) { + if (component is StaticMover mover) { + Remove(mover); // get rid of behavior like "disappear with cassette block" or "get double-size hitbox" + Depth = 11; // display just below active cassette blocks + isInCassetteBlock = true; + break; + } + } + } + + base.Added(scene); + } + + public override void Update() { + base.Update(); + + if (isInCassetteBlock && !Visible) { + Depth = 11; // reset depth when losing the seed + } + } + } +} diff --git a/Entities/CaveWall.cs b/Entities/CaveWall.cs index 6fbdeb8..15d211a 100644 --- a/Entities/CaveWall.cs +++ b/Entities/CaveWall.cs @@ -1,4 +1,4 @@ -using Celeste.Mod.Entities; +using Celeste.Mod.Entities; using Microsoft.Xna.Framework; using Monocle; using System; @@ -22,26 +22,26 @@ public class CaveWall : Entity { private float transitionStartAlpha; - private bool transitionFade; - - private CaveWall master; - - private bool awake; - - public List Group; - - public Point GroupBoundsMin; - - public Point GroupBoundsMax; - - public bool HasGroup { - get; - private set; - } - - public bool MasterOfGroup { - get; - private set; + private bool transitionFade; + + private CaveWall master; + + private bool awake; + + public List Group; + + public Point GroupBoundsMin; + + public Point GroupBoundsMax; + + public bool HasGroup { + get; + private set; + } + + public bool MasterOfGroup { + get; + private set; } public CaveWall(Vector2 position, char tile, float width, float height, bool disableTransitionFading) @@ -58,44 +58,44 @@ public CaveWall(EntityData data, Vector2 offset) } public override void Awake(Scene scene) { - base.Awake(scene); - - awake = true; - if (!HasGroup) { - MasterOfGroup = true; - Group = new List(); - GroupBoundsMin = new Point((int) X, (int) Y); - GroupBoundsMax = new Point((int) Right, (int) Bottom); - AddToGroupAndFindChildren(this); - - Rectangle rectangle = new Rectangle(GroupBoundsMin.X / 8, GroupBoundsMin.Y / 8, (GroupBoundsMax.X - GroupBoundsMin.X) / 8 + 1, (GroupBoundsMax.Y - GroupBoundsMin.Y) / 8 + 1); + base.Awake(scene); + + awake = true; + if (!HasGroup) { + MasterOfGroup = true; + Group = new List(); + GroupBoundsMin = new Point((int) X, (int) Y); + GroupBoundsMax = new Point((int) Right, (int) Bottom); + AddToGroupAndFindChildren(this); + + Rectangle rectangle = new Rectangle(GroupBoundsMin.X / 8, GroupBoundsMin.Y / 8, (GroupBoundsMax.X - GroupBoundsMin.X) / 8 + 1, (GroupBoundsMax.Y - GroupBoundsMin.Y) / 8 + 1); Level level = SceneAs(); - Rectangle tileBounds = level.Session.MapData.TileBounds; - VirtualMap virtualMap = level.SolidsData.Clone(); - foreach (CaveWall item in Group) { - int x = (int) (item.X / 8f) - level.Session.MapData.TileBounds.X; - int y = (int) (item.Y / 8f - level.Session.MapData.TileBounds.Y); - int tilesX = (int) (item.Width / 8f); - int tilesY = (int) (item.Height / 8f); - for (int i = x; i < x + tilesX; i++) { - for (int j = y; j < y + tilesY; j++) { - virtualMap[i, j] = item.fillTile; - } - } - } - // Pretend that the CaveWalls are just part of the map, then draw the CaveWalls on top of it - // This is a complicated solution to getting the CaveWalls to blend with both the foregroundTiles and each other + Rectangle tileBounds = level.Session.MapData.TileBounds; + VirtualMap virtualMap = level.SolidsData.Clone(); + foreach (CaveWall item in Group) { + int x = (int) (item.X / 8f) - level.Session.MapData.TileBounds.X; + int y = (int) (item.Y / 8f - level.Session.MapData.TileBounds.Y); + int tilesX = (int) (item.Width / 8f); + int tilesY = (int) (item.Height / 8f); + for (int i = x; i < x + tilesX; i++) { + for (int j = y; j < y + tilesY; j++) { + virtualMap[i, j] = item.fillTile; + } + } + } + // Pretend that the CaveWalls are just part of the map, then draw the CaveWalls on top of it + // This is a complicated solution to getting the CaveWalls to blend with both the foregroundTiles and each other foreach (CaveWall item in Group) { int x = (int) item.X / 8 - tileBounds.Left; - int y = (int) item.Y / 8 - tileBounds.Top; + int y = (int) item.Y / 8 - tileBounds.Top; int tilesX = (int) item.Width / 8; int tilesY = (int) item.Height / 8; - item.tiles = GFX.FGAutotiler.GenerateOverlay(item.fillTile, x, y, tilesX, tilesY, virtualMap).TileGrid; - item.Add(item.tiles); + item.tiles = GFX.FGAutotiler.GenerateOverlay(item.fillTile, x, y, tilesX, tilesY, virtualMap).TileGrid; + item.Add(item.tiles); item.Add(new TileInterceptor(item.tiles, highPriority: false)); - } + } } - TryToInitPosition(); + TryToInitPosition(); if (CollideCheck()) { tiles.Alpha = 0f; @@ -104,51 +104,51 @@ public override void Awake(Scene scene) { cutout.Visible = false; } - if (!disableTransitionFading) { - TransitionListener transitionListener = new TransitionListener(); - transitionListener.OnOut = OnTransitionOut; - transitionListener.OnOutBegin = OnTransitionOutBegin; - transitionListener.OnIn = OnTransitionIn; - transitionListener.OnInBegin = OnTransitionInBegin; - Add(transitionListener); + if (!disableTransitionFading) { + TransitionListener transitionListener = new TransitionListener(); + transitionListener.OnOut = OnTransitionOut; + transitionListener.OnOutBegin = OnTransitionOutBegin; + transitionListener.OnIn = OnTransitionIn; + transitionListener.OnInBegin = OnTransitionInBegin; + Add(transitionListener); + } + } + + private void TryToInitPosition() { + if (MasterOfGroup) { + foreach (CaveWall item in Group) { + if (!item.awake) { + return; + } + } + } else { + master.TryToInitPosition(); + } + } + + private void AddToGroupAndFindChildren(CaveWall from) { + if (from.X < GroupBoundsMin.X) { + GroupBoundsMin.X = (int) from.X; + } + if (from.Y < GroupBoundsMin.Y) { + GroupBoundsMin.Y = (int) from.Y; + } + if (from.Right > GroupBoundsMax.X) { + GroupBoundsMax.X = (int) from.Right; + } + if (from.Bottom > GroupBoundsMax.Y) { + GroupBoundsMax.Y = (int) from.Bottom; + } + from.HasGroup = true; + Group.Add(from); + if (from != this) { + from.master = this; + } + foreach (CaveWall entity in Scene.Tracker.GetEntities()) { + if (!entity.HasGroup && (Scene.CollideCheck(new Rectangle((int) from.X - 1, (int) from.Y, (int) from.Width + 2, (int) from.Height), entity) || Scene.CollideCheck(new Rectangle((int) from.X, (int) from.Y - 1, (int) from.Width, (int) from.Height + 2), entity))) { + AddToGroupAndFindChildren(entity); + } } - } - - private void TryToInitPosition() { - if (MasterOfGroup) { - foreach (CaveWall item in Group) { - if (!item.awake) { - return; - } - } - } else { - master.TryToInitPosition(); - } - } - - private void AddToGroupAndFindChildren(CaveWall from) { - if (from.X < GroupBoundsMin.X) { - GroupBoundsMin.X = (int) from.X; - } - if (from.Y < GroupBoundsMin.Y) { - GroupBoundsMin.Y = (int) from.Y; - } - if (from.Right > GroupBoundsMax.X) { - GroupBoundsMax.X = (int) from.Right; - } - if (from.Bottom > GroupBoundsMax.Y) { - GroupBoundsMax.Y = (int) from.Bottom; - } - from.HasGroup = true; - Group.Add(from); - if (from != this) { - from.master = this; - } - foreach (CaveWall entity in Scene.Tracker.GetEntities()) { - if (!entity.HasGroup && (Scene.CollideCheck(new Rectangle((int) from.X - 1, (int) from.Y, (int) from.Width + 2, (int) from.Height), entity) || Scene.CollideCheck(new Rectangle((int) from.X, (int) from.Y - 1, (int) from.Width, (int) from.Height + 2), entity))) { - AddToGroupAndFindChildren(entity); - } - } } private void OnTransitionOutBegin() { @@ -167,10 +167,10 @@ private void OnTransitionOut(float percent) { } private void OnTransitionInBegin() { - Level level = SceneAs(); + Level level = SceneAs(); if (MasterOfGroup) { Player player = null; - foreach (CaveWall entity in Group) { + foreach (CaveWall entity in Group) { if (level.PreviousBounds.HasValue && Collide.CheckRect(entity, level.PreviousBounds.Value)) { entity.transitionFade = true; entity.tiles.Alpha = 0f; @@ -192,7 +192,7 @@ private void OnTransitionInBegin() { } } - private void OnTransitionIn(float percent) { + private void OnTransitionIn(float percent) { if (transitionFade) { tiles.Alpha = percent; } @@ -212,27 +212,27 @@ public override void Update() { if (tiles.Alpha >= 1f) { tiles.Alpha = 1; } - } - - if (MasterOfGroup) { - Player player = null; + } + + if (MasterOfGroup) { + Player player = null; foreach (CaveWall entity in Group) { - player = entity.CollideFirst(); + player = entity.CollideFirst(); if (player != null) { break; } - } - // The wall shouldn't fade out when a player dies in it - PlayerDeadBody playerDeadBody = null; - bool bodyCollided = false; + } + // The wall shouldn't fade out when a player dies in it + PlayerDeadBody playerDeadBody = null; + bool bodyCollided = false; if ((playerDeadBody = SceneAs().Entities.FindFirst()) != null) { - foreach (CaveWall entity in Group) { + foreach (CaveWall entity in Group) { bodyCollided = playerDeadBody.Position.X >= entity.Left && playerDeadBody.Position.X <= entity.Right && playerDeadBody.Position.Y >= entity.Top && playerDeadBody.Position.Y <= entity.Bottom; if (bodyCollided) { break; } - } - } + } + } if ((player != null && player.StateMachine.State != 9) || bodyCollided) { fadeOut = true; @@ -246,7 +246,7 @@ public override void Update() { entity.fadeOut = false; entity.fadeIn = true; } - } + } } } diff --git a/Entities/CrystalBombDetonator.cs b/Entities/CrystalBombDetonator.cs index 5504331..0c78df8 100644 --- a/Entities/CrystalBombDetonator.cs +++ b/Entities/CrystalBombDetonator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Reflection; using Celeste.Mod.Entities; diff --git a/Entities/CrystalBombDetonatorRenderer.cs b/Entities/CrystalBombDetonatorRenderer.cs index c77eaa6..6c13b89 100644 --- a/Entities/CrystalBombDetonatorRenderer.cs +++ b/Entities/CrystalBombDetonatorRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Monocle; @@ -40,7 +40,7 @@ public void Track(CrystalBombDetonator block) { levelTileBounds = (Scene as Level).TileBounds; tiles = new VirtualMap(levelTileBounds.Width, levelTileBounds.Height, false); } - + for (int xTile = (int)block.X / 8; xTile < (block.Right / 8f); xTile++) for (int yTile = (int)block.Y / 8; yTile < (block.Bottom / 8f); yTile++) tiles[xTile - levelTileBounds.X, yTile - levelTileBounds.Y] = true; @@ -145,7 +145,7 @@ private void OnRenderBloom() { public override void Render() { if (trackedFields.Count <= 0) return; - + Color color = Color.Purple * 0.45f; Color value = Color.Purple * 0.55f; foreach (CrystalBombDetonator crystalBombDetonator in trackedFields) { @@ -203,7 +203,7 @@ private float GetWaveAt(float offset, float along, float length) { if (Parent.Solidify >= 1f) return 0f; - + float num = offset + along * 0.25f; float num2 = (float) (Math.Sin((double) num) * 2.0 + Math.Sin((double) (num * 0.25f))); return (1f + num2 * Ease.SineInOut(Calc.YoYo(along / length))) * (1f - Parent.Solidify); diff --git a/Entities/CustomBirdTutorial.cs b/Entities/CustomBirdTutorial.cs index b0adba9..8571a08 100644 --- a/Entities/CustomBirdTutorial.cs +++ b/Entities/CustomBirdTutorial.cs @@ -1,120 +1,120 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; -using System.Collections.Generic; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/CustomBirdTutorial")] - [Tracked] - class CustomBirdTutorial : BirdNPC { - public string BirdId; - private bool onlyOnce; - private bool caw; - - private bool triggered = false; - private bool flewAway = false; - - private BirdTutorialGui gui; - - private static Dictionary directions = new Dictionary() { - { "Left", new Vector2(-1, 0) }, - { "Right", new Vector2(1, 0) }, - { "Up", new Vector2(0, -1) }, - { "Down", new Vector2(0, 1) }, - { "UpLeft", new Vector2(-1, -1) }, - { "UpRight", new Vector2(1, -1) }, - { "DownLeft", new Vector2(-1, 1) }, - { "DownRight", new Vector2(1, 1) } - }; - - public CustomBirdTutorial(EntityData data, Vector2 offset) : base(data, offset) { - BirdId = data.Attr("birdId"); - onlyOnce = data.Bool("onlyOnce"); - caw = data.Bool("caw"); - Facing = data.Bool("faceLeft") ? Facings.Left : Facings.Right; - - object info; - object[] controls; - - // parse the info ("title") - string infoString = data.Attr("info"); - if (GFX.Gui.Has(infoString)) { - info = GFX.Gui[infoString]; - } else { - info = Dialog.Clean(infoString); - } - - int extraAdvance = 0; - - // go ahead and parse the controls. Controls can be textures, VirtualButtons, directions or strings. - string[] controlsStrings = data.Attr("controls").Split(','); - controls = new object[controlsStrings.Length]; - for (int i = 0; i < controls.Length; i++) { - string controlString = controlsStrings[i]; - - if (GFX.Gui.Has(controlString)) { - // this is a texture. - controls[i] = GFX.Gui[controlString]; - } else if (directions.ContainsKey(controlString)) { - // this is a direction. - controls[i] = directions[controlString]; - } else { - FieldInfo matchingInput = typeof(Input).GetField(controlString, BindingFlags.Static | BindingFlags.Public); - if (matchingInput?.GetValue(null)?.GetType() == typeof(VirtualButton)) { - // this is a button. - controls[i] = matchingInput.GetValue(null); - } else { - // when BirdTutorialGui renders text, it is offset by 1px on the right. - // width computation doesn't take this 1px into account, so we should add it back in. - extraAdvance++; - if (i == 0) { - // as the text is rendered 1px to the right, if the first thing is a string, there will be 1px more padding on the left. - // we should add that extra px on the right as well. - extraAdvance++; - } - - if (controlString.StartsWith("dialog:")) { - // treat that as a dialog key. - controls[i] = Dialog.Clean(controlString.Substring("dialog:".Length)); - } else { - // treat that as a plain string. - controls[i] = controlString; - } - } - } - } - - gui = new BirdTutorialGui(this, new Vector2(0f, -16f), info, controls); - - DynData guiData = new DynData(gui); - // if there is no first line, resize the bubble accordingly. - if (string.IsNullOrEmpty(infoString)) { - guiData["infoHeight"] = 0f; - } - // apply the extra width. - guiData["controlsWidth"] = guiData.Get("controlsWidth") + extraAdvance; - } - - public void TriggerShowTutorial() { - if (!triggered) { - triggered = true; - Add(new Coroutine(ShowTutorial(gui, caw))); - } - } - - public void TriggerHideTutorial() { - if (triggered && !flewAway) { - flewAway = true; - - Add(new Coroutine(HideTutorial())); - Add(new Coroutine(StartleAndFlyAway())); - - if (onlyOnce) { - SceneAs().Session.DoNotLoad.Add(EntityID); - } - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; +using System.Collections.Generic; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/CustomBirdTutorial")] + [Tracked] + class CustomBirdTutorial : BirdNPC { + public string BirdId; + private bool onlyOnce; + private bool caw; + + private bool triggered = false; + private bool flewAway = false; + + private BirdTutorialGui gui; + + private static Dictionary directions = new Dictionary() { + { "Left", new Vector2(-1, 0) }, + { "Right", new Vector2(1, 0) }, + { "Up", new Vector2(0, -1) }, + { "Down", new Vector2(0, 1) }, + { "UpLeft", new Vector2(-1, -1) }, + { "UpRight", new Vector2(1, -1) }, + { "DownLeft", new Vector2(-1, 1) }, + { "DownRight", new Vector2(1, 1) } + }; + + public CustomBirdTutorial(EntityData data, Vector2 offset) : base(data, offset) { + BirdId = data.Attr("birdId"); + onlyOnce = data.Bool("onlyOnce"); + caw = data.Bool("caw"); + Facing = data.Bool("faceLeft") ? Facings.Left : Facings.Right; + + object info; + object[] controls; + + // parse the info ("title") + string infoString = data.Attr("info"); + if (GFX.Gui.Has(infoString)) { + info = GFX.Gui[infoString]; + } else { + info = Dialog.Clean(infoString); + } + + int extraAdvance = 0; + + // go ahead and parse the controls. Controls can be textures, VirtualButtons, directions or strings. + string[] controlsStrings = data.Attr("controls").Split(','); + controls = new object[controlsStrings.Length]; + for (int i = 0; i < controls.Length; i++) { + string controlString = controlsStrings[i]; + + if (GFX.Gui.Has(controlString)) { + // this is a texture. + controls[i] = GFX.Gui[controlString]; + } else if (directions.ContainsKey(controlString)) { + // this is a direction. + controls[i] = directions[controlString]; + } else { + FieldInfo matchingInput = typeof(Input).GetField(controlString, BindingFlags.Static | BindingFlags.Public); + if (matchingInput?.GetValue(null)?.GetType() == typeof(VirtualButton)) { + // this is a button. + controls[i] = matchingInput.GetValue(null); + } else { + // when BirdTutorialGui renders text, it is offset by 1px on the right. + // width computation doesn't take this 1px into account, so we should add it back in. + extraAdvance++; + if (i == 0) { + // as the text is rendered 1px to the right, if the first thing is a string, there will be 1px more padding on the left. + // we should add that extra px on the right as well. + extraAdvance++; + } + + if (controlString.StartsWith("dialog:")) { + // treat that as a dialog key. + controls[i] = Dialog.Clean(controlString.Substring("dialog:".Length)); + } else { + // treat that as a plain string. + controls[i] = controlString; + } + } + } + } + + gui = new BirdTutorialGui(this, new Vector2(0f, -16f), info, controls); + + DynData guiData = new DynData(gui); + // if there is no first line, resize the bubble accordingly. + if (string.IsNullOrEmpty(infoString)) { + guiData["infoHeight"] = 0f; + } + // apply the extra width. + guiData["controlsWidth"] = guiData.Get("controlsWidth") + extraAdvance; + } + + public void TriggerShowTutorial() { + if (!triggered) { + triggered = true; + Add(new Coroutine(ShowTutorial(gui, caw))); + } + } + + public void TriggerHideTutorial() { + if (triggered && !flewAway) { + flewAway = true; + + Add(new Coroutine(HideTutorial())); + Add(new Coroutine(StartleAndFlyAway())); + + if (onlyOnce) { + SceneAs().Session.DoNotLoad.Add(EntityID); + } + } + } + } +} diff --git a/Entities/CustomRespawnTimeRefill.cs b/Entities/CustomRespawnTimeRefill.cs index 5e651c5..9720ae1 100644 --- a/Entities/CustomRespawnTimeRefill.cs +++ b/Entities/CustomRespawnTimeRefill.cs @@ -1,25 +1,25 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; -using System; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/CustomRespawnTimeRefill")] - class CustomRespawnTimeRefill : Refill { - public CustomRespawnTimeRefill(EntityData data, Vector2 offset) : base(data, offset) { - DynData self = new DynData(this); - float respawnTime = data.Float("respawnTime", 2.5f); - - // wrap the original OnPlayer method to modify the respawnTimer if it gets reset to 2.5f. - PlayerCollider collider = Get(); - Action orig = collider.OnCollide; - collider.OnCollide = player => { - orig(player); - if (self.Get("respawnTimer") == 2.5f) { - self["respawnTimer"] = respawnTime; - } - }; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; +using System; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/CustomRespawnTimeRefill")] + class CustomRespawnTimeRefill : Refill { + public CustomRespawnTimeRefill(EntityData data, Vector2 offset) : base(data, offset) { + DynData self = new DynData(this); + float respawnTime = data.Float("respawnTime", 2.5f); + + // wrap the original OnPlayer method to modify the respawnTimer if it gets reset to 2.5f. + PlayerCollider collider = Get(); + Action orig = collider.OnCollide; + collider.OnCollide = player => { + orig(player); + if (self.Get("respawnTimer") == 2.5f) { + self["respawnTimer"] = respawnTime; + } + }; + } + } +} diff --git a/Entities/CustomSandwichLava.cs b/Entities/CustomSandwichLava.cs index 961b48b..22aa1fb 100644 --- a/Entities/CustomSandwichLava.cs +++ b/Entities/CustomSandwichLava.cs @@ -1,291 +1,291 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// Almost a copypaste of the vanilla SandwichLava class, but: - /// - plays nicely with vertical rooms - /// - can be made one-way - /// - has customizable speed and gap. - /// - [CustomEntity("SpringCollab2020/CustomSandwichLava")] - [Tracked] - class CustomSandwichLava : Entity { - private static FieldInfo lavaBlockerTriggerEnabled = typeof(LavaBlockerTrigger).GetField("enabled", BindingFlags.NonPublic | BindingFlags.Instance); - - // if the player collides with one of those, lava should be forced into waiting. - private List lavaBlockerTriggers; - - public enum DirectionMode { - AlwaysUp, AlwaysDown, CoreModeBased - }; - - private const float TopOffset = -160f; - - // parameters - private float startX; - public DirectionMode Direction; - public float Speed; - // the extra pixels each side has to be shifted towards each other compared to vanilla - // to comply with the "sandwichGap" setting. - private float sandwichDisplacement; - - // state - private bool iceMode; - private float lerp; - public bool Waiting; - private bool leaving = false; - private float delay = 0f; - private bool persistent; - private bool entering = true; - - // during a transition, those hold the Y positions of lava parts from the beginning of the transition. - private float transitionStartY; - private float transitionStartTopRectY; - private float transitionStartBottomRectY; - - private LavaRect bottomRect; - private LavaRect topRect; - - private SoundSource loopSfx; - - // in vanilla, this is SceneAs().Bounds.Bottom. This is bad with vertical rooms. - private float centerY => SceneAs().Camera.Bottom - 10f; - - public CustomSandwichLava(EntityData data, Vector2 offset) { - startX = data.Position.X + offset.X; - Direction = data.Enum("direction", DirectionMode.CoreModeBased); - Speed = data.Float("speed", 20f); - - // vanilla is 160. so, setting sandwichGap to 120 requires each side to be shifted by 20 pixels towards the other ((160 - 120) / 2). - sandwichDisplacement = (160f - data.Float("sandwichGap", 160f)) / 2; - - Depth = -1000000; - Collider = new ColliderList(new Hitbox(340f, 120f, 0f, -sandwichDisplacement), new Hitbox(340f, 120f, 0f, -280f + sandwichDisplacement)); - Visible = false; - - Add(loopSfx = new SoundSource()); - Add(new PlayerCollider(OnPlayer)); - Add(new CoreModeListener(OnChangeMode)); - - Add(bottomRect = new LavaRect(400f, 200f, 4)); - bottomRect.Position = new Vector2(-40f, 0f); - bottomRect.OnlyMode = LavaRect.OnlyModes.OnlyTop; - bottomRect.SmallWaveAmplitude = 2f; - - Add(topRect = new LavaRect(400f, 200f, 4)); - topRect.Position = new Vector2(-40f, -360f); - topRect.OnlyMode = LavaRect.OnlyModes.OnlyBottom; - topRect.SmallWaveAmplitude = 2f; - topRect.BigWaveAmplitude = (bottomRect.BigWaveAmplitude = 2f); - topRect.CurveAmplitude = (bottomRect.CurveAmplitude = 4f); - - Add(new TransitionListener { - OnOutBegin = () => { - // save the Y positions - transitionStartY = Y; - transitionStartTopRectY = topRect.Position.Y; - transitionStartBottomRectY = bottomRect.Position.Y; - - if (persistent && Scene != null && Scene.Entities.FindAll().Count <= 1) { - // no lava in the next room: leave - Leave(); - } else { - // look up for all lava blocker triggers in the next room. - lavaBlockerTriggers = Scene.Entities.OfType().ToList(); - } - }, - OnOut = progress => { - if (Scene != null) { - X = (Scene as Level).Camera.X; - if (!leaving) { - // make the lava elements transition smoothly to their expected positions. - Y = MathHelper.Lerp(transitionStartY, centerY, progress); - topRect.Position.Y = MathHelper.Lerp(transitionStartTopRectY, TopOffset - topRect.Height + sandwichDisplacement, progress); - bottomRect.Position.Y = MathHelper.Lerp(transitionStartBottomRectY, -sandwichDisplacement, progress); - } - } - - if ((progress > 0.95f) && leaving) { - // lava is leaving, transition is over soon => remove it - RemoveSelf(); - } - }, - OnInEnd = () => { - if (entering) { - // transition is over. grab the camera position now since it's done moving. - Y = centerY; - entering = false; - } - } - }); - } - - public override void Added(Scene scene) { - base.Added(scene); - X = SceneAs().Bounds.Left - 10; - Y = centerY; - iceMode = (SceneAs().Session.CoreMode == Session.CoreModes.Cold); - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - Player entity = Scene.Tracker.GetEntity(); - if (entity != null && (entity.JustRespawned || entity.X < startX)) { - Waiting = true; - } - - List existingLavas = Scene.Entities.FindAll(); - - bool removedSelf = false; - if (!persistent && existingLavas.Count >= 2) { - // another lava entity already exists. - CustomSandwichLava sandwichLava = (existingLavas[0] == this) ? existingLavas[1] : existingLavas[0]; - if (!sandwichLava.leaving) { - // transfer new settings to the existing lava. - sandwichLava.startX = startX; - sandwichLava.Waiting = true; - sandwichLava.Direction = Direction; - sandwichLava.Speed = Speed; - sandwichLava.sandwichDisplacement = sandwichDisplacement; - sandwichLava.Collider = Collider; - entering = false; - RemoveSelf(); - removedSelf = true; - } - } - - if (!removedSelf) { - // turn the lava persistent, so that it animates during room transitions - // and deals smoothly with successive rooms with sandwich lava. - persistent = true; - Tag = Tags.Persistent; - - if ((scene as Level).LastIntroType != Player.IntroTypes.Respawn) { - // throw the two lava parts off-screen. - topRect.Position.Y -= 60f + sandwichDisplacement; - bottomRect.Position.Y += 60f + sandwichDisplacement; - } else { - Visible = true; - } - - loopSfx.Play("event:/game/09_core/rising_threat", "room_state", iceMode ? 1 : 0); - loopSfx.Position = new Vector2(Width / 2f, 0f); - - // look up for all lava blocker triggers in the room. - lavaBlockerTriggers = scene.Entities.OfType().ToList(); - } - } - - private void OnChangeMode(Session.CoreModes mode) { - iceMode = (mode == Session.CoreModes.Cold); - loopSfx.Param("room_state", iceMode ? 1 : 0); - } - - private void OnPlayer(Player player) { - if (Waiting) { - return; - } - if (SaveData.Instance.Assists.Invincible) { - if (delay <= 0f) { - int direction = (player.Y > Y + bottomRect.Position.Y - 32f) ? 1 : -1; - - float from = Y; - float to = Y + (direction * 48); - player.Speed.Y = -direction * 200; - - if (direction > 0) { - player.RefillDash(); - } - - Tween.Set(this, Tween.TweenMode.Oneshot, 0.4f, Ease.CubeOut, tween => { - Y = MathHelper.Lerp(from, to, tween.Eased); - }); - - delay = 0.5f; - loopSfx.Param("rising", 0f); - Audio.Play("event:/game/general/assist_screenbottom", player.Position); - } - } else { - player.Die(-Vector2.UnitY); - } - } - - public bool IsLeaving() { - return leaving; - } - - public void Leave() { - AddTag(Tags.TransitionUpdate); - leaving = true; - Collidable = false; - Alarm.Set(this, 2f, delegate { - RemoveSelf(); - }); - } - - public override void Update() { - if (entering) { - // set the Y position again on the first Update call, since the camera is finished being set up. - Y = centerY; - entering = false; - } - - Level level = Scene as Level; - X = level.Camera.X; - delay -= Engine.DeltaTime; - base.Update(); - Visible = true; - - Player player = Scene.Tracker.GetEntity(); - if (player != null) { - LavaBlockerTrigger collidedTrigger = lavaBlockerTriggers.Find(trigger => player.CollideCheck(trigger)); - - if (collidedTrigger != null && (bool) lavaBlockerTriggerEnabled.GetValue(collidedTrigger)) { - // player is in a lava blocker trigger and it is enabled; block the lava. - Waiting = true; - } - } - - if (Waiting) { - Y = Calc.Approach(Y, centerY, 128f * Engine.DeltaTime); - - loopSfx.Param("rising", 0f); - if (player != null && player.X >= startX && !player.JustRespawned && player.StateMachine.State != 11) { - Waiting = false; - } - } else if (!leaving && delay <= 0f) { - loopSfx.Param("rising", 1f); - if (Direction == DirectionMode.AlwaysDown || (Direction == DirectionMode.CoreModeBased && iceMode)) { - Y += Speed * Engine.DeltaTime; - } else { - Y -= Speed * Engine.DeltaTime; - } - } - - topRect.Position.Y = Calc.Approach(topRect.Position.Y, TopOffset - topRect.Height + (leaving ? (-512) : sandwichDisplacement), (leaving ? 256 : 64) * Engine.DeltaTime); - bottomRect.Position.Y = Calc.Approach(bottomRect.Position.Y, leaving ? 512 : -sandwichDisplacement, (leaving ? 256 : 64) * Engine.DeltaTime); - - lerp = Calc.Approach(lerp, iceMode ? 1 : 0, Engine.DeltaTime * 4f); - - bottomRect.SurfaceColor = Color.Lerp(RisingLava.Hot[0], RisingLava.Cold[0], lerp); - bottomRect.EdgeColor = Color.Lerp(RisingLava.Hot[1], RisingLava.Cold[1], lerp); - bottomRect.CenterColor = Color.Lerp(RisingLava.Hot[2], RisingLava.Cold[2], lerp); - bottomRect.Spikey = lerp * 5f; - bottomRect.UpdateMultiplier = (1f - lerp) * 2f; - bottomRect.Fade = (iceMode ? 128 : 32); - - topRect.SurfaceColor = bottomRect.SurfaceColor; - topRect.EdgeColor = bottomRect.EdgeColor; - topRect.CenterColor = bottomRect.CenterColor; - topRect.Spikey = bottomRect.Spikey; - topRect.UpdateMultiplier = bottomRect.UpdateMultiplier; - topRect.Fade = bottomRect.Fade; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// Almost a copypaste of the vanilla SandwichLava class, but: + /// - plays nicely with vertical rooms + /// - can be made one-way + /// - has customizable speed and gap. + /// + [CustomEntity("SpringCollab2020/CustomSandwichLava")] + [Tracked] + class CustomSandwichLava : Entity { + private static FieldInfo lavaBlockerTriggerEnabled = typeof(LavaBlockerTrigger).GetField("enabled", BindingFlags.NonPublic | BindingFlags.Instance); + + // if the player collides with one of those, lava should be forced into waiting. + private List lavaBlockerTriggers; + + public enum DirectionMode { + AlwaysUp, AlwaysDown, CoreModeBased + }; + + private const float TopOffset = -160f; + + // parameters + private float startX; + public DirectionMode Direction; + public float Speed; + // the extra pixels each side has to be shifted towards each other compared to vanilla + // to comply with the "sandwichGap" setting. + private float sandwichDisplacement; + + // state + private bool iceMode; + private float lerp; + public bool Waiting; + private bool leaving = false; + private float delay = 0f; + private bool persistent; + private bool entering = true; + + // during a transition, those hold the Y positions of lava parts from the beginning of the transition. + private float transitionStartY; + private float transitionStartTopRectY; + private float transitionStartBottomRectY; + + private LavaRect bottomRect; + private LavaRect topRect; + + private SoundSource loopSfx; + + // in vanilla, this is SceneAs().Bounds.Bottom. This is bad with vertical rooms. + private float centerY => SceneAs().Camera.Bottom - 10f; + + public CustomSandwichLava(EntityData data, Vector2 offset) { + startX = data.Position.X + offset.X; + Direction = data.Enum("direction", DirectionMode.CoreModeBased); + Speed = data.Float("speed", 20f); + + // vanilla is 160. so, setting sandwichGap to 120 requires each side to be shifted by 20 pixels towards the other ((160 - 120) / 2). + sandwichDisplacement = (160f - data.Float("sandwichGap", 160f)) / 2; + + Depth = -1000000; + Collider = new ColliderList(new Hitbox(340f, 120f, 0f, -sandwichDisplacement), new Hitbox(340f, 120f, 0f, -280f + sandwichDisplacement)); + Visible = false; + + Add(loopSfx = new SoundSource()); + Add(new PlayerCollider(OnPlayer)); + Add(new CoreModeListener(OnChangeMode)); + + Add(bottomRect = new LavaRect(400f, 200f, 4)); + bottomRect.Position = new Vector2(-40f, 0f); + bottomRect.OnlyMode = LavaRect.OnlyModes.OnlyTop; + bottomRect.SmallWaveAmplitude = 2f; + + Add(topRect = new LavaRect(400f, 200f, 4)); + topRect.Position = new Vector2(-40f, -360f); + topRect.OnlyMode = LavaRect.OnlyModes.OnlyBottom; + topRect.SmallWaveAmplitude = 2f; + topRect.BigWaveAmplitude = (bottomRect.BigWaveAmplitude = 2f); + topRect.CurveAmplitude = (bottomRect.CurveAmplitude = 4f); + + Add(new TransitionListener { + OnOutBegin = () => { + // save the Y positions + transitionStartY = Y; + transitionStartTopRectY = topRect.Position.Y; + transitionStartBottomRectY = bottomRect.Position.Y; + + if (persistent && Scene != null && Scene.Entities.FindAll().Count <= 1) { + // no lava in the next room: leave + Leave(); + } else { + // look up for all lava blocker triggers in the next room. + lavaBlockerTriggers = Scene.Entities.OfType().ToList(); + } + }, + OnOut = progress => { + if (Scene != null) { + X = (Scene as Level).Camera.X; + if (!leaving) { + // make the lava elements transition smoothly to their expected positions. + Y = MathHelper.Lerp(transitionStartY, centerY, progress); + topRect.Position.Y = MathHelper.Lerp(transitionStartTopRectY, TopOffset - topRect.Height + sandwichDisplacement, progress); + bottomRect.Position.Y = MathHelper.Lerp(transitionStartBottomRectY, -sandwichDisplacement, progress); + } + } + + if ((progress > 0.95f) && leaving) { + // lava is leaving, transition is over soon => remove it + RemoveSelf(); + } + }, + OnInEnd = () => { + if (entering) { + // transition is over. grab the camera position now since it's done moving. + Y = centerY; + entering = false; + } + } + }); + } + + public override void Added(Scene scene) { + base.Added(scene); + X = SceneAs().Bounds.Left - 10; + Y = centerY; + iceMode = (SceneAs().Session.CoreMode == Session.CoreModes.Cold); + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + Player entity = Scene.Tracker.GetEntity(); + if (entity != null && (entity.JustRespawned || entity.X < startX)) { + Waiting = true; + } + + List existingLavas = Scene.Entities.FindAll(); + + bool removedSelf = false; + if (!persistent && existingLavas.Count >= 2) { + // another lava entity already exists. + CustomSandwichLava sandwichLava = (existingLavas[0] == this) ? existingLavas[1] : existingLavas[0]; + if (!sandwichLava.leaving) { + // transfer new settings to the existing lava. + sandwichLava.startX = startX; + sandwichLava.Waiting = true; + sandwichLava.Direction = Direction; + sandwichLava.Speed = Speed; + sandwichLava.sandwichDisplacement = sandwichDisplacement; + sandwichLava.Collider = Collider; + entering = false; + RemoveSelf(); + removedSelf = true; + } + } + + if (!removedSelf) { + // turn the lava persistent, so that it animates during room transitions + // and deals smoothly with successive rooms with sandwich lava. + persistent = true; + Tag = Tags.Persistent; + + if ((scene as Level).LastIntroType != Player.IntroTypes.Respawn) { + // throw the two lava parts off-screen. + topRect.Position.Y -= 60f + sandwichDisplacement; + bottomRect.Position.Y += 60f + sandwichDisplacement; + } else { + Visible = true; + } + + loopSfx.Play("event:/game/09_core/rising_threat", "room_state", iceMode ? 1 : 0); + loopSfx.Position = new Vector2(Width / 2f, 0f); + + // look up for all lava blocker triggers in the room. + lavaBlockerTriggers = scene.Entities.OfType().ToList(); + } + } + + private void OnChangeMode(Session.CoreModes mode) { + iceMode = (mode == Session.CoreModes.Cold); + loopSfx.Param("room_state", iceMode ? 1 : 0); + } + + private void OnPlayer(Player player) { + if (Waiting) { + return; + } + if (SaveData.Instance.Assists.Invincible) { + if (delay <= 0f) { + int direction = (player.Y > Y + bottomRect.Position.Y - 32f) ? 1 : -1; + + float from = Y; + float to = Y + (direction * 48); + player.Speed.Y = -direction * 200; + + if (direction > 0) { + player.RefillDash(); + } + + Tween.Set(this, Tween.TweenMode.Oneshot, 0.4f, Ease.CubeOut, tween => { + Y = MathHelper.Lerp(from, to, tween.Eased); + }); + + delay = 0.5f; + loopSfx.Param("rising", 0f); + Audio.Play("event:/game/general/assist_screenbottom", player.Position); + } + } else { + player.Die(-Vector2.UnitY); + } + } + + public bool IsLeaving() { + return leaving; + } + + public void Leave() { + AddTag(Tags.TransitionUpdate); + leaving = true; + Collidable = false; + Alarm.Set(this, 2f, delegate { + RemoveSelf(); + }); + } + + public override void Update() { + if (entering) { + // set the Y position again on the first Update call, since the camera is finished being set up. + Y = centerY; + entering = false; + } + + Level level = Scene as Level; + X = level.Camera.X; + delay -= Engine.DeltaTime; + base.Update(); + Visible = true; + + Player player = Scene.Tracker.GetEntity(); + if (player != null) { + LavaBlockerTrigger collidedTrigger = lavaBlockerTriggers.Find(trigger => player.CollideCheck(trigger)); + + if (collidedTrigger != null && (bool) lavaBlockerTriggerEnabled.GetValue(collidedTrigger)) { + // player is in a lava blocker trigger and it is enabled; block the lava. + Waiting = true; + } + } + + if (Waiting) { + Y = Calc.Approach(Y, centerY, 128f * Engine.DeltaTime); + + loopSfx.Param("rising", 0f); + if (player != null && player.X >= startX && !player.JustRespawned && player.StateMachine.State != 11) { + Waiting = false; + } + } else if (!leaving && delay <= 0f) { + loopSfx.Param("rising", 1f); + if (Direction == DirectionMode.AlwaysDown || (Direction == DirectionMode.CoreModeBased && iceMode)) { + Y += Speed * Engine.DeltaTime; + } else { + Y -= Speed * Engine.DeltaTime; + } + } + + topRect.Position.Y = Calc.Approach(topRect.Position.Y, TopOffset - topRect.Height + (leaving ? (-512) : sandwichDisplacement), (leaving ? 256 : 64) * Engine.DeltaTime); + bottomRect.Position.Y = Calc.Approach(bottomRect.Position.Y, leaving ? 512 : -sandwichDisplacement, (leaving ? 256 : 64) * Engine.DeltaTime); + + lerp = Calc.Approach(lerp, iceMode ? 1 : 0, Engine.DeltaTime * 4f); + + bottomRect.SurfaceColor = Color.Lerp(RisingLava.Hot[0], RisingLava.Cold[0], lerp); + bottomRect.EdgeColor = Color.Lerp(RisingLava.Hot[1], RisingLava.Cold[1], lerp); + bottomRect.CenterColor = Color.Lerp(RisingLava.Hot[2], RisingLava.Cold[2], lerp); + bottomRect.Spikey = lerp * 5f; + bottomRect.UpdateMultiplier = (1f - lerp) * 2f; + bottomRect.Fade = (iceMode ? 128 : 32); + + topRect.SurfaceColor = bottomRect.SurfaceColor; + topRect.EdgeColor = bottomRect.EdgeColor; + topRect.CenterColor = bottomRect.CenterColor; + topRect.Spikey = bottomRect.Spikey; + topRect.UpdateMultiplier = bottomRect.UpdateMultiplier; + topRect.Fade = bottomRect.Fade; + } + } +} diff --git a/Entities/CustomizableGlassBlock.cs b/Entities/CustomizableGlassBlock.cs index 2340f64..9b3ada8 100644 --- a/Entities/CustomizableGlassBlock.cs +++ b/Entities/CustomizableGlassBlock.cs @@ -1,84 +1,84 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// Glass Block pulled from Celeste 1.2.6.1. - /// Usable with CustomizableGlassBlockController to get custom star/bg colors. - /// - [Tracked] - [CustomEntity("SpringCollab2020/CustomizableGlassBlock")] - public class CustomizableGlassBlock : Solid { - private struct Line { - public Vector2 A; - public Vector2 B; - - public Line(Vector2 a, Vector2 b) { - A = a; - B = b; - } - } - - private List lines = new List(); - private Color lineColor = Color.White; - - public CustomizableGlassBlock(Vector2 position, float width, float height, bool behindFgTiles) - : base(position, width, height, safe: false) { - - Depth = behindFgTiles ? -9995 : -10000; - Add(new LightOcclude()); - Add(new MirrorSurface()); - SurfaceSoundIndex = 32; - } - - public CustomizableGlassBlock(EntityData data, Vector2 offset) - : this(data.Position + offset, data.Width, data.Height, data.Bool("behindFgTiles")) { } - - public override void Awake(Scene scene) { - base.Awake(scene); - - int widthInTiles = (int) Width / 8; - int heightInTiles = (int) Height / 8; - AddSide(new Vector2(0f, 0f), new Vector2(0f, -1f), widthInTiles); - AddSide(new Vector2(widthInTiles - 1, 0f), new Vector2(1f, 0f), heightInTiles); - AddSide(new Vector2(widthInTiles - 1, heightInTiles - 1), new Vector2(0f, 1f), widthInTiles); - AddSide(new Vector2(0f, heightInTiles - 1), new Vector2(-1f, 0f), heightInTiles); - } - - private void AddSide(Vector2 start, Vector2 normal, int tiles) { - Vector2 vector = new Vector2(0f - normal.Y, normal.X); - for (int i = 0; i < tiles; i++) { - if (Open(start + vector * i + normal)) { - Vector2 a = (start + vector * i) * 8f + new Vector2(4f) - vector * 4f + normal * 4f; - if (!Open(start + vector * (i - 1))) { - a -= vector; - } - for (; i < tiles && Open(start + vector * i + normal); i++) { } - - Vector2 b = (start + vector * i) * 8f + new Vector2(4f) - vector * 4f + normal * 4f; - if (!Open(start + vector * i)) { - b += vector; - } - - lines.Add(new Line(a, b)); - } - } - } - - private bool Open(Vector2 tile) { - Vector2 point = new Vector2(X + tile.X * 8f + 4f, Y + tile.Y * 8f + 4f); - if (!Scene.CollideCheck(point)) { - return !Scene.CollideCheck(point); - } - return false; - } - - public override void Render() { - foreach (Line line in lines) { - Draw.Line(Position + line.A, Position + line.B, lineColor); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// Glass Block pulled from Celeste 1.2.6.1. + /// Usable with CustomizableGlassBlockController to get custom star/bg colors. + /// + [Tracked] + [CustomEntity("SpringCollab2020/CustomizableGlassBlock")] + public class CustomizableGlassBlock : Solid { + private struct Line { + public Vector2 A; + public Vector2 B; + + public Line(Vector2 a, Vector2 b) { + A = a; + B = b; + } + } + + private List lines = new List(); + private Color lineColor = Color.White; + + public CustomizableGlassBlock(Vector2 position, float width, float height, bool behindFgTiles) + : base(position, width, height, safe: false) { + + Depth = behindFgTiles ? -9995 : -10000; + Add(new LightOcclude()); + Add(new MirrorSurface()); + SurfaceSoundIndex = 32; + } + + public CustomizableGlassBlock(EntityData data, Vector2 offset) + : this(data.Position + offset, data.Width, data.Height, data.Bool("behindFgTiles")) { } + + public override void Awake(Scene scene) { + base.Awake(scene); + + int widthInTiles = (int) Width / 8; + int heightInTiles = (int) Height / 8; + AddSide(new Vector2(0f, 0f), new Vector2(0f, -1f), widthInTiles); + AddSide(new Vector2(widthInTiles - 1, 0f), new Vector2(1f, 0f), heightInTiles); + AddSide(new Vector2(widthInTiles - 1, heightInTiles - 1), new Vector2(0f, 1f), widthInTiles); + AddSide(new Vector2(0f, heightInTiles - 1), new Vector2(-1f, 0f), heightInTiles); + } + + private void AddSide(Vector2 start, Vector2 normal, int tiles) { + Vector2 vector = new Vector2(0f - normal.Y, normal.X); + for (int i = 0; i < tiles; i++) { + if (Open(start + vector * i + normal)) { + Vector2 a = (start + vector * i) * 8f + new Vector2(4f) - vector * 4f + normal * 4f; + if (!Open(start + vector * (i - 1))) { + a -= vector; + } + for (; i < tiles && Open(start + vector * i + normal); i++) { } + + Vector2 b = (start + vector * i) * 8f + new Vector2(4f) - vector * 4f + normal * 4f; + if (!Open(start + vector * i)) { + b += vector; + } + + lines.Add(new Line(a, b)); + } + } + } + + private bool Open(Vector2 tile) { + Vector2 point = new Vector2(X + tile.X * 8f + 4f, Y + tile.Y * 8f + 4f); + if (!Scene.CollideCheck(point)) { + return !Scene.CollideCheck(point); + } + return false; + } + + public override void Render() { + foreach (Line line in lines) { + Draw.Line(Position + line.A, Position + line.B, lineColor); + } + } + } +} diff --git a/Entities/CustomizableGlassBlockController.cs b/Entities/CustomizableGlassBlockController.cs index f5c1776..e5bb0a0 100644 --- a/Entities/CustomizableGlassBlockController.cs +++ b/Entities/CustomizableGlassBlockController.cs @@ -1,282 +1,282 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using Monocle; -using System.Collections.Generic; -using System.Linq; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// A non-global and customizable version of the vanilla GlassBlockBg class. - /// This draws a background for all CustomizableGlassBlocks in the room. - /// - [CustomEntity("SpringCollab2020/CustomizableGlassBlockController")] - [Tracked] - class CustomizableGlassBlockController : Entity { - private struct Star { - public Vector2 Position; - public MTexture Texture; - public Color Color; - public Vector2 Scroll; - } - - private struct Ray { - public Vector2 Position; - public float Width; - public float Length; - public Color Color; - } - - private const int StarCount = 100; - private const int RayCount = 50; - - private Star[] stars = new Star[StarCount]; - private Ray[] rays = new Ray[RayCount]; - - private VertexPositionColor[] verts = new VertexPositionColor[2700]; - - private Vector2 rayNormal = new Vector2(-5f, -8f).SafeNormalize(); - - private VirtualRenderTarget beamsTarget; - private VirtualRenderTarget starsTarget; - - private bool hasBlocks; - - private Color[] starColors; - private Color bgColor; - - private float alpha = 1f; - - public CustomizableGlassBlockController(EntityData data, Vector2 offset) { - // parse the "starColors" and "bgColor" parameters - string[] starColorsAsStrings = data.Attr("starColors").Split(','); - starColors = new Color[starColorsAsStrings.Length]; - for (int i = 0; i < starColors.Length; i++) { - starColors[i] = Calc.HexToColor(starColorsAsStrings[i]); - } - bgColor = Calc.HexToColor(data.Attr("bgColor")); - - // do some more initialization. - Add(new BeforeRenderHook(BeforeRender)); - Depth = -9990; - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - // check if there is already a CustomizableGlassBlockController in the room (supposedly from the previous room) - // so that we can carry values over. - if (scene.Tracker.GetEntities() - .Find(controller => controller != this) is CustomizableGlassBlockController controllerFromPreviousRoom) { - - // carry over the state so that stars/rays don't change places. - stars = (Star[]) controllerFromPreviousRoom.stars.Clone(); - rays = controllerFromPreviousRoom.rays; - - if (!controllerFromPreviousRoom.starColors.SequenceEqual(starColors)) { - // start out transparent and fade in since starColors changed. - alpha = 0f; - - // reroll colors since they changed. we copy the Stars so that the original ones are unchanged. - for (int i = 0; i < stars.Length; i++) { - stars[i] = new Star { - Position = stars[i].Position, - Texture = stars[i].Texture, - Color = Calc.Random.Choose(starColors), - Scroll = stars[i].Scroll - }; - } - } else if (controllerFromPreviousRoom.bgColor != bgColor) { - // start out transparent and fade in since bgColor changed. - alpha = 0f; - } - } else { - // initialize stars and rays from scratch like vanilla does. - List starTextures = GFX.Game.GetAtlasSubtextures("particles/stars/"); - for (int i = 0; i < stars.Length; i++) { - stars[i].Position.X = Calc.Random.Next(320); - stars[i].Position.Y = Calc.Random.Next(180); - stars[i].Texture = Calc.Random.Choose(starTextures); - stars[i].Color = Calc.Random.Choose(starColors); - stars[i].Scroll = Vector2.One * Calc.Random.NextFloat(0.05f); - } - - for (int j = 0; j < rays.Length; j++) { - rays[j].Position.X = Calc.Random.Next(320); - rays[j].Position.Y = Calc.Random.Next(180); - rays[j].Width = Calc.Random.Range(4f, 16f); - rays[j].Length = Calc.Random.Choose(48, 96, 128); - rays[j].Color = Color.White * Calc.Random.Range(0.2f, 0.4f); - } - } - } - - private void BeforeRender() { - List glassBlocks = Scene.Tracker.GetEntities(); - hasBlocks = (glassBlocks.Count > 0); - if (!hasBlocks) { - return; - } - - Camera camera = (Scene as Level).Camera; - int screenWidth = 320; - int screenHeight = 180; - - // draw stars - if (starsTarget == null) { - starsTarget = VirtualContent.CreateRenderTarget("customizable-glass-block-surfaces", screenWidth, screenHeight); - } - - Engine.Graphics.GraphicsDevice.SetRenderTarget(starsTarget); - Engine.Graphics.GraphicsDevice.Clear(Color.Transparent); - Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, null, Matrix.Identity); - Vector2 origin = new Vector2(8f, 8f); - for (int i = 0; i < stars.Length; i++) { - MTexture starTexture = stars[i].Texture; - Color starColor = stars[i].Color * alpha; - Vector2 starScroll = stars[i].Scroll; - Vector2 starActualPosition = default; - starActualPosition.X = Mod(stars[i].Position.X - camera.X * (1f - starScroll.X), screenWidth); - starActualPosition.Y = Mod(stars[i].Position.Y - camera.Y * (1f - starScroll.Y), screenHeight); - starTexture.Draw(starActualPosition, origin, starColor); - - if (starActualPosition.X < origin.X) { - starTexture.Draw(starActualPosition + new Vector2(screenWidth, 0f), origin, starColor); - } else if (starActualPosition.X > screenWidth - origin.X) { - starTexture.Draw(starActualPosition - new Vector2(screenWidth, 0f), origin, starColor); - } - - if (starActualPosition.Y < origin.Y) { - starTexture.Draw(starActualPosition + new Vector2(0f, screenHeight), origin, starColor); - } else if (starActualPosition.Y > screenHeight - origin.Y) { - starTexture.Draw(starActualPosition - new Vector2(0f, screenHeight), origin, starColor); - } - } - Draw.SpriteBatch.End(); - - // draw rays/beams - int vertex = 0; - for (int j = 0; j < rays.Length; j++) { - Vector2 rayPosition = default; - rayPosition.X = Mod(rays[j].Position.X - camera.X * 0.9f, screenWidth); - rayPosition.Y = Mod(rays[j].Position.Y - camera.Y * 0.9f, screenHeight); - DrawRay(rayPosition, ref vertex, ref rays[j]); - if (rayPosition.X < 64f) { - DrawRay(rayPosition + new Vector2(screenWidth, 0f), ref vertex, ref rays[j]); - } else if (rayPosition.X > (screenWidth - 64)) { - DrawRay(rayPosition - new Vector2(screenWidth, 0f), ref vertex, ref rays[j]); - } - if (rayPosition.Y < 64f) { - DrawRay(rayPosition + new Vector2(0f, screenHeight), ref vertex, ref rays[j]); - } else if (rayPosition.Y > (screenHeight - 64)) { - DrawRay(rayPosition - new Vector2(0f, screenHeight), ref vertex, ref rays[j]); - } - } - - if (beamsTarget == null) { - beamsTarget = VirtualContent.CreateRenderTarget("customizable-glass-block-beams", screenWidth, screenHeight); - } - - Engine.Graphics.GraphicsDevice.SetRenderTarget(beamsTarget); - Engine.Graphics.GraphicsDevice.Clear(Color.Transparent); - GFX.DrawVertices(Matrix.Identity, verts, vertex); - - // if fading in, update the alpha value to fade in in ~0.25 seconds. - if (alpha != 1f) { - alpha = Calc.Approach(alpha, 1f, Engine.DeltaTime * 4f); - } - } - - private void DrawRay(Vector2 position, ref int vertex, ref Ray ray) { - Vector2 value = new Vector2(0f - rayNormal.Y, rayNormal.X); - Vector2 value2 = rayNormal * ray.Width * 0.5f; - Vector2 value3 = value * ray.Length * 0.25f * 0.5f; - Vector2 value4 = value * ray.Length * 0.5f * 0.5f; - Vector2 v = position + value2 - value3 - value4; - Vector2 v2 = position - value2 - value3 - value4; - Vector2 vector = position + value2 - value3; - Vector2 vector2 = position - value2 - value3; - Vector2 vector3 = position + value2 + value3; - Vector2 vector4 = position - value2 + value3; - Vector2 v3 = position + value2 + value3 + value4; - Vector2 v4 = position - value2 + value3 + value4; - Color transparent = Color.Transparent; - Color color = ray.Color * alpha; - Quad(ref vertex, v, vector, vector2, v2, transparent, color, color, transparent); - Quad(ref vertex, vector, vector3, vector4, vector2, color, color, color, color); - Quad(ref vertex, vector3, v3, v4, vector4, color, transparent, transparent, color); - } - - private void Quad(ref int vertex, Vector2 v0, Vector2 v1, Vector2 v2, Vector2 v3, Color c0, Color c1, Color c2, Color c3) { - verts[vertex].Position.X = v0.X; - verts[vertex].Position.Y = v0.Y; - verts[vertex++].Color = c0; - verts[vertex].Position.X = v1.X; - verts[vertex].Position.Y = v1.Y; - verts[vertex++].Color = c1; - verts[vertex].Position.X = v2.X; - verts[vertex].Position.Y = v2.Y; - verts[vertex++].Color = c2; - verts[vertex].Position.X = v0.X; - verts[vertex].Position.Y = v0.Y; - verts[vertex++].Color = c0; - verts[vertex].Position.X = v2.X; - verts[vertex].Position.Y = v2.Y; - verts[vertex++].Color = c2; - verts[vertex].Position.X = v3.X; - verts[vertex].Position.Y = v3.Y; - verts[vertex++].Color = c3; - } - - public override void Render() { - if (hasBlocks) { - Vector2 position = (Scene as Level).Camera.Position; - List glassBlocks = Scene.Tracker.GetEntities(); - - foreach (Entity glassBlock in glassBlocks) { - Draw.Rect(glassBlock.X, glassBlock.Y, glassBlock.Width, glassBlock.Height, bgColor * alpha); - } - - if (starsTarget != null && !starsTarget.IsDisposed) { - foreach (Entity glassBlock in glassBlocks) { - Rectangle value = new Rectangle((int) (glassBlock.X - position.X), (int) (glassBlock.Y - position.Y), (int) glassBlock.Width, (int) glassBlock.Height); - Draw.SpriteBatch.Draw(starsTarget, glassBlock.Position, value, Color.White); - } - } - - if (beamsTarget != null && !beamsTarget.IsDisposed) { - foreach (Entity glassBlock in glassBlocks) { - Rectangle value2 = new Rectangle((int) (glassBlock.X - position.X), (int) (glassBlock.Y - position.Y), (int) glassBlock.Width, (int) glassBlock.Height); - Draw.SpriteBatch.Draw((RenderTarget2D) beamsTarget, glassBlock.Position, value2, Color.White); - } - } - } - } - - public override void Removed(Scene scene) { - base.Removed(scene); - Dispose(); - } - - public override void SceneEnd(Scene scene) { - base.SceneEnd(scene); - Dispose(); - } - - public void Dispose() { - if (starsTarget != null && !starsTarget.IsDisposed) { - starsTarget.Dispose(); - } - - if (beamsTarget != null && !beamsTarget.IsDisposed) { - beamsTarget.Dispose(); - } - starsTarget = null; - beamsTarget = null; - } - - private float Mod(float x, float m) { - return (x % m + m) % m; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Monocle; +using System.Collections.Generic; +using System.Linq; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// A non-global and customizable version of the vanilla GlassBlockBg class. + /// This draws a background for all CustomizableGlassBlocks in the room. + /// + [CustomEntity("SpringCollab2020/CustomizableGlassBlockController")] + [Tracked] + class CustomizableGlassBlockController : Entity { + private struct Star { + public Vector2 Position; + public MTexture Texture; + public Color Color; + public Vector2 Scroll; + } + + private struct Ray { + public Vector2 Position; + public float Width; + public float Length; + public Color Color; + } + + private const int StarCount = 100; + private const int RayCount = 50; + + private Star[] stars = new Star[StarCount]; + private Ray[] rays = new Ray[RayCount]; + + private VertexPositionColor[] verts = new VertexPositionColor[2700]; + + private Vector2 rayNormal = new Vector2(-5f, -8f).SafeNormalize(); + + private VirtualRenderTarget beamsTarget; + private VirtualRenderTarget starsTarget; + + private bool hasBlocks; + + private Color[] starColors; + private Color bgColor; + + private float alpha = 1f; + + public CustomizableGlassBlockController(EntityData data, Vector2 offset) { + // parse the "starColors" and "bgColor" parameters + string[] starColorsAsStrings = data.Attr("starColors").Split(','); + starColors = new Color[starColorsAsStrings.Length]; + for (int i = 0; i < starColors.Length; i++) { + starColors[i] = Calc.HexToColor(starColorsAsStrings[i]); + } + bgColor = Calc.HexToColor(data.Attr("bgColor")); + + // do some more initialization. + Add(new BeforeRenderHook(BeforeRender)); + Depth = -9990; + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + // check if there is already a CustomizableGlassBlockController in the room (supposedly from the previous room) + // so that we can carry values over. + if (scene.Tracker.GetEntities() + .Find(controller => controller != this) is CustomizableGlassBlockController controllerFromPreviousRoom) { + + // carry over the state so that stars/rays don't change places. + stars = (Star[]) controllerFromPreviousRoom.stars.Clone(); + rays = controllerFromPreviousRoom.rays; + + if (!controllerFromPreviousRoom.starColors.SequenceEqual(starColors)) { + // start out transparent and fade in since starColors changed. + alpha = 0f; + + // reroll colors since they changed. we copy the Stars so that the original ones are unchanged. + for (int i = 0; i < stars.Length; i++) { + stars[i] = new Star { + Position = stars[i].Position, + Texture = stars[i].Texture, + Color = Calc.Random.Choose(starColors), + Scroll = stars[i].Scroll + }; + } + } else if (controllerFromPreviousRoom.bgColor != bgColor) { + // start out transparent and fade in since bgColor changed. + alpha = 0f; + } + } else { + // initialize stars and rays from scratch like vanilla does. + List starTextures = GFX.Game.GetAtlasSubtextures("particles/stars/"); + for (int i = 0; i < stars.Length; i++) { + stars[i].Position.X = Calc.Random.Next(320); + stars[i].Position.Y = Calc.Random.Next(180); + stars[i].Texture = Calc.Random.Choose(starTextures); + stars[i].Color = Calc.Random.Choose(starColors); + stars[i].Scroll = Vector2.One * Calc.Random.NextFloat(0.05f); + } + + for (int j = 0; j < rays.Length; j++) { + rays[j].Position.X = Calc.Random.Next(320); + rays[j].Position.Y = Calc.Random.Next(180); + rays[j].Width = Calc.Random.Range(4f, 16f); + rays[j].Length = Calc.Random.Choose(48, 96, 128); + rays[j].Color = Color.White * Calc.Random.Range(0.2f, 0.4f); + } + } + } + + private void BeforeRender() { + List glassBlocks = Scene.Tracker.GetEntities(); + hasBlocks = (glassBlocks.Count > 0); + if (!hasBlocks) { + return; + } + + Camera camera = (Scene as Level).Camera; + int screenWidth = 320; + int screenHeight = 180; + + // draw stars + if (starsTarget == null) { + starsTarget = VirtualContent.CreateRenderTarget("customizable-glass-block-surfaces", screenWidth, screenHeight); + } + + Engine.Graphics.GraphicsDevice.SetRenderTarget(starsTarget); + Engine.Graphics.GraphicsDevice.Clear(Color.Transparent); + Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, null, Matrix.Identity); + Vector2 origin = new Vector2(8f, 8f); + for (int i = 0; i < stars.Length; i++) { + MTexture starTexture = stars[i].Texture; + Color starColor = stars[i].Color * alpha; + Vector2 starScroll = stars[i].Scroll; + Vector2 starActualPosition = default; + starActualPosition.X = Mod(stars[i].Position.X - camera.X * (1f - starScroll.X), screenWidth); + starActualPosition.Y = Mod(stars[i].Position.Y - camera.Y * (1f - starScroll.Y), screenHeight); + starTexture.Draw(starActualPosition, origin, starColor); + + if (starActualPosition.X < origin.X) { + starTexture.Draw(starActualPosition + new Vector2(screenWidth, 0f), origin, starColor); + } else if (starActualPosition.X > screenWidth - origin.X) { + starTexture.Draw(starActualPosition - new Vector2(screenWidth, 0f), origin, starColor); + } + + if (starActualPosition.Y < origin.Y) { + starTexture.Draw(starActualPosition + new Vector2(0f, screenHeight), origin, starColor); + } else if (starActualPosition.Y > screenHeight - origin.Y) { + starTexture.Draw(starActualPosition - new Vector2(0f, screenHeight), origin, starColor); + } + } + Draw.SpriteBatch.End(); + + // draw rays/beams + int vertex = 0; + for (int j = 0; j < rays.Length; j++) { + Vector2 rayPosition = default; + rayPosition.X = Mod(rays[j].Position.X - camera.X * 0.9f, screenWidth); + rayPosition.Y = Mod(rays[j].Position.Y - camera.Y * 0.9f, screenHeight); + DrawRay(rayPosition, ref vertex, ref rays[j]); + if (rayPosition.X < 64f) { + DrawRay(rayPosition + new Vector2(screenWidth, 0f), ref vertex, ref rays[j]); + } else if (rayPosition.X > (screenWidth - 64)) { + DrawRay(rayPosition - new Vector2(screenWidth, 0f), ref vertex, ref rays[j]); + } + if (rayPosition.Y < 64f) { + DrawRay(rayPosition + new Vector2(0f, screenHeight), ref vertex, ref rays[j]); + } else if (rayPosition.Y > (screenHeight - 64)) { + DrawRay(rayPosition - new Vector2(0f, screenHeight), ref vertex, ref rays[j]); + } + } + + if (beamsTarget == null) { + beamsTarget = VirtualContent.CreateRenderTarget("customizable-glass-block-beams", screenWidth, screenHeight); + } + + Engine.Graphics.GraphicsDevice.SetRenderTarget(beamsTarget); + Engine.Graphics.GraphicsDevice.Clear(Color.Transparent); + GFX.DrawVertices(Matrix.Identity, verts, vertex); + + // if fading in, update the alpha value to fade in in ~0.25 seconds. + if (alpha != 1f) { + alpha = Calc.Approach(alpha, 1f, Engine.DeltaTime * 4f); + } + } + + private void DrawRay(Vector2 position, ref int vertex, ref Ray ray) { + Vector2 value = new Vector2(0f - rayNormal.Y, rayNormal.X); + Vector2 value2 = rayNormal * ray.Width * 0.5f; + Vector2 value3 = value * ray.Length * 0.25f * 0.5f; + Vector2 value4 = value * ray.Length * 0.5f * 0.5f; + Vector2 v = position + value2 - value3 - value4; + Vector2 v2 = position - value2 - value3 - value4; + Vector2 vector = position + value2 - value3; + Vector2 vector2 = position - value2 - value3; + Vector2 vector3 = position + value2 + value3; + Vector2 vector4 = position - value2 + value3; + Vector2 v3 = position + value2 + value3 + value4; + Vector2 v4 = position - value2 + value3 + value4; + Color transparent = Color.Transparent; + Color color = ray.Color * alpha; + Quad(ref vertex, v, vector, vector2, v2, transparent, color, color, transparent); + Quad(ref vertex, vector, vector3, vector4, vector2, color, color, color, color); + Quad(ref vertex, vector3, v3, v4, vector4, color, transparent, transparent, color); + } + + private void Quad(ref int vertex, Vector2 v0, Vector2 v1, Vector2 v2, Vector2 v3, Color c0, Color c1, Color c2, Color c3) { + verts[vertex].Position.X = v0.X; + verts[vertex].Position.Y = v0.Y; + verts[vertex++].Color = c0; + verts[vertex].Position.X = v1.X; + verts[vertex].Position.Y = v1.Y; + verts[vertex++].Color = c1; + verts[vertex].Position.X = v2.X; + verts[vertex].Position.Y = v2.Y; + verts[vertex++].Color = c2; + verts[vertex].Position.X = v0.X; + verts[vertex].Position.Y = v0.Y; + verts[vertex++].Color = c0; + verts[vertex].Position.X = v2.X; + verts[vertex].Position.Y = v2.Y; + verts[vertex++].Color = c2; + verts[vertex].Position.X = v3.X; + verts[vertex].Position.Y = v3.Y; + verts[vertex++].Color = c3; + } + + public override void Render() { + if (hasBlocks) { + Vector2 position = (Scene as Level).Camera.Position; + List glassBlocks = Scene.Tracker.GetEntities(); + + foreach (Entity glassBlock in glassBlocks) { + Draw.Rect(glassBlock.X, glassBlock.Y, glassBlock.Width, glassBlock.Height, bgColor * alpha); + } + + if (starsTarget != null && !starsTarget.IsDisposed) { + foreach (Entity glassBlock in glassBlocks) { + Rectangle value = new Rectangle((int) (glassBlock.X - position.X), (int) (glassBlock.Y - position.Y), (int) glassBlock.Width, (int) glassBlock.Height); + Draw.SpriteBatch.Draw(starsTarget, glassBlock.Position, value, Color.White); + } + } + + if (beamsTarget != null && !beamsTarget.IsDisposed) { + foreach (Entity glassBlock in glassBlocks) { + Rectangle value2 = new Rectangle((int) (glassBlock.X - position.X), (int) (glassBlock.Y - position.Y), (int) glassBlock.Width, (int) glassBlock.Height); + Draw.SpriteBatch.Draw((RenderTarget2D) beamsTarget, glassBlock.Position, value2, Color.White); + } + } + } + } + + public override void Removed(Scene scene) { + base.Removed(scene); + Dispose(); + } + + public override void SceneEnd(Scene scene) { + base.SceneEnd(scene); + Dispose(); + } + + public void Dispose() { + if (starsTarget != null && !starsTarget.IsDisposed) { + starsTarget.Dispose(); + } + + if (beamsTarget != null && !beamsTarget.IsDisposed) { + beamsTarget.Dispose(); + } + starsTarget = null; + beamsTarget = null; + } + + private float Mod(float x, float m) { + return (x % m + m) % m; + } + } +} diff --git a/Entities/DashSpring.cs b/Entities/DashSpring.cs index 6d4201c..859b7d0 100644 --- a/Entities/DashSpring.cs +++ b/Entities/DashSpring.cs @@ -1,76 +1,76 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/dashSpring", "SpringCollab2020/wallDashSpringRight", "SpringCollab2020/wallDashSpringLeft")] - public class DashSpring : Spring { - - private static FieldInfo playerCanUseInfo = typeof(Spring).GetField("playerCanUse", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField); - private static FieldInfo spriteInfo = typeof(Spring).GetField("sprite", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField); - - private static MethodInfo BounceAnimateInfo = typeof(Spring).GetMethod("BounceAnimate", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod); - - public DashSpring(Vector2 position, Orientations orientation, bool playerCanUse) - : base(position, orientation, playerCanUse) { - // Only one other player collider is added so it can easily be removed - Remove(Get()); - Add(new PlayerCollider(OnCollide)); - Sprite sprite = (Sprite) spriteInfo.GetValue(this); - sprite.Reset(GFX.Game, "objects/SpringCollab2020/dashSpring/"); - sprite.Add("idle", "", 0f, default(int)); - sprite.Add("bounce", "", 0.07f, "idle", 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5); - sprite.Add("disabled", "white", 0.07f); - sprite.Play("idle"); - sprite.Origin.X = sprite.Width / 2f; - sprite.Origin.Y = sprite.Height; - - } - - public DashSpring(EntityData data, Vector2 offset) - : this(data.Position + offset, GetOrientationFromName(data.Name), data.Bool("playerCanUse", true)) { - } - - public static Orientations GetOrientationFromName(string name) { - switch (name) { - case "SpringCollab2020/dashSpring": - return Orientations.Floor; - case "SpringCollab2020/wallDashSpringRight": - return Orientations.WallRight; - case "SpringCollab2020/wallDashSpringLeft": - return Orientations.WallLeft; - default: - throw new Exception("Dash Spring name doesn't correlate to a valid Orientation!"); - } - } - - protected void OnCollide(Player player) { - if (player.StateMachine.State == 9 || !((bool) playerCanUseInfo.GetValue(this)) || !player.DashAttacking) { - return; - } - if (Orientation == Orientations.Floor) { - if (player.Speed.Y >= 0f) { - BounceAnimateInfo.Invoke(this, null); - player.SuperBounce(base.Top); - } - return; - } - if (Orientation == Orientations.WallLeft) { - if (player.SideBounce(1, base.Right, base.CenterY)) { - BounceAnimateInfo.Invoke(this, null); - } - return; - } - if (Orientation == Orientations.WallRight) { - if (player.SideBounce(-1, base.Left, base.CenterY)) { - BounceAnimateInfo.Invoke(this, null); - } - return; - } - throw new Exception("Orientation not supported!"); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/dashSpring", "SpringCollab2020/wallDashSpringRight", "SpringCollab2020/wallDashSpringLeft")] + public class DashSpring : Spring { + + private static FieldInfo playerCanUseInfo = typeof(Spring).GetField("playerCanUse", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField); + private static FieldInfo spriteInfo = typeof(Spring).GetField("sprite", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField); + + private static MethodInfo BounceAnimateInfo = typeof(Spring).GetMethod("BounceAnimate", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod); + + public DashSpring(Vector2 position, Orientations orientation, bool playerCanUse) + : base(position, orientation, playerCanUse) { + // Only one other player collider is added so it can easily be removed + Remove(Get()); + Add(new PlayerCollider(OnCollide)); + Sprite sprite = (Sprite) spriteInfo.GetValue(this); + sprite.Reset(GFX.Game, "objects/SpringCollab2020/dashSpring/"); + sprite.Add("idle", "", 0f, default(int)); + sprite.Add("bounce", "", 0.07f, "idle", 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5); + sprite.Add("disabled", "white", 0.07f); + sprite.Play("idle"); + sprite.Origin.X = sprite.Width / 2f; + sprite.Origin.Y = sprite.Height; + + } + + public DashSpring(EntityData data, Vector2 offset) + : this(data.Position + offset, GetOrientationFromName(data.Name), data.Bool("playerCanUse", true)) { + } + + public static Orientations GetOrientationFromName(string name) { + switch (name) { + case "SpringCollab2020/dashSpring": + return Orientations.Floor; + case "SpringCollab2020/wallDashSpringRight": + return Orientations.WallRight; + case "SpringCollab2020/wallDashSpringLeft": + return Orientations.WallLeft; + default: + throw new Exception("Dash Spring name doesn't correlate to a valid Orientation!"); + } + } + + protected void OnCollide(Player player) { + if (player.StateMachine.State == 9 || !((bool) playerCanUseInfo.GetValue(this)) || !player.DashAttacking) { + return; + } + if (Orientation == Orientations.Floor) { + if (player.Speed.Y >= 0f) { + BounceAnimateInfo.Invoke(this, null); + player.SuperBounce(base.Top); + } + return; + } + if (Orientation == Orientations.WallLeft) { + if (player.SideBounce(1, base.Right, base.CenterY)) { + BounceAnimateInfo.Invoke(this, null); + } + return; + } + if (Orientation == Orientations.WallRight) { + if (player.SideBounce(-1, base.Left, base.CenterY)) { + BounceAnimateInfo.Invoke(this, null); + } + return; + } + throw new Exception("Orientation not supported!"); + } + } +} diff --git a/Entities/DiagonalWingedStrawberry.cs b/Entities/DiagonalWingedStrawberry.cs index 843acd9..51a3acb 100644 --- a/Entities/DiagonalWingedStrawberry.cs +++ b/Entities/DiagonalWingedStrawberry.cs @@ -1,4 +1,4 @@ -using Celeste.Mod.Entities; +using Celeste.Mod.Entities; using Microsoft.Xna.Framework; using Monocle; using MonoMod.Cil; @@ -18,7 +18,7 @@ public DiagonalWingedStrawberry(EntityData data, Vector2 offset, EntityID gid) : if (comp is DashListener) Components.Remove(comp); } - + Add(new DashListener { OnDash = new Action(OnDiagDash) }); diff --git a/Entities/FlagSwitchGate.cs b/Entities/FlagSwitchGate.cs index 1e5b5c6..636feac 100644 --- a/Entities/FlagSwitchGate.cs +++ b/Entities/FlagSwitchGate.cs @@ -1,242 +1,242 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System; -using System.Collections; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// A switch gate triggered by a flag touch switch. - /// - /// Attributes: - /// - flag: the session flag this switch gate reacts to. Must be the same across the touch switch group. - /// - icon: the name of the icon for the switch gate (relative to objects/SpringCollab2020/flagSwitchGate) or "vanilla" for the default one. - /// - persistent: enable to have the gate stay open even when you die / change rooms. - /// - inactiveColor / activeColor / finishColor: custom colors for the touch switch. - /// - sprite: the texture for the gate block. - /// - [CustomEntity("SpringCollab2020/FlagSwitchGate")] - [Tracked] - class FlagSwitchGate : Solid { - private ParticleType P_RecoloredFire; - - private MTexture[,] nineSlice; - - private Sprite icon; - private Vector2 iconOffset; - - private Wiggler wiggler; - - private Vector2 node; - - private SoundSource openSfx; - - public string Flag { get; private set; } - - private bool persistent; - private bool triggered; - - private Color inactiveColor; - private Color activeColor; - private Color finishColor; - - public FlagSwitchGate(EntityData data, Vector2 offset) - : base(data.Position + offset, data.Width, data.Height, safe: false) { - - node = data.Nodes[0] + offset; - Flag = data.Attr("flag"); - persistent = data.Bool("persistent"); - - inactiveColor = Calc.HexToColor(data.Attr("inactiveColor", "5FCDE4")); - activeColor = Calc.HexToColor(data.Attr("activeColor", "FFFFFF")); - finishColor = Calc.HexToColor(data.Attr("finishColor", "F141DF")); - - P_RecoloredFire = new ParticleType(TouchSwitch.P_Fire) { - Color = finishColor - }; - - string iconAttribute = data.Attr("icon", "vanilla"); - icon = new Sprite(GFX.Game, iconAttribute == "vanilla" ? "objects/switchgate/icon" : $"objects/SpringCollab2020/flagSwitchGate/{iconAttribute}/icon"); - Add(icon); - icon.Add("spin", "", 0.1f, "spin"); - icon.Play("spin"); - icon.Rate = 0f; - icon.Color = inactiveColor; - icon.Position = (iconOffset = new Vector2(data.Width / 2f, data.Height / 2f)); - icon.CenterOrigin(); - Add(wiggler = Wiggler.Create(0.5f, 4f, f => { - icon.Scale = Vector2.One * (1f + f); - })); - - MTexture nineSliceTexture = GFX.Game["objects/switchgate/" + data.Attr("sprite", "block")]; - nineSlice = new MTexture[3, 3]; - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - nineSlice[i, j] = nineSliceTexture.GetSubtexture(new Rectangle(i * 8, j * 8, 8, 8)); - } - } - - Add(openSfx = new SoundSource()); - Add(new LightOcclude(0.5f)); - } - - public override void Awake(Scene scene) { - base.Awake(scene); - if (SceneAs().Session.GetFlag(Flag)) { - MoveTo(node); - icon.Rate = 0f; - icon.SetAnimationFrame(0); - icon.Color = finishColor; - } else { - Add(new Coroutine(Sequence(node))); - } - } - - public override void Render() { - float widthInTiles = Collider.Width / 8f - 1f; - float heightInTiles = Collider.Height / 8f - 1f; - for (int i = 0; i <= widthInTiles; i++) { - for (int j = 0; j <= heightInTiles; j++) { - int tilePartX = (i < widthInTiles) ? Math.Min(i, 1) : 2; - int tilePartY = (j < heightInTiles) ? Math.Min(j, 1) : 2; - nineSlice[tilePartX, tilePartY].Draw(Position + base.Shake + new Vector2(i * 8, j * 8)); - } - } - - icon.Position = iconOffset + Shake; - icon.DrawOutline(); - - base.Render(); - } - - public void Trigger() { - triggered = true; - - if (persistent) { - SceneAs().Session.SetFlag(Flag, true); - } - } - - private IEnumerator Sequence(Vector2 node) { - Vector2 start = Position; - - while (!triggered) { - yield return null; - } - - yield return 0.1f; - - // animate the icon - openSfx.Play("event:/game/general/touchswitch_gate_open"); - StartShaking(0.5f); - while (icon.Rate < 1f) { - icon.Color = Color.Lerp(inactiveColor, activeColor, icon.Rate); - icon.Rate += Engine.DeltaTime * 2f; - yield return null; - } - - yield return 0.1f; - - // move the switch gate, emitting particles along the way - int particleAt = 0; - Tween tween = Tween.Create(Tween.TweenMode.Oneshot, Ease.CubeOut, 2f, start: true); - tween.OnUpdate = tweenArg => { - MoveTo(Vector2.Lerp(start, node, tweenArg.Eased)); - if (Scene.OnInterval(0.1f)) { - particleAt++; - particleAt %= 2; - for (int tileX = 0; tileX < Width / 8f; tileX++) { - for (int tileY = 0; tileY < Height / 8f; tileY++) { - if ((tileX + tileY) % 2 == particleAt) { - SceneAs().ParticlesBG.Emit(SwitchGate.P_Behind, - Position + new Vector2(tileX * 8, tileY * 8) + Calc.Random.Range(Vector2.One * 2f, Vector2.One * 6f)); - } - } - } - } - }; - Add(tween); - - yield return 1.8f; - - bool collidableBackup = Collidable; - Collidable = false; - - // collide dust particles on the left - if (node.X <= start.X) { - Vector2 add = new Vector2(0f, 2f); - for (int tileY = 0; tileY < Height / 8f; tileY++) { - Vector2 collideAt = new Vector2(Left - 1f, Top + 4f + (tileY * 8)); - Vector2 noCollideAt = collideAt + Vector2.UnitX; - if (Scene.CollideCheck(collideAt) && !Scene.CollideCheck(noCollideAt)) { - SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt + add, (float) Math.PI); - SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt - add, (float) Math.PI); - } - } - } - - // collide dust particles on the rigth - if (node.X >= start.X) { - Vector2 add = new Vector2(0f, 2f); - for (int tileY = 0; tileY < Height / 8f; tileY++) { - Vector2 collideAt = new Vector2(Right + 1f, Top + 4f + (tileY * 8)); - Vector2 noCollideAt = collideAt - Vector2.UnitX * 2f; - if (Scene.CollideCheck(collideAt) && !Scene.CollideCheck(noCollideAt)) { - SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt + add, 0f); - SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt - add, 0f); - } - } - } - - // collide dust particles on the top - if (node.Y <= start.Y) { - Vector2 add = new Vector2(2f, 0f); - for (int tileX = 0; tileX < Width / 8f; tileX++) { - Vector2 collideAt = new Vector2(Left + 4f + (tileX * 8), Top - 1f); - Vector2 noCollideAt = collideAt + Vector2.UnitY; - if (Scene.CollideCheck(collideAt) && !Scene.CollideCheck(noCollideAt)) { - SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt + add, -(float) Math.PI / 2f); - SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt - add, -(float) Math.PI / 2f); - } - } - } - - // collide dust particles on the bottom - if (node.Y >= start.Y) { - Vector2 add = new Vector2(2f, 0f); - for (int tileX = 0; tileX < Width / 8f; tileX++) { - Vector2 collideAt = new Vector2(Left + 4f + (tileX * 8), Bottom + 1f); - Vector2 noCollideAt = collideAt - Vector2.UnitY * 2f; - if (Scene.CollideCheck(collideAt) && !Scene.CollideCheck(noCollideAt)) { - SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt + add, (float) Math.PI / 2f); - SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt - add, (float) Math.PI / 2f); - } - } - } - Collidable = collidableBackup; - - // moving is over - Audio.Play("event:/game/general/touchswitch_gate_finish", Position); - StartShaking(0.2f); - while (icon.Rate > 0f) { - icon.Color = Color.Lerp(activeColor, finishColor, 1f - icon.Rate); - icon.Rate -= Engine.DeltaTime * 4f; - yield return null; - } - icon.Rate = 0f; - icon.SetAnimationFrame(0); - wiggler.Start(); - - // emit fire particles if the block is not behind a solid. - collidableBackup = Collidable; - Collidable = false; - if (!Scene.CollideCheck(Center)) { - for (int i = 0; i < 32; i++) { - float angle = Calc.Random.NextFloat((float) Math.PI * 2f); - SceneAs().ParticlesFG.Emit(P_RecoloredFire, Position + iconOffset + Calc.AngleToVector(angle, 4f), angle); - } - } - Collidable = collidableBackup; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System; +using System.Collections; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// A switch gate triggered by a flag touch switch. + /// + /// Attributes: + /// - flag: the session flag this switch gate reacts to. Must be the same across the touch switch group. + /// - icon: the name of the icon for the switch gate (relative to objects/SpringCollab2020/flagSwitchGate) or "vanilla" for the default one. + /// - persistent: enable to have the gate stay open even when you die / change rooms. + /// - inactiveColor / activeColor / finishColor: custom colors for the touch switch. + /// - sprite: the texture for the gate block. + /// + [CustomEntity("SpringCollab2020/FlagSwitchGate")] + [Tracked] + class FlagSwitchGate : Solid { + private ParticleType P_RecoloredFire; + + private MTexture[,] nineSlice; + + private Sprite icon; + private Vector2 iconOffset; + + private Wiggler wiggler; + + private Vector2 node; + + private SoundSource openSfx; + + public string Flag { get; private set; } + + private bool persistent; + private bool triggered; + + private Color inactiveColor; + private Color activeColor; + private Color finishColor; + + public FlagSwitchGate(EntityData data, Vector2 offset) + : base(data.Position + offset, data.Width, data.Height, safe: false) { + + node = data.Nodes[0] + offset; + Flag = data.Attr("flag"); + persistent = data.Bool("persistent"); + + inactiveColor = Calc.HexToColor(data.Attr("inactiveColor", "5FCDE4")); + activeColor = Calc.HexToColor(data.Attr("activeColor", "FFFFFF")); + finishColor = Calc.HexToColor(data.Attr("finishColor", "F141DF")); + + P_RecoloredFire = new ParticleType(TouchSwitch.P_Fire) { + Color = finishColor + }; + + string iconAttribute = data.Attr("icon", "vanilla"); + icon = new Sprite(GFX.Game, iconAttribute == "vanilla" ? "objects/switchgate/icon" : $"objects/SpringCollab2020/flagSwitchGate/{iconAttribute}/icon"); + Add(icon); + icon.Add("spin", "", 0.1f, "spin"); + icon.Play("spin"); + icon.Rate = 0f; + icon.Color = inactiveColor; + icon.Position = (iconOffset = new Vector2(data.Width / 2f, data.Height / 2f)); + icon.CenterOrigin(); + Add(wiggler = Wiggler.Create(0.5f, 4f, f => { + icon.Scale = Vector2.One * (1f + f); + })); + + MTexture nineSliceTexture = GFX.Game["objects/switchgate/" + data.Attr("sprite", "block")]; + nineSlice = new MTexture[3, 3]; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + nineSlice[i, j] = nineSliceTexture.GetSubtexture(new Rectangle(i * 8, j * 8, 8, 8)); + } + } + + Add(openSfx = new SoundSource()); + Add(new LightOcclude(0.5f)); + } + + public override void Awake(Scene scene) { + base.Awake(scene); + if (SceneAs().Session.GetFlag(Flag)) { + MoveTo(node); + icon.Rate = 0f; + icon.SetAnimationFrame(0); + icon.Color = finishColor; + } else { + Add(new Coroutine(Sequence(node))); + } + } + + public override void Render() { + float widthInTiles = Collider.Width / 8f - 1f; + float heightInTiles = Collider.Height / 8f - 1f; + for (int i = 0; i <= widthInTiles; i++) { + for (int j = 0; j <= heightInTiles; j++) { + int tilePartX = (i < widthInTiles) ? Math.Min(i, 1) : 2; + int tilePartY = (j < heightInTiles) ? Math.Min(j, 1) : 2; + nineSlice[tilePartX, tilePartY].Draw(Position + base.Shake + new Vector2(i * 8, j * 8)); + } + } + + icon.Position = iconOffset + Shake; + icon.DrawOutline(); + + base.Render(); + } + + public void Trigger() { + triggered = true; + + if (persistent) { + SceneAs().Session.SetFlag(Flag, true); + } + } + + private IEnumerator Sequence(Vector2 node) { + Vector2 start = Position; + + while (!triggered) { + yield return null; + } + + yield return 0.1f; + + // animate the icon + openSfx.Play("event:/game/general/touchswitch_gate_open"); + StartShaking(0.5f); + while (icon.Rate < 1f) { + icon.Color = Color.Lerp(inactiveColor, activeColor, icon.Rate); + icon.Rate += Engine.DeltaTime * 2f; + yield return null; + } + + yield return 0.1f; + + // move the switch gate, emitting particles along the way + int particleAt = 0; + Tween tween = Tween.Create(Tween.TweenMode.Oneshot, Ease.CubeOut, 2f, start: true); + tween.OnUpdate = tweenArg => { + MoveTo(Vector2.Lerp(start, node, tweenArg.Eased)); + if (Scene.OnInterval(0.1f)) { + particleAt++; + particleAt %= 2; + for (int tileX = 0; tileX < Width / 8f; tileX++) { + for (int tileY = 0; tileY < Height / 8f; tileY++) { + if ((tileX + tileY) % 2 == particleAt) { + SceneAs().ParticlesBG.Emit(SwitchGate.P_Behind, + Position + new Vector2(tileX * 8, tileY * 8) + Calc.Random.Range(Vector2.One * 2f, Vector2.One * 6f)); + } + } + } + } + }; + Add(tween); + + yield return 1.8f; + + bool collidableBackup = Collidable; + Collidable = false; + + // collide dust particles on the left + if (node.X <= start.X) { + Vector2 add = new Vector2(0f, 2f); + for (int tileY = 0; tileY < Height / 8f; tileY++) { + Vector2 collideAt = new Vector2(Left - 1f, Top + 4f + (tileY * 8)); + Vector2 noCollideAt = collideAt + Vector2.UnitX; + if (Scene.CollideCheck(collideAt) && !Scene.CollideCheck(noCollideAt)) { + SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt + add, (float) Math.PI); + SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt - add, (float) Math.PI); + } + } + } + + // collide dust particles on the rigth + if (node.X >= start.X) { + Vector2 add = new Vector2(0f, 2f); + for (int tileY = 0; tileY < Height / 8f; tileY++) { + Vector2 collideAt = new Vector2(Right + 1f, Top + 4f + (tileY * 8)); + Vector2 noCollideAt = collideAt - Vector2.UnitX * 2f; + if (Scene.CollideCheck(collideAt) && !Scene.CollideCheck(noCollideAt)) { + SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt + add, 0f); + SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt - add, 0f); + } + } + } + + // collide dust particles on the top + if (node.Y <= start.Y) { + Vector2 add = new Vector2(2f, 0f); + for (int tileX = 0; tileX < Width / 8f; tileX++) { + Vector2 collideAt = new Vector2(Left + 4f + (tileX * 8), Top - 1f); + Vector2 noCollideAt = collideAt + Vector2.UnitY; + if (Scene.CollideCheck(collideAt) && !Scene.CollideCheck(noCollideAt)) { + SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt + add, -(float) Math.PI / 2f); + SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt - add, -(float) Math.PI / 2f); + } + } + } + + // collide dust particles on the bottom + if (node.Y >= start.Y) { + Vector2 add = new Vector2(2f, 0f); + for (int tileX = 0; tileX < Width / 8f; tileX++) { + Vector2 collideAt = new Vector2(Left + 4f + (tileX * 8), Bottom + 1f); + Vector2 noCollideAt = collideAt - Vector2.UnitY * 2f; + if (Scene.CollideCheck(collideAt) && !Scene.CollideCheck(noCollideAt)) { + SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt + add, (float) Math.PI / 2f); + SceneAs().ParticlesFG.Emit(SwitchGate.P_Dust, collideAt - add, (float) Math.PI / 2f); + } + } + } + Collidable = collidableBackup; + + // moving is over + Audio.Play("event:/game/general/touchswitch_gate_finish", Position); + StartShaking(0.2f); + while (icon.Rate > 0f) { + icon.Color = Color.Lerp(activeColor, finishColor, 1f - icon.Rate); + icon.Rate -= Engine.DeltaTime * 4f; + yield return null; + } + icon.Rate = 0f; + icon.SetAnimationFrame(0); + wiggler.Start(); + + // emit fire particles if the block is not behind a solid. + collidableBackup = Collidable; + Collidable = false; + if (!Scene.CollideCheck(Center)) { + for (int i = 0; i < 32; i++) { + float angle = Calc.Random.NextFloat((float) Math.PI * 2f); + SceneAs().ParticlesFG.Emit(P_RecoloredFire, Position + iconOffset + Calc.AngleToVector(angle, 4f), angle); + } + } + Collidable = collidableBackup; + } + } +} diff --git a/Entities/FlagToggleComponent.cs b/Entities/FlagToggleComponent.cs index 2fb5b9f..ac13a41 100644 --- a/Entities/FlagToggleComponent.cs +++ b/Entities/FlagToggleComponent.cs @@ -1,45 +1,45 @@ -using Monocle; -using System; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// A component that can be attached to an entity to make it (dis)appear depending on a flag. - /// - class FlagToggleComponent : Component { - public bool Enabled = true; - private string flag; - private Action onDisable; - private Action onEnable; - private bool inverted; - - public FlagToggleComponent(string flag, bool inverted, Action onDisable = null, Action onEnable = null) : base(true, false) { - this.flag = flag; - this.inverted = inverted; - this.onDisable = onDisable; - this.onEnable = onEnable; - } - - public override void Update() { - base.Update(); - UpdateFlag(); - } - - public void UpdateFlag() { - if ((!inverted && SceneAs().Session.GetFlag(flag) != Enabled) - || (inverted && SceneAs().Session.GetFlag(flag) == Enabled)) { - - if (Enabled) { - // disable the entity. - Entity.Visible = Entity.Collidable = false; - onDisable?.Invoke(); - Enabled = false; - } else { - // enable the entity. - Entity.Visible = Entity.Collidable = true; - onEnable?.Invoke(); - Enabled = true; - } - } - } - } -} +using Monocle; +using System; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// A component that can be attached to an entity to make it (dis)appear depending on a flag. + /// + class FlagToggleComponent : Component { + public bool Enabled = true; + private string flag; + private Action onDisable; + private Action onEnable; + private bool inverted; + + public FlagToggleComponent(string flag, bool inverted, Action onDisable = null, Action onEnable = null) : base(true, false) { + this.flag = flag; + this.inverted = inverted; + this.onDisable = onDisable; + this.onEnable = onEnable; + } + + public override void Update() { + base.Update(); + UpdateFlag(); + } + + public void UpdateFlag() { + if ((!inverted && SceneAs().Session.GetFlag(flag) != Enabled) + || (inverted && SceneAs().Session.GetFlag(flag) == Enabled)) { + + if (Enabled) { + // disable the entity. + Entity.Visible = Entity.Collidable = false; + onDisable?.Invoke(); + Enabled = false; + } else { + // enable the entity. + Entity.Visible = Entity.Collidable = true; + onEnable?.Invoke(); + Enabled = true; + } + } + } + } +} diff --git a/Entities/FlagToggleStarRotateSpinner.cs b/Entities/FlagToggleStarRotateSpinner.cs index b4dca7f..1299a71 100644 --- a/Entities/FlagToggleStarRotateSpinner.cs +++ b/Entities/FlagToggleStarRotateSpinner.cs @@ -1,29 +1,29 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/FlagToggleStarRotateSpinner")] - class FlagToggleStarRotateSpinner : StarRotateSpinner { - private FlagToggleComponent toggle; - - public FlagToggleStarRotateSpinner(EntityData data, Vector2 offset) : base(data, offset) { - Add(toggle = new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"))); - } - - public override void Added(Scene scene) { - base.Added(scene); - toggle.UpdateFlag(); - } - - public override void Update() { - if (toggle.Enabled) { - // update the entity as usual - base.Update(); - } else { - // freeze the entity, but continue updating the component - toggle.Update(); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/FlagToggleStarRotateSpinner")] + class FlagToggleStarRotateSpinner : StarRotateSpinner { + private FlagToggleComponent toggle; + + public FlagToggleStarRotateSpinner(EntityData data, Vector2 offset) : base(data, offset) { + Add(toggle = new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"))); + } + + public override void Added(Scene scene) { + base.Added(scene); + toggle.UpdateFlag(); + } + + public override void Update() { + if (toggle.Enabled) { + // update the entity as usual + base.Update(); + } else { + // freeze the entity, but continue updating the component + toggle.Update(); + } + } + } +} diff --git a/Entities/FlagToggleStarTrackSpinner.cs b/Entities/FlagToggleStarTrackSpinner.cs index 9356516..0e413ad 100644 --- a/Entities/FlagToggleStarTrackSpinner.cs +++ b/Entities/FlagToggleStarTrackSpinner.cs @@ -1,29 +1,29 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/FlagToggleStarTrackSpinner")] - class FlagToggleStarTrackSpinner : StarTrackSpinner { - private FlagToggleComponent toggle; - - public FlagToggleStarTrackSpinner(EntityData data, Vector2 offset) : base(data, offset) { - Add(toggle = new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"))); - } - - public override void Added(Scene scene) { - base.Added(scene); - toggle.UpdateFlag(); - } - - public override void Update() { - if (toggle.Enabled) { - // update the entity as usual - base.Update(); - } else { - // freeze the entity, but continue updating the component - toggle.Update(); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/FlagToggleStarTrackSpinner")] + class FlagToggleStarTrackSpinner : StarTrackSpinner { + private FlagToggleComponent toggle; + + public FlagToggleStarTrackSpinner(EntityData data, Vector2 offset) : base(data, offset) { + Add(toggle = new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"))); + } + + public override void Added(Scene scene) { + base.Added(scene); + toggle.UpdateFlag(); + } + + public override void Update() { + if (toggle.Enabled) { + // update the entity as usual + base.Update(); + } else { + // freeze the entity, but continue updating the component + toggle.Update(); + } + } + } +} diff --git a/Entities/FlagToggleWater.cs b/Entities/FlagToggleWater.cs index 0e703e4..6a1dcfc 100644 --- a/Entities/FlagToggleWater.cs +++ b/Entities/FlagToggleWater.cs @@ -1,36 +1,36 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/FlagToggleWater")] - [TrackedAs(typeof(Water))] - class FlagToggleWater : Water { - private FlagToggleComponent toggle; - - public FlagToggleWater(EntityData data, Vector2 offset) : base(data, offset) { - // look up the displacement renderer to be able to toggle it. - DisplacementRenderHook displacementHook = null; - foreach (Component component in this) { - if (component is DisplacementRenderHook displacement) { - displacementHook = displacement; - break; - } - } - - // when the water is toggled, toggle the displacement as well. - Add(toggle = new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"), - () => Remove(displacementHook), () => Add(displacementHook))); - } - - public override void Update() { - if (toggle.Enabled) { - // update the entity as usual - base.Update(); - } else { - // freeze the entity, but continue updating the component - toggle.Update(); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/FlagToggleWater")] + [TrackedAs(typeof(Water))] + class FlagToggleWater : Water { + private FlagToggleComponent toggle; + + public FlagToggleWater(EntityData data, Vector2 offset) : base(data, offset) { + // look up the displacement renderer to be able to toggle it. + DisplacementRenderHook displacementHook = null; + foreach (Component component in this) { + if (component is DisplacementRenderHook displacement) { + displacementHook = displacement; + break; + } + } + + // when the water is toggled, toggle the displacement as well. + Add(toggle = new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"), + () => Remove(displacementHook), () => Add(displacementHook))); + } + + public override void Update() { + if (toggle.Enabled) { + // update the entity as usual + base.Update(); + } else { + // freeze the entity, but continue updating the component + toggle.Update(); + } + } + } +} diff --git a/Entities/FlagToggleWaterfall.cs b/Entities/FlagToggleWaterfall.cs index d6d1514..c167255 100644 --- a/Entities/FlagToggleWaterfall.cs +++ b/Entities/FlagToggleWaterfall.cs @@ -1,84 +1,84 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/FlagToggleWaterfall")] - class FlagToggleWaterfall : WaterFall { - private FlagToggleComponent toggle; - private SoundSource loopingSfx; - private SoundSource enteringSfx; - private string loopingSfxEvent; - private string enteringSfxEvent; - - private bool alreadyTurnedOnOnce = false; - - public FlagToggleWaterfall(EntityData data, Vector2 offset) : base(data, offset) { - Add(toggle = new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"), () => { - // disable the waterfall sound. - loopingSfx.Stop(); - enteringSfx.Stop(); - }, () => { - // enable it again. - loopingSfx.Play(loopingSfxEvent); - enteringSfx.Play(enteringSfxEvent); - })); - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - // store the values for these private variables. - DynData self = new DynData(this); - loopingSfx = self.Get("loopingSfx"); - enteringSfx = self.Get("enteringSfx"); - loopingSfxEvent = loopingSfx.EventName; - enteringSfxEvent = enteringSfx.EventName; - - toggle.UpdateFlag(); - - if (toggle.Enabled) { - alreadyTurnedOnOnce = true; - } - } - - public override void Update() { - if (toggle.Enabled) { - // if we are turning the waterfall on for the first time, compute its height, now that other entities - // (pools of water, underwater switch controller) have been toggled to match as well. - if (!alreadyTurnedOnOnce) { - DynData self = new DynData(this); - float height = 8f; - Water water = null; - Solid solid = null; - while (Y + height < SceneAs().Bounds.Bottom - && (water = Scene.CollideFirst(new Rectangle((int) X, (int) (Y + height), 8, 8))) == null - && ((solid = Scene.CollideFirst(new Rectangle((int) X, (int) (Y + height), 8, 8))) == null || !solid.BlockWaterfalls)) { - - height += 8f; - solid = null; - } - if (water != null && !Scene.CollideCheck(new Rectangle((int) X, (int) (Y + height), 8, 16))) { - enteringSfxEvent = "event:/env/local/waterfall_small_in_deep"; - } else { - enteringSfxEvent = "event:/env/local/waterfall_small_in_shallow"; - } - enteringSfx.Play(enteringSfxEvent); - - self["height"] = height; - self["water"] = water; - self["solid"] = solid; - - alreadyTurnedOnOnce = true; - } - - // update the entity as usual - base.Update(); - } else { - // freeze the entity, but continue updating the component - toggle.Update(); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/FlagToggleWaterfall")] + class FlagToggleWaterfall : WaterFall { + private FlagToggleComponent toggle; + private SoundSource loopingSfx; + private SoundSource enteringSfx; + private string loopingSfxEvent; + private string enteringSfxEvent; + + private bool alreadyTurnedOnOnce = false; + + public FlagToggleWaterfall(EntityData data, Vector2 offset) : base(data, offset) { + Add(toggle = new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"), () => { + // disable the waterfall sound. + loopingSfx.Stop(); + enteringSfx.Stop(); + }, () => { + // enable it again. + loopingSfx.Play(loopingSfxEvent); + enteringSfx.Play(enteringSfxEvent); + })); + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + // store the values for these private variables. + DynData self = new DynData(this); + loopingSfx = self.Get("loopingSfx"); + enteringSfx = self.Get("enteringSfx"); + loopingSfxEvent = loopingSfx.EventName; + enteringSfxEvent = enteringSfx.EventName; + + toggle.UpdateFlag(); + + if (toggle.Enabled) { + alreadyTurnedOnOnce = true; + } + } + + public override void Update() { + if (toggle.Enabled) { + // if we are turning the waterfall on for the first time, compute its height, now that other entities + // (pools of water, underwater switch controller) have been toggled to match as well. + if (!alreadyTurnedOnOnce) { + DynData self = new DynData(this); + float height = 8f; + Water water = null; + Solid solid = null; + while (Y + height < SceneAs().Bounds.Bottom + && (water = Scene.CollideFirst(new Rectangle((int) X, (int) (Y + height), 8, 8))) == null + && ((solid = Scene.CollideFirst(new Rectangle((int) X, (int) (Y + height), 8, 8))) == null || !solid.BlockWaterfalls)) { + + height += 8f; + solid = null; + } + if (water != null && !Scene.CollideCheck(new Rectangle((int) X, (int) (Y + height), 8, 16))) { + enteringSfxEvent = "event:/env/local/waterfall_small_in_deep"; + } else { + enteringSfxEvent = "event:/env/local/waterfall_small_in_shallow"; + } + enteringSfx.Play(enteringSfxEvent); + + self["height"] = height; + self["water"] = water; + self["solid"] = solid; + + alreadyTurnedOnOnce = true; + } + + // update the entity as usual + base.Update(); + } else { + // freeze the entity, but continue updating the component + toggle.Update(); + } + } + } +} diff --git a/Entities/FlagTouchSwitch.cs b/Entities/FlagTouchSwitch.cs index 9d5aa3f..226c2b3 100644 --- a/Entities/FlagTouchSwitch.cs +++ b/Entities/FlagTouchSwitch.cs @@ -1,277 +1,277 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// A touch switch triggering an arbitrary session flag. - /// - /// Attributes: - /// - flag: the session flag this touch switch sets. Must be the same across the whole touch switch group. - /// - icon: the name of the icon for the touch switch (relative to objects/SpringCollab2020/flagTouchSwitch) or "vanilla" for the default one. - /// - persistent: enable to have the switch stay active even when you die / change rooms. - /// - inactiveColor / activeColor / finishColor: custom colors for the touch switch. - /// - [CustomEntity("SpringCollab2020/FlagTouchSwitch")] - [Tracked] - class FlagTouchSwitch : Entity { - private static FieldInfo seekerPushRadius = typeof(Seeker).GetField("pushRadius", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo seekerPhysicsHitbox = typeof(Seeker).GetField("physicsHitbox", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo pufferPushRadius = typeof(Puffer).GetField("pushRadius", BindingFlags.NonPublic | BindingFlags.Instance); - - public static void Load() { - On.Celeste.Seeker.RegenerateCoroutine += onSeekerRegenerateCoroutine; - On.Celeste.Puffer.Explode += onPufferExplode; - } - - public static void Unload() { - On.Celeste.Seeker.RegenerateCoroutine -= onSeekerRegenerateCoroutine; - On.Celeste.Puffer.Explode -= onPufferExplode; - } - - private static IEnumerator onSeekerRegenerateCoroutine(On.Celeste.Seeker.orig_RegenerateCoroutine orig, Seeker self) { - IEnumerator origEnum = orig(self); - while (origEnum.MoveNext()) { - yield return origEnum.Current; - } - - // make the seeker check for flag touch switches as well. - self.Collider = (Collider) seekerPushRadius.GetValue(self); - turnOnTouchSwitchesCollidingWith(self); - self.Collider = (Collider) seekerPhysicsHitbox.GetValue(self); - } - - private static void onPufferExplode(On.Celeste.Puffer.orig_Explode orig, Puffer self) { - orig(self); - - // make the puffer check for flag touch switches as well. - Collider oldCollider = self.Collider; - self.Collider = (Collider) pufferPushRadius.GetValue(self); - turnOnTouchSwitchesCollidingWith(self); - self.Collider = oldCollider; - } - - private static void turnOnTouchSwitchesCollidingWith(Entity self) { - foreach (FlagTouchSwitch touchSwitch in self.Scene.Tracker.GetEntities()) { - if (self.CollideCheck(touchSwitch)) { - touchSwitch.turnOn(); - } - } - } - - private ParticleType P_RecoloredFire; - - private int id; - private string flag; - - // contains all the touch switches in the room - private List allTouchSwitchesInRoom; - - private bool activated = false; - private bool finished = false; - - private SoundSource touchSfx; - - private MTexture border = GFX.Game["objects/touchswitch/container"]; - - private Sprite icon; - private bool persistent; - - private Color inactiveColor; - private Color activeColor; - private Color finishColor; - - private float ease; - - private Wiggler wiggler; - - private Vector2 pulse = Vector2.One; - - private float timer = 0f; - - private BloomPoint bloom; - - private Level level => (Level) Scene; - - public FlagTouchSwitch(EntityData data, Vector2 offset) - : base(data.Position + offset) { - - Depth = 2000; - - id = data.ID; - flag = data.Attr("flag"); - persistent = data.Bool("persistent", false); - - inactiveColor = Calc.HexToColor(data.Attr("inactiveColor", "5FCDE4")); - activeColor = Calc.HexToColor(data.Attr("activeColor", "FFFFFF")); - finishColor = Calc.HexToColor(data.Attr("finishColor", "F141DF")); - - P_RecoloredFire = new ParticleType(TouchSwitch.P_Fire) { - Color = finishColor - }; - - // set up collision - Collider = new Hitbox(16f, 16f, -8f, -8f); - Add(new PlayerCollider(onPlayer, null, new Hitbox(30f, 30f, -15f, -15f))); - Add(new HoldableCollider(onHoldable, new Hitbox(20f, 20f, -10f, -10f))); - Add(new SeekerCollider(onSeeker, new Hitbox(24f, 24f, -12f, -12f))); - - // set up the icon - string iconAttribute = data.Attr("icon", "vanilla"); - icon = new Sprite(GFX.Game, iconAttribute == "vanilla" ? "objects/touchswitch/icon" : $"objects/SpringCollab2020/flagTouchSwitch/{iconAttribute}/icon"); - Add(icon); - icon.Add("idle", "", 0f, default(int)); - icon.Add("spin", "", 0.1f, new Chooser("spin", 1f), 0, 1, 2, 3, 4, 5); - icon.Play("spin"); - icon.Color = inactiveColor; - icon.CenterOrigin(); - - Add(bloom = new BloomPoint(0f, 16f)); - bloom.Alpha = 0f; - - Add(wiggler = Wiggler.Create(0.5f, 4f, v => { - pulse = Vector2.One * (1f + v * 0.25f); - })); - - Add(new VertexLight(Color.White, 0.8f, 16, 32)); - Add(touchSfx = new SoundSource()); - } - - public override void Added(Scene scene) { - base.Added(scene); - - if (level.Session.GetFlag(flag)) { - // start directly finished, since the session flag is already set. - activated = true; - finished = true; - - icon.Rate = 0.1f; - icon.Play("idle"); - icon.Color = finishColor; - ease = 1f; - } else if (level.Session.GetFlag(flag + "_switch" + id)) { - // only that switch is activated, not the whole group. - activated = true; - - icon.Rate = 4f; - icon.Color = activeColor; - ease = 1f; - } - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - // look around for other touch switches that belong to the same group (same flag). - allTouchSwitchesInRoom = Scene.Tracker.GetEntities() - .FindAll(touchSwitch => (touchSwitch as FlagTouchSwitch)?.flag == flag).OfType().ToList(); - } - - private void onPlayer(Player player) { - turnOn(); - } - - private void onHoldable(Holdable h) { - turnOn(); - } - - private void onSeeker(Seeker seeker) { - if (SceneAs().InsideCamera(Position, 10f)) { - turnOn(); - } - } - - private void turnOn() { - if (!activated) { - touchSfx.Play("event:/game/general/touchswitch_any"); - - activated = true; - - // animation - wiggler.Start(); - for (int i = 0; i < 32; i++) { - float num = Calc.Random.NextFloat((float) Math.PI * 2f); - level.Particles.Emit(TouchSwitch.P_FireWhite, Position + Calc.AngleToVector(num, 6f), num); - } - icon.Rate = 4f; - - if (persistent) { - // this switch is persistent. save its activation in the session. - level.Session.SetFlag(flag + "_switch" + id, true); - } - - if ((SpringCollab2020MapDataProcessor.FlagTouchSwitches.Count <= level.Session.Area.ID - || SpringCollab2020MapDataProcessor.FlagTouchSwitches[level.Session.Area.ID][(int) level.Session.Area.Mode][flag] - .All(touchSwitchID => touchSwitchID.Level == level.Session.Level || level.Session.GetFlag(flag + "_switch" + touchSwitchID.ID))) - && allTouchSwitchesInRoom.All(touchSwitch => touchSwitch.activated)) { - - // all switches in the room are enabled, and all session flags for switches outside the room are enabled. - // so, the group is complete. - - foreach (FlagTouchSwitch touchSwitch in allTouchSwitchesInRoom) { - touchSwitch.finish(); - } - - SoundEmitter.Play("event:/game/general/touchswitch_last_oneshot"); - Add(new SoundSource("event:/game/general/touchswitch_last_cutoff")); - - // trigger associated switch gate(s). - foreach (FlagSwitchGate switchGate in Scene.Tracker.GetEntities().OfType()) { - if (switchGate.Flag == flag) { - switchGate.Trigger(); - } - } - - // if all the switches are persistent, the flag it's setting is persistent. - if (allTouchSwitchesInRoom.All(touchSwitch => touchSwitch.persistent)) { - level.Session.SetFlag(flag, true); - } - } - } - } - - private void finish() { - finished = true; - ease = 0f; - } - - public override void Update() { - timer += Engine.DeltaTime * 8f; - ease = Calc.Approach(ease, (finished || activated) ? 1f : 0f, Engine.DeltaTime * 2f); - - icon.Color = Color.Lerp(inactiveColor, finished ? finishColor : activeColor, ease); - icon.Color *= 0.5f + ((float) Math.Sin(timer) + 1f) / 2f * (1f - ease) * 0.5f + 0.5f * ease; - - bloom.Alpha = ease; - if (finished) { - if (icon.Rate > 0.1f) { - icon.Rate -= 2f * Engine.DeltaTime; - if (icon.Rate <= 0.1f) { - icon.Rate = 0.1f; - wiggler.Start(); - icon.Play("idle"); - level.Displacement.AddBurst(Position, 0.6f, 4f, 28f, 0.2f); - } - } else if (Scene.OnInterval(0.03f)) { - Vector2 position = Position + new Vector2(0f, 1f) + Calc.AngleToVector(Calc.Random.NextAngle(), 5f); - level.ParticlesBG.Emit(P_RecoloredFire, position); - } - } - - base.Update(); - } - - public override void Render() { - border.DrawCentered(Position + new Vector2(0f, -1f), Color.Black); - border.DrawCentered(Position, icon.Color, pulse); - base.Render(); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// A touch switch triggering an arbitrary session flag. + /// + /// Attributes: + /// - flag: the session flag this touch switch sets. Must be the same across the whole touch switch group. + /// - icon: the name of the icon for the touch switch (relative to objects/SpringCollab2020/flagTouchSwitch) or "vanilla" for the default one. + /// - persistent: enable to have the switch stay active even when you die / change rooms. + /// - inactiveColor / activeColor / finishColor: custom colors for the touch switch. + /// + [CustomEntity("SpringCollab2020/FlagTouchSwitch")] + [Tracked] + class FlagTouchSwitch : Entity { + private static FieldInfo seekerPushRadius = typeof(Seeker).GetField("pushRadius", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo seekerPhysicsHitbox = typeof(Seeker).GetField("physicsHitbox", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo pufferPushRadius = typeof(Puffer).GetField("pushRadius", BindingFlags.NonPublic | BindingFlags.Instance); + + public static void Load() { + On.Celeste.Seeker.RegenerateCoroutine += onSeekerRegenerateCoroutine; + On.Celeste.Puffer.Explode += onPufferExplode; + } + + public static void Unload() { + On.Celeste.Seeker.RegenerateCoroutine -= onSeekerRegenerateCoroutine; + On.Celeste.Puffer.Explode -= onPufferExplode; + } + + private static IEnumerator onSeekerRegenerateCoroutine(On.Celeste.Seeker.orig_RegenerateCoroutine orig, Seeker self) { + IEnumerator origEnum = orig(self); + while (origEnum.MoveNext()) { + yield return origEnum.Current; + } + + // make the seeker check for flag touch switches as well. + self.Collider = (Collider) seekerPushRadius.GetValue(self); + turnOnTouchSwitchesCollidingWith(self); + self.Collider = (Collider) seekerPhysicsHitbox.GetValue(self); + } + + private static void onPufferExplode(On.Celeste.Puffer.orig_Explode orig, Puffer self) { + orig(self); + + // make the puffer check for flag touch switches as well. + Collider oldCollider = self.Collider; + self.Collider = (Collider) pufferPushRadius.GetValue(self); + turnOnTouchSwitchesCollidingWith(self); + self.Collider = oldCollider; + } + + private static void turnOnTouchSwitchesCollidingWith(Entity self) { + foreach (FlagTouchSwitch touchSwitch in self.Scene.Tracker.GetEntities()) { + if (self.CollideCheck(touchSwitch)) { + touchSwitch.turnOn(); + } + } + } + + private ParticleType P_RecoloredFire; + + private int id; + private string flag; + + // contains all the touch switches in the room + private List allTouchSwitchesInRoom; + + private bool activated = false; + private bool finished = false; + + private SoundSource touchSfx; + + private MTexture border = GFX.Game["objects/touchswitch/container"]; + + private Sprite icon; + private bool persistent; + + private Color inactiveColor; + private Color activeColor; + private Color finishColor; + + private float ease; + + private Wiggler wiggler; + + private Vector2 pulse = Vector2.One; + + private float timer = 0f; + + private BloomPoint bloom; + + private Level level => (Level) Scene; + + public FlagTouchSwitch(EntityData data, Vector2 offset) + : base(data.Position + offset) { + + Depth = 2000; + + id = data.ID; + flag = data.Attr("flag"); + persistent = data.Bool("persistent", false); + + inactiveColor = Calc.HexToColor(data.Attr("inactiveColor", "5FCDE4")); + activeColor = Calc.HexToColor(data.Attr("activeColor", "FFFFFF")); + finishColor = Calc.HexToColor(data.Attr("finishColor", "F141DF")); + + P_RecoloredFire = new ParticleType(TouchSwitch.P_Fire) { + Color = finishColor + }; + + // set up collision + Collider = new Hitbox(16f, 16f, -8f, -8f); + Add(new PlayerCollider(onPlayer, null, new Hitbox(30f, 30f, -15f, -15f))); + Add(new HoldableCollider(onHoldable, new Hitbox(20f, 20f, -10f, -10f))); + Add(new SeekerCollider(onSeeker, new Hitbox(24f, 24f, -12f, -12f))); + + // set up the icon + string iconAttribute = data.Attr("icon", "vanilla"); + icon = new Sprite(GFX.Game, iconAttribute == "vanilla" ? "objects/touchswitch/icon" : $"objects/SpringCollab2020/flagTouchSwitch/{iconAttribute}/icon"); + Add(icon); + icon.Add("idle", "", 0f, default(int)); + icon.Add("spin", "", 0.1f, new Chooser("spin", 1f), 0, 1, 2, 3, 4, 5); + icon.Play("spin"); + icon.Color = inactiveColor; + icon.CenterOrigin(); + + Add(bloom = new BloomPoint(0f, 16f)); + bloom.Alpha = 0f; + + Add(wiggler = Wiggler.Create(0.5f, 4f, v => { + pulse = Vector2.One * (1f + v * 0.25f); + })); + + Add(new VertexLight(Color.White, 0.8f, 16, 32)); + Add(touchSfx = new SoundSource()); + } + + public override void Added(Scene scene) { + base.Added(scene); + + if (level.Session.GetFlag(flag)) { + // start directly finished, since the session flag is already set. + activated = true; + finished = true; + + icon.Rate = 0.1f; + icon.Play("idle"); + icon.Color = finishColor; + ease = 1f; + } else if (level.Session.GetFlag(flag + "_switch" + id)) { + // only that switch is activated, not the whole group. + activated = true; + + icon.Rate = 4f; + icon.Color = activeColor; + ease = 1f; + } + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + // look around for other touch switches that belong to the same group (same flag). + allTouchSwitchesInRoom = Scene.Tracker.GetEntities() + .FindAll(touchSwitch => (touchSwitch as FlagTouchSwitch)?.flag == flag).OfType().ToList(); + } + + private void onPlayer(Player player) { + turnOn(); + } + + private void onHoldable(Holdable h) { + turnOn(); + } + + private void onSeeker(Seeker seeker) { + if (SceneAs().InsideCamera(Position, 10f)) { + turnOn(); + } + } + + private void turnOn() { + if (!activated) { + touchSfx.Play("event:/game/general/touchswitch_any"); + + activated = true; + + // animation + wiggler.Start(); + for (int i = 0; i < 32; i++) { + float num = Calc.Random.NextFloat((float) Math.PI * 2f); + level.Particles.Emit(TouchSwitch.P_FireWhite, Position + Calc.AngleToVector(num, 6f), num); + } + icon.Rate = 4f; + + if (persistent) { + // this switch is persistent. save its activation in the session. + level.Session.SetFlag(flag + "_switch" + id, true); + } + + if ((SpringCollab2020MapDataProcessor.FlagTouchSwitches.Count <= level.Session.Area.ID + || SpringCollab2020MapDataProcessor.FlagTouchSwitches[level.Session.Area.ID][(int) level.Session.Area.Mode][flag] + .All(touchSwitchID => touchSwitchID.Level == level.Session.Level || level.Session.GetFlag(flag + "_switch" + touchSwitchID.ID))) + && allTouchSwitchesInRoom.All(touchSwitch => touchSwitch.activated)) { + + // all switches in the room are enabled, and all session flags for switches outside the room are enabled. + // so, the group is complete. + + foreach (FlagTouchSwitch touchSwitch in allTouchSwitchesInRoom) { + touchSwitch.finish(); + } + + SoundEmitter.Play("event:/game/general/touchswitch_last_oneshot"); + Add(new SoundSource("event:/game/general/touchswitch_last_cutoff")); + + // trigger associated switch gate(s). + foreach (FlagSwitchGate switchGate in Scene.Tracker.GetEntities().OfType()) { + if (switchGate.Flag == flag) { + switchGate.Trigger(); + } + } + + // if all the switches are persistent, the flag it's setting is persistent. + if (allTouchSwitchesInRoom.All(touchSwitch => touchSwitch.persistent)) { + level.Session.SetFlag(flag, true); + } + } + } + } + + private void finish() { + finished = true; + ease = 0f; + } + + public override void Update() { + timer += Engine.DeltaTime * 8f; + ease = Calc.Approach(ease, (finished || activated) ? 1f : 0f, Engine.DeltaTime * 2f); + + icon.Color = Color.Lerp(inactiveColor, finished ? finishColor : activeColor, ease); + icon.Color *= 0.5f + ((float) Math.Sin(timer) + 1f) / 2f * (1f - ease) * 0.5f + 0.5f * ease; + + bloom.Alpha = ease; + if (finished) { + if (icon.Rate > 0.1f) { + icon.Rate -= 2f * Engine.DeltaTime; + if (icon.Rate <= 0.1f) { + icon.Rate = 0.1f; + wiggler.Start(); + icon.Play("idle"); + level.Displacement.AddBurst(Position, 0.6f, 4f, 28f, 0.2f); + } + } else if (Scene.OnInterval(0.03f)) { + Vector2 position = Position + new Vector2(0f, 1f) + Calc.AngleToVector(Calc.Random.NextAngle(), 5f); + level.ParticlesBG.Emit(P_RecoloredFire, position); + } + } + + base.Update(); + } + + public override void Render() { + border.DrawCentered(Position + new Vector2(0f, -1f), Color.Black); + border.DrawCentered(Position, icon.Color, pulse); + base.Render(); + } + } +} diff --git a/Entities/FloatierSpaceBlock.cs b/Entities/FloatierSpaceBlock.cs index 8750007..e8bc529 100644 --- a/Entities/FloatierSpaceBlock.cs +++ b/Entities/FloatierSpaceBlock.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Reflection; using Celeste.Mod.Entities; @@ -19,7 +19,7 @@ public class FloatierSpaceBlock : FloatySpaceBlock { private static FieldInfo dashDirectionInfo = typeof(FloatySpaceBlock).GetField("dashDirection", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField); private static PropertyInfo MasterOfGroupInfo = typeof(FloatySpaceBlock).GetProperty("MasterOfGroup", BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); - + public FloatierSpaceBlock(EntityData data, Vector2 offset) : base(data, offset) { floatinessBoost = data.Float("floatinessMultiplier", 1); dashEaseMultiplier = data.Float("bounceBackMultiplier", 1); diff --git a/Entities/ForegroundReflectionTentacles.cs b/Entities/ForegroundReflectionTentacles.cs index b276d27..082efc7 100644 --- a/Entities/ForegroundReflectionTentacles.cs +++ b/Entities/ForegroundReflectionTentacles.cs @@ -1,38 +1,38 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/ForegroundReflectionTentacles")] - [TrackedAs(typeof(ReflectionTentacles))] - class ForegroundReflectionTentacles : ReflectionTentacles { - public ForegroundReflectionTentacles() : base() { } - public ForegroundReflectionTentacles(EntityData data, Vector2 offset) : base(data, offset) { } - - public override void Added(Scene scene) { - // turn off createdFromLevel to prevent vanilla from spawning ReflectionTentacles. - DynData self = new DynData(this); - bool createdFromLevel = self.Get("createdFromLevel"); - self["createdFromLevel"] = false; - - // run vanilla code. - base.Added(scene); - - // restore the createdFromLevel value. - self["createdFromLevel"] = createdFromLevel; - - // add tentacles like vanilla would, but make them ForegroundReflectionTentacles. - if (createdFromLevel) { - for (int i = 1; i < 4; i++) { - ForegroundReflectionTentacles reflectionTentacles = new ForegroundReflectionTentacles(); - reflectionTentacles.Create(self.Get("fearDistance"), self.Get("slideUntilIndex"), i, Nodes); - scene.Add(reflectionTentacles); - } - } - - // bring all tentacles to the foreground. - Depth = -1000000 + self.Get("layer"); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/ForegroundReflectionTentacles")] + [TrackedAs(typeof(ReflectionTentacles))] + class ForegroundReflectionTentacles : ReflectionTentacles { + public ForegroundReflectionTentacles() : base() { } + public ForegroundReflectionTentacles(EntityData data, Vector2 offset) : base(data, offset) { } + + public override void Added(Scene scene) { + // turn off createdFromLevel to prevent vanilla from spawning ReflectionTentacles. + DynData self = new DynData(this); + bool createdFromLevel = self.Get("createdFromLevel"); + self["createdFromLevel"] = false; + + // run vanilla code. + base.Added(scene); + + // restore the createdFromLevel value. + self["createdFromLevel"] = createdFromLevel; + + // add tentacles like vanilla would, but make them ForegroundReflectionTentacles. + if (createdFromLevel) { + for (int i = 1; i < 4; i++) { + ForegroundReflectionTentacles reflectionTentacles = new ForegroundReflectionTentacles(); + reflectionTentacles.Create(self.Get("fearDistance"), self.Get("slideUntilIndex"), i, Nodes); + scene.Add(reflectionTentacles); + } + } + + // bring all tentacles to the foreground. + Depth = -1000000 + self.Get("layer"); + } + } +} diff --git a/Entities/GlassBerry.cs b/Entities/GlassBerry.cs index bfdd95e..a6c1012 100644 --- a/Entities/GlassBerry.cs +++ b/Entities/GlassBerry.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using Microsoft.Xna.Framework; @@ -8,7 +8,7 @@ /* * Glass Berry (Spring Collab 2020) * https://github.com/EverestAPI/SpringCollab2020/ - * + * * A custom Strawberry which can be collected normally. * It will break and return to its home location if the player dashes while carrying it. */ @@ -21,7 +21,7 @@ namespace Celeste.Mod.SpringCollab2020.Entities { [RegisterStrawberry(true, false)] [CustomEntity("SpringCollab2020/glassBerry")] class GlassBerry : Entity, IStrawberry, IStrawberrySeeded { - // Requested implementations for using IStrawberrySeeded. + // Requested implementations for using IStrawberrySeeded. // These are the most basic implementations thereof and there is likely no reason to change these. public List Seeds { get; } public string gotSeedFlag => "collected_seeds_of_" + ID.ToString(); diff --git a/Entities/GroupedTriggerSpikes.cs b/Entities/GroupedTriggerSpikes.cs index 4e04bb5..291900a 100644 --- a/Entities/GroupedTriggerSpikes.cs +++ b/Entities/GroupedTriggerSpikes.cs @@ -1,319 +1,319 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System; -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// TriggerSpikes that all come out when leaving the group. - /// - [CustomEntity( - "SpringCollab2020/GroupedTriggerSpikesUp = LoadUp", - "SpringCollab2020/GroupedTriggerSpikesDown = LoadDown", - "SpringCollab2020/GroupedTriggerSpikesLeft = LoadLeft", - "SpringCollab2020/GroupedTriggerSpikesRight = LoadRight" - )] - public class GroupedTriggerSpikes : Entity { - - public static Entity LoadUp(Level level, LevelData levelData, Vector2 offset, EntityData entityData) - => new GroupedTriggerSpikes(entityData, offset, Directions.Up); - public static Entity LoadDown(Level level, LevelData levelData, Vector2 offset, EntityData entityData) - => new GroupedTriggerSpikes(entityData, offset, Directions.Down); - public static Entity LoadLeft(Level level, LevelData levelData, Vector2 offset, EntityData entityData) - => new GroupedTriggerSpikes(entityData, offset, Directions.Left); - public static Entity LoadRight(Level level, LevelData levelData, Vector2 offset, EntityData entityData) - => new GroupedTriggerSpikes(entityData, offset, Directions.Right); - - private const float DelayTime = 0.4f; - - public bool Triggered = false; - public float DelayTimer; - public float Lerp; - - private int size; - private Directions direction; - private string overrideType; - - private Vector2 outwards; - - private Vector2 shakeOffset; - - private string spikeType; - - private Vector2[] spikePositions; - private List spikeTextures; - - private bool blockingLedge = false; - - public GroupedTriggerSpikes(EntityData data, Vector2 offset, Directions dir) - : this(data.Position + offset, GetSize(data, dir), dir, data.Attr("type", "default"), data.Bool("behindMoveBlocks", false)) { - } - - public GroupedTriggerSpikes(Vector2 position, int size, Directions direction, string overrideType, bool behindMoveBlocks) - : base(position) { - - this.size = size; - this.direction = direction; - this.overrideType = overrideType; - - switch (direction) { - case Directions.Up: - outwards = new Vector2(0f, -1f); - Collider = new Hitbox(size, 3f, 0f, -3f); - Add(new SafeGroundBlocker()); - Add(new LedgeBlocker(UpSafeBlockCheck)); - break; - - case Directions.Down: - outwards = new Vector2(0f, 1f); - Collider = new Hitbox(size, 3f, 0f, 0f); - break; - - case Directions.Left: - outwards = new Vector2(-1f, 0f); - Collider = new Hitbox(3f, size, -3f, 0f); - - Add(new SafeGroundBlocker()); - Add(new LedgeBlocker(SideSafeBlockCheck)); - break; - - case Directions.Right: - outwards = new Vector2(1f, 0f); - Collider = new Hitbox(3f, size, 0f, 0f); - - Add(new SafeGroundBlocker()); - Add(new LedgeBlocker(SideSafeBlockCheck)); - break; - } - - Add(new PlayerCollider(OnCollide)); - - Add(new StaticMover { - OnShake = OnShake, - SolidChecker = IsRiding, - JumpThruChecker = IsRiding - }); - - if (behindMoveBlocks) { - Depth = 0; // move blocks have Depth = -1. - } else { - Depth = -50; - } - } - - public override void Added(Scene scene) { - base.Added(scene); - - AreaData areaData = AreaData.Get(scene); - spikeType = areaData.Spike; - if (!string.IsNullOrEmpty(overrideType) && overrideType != "default") { - spikeType = overrideType; - } - - string dir = direction.ToString().ToLower(); - - if (spikeType == "tentacles") { - throw new NotSupportedException("Trigger tentacles currently not supported"); - } - - spikePositions = new Vector2[size / 8]; - spikeTextures = GFX.Game.GetAtlasSubtextures("danger/spikes/" + spikeType + "_" + dir); - for (int i = 0; i < spikePositions.Length; i++) { - switch (direction) { - case Directions.Up: - spikePositions[i] = Vector2.UnitX * (i + 0.5f) * 8f + Vector2.UnitY; - break; - - case Directions.Down: - spikePositions[i] = Vector2.UnitX * (i + 0.5f) * 8f - Vector2.UnitY; - break; - - case Directions.Left: - spikePositions[i] = Vector2.UnitY * (i + 0.5f) * 8f + Vector2.UnitX; - break; - - case Directions.Right: - spikePositions[i] = Vector2.UnitY * (i + 0.5f) * 8f - Vector2.UnitX; - break; - } - } - } - - private void OnShake(Vector2 amount) { - shakeOffset += amount; - } - - private bool UpSafeBlockCheck(Player player) { - int dir = 8 * (int) player.Facing; - int left = (int) ((player.Left + dir - Left) / 4f); - int right = (int) ((player.Right + dir - Left) / 4f); - - if (right < 0 || left >= spikePositions.Length) { - return false; - } - - return Lerp >= 1f; - } - - private bool SideSafeBlockCheck(Player player) { - int top = (int) ((player.Top - Top) / 4f); - int bottom = (int) ((player.Bottom - Top) / 4f); - - if (bottom < 0 || top >= spikePositions.Length) - return false; - - return Lerp >= 1f; - } - - private void OnCollide(Player player) { - GetPlayerCollideIndex(player, out int minIndex, out int maxIndex); - if (minIndex < 0 || minIndex >= spikePositions.Length) { - return; - } - - if (!Triggered) { - Audio.Play("event:/game/03_resort/fluff_tendril_touch", Position + spikePositions[minIndex]); - Triggered = true; - DelayTimer = DelayTime; - } else if (Lerp >= 1f) { - player.Die(outwards); - } - } - - private void GetPlayerCollideIndex(Player player, out int minIndex, out int maxIndex) { - minIndex = maxIndex = -1; - - switch (direction) { - case Directions.Up: - if (player.Speed.Y >= 0f) { - minIndex = (int) ((player.Left - Left) / 8f); - maxIndex = (int) ((player.Right - Left) / 8f); - } - break; - - case Directions.Down: - if (player.Speed.Y <= 0f) { - minIndex = (int) ((player.Left - Left) / 8f); - maxIndex = (int) ((player.Right - Left) / 8f); - } - break; - - case Directions.Left: - if (player.Speed.X >= 0f) { - minIndex = (int) ((player.Top - Top) / 8f); - maxIndex = (int) ((player.Bottom - Top) / 8f); - } - break; - - case Directions.Right: - if (player.Speed.X <= 0f) { - minIndex = (int) ((player.Top - Top) / 8f); - maxIndex = (int) ((player.Bottom - Top) / 8f); - } - break; - } - } - - private static int GetSize(EntityData data, Directions dir) { - return - dir > Directions.Down ? - data.Height : - data.Width; - } - - public override void Update() { - base.Update(); - - if (Triggered) { - if (DelayTimer > 0f) { - DelayTimer -= Engine.DeltaTime; - if (DelayTimer <= 0f) { - if (CollideCheck()) { - DelayTimer = 0.05f; - } else { - Audio.Play("event:/game/03_resort/fluff_tendril_emerge", Position + spikePositions[spikePositions.Length / 2]); - } - } - } else { - Lerp = Calc.Approach(Lerp, 1f, 8f * Engine.DeltaTime); - } - } else { - Lerp = Calc.Approach(Lerp, 0f, 4f * Engine.DeltaTime); - if (Lerp <= 0f) { - Triggered = false; - } - } - - // "climb hopping" should be blocked if trigger spikes are going to kill Madeline (Lerp >= 1). - // this is done by adding a LedgeBlocker component. - if (blockingLedge != (Lerp >= 1f)) { - blockingLedge = !blockingLedge; - - // add or remove the ledge blocker depending on the need. - if (blockingLedge) { - Add(new LedgeBlocker()); - } else { - foreach (Component component in this) { - if (component is LedgeBlocker) { - Remove(component); - break; - } - } - } - } - } - - public override void Render() { - base.Render(); - - Vector2 justify = Vector2.One * 0.5f; - switch (direction) { - case Directions.Up: - justify = new Vector2(0.5f, 1f); - break; - case Directions.Down: - justify = new Vector2(0.5f, 0f); - break; - case Directions.Left: - justify = new Vector2(1f, 0.5f); - break; - case Directions.Right: - justify = new Vector2(0f, 0.5f); - break; - } - - for (int i = 0; i < spikePositions.Length; i++) { - MTexture tex = spikeTextures[0]; - Vector2 pos = Position + shakeOffset + spikePositions[i] + outwards * (-4f + Lerp * 4f); - tex.DrawJustified(pos, justify); - } - } - - private bool IsRiding(Solid solid) { - switch (direction) { - case Directions.Up: - return CollideCheckOutside(solid, Position + Vector2.UnitY); - case Directions.Down: - return CollideCheckOutside(solid, Position - Vector2.UnitY); - case Directions.Left: - return CollideCheckOutside(solid, Position + Vector2.UnitX); - case Directions.Right: - return CollideCheckOutside(solid, Position - Vector2.UnitX); - default: - return false; - } - } - - private bool IsRiding(JumpThru jumpThru) { - return direction == Directions.Up && CollideCheck(jumpThru, Position + Vector2.UnitY); - } - - public enum Directions { - Up, - Down, - Left, - Right - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System; +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// TriggerSpikes that all come out when leaving the group. + /// + [CustomEntity( + "SpringCollab2020/GroupedTriggerSpikesUp = LoadUp", + "SpringCollab2020/GroupedTriggerSpikesDown = LoadDown", + "SpringCollab2020/GroupedTriggerSpikesLeft = LoadLeft", + "SpringCollab2020/GroupedTriggerSpikesRight = LoadRight" + )] + public class GroupedTriggerSpikes : Entity { + + public static Entity LoadUp(Level level, LevelData levelData, Vector2 offset, EntityData entityData) + => new GroupedTriggerSpikes(entityData, offset, Directions.Up); + public static Entity LoadDown(Level level, LevelData levelData, Vector2 offset, EntityData entityData) + => new GroupedTriggerSpikes(entityData, offset, Directions.Down); + public static Entity LoadLeft(Level level, LevelData levelData, Vector2 offset, EntityData entityData) + => new GroupedTriggerSpikes(entityData, offset, Directions.Left); + public static Entity LoadRight(Level level, LevelData levelData, Vector2 offset, EntityData entityData) + => new GroupedTriggerSpikes(entityData, offset, Directions.Right); + + private const float DelayTime = 0.4f; + + public bool Triggered = false; + public float DelayTimer; + public float Lerp; + + private int size; + private Directions direction; + private string overrideType; + + private Vector2 outwards; + + private Vector2 shakeOffset; + + private string spikeType; + + private Vector2[] spikePositions; + private List spikeTextures; + + private bool blockingLedge = false; + + public GroupedTriggerSpikes(EntityData data, Vector2 offset, Directions dir) + : this(data.Position + offset, GetSize(data, dir), dir, data.Attr("type", "default"), data.Bool("behindMoveBlocks", false)) { + } + + public GroupedTriggerSpikes(Vector2 position, int size, Directions direction, string overrideType, bool behindMoveBlocks) + : base(position) { + + this.size = size; + this.direction = direction; + this.overrideType = overrideType; + + switch (direction) { + case Directions.Up: + outwards = new Vector2(0f, -1f); + Collider = new Hitbox(size, 3f, 0f, -3f); + Add(new SafeGroundBlocker()); + Add(new LedgeBlocker(UpSafeBlockCheck)); + break; + + case Directions.Down: + outwards = new Vector2(0f, 1f); + Collider = new Hitbox(size, 3f, 0f, 0f); + break; + + case Directions.Left: + outwards = new Vector2(-1f, 0f); + Collider = new Hitbox(3f, size, -3f, 0f); + + Add(new SafeGroundBlocker()); + Add(new LedgeBlocker(SideSafeBlockCheck)); + break; + + case Directions.Right: + outwards = new Vector2(1f, 0f); + Collider = new Hitbox(3f, size, 0f, 0f); + + Add(new SafeGroundBlocker()); + Add(new LedgeBlocker(SideSafeBlockCheck)); + break; + } + + Add(new PlayerCollider(OnCollide)); + + Add(new StaticMover { + OnShake = OnShake, + SolidChecker = IsRiding, + JumpThruChecker = IsRiding + }); + + if (behindMoveBlocks) { + Depth = 0; // move blocks have Depth = -1. + } else { + Depth = -50; + } + } + + public override void Added(Scene scene) { + base.Added(scene); + + AreaData areaData = AreaData.Get(scene); + spikeType = areaData.Spike; + if (!string.IsNullOrEmpty(overrideType) && overrideType != "default") { + spikeType = overrideType; + } + + string dir = direction.ToString().ToLower(); + + if (spikeType == "tentacles") { + throw new NotSupportedException("Trigger tentacles currently not supported"); + } + + spikePositions = new Vector2[size / 8]; + spikeTextures = GFX.Game.GetAtlasSubtextures("danger/spikes/" + spikeType + "_" + dir); + for (int i = 0; i < spikePositions.Length; i++) { + switch (direction) { + case Directions.Up: + spikePositions[i] = Vector2.UnitX * (i + 0.5f) * 8f + Vector2.UnitY; + break; + + case Directions.Down: + spikePositions[i] = Vector2.UnitX * (i + 0.5f) * 8f - Vector2.UnitY; + break; + + case Directions.Left: + spikePositions[i] = Vector2.UnitY * (i + 0.5f) * 8f + Vector2.UnitX; + break; + + case Directions.Right: + spikePositions[i] = Vector2.UnitY * (i + 0.5f) * 8f - Vector2.UnitX; + break; + } + } + } + + private void OnShake(Vector2 amount) { + shakeOffset += amount; + } + + private bool UpSafeBlockCheck(Player player) { + int dir = 8 * (int) player.Facing; + int left = (int) ((player.Left + dir - Left) / 4f); + int right = (int) ((player.Right + dir - Left) / 4f); + + if (right < 0 || left >= spikePositions.Length) { + return false; + } + + return Lerp >= 1f; + } + + private bool SideSafeBlockCheck(Player player) { + int top = (int) ((player.Top - Top) / 4f); + int bottom = (int) ((player.Bottom - Top) / 4f); + + if (bottom < 0 || top >= spikePositions.Length) + return false; + + return Lerp >= 1f; + } + + private void OnCollide(Player player) { + GetPlayerCollideIndex(player, out int minIndex, out int maxIndex); + if (minIndex < 0 || minIndex >= spikePositions.Length) { + return; + } + + if (!Triggered) { + Audio.Play("event:/game/03_resort/fluff_tendril_touch", Position + spikePositions[minIndex]); + Triggered = true; + DelayTimer = DelayTime; + } else if (Lerp >= 1f) { + player.Die(outwards); + } + } + + private void GetPlayerCollideIndex(Player player, out int minIndex, out int maxIndex) { + minIndex = maxIndex = -1; + + switch (direction) { + case Directions.Up: + if (player.Speed.Y >= 0f) { + minIndex = (int) ((player.Left - Left) / 8f); + maxIndex = (int) ((player.Right - Left) / 8f); + } + break; + + case Directions.Down: + if (player.Speed.Y <= 0f) { + minIndex = (int) ((player.Left - Left) / 8f); + maxIndex = (int) ((player.Right - Left) / 8f); + } + break; + + case Directions.Left: + if (player.Speed.X >= 0f) { + minIndex = (int) ((player.Top - Top) / 8f); + maxIndex = (int) ((player.Bottom - Top) / 8f); + } + break; + + case Directions.Right: + if (player.Speed.X <= 0f) { + minIndex = (int) ((player.Top - Top) / 8f); + maxIndex = (int) ((player.Bottom - Top) / 8f); + } + break; + } + } + + private static int GetSize(EntityData data, Directions dir) { + return + dir > Directions.Down ? + data.Height : + data.Width; + } + + public override void Update() { + base.Update(); + + if (Triggered) { + if (DelayTimer > 0f) { + DelayTimer -= Engine.DeltaTime; + if (DelayTimer <= 0f) { + if (CollideCheck()) { + DelayTimer = 0.05f; + } else { + Audio.Play("event:/game/03_resort/fluff_tendril_emerge", Position + spikePositions[spikePositions.Length / 2]); + } + } + } else { + Lerp = Calc.Approach(Lerp, 1f, 8f * Engine.DeltaTime); + } + } else { + Lerp = Calc.Approach(Lerp, 0f, 4f * Engine.DeltaTime); + if (Lerp <= 0f) { + Triggered = false; + } + } + + // "climb hopping" should be blocked if trigger spikes are going to kill Madeline (Lerp >= 1). + // this is done by adding a LedgeBlocker component. + if (blockingLedge != (Lerp >= 1f)) { + blockingLedge = !blockingLedge; + + // add or remove the ledge blocker depending on the need. + if (blockingLedge) { + Add(new LedgeBlocker()); + } else { + foreach (Component component in this) { + if (component is LedgeBlocker) { + Remove(component); + break; + } + } + } + } + } + + public override void Render() { + base.Render(); + + Vector2 justify = Vector2.One * 0.5f; + switch (direction) { + case Directions.Up: + justify = new Vector2(0.5f, 1f); + break; + case Directions.Down: + justify = new Vector2(0.5f, 0f); + break; + case Directions.Left: + justify = new Vector2(1f, 0.5f); + break; + case Directions.Right: + justify = new Vector2(0f, 0.5f); + break; + } + + for (int i = 0; i < spikePositions.Length; i++) { + MTexture tex = spikeTextures[0]; + Vector2 pos = Position + shakeOffset + spikePositions[i] + outwards * (-4f + Lerp * 4f); + tex.DrawJustified(pos, justify); + } + } + + private bool IsRiding(Solid solid) { + switch (direction) { + case Directions.Up: + return CollideCheckOutside(solid, Position + Vector2.UnitY); + case Directions.Down: + return CollideCheckOutside(solid, Position - Vector2.UnitY); + case Directions.Left: + return CollideCheckOutside(solid, Position + Vector2.UnitX); + case Directions.Right: + return CollideCheckOutside(solid, Position - Vector2.UnitX); + default: + return false; + } + } + + private bool IsRiding(JumpThru jumpThru) { + return direction == Directions.Up && CollideCheck(jumpThru, Position + Vector2.UnitY); + } + + public enum Directions { + Up, + Down, + Left, + Right + } + } +} diff --git a/Entities/HorizontalRoomWrapController.cs b/Entities/HorizontalRoomWrapController.cs index e4d7700..6f6b1ed 100644 --- a/Entities/HorizontalRoomWrapController.cs +++ b/Entities/HorizontalRoomWrapController.cs @@ -1,28 +1,28 @@ -using Celeste.Mod.Entities; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// A controller creating a horizontal room wrap effect in the room it is placed in. - /// Pulled straight from Celsius by 0x0ade. - /// - [CustomEntity("SpringCollab2020/HorizontalRoomWrapController")] - public class HorizontalRoomWrapController : Entity { - public override void Update() { - base.Update(); - - Camera camera = SceneAs().Camera; - Player player = Scene.Tracker.GetEntity(); - if (player != null) { - if (player.Left > camera.Right + 12f) { - // right -> left wrap - player.Right = camera.Left - 4f; - } else if (player.Right < camera.Left - 4f) { - // left -> right wrap - player.Left = camera.Right + 12f; - } - } - } - } - -} +using Celeste.Mod.Entities; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// A controller creating a horizontal room wrap effect in the room it is placed in. + /// Pulled straight from Celsius by 0x0ade. + /// + [CustomEntity("SpringCollab2020/HorizontalRoomWrapController")] + public class HorizontalRoomWrapController : Entity { + public override void Update() { + base.Update(); + + Camera camera = SceneAs().Camera; + Player player = Scene.Tracker.GetEntity(); + if (player != null) { + if (player.Left > camera.Right + 12f) { + // right -> left wrap + player.Right = camera.Left - 4f; + } else if (player.Right < camera.Left - 4f) { + // left -> right wrap + player.Left = camera.Right + 12f; + } + } + } + } + +} diff --git a/Entities/InstantFallingBlock.cs b/Entities/InstantFallingBlock.cs index 15d0daa..8e143c2 100644 --- a/Entities/InstantFallingBlock.cs +++ b/Entities/InstantFallingBlock.cs @@ -1,12 +1,12 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/InstantFallingBlock")] - class InstantFallingBlock : FallingBlock { - public InstantFallingBlock(EntityData data, Vector2 offset) : base(data, offset) { - // this block starts triggered right away. that's about it. - Triggered = true; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/InstantFallingBlock")] + class InstantFallingBlock : FallingBlock { + public InstantFallingBlock(EntityData data, Vector2 offset) : base(data, offset) { + // this block starts triggered right away. that's about it. + Triggered = true; + } + } +} diff --git a/Entities/InvisibleLightSource.cs b/Entities/InvisibleLightSource.cs index 943f40c..29f8f83 100644 --- a/Entities/InvisibleLightSource.cs +++ b/Entities/InvisibleLightSource.cs @@ -1,4 +1,4 @@ -using Monocle; +using Monocle; using Microsoft.Xna.Framework; using Celeste.Mod.Entities; using System.Reflection; @@ -36,7 +36,7 @@ public static Color GetColor(string color) { try { return Calc.HexToColor(color.Replace("#", "")); - } + } catch { Logger.Log("ColorHelper", "Failed to transform color " + color + ", returning Color.White"); } diff --git a/Entities/LightningDashSwitch.cs b/Entities/LightningDashSwitch.cs index f2fb227..5afe3b3 100644 --- a/Entities/LightningDashSwitch.cs +++ b/Entities/LightningDashSwitch.cs @@ -1,4 +1,4 @@ -using Celeste.Mod.Entities; +using Celeste.Mod.Entities; using Microsoft.Xna.Framework; using Monocle; using System; diff --git a/Entities/LitBlueTorch.cs b/Entities/LitBlueTorch.cs index 42f72e9..626d43c 100644 --- a/Entities/LitBlueTorch.cs +++ b/Entities/LitBlueTorch.cs @@ -1,18 +1,18 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/LitBlueTorch")] - class LitBlueTorch : Torch { - public LitBlueTorch(EntityData data, Vector2 offset, EntityID id) : base(data, offset, id) { } - - public override void Added(Scene scene) { - // by turning on startLit in Added but not in the constructor, we make the torch blue instead of yellow. - new DynData(this)["startLit"] = true; - - base.Added(scene); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/LitBlueTorch")] + class LitBlueTorch : Torch { + public LitBlueTorch(EntityData data, Vector2 offset, EntityID id) : base(data, offset, id) { } + + public override void Added(Scene scene) { + // by turning on startLit in Added but not in the constructor, we make the torch blue instead of yellow. + new DynData(this)["startLit"] = true; + + base.Added(scene); + } + } +} diff --git a/Entities/MoveBlockBarrier.cs b/Entities/MoveBlockBarrier.cs index 4e39dfb..b5c8e3c 100644 --- a/Entities/MoveBlockBarrier.cs +++ b/Entities/MoveBlockBarrier.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Reflection; using Celeste.Mod.Entities; @@ -11,7 +11,7 @@ namespace Celeste.Mod.SpringCollab2020.Entities { public class MoveBlockBarrier : SeekerBarrier { private static FieldInfo particlesInfo = typeof(SeekerBarrier).GetField("particles", BindingFlags.GetField | BindingFlags.Instance | BindingFlags.NonPublic); - + public MoveBlockBarrier(EntityData data, Vector2 offset) : base(data, offset) { } diff --git a/Entities/MoveBlockBarrierRenderer.cs b/Entities/MoveBlockBarrierRenderer.cs index 64a0383..57731fa 100644 --- a/Entities/MoveBlockBarrierRenderer.cs +++ b/Entities/MoveBlockBarrierRenderer.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Monocle; using System; using System.Collections.Generic; diff --git a/Entities/MoveBlockCustomSpeed.cs b/Entities/MoveBlockCustomSpeed.cs index 78ec27b..b3ded67 100644 --- a/Entities/MoveBlockCustomSpeed.cs +++ b/Entities/MoveBlockCustomSpeed.cs @@ -1,53 +1,53 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System.Collections; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/MoveBlockCustomSpeed")] - class MoveBlockCustomSpeed : MoveBlock { - private static MethodInfo moveBlockController = typeof(MoveBlock).GetMethod("Controller", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo moveBlockTargetSpeed = typeof(MoveBlock).GetField("targetSpeed", BindingFlags.NonPublic | BindingFlags.Instance); - - private float moveSpeed; - - public MoveBlockCustomSpeed(EntityData data, Vector2 offset) : base(data, offset) { - moveSpeed = data.Float("moveSpeed"); - - // remove the Controller coroutine. - foreach (Component component in this) { - if (component.GetType() == typeof(Coroutine)) { - Remove(component); - break; - } - } - - // replace it with our own "wrapped" coroutine. - Add(new Coroutine(controllerWrapper())); - } - - private IEnumerator controllerWrapper() { - IEnumerator controller = (IEnumerator) moveBlockController.Invoke(this, new object[0]); - - bool checkNext = false; - - while (controller.MoveNext()) { - // the target speed is set just AFTER a "yield return 0.2f". - // if we encounter it, we should check its value and change it if required on the next frame. - if (controller.Current != null && (float) controller.Current == 0.2f) { - checkNext = true; - } else if (checkNext) { - checkNext = false; - - // if the speed is 60 (block is moving), replace it with our custom speed. - if ((float) moveBlockTargetSpeed.GetValue(this) == 60f) { - moveBlockTargetSpeed.SetValue(this, moveSpeed); - } - } - - yield return controller.Current; - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System.Collections; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/MoveBlockCustomSpeed")] + class MoveBlockCustomSpeed : MoveBlock { + private static MethodInfo moveBlockController = typeof(MoveBlock).GetMethod("Controller", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo moveBlockTargetSpeed = typeof(MoveBlock).GetField("targetSpeed", BindingFlags.NonPublic | BindingFlags.Instance); + + private float moveSpeed; + + public MoveBlockCustomSpeed(EntityData data, Vector2 offset) : base(data, offset) { + moveSpeed = data.Float("moveSpeed"); + + // remove the Controller coroutine. + foreach (Component component in this) { + if (component.GetType() == typeof(Coroutine)) { + Remove(component); + break; + } + } + + // replace it with our own "wrapped" coroutine. + Add(new Coroutine(controllerWrapper())); + } + + private IEnumerator controllerWrapper() { + IEnumerator controller = (IEnumerator) moveBlockController.Invoke(this, new object[0]); + + bool checkNext = false; + + while (controller.MoveNext()) { + // the target speed is set just AFTER a "yield return 0.2f". + // if we encounter it, we should check its value and change it if required on the next frame. + if (controller.Current != null && (float) controller.Current == 0.2f) { + checkNext = true; + } else if (checkNext) { + checkNext = false; + + // if the speed is 60 (block is moving), replace it with our custom speed. + if ((float) moveBlockTargetSpeed.GetValue(this) == 60f) { + moveBlockTargetSpeed.SetValue(this, moveSpeed); + } + } + + yield return controller.Current; + } + } + } +} diff --git a/Entities/MultiNodeMovingPlatform.cs b/Entities/MultiNodeMovingPlatform.cs index f1c6deb..b260e06 100644 --- a/Entities/MultiNodeMovingPlatform.cs +++ b/Entities/MultiNodeMovingPlatform.cs @@ -1,243 +1,243 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/MultiNodeMovingPlatform")] - class MultiNodeMovingPlatform : JumpThru { - private enum Mode { - BackAndForth, BackAndForthNoPause, Loop, LoopNoPause, TeleportBack - } - - // settings - private Vector2[] nodes; - private float moveTime; - private float pauseTime; - private string overrideTexture; - private Mode mode; - private bool easing; - - private MTexture[] textures; - private float[] nodePercentages; - - private string lastSfx; - private SoundSource sfx; - - // status tracking - private float pauseTimer; - private int prevNodeIndex = 0; - private int nextNodeIndex = 1; - private float percent; - private int direction = 1; - - // sinking effect status tracking - private float addY; - private float sinkTimer; - - public MultiNodeMovingPlatform(EntityData data, Vector2 offset) - : base(data.Position + offset, data.Width, false) { - - // read attributes - moveTime = data.Float("moveTime", 2f); - pauseTime = data.Float("pauseTime"); - overrideTexture = data.Attr("texture", "default"); - mode = data.Enum("mode", Mode.Loop); - easing = data.Bool("easing", true); - - // read nodes - nodes = new Vector2[data.Nodes.Length + 1]; - nodes[0] = data.Position + offset; - for (int i = 0; i < data.Nodes.Length; i++) { - nodes[i + 1] = data.Nodes[i] + offset; - } - - // set up sounds and lighting - Add(sfx = new SoundSource()); - SurfaceSoundIndex = 5; - lastSfx = (Math.Sign(nodes[0].X - nodes[1].X) > 0 || Math.Sign(nodes[0].Y - nodes[1].Y) > 0) ? - "event:/game/03_resort/platform_horiz_left" : "event:/game/03_resort/platform_horiz_right"; - - Add(new LightOcclude(0.2f)); - - if (mode == Mode.BackAndForthNoPause || mode == Mode.LoopNoPause) { - nodePercentages = new float[mode == Mode.LoopNoPause ? nodes.Length : nodes.Length - 1]; - float totalDistance = 0; - - // compute the distance between each node. - for (int i = 0; i < nodes.Length - 1; i++) { - float distance = Vector2.Distance(nodes[i], nodes[i + 1]); - nodePercentages[i] = totalDistance + distance; - totalDistance += distance; - } - - // if looping, also compute the distance between the last node and the first one. - if (mode == Mode.LoopNoPause) { - float distance = Vector2.Distance(nodes[nodes.Length - 1], nodes[0]); - nodePercentages[nodes.Length - 1] = totalDistance + distance; - totalDistance += distance; - } - - // turn them into percentages. - for (int i = 0; i < nodePercentages.Length; i++) { - nodePercentages[i] /= totalDistance; - } - } - } - - public override void Added(Scene scene) { - base.Added(scene); - - // read the matching texture - if (string.IsNullOrEmpty(overrideTexture)) { - overrideTexture = AreaData.Get(scene).WoodPlatform; - } - MTexture platformTexture = GFX.Game["objects/woodPlatform/" + overrideTexture]; - textures = new MTexture[platformTexture.Width / 8]; - for (int i = 0; i < textures.Length; i++) { - textures[i] = platformTexture.GetSubtexture(i * 8, 0, 8, 8); - } - - // draw lines between all nodes - Vector2 lineOffset = new Vector2(Width, Height + 4f) / 2f; - scene.Add(new MovingPlatformLine(nodes[0] + lineOffset, nodes[1] + lineOffset)); - if (nodes.Length > 2) { - for (int i = 1; i < nodes.Length - 1; i++) { - scene.Add(new MovingPlatformLine(nodes[i] + lineOffset, nodes[i + 1] + lineOffset)); - } - - if (mode == Mode.Loop || mode == Mode.LoopNoPause) { - scene.Add(new MovingPlatformLine(nodes[nodes.Length - 1] + lineOffset, nodes[0] + lineOffset)); - } - } - } - - public override void Render() { - textures[0].Draw(Position); - for (int i = 8; i < Width - 8f; i += 8) { - textures[1].Draw(Position + new Vector2(i, 0f)); - } - textures[3].Draw(Position + new Vector2(Width - 8f, 0f)); - textures[2].Draw(Position + new Vector2(Width / 2f - 4f, 0f)); - } - - public override void OnStaticMoverTrigger(StaticMover sm) { - sinkTimer = 0.4f; - } - - public override void Update() { - base.Update(); - - // manage the "sinking" effect when the player is on the platform - if (HasPlayerRider()) { - sinkTimer = 0.2f; - addY = Calc.Approach(addY, 3f, 50f * Engine.DeltaTime); - } else if (sinkTimer > 0f) { - sinkTimer -= Engine.DeltaTime; - addY = Calc.Approach(addY, 3f, 50f * Engine.DeltaTime); - } else { - addY = Calc.Approach(addY, 0f, 20f * Engine.DeltaTime); - } - - if (pauseTimer > 0f) { - // the platform is currently paused at a node. - pauseTimer -= Engine.DeltaTime; - - // still update the position to be sure to apply addY. - MoveTo(nodes[prevNodeIndex] + new Vector2(0f, addY)); - return; - } else { - if (percent == 0) { - // the platform started moving. play sound - if (lastSfx == "event:/game/03_resort/platform_horiz_left") { - sfx.Play(lastSfx = "event:/game/03_resort/platform_horiz_right"); - } else { - sfx.Play(lastSfx = "event:/game/03_resort/platform_horiz_left"); - } - } - - // move forward... - percent = Calc.Approach(percent, 1f, Engine.DeltaTime / moveTime); - - if (mode == Mode.BackAndForthNoPause || mode == Mode.LoopNoPause) { - // NO PAUSE MODES: the "percentage" is the progress for the whole track, including all nodes. - - if (percent == 1f) { - // we reached the last node. - // pause, then start over. - percent = 0f; - pauseTimer = pauseTime; - - if (mode == Mode.BackAndForthNoPause) { - // the current node we're stopped at is either the last node, or the first one, depending on the direction. - prevNodeIndex = direction == 1 ? nodes.Length - 1 : 0; - - // go the other way round now. - direction = -direction; - } - - MoveTo(nodes[prevNodeIndex] + new Vector2(0f, addY)); - } else { - // OTHER MODES: the "percentage" is the progress between the current node and the next one. - - float easedPercentage = applyEase(direction == 1 ? percent : 1 - percent); - - // for example, if node percentages are 0.2 and 1, and easedPercentage is 0.6, nextNodeIndex = 1. - int nextNodeIndex = 0; - while (nodePercentages[nextNodeIndex] < easedPercentage) { - nextNodeIndex++; - } - - // in this case, previousNodePercentage = 0.2 and nextNodePercentage = 1. ClampedMap will remap 0.6 to 0.5 since this is halfway between 0.2 and 1. - float previousNodePercentage = nextNodeIndex == 0 ? 0 : nodePercentages[nextNodeIndex - 1]; - float nextNodePercentage = nodePercentages[nextNodeIndex]; - - MoveTo(Vector2.Lerp(nodes[nextNodeIndex], nodes[(nextNodeIndex + 1) % nodes.Length], - Calc.ClampedMap(easedPercentage, previousNodePercentage, nextNodePercentage)) + new Vector2(0f, addY)); - } - } else { - // lerp between the previous node and the next one. - MoveTo(Vector2.Lerp(nodes[prevNodeIndex], nodes[nextNodeIndex], applyEase(percent)) + new Vector2(0f, addY)); - - if (percent == 1f) { - // reached the end. start waiting before moving again, and switch the target to the next node. - prevNodeIndex = nextNodeIndex; - nextNodeIndex = prevNodeIndex + direction; - if (nextNodeIndex < 0) { - // done moving back, let's move forth again - nextNodeIndex = 1; - direction = 1; - } else if (nextNodeIndex >= nodes.Length) { - // reached the last node - if (mode == Mode.Loop) { - // go to the first node - nextNodeIndex = 0; - } else if (mode == Mode.TeleportBack) { - // go back to the first node instantly. - prevNodeIndex = 0; - nextNodeIndex = 1; - } else if (mode == Mode.BackAndForth) { - // start going back - nextNodeIndex -= 2; - direction = -1; - } - } - percent = 0; - pauseTimer = pauseTime; - } - } - } - } - - /// - /// Apply easing if the "easing" attribute is true, else don't modify the value. - /// - private float applyEase(float rawValue) { - if (easing) { - return Ease.SineInOut(rawValue); - } - - return rawValue; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/MultiNodeMovingPlatform")] + class MultiNodeMovingPlatform : JumpThru { + private enum Mode { + BackAndForth, BackAndForthNoPause, Loop, LoopNoPause, TeleportBack + } + + // settings + private Vector2[] nodes; + private float moveTime; + private float pauseTime; + private string overrideTexture; + private Mode mode; + private bool easing; + + private MTexture[] textures; + private float[] nodePercentages; + + private string lastSfx; + private SoundSource sfx; + + // status tracking + private float pauseTimer; + private int prevNodeIndex = 0; + private int nextNodeIndex = 1; + private float percent; + private int direction = 1; + + // sinking effect status tracking + private float addY; + private float sinkTimer; + + public MultiNodeMovingPlatform(EntityData data, Vector2 offset) + : base(data.Position + offset, data.Width, false) { + + // read attributes + moveTime = data.Float("moveTime", 2f); + pauseTime = data.Float("pauseTime"); + overrideTexture = data.Attr("texture", "default"); + mode = data.Enum("mode", Mode.Loop); + easing = data.Bool("easing", true); + + // read nodes + nodes = new Vector2[data.Nodes.Length + 1]; + nodes[0] = data.Position + offset; + for (int i = 0; i < data.Nodes.Length; i++) { + nodes[i + 1] = data.Nodes[i] + offset; + } + + // set up sounds and lighting + Add(sfx = new SoundSource()); + SurfaceSoundIndex = 5; + lastSfx = (Math.Sign(nodes[0].X - nodes[1].X) > 0 || Math.Sign(nodes[0].Y - nodes[1].Y) > 0) ? + "event:/game/03_resort/platform_horiz_left" : "event:/game/03_resort/platform_horiz_right"; + + Add(new LightOcclude(0.2f)); + + if (mode == Mode.BackAndForthNoPause || mode == Mode.LoopNoPause) { + nodePercentages = new float[mode == Mode.LoopNoPause ? nodes.Length : nodes.Length - 1]; + float totalDistance = 0; + + // compute the distance between each node. + for (int i = 0; i < nodes.Length - 1; i++) { + float distance = Vector2.Distance(nodes[i], nodes[i + 1]); + nodePercentages[i] = totalDistance + distance; + totalDistance += distance; + } + + // if looping, also compute the distance between the last node and the first one. + if (mode == Mode.LoopNoPause) { + float distance = Vector2.Distance(nodes[nodes.Length - 1], nodes[0]); + nodePercentages[nodes.Length - 1] = totalDistance + distance; + totalDistance += distance; + } + + // turn them into percentages. + for (int i = 0; i < nodePercentages.Length; i++) { + nodePercentages[i] /= totalDistance; + } + } + } + + public override void Added(Scene scene) { + base.Added(scene); + + // read the matching texture + if (string.IsNullOrEmpty(overrideTexture)) { + overrideTexture = AreaData.Get(scene).WoodPlatform; + } + MTexture platformTexture = GFX.Game["objects/woodPlatform/" + overrideTexture]; + textures = new MTexture[platformTexture.Width / 8]; + for (int i = 0; i < textures.Length; i++) { + textures[i] = platformTexture.GetSubtexture(i * 8, 0, 8, 8); + } + + // draw lines between all nodes + Vector2 lineOffset = new Vector2(Width, Height + 4f) / 2f; + scene.Add(new MovingPlatformLine(nodes[0] + lineOffset, nodes[1] + lineOffset)); + if (nodes.Length > 2) { + for (int i = 1; i < nodes.Length - 1; i++) { + scene.Add(new MovingPlatformLine(nodes[i] + lineOffset, nodes[i + 1] + lineOffset)); + } + + if (mode == Mode.Loop || mode == Mode.LoopNoPause) { + scene.Add(new MovingPlatformLine(nodes[nodes.Length - 1] + lineOffset, nodes[0] + lineOffset)); + } + } + } + + public override void Render() { + textures[0].Draw(Position); + for (int i = 8; i < Width - 8f; i += 8) { + textures[1].Draw(Position + new Vector2(i, 0f)); + } + textures[3].Draw(Position + new Vector2(Width - 8f, 0f)); + textures[2].Draw(Position + new Vector2(Width / 2f - 4f, 0f)); + } + + public override void OnStaticMoverTrigger(StaticMover sm) { + sinkTimer = 0.4f; + } + + public override void Update() { + base.Update(); + + // manage the "sinking" effect when the player is on the platform + if (HasPlayerRider()) { + sinkTimer = 0.2f; + addY = Calc.Approach(addY, 3f, 50f * Engine.DeltaTime); + } else if (sinkTimer > 0f) { + sinkTimer -= Engine.DeltaTime; + addY = Calc.Approach(addY, 3f, 50f * Engine.DeltaTime); + } else { + addY = Calc.Approach(addY, 0f, 20f * Engine.DeltaTime); + } + + if (pauseTimer > 0f) { + // the platform is currently paused at a node. + pauseTimer -= Engine.DeltaTime; + + // still update the position to be sure to apply addY. + MoveTo(nodes[prevNodeIndex] + new Vector2(0f, addY)); + return; + } else { + if (percent == 0) { + // the platform started moving. play sound + if (lastSfx == "event:/game/03_resort/platform_horiz_left") { + sfx.Play(lastSfx = "event:/game/03_resort/platform_horiz_right"); + } else { + sfx.Play(lastSfx = "event:/game/03_resort/platform_horiz_left"); + } + } + + // move forward... + percent = Calc.Approach(percent, 1f, Engine.DeltaTime / moveTime); + + if (mode == Mode.BackAndForthNoPause || mode == Mode.LoopNoPause) { + // NO PAUSE MODES: the "percentage" is the progress for the whole track, including all nodes. + + if (percent == 1f) { + // we reached the last node. + // pause, then start over. + percent = 0f; + pauseTimer = pauseTime; + + if (mode == Mode.BackAndForthNoPause) { + // the current node we're stopped at is either the last node, or the first one, depending on the direction. + prevNodeIndex = direction == 1 ? nodes.Length - 1 : 0; + + // go the other way round now. + direction = -direction; + } + + MoveTo(nodes[prevNodeIndex] + new Vector2(0f, addY)); + } else { + // OTHER MODES: the "percentage" is the progress between the current node and the next one. + + float easedPercentage = applyEase(direction == 1 ? percent : 1 - percent); + + // for example, if node percentages are 0.2 and 1, and easedPercentage is 0.6, nextNodeIndex = 1. + int nextNodeIndex = 0; + while (nodePercentages[nextNodeIndex] < easedPercentage) { + nextNodeIndex++; + } + + // in this case, previousNodePercentage = 0.2 and nextNodePercentage = 1. ClampedMap will remap 0.6 to 0.5 since this is halfway between 0.2 and 1. + float previousNodePercentage = nextNodeIndex == 0 ? 0 : nodePercentages[nextNodeIndex - 1]; + float nextNodePercentage = nodePercentages[nextNodeIndex]; + + MoveTo(Vector2.Lerp(nodes[nextNodeIndex], nodes[(nextNodeIndex + 1) % nodes.Length], + Calc.ClampedMap(easedPercentage, previousNodePercentage, nextNodePercentage)) + new Vector2(0f, addY)); + } + } else { + // lerp between the previous node and the next one. + MoveTo(Vector2.Lerp(nodes[prevNodeIndex], nodes[nextNodeIndex], applyEase(percent)) + new Vector2(0f, addY)); + + if (percent == 1f) { + // reached the end. start waiting before moving again, and switch the target to the next node. + prevNodeIndex = nextNodeIndex; + nextNodeIndex = prevNodeIndex + direction; + if (nextNodeIndex < 0) { + // done moving back, let's move forth again + nextNodeIndex = 1; + direction = 1; + } else if (nextNodeIndex >= nodes.Length) { + // reached the last node + if (mode == Mode.Loop) { + // go to the first node + nextNodeIndex = 0; + } else if (mode == Mode.TeleportBack) { + // go back to the first node instantly. + prevNodeIndex = 0; + nextNodeIndex = 1; + } else if (mode == Mode.BackAndForth) { + // start going back + nextNodeIndex -= 2; + direction = -1; + } + } + percent = 0; + pauseTimer = pauseTime; + } + } + } + } + + /// + /// Apply easing if the "easing" attribute is true, else don't modify the value. + /// + private float applyEase(float rawValue) { + if (easing) { + return Ease.SineInOut(rawValue); + } + + return rawValue; + } + } +} diff --git a/Entities/MultiRoomStrawberry.cs b/Entities/MultiRoomStrawberry.cs index acf2c93..8d03944 100644 --- a/Entities/MultiRoomStrawberry.cs +++ b/Entities/MultiRoomStrawberry.cs @@ -1,72 +1,72 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/MultiRoomStrawberry")] - [RegisterStrawberry(true, false)] - class MultiRoomStrawberry : Strawberry { - private int seedCount; - private EntityID id; - - public MultiRoomStrawberry(EntityData data, Vector2 offset, EntityID gid) : base(data, offset, gid) { - seedCount = data.Int("seedCount"); - id = gid; - } - - public override void Added(Scene scene) { - // trick the berry into thinking it has vanilla seeds, so that it doesn't appear right away - StrawberrySeed dummySeed = new StrawberrySeed(this, Vector2.Zero, 0, false); - Seeds = new List { dummySeed }; - - base.Added(scene); - - scene.Remove(dummySeed); - Seeds = null; - } - - public override void Update() { - base.Update(); - - if (WaitingOnSeeds) { - Player player = Scene.Tracker.GetEntity(); - if (player != null) { - // look at all the player followers, and filter all the seeds that match our berry. - List matchingSeeds = new List(); - foreach (Follower follower in player.Leader.Followers) { - if (follower.Entity is MultiRoomStrawberrySeed seed) { - if (seed.BerryID.Level == id.Level && seed.BerryID.ID == id.ID) { - matchingSeeds.Add(seed); - } - } - } - - if (matchingSeeds.Count >= seedCount) { - // all seeds have been gathered! associate the berry and the seeds, then trigger the cutscene. - Seeds = matchingSeeds; - foreach (StrawberrySeed seed in matchingSeeds) { - seed.Strawberry = this; - } - - // build the "seed merging" cutscene with a transition listener to prevent it from breaking the game if the player transitions out before time is frozen. - CutsceneEntity seedsCutscene = new CSGEN_StrawberrySeeds(this); - seedsCutscene.Add(new TransitionListener() { - OnOutBegin = () => SceneAs().SkipCutscene() - }); - Scene.Add(seedsCutscene); - - // also clean up the session, since the seeds are now gone. - List seedList = SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds; - for (int i = 0; i < seedList.Count; i++) { - if (seedList[i].BerryID.Level == id.Level && seedList[i].BerryID.ID == id.ID) { - seedList.RemoveAt(i); - i--; - } - } - } - } - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/MultiRoomStrawberry")] + [RegisterStrawberry(true, false)] + class MultiRoomStrawberry : Strawberry { + private int seedCount; + private EntityID id; + + public MultiRoomStrawberry(EntityData data, Vector2 offset, EntityID gid) : base(data, offset, gid) { + seedCount = data.Int("seedCount"); + id = gid; + } + + public override void Added(Scene scene) { + // trick the berry into thinking it has vanilla seeds, so that it doesn't appear right away + StrawberrySeed dummySeed = new StrawberrySeed(this, Vector2.Zero, 0, false); + Seeds = new List { dummySeed }; + + base.Added(scene); + + scene.Remove(dummySeed); + Seeds = null; + } + + public override void Update() { + base.Update(); + + if (WaitingOnSeeds) { + Player player = Scene.Tracker.GetEntity(); + if (player != null) { + // look at all the player followers, and filter all the seeds that match our berry. + List matchingSeeds = new List(); + foreach (Follower follower in player.Leader.Followers) { + if (follower.Entity is MultiRoomStrawberrySeed seed) { + if (seed.BerryID.Level == id.Level && seed.BerryID.ID == id.ID) { + matchingSeeds.Add(seed); + } + } + } + + if (matchingSeeds.Count >= seedCount) { + // all seeds have been gathered! associate the berry and the seeds, then trigger the cutscene. + Seeds = matchingSeeds; + foreach (StrawberrySeed seed in matchingSeeds) { + seed.Strawberry = this; + } + + // build the "seed merging" cutscene with a transition listener to prevent it from breaking the game if the player transitions out before time is frozen. + CutsceneEntity seedsCutscene = new CSGEN_StrawberrySeeds(this); + seedsCutscene.Add(new TransitionListener() { + OnOutBegin = () => SceneAs().SkipCutscene() + }); + Scene.Add(seedsCutscene); + + // also clean up the session, since the seeds are now gone. + List seedList = SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds; + for (int i = 0; i < seedList.Count; i++) { + if (seedList[i].BerryID.Level == id.Level && seedList[i].BerryID.ID == id.ID) { + seedList.RemoveAt(i); + i--; + } + } + } + } + } + } + } +} diff --git a/Entities/MultiRoomStrawberrySeed.cs b/Entities/MultiRoomStrawberrySeed.cs index 1009204..028213f 100644 --- a/Entities/MultiRoomStrawberrySeed.cs +++ b/Entities/MultiRoomStrawberrySeed.cs @@ -1,196 +1,196 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using Monocle; -using MonoMod.Utils; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/MultiRoomStrawberrySeed")] - [Tracked] - class MultiRoomStrawberrySeed : StrawberrySeed { - public static void Load() { - On.Celeste.Level.LoadLevel += onLoadLevel; - On.Celeste.LightingRenderer.BeforeRender += onLightingBeforeRender; - } - - public static void Unload() { - On.Celeste.Level.LoadLevel -= onLoadLevel; - On.Celeste.LightingRenderer.BeforeRender -= onLightingBeforeRender; - } - - private static void onLoadLevel(On.Celeste.Level.orig_LoadLevel orig, Level self, Player.IntroTypes playerIntro, bool isFromLoader) { - orig(self, playerIntro, isFromLoader); - - if (playerIntro != Player.IntroTypes.Transition) { - Player player = self.Tracker.GetEntity(); - - if (player != null) { - Vector2 seedPosition = player.Position; - - // we have to restore collected strawberry seeds. - foreach (SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo in SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds) { - seedPosition += new Vector2(-12 * (int) player.Facing, -8f); - - self.Add(new MultiRoomStrawberrySeed(player, seedPosition, sessionSeedInfo)); - } - } - } - } - - private static void onLightingBeforeRender(On.Celeste.LightingRenderer.orig_BeforeRender orig, LightingRenderer self, Scene scene) { - orig(self, scene); - - Draw.SpriteBatch.GraphicsDevice.SetRenderTarget(GameplayBuffers.Light); - Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, null, Matrix.Identity); - foreach (MultiRoomStrawberrySeed seed in scene.Tracker.GetEntities()) { - if (seed.cutoutTexture != null) { - Draw.SpriteBatch.Draw(seed.cutoutTexture.Texture.Texture, seed.Position + seed.spriteObject.Position - (scene as Level).Camera.Position - - new Vector2(seed.cutoutTexture.Width / 2, seed.cutoutTexture.Height / 2), Color.White); - } - } - Draw.SpriteBatch.End(); - } - - private DynData selfStrawberrySeed; - - private int index; - public EntityID BerryID; - - private float canLoseTimerMirror; - private Player player; - private bool spawnedAsFollower = false; - - private string sprite; - private bool ghost; - - private Sprite spriteObject; - private MTexture cutoutTexture; - - public MultiRoomStrawberrySeed(Vector2 position, int index, bool ghost, string sprite, string ghostSprite, bool ignoreLighting) : base(null, position, index, ghost) { - selfStrawberrySeed = new DynData(this); - - this.index = index; - this.ghost = ghost; - this.sprite = ghost ? ghostSprite : sprite; - - if (ignoreLighting) { - cutoutTexture = GFX.Game["collectables/" + sprite + "_cutout"]; - } - - foreach (Component component in this) { - if (component is PlayerCollider playerCollider) { - playerCollider.OnCollide = OnPlayer; - } - } - } - - public MultiRoomStrawberrySeed(EntityData data, Vector2 offset) : this(data.Position + offset, data.Int("index"), - SaveData.Instance.CheckStrawberry(new EntityID(data.Attr("berryLevel"), data.Int("berryID"))), - data.Attr("sprite", "strawberry/seed"), data.Attr("ghostSprite", "ghostberry/seed"), data.Bool("ignoreLighting")) { - - BerryID = new EntityID(data.Attr("berryLevel"), data.Int("berryID")); - } - - private MultiRoomStrawberrySeed(Player player, Vector2 position, SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo) - : this(position, sessionSeedInfo.Index, SaveData.Instance.CheckStrawberry(sessionSeedInfo.BerryID), sessionSeedInfo.Sprite, sessionSeedInfo.Sprite, sessionSeedInfo.IgnoreLighting) { - - BerryID = sessionSeedInfo.BerryID; - - // the seed is collected right away. - this.player = player; - spawnedAsFollower = true; - } - - public override void Added(Scene scene) { - base.Added(scene); - - if (!spawnedAsFollower) { - if (SceneAs().Session.GetFlag("collected_seeds_of_" + BerryID.ToString())) { - // if all seeds for this berry were already collected (the berry was already formed), commit remove self. - RemoveSelf(); - } else { - // if the seed already follows the player, commit remove self. - foreach (SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo in SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds) { - if (sessionSeedInfo.Index == index && sessionSeedInfo.BerryID.ID == BerryID.ID && sessionSeedInfo.BerryID.Level == BerryID.Level) { - RemoveSelf(); - break; - } - } - } - } - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - if ((ghost && sprite != "ghostberry/seed") || (!ghost && sprite != "strawberry/seed")) { - // the sprite is non-default. replace it. - Sprite vanillaSprite = selfStrawberrySeed.Get("sprite"); - - // build the new sprite. - MTexture frame0 = GFX.Game["collectables/" + sprite + "00"]; - MTexture frame1 = GFX.Game["collectables/" + sprite + "01"]; - - Sprite modSprite = new Sprite(GFX.Game, sprite); - modSprite.CenterOrigin(); - modSprite.Justify = new Vector2(0.5f, 0.5f); - modSprite.AddLoop("idle", 0.1f, new MTexture[] { - frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, - frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame1 - }); - modSprite.AddLoop("noFlash", 0.1f, new MTexture[] { frame0 }); - - // copy over the values from the vanilla sprite - modSprite.Position = vanillaSprite.Position; - modSprite.Color = vanillaSprite.Color; - modSprite.OnFrameChange = vanillaSprite.OnFrameChange; - modSprite.Play("idle"); - modSprite.SetAnimationFrame(vanillaSprite.CurrentAnimationFrame); - - // and replace it for good - Remove(vanillaSprite); - Add(modSprite); - selfStrawberrySeed["sprite"] = modSprite; - } - - if (spawnedAsFollower) { - player.Leader.GainFollower(selfStrawberrySeed.Get("follower")); - canLoseTimerMirror = 0.25f; - Collidable = false; - Depth = -1000000; - AddTag(Tags.Persistent); - } - - // get a reference to the sprite. this will be used to "cut out" the lighting renderer. - spriteObject = selfStrawberrySeed.Get("sprite"); - } - - private void OnPlayer(Player player) { - Audio.Play("event:/game/general/seed_touch", Position, "count", index); - player.Leader.GainFollower(selfStrawberrySeed.Get("follower")); - canLoseTimerMirror = 0.25f; - Collidable = false; - Depth = -1000000; - AddTag(Tags.Persistent); - - // Add the info for this berry seed to the session. - SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo = new SpringCollab2020Session.MultiRoomStrawberrySeedInfo(); - sessionSeedInfo.Index = index; - sessionSeedInfo.BerryID = BerryID; - sessionSeedInfo.Sprite = sprite; - sessionSeedInfo.IgnoreLighting = (cutoutTexture != null); - SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds.Add(sessionSeedInfo); - } - - public override void Update() { - base.Update(); - - // be sure the canLoseTimer always has a positive value. we don't want the player to lose this berry seed. - canLoseTimerMirror -= Engine.DeltaTime; - if (canLoseTimerMirror < 1f) { - canLoseTimerMirror = 1000f; - selfStrawberrySeed["canLoseTimer"] = 1000f; - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Monocle; +using MonoMod.Utils; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/MultiRoomStrawberrySeed")] + [Tracked] + class MultiRoomStrawberrySeed : StrawberrySeed { + public static void Load() { + On.Celeste.Level.LoadLevel += onLoadLevel; + On.Celeste.LightingRenderer.BeforeRender += onLightingBeforeRender; + } + + public static void Unload() { + On.Celeste.Level.LoadLevel -= onLoadLevel; + On.Celeste.LightingRenderer.BeforeRender -= onLightingBeforeRender; + } + + private static void onLoadLevel(On.Celeste.Level.orig_LoadLevel orig, Level self, Player.IntroTypes playerIntro, bool isFromLoader) { + orig(self, playerIntro, isFromLoader); + + if (playerIntro != Player.IntroTypes.Transition) { + Player player = self.Tracker.GetEntity(); + + if (player != null) { + Vector2 seedPosition = player.Position; + + // we have to restore collected strawberry seeds. + foreach (SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo in SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds) { + seedPosition += new Vector2(-12 * (int) player.Facing, -8f); + + self.Add(new MultiRoomStrawberrySeed(player, seedPosition, sessionSeedInfo)); + } + } + } + } + + private static void onLightingBeforeRender(On.Celeste.LightingRenderer.orig_BeforeRender orig, LightingRenderer self, Scene scene) { + orig(self, scene); + + Draw.SpriteBatch.GraphicsDevice.SetRenderTarget(GameplayBuffers.Light); + Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, null, Matrix.Identity); + foreach (MultiRoomStrawberrySeed seed in scene.Tracker.GetEntities()) { + if (seed.cutoutTexture != null) { + Draw.SpriteBatch.Draw(seed.cutoutTexture.Texture.Texture, seed.Position + seed.spriteObject.Position - (scene as Level).Camera.Position + - new Vector2(seed.cutoutTexture.Width / 2, seed.cutoutTexture.Height / 2), Color.White); + } + } + Draw.SpriteBatch.End(); + } + + private DynData selfStrawberrySeed; + + private int index; + public EntityID BerryID; + + private float canLoseTimerMirror; + private Player player; + private bool spawnedAsFollower = false; + + private string sprite; + private bool ghost; + + private Sprite spriteObject; + private MTexture cutoutTexture; + + public MultiRoomStrawberrySeed(Vector2 position, int index, bool ghost, string sprite, string ghostSprite, bool ignoreLighting) : base(null, position, index, ghost) { + selfStrawberrySeed = new DynData(this); + + this.index = index; + this.ghost = ghost; + this.sprite = ghost ? ghostSprite : sprite; + + if (ignoreLighting) { + cutoutTexture = GFX.Game["collectables/" + sprite + "_cutout"]; + } + + foreach (Component component in this) { + if (component is PlayerCollider playerCollider) { + playerCollider.OnCollide = OnPlayer; + } + } + } + + public MultiRoomStrawberrySeed(EntityData data, Vector2 offset) : this(data.Position + offset, data.Int("index"), + SaveData.Instance.CheckStrawberry(new EntityID(data.Attr("berryLevel"), data.Int("berryID"))), + data.Attr("sprite", "strawberry/seed"), data.Attr("ghostSprite", "ghostberry/seed"), data.Bool("ignoreLighting")) { + + BerryID = new EntityID(data.Attr("berryLevel"), data.Int("berryID")); + } + + private MultiRoomStrawberrySeed(Player player, Vector2 position, SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo) + : this(position, sessionSeedInfo.Index, SaveData.Instance.CheckStrawberry(sessionSeedInfo.BerryID), sessionSeedInfo.Sprite, sessionSeedInfo.Sprite, sessionSeedInfo.IgnoreLighting) { + + BerryID = sessionSeedInfo.BerryID; + + // the seed is collected right away. + this.player = player; + spawnedAsFollower = true; + } + + public override void Added(Scene scene) { + base.Added(scene); + + if (!spawnedAsFollower) { + if (SceneAs().Session.GetFlag("collected_seeds_of_" + BerryID.ToString())) { + // if all seeds for this berry were already collected (the berry was already formed), commit remove self. + RemoveSelf(); + } else { + // if the seed already follows the player, commit remove self. + foreach (SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo in SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds) { + if (sessionSeedInfo.Index == index && sessionSeedInfo.BerryID.ID == BerryID.ID && sessionSeedInfo.BerryID.Level == BerryID.Level) { + RemoveSelf(); + break; + } + } + } + } + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + if ((ghost && sprite != "ghostberry/seed") || (!ghost && sprite != "strawberry/seed")) { + // the sprite is non-default. replace it. + Sprite vanillaSprite = selfStrawberrySeed.Get("sprite"); + + // build the new sprite. + MTexture frame0 = GFX.Game["collectables/" + sprite + "00"]; + MTexture frame1 = GFX.Game["collectables/" + sprite + "01"]; + + Sprite modSprite = new Sprite(GFX.Game, sprite); + modSprite.CenterOrigin(); + modSprite.Justify = new Vector2(0.5f, 0.5f); + modSprite.AddLoop("idle", 0.1f, new MTexture[] { + frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, + frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame0, frame1 + }); + modSprite.AddLoop("noFlash", 0.1f, new MTexture[] { frame0 }); + + // copy over the values from the vanilla sprite + modSprite.Position = vanillaSprite.Position; + modSprite.Color = vanillaSprite.Color; + modSprite.OnFrameChange = vanillaSprite.OnFrameChange; + modSprite.Play("idle"); + modSprite.SetAnimationFrame(vanillaSprite.CurrentAnimationFrame); + + // and replace it for good + Remove(vanillaSprite); + Add(modSprite); + selfStrawberrySeed["sprite"] = modSprite; + } + + if (spawnedAsFollower) { + player.Leader.GainFollower(selfStrawberrySeed.Get("follower")); + canLoseTimerMirror = 0.25f; + Collidable = false; + Depth = -1000000; + AddTag(Tags.Persistent); + } + + // get a reference to the sprite. this will be used to "cut out" the lighting renderer. + spriteObject = selfStrawberrySeed.Get("sprite"); + } + + private void OnPlayer(Player player) { + Audio.Play("event:/game/general/seed_touch", Position, "count", index); + player.Leader.GainFollower(selfStrawberrySeed.Get("follower")); + canLoseTimerMirror = 0.25f; + Collidable = false; + Depth = -1000000; + AddTag(Tags.Persistent); + + // Add the info for this berry seed to the session. + SpringCollab2020Session.MultiRoomStrawberrySeedInfo sessionSeedInfo = new SpringCollab2020Session.MultiRoomStrawberrySeedInfo(); + sessionSeedInfo.Index = index; + sessionSeedInfo.BerryID = BerryID; + sessionSeedInfo.Sprite = sprite; + sessionSeedInfo.IgnoreLighting = (cutoutTexture != null); + SpringCollab2020Module.Instance.Session.CollectedMultiRoomStrawberrySeeds.Add(sessionSeedInfo); + } + + public override void Update() { + base.Update(); + + // be sure the canLoseTimer always has a positive value. we don't want the player to lose this berry seed. + canLoseTimerMirror -= Engine.DeltaTime; + if (canLoseTimerMirror < 1f) { + canLoseTimerMirror = 1000f; + selfStrawberrySeed["canLoseTimer"] = 1000f; + } + } + } +} diff --git a/Entities/NegativeSummitCheckpoint.cs b/Entities/NegativeSummitCheckpoint.cs index 7c6c3cf..345a726 100644 --- a/Entities/NegativeSummitCheckpoint.cs +++ b/Entities/NegativeSummitCheckpoint.cs @@ -1,22 +1,22 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using MonoMod.Utils; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/NegativeSummitCheckpoint")] - class NegativeSummitCheckpoint : SummitCheckpoint { - public NegativeSummitCheckpoint(EntityData data, Vector2 offset) : base(data, offset) { - // convert the checkpoint number to string. the minus sign is replaced by :, because this is the character after 9 in the chartable. - // so, : will render the number10.png sprite, which is a minus sign. - string numberString = Number.ToString().Replace("-", ":"); - - // add leading zeroes - while (numberString.Length < 2) { - numberString = $"0{numberString}"; - } - - // replace the vanilla number string, which will be rendered. - new DynData(this)["numberString"] = numberString; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using MonoMod.Utils; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/NegativeSummitCheckpoint")] + class NegativeSummitCheckpoint : SummitCheckpoint { + public NegativeSummitCheckpoint(EntityData data, Vector2 offset) : base(data, offset) { + // convert the checkpoint number to string. the minus sign is replaced by :, because this is the character after 9 in the chartable. + // so, : will render the number10.png sprite, which is a minus sign. + string numberString = Number.ToString().Replace("-", ":"); + + // add leading zeroes + while (numberString.Length < 2) { + numberString = $"0{numberString}"; + } + + // replace the vanilla number string, which will be rendered. + new DynData(this)["numberString"] = numberString; + } + } +} diff --git a/Entities/NoDashRefillSpring.cs b/Entities/NoDashRefillSpring.cs index 311efbf..09addbe 100644 --- a/Entities/NoDashRefillSpring.cs +++ b/Entities/NoDashRefillSpring.cs @@ -1,86 +1,86 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; -using System; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/NoDashRefillSpring", "SpringCollab2020/NoDashRefillSpringLeft", "SpringCollab2020/NoDashRefillSpringRight")] - class NoDashRefillSpring : Spring { - private static MethodInfo bounceAnimate = typeof(Spring).GetMethod("BounceAnimate", BindingFlags.NonPublic | BindingFlags.Instance); - private static object[] noParams = new object[0]; - - public NoDashRefillSpring(EntityData data, Vector2 offset) - : base(data.Position + offset, GetOrientationFromName(data.Name), data.Bool("playerCanUse", true)) { - - DynData selfSpring = new DynData(this); - - // remove the vanilla player collider. this is the one thing we want to mod here. - foreach (Component component in this) { - if (component.GetType() == typeof(PlayerCollider)) { - Remove(component); - break; - } - } - - // replace it with our own collider. - if (data.Bool("playerCanUse", true)) { - Add(new PlayerCollider(OnCollide)); - } - - // replace the vanilla sprite with our custom one. - Sprite sprite = selfSpring.Get("sprite"); - sprite.Reset(GFX.Game, "objects/SpringCollab2020/noDashRefillSpring/"); - sprite.Add("idle", "", 0f, default(int)); - sprite.Add("bounce", "", 0.07f, "idle", 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5); - sprite.Add("disabled", "white", 0.07f); - sprite.Play("idle"); - sprite.Origin.X = sprite.Width / 2f; - sprite.Origin.Y = sprite.Height; - } - - private static Orientations GetOrientationFromName(string name) { - switch (name) { - case "SpringCollab2020/NoDashRefillSpring": - return Orientations.Floor; - case "SpringCollab2020/NoDashRefillSpringRight": - return Orientations.WallRight; - case "SpringCollab2020/NoDashRefillSpringLeft": - return Orientations.WallLeft; - default: - throw new Exception("No Dash Refill Spring name doesn't correlate to a valid Orientation!"); - } - } - - - private void OnCollide(Player player) { - if (player.StateMachine.State == 9) { - return; - } - - // Save dash count. Dashes are reloaded by SideBounce and SuperBounce. - int originalDashCount = player.Dashes; - - if (Orientation == Orientations.Floor) { - if (player.Speed.Y >= 0f) { - bounceAnimate.Invoke(this, noParams); - player.SuperBounce(Top); - } - } else if (Orientation == Orientations.WallLeft) { - if (player.SideBounce(1, Right, CenterY)) { - bounceAnimate.Invoke(this, noParams); - } - } else if (Orientation == Orientations.WallRight) { - if (player.SideBounce(-1, Left, CenterY)) { - bounceAnimate.Invoke(this, noParams); - } - } else { - throw new Exception("Orientation not supported!"); - } - - // Restore original dash count. - player.Dashes = originalDashCount; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; +using System; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/NoDashRefillSpring", "SpringCollab2020/NoDashRefillSpringLeft", "SpringCollab2020/NoDashRefillSpringRight")] + class NoDashRefillSpring : Spring { + private static MethodInfo bounceAnimate = typeof(Spring).GetMethod("BounceAnimate", BindingFlags.NonPublic | BindingFlags.Instance); + private static object[] noParams = new object[0]; + + public NoDashRefillSpring(EntityData data, Vector2 offset) + : base(data.Position + offset, GetOrientationFromName(data.Name), data.Bool("playerCanUse", true)) { + + DynData selfSpring = new DynData(this); + + // remove the vanilla player collider. this is the one thing we want to mod here. + foreach (Component component in this) { + if (component.GetType() == typeof(PlayerCollider)) { + Remove(component); + break; + } + } + + // replace it with our own collider. + if (data.Bool("playerCanUse", true)) { + Add(new PlayerCollider(OnCollide)); + } + + // replace the vanilla sprite with our custom one. + Sprite sprite = selfSpring.Get("sprite"); + sprite.Reset(GFX.Game, "objects/SpringCollab2020/noDashRefillSpring/"); + sprite.Add("idle", "", 0f, default(int)); + sprite.Add("bounce", "", 0.07f, "idle", 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5); + sprite.Add("disabled", "white", 0.07f); + sprite.Play("idle"); + sprite.Origin.X = sprite.Width / 2f; + sprite.Origin.Y = sprite.Height; + } + + private static Orientations GetOrientationFromName(string name) { + switch (name) { + case "SpringCollab2020/NoDashRefillSpring": + return Orientations.Floor; + case "SpringCollab2020/NoDashRefillSpringRight": + return Orientations.WallRight; + case "SpringCollab2020/NoDashRefillSpringLeft": + return Orientations.WallLeft; + default: + throw new Exception("No Dash Refill Spring name doesn't correlate to a valid Orientation!"); + } + } + + + private void OnCollide(Player player) { + if (player.StateMachine.State == 9) { + return; + } + + // Save dash count. Dashes are reloaded by SideBounce and SuperBounce. + int originalDashCount = player.Dashes; + + if (Orientation == Orientations.Floor) { + if (player.Speed.Y >= 0f) { + bounceAnimate.Invoke(this, noParams); + player.SuperBounce(Top); + } + } else if (Orientation == Orientations.WallLeft) { + if (player.SideBounce(1, Right, CenterY)) { + bounceAnimate.Invoke(this, noParams); + } + } else if (Orientation == Orientations.WallRight) { + if (player.SideBounce(-1, Left, CenterY)) { + bounceAnimate.Invoke(this, noParams); + } + } else { + throw new Exception("Orientation not supported!"); + } + + // Restore original dash count. + player.Dashes = originalDashCount; + } + } +} diff --git a/Entities/NonBadelineMovingBlock.cs b/Entities/NonBadelineMovingBlock.cs index 2377df5..94a5ac4 100644 --- a/Entities/NonBadelineMovingBlock.cs +++ b/Entities/NonBadelineMovingBlock.cs @@ -1,14 +1,14 @@ -using Celeste.Mod.Entities; +using Celeste.Mod.Entities; using Microsoft.Xna.Framework; -using Monocle; +using Monocle; using System; using System.Collections; -namespace Celeste.Mod.SpringCollab2020.Entities { +namespace Celeste.Mod.SpringCollab2020.Entities { [CustomEntity("SpringCollab2020/nonBadelineMovingBlock")] [Tracked(false)] public class NonBadelineMovingBlock : Solid { - + private float startDelay; private int nodeIndex; @@ -42,10 +42,10 @@ public NonBadelineMovingBlock(Vector2[] nodes, float width, float height, char t public NonBadelineMovingBlock(EntityData data, Vector2 offset) : this(data.NodesWithPosition(offset), data.Width, data.Height, data.Char("tiletype", 'g'), data.Char("highlightTiletype", 'G')) { - } - + } + public override void Added(Scene scene) { - base.Added(scene); + base.Added(scene); StartMoving(0); } diff --git a/Entities/NonCoreModeWallBooster.cs b/Entities/NonCoreModeWallBooster.cs index 23ecee9..0b834a4 100755 --- a/Entities/NonCoreModeWallBooster.cs +++ b/Entities/NonCoreModeWallBooster.cs @@ -1,13 +1,13 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/NonCoreModeWallBooster")] - [TrackedAs(typeof(WallBooster))] - class NonCoreModeWallBooster : WallBooster { - public NonCoreModeWallBooster(EntityData data, Vector2 offset) : base(data, offset) { - Remove(Get()); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/NonCoreModeWallBooster")] + [TrackedAs(typeof(WallBooster))] + class NonCoreModeWallBooster : WallBooster { + public NonCoreModeWallBooster(EntityData data, Vector2 offset) : base(data, offset) { + Remove(Get()); + } + } +} diff --git a/Entities/RainbowSpinnerColorAreaController.cs b/Entities/RainbowSpinnerColorAreaController.cs index f3e9f35..c0b50da 100644 --- a/Entities/RainbowSpinnerColorAreaController.cs +++ b/Entities/RainbowSpinnerColorAreaController.cs @@ -1,70 +1,70 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/RainbowSpinnerColorAreaController")] - [Tracked] - class RainbowSpinnerColorAreaController : Entity { - private static bool rainbowSpinnerHueHooked = false; - - // the parameters for this spinner controller. - private Color[] colors; - private float gradientSize; - - public RainbowSpinnerColorAreaController(EntityData data, Vector2 offset) : base(data.Position + offset) { - // convert the color list to Color objects - string[] colorsAsStrings = data.Attr("colors", "89E5AE,88E0E0,87A9DD,9887DB,D088E2").Split(','); - colors = new Color[colorsAsStrings.Length]; - for (int i = 0; i < colors.Length; i++) { - colors[i] = Calc.HexToColor(colorsAsStrings[i]); - } - - gradientSize = data.Float("gradientSize", 280); - - // make this controller collidable. - Collider = new Hitbox(data.Width, data.Height); - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - // enable the hook on rainbow spinner hue. - if (!rainbowSpinnerHueHooked) { - On.Celeste.CrystalStaticSpinner.GetHue += getRainbowSpinnerHue; - rainbowSpinnerHueHooked = true; - } - } - - public override void Removed(Scene scene) { - base.Removed(scene); - - // if this controller was the last in the scene, disable the hook on rainbow spinner hue. - if (rainbowSpinnerHueHooked && scene.Tracker.CountEntities() <= 1) { - On.Celeste.CrystalStaticSpinner.GetHue -= getRainbowSpinnerHue; - rainbowSpinnerHueHooked = false; - } - } - - public override void SceneEnd(Scene scene) { - base.SceneEnd(scene); - - // leaving level; disable the hook on rainbow spinner hue. - if (rainbowSpinnerHueHooked) { - On.Celeste.CrystalStaticSpinner.GetHue -= getRainbowSpinnerHue; - rainbowSpinnerHueHooked = false; - }; - } - - private static Color getRainbowSpinnerHue(On.Celeste.CrystalStaticSpinner.orig_GetHue orig, CrystalStaticSpinner self, Vector2 position) { - RainbowSpinnerColorAreaController controller = self.CollideFirst(); - if (controller != null) { - // apply the color from the controller we are in. - return RainbowSpinnerColorController.getModHue(controller.colors, controller.gradientSize, self.Scene, position); - } else { - // we are not in a controller; apply the vanilla color. - return orig(self, position); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/RainbowSpinnerColorAreaController")] + [Tracked] + class RainbowSpinnerColorAreaController : Entity { + private static bool rainbowSpinnerHueHooked = false; + + // the parameters for this spinner controller. + private Color[] colors; + private float gradientSize; + + public RainbowSpinnerColorAreaController(EntityData data, Vector2 offset) : base(data.Position + offset) { + // convert the color list to Color objects + string[] colorsAsStrings = data.Attr("colors", "89E5AE,88E0E0,87A9DD,9887DB,D088E2").Split(','); + colors = new Color[colorsAsStrings.Length]; + for (int i = 0; i < colors.Length; i++) { + colors[i] = Calc.HexToColor(colorsAsStrings[i]); + } + + gradientSize = data.Float("gradientSize", 280); + + // make this controller collidable. + Collider = new Hitbox(data.Width, data.Height); + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + // enable the hook on rainbow spinner hue. + if (!rainbowSpinnerHueHooked) { + On.Celeste.CrystalStaticSpinner.GetHue += getRainbowSpinnerHue; + rainbowSpinnerHueHooked = true; + } + } + + public override void Removed(Scene scene) { + base.Removed(scene); + + // if this controller was the last in the scene, disable the hook on rainbow spinner hue. + if (rainbowSpinnerHueHooked && scene.Tracker.CountEntities() <= 1) { + On.Celeste.CrystalStaticSpinner.GetHue -= getRainbowSpinnerHue; + rainbowSpinnerHueHooked = false; + } + } + + public override void SceneEnd(Scene scene) { + base.SceneEnd(scene); + + // leaving level; disable the hook on rainbow spinner hue. + if (rainbowSpinnerHueHooked) { + On.Celeste.CrystalStaticSpinner.GetHue -= getRainbowSpinnerHue; + rainbowSpinnerHueHooked = false; + }; + } + + private static Color getRainbowSpinnerHue(On.Celeste.CrystalStaticSpinner.orig_GetHue orig, CrystalStaticSpinner self, Vector2 position) { + RainbowSpinnerColorAreaController controller = self.CollideFirst(); + if (controller != null) { + // apply the color from the controller we are in. + return RainbowSpinnerColorController.getModHue(controller.colors, controller.gradientSize, self.Scene, position); + } else { + // we are not in a controller; apply the vanilla color. + return orig(self, position); + } + } + } +} diff --git a/Entities/RainbowSpinnerColorController.cs b/Entities/RainbowSpinnerColorController.cs index 2ac11e5..1592570 100644 --- a/Entities/RainbowSpinnerColorController.cs +++ b/Entities/RainbowSpinnerColorController.cs @@ -1,129 +1,129 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// A controller allowing for customization of rainbow spinner colors. - /// - [CustomEntity("SpringCollab2020/RainbowSpinnerColorController")] - class RainbowSpinnerColorController : Entity { - private static bool rainbowSpinnerHueHooked = false; - - // the spinner controller on the current screen. - private static RainbowSpinnerColorController spinnerControllerOnScreen; - - // during transitions: the spinner controller on the next screen, and the progress between both screens. - // transitionProgress = -1 means no transition is ongoing. - private static RainbowSpinnerColorController nextSpinnerController; - private static float transitionProgress = -1f; - - // the parameters for this spinner controller. - private Color[] colors; - private float gradientSize; - - public RainbowSpinnerColorController(EntityData data, Vector2 offset) : base(data.Position + offset) { - // convert the color list to Color objects - string[] colorsAsStrings = data.Attr("colors", "89E5AE,88E0E0,87A9DD,9887DB,D088E2").Split(','); - colors = new Color[colorsAsStrings.Length]; - for (int i = 0; i < colors.Length; i++) { - colors[i] = Calc.HexToColor(colorsAsStrings[i]); - } - - gradientSize = data.Float("gradientSize", 280); - - Add(new TransitionListener { - OnIn = progress => transitionProgress = progress, - OnOut = progress => transitionProgress = progress, - OnInBegin = () => transitionProgress = 0f, - OnInEnd = () => transitionProgress = -1f - }); - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - // this is the controller for the next screen. - nextSpinnerController = this; - - // enable the hook on rainbow spinner hue. - if (!rainbowSpinnerHueHooked) { - On.Celeste.CrystalStaticSpinner.GetHue += getRainbowSpinnerHue; - rainbowSpinnerHueHooked = true; - } - } - - public override void Removed(Scene scene) { - base.Removed(scene); - - // the "current" spinner controller is now the one from the next screen. - spinnerControllerOnScreen = nextSpinnerController; - nextSpinnerController = null; - - // the transition (if any) is over. - transitionProgress = -1f; - - // if there is none, clean up the hook on the spinner hue. - if (spinnerControllerOnScreen == null && rainbowSpinnerHueHooked) { - On.Celeste.CrystalStaticSpinner.GetHue -= getRainbowSpinnerHue; - rainbowSpinnerHueHooked = false; - } - } - - public override void SceneEnd(Scene scene) { - base.SceneEnd(scene); - - // leaving level: forget about all controllers and clean up the hook if present. - spinnerControllerOnScreen = null; - nextSpinnerController = null; - if (rainbowSpinnerHueHooked) { - On.Celeste.CrystalStaticSpinner.GetHue -= getRainbowSpinnerHue; - rainbowSpinnerHueHooked = false; - }; - } - - private static Color getRainbowSpinnerHue(On.Celeste.CrystalStaticSpinner.orig_GetHue orig, CrystalStaticSpinner self, Vector2 position) { - if (transitionProgress == -1f) { - // no transition is ongoing. - // if only nextSpinnerController is defined, move it into spinnerControllerOnScreen. - if (spinnerControllerOnScreen == null) { - spinnerControllerOnScreen = nextSpinnerController; - nextSpinnerController = null; - } - - return getModHue(spinnerControllerOnScreen.colors, spinnerControllerOnScreen.gradientSize, self.Scene, self.Position); - } else { - // get the spinner color in the room we're coming from. - Color fromRoomColor; - if (spinnerControllerOnScreen != null) { - fromRoomColor = getModHue(spinnerControllerOnScreen.colors, spinnerControllerOnScreen.gradientSize, self.Scene, self.Position); - } else { - fromRoomColor = orig(self, position); - } - - // get the spinner color in the room we're going to. - Color toRoomColor; - if (nextSpinnerController != null) { - toRoomColor = getModHue(nextSpinnerController.colors, nextSpinnerController.gradientSize, self.Scene, self.Position); - } else { - toRoomColor = orig(self, position); - } - - // transition smoothly between both. - return Color.Lerp(fromRoomColor, toRoomColor, transitionProgress); - } - } - - internal static Color getModHue(Color[] colors, float gradientSize, Scene scene, Vector2 position) { - float progress = Calc.YoYo((position.Length() + scene.TimeActive * 50f) % gradientSize / gradientSize); - if (progress == 1) { - return colors[colors.Length - 1]; - } - - float globalProgress = (colors.Length - 1) * progress; - int colorIndex = (int) globalProgress; - float progressInIndex = globalProgress - colorIndex; - return Color.Lerp(colors[colorIndex], colors[colorIndex + 1], progressInIndex); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// A controller allowing for customization of rainbow spinner colors. + /// + [CustomEntity("SpringCollab2020/RainbowSpinnerColorController")] + class RainbowSpinnerColorController : Entity { + private static bool rainbowSpinnerHueHooked = false; + + // the spinner controller on the current screen. + private static RainbowSpinnerColorController spinnerControllerOnScreen; + + // during transitions: the spinner controller on the next screen, and the progress between both screens. + // transitionProgress = -1 means no transition is ongoing. + private static RainbowSpinnerColorController nextSpinnerController; + private static float transitionProgress = -1f; + + // the parameters for this spinner controller. + private Color[] colors; + private float gradientSize; + + public RainbowSpinnerColorController(EntityData data, Vector2 offset) : base(data.Position + offset) { + // convert the color list to Color objects + string[] colorsAsStrings = data.Attr("colors", "89E5AE,88E0E0,87A9DD,9887DB,D088E2").Split(','); + colors = new Color[colorsAsStrings.Length]; + for (int i = 0; i < colors.Length; i++) { + colors[i] = Calc.HexToColor(colorsAsStrings[i]); + } + + gradientSize = data.Float("gradientSize", 280); + + Add(new TransitionListener { + OnIn = progress => transitionProgress = progress, + OnOut = progress => transitionProgress = progress, + OnInBegin = () => transitionProgress = 0f, + OnInEnd = () => transitionProgress = -1f + }); + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + // this is the controller for the next screen. + nextSpinnerController = this; + + // enable the hook on rainbow spinner hue. + if (!rainbowSpinnerHueHooked) { + On.Celeste.CrystalStaticSpinner.GetHue += getRainbowSpinnerHue; + rainbowSpinnerHueHooked = true; + } + } + + public override void Removed(Scene scene) { + base.Removed(scene); + + // the "current" spinner controller is now the one from the next screen. + spinnerControllerOnScreen = nextSpinnerController; + nextSpinnerController = null; + + // the transition (if any) is over. + transitionProgress = -1f; + + // if there is none, clean up the hook on the spinner hue. + if (spinnerControllerOnScreen == null && rainbowSpinnerHueHooked) { + On.Celeste.CrystalStaticSpinner.GetHue -= getRainbowSpinnerHue; + rainbowSpinnerHueHooked = false; + } + } + + public override void SceneEnd(Scene scene) { + base.SceneEnd(scene); + + // leaving level: forget about all controllers and clean up the hook if present. + spinnerControllerOnScreen = null; + nextSpinnerController = null; + if (rainbowSpinnerHueHooked) { + On.Celeste.CrystalStaticSpinner.GetHue -= getRainbowSpinnerHue; + rainbowSpinnerHueHooked = false; + }; + } + + private static Color getRainbowSpinnerHue(On.Celeste.CrystalStaticSpinner.orig_GetHue orig, CrystalStaticSpinner self, Vector2 position) { + if (transitionProgress == -1f) { + // no transition is ongoing. + // if only nextSpinnerController is defined, move it into spinnerControllerOnScreen. + if (spinnerControllerOnScreen == null) { + spinnerControllerOnScreen = nextSpinnerController; + nextSpinnerController = null; + } + + return getModHue(spinnerControllerOnScreen.colors, spinnerControllerOnScreen.gradientSize, self.Scene, self.Position); + } else { + // get the spinner color in the room we're coming from. + Color fromRoomColor; + if (spinnerControllerOnScreen != null) { + fromRoomColor = getModHue(spinnerControllerOnScreen.colors, spinnerControllerOnScreen.gradientSize, self.Scene, self.Position); + } else { + fromRoomColor = orig(self, position); + } + + // get the spinner color in the room we're going to. + Color toRoomColor; + if (nextSpinnerController != null) { + toRoomColor = getModHue(nextSpinnerController.colors, nextSpinnerController.gradientSize, self.Scene, self.Position); + } else { + toRoomColor = orig(self, position); + } + + // transition smoothly between both. + return Color.Lerp(fromRoomColor, toRoomColor, transitionProgress); + } + } + + internal static Color getModHue(Color[] colors, float gradientSize, Scene scene, Vector2 position) { + float progress = Calc.YoYo((position.Length() + scene.TimeActive * 50f) % gradientSize / gradientSize); + if (progress == 1) { + return colors[colors.Length - 1]; + } + + float globalProgress = (colors.Length - 1) * progress; + int colorIndex = (int) globalProgress; + float progressInIndex = globalProgress - colorIndex; + return Color.Lerp(colors[colorIndex], colors[colorIndex + 1], progressInIndex); + } + } +} diff --git a/Entities/RespawningJellyfish.cs b/Entities/RespawningJellyfish.cs index 4b8f935..76375fa 100644 --- a/Entities/RespawningJellyfish.cs +++ b/Entities/RespawningJellyfish.cs @@ -1,107 +1,107 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; -using System.Collections; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/RespawningJellyfish")] - class RespawningJellyfish : Glider { - private DynData self; - - private float respawnTime; - private bool bubble; - - private Sprite sprite; - - private Vector2 initialPosition; - private bool respawning; - - private bool shouldRespawn = true; - - public RespawningJellyfish(EntityData data, Vector2 offset) : base(data, offset) { - respawnTime = data.Float("respawnTime"); - bubble = data.Bool("bubble"); - initialPosition = Position; - respawning = false; - - // get the sprite, and give it a respawn animation. - self = new DynData(this); - sprite = self.Get("sprite"); - new DynData(sprite)["atlas"] = GFX.Game; - sprite.Add("respawn", "objects/SpringCollab2020/glider/respawn", 0.03f, "idle"); - - // listen for transitions: if the jelly is carried to another screen, it should not respawn anymore. - Add(new TransitionListener() { - OnOutBegin = () => shouldRespawn = false - }); - } - - public override void Update() { - if (shouldRespawn && !respawning && Top + Speed.Y * Engine.DeltaTime > (SceneAs().Bounds.Bottom + 16)) { - // the jellyfish glided off-screen. - removeAndRespawn(); - } - - base.Update(); - - if (shouldRespawn && !respawning && self.Get("destroyed")) { - // replace the vanilla destroy routine with our custom one. - foreach (Component component in this) { - if (component is Coroutine) { - Remove(component); - Add(new Coroutine(destroyThenRespawnRoutine())); - break; - } - } - } - } - - 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())); - } - - 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; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; +using System.Collections; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/RespawningJellyfish")] + class RespawningJellyfish : Glider { + private DynData self; + + private float respawnTime; + private bool bubble; + + private Sprite sprite; + + private Vector2 initialPosition; + private bool respawning; + + private bool shouldRespawn = true; + + public RespawningJellyfish(EntityData data, Vector2 offset) : base(data, offset) { + respawnTime = data.Float("respawnTime"); + bubble = data.Bool("bubble"); + initialPosition = Position; + respawning = false; + + // get the sprite, and give it a respawn animation. + self = new DynData(this); + sprite = self.Get("sprite"); + new DynData(sprite)["atlas"] = GFX.Game; + sprite.Add("respawn", "objects/SpringCollab2020/glider/respawn", 0.03f, "idle"); + + // listen for transitions: if the jelly is carried to another screen, it should not respawn anymore. + Add(new TransitionListener() { + OnOutBegin = () => shouldRespawn = false + }); + } + + public override void Update() { + if (shouldRespawn && !respawning && Top + Speed.Y * Engine.DeltaTime > (SceneAs().Bounds.Bottom + 16)) { + // the jellyfish glided off-screen. + removeAndRespawn(); + } + + base.Update(); + + if (shouldRespawn && !respawning && self.Get("destroyed")) { + // replace the vanilla destroy routine with our custom one. + foreach (Component component in this) { + if (component is Coroutine) { + Remove(component); + Add(new Coroutine(destroyThenRespawnRoutine())); + break; + } + } + } + } + + 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())); + } + + 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; + } + } +} diff --git a/Entities/RetractSpinner.cs b/Entities/RetractSpinner.cs index 7adf710..ab21e95 100644 --- a/Entities/RetractSpinner.cs +++ b/Entities/RetractSpinner.cs @@ -1,118 +1,118 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System; -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/RetractSpinner")] - public class RetractSpinner : Entity { - - private float offset; - - private Image retractedSprite; - private Image expandedSprite; - - private bool isExpanded = false; - - public RetractSpinner(EntityData data, Vector2 offset) : base(data.Position + offset) { - // pulled straight from vanilla spinners. - this.offset = Calc.Random.NextFloat(); - Tag = Tags.TransitionUpdate; - Collider = new ColliderList(new Circle(6f), new Hitbox(16f, 4f, -8f, -3f)); - Visible = false; - Add(new PlayerCollider(OnPlayer)); - Add(new HoldableCollider(OnHoldable)); - Add(new LedgeBlocker()); - Depth = 1; // just below Madeline, since the default state is retracted - if (data.Bool("attachToSolid")) { - Add(new StaticMover { - OnShake = OnShake, - SolidChecker = IsRiding, - OnDestroy = RemoveSelf - }); - } - int randomSeed = Calc.Random.Next(); - - // load the retracted and expanded sprites. - Calc.PushRandom(randomSeed); - List expandedVariations = GFX.Game.GetAtlasSubtextures("danger/SpringCollab2020/retractspinner/urchin_harm"); - List retractedVariations = GFX.Game.GetAtlasSubtextures("danger/SpringCollab2020/retractspinner/urchin_safe"); - MTexture expandedTexture = Calc.Random.Choose(expandedVariations); - MTexture retractedTexture = Calc.Random.Choose(retractedVariations); - expandedSprite = new Image(expandedTexture).SetOrigin(12f, 12f); - retractedSprite = new Image(retractedTexture).SetOrigin(12f, 12f); - Calc.PopRandom(); - - // the default state is "retracted". add the matching sprite - Add(retractedSprite); - } - - public override void Update() { - if (!Visible) { - // check if in view, if so make the spinner visible. - Collidable = false; - if (InView()) { - Visible = true; - } - } else { - base.Update(); - - // hide out-of-view spinners like the vanilla "spinner cycle" - if (Scene.OnInterval(0.25f, offset) && !InView()) { - Visible = false; - } - - // check if spinners are close enough to the player to get collidable, like the vanilla "spinner cycle" - if (Scene.OnInterval(0.05f, offset)) { - Player player = Scene.Tracker.GetEntity(); - if (player != null) { - // those spinners are expanded only when in contact with water. - if (isExpanded != CollideCheck()) { - // the state has to be changed. - if (isExpanded) { - Remove(expandedSprite); - Add(retractedSprite); - Depth = 1; // just below Madeline - } else { - Remove(retractedSprite); - Add(expandedSprite); - Depth = -8500; // vanilla spinner depth - } - isExpanded = !isExpanded; - } - - // spinners are collidable if expanded + close enough to the player (same rule as vanilla). - Collidable = isExpanded && Math.Abs(player.X - X) < 128f && Math.Abs(player.Y - Y) < 128f; - } - } - } - } - - // those are pulled straight from vanilla. - private bool InView() { - Camera camera = (Scene as Level).Camera; - return X > camera.X - 16f && Y > camera.Y - 16f && X < camera.X + 320f + 16f && Y < camera.Y + 180f + 16f; - } - - private void OnShake(Vector2 pos) { - foreach (Component component in Components) { - if (component is Image image) { - image.Position = pos; - } - } - } - - private bool IsRiding(Solid solid) { - return CollideCheck(solid); - } - - private void OnPlayer(Player player) { - player.Die((player.Position - Position).SafeNormalize()); - } - - private void OnHoldable(Holdable h) { - h.HitSpinner(this); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System; +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/RetractSpinner")] + public class RetractSpinner : Entity { + + private float offset; + + private Image retractedSprite; + private Image expandedSprite; + + private bool isExpanded = false; + + public RetractSpinner(EntityData data, Vector2 offset) : base(data.Position + offset) { + // pulled straight from vanilla spinners. + this.offset = Calc.Random.NextFloat(); + Tag = Tags.TransitionUpdate; + Collider = new ColliderList(new Circle(6f), new Hitbox(16f, 4f, -8f, -3f)); + Visible = false; + Add(new PlayerCollider(OnPlayer)); + Add(new HoldableCollider(OnHoldable)); + Add(new LedgeBlocker()); + Depth = 1; // just below Madeline, since the default state is retracted + if (data.Bool("attachToSolid")) { + Add(new StaticMover { + OnShake = OnShake, + SolidChecker = IsRiding, + OnDestroy = RemoveSelf + }); + } + int randomSeed = Calc.Random.Next(); + + // load the retracted and expanded sprites. + Calc.PushRandom(randomSeed); + List expandedVariations = GFX.Game.GetAtlasSubtextures("danger/SpringCollab2020/retractspinner/urchin_harm"); + List retractedVariations = GFX.Game.GetAtlasSubtextures("danger/SpringCollab2020/retractspinner/urchin_safe"); + MTexture expandedTexture = Calc.Random.Choose(expandedVariations); + MTexture retractedTexture = Calc.Random.Choose(retractedVariations); + expandedSprite = new Image(expandedTexture).SetOrigin(12f, 12f); + retractedSprite = new Image(retractedTexture).SetOrigin(12f, 12f); + Calc.PopRandom(); + + // the default state is "retracted". add the matching sprite + Add(retractedSprite); + } + + public override void Update() { + if (!Visible) { + // check if in view, if so make the spinner visible. + Collidable = false; + if (InView()) { + Visible = true; + } + } else { + base.Update(); + + // hide out-of-view spinners like the vanilla "spinner cycle" + if (Scene.OnInterval(0.25f, offset) && !InView()) { + Visible = false; + } + + // check if spinners are close enough to the player to get collidable, like the vanilla "spinner cycle" + if (Scene.OnInterval(0.05f, offset)) { + Player player = Scene.Tracker.GetEntity(); + if (player != null) { + // those spinners are expanded only when in contact with water. + if (isExpanded != CollideCheck()) { + // the state has to be changed. + if (isExpanded) { + Remove(expandedSprite); + Add(retractedSprite); + Depth = 1; // just below Madeline + } else { + Remove(retractedSprite); + Add(expandedSprite); + Depth = -8500; // vanilla spinner depth + } + isExpanded = !isExpanded; + } + + // spinners are collidable if expanded + close enough to the player (same rule as vanilla). + Collidable = isExpanded && Math.Abs(player.X - X) < 128f && Math.Abs(player.Y - Y) < 128f; + } + } + } + } + + // those are pulled straight from vanilla. + private bool InView() { + Camera camera = (Scene as Level).Camera; + return X > camera.X - 16f && Y > camera.Y - 16f && X < camera.X + 320f + 16f && Y < camera.Y + 180f + 16f; + } + + private void OnShake(Vector2 pos) { + foreach (Component component in Components) { + if (component is Image image) { + image.Position = pos; + } + } + } + + private bool IsRiding(Solid solid) { + return CollideCheck(solid); + } + + private void OnPlayer(Player player) { + player.Die((player.Position - Position).SafeNormalize()); + } + + private void OnHoldable(Holdable h) { + h.HitSpinner(this); + } + } +} diff --git a/Entities/SafeRespawnCrumble.cs b/Entities/SafeRespawnCrumble.cs index 6f8f426..d4ccc1a 100644 --- a/Entities/SafeRespawnCrumble.cs +++ b/Entities/SafeRespawnCrumble.cs @@ -1,23 +1,23 @@ -using System.Collections; +using System.Collections; using System.Collections.Generic; using Microsoft.Xna.Framework; using Monocle; using Celeste.Mod.Entities; using System.Reflection; -using System; - +using System; + /* * Safe Respawn Crumble (Spring Collab 2020) * https://github.com/EverestAPI/SpringCollab2020/ - * + * * A Crumble Platform which is always "crumbled", * unless the player respawns in its vicinity * or is bubbled to it via a CassetteReturn routine. - * + * * It performs a similar role to Mario Maker's pink block platform, * which only spawns when a user playtests their level from an aerial position * and disappears once the user jumps off of it. - */ + */ namespace Celeste.Mod.SpringCollab2020.Entities { [Tracked(false)] [CustomEntity("SpringCollab2020/safeRespawnCrumble")] @@ -42,30 +42,30 @@ public override void Added(Scene scene) { MTexture tileTexture = GFX.Game["objects/SpringCollab2020/safeRespawnCrumble/tile"]; tiles = new List(); - outlineTiles = new List(); - - if (outlineVisible) { - if (Width <= 8f) { - Image toDraw = new Image(outlineTexture.GetSubtexture(24, 0, 8, 8, null)); - toDraw.Color = Color.White; - outlineTiles.Add(toDraw); - } else { - // Select left, middle, or right - for (int tile = 0; (float) tile < base.Width; tile += 8) { - int tileTex; - if (tile == 0) - tileTex = 0; - else if (tile > 0 && (float) tile < base.Width - 8f) - tileTex = 1; - else - tileTex = 2; - - Image toDraw = new Image(outlineTexture.GetSubtexture(tileTex * 8, 0, 8, 8)); - toDraw.Position = new Vector2((float) tile, 0f); - outlineTiles.Add(toDraw); - Add(toDraw); - } - } + outlineTiles = new List(); + + if (outlineVisible) { + if (Width <= 8f) { + Image toDraw = new Image(outlineTexture.GetSubtexture(24, 0, 8, 8, null)); + toDraw.Color = Color.White; + outlineTiles.Add(toDraw); + } else { + // Select left, middle, or right + for (int tile = 0; (float) tile < base.Width; tile += 8) { + int tileTex; + if (tile == 0) + tileTex = 0; + else if (tile > 0 && (float) tile < base.Width - 8f) + tileTex = 1; + else + tileTex = 2; + + Image toDraw = new Image(outlineTexture.GetSubtexture(tileTex * 8, 0, 8, 8)); + toDraw.Position = new Vector2((float) tile, 0f); + outlineTiles.Add(toDraw); + Add(toDraw); + } + } } // Add pink-block tiles @@ -162,19 +162,19 @@ private IEnumerator TileFade(float to, List targetTiles, bool fast = fals public static void Load() { On.Celeste.Player.CassetteFlyCoroutine += SafeActivatorCassetteFly; On.Celeste.Player.IntroRespawnBegin += SafeActivatorRespawn; - } - + } + public static void Unload() { On.Celeste.Player.CassetteFlyCoroutine -= SafeActivatorCassetteFly; On.Celeste.Player.IntroRespawnBegin -= SafeActivatorRespawn; - } - + } + private static IEnumerator SafeActivatorCassetteFly(On.Celeste.Player.orig_CassetteFlyCoroutine orig, Player self) { IEnumerator origEnum = orig(self); - while (origEnum.MoveNext()) + while (origEnum.MoveNext()) yield return origEnum.Current; - SafeActivate(self, false); + SafeActivate(self, false); } private static void SafeActivatorRespawn(On.Celeste.Player.orig_IntroRespawnBegin orig, Player self) { diff --git a/Entities/SeekerCustomColors.cs b/Entities/SeekerCustomColors.cs index 4f6e7fe..54a4d83 100644 --- a/Entities/SeekerCustomColors.cs +++ b/Entities/SeekerCustomColors.cs @@ -1,93 +1,93 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/SeekerCustomColors")] - [TrackedAs(typeof(Seeker))] - class SeekerCustomColors : Seeker { - public static void Load() { - On.Celeste.Seeker.GotBouncedOn += modGotBouncedOn; - } - - public static void Unload() { - On.Celeste.Seeker.GotBouncedOn -= modGotBouncedOn; - } - - private static void modGotBouncedOn(On.Celeste.Seeker.orig_GotBouncedOn orig, Seeker self, Entity entity) { - // if the seeker is one with custom colors, switch out the "stomp" particle that will be used by vanilla code. - bool hasCustomColor = self is SeekerCustomColors; - if (hasCustomColor) { - P_Stomp = (self as SeekerCustomColors).pStomp; - } - - orig(self, entity); - - // restore the stomp particle. - if (hasCustomColor) { - P_Stomp = basePStomp; - } - } - - private static ParticleType basePAttack; - private static ParticleType basePHitWall; - private static ParticleType basePStomp; - private static ParticleType basePRegen; - private static Color baseTrailColor; - - private ParticleType pAttack; // blinks between 99e550 and ddffbc - private ParticleType pHitWall; // blinks between 99e550 and ddffbc (same as Attack) - private ParticleType pStomp; // copies pAttack - private ParticleType pRegen; // blinks between cbdbfc and 575fd9 - private Color trailColor; - - private DynData self; - - public SeekerCustomColors(EntityData data, Vector2 offset) : base(data, offset) { - if (basePAttack == null) { - basePAttack = P_Attack; - basePHitWall = P_HitWall; - basePStomp = P_Stomp; - basePRegen = P_Regen; - baseTrailColor = TrailColor; - } - - pAttack = new ParticleType(P_Attack) { - Color = Calc.HexToColor(data.Attr("attackParticleColor1")), - Color2 = Calc.HexToColor(data.Attr("attackParticleColor2")) - }; - pHitWall = new ParticleType(P_HitWall) { - Color = Calc.HexToColor(data.Attr("attackParticleColor1")), - Color2 = Calc.HexToColor(data.Attr("attackParticleColor2")) - }; - pStomp = new ParticleType(P_Stomp) { - Color = Calc.HexToColor(data.Attr("attackParticleColor1")), - Color2 = Calc.HexToColor(data.Attr("attackParticleColor2")) - }; - pRegen = new ParticleType(P_Regen) { - Color = Calc.HexToColor(data.Attr("regenParticleColor1")), - Color2 = Calc.HexToColor(data.Attr("regenParticleColor2")) - }; - trailColor = Calc.HexToColor(data.Attr("trailColor")); - - self = new DynData(this); - } - - public override void Update() { - P_Attack = pAttack; - P_HitWall = pHitWall; - P_Regen = pRegen; - self["TrailColor"] = trailColor; - - base.Update(); - - P_Attack = basePAttack; - P_HitWall = basePHitWall; - P_Regen = basePRegen; - self["TrailColor"] = baseTrailColor; - } - - - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/SeekerCustomColors")] + [TrackedAs(typeof(Seeker))] + class SeekerCustomColors : Seeker { + public static void Load() { + On.Celeste.Seeker.GotBouncedOn += modGotBouncedOn; + } + + public static void Unload() { + On.Celeste.Seeker.GotBouncedOn -= modGotBouncedOn; + } + + private static void modGotBouncedOn(On.Celeste.Seeker.orig_GotBouncedOn orig, Seeker self, Entity entity) { + // if the seeker is one with custom colors, switch out the "stomp" particle that will be used by vanilla code. + bool hasCustomColor = self is SeekerCustomColors; + if (hasCustomColor) { + P_Stomp = (self as SeekerCustomColors).pStomp; + } + + orig(self, entity); + + // restore the stomp particle. + if (hasCustomColor) { + P_Stomp = basePStomp; + } + } + + private static ParticleType basePAttack; + private static ParticleType basePHitWall; + private static ParticleType basePStomp; + private static ParticleType basePRegen; + private static Color baseTrailColor; + + private ParticleType pAttack; // blinks between 99e550 and ddffbc + private ParticleType pHitWall; // blinks between 99e550 and ddffbc (same as Attack) + private ParticleType pStomp; // copies pAttack + private ParticleType pRegen; // blinks between cbdbfc and 575fd9 + private Color trailColor; + + private DynData self; + + public SeekerCustomColors(EntityData data, Vector2 offset) : base(data, offset) { + if (basePAttack == null) { + basePAttack = P_Attack; + basePHitWall = P_HitWall; + basePStomp = P_Stomp; + basePRegen = P_Regen; + baseTrailColor = TrailColor; + } + + pAttack = new ParticleType(P_Attack) { + Color = Calc.HexToColor(data.Attr("attackParticleColor1")), + Color2 = Calc.HexToColor(data.Attr("attackParticleColor2")) + }; + pHitWall = new ParticleType(P_HitWall) { + Color = Calc.HexToColor(data.Attr("attackParticleColor1")), + Color2 = Calc.HexToColor(data.Attr("attackParticleColor2")) + }; + pStomp = new ParticleType(P_Stomp) { + Color = Calc.HexToColor(data.Attr("attackParticleColor1")), + Color2 = Calc.HexToColor(data.Attr("attackParticleColor2")) + }; + pRegen = new ParticleType(P_Regen) { + Color = Calc.HexToColor(data.Attr("regenParticleColor1")), + Color2 = Calc.HexToColor(data.Attr("regenParticleColor2")) + }; + trailColor = Calc.HexToColor(data.Attr("trailColor")); + + self = new DynData(this); + } + + public override void Update() { + P_Attack = pAttack; + P_HitWall = pHitWall; + P_Regen = pRegen; + self["TrailColor"] = trailColor; + + base.Update(); + + P_Attack = basePAttack; + P_HitWall = basePHitWall; + P_Regen = basePRegen; + self["TrailColor"] = baseTrailColor; + } + + + } +} diff --git a/Entities/SeekerStatueCustomColors.cs b/Entities/SeekerStatueCustomColors.cs index 3ae7293..1f2538b 100644 --- a/Entities/SeekerStatueCustomColors.cs +++ b/Entities/SeekerStatueCustomColors.cs @@ -1,36 +1,36 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/SeekerStatueCustomColors")] - class SeekerStatueCustomColors : SeekerStatue { - private static ParticleType basePBreakOut; - private ParticleType pBreakOut; // chooses between 643e73 and 3e2854 - - public SeekerStatueCustomColors(EntityData data, Vector2 offset) : base(data, offset) { - if (basePBreakOut == null) { - basePBreakOut = Seeker.P_BreakOut; - } - - pBreakOut = new ParticleType(Seeker.P_BreakOut) { - Color = Calc.HexToColor(data.Attr("breakOutParticleColor1")), - Color2 = Calc.HexToColor(data.Attr("breakOutParticleColor2")) - }; - - new DynData(this).Get("sprite").OnLastFrame = anim => { - if (anim == "hatch") { - Scene.Add(new SeekerCustomColors(data, offset) { Light = { Alpha = 0f } }); - RemoveSelf(); - } - }; - } - - public override void Update() { - Seeker.P_BreakOut = pBreakOut; - base.Update(); - Seeker.P_BreakOut = basePBreakOut; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/SeekerStatueCustomColors")] + class SeekerStatueCustomColors : SeekerStatue { + private static ParticleType basePBreakOut; + private ParticleType pBreakOut; // chooses between 643e73 and 3e2854 + + public SeekerStatueCustomColors(EntityData data, Vector2 offset) : base(data, offset) { + if (basePBreakOut == null) { + basePBreakOut = Seeker.P_BreakOut; + } + + pBreakOut = new ParticleType(Seeker.P_BreakOut) { + Color = Calc.HexToColor(data.Attr("breakOutParticleColor1")), + Color2 = Calc.HexToColor(data.Attr("breakOutParticleColor2")) + }; + + new DynData(this).Get("sprite").OnLastFrame = anim => { + if (anim == "hatch") { + Scene.Add(new SeekerCustomColors(data, offset) { Light = { Alpha = 0f } }); + RemoveSelf(); + } + }; + } + + public override void Update() { + Seeker.P_BreakOut = pBreakOut; + base.Update(); + Seeker.P_BreakOut = basePBreakOut; + } + } +} diff --git a/Entities/SidewaysJumpThru.cs b/Entities/SidewaysJumpThru.cs index 03b8b83..b17de4c 100644 --- a/Entities/SidewaysJumpThru.cs +++ b/Entities/SidewaysJumpThru.cs @@ -1,341 +1,341 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Mono.Cecil; -using Mono.Cecil.Cil; -using Monocle; -using MonoMod.Cil; -using MonoMod.RuntimeDetour; -using System; -using System.Linq; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/SidewaysJumpThru")] - [Tracked] - class SidewaysJumpThru : Entity { - - private static ILHook hookOnUpdateSprite; - - private static FieldInfo actorMovementCounter = typeof(Actor).GetField("movementCounter", BindingFlags.Instance | BindingFlags.NonPublic); - - private static bool hooksActive = false; - - public static void Load() { - On.Celeste.LevelLoader.ctor += onLevelLoad; - On.Celeste.OverworldLoader.ctor += onOverworldLoad; - } - - public static void Unload() { - On.Celeste.LevelLoader.ctor -= onLevelLoad; - On.Celeste.OverworldLoader.ctor -= onOverworldLoad; - - deactivateHooks(); - } - - private static void onLevelLoad(On.Celeste.LevelLoader.orig_ctor orig, LevelLoader self, Session session, Vector2? startPosition) { - orig(self, session, startPosition); - - if (session.MapData?.Levels?.Any(level => level.Entities?.Any(entity => entity.Name == "SpringCollab2020/SidewaysJumpThru") ?? false) ?? false) { - activateHooks(); - } else { - deactivateHooks(); - } - } - - private static void onOverworldLoad(On.Celeste.OverworldLoader.orig_ctor orig, OverworldLoader self, Overworld.StartMode startMode, HiresSnow snow) { - orig(self, startMode, snow); - - if (startMode != (Overworld.StartMode) (-1)) { // -1 = in-game overworld from the collab utils - deactivateHooks(); - } - } - - public static void activateHooks() { - if (hooksActive) { - return; - } - hooksActive = true; - - Logger.Log(LogLevel.Info, "SpringCollab2020/SidewaysJumpThru", "=== Activating sideways jumpthru hooks"); - - string updateSpriteMethodToPatch = Everest.Loader.DependencyLoaded(new EverestModuleMetadata { Name = "Everest", Version = new Version(1, 1432) }) ? - "orig_UpdateSprite" : "UpdateSprite"; - - // implement the basic collision between actors/platforms and sideways jumpthrus. - IL.Celeste.Actor.MoveHExact += addSidewaysJumpthrusInHorizontalMoveMethods; - IL.Celeste.Platform.MoveHExactCollideSolids += addSidewaysJumpthrusInHorizontalMoveMethods; - - // block "climb hopping" on top of sideways jumpthrus, because this just looks weird. - On.Celeste.Player.ClimbHopBlockedCheck += onPlayerClimbHopBlockedCheck; - - // mod collide checks to include sideways jumpthrus, so that the player behaves with them like with walls. - IL.Celeste.Player.WallJumpCheck += modCollideChecks; // allow player to walljump off them - IL.Celeste.Player.ClimbCheck += modCollideChecks; // allow player to climb on them - IL.Celeste.Player.ClimbBegin += modCollideChecks; // if not applied, the player will clip through jumpthrus if trying to climb on them - IL.Celeste.Player.ClimbUpdate += modCollideChecks; // when climbing, jumpthrus are handled like walls - IL.Celeste.Player.SlipCheck += modCollideChecks; // make climbing on jumpthrus not slippery - IL.Celeste.Player.NormalUpdate += modCollideChecks; // get the wall slide effect - IL.Celeste.Player.OnCollideH += modCollideChecks; // handle dashes against jumpthrus properly, without "shifting" down - IL.Celeste.Seeker.OnCollideH += modCollideChecks; // make seekers bump against jumpthrus, instead of vibrating at maximum velocity - - // don't make Madeline duck when dashing against a sideways jumpthru - On.Celeste.Player.DuckFreeAt += preventDuckWhenDashingAgainstJumpthru; - - // have the push animation when Madeline runs against a jumpthru for example - hookOnUpdateSprite = new ILHook(typeof(Player).GetMethod(updateSpriteMethodToPatch, BindingFlags.NonPublic | BindingFlags.Instance), modCollideChecks); - - // one extra hook that kills the player momentum when hitting a jumpthru so that they don't get "stuck" on them. - On.Celeste.Player.NormalUpdate += onPlayerNormalUpdate; - } - - public static void deactivateHooks() { - if (!hooksActive) { - return; - } - hooksActive = false; - - Logger.Log(LogLevel.Info, "SpringCollab2020/SidewaysJumpThru", "=== Deactivating sideways jumpthru hooks"); - IL.Celeste.Actor.MoveHExact -= addSidewaysJumpthrusInHorizontalMoveMethods; - IL.Celeste.Platform.MoveHExactCollideSolids -= addSidewaysJumpthrusInHorizontalMoveMethods; - - On.Celeste.Player.ClimbHopBlockedCheck -= onPlayerClimbHopBlockedCheck; - - IL.Celeste.Player.WallJumpCheck -= modCollideChecks; - IL.Celeste.Player.ClimbCheck -= modCollideChecks; - IL.Celeste.Player.ClimbBegin -= modCollideChecks; - IL.Celeste.Player.ClimbUpdate -= modCollideChecks; - IL.Celeste.Player.SlipCheck -= modCollideChecks; - IL.Celeste.Player.NormalUpdate -= modCollideChecks; - IL.Celeste.Player.OnCollideH -= modCollideChecks; - IL.Celeste.Seeker.OnCollideH -= modCollideChecks; - hookOnUpdateSprite?.Dispose(); - - On.Celeste.Player.DuckFreeAt -= preventDuckWhenDashingAgainstJumpthru; - - On.Celeste.Player.NormalUpdate -= onPlayerNormalUpdate; - } - - private static void addSidewaysJumpthrusInHorizontalMoveMethods(ILContext il) { - ILCursor cursor = new ILCursor(il); - - if (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCall("CollideFirst"))) { - Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Injecting sideways jumpthru check at {cursor.Index} in IL for {il.Method.Name}"); - cursor.Emit(OpCodes.Ldarg_0); - cursor.Emit(OpCodes.Ldarg_1); - cursor.EmitDelegate>((orig, self, moveH) => { - if (orig != null) - return orig; - - int moveDirection = Math.Sign(moveH); - bool movingLeftToRight = moveH > 0; - if (checkCollisionWithSidewaysJumpthruWhileMoving(self, moveDirection, movingLeftToRight)) { - return new Solid(Vector2.Zero, 0, 0, false); // what matters is that it is non null. - } - - return null; - }); - } - } - - private static bool checkCollisionWithSidewaysJumpthruWhileMoving(Entity self, int moveDirection, bool movingLeftToRight) { - // check if colliding with a sideways jumpthru - SidewaysJumpThru jumpThru = self.CollideFirstOutside(self.Position + Vector2.UnitX * moveDirection); - if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) { - // there is a sideways jump-thru and we are moving in the opposite direction => collision - return true; - } - - return false; - } - - private static bool onPlayerClimbHopBlockedCheck(On.Celeste.Player.orig_ClimbHopBlockedCheck orig, Player self) { - bool vanillaCheck = orig(self); - if (vanillaCheck) - return vanillaCheck; - - // block climb hops on jumpthrus because those look weird - return self.CollideCheckOutside(self.Position + Vector2.UnitX * (int) self.Facing); - } - - private static void modCollideChecks(ILContext il) { - ILCursor cursor = new ILCursor(il); - - // create a Vector2 temporary variable - VariableDefinition checkAtPositionStore = new VariableDefinition(il.Import(typeof(Vector2))); - il.Body.Variables.Add(checkAtPositionStore); - - bool isClimb = il.Method.Name.Contains("Climb"); - bool isWallJump = il.Method.Name.Contains("WallJump") || il.Method.Name.Contains("NormalUpdate"); - - while (cursor.Next != null) { - Instruction next = cursor.Next; - - // we want to replace all CollideChecks with solids here. - if (next.OpCode == OpCodes.Call && (next.Operand as MethodReference)?.FullName == "System.Boolean Monocle.Entity::CollideCheck(Microsoft.Xna.Framework.Vector2)") { - Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Patching Entity.CollideCheck to include sideways jumpthrus at {cursor.Index} in IL for {il.Method.Name}"); - - callOrigMethodKeepingEverythingOnStack(cursor, checkAtPositionStore, isSceneCollideCheck: false); - - // mod the result - cursor.EmitDelegate>((orig, self, checkAtPosition) => { - // we still want to check for solids... - if (orig) { - return true; - } - - // if we are not checking a side, this certainly has nothing to do with jumpthrus. - if (self.Position.X == checkAtPosition.X) - return false; - - return entityCollideCheckWithSidewaysJumpthrus(self, checkAtPosition, isClimb, isWallJump); - }); - } - - if (next.OpCode == OpCodes.Callvirt && (next.Operand as MethodReference)?.FullName == "System.Boolean Monocle.Scene::CollideCheck(Microsoft.Xna.Framework.Vector2)") { - Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Patching Scene.CollideCheck to include sideways jumpthrus at {cursor.Index} in IL for {il.Method.Name}"); - - callOrigMethodKeepingEverythingOnStack(cursor, checkAtPositionStore, isSceneCollideCheck: true); - - cursor.EmitDelegate>((orig, self, vector) => { - if (orig) { - return true; - } - return sceneCollideCheckWithSidewaysJumpthrus(self, vector, isClimb, isWallJump); - }); - } - - cursor.Index++; - } - } - - private static bool preventDuckWhenDashingAgainstJumpthru(On.Celeste.Player.orig_DuckFreeAt orig, Player self, Vector2 at) { - return orig(self, at) && !entityCollideCheckWithSidewaysJumpthrus(self, at, false, false); - } - - private static void callOrigMethodKeepingEverythingOnStack(ILCursor cursor, VariableDefinition checkAtPositionStore, bool isSceneCollideCheck) { - // store the position in the local variable - cursor.Emit(OpCodes.Stloc, checkAtPositionStore); - cursor.Emit(OpCodes.Ldloc, checkAtPositionStore); - - // let vanilla call CollideCheck - cursor.Index++; - - // reload the parameters - cursor.Emit(OpCodes.Ldarg_0); - if (isSceneCollideCheck) { - cursor.Emit(OpCodes.Call, typeof(Entity).GetProperty("Scene").GetGetMethod()); - } - - cursor.Emit(OpCodes.Ldloc, checkAtPositionStore); - } - - private static bool entityCollideCheckWithSidewaysJumpthrus(Entity self, Vector2 checkAtPosition, bool isClimb, bool isWallJump) { - // our entity collides if this is with a jumpthru and we are colliding with the solid side of it. - // we are in this case if the jumpthru is left to right (the "solid" side of it is the right one) - // and we are checking the collision on the left side of the player for example. - bool collideOnLeftSideOfPlayer = (self.Position.X > checkAtPosition.X); - SidewaysJumpThru jumpthru = self.CollideFirstOutside(checkAtPosition); - return jumpthru != null && (self is Player || self is Seeker) && jumpthru.AllowLeftToRight == collideOnLeftSideOfPlayer - && jumpthru.Bottom >= self.Top + checkAtPosition.Y - self.Position.Y + 3; - } - - private static bool sceneCollideCheckWithSidewaysJumpthrus(Scene self, Vector2 vector, bool isClimb, bool isWallJump) { - return self.CollideCheck(vector); - } - - private static int onPlayerNormalUpdate(On.Celeste.Player.orig_NormalUpdate orig, Player self) { - int result = orig(self); - - // kill speed if player is going towards a jumpthru. - if (self.Speed.X != 0) { - bool movingLeftToRight = self.Speed.X > 0; - SidewaysJumpThru jumpThru = self.CollideFirstOutside(self.Position + Vector2.UnitX * Math.Sign(self.Speed.X)); - if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) { - self.Speed.X = 0; - } - } - - return result; - } - - // ======== Begin of entity code ======== - - private int lines; - private string overrideTexture; - private float animationDelay; - - public bool AllowLeftToRight; - - public SidewaysJumpThru(Vector2 position, int height, bool allowLeftToRight, string overrideTexture, float animationDelay) - : base(position) { - - lines = height / 8; - AllowLeftToRight = allowLeftToRight; - Depth = -60; - this.overrideTexture = overrideTexture; - this.animationDelay = animationDelay; - - float hitboxOffset = 0f; - if (AllowLeftToRight) - hitboxOffset = 3f; - - Collider = new Hitbox(5f, height, hitboxOffset, 0); - } - - public SidewaysJumpThru(EntityData data, Vector2 offset) - : this(data.Position + offset, data.Height, !data.Bool("left"), data.Attr("texture", "default"), data.Float("animationDelay", 0f)) { - } - - public override void Awake(Scene scene) { - if (animationDelay > 0f) { - for (int i = 0; i < lines; i++) { - Sprite jumpthruSprite = new Sprite(GFX.Game, "objects/jumpthru/" + overrideTexture); - jumpthruSprite.AddLoop("idle", "", animationDelay); - - jumpthruSprite.Y = i * 8; - jumpthruSprite.Rotation = (float) (Math.PI / 2); - if (AllowLeftToRight) - jumpthruSprite.X = 8; - else - jumpthruSprite.Scale.Y = -1; - - jumpthruSprite.Play("idle"); - Add(jumpthruSprite); - } - } else { - AreaData areaData = AreaData.Get(scene); - string jumpthru = areaData.Jumpthru; - if (!string.IsNullOrEmpty(overrideTexture) && !overrideTexture.Equals("default")) { - jumpthru = overrideTexture; - } - - MTexture mTexture = GFX.Game["objects/jumpthru/" + jumpthru]; - int num = mTexture.Width / 8; - for (int i = 0; i < lines; i++) { - int xTilePosition; - int yTilePosition; - if (i == 0) { - xTilePosition = 0; - yTilePosition = ((!CollideCheck(Position + new Vector2(0f, -1f))) ? 1 : 0); - } else if (i == lines - 1) { - xTilePosition = num - 1; - yTilePosition = ((!CollideCheck(Position + new Vector2(0f, 1f))) ? 1 : 0); - } else { - xTilePosition = 1 + Calc.Random.Next(num - 2); - yTilePosition = Calc.Random.Choose(0, 1); - } - Image image = new Image(mTexture.GetSubtexture(xTilePosition * 8, yTilePosition * 8, 8, 8)); - image.Y = i * 8; - image.Rotation = (float) (Math.PI / 2); - - if (AllowLeftToRight) - image.X = 8; - else - image.Scale.Y = -1; - - Add(image); - } - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Monocle; +using MonoMod.Cil; +using MonoMod.RuntimeDetour; +using System; +using System.Linq; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/SidewaysJumpThru")] + [Tracked] + class SidewaysJumpThru : Entity { + + private static ILHook hookOnUpdateSprite; + + private static FieldInfo actorMovementCounter = typeof(Actor).GetField("movementCounter", BindingFlags.Instance | BindingFlags.NonPublic); + + private static bool hooksActive = false; + + public static void Load() { + On.Celeste.LevelLoader.ctor += onLevelLoad; + On.Celeste.OverworldLoader.ctor += onOverworldLoad; + } + + public static void Unload() { + On.Celeste.LevelLoader.ctor -= onLevelLoad; + On.Celeste.OverworldLoader.ctor -= onOverworldLoad; + + deactivateHooks(); + } + + private static void onLevelLoad(On.Celeste.LevelLoader.orig_ctor orig, LevelLoader self, Session session, Vector2? startPosition) { + orig(self, session, startPosition); + + if (session.MapData?.Levels?.Any(level => level.Entities?.Any(entity => entity.Name == "SpringCollab2020/SidewaysJumpThru") ?? false) ?? false) { + activateHooks(); + } else { + deactivateHooks(); + } + } + + private static void onOverworldLoad(On.Celeste.OverworldLoader.orig_ctor orig, OverworldLoader self, Overworld.StartMode startMode, HiresSnow snow) { + orig(self, startMode, snow); + + if (startMode != (Overworld.StartMode) (-1)) { // -1 = in-game overworld from the collab utils + deactivateHooks(); + } + } + + public static void activateHooks() { + if (hooksActive) { + return; + } + hooksActive = true; + + Logger.Log(LogLevel.Info, "SpringCollab2020/SidewaysJumpThru", "=== Activating sideways jumpthru hooks"); + + string updateSpriteMethodToPatch = Everest.Loader.DependencyLoaded(new EverestModuleMetadata { Name = "Everest", Version = new Version(1, 1432) }) ? + "orig_UpdateSprite" : "UpdateSprite"; + + // implement the basic collision between actors/platforms and sideways jumpthrus. + IL.Celeste.Actor.MoveHExact += addSidewaysJumpthrusInHorizontalMoveMethods; + IL.Celeste.Platform.MoveHExactCollideSolids += addSidewaysJumpthrusInHorizontalMoveMethods; + + // block "climb hopping" on top of sideways jumpthrus, because this just looks weird. + On.Celeste.Player.ClimbHopBlockedCheck += onPlayerClimbHopBlockedCheck; + + // mod collide checks to include sideways jumpthrus, so that the player behaves with them like with walls. + IL.Celeste.Player.WallJumpCheck += modCollideChecks; // allow player to walljump off them + IL.Celeste.Player.ClimbCheck += modCollideChecks; // allow player to climb on them + IL.Celeste.Player.ClimbBegin += modCollideChecks; // if not applied, the player will clip through jumpthrus if trying to climb on them + IL.Celeste.Player.ClimbUpdate += modCollideChecks; // when climbing, jumpthrus are handled like walls + IL.Celeste.Player.SlipCheck += modCollideChecks; // make climbing on jumpthrus not slippery + IL.Celeste.Player.NormalUpdate += modCollideChecks; // get the wall slide effect + IL.Celeste.Player.OnCollideH += modCollideChecks; // handle dashes against jumpthrus properly, without "shifting" down + IL.Celeste.Seeker.OnCollideH += modCollideChecks; // make seekers bump against jumpthrus, instead of vibrating at maximum velocity + + // don't make Madeline duck when dashing against a sideways jumpthru + On.Celeste.Player.DuckFreeAt += preventDuckWhenDashingAgainstJumpthru; + + // have the push animation when Madeline runs against a jumpthru for example + hookOnUpdateSprite = new ILHook(typeof(Player).GetMethod(updateSpriteMethodToPatch, BindingFlags.NonPublic | BindingFlags.Instance), modCollideChecks); + + // one extra hook that kills the player momentum when hitting a jumpthru so that they don't get "stuck" on them. + On.Celeste.Player.NormalUpdate += onPlayerNormalUpdate; + } + + public static void deactivateHooks() { + if (!hooksActive) { + return; + } + hooksActive = false; + + Logger.Log(LogLevel.Info, "SpringCollab2020/SidewaysJumpThru", "=== Deactivating sideways jumpthru hooks"); + IL.Celeste.Actor.MoveHExact -= addSidewaysJumpthrusInHorizontalMoveMethods; + IL.Celeste.Platform.MoveHExactCollideSolids -= addSidewaysJumpthrusInHorizontalMoveMethods; + + On.Celeste.Player.ClimbHopBlockedCheck -= onPlayerClimbHopBlockedCheck; + + IL.Celeste.Player.WallJumpCheck -= modCollideChecks; + IL.Celeste.Player.ClimbCheck -= modCollideChecks; + IL.Celeste.Player.ClimbBegin -= modCollideChecks; + IL.Celeste.Player.ClimbUpdate -= modCollideChecks; + IL.Celeste.Player.SlipCheck -= modCollideChecks; + IL.Celeste.Player.NormalUpdate -= modCollideChecks; + IL.Celeste.Player.OnCollideH -= modCollideChecks; + IL.Celeste.Seeker.OnCollideH -= modCollideChecks; + hookOnUpdateSprite?.Dispose(); + + On.Celeste.Player.DuckFreeAt -= preventDuckWhenDashingAgainstJumpthru; + + On.Celeste.Player.NormalUpdate -= onPlayerNormalUpdate; + } + + private static void addSidewaysJumpthrusInHorizontalMoveMethods(ILContext il) { + ILCursor cursor = new ILCursor(il); + + if (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCall("CollideFirst"))) { + Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Injecting sideways jumpthru check at {cursor.Index} in IL for {il.Method.Name}"); + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Ldarg_1); + cursor.EmitDelegate>((orig, self, moveH) => { + if (orig != null) + return orig; + + int moveDirection = Math.Sign(moveH); + bool movingLeftToRight = moveH > 0; + if (checkCollisionWithSidewaysJumpthruWhileMoving(self, moveDirection, movingLeftToRight)) { + return new Solid(Vector2.Zero, 0, 0, false); // what matters is that it is non null. + } + + return null; + }); + } + } + + private static bool checkCollisionWithSidewaysJumpthruWhileMoving(Entity self, int moveDirection, bool movingLeftToRight) { + // check if colliding with a sideways jumpthru + SidewaysJumpThru jumpThru = self.CollideFirstOutside(self.Position + Vector2.UnitX * moveDirection); + if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) { + // there is a sideways jump-thru and we are moving in the opposite direction => collision + return true; + } + + return false; + } + + private static bool onPlayerClimbHopBlockedCheck(On.Celeste.Player.orig_ClimbHopBlockedCheck orig, Player self) { + bool vanillaCheck = orig(self); + if (vanillaCheck) + return vanillaCheck; + + // block climb hops on jumpthrus because those look weird + return self.CollideCheckOutside(self.Position + Vector2.UnitX * (int) self.Facing); + } + + private static void modCollideChecks(ILContext il) { + ILCursor cursor = new ILCursor(il); + + // create a Vector2 temporary variable + VariableDefinition checkAtPositionStore = new VariableDefinition(il.Import(typeof(Vector2))); + il.Body.Variables.Add(checkAtPositionStore); + + bool isClimb = il.Method.Name.Contains("Climb"); + bool isWallJump = il.Method.Name.Contains("WallJump") || il.Method.Name.Contains("NormalUpdate"); + + while (cursor.Next != null) { + Instruction next = cursor.Next; + + // we want to replace all CollideChecks with solids here. + if (next.OpCode == OpCodes.Call && (next.Operand as MethodReference)?.FullName == "System.Boolean Monocle.Entity::CollideCheck(Microsoft.Xna.Framework.Vector2)") { + Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Patching Entity.CollideCheck to include sideways jumpthrus at {cursor.Index} in IL for {il.Method.Name}"); + + callOrigMethodKeepingEverythingOnStack(cursor, checkAtPositionStore, isSceneCollideCheck: false); + + // mod the result + cursor.EmitDelegate>((orig, self, checkAtPosition) => { + // we still want to check for solids... + if (orig) { + return true; + } + + // if we are not checking a side, this certainly has nothing to do with jumpthrus. + if (self.Position.X == checkAtPosition.X) + return false; + + return entityCollideCheckWithSidewaysJumpthrus(self, checkAtPosition, isClimb, isWallJump); + }); + } + + if (next.OpCode == OpCodes.Callvirt && (next.Operand as MethodReference)?.FullName == "System.Boolean Monocle.Scene::CollideCheck(Microsoft.Xna.Framework.Vector2)") { + Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Patching Scene.CollideCheck to include sideways jumpthrus at {cursor.Index} in IL for {il.Method.Name}"); + + callOrigMethodKeepingEverythingOnStack(cursor, checkAtPositionStore, isSceneCollideCheck: true); + + cursor.EmitDelegate>((orig, self, vector) => { + if (orig) { + return true; + } + return sceneCollideCheckWithSidewaysJumpthrus(self, vector, isClimb, isWallJump); + }); + } + + cursor.Index++; + } + } + + private static bool preventDuckWhenDashingAgainstJumpthru(On.Celeste.Player.orig_DuckFreeAt orig, Player self, Vector2 at) { + return orig(self, at) && !entityCollideCheckWithSidewaysJumpthrus(self, at, false, false); + } + + private static void callOrigMethodKeepingEverythingOnStack(ILCursor cursor, VariableDefinition checkAtPositionStore, bool isSceneCollideCheck) { + // store the position in the local variable + cursor.Emit(OpCodes.Stloc, checkAtPositionStore); + cursor.Emit(OpCodes.Ldloc, checkAtPositionStore); + + // let vanilla call CollideCheck + cursor.Index++; + + // reload the parameters + cursor.Emit(OpCodes.Ldarg_0); + if (isSceneCollideCheck) { + cursor.Emit(OpCodes.Call, typeof(Entity).GetProperty("Scene").GetGetMethod()); + } + + cursor.Emit(OpCodes.Ldloc, checkAtPositionStore); + } + + private static bool entityCollideCheckWithSidewaysJumpthrus(Entity self, Vector2 checkAtPosition, bool isClimb, bool isWallJump) { + // our entity collides if this is with a jumpthru and we are colliding with the solid side of it. + // we are in this case if the jumpthru is left to right (the "solid" side of it is the right one) + // and we are checking the collision on the left side of the player for example. + bool collideOnLeftSideOfPlayer = (self.Position.X > checkAtPosition.X); + SidewaysJumpThru jumpthru = self.CollideFirstOutside(checkAtPosition); + return jumpthru != null && (self is Player || self is Seeker) && jumpthru.AllowLeftToRight == collideOnLeftSideOfPlayer + && jumpthru.Bottom >= self.Top + checkAtPosition.Y - self.Position.Y + 3; + } + + private static bool sceneCollideCheckWithSidewaysJumpthrus(Scene self, Vector2 vector, bool isClimb, bool isWallJump) { + return self.CollideCheck(vector); + } + + private static int onPlayerNormalUpdate(On.Celeste.Player.orig_NormalUpdate orig, Player self) { + int result = orig(self); + + // kill speed if player is going towards a jumpthru. + if (self.Speed.X != 0) { + bool movingLeftToRight = self.Speed.X > 0; + SidewaysJumpThru jumpThru = self.CollideFirstOutside(self.Position + Vector2.UnitX * Math.Sign(self.Speed.X)); + if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) { + self.Speed.X = 0; + } + } + + return result; + } + + // ======== Begin of entity code ======== + + private int lines; + private string overrideTexture; + private float animationDelay; + + public bool AllowLeftToRight; + + public SidewaysJumpThru(Vector2 position, int height, bool allowLeftToRight, string overrideTexture, float animationDelay) + : base(position) { + + lines = height / 8; + AllowLeftToRight = allowLeftToRight; + Depth = -60; + this.overrideTexture = overrideTexture; + this.animationDelay = animationDelay; + + float hitboxOffset = 0f; + if (AllowLeftToRight) + hitboxOffset = 3f; + + Collider = new Hitbox(5f, height, hitboxOffset, 0); + } + + public SidewaysJumpThru(EntityData data, Vector2 offset) + : this(data.Position + offset, data.Height, !data.Bool("left"), data.Attr("texture", "default"), data.Float("animationDelay", 0f)) { + } + + public override void Awake(Scene scene) { + if (animationDelay > 0f) { + for (int i = 0; i < lines; i++) { + Sprite jumpthruSprite = new Sprite(GFX.Game, "objects/jumpthru/" + overrideTexture); + jumpthruSprite.AddLoop("idle", "", animationDelay); + + jumpthruSprite.Y = i * 8; + jumpthruSprite.Rotation = (float) (Math.PI / 2); + if (AllowLeftToRight) + jumpthruSprite.X = 8; + else + jumpthruSprite.Scale.Y = -1; + + jumpthruSprite.Play("idle"); + Add(jumpthruSprite); + } + } else { + AreaData areaData = AreaData.Get(scene); + string jumpthru = areaData.Jumpthru; + if (!string.IsNullOrEmpty(overrideTexture) && !overrideTexture.Equals("default")) { + jumpthru = overrideTexture; + } + + MTexture mTexture = GFX.Game["objects/jumpthru/" + jumpthru]; + int num = mTexture.Width / 8; + for (int i = 0; i < lines; i++) { + int xTilePosition; + int yTilePosition; + if (i == 0) { + xTilePosition = 0; + yTilePosition = ((!CollideCheck(Position + new Vector2(0f, -1f))) ? 1 : 0); + } else if (i == lines - 1) { + xTilePosition = num - 1; + yTilePosition = ((!CollideCheck(Position + new Vector2(0f, 1f))) ? 1 : 0); + } else { + xTilePosition = 1 + Calc.Random.Next(num - 2); + yTilePosition = Calc.Random.Choose(0, 1); + } + Image image = new Image(mTexture.GetSubtexture(xTilePosition * 8, yTilePosition * 8, 8, 8)); + image.Y = i * 8; + image.Rotation = (float) (Math.PI / 2); + + if (AllowLeftToRight) + image.X = 8; + else + image.Scale.Y = -1; + + Add(image); + } + } + } + } +} diff --git a/Entities/SidewaysLava.cs b/Entities/SidewaysLava.cs index c59d8a2..935da81 100644 --- a/Entities/SidewaysLava.cs +++ b/Entities/SidewaysLava.cs @@ -1,378 +1,378 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// Mashup of vanilla RisingLava and SandwichLava, allowing for lava coming from the sides instead of from the top / bottom. - /// - /// Attributes: - /// - intro: if true, lava will be invisible until the player moves - /// - lavaMode: allows picking the lava direction (left to right, right to left, or sandwich) - /// - speedMultiplier: multiplies the vanilla speed for lava - /// - [CustomEntity("SpringCollab2020/SidewaysLava")] - public class SidewaysLava : Entity { - private static FieldInfo lavaBlockerTriggerEnabled = typeof(LavaBlockerTrigger).GetField("enabled", BindingFlags.NonPublic | BindingFlags.Instance); - - private enum LavaMode { - LeftToRight, RightToLeft, Sandwich - } - - private const float Speed = 30f; - - // if the player collides with one of those, lava should be forced into waiting. - private List lavaBlockerTriggers; - - // atrributes - private bool intro; - private LavaMode lavaMode; - private float speedMultiplier; - - // state keeping - private bool iceMode; - private bool waiting; - private float delay = 0f; - private float lerp; - - // sandwich-specific stuff - private bool sandwichLeaving = false; - private float sandwichTransitionStartX = 0; - private bool sandwichHasToSetPosition = false; - private bool sandwichTransferred = false; - - private SidewaysLavaRect leftRect; - private SidewaysLavaRect rightRect; - - private SoundSource loopSfx; - - public static Color[] Hot = new Color[3] { - Calc.HexToColor("ff8933"), - Calc.HexToColor("f25e29"), - Calc.HexToColor("d01c01") - }; - - public static Color[] Cold = new Color[3] { - Calc.HexToColor("33ffe7"), - Calc.HexToColor("4ca2eb"), - Calc.HexToColor("0151d0") - }; - - public SidewaysLava(bool intro, string lavaMode, float speedMultiplier) : this(new EntityData() { - Values = new Dictionary() { - { "intro", intro }, { "lavaMode", lavaMode }, { "speedMultiplier", speedMultiplier } - } - }, Vector2.Zero) { } - - public SidewaysLava(EntityData data, Vector2 offset) { - intro = data.Bool("intro", false); - lavaMode = data.Enum("lavaMode", LavaMode.LeftToRight); - speedMultiplier = data.Float("speedMultiplier", 1f); - - Depth = -1000000; - - if (lavaMode == LavaMode.LeftToRight) { - // one hitbox on the left. - Collider = new Hitbox(340f, 200f, -340f); - } else if (lavaMode == LavaMode.RightToLeft) { - // one hitbox on the right. - Collider = new Hitbox(340f, 200f, 320f); - } else { - // hitboxes on both sides, 280px apart. - Collider = new ColliderList(new Hitbox(340f, 200f, -340f), new Hitbox(340f, 200f, 280f)); - } - - Visible = false; - Add(new PlayerCollider(OnPlayer)); - Add(new CoreModeListener(OnChangeMode)); - Add(loopSfx = new SoundSource()); - - if (lavaMode != LavaMode.RightToLeft) { - // add the left lava rect, just off-screen (it is 340px wide) - Add(leftRect = new SidewaysLavaRect(340f, 200f, 4, SidewaysLavaRect.OnlyModes.OnlyLeft)); - leftRect.Position = new Vector2(-340f, 0f); - leftRect.SmallWaveAmplitude = 2f; - } - if (lavaMode != LavaMode.LeftToRight) { - // add the right lava rect, just off-screen (the screen is 320px wide) - Add(rightRect = new SidewaysLavaRect(340f, 200f, 4, SidewaysLavaRect.OnlyModes.OnlyRight)); - rightRect.Position = new Vector2(lavaMode == LavaMode.Sandwich ? 280f : 320f, 0f); - rightRect.SmallWaveAmplitude = 2f; - } - - if (lavaMode == LavaMode.Sandwich) { - // listen to transitions since we need the sandwich lava to deal smoothly with them. - Add(new TransitionListener { - OnOutBegin = () => { - sandwichTransitionStartX = X; - if (!sandwichTransferred) { - // the next screen has no sideways sandwich lava. so, just leave. - AddTag(Tags.TransitionUpdate); - sandwichLeaving = true; - Collidable = false; - Alarm.Set(this, 2f, () => RemoveSelf()); - } else { - sandwichTransferred = false; - - // look up for all lava blocker triggers in the next room. - lavaBlockerTriggers = Scene.Entities.OfType().ToList(); - } - }, - OnOut = progress => { - if (Scene != null) { - Level level = Scene as Level; - - // make sure the sandwich lava is following the transition. - Y = level.Camera.Y - 10f; - - if (!sandwichLeaving) { - // make the lava smoothly go back to 20px on each side. - X = MathHelper.Lerp(sandwichTransitionStartX, level.Camera.Left + 20f, progress); - } - } - if (progress > 0.95f && sandwichLeaving) { - // destroy the lava, since transition is almost done. - RemoveSelf(); - } - } - }); - } - } - - public override void Added(Scene scene) { - base.Added(scene); - - iceMode = (SceneAs().Session.CoreMode == Session.CoreModes.Cold); - loopSfx.Play("event:/game/09_core/rising_threat", "room_state", iceMode ? 1 : 0); - - if (lavaMode == LavaMode.LeftToRight) { - // make the lava off-screen by 16px. - X = SceneAs().Bounds.Left - 16; - // sound comes from the left side. - loopSfx.Position = new Vector2(0f, Height / 2f); - - } else if (lavaMode == LavaMode.RightToLeft) { - // same, except the lava is offset by 320px. That gives Right - 320 + 16 = Right - 304. - X = SceneAs().Bounds.Right - 304; - // sound comes from the right side. - loopSfx.Position = new Vector2(320f, Height / 2f); - - } else { - // the position should be set on the first Update call, in case the level starts with a room with lava in it - // and the camera doesn't really exist yet. - sandwichHasToSetPosition = true; - // sound comes from the middle. - loopSfx.Position = new Vector2(140f, Height / 2f); - } - - Y = SceneAs().Bounds.Top - 10; - - if (lavaMode == LavaMode.Sandwich) { - // check if another sandwich lava is already here. - List sandwichLavas = new List(Scene.Entities.FindAll() - .Where(lava => (lava as SidewaysLava).lavaMode == LavaMode.Sandwich)); - - bool didRemoveSelf = false; - if (sandwichLavas.Count >= 2) { - SidewaysLava otherLava = (sandwichLavas[0] == this) ? sandwichLavas[1] : sandwichLavas[0]; - if (!otherLava.sandwichLeaving) { - // just let the existing lava do the job. transfer settings to it. - otherLava.speedMultiplier = speedMultiplier; - otherLava.sandwichTransferred = true; - RemoveSelf(); - didRemoveSelf = true; - } - } - - if (!didRemoveSelf) { - // we should make ourselves persistent to handle transitions smoothly. - Tag = Tags.Persistent; - - if ((scene as Level).LastIntroType != Player.IntroTypes.Respawn) { - // both rects start from off-screen, and fade in. - leftRect.Position.X -= 60f; - rightRect.Position.X += 60f; - } else { - // start directly visible if we respawned in the room (likely from dying in it). - Visible = true; - } - } - } - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - if (intro || (Scene.Tracker.GetEntity()?.JustRespawned ?? false)) { - // wait for the player to move before starting. - waiting = true; - } - - if (intro) { - Visible = true; - } - - // look up for all lava blocker triggers in the room. - lavaBlockerTriggers = scene.Entities.OfType().ToList(); - } - - private void OnChangeMode(Session.CoreModes mode) { - iceMode = (mode == Session.CoreModes.Cold); - loopSfx.Param("room_state", iceMode ? 1 : 0); - } - - private void OnPlayer(Player player) { - int direction; // 1 if right lava was hit, -1 is left lava was hit. - if (lavaMode == LavaMode.LeftToRight) { - direction = -1; - } else if (lavaMode == LavaMode.RightToLeft) { - direction = 1; - } else { - // determine which side was hit depending on the player position. - direction = (player.X > X + rightRect.Position.X - 32f) ? 1 : -1; - } - - if (SaveData.Instance.Assists.Invincible) { - if (delay <= 0f) { - float from = X; - float to = X + 48f * direction; - player.Speed.X = -200f * direction; - - player.RefillDash(); - Tween.Set(this, Tween.TweenMode.Oneshot, 0.4f, Ease.CubeOut, delegate (Tween t) { - X = MathHelper.Lerp(from, to, t.Eased); - }); - delay = 0.5f; - loopSfx.Param("rising", 0f); - Audio.Play("event:/game/general/assist_screenbottom", player.Position); - } - } else { - player.Die(Vector2.UnitX * -direction); - } - } - - public override void Update() { - if (sandwichHasToSetPosition) { - sandwichHasToSetPosition = false; - - // should be 20px to the right, so that the right rect is at 300px and both rects have the same on-screen size (20px). - X = SceneAs().Camera.Left + 20f; - } - - delay -= Engine.DeltaTime; - Y = SceneAs().Camera.Y - 10f; - base.Update(); - Visible = true; - - Player player = Scene.Tracker.GetEntity(); - if (player != null) { - LavaBlockerTrigger collidedTrigger = lavaBlockerTriggers.Find(trigger => player.CollideCheck(trigger)); - - if (collidedTrigger != null && (bool) lavaBlockerTriggerEnabled.GetValue(collidedTrigger)) { - // player is in a lava blocker trigger and it is enabled; block the lava. - waiting = true; - } - } - - if (waiting) { - loopSfx.Param("rising", 0f); - - if (player == null || !player.JustRespawned) { - waiting = false; - } else { - // the sandwich lava fade in animation is not handled here. - if (lavaMode != LavaMode.Sandwich) { - float target; - if (lavaMode == LavaMode.LeftToRight) { - // stop 32px to the left of the player. - target = player.X - 32f; - } else { - // stop 32px to the right of the player. since lava is offset by 320px, that gives 320 - 32 = 288px. - target = player.X - 288f; - } - - if (!intro && player != null && player.JustRespawned) { - X = Calc.Approach(X, target, 32f * speedMultiplier * Engine.DeltaTime); - } - } - } - } else { - if (lavaMode != LavaMode.Sandwich) { - // this is the X position around which the speed factor will be set. At this position, speedFactor = 1. - float positionThreshold; - // the current lava position. - float currentPosition; - // the direction the lava moves at (1 = right, -1 = left). - int direction; - - if (lavaMode == LavaMode.LeftToRight) { - positionThreshold = SceneAs().Camera.Left + 21f; - // if lava is too far away, drag it in. - if (Right < positionThreshold - 96f) { - Right = positionThreshold - 96f; - } - currentPosition = Right; - direction = 1; - } else { - positionThreshold = SceneAs().Camera.Right - 21f; - // if lava is too far away, drag it in. - if (Left > positionThreshold + 96f) { - Left = positionThreshold + 96f; - } - - // note: positionThreshold and currentPosition are negative here because the direction is inversed. - positionThreshold *= -1; - currentPosition = -Left; - direction = -1; - } - - // those constants are just pulled from vanilla * 320 / 180, in an attempt to scale it for horizontal movement. - float speedFactor = (currentPosition > positionThreshold) ? - Calc.ClampedMap(currentPosition - positionThreshold, 0f, 56f, 1f, 0.5f) : - Calc.ClampedMap(currentPosition - positionThreshold, 0f, 170f, 1f, 2f); - - if (delay <= 0f) { - loopSfx.Param("rising", 1f); - X += Speed * speedFactor * speedMultiplier * direction * Engine.DeltaTime; - } - } else { - // sandwich lava moves at a constant speed depending on core mode. - int direction = iceMode ? -1 : 1; - loopSfx.Param("rising", 1f); - X += 20f * speedMultiplier * direction * Engine.DeltaTime; - } - } - - // lerp both lava rects when changing core mode. - lerp = Calc.Approach(lerp, iceMode ? 1 : 0, Engine.DeltaTime * 4f); - - if (leftRect != null) { - leftRect.SurfaceColor = Color.Lerp(Hot[0], Cold[0], lerp); - leftRect.EdgeColor = Color.Lerp(Hot[1], Cold[1], lerp); - leftRect.CenterColor = Color.Lerp(Hot[2], Cold[2], lerp); - leftRect.Spikey = lerp * 5f; - leftRect.UpdateMultiplier = (1f - lerp) * 2f; - leftRect.Fade = (iceMode ? 128 : 32); - } - - if (rightRect != null) { - rightRect.SurfaceColor = Color.Lerp(Hot[0], Cold[0], lerp); - rightRect.EdgeColor = Color.Lerp(Hot[1], Cold[1], lerp); - rightRect.CenterColor = Color.Lerp(Hot[2], Cold[2], lerp); - rightRect.Spikey = lerp * 5f; - rightRect.UpdateMultiplier = (1f - lerp) * 2f; - rightRect.Fade = (iceMode ? 128 : 32); - } - - if (lavaMode == LavaMode.Sandwich) { - // move lava rects towards their intended positions: -340 (0 - its width) for the left rect, 280 for the right rect. - // if leaving, move them away quickly instead. - leftRect.Position.X = Calc.Approach(leftRect.Position.X, -340 + (sandwichLeaving ? -512 : 0), (sandwichLeaving ? 512 : 64) * Engine.DeltaTime); - rightRect.Position.X = Calc.Approach(rightRect.Position.X, 280 + (sandwichLeaving ? 512 : 0), (sandwichLeaving ? 512 : 64) * Engine.DeltaTime); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// Mashup of vanilla RisingLava and SandwichLava, allowing for lava coming from the sides instead of from the top / bottom. + /// + /// Attributes: + /// - intro: if true, lava will be invisible until the player moves + /// - lavaMode: allows picking the lava direction (left to right, right to left, or sandwich) + /// - speedMultiplier: multiplies the vanilla speed for lava + /// + [CustomEntity("SpringCollab2020/SidewaysLava")] + public class SidewaysLava : Entity { + private static FieldInfo lavaBlockerTriggerEnabled = typeof(LavaBlockerTrigger).GetField("enabled", BindingFlags.NonPublic | BindingFlags.Instance); + + private enum LavaMode { + LeftToRight, RightToLeft, Sandwich + } + + private const float Speed = 30f; + + // if the player collides with one of those, lava should be forced into waiting. + private List lavaBlockerTriggers; + + // atrributes + private bool intro; + private LavaMode lavaMode; + private float speedMultiplier; + + // state keeping + private bool iceMode; + private bool waiting; + private float delay = 0f; + private float lerp; + + // sandwich-specific stuff + private bool sandwichLeaving = false; + private float sandwichTransitionStartX = 0; + private bool sandwichHasToSetPosition = false; + private bool sandwichTransferred = false; + + private SidewaysLavaRect leftRect; + private SidewaysLavaRect rightRect; + + private SoundSource loopSfx; + + public static Color[] Hot = new Color[3] { + Calc.HexToColor("ff8933"), + Calc.HexToColor("f25e29"), + Calc.HexToColor("d01c01") + }; + + public static Color[] Cold = new Color[3] { + Calc.HexToColor("33ffe7"), + Calc.HexToColor("4ca2eb"), + Calc.HexToColor("0151d0") + }; + + public SidewaysLava(bool intro, string lavaMode, float speedMultiplier) : this(new EntityData() { + Values = new Dictionary() { + { "intro", intro }, { "lavaMode", lavaMode }, { "speedMultiplier", speedMultiplier } + } + }, Vector2.Zero) { } + + public SidewaysLava(EntityData data, Vector2 offset) { + intro = data.Bool("intro", false); + lavaMode = data.Enum("lavaMode", LavaMode.LeftToRight); + speedMultiplier = data.Float("speedMultiplier", 1f); + + Depth = -1000000; + + if (lavaMode == LavaMode.LeftToRight) { + // one hitbox on the left. + Collider = new Hitbox(340f, 200f, -340f); + } else if (lavaMode == LavaMode.RightToLeft) { + // one hitbox on the right. + Collider = new Hitbox(340f, 200f, 320f); + } else { + // hitboxes on both sides, 280px apart. + Collider = new ColliderList(new Hitbox(340f, 200f, -340f), new Hitbox(340f, 200f, 280f)); + } + + Visible = false; + Add(new PlayerCollider(OnPlayer)); + Add(new CoreModeListener(OnChangeMode)); + Add(loopSfx = new SoundSource()); + + if (lavaMode != LavaMode.RightToLeft) { + // add the left lava rect, just off-screen (it is 340px wide) + Add(leftRect = new SidewaysLavaRect(340f, 200f, 4, SidewaysLavaRect.OnlyModes.OnlyLeft)); + leftRect.Position = new Vector2(-340f, 0f); + leftRect.SmallWaveAmplitude = 2f; + } + if (lavaMode != LavaMode.LeftToRight) { + // add the right lava rect, just off-screen (the screen is 320px wide) + Add(rightRect = new SidewaysLavaRect(340f, 200f, 4, SidewaysLavaRect.OnlyModes.OnlyRight)); + rightRect.Position = new Vector2(lavaMode == LavaMode.Sandwich ? 280f : 320f, 0f); + rightRect.SmallWaveAmplitude = 2f; + } + + if (lavaMode == LavaMode.Sandwich) { + // listen to transitions since we need the sandwich lava to deal smoothly with them. + Add(new TransitionListener { + OnOutBegin = () => { + sandwichTransitionStartX = X; + if (!sandwichTransferred) { + // the next screen has no sideways sandwich lava. so, just leave. + AddTag(Tags.TransitionUpdate); + sandwichLeaving = true; + Collidable = false; + Alarm.Set(this, 2f, () => RemoveSelf()); + } else { + sandwichTransferred = false; + + // look up for all lava blocker triggers in the next room. + lavaBlockerTriggers = Scene.Entities.OfType().ToList(); + } + }, + OnOut = progress => { + if (Scene != null) { + Level level = Scene as Level; + + // make sure the sandwich lava is following the transition. + Y = level.Camera.Y - 10f; + + if (!sandwichLeaving) { + // make the lava smoothly go back to 20px on each side. + X = MathHelper.Lerp(sandwichTransitionStartX, level.Camera.Left + 20f, progress); + } + } + if (progress > 0.95f && sandwichLeaving) { + // destroy the lava, since transition is almost done. + RemoveSelf(); + } + } + }); + } + } + + public override void Added(Scene scene) { + base.Added(scene); + + iceMode = (SceneAs().Session.CoreMode == Session.CoreModes.Cold); + loopSfx.Play("event:/game/09_core/rising_threat", "room_state", iceMode ? 1 : 0); + + if (lavaMode == LavaMode.LeftToRight) { + // make the lava off-screen by 16px. + X = SceneAs().Bounds.Left - 16; + // sound comes from the left side. + loopSfx.Position = new Vector2(0f, Height / 2f); + + } else if (lavaMode == LavaMode.RightToLeft) { + // same, except the lava is offset by 320px. That gives Right - 320 + 16 = Right - 304. + X = SceneAs().Bounds.Right - 304; + // sound comes from the right side. + loopSfx.Position = new Vector2(320f, Height / 2f); + + } else { + // the position should be set on the first Update call, in case the level starts with a room with lava in it + // and the camera doesn't really exist yet. + sandwichHasToSetPosition = true; + // sound comes from the middle. + loopSfx.Position = new Vector2(140f, Height / 2f); + } + + Y = SceneAs().Bounds.Top - 10; + + if (lavaMode == LavaMode.Sandwich) { + // check if another sandwich lava is already here. + List sandwichLavas = new List(Scene.Entities.FindAll() + .Where(lava => (lava as SidewaysLava).lavaMode == LavaMode.Sandwich)); + + bool didRemoveSelf = false; + if (sandwichLavas.Count >= 2) { + SidewaysLava otherLava = (sandwichLavas[0] == this) ? sandwichLavas[1] : sandwichLavas[0]; + if (!otherLava.sandwichLeaving) { + // just let the existing lava do the job. transfer settings to it. + otherLava.speedMultiplier = speedMultiplier; + otherLava.sandwichTransferred = true; + RemoveSelf(); + didRemoveSelf = true; + } + } + + if (!didRemoveSelf) { + // we should make ourselves persistent to handle transitions smoothly. + Tag = Tags.Persistent; + + if ((scene as Level).LastIntroType != Player.IntroTypes.Respawn) { + // both rects start from off-screen, and fade in. + leftRect.Position.X -= 60f; + rightRect.Position.X += 60f; + } else { + // start directly visible if we respawned in the room (likely from dying in it). + Visible = true; + } + } + } + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + if (intro || (Scene.Tracker.GetEntity()?.JustRespawned ?? false)) { + // wait for the player to move before starting. + waiting = true; + } + + if (intro) { + Visible = true; + } + + // look up for all lava blocker triggers in the room. + lavaBlockerTriggers = scene.Entities.OfType().ToList(); + } + + private void OnChangeMode(Session.CoreModes mode) { + iceMode = (mode == Session.CoreModes.Cold); + loopSfx.Param("room_state", iceMode ? 1 : 0); + } + + private void OnPlayer(Player player) { + int direction; // 1 if right lava was hit, -1 is left lava was hit. + if (lavaMode == LavaMode.LeftToRight) { + direction = -1; + } else if (lavaMode == LavaMode.RightToLeft) { + direction = 1; + } else { + // determine which side was hit depending on the player position. + direction = (player.X > X + rightRect.Position.X - 32f) ? 1 : -1; + } + + if (SaveData.Instance.Assists.Invincible) { + if (delay <= 0f) { + float from = X; + float to = X + 48f * direction; + player.Speed.X = -200f * direction; + + player.RefillDash(); + Tween.Set(this, Tween.TweenMode.Oneshot, 0.4f, Ease.CubeOut, delegate (Tween t) { + X = MathHelper.Lerp(from, to, t.Eased); + }); + delay = 0.5f; + loopSfx.Param("rising", 0f); + Audio.Play("event:/game/general/assist_screenbottom", player.Position); + } + } else { + player.Die(Vector2.UnitX * -direction); + } + } + + public override void Update() { + if (sandwichHasToSetPosition) { + sandwichHasToSetPosition = false; + + // should be 20px to the right, so that the right rect is at 300px and both rects have the same on-screen size (20px). + X = SceneAs().Camera.Left + 20f; + } + + delay -= Engine.DeltaTime; + Y = SceneAs().Camera.Y - 10f; + base.Update(); + Visible = true; + + Player player = Scene.Tracker.GetEntity(); + if (player != null) { + LavaBlockerTrigger collidedTrigger = lavaBlockerTriggers.Find(trigger => player.CollideCheck(trigger)); + + if (collidedTrigger != null && (bool) lavaBlockerTriggerEnabled.GetValue(collidedTrigger)) { + // player is in a lava blocker trigger and it is enabled; block the lava. + waiting = true; + } + } + + if (waiting) { + loopSfx.Param("rising", 0f); + + if (player == null || !player.JustRespawned) { + waiting = false; + } else { + // the sandwich lava fade in animation is not handled here. + if (lavaMode != LavaMode.Sandwich) { + float target; + if (lavaMode == LavaMode.LeftToRight) { + // stop 32px to the left of the player. + target = player.X - 32f; + } else { + // stop 32px to the right of the player. since lava is offset by 320px, that gives 320 - 32 = 288px. + target = player.X - 288f; + } + + if (!intro && player != null && player.JustRespawned) { + X = Calc.Approach(X, target, 32f * speedMultiplier * Engine.DeltaTime); + } + } + } + } else { + if (lavaMode != LavaMode.Sandwich) { + // this is the X position around which the speed factor will be set. At this position, speedFactor = 1. + float positionThreshold; + // the current lava position. + float currentPosition; + // the direction the lava moves at (1 = right, -1 = left). + int direction; + + if (lavaMode == LavaMode.LeftToRight) { + positionThreshold = SceneAs().Camera.Left + 21f; + // if lava is too far away, drag it in. + if (Right < positionThreshold - 96f) { + Right = positionThreshold - 96f; + } + currentPosition = Right; + direction = 1; + } else { + positionThreshold = SceneAs().Camera.Right - 21f; + // if lava is too far away, drag it in. + if (Left > positionThreshold + 96f) { + Left = positionThreshold + 96f; + } + + // note: positionThreshold and currentPosition are negative here because the direction is inversed. + positionThreshold *= -1; + currentPosition = -Left; + direction = -1; + } + + // those constants are just pulled from vanilla * 320 / 180, in an attempt to scale it for horizontal movement. + float speedFactor = (currentPosition > positionThreshold) ? + Calc.ClampedMap(currentPosition - positionThreshold, 0f, 56f, 1f, 0.5f) : + Calc.ClampedMap(currentPosition - positionThreshold, 0f, 170f, 1f, 2f); + + if (delay <= 0f) { + loopSfx.Param("rising", 1f); + X += Speed * speedFactor * speedMultiplier * direction * Engine.DeltaTime; + } + } else { + // sandwich lava moves at a constant speed depending on core mode. + int direction = iceMode ? -1 : 1; + loopSfx.Param("rising", 1f); + X += 20f * speedMultiplier * direction * Engine.DeltaTime; + } + } + + // lerp both lava rects when changing core mode. + lerp = Calc.Approach(lerp, iceMode ? 1 : 0, Engine.DeltaTime * 4f); + + if (leftRect != null) { + leftRect.SurfaceColor = Color.Lerp(Hot[0], Cold[0], lerp); + leftRect.EdgeColor = Color.Lerp(Hot[1], Cold[1], lerp); + leftRect.CenterColor = Color.Lerp(Hot[2], Cold[2], lerp); + leftRect.Spikey = lerp * 5f; + leftRect.UpdateMultiplier = (1f - lerp) * 2f; + leftRect.Fade = (iceMode ? 128 : 32); + } + + if (rightRect != null) { + rightRect.SurfaceColor = Color.Lerp(Hot[0], Cold[0], lerp); + rightRect.EdgeColor = Color.Lerp(Hot[1], Cold[1], lerp); + rightRect.CenterColor = Color.Lerp(Hot[2], Cold[2], lerp); + rightRect.Spikey = lerp * 5f; + rightRect.UpdateMultiplier = (1f - lerp) * 2f; + rightRect.Fade = (iceMode ? 128 : 32); + } + + if (lavaMode == LavaMode.Sandwich) { + // move lava rects towards their intended positions: -340 (0 - its width) for the left rect, 280 for the right rect. + // if leaving, move them away quickly instead. + leftRect.Position.X = Calc.Approach(leftRect.Position.X, -340 + (sandwichLeaving ? -512 : 0), (sandwichLeaving ? 512 : 64) * Engine.DeltaTime); + rightRect.Position.X = Calc.Approach(rightRect.Position.X, 280 + (sandwichLeaving ? 512 : 0), (sandwichLeaving ? 512 : 64) * Engine.DeltaTime); + } + } + } +} diff --git a/Entities/SidewaysLavaRect.cs b/Entities/SidewaysLavaRect.cs index 2ec7c3e..d1799d9 100644 --- a/Entities/SidewaysLavaRect.cs +++ b/Entities/SidewaysLavaRect.cs @@ -1,288 +1,288 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using Monocle; -using System; -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020.Entities { - // Strongly based on vanilla LavaRect, except their upper side is left or right, and bubbles go sideways. - class SidewaysLavaRect : Component { - public enum OnlyModes { - OnlyLeft, - OnlyRight - } - - private struct Bubble { - public Vector2 Position; - public float Speed; - public float Alpha; - } - - private struct SurfaceBubble { - public float Y; - public float Frame; - public byte Animation; - } - - public Vector2 Position; - - public float Fade = 16f; - public float Spikey = 0f; - - public OnlyModes OnlyMode; - - public float SmallWaveAmplitude = 1f; - public float BigWaveAmplitude = 4f; - public float CurveAmplitude = 7f; - public float UpdateMultiplier = 1f; - - public Color SurfaceColor = Color.White; - public Color EdgeColor = Color.LightGray; - public Color CenterColor = Color.DarkGray; - - private float timer = Calc.Random.NextFloat(100f); - - private VertexPositionColor[] verts; - private int vertCount; - - private bool dirty; - - private Bubble[] bubbles; - private SurfaceBubble[] surfaceBubbles; - private int surfaceBubbleIndex; - private List> surfaceBubbleAnimations; - - public int SurfaceStep { - get; - private set; - } - - public float Width { - get; - private set; - } - - public float Height { - get; - private set; - } - - public SidewaysLavaRect(float width, float height, int step, OnlyModes onlyMode) - : base(active: true, visible: true) { - - OnlyMode = onlyMode; - - Resize(width, height, step); - } - - public void Resize(float width, float height, int step) { - Width = width; - Height = height; - SurfaceStep = step; - dirty = true; - - int surroundingSize = (int) (width / SurfaceStep * 2f + height / SurfaceStep * 2f + 4f); - verts = new VertexPositionColor[surroundingSize * 3 * 6 + 6]; - - // spread bubbles in the LavaRect. - bubbles = new Bubble[(int) (width * height * 0.005f)]; - surfaceBubbles = new SurfaceBubble[(int) Math.Max(4f, bubbles.Length * 0.25f)]; - for (int i = 0; i < bubbles.Length; i++) { - bubbles[i].Position = new Vector2(1f + Calc.Random.NextFloat(Width - 2f), Calc.Random.NextFloat(Height)); - bubbles[i].Speed = Calc.Random.Range(4, 12); - bubbles[i].Alpha = Calc.Random.Range(0.4f, 0.8f); - } - // initialize empty structures for surface bubbles. - for (int j = 0; j < surfaceBubbles.Length; j++) { - surfaceBubbles[j].Y = -1f; - } - surfaceBubbleAnimations = new List>(); - surfaceBubbleAnimations.Add(GFX.Game.GetAtlasSubtextures(OnlyMode == OnlyModes.OnlyLeft ? - "danger/SpringCollab2020/sidewayslava/bubble_right_a" : "danger/SpringCollab2020/sidewayslava/bubble_left_a")); - } - - public override void Update() { - timer += UpdateMultiplier * Engine.DeltaTime; - if (UpdateMultiplier != 0f) { - dirty = true; - } - - // make bubbles bubble "up" (actually right). - for (int i = 0; i < bubbles.Length; i++) { - bool limitReached = false; - - if (OnlyMode == OnlyModes.OnlyLeft) { - bubbles[i].Position.X += UpdateMultiplier * bubbles[i].Speed * Engine.DeltaTime; - if (bubbles[i].Position.X > Width - 2f + Wave((int) (bubbles[i].Position.Y / SurfaceStep), Height)) { - // bubble reached the surface. reset it - bubbles[i].Position.X = 1f; - limitReached = true; - } - } else { - bubbles[i].Position.X -= UpdateMultiplier * bubbles[i].Speed * Engine.DeltaTime; - if (bubbles[i].Position.X < 2f - Wave((int) (bubbles[i].Position.Y / SurfaceStep), Height)) { - // bubble reached the surface. reset it - bubbles[i].Position.X = Width - 1f; - limitReached = true; - } - } - - if (limitReached) { - if (Calc.Random.Chance(0.75f)) { - // we want the bubble to explode at the surface. - surfaceBubbles[surfaceBubbleIndex].Y = bubbles[i].Position.Y; - surfaceBubbles[surfaceBubbleIndex].Frame = 0f; - surfaceBubbles[surfaceBubbleIndex].Animation = (byte) Calc.Random.Next(surfaceBubbleAnimations.Count); - surfaceBubbleIndex = (surfaceBubbleIndex + 1) % surfaceBubbles.Length; - } - } - } - - // make surface bubbles animations advance. - for (int j = 0; j < surfaceBubbles.Length; j++) { - if (surfaceBubbles[j].Y >= 0f) { - surfaceBubbles[j].Frame += Engine.DeltaTime * 6f; - if (surfaceBubbles[j].Frame >= surfaceBubbleAnimations[surfaceBubbles[j].Animation].Count) { - // animation is over: clean up! - surfaceBubbles[j].Y = -1f; - } - } - } - - base.Update(); - } - - private float Sin(float value) { - return (1f + (float) Math.Sin(value)) / 2f; - } - - private float Wave(int step, float length) { - int stepOffset = step * SurfaceStep; - float waveOffset = Sin(stepOffset * 0.25f + timer * 4f) * SmallWaveAmplitude; - waveOffset += Sin(stepOffset * 0.05f + timer * 0.5f) * BigWaveAmplitude; - if (step % 2 == 0) { - waveOffset += Spikey; - } - waveOffset += (1f - Calc.YoYo(stepOffset / length)) * CurveAmplitude; - return waveOffset; - } - - private void Quad(ref int vert, Vector2 va, Vector2 vb, Vector2 vc, Vector2 vd, Color color) { - Quad(ref vert, va, color, vb, color, vc, color, vd, color); - } - - private void Quad(ref int vert, Vector2 va, Color ca, Vector2 vb, Color cb, Vector2 vc, Color cc, Vector2 vd, Color cd) { - verts[vert].Position.X = va.X; - verts[vert].Position.Y = va.Y; - verts[vert++].Color = ca; - verts[vert].Position.X = vb.X; - verts[vert].Position.Y = vb.Y; - verts[vert++].Color = cb; - verts[vert].Position.X = vc.X; - verts[vert].Position.Y = vc.Y; - verts[vert++].Color = cc; - verts[vert].Position.X = va.X; - verts[vert].Position.Y = va.Y; - verts[vert++].Color = ca; - verts[vert].Position.X = vc.X; - verts[vert].Position.Y = vc.Y; - verts[vert++].Color = cc; - verts[vert].Position.X = vd.X; - verts[vert].Position.Y = vd.Y; - verts[vert++].Color = cd; - } - - private void Edge(ref int vert, Vector2 a, Vector2 b, float fade) { - float edgeSize = (a - b).Length(); - float stepCount = edgeSize / SurfaceStep; - Vector2 direction = (b - a).SafeNormalize(); - Vector2 perpendicular = direction.Perpendicular(); - for (int i = 1; i <= stepCount; i++) { - // values for the previous position - Vector2 positionBefore = Vector2.Lerp(a, b, (i - 1) / stepCount); - float waveOffsetBefore = Wave(i - 1, edgeSize); - Vector2 wavePositionBefore = positionBefore - perpendicular * waveOffsetBefore; - - // values for the current position - Vector2 position = Vector2.Lerp(a, b, i / stepCount); - float waveOffset = Wave(i, edgeSize); - Vector2 wavePosition = position - perpendicular * waveOffset; - - Quad(ref vert, wavePositionBefore + perpendicular, EdgeColor, - wavePosition + perpendicular, EdgeColor, - position + perpendicular * (fade - waveOffset), CenterColor, - positionBefore + perpendicular * (fade - waveOffsetBefore), CenterColor); - - Quad(ref vert, positionBefore + perpendicular * (fade - waveOffsetBefore), - position + perpendicular * (fade - waveOffset), - position + perpendicular * fade, - positionBefore + perpendicular * fade, - CenterColor); - - Quad(ref vert, wavePositionBefore, - wavePosition, - wavePosition + perpendicular, - wavePositionBefore + perpendicular * 1f, - SurfaceColor); - } - } - - public override void Render() { - GameplayRenderer.End(); - - // render the edges of the lava rect. - if (dirty) { - Vector2 zero = Vector2.Zero; - - Vector2 topLeft = zero; - Vector2 topRight = new Vector2(zero.X + Width, zero.Y); - Vector2 bottomLeft = new Vector2(zero.X, zero.Y + Height); - Vector2 bottomRight = zero + new Vector2(Width, Height); - - Vector2 fadeOffset = new Vector2(Math.Min(Fade, Width / 2f), Math.Min(Fade, Height / 2f)); - vertCount = 0; - if (OnlyMode == OnlyModes.OnlyLeft) { - Edge(ref vertCount, topRight, bottomRight, fadeOffset.X); - Quad(ref vertCount, topLeft, topRight - new Vector2(fadeOffset.X, 0f), bottomRight - new Vector2(fadeOffset.X, 0f), bottomLeft, CenterColor); - } else if (OnlyMode == OnlyModes.OnlyRight) { - Edge(ref vertCount, bottomLeft, topLeft, fadeOffset.X); - Quad(ref vertCount, topLeft + new Vector2(fadeOffset.X, 0f), topRight, bottomRight, bottomLeft + new Vector2(fadeOffset.X, 0f), CenterColor); - } - - dirty = false; - } - - // render the vertices - Camera camera = (Scene as Level).Camera; - GFX.DrawVertices(Matrix.CreateTranslation(new Vector3(Entity.Position + Position, 0f)) * camera.Matrix, verts, vertCount); - - GameplayRenderer.Begin(); - - Vector2 rectPosition = Entity.Position + Position; - - // render bubbles - MTexture bubbleTexture = GFX.Game["particles/bubble"]; - for (int i = 0; i < bubbles.Length; i++) { - bubbleTexture.DrawCentered(rectPosition + bubbles[i].Position, SurfaceColor * bubbles[i].Alpha); - } - - // render surface bubbles - for (int j = 0; j < surfaceBubbles.Length; j++) { - if (surfaceBubbles[j].Y >= 0f) { - MTexture surfaceBubbleTexture = surfaceBubbleAnimations[surfaceBubbles[j].Animation][(int) surfaceBubbles[j].Frame]; - int bubbleOffset = (int) (surfaceBubbles[j].Y / SurfaceStep); - - float x; - if (OnlyMode == OnlyModes.OnlyLeft) - x = Width + 7f + Wave(bubbleOffset, Height); - else - x = 1f - Wave(bubbleOffset, Height); - - surfaceBubbleTexture.DrawJustified(rectPosition + - new Vector2(x, OnlyMode == OnlyModes.OnlyRight ? Height - bubbleOffset * SurfaceStep : bubbleOffset * SurfaceStep), - new Vector2(1f, 0.5f), SurfaceColor); - } - } - } - } -} +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Monocle; +using System; +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020.Entities { + // Strongly based on vanilla LavaRect, except their upper side is left or right, and bubbles go sideways. + class SidewaysLavaRect : Component { + public enum OnlyModes { + OnlyLeft, + OnlyRight + } + + private struct Bubble { + public Vector2 Position; + public float Speed; + public float Alpha; + } + + private struct SurfaceBubble { + public float Y; + public float Frame; + public byte Animation; + } + + public Vector2 Position; + + public float Fade = 16f; + public float Spikey = 0f; + + public OnlyModes OnlyMode; + + public float SmallWaveAmplitude = 1f; + public float BigWaveAmplitude = 4f; + public float CurveAmplitude = 7f; + public float UpdateMultiplier = 1f; + + public Color SurfaceColor = Color.White; + public Color EdgeColor = Color.LightGray; + public Color CenterColor = Color.DarkGray; + + private float timer = Calc.Random.NextFloat(100f); + + private VertexPositionColor[] verts; + private int vertCount; + + private bool dirty; + + private Bubble[] bubbles; + private SurfaceBubble[] surfaceBubbles; + private int surfaceBubbleIndex; + private List> surfaceBubbleAnimations; + + public int SurfaceStep { + get; + private set; + } + + public float Width { + get; + private set; + } + + public float Height { + get; + private set; + } + + public SidewaysLavaRect(float width, float height, int step, OnlyModes onlyMode) + : base(active: true, visible: true) { + + OnlyMode = onlyMode; + + Resize(width, height, step); + } + + public void Resize(float width, float height, int step) { + Width = width; + Height = height; + SurfaceStep = step; + dirty = true; + + int surroundingSize = (int) (width / SurfaceStep * 2f + height / SurfaceStep * 2f + 4f); + verts = new VertexPositionColor[surroundingSize * 3 * 6 + 6]; + + // spread bubbles in the LavaRect. + bubbles = new Bubble[(int) (width * height * 0.005f)]; + surfaceBubbles = new SurfaceBubble[(int) Math.Max(4f, bubbles.Length * 0.25f)]; + for (int i = 0; i < bubbles.Length; i++) { + bubbles[i].Position = new Vector2(1f + Calc.Random.NextFloat(Width - 2f), Calc.Random.NextFloat(Height)); + bubbles[i].Speed = Calc.Random.Range(4, 12); + bubbles[i].Alpha = Calc.Random.Range(0.4f, 0.8f); + } + // initialize empty structures for surface bubbles. + for (int j = 0; j < surfaceBubbles.Length; j++) { + surfaceBubbles[j].Y = -1f; + } + surfaceBubbleAnimations = new List>(); + surfaceBubbleAnimations.Add(GFX.Game.GetAtlasSubtextures(OnlyMode == OnlyModes.OnlyLeft ? + "danger/SpringCollab2020/sidewayslava/bubble_right_a" : "danger/SpringCollab2020/sidewayslava/bubble_left_a")); + } + + public override void Update() { + timer += UpdateMultiplier * Engine.DeltaTime; + if (UpdateMultiplier != 0f) { + dirty = true; + } + + // make bubbles bubble "up" (actually right). + for (int i = 0; i < bubbles.Length; i++) { + bool limitReached = false; + + if (OnlyMode == OnlyModes.OnlyLeft) { + bubbles[i].Position.X += UpdateMultiplier * bubbles[i].Speed * Engine.DeltaTime; + if (bubbles[i].Position.X > Width - 2f + Wave((int) (bubbles[i].Position.Y / SurfaceStep), Height)) { + // bubble reached the surface. reset it + bubbles[i].Position.X = 1f; + limitReached = true; + } + } else { + bubbles[i].Position.X -= UpdateMultiplier * bubbles[i].Speed * Engine.DeltaTime; + if (bubbles[i].Position.X < 2f - Wave((int) (bubbles[i].Position.Y / SurfaceStep), Height)) { + // bubble reached the surface. reset it + bubbles[i].Position.X = Width - 1f; + limitReached = true; + } + } + + if (limitReached) { + if (Calc.Random.Chance(0.75f)) { + // we want the bubble to explode at the surface. + surfaceBubbles[surfaceBubbleIndex].Y = bubbles[i].Position.Y; + surfaceBubbles[surfaceBubbleIndex].Frame = 0f; + surfaceBubbles[surfaceBubbleIndex].Animation = (byte) Calc.Random.Next(surfaceBubbleAnimations.Count); + surfaceBubbleIndex = (surfaceBubbleIndex + 1) % surfaceBubbles.Length; + } + } + } + + // make surface bubbles animations advance. + for (int j = 0; j < surfaceBubbles.Length; j++) { + if (surfaceBubbles[j].Y >= 0f) { + surfaceBubbles[j].Frame += Engine.DeltaTime * 6f; + if (surfaceBubbles[j].Frame >= surfaceBubbleAnimations[surfaceBubbles[j].Animation].Count) { + // animation is over: clean up! + surfaceBubbles[j].Y = -1f; + } + } + } + + base.Update(); + } + + private float Sin(float value) { + return (1f + (float) Math.Sin(value)) / 2f; + } + + private float Wave(int step, float length) { + int stepOffset = step * SurfaceStep; + float waveOffset = Sin(stepOffset * 0.25f + timer * 4f) * SmallWaveAmplitude; + waveOffset += Sin(stepOffset * 0.05f + timer * 0.5f) * BigWaveAmplitude; + if (step % 2 == 0) { + waveOffset += Spikey; + } + waveOffset += (1f - Calc.YoYo(stepOffset / length)) * CurveAmplitude; + return waveOffset; + } + + private void Quad(ref int vert, Vector2 va, Vector2 vb, Vector2 vc, Vector2 vd, Color color) { + Quad(ref vert, va, color, vb, color, vc, color, vd, color); + } + + private void Quad(ref int vert, Vector2 va, Color ca, Vector2 vb, Color cb, Vector2 vc, Color cc, Vector2 vd, Color cd) { + verts[vert].Position.X = va.X; + verts[vert].Position.Y = va.Y; + verts[vert++].Color = ca; + verts[vert].Position.X = vb.X; + verts[vert].Position.Y = vb.Y; + verts[vert++].Color = cb; + verts[vert].Position.X = vc.X; + verts[vert].Position.Y = vc.Y; + verts[vert++].Color = cc; + verts[vert].Position.X = va.X; + verts[vert].Position.Y = va.Y; + verts[vert++].Color = ca; + verts[vert].Position.X = vc.X; + verts[vert].Position.Y = vc.Y; + verts[vert++].Color = cc; + verts[vert].Position.X = vd.X; + verts[vert].Position.Y = vd.Y; + verts[vert++].Color = cd; + } + + private void Edge(ref int vert, Vector2 a, Vector2 b, float fade) { + float edgeSize = (a - b).Length(); + float stepCount = edgeSize / SurfaceStep; + Vector2 direction = (b - a).SafeNormalize(); + Vector2 perpendicular = direction.Perpendicular(); + for (int i = 1; i <= stepCount; i++) { + // values for the previous position + Vector2 positionBefore = Vector2.Lerp(a, b, (i - 1) / stepCount); + float waveOffsetBefore = Wave(i - 1, edgeSize); + Vector2 wavePositionBefore = positionBefore - perpendicular * waveOffsetBefore; + + // values for the current position + Vector2 position = Vector2.Lerp(a, b, i / stepCount); + float waveOffset = Wave(i, edgeSize); + Vector2 wavePosition = position - perpendicular * waveOffset; + + Quad(ref vert, wavePositionBefore + perpendicular, EdgeColor, + wavePosition + perpendicular, EdgeColor, + position + perpendicular * (fade - waveOffset), CenterColor, + positionBefore + perpendicular * (fade - waveOffsetBefore), CenterColor); + + Quad(ref vert, positionBefore + perpendicular * (fade - waveOffsetBefore), + position + perpendicular * (fade - waveOffset), + position + perpendicular * fade, + positionBefore + perpendicular * fade, + CenterColor); + + Quad(ref vert, wavePositionBefore, + wavePosition, + wavePosition + perpendicular, + wavePositionBefore + perpendicular * 1f, + SurfaceColor); + } + } + + public override void Render() { + GameplayRenderer.End(); + + // render the edges of the lava rect. + if (dirty) { + Vector2 zero = Vector2.Zero; + + Vector2 topLeft = zero; + Vector2 topRight = new Vector2(zero.X + Width, zero.Y); + Vector2 bottomLeft = new Vector2(zero.X, zero.Y + Height); + Vector2 bottomRight = zero + new Vector2(Width, Height); + + Vector2 fadeOffset = new Vector2(Math.Min(Fade, Width / 2f), Math.Min(Fade, Height / 2f)); + vertCount = 0; + if (OnlyMode == OnlyModes.OnlyLeft) { + Edge(ref vertCount, topRight, bottomRight, fadeOffset.X); + Quad(ref vertCount, topLeft, topRight - new Vector2(fadeOffset.X, 0f), bottomRight - new Vector2(fadeOffset.X, 0f), bottomLeft, CenterColor); + } else if (OnlyMode == OnlyModes.OnlyRight) { + Edge(ref vertCount, bottomLeft, topLeft, fadeOffset.X); + Quad(ref vertCount, topLeft + new Vector2(fadeOffset.X, 0f), topRight, bottomRight, bottomLeft + new Vector2(fadeOffset.X, 0f), CenterColor); + } + + dirty = false; + } + + // render the vertices + Camera camera = (Scene as Level).Camera; + GFX.DrawVertices(Matrix.CreateTranslation(new Vector3(Entity.Position + Position, 0f)) * camera.Matrix, verts, vertCount); + + GameplayRenderer.Begin(); + + Vector2 rectPosition = Entity.Position + Position; + + // render bubbles + MTexture bubbleTexture = GFX.Game["particles/bubble"]; + for (int i = 0; i < bubbles.Length; i++) { + bubbleTexture.DrawCentered(rectPosition + bubbles[i].Position, SurfaceColor * bubbles[i].Alpha); + } + + // render surface bubbles + for (int j = 0; j < surfaceBubbles.Length; j++) { + if (surfaceBubbles[j].Y >= 0f) { + MTexture surfaceBubbleTexture = surfaceBubbleAnimations[surfaceBubbles[j].Animation][(int) surfaceBubbles[j].Frame]; + int bubbleOffset = (int) (surfaceBubbles[j].Y / SurfaceStep); + + float x; + if (OnlyMode == OnlyModes.OnlyLeft) + x = Width + 7f + Wave(bubbleOffset, Height); + else + x = 1f - Wave(bubbleOffset, Height); + + surfaceBubbleTexture.DrawJustified(rectPosition + + new Vector2(x, OnlyMode == OnlyModes.OnlyRight ? Height - bubbleOffset * SurfaceStep : bubbleOffset * SurfaceStep), + new Vector2(1f, 0.5f), SurfaceColor); + } + } + } + } +} diff --git a/Entities/SpikeJumpThroughController.cs b/Entities/SpikeJumpThroughController.cs index 6a44209..38b964d 100644 --- a/Entities/SpikeJumpThroughController.cs +++ b/Entities/SpikeJumpThroughController.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Monocle; using Celeste.Mod.SpringCollab2020; using Celeste.Mod.Entities; diff --git a/Entities/StaticPuffer.cs b/Entities/StaticPuffer.cs index 5070611..28bfca2 100644 --- a/Entities/StaticPuffer.cs +++ b/Entities/StaticPuffer.cs @@ -1,48 +1,48 @@ -using Celeste.Mod.Entities; -using Monocle; -using Microsoft.Xna.Framework; -using MonoMod.Cil; -using System; -using Mono.Cecil.Cil; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/StaticPuffer")] - class StaticPuffer : Puffer { - public static void Load() { - IL.Celeste.Puffer.ctor_Vector2_bool += onPufferConstructor; - } - - public static void Unload() { - IL.Celeste.Puffer.ctor_Vector2_bool -= onPufferConstructor; - } - - private static void onPufferConstructor(ILContext il) { - ILCursor cursor = new ILCursor(il); - - while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("Randomize"))) { - Logger.Log("SpringCollab2020/StaticPuffer", $"Injecting call to unrandomize puffer sine wave at {cursor.Index} in IL for Puffer constructor"); - - cursor.Emit(OpCodes.Ldarg_0); - cursor.Emit(OpCodes.Ldarg_0); - cursor.Emit(OpCodes.Ldfld, typeof(Puffer).GetField("idleSine", BindingFlags.NonPublic | BindingFlags.Instance)); - cursor.EmitDelegate>((self, idleSine) => { - if (self is StaticPuffer) { - // unrandomize the initial pufferfish position. - idleSine.Reset(); - } - }); - } - } - - public StaticPuffer(EntityData data, Vector2 offset) : base(data, offset) { - // remove the sine wave component so that it isn't updated. - Get()?.RemoveSelf(); - - // offset the horizontal position by a tiny bit. - // Vanilla puffers have a non-integer position (due to the randomized offset), making it impossible to be boosted downwards, - // so we want to do the same. - Position.X += 0.0001f; - } - } -} +using Celeste.Mod.Entities; +using Monocle; +using Microsoft.Xna.Framework; +using MonoMod.Cil; +using System; +using Mono.Cecil.Cil; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/StaticPuffer")] + class StaticPuffer : Puffer { + public static void Load() { + IL.Celeste.Puffer.ctor_Vector2_bool += onPufferConstructor; + } + + public static void Unload() { + IL.Celeste.Puffer.ctor_Vector2_bool -= onPufferConstructor; + } + + private static void onPufferConstructor(ILContext il) { + ILCursor cursor = new ILCursor(il); + + while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("Randomize"))) { + Logger.Log("SpringCollab2020/StaticPuffer", $"Injecting call to unrandomize puffer sine wave at {cursor.Index} in IL for Puffer constructor"); + + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Ldfld, typeof(Puffer).GetField("idleSine", BindingFlags.NonPublic | BindingFlags.Instance)); + cursor.EmitDelegate>((self, idleSine) => { + if (self is StaticPuffer) { + // unrandomize the initial pufferfish position. + idleSine.Reset(); + } + }); + } + } + + public StaticPuffer(EntityData data, Vector2 offset) : base(data, offset) { + // remove the sine wave component so that it isn't updated. + Get()?.RemoveSelf(); + + // offset the horizontal position by a tiny bit. + // Vanilla puffers have a non-integer position (due to the randomized offset), making it impossible to be boosted downwards, + // so we want to do the same. + Position.X += 0.0001f; + } + } +} diff --git a/Entities/StrawberryIgnoringLighting.cs b/Entities/StrawberryIgnoringLighting.cs index a52231b..f6c0279 100644 --- a/Entities/StrawberryIgnoringLighting.cs +++ b/Entities/StrawberryIgnoringLighting.cs @@ -1,56 +1,56 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using Monocle; -using MonoMod.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Celeste.Mod.SpringCollab2020.Entities { - /// - /// This class adds a custom attribute to vanilla strawberries. This is not a new entity. - /// - class StrawberryIgnoringLighting { - private static MTexture strawberryCutoutTexture; - - public static void Load() { - On.Celeste.Strawberry.ctor += onStrawberryConstructor; - On.Celeste.LightingRenderer.BeforeRender += onLightingBeforeRender; - } - - public static void LoadContent() { - strawberryCutoutTexture = GFX.Game["collectables/SpringCollab2020/strawberry/cutout"]; - } - - public static void Unload() { - On.Celeste.Strawberry.ctor -= onStrawberryConstructor; - On.Celeste.LightingRenderer.BeforeRender -= onLightingBeforeRender; - } - - private static void onStrawberryConstructor(On.Celeste.Strawberry.orig_ctor orig, Strawberry self, EntityData data, Vector2 offset, EntityID gid) { - orig(self, data, offset, gid); - - // save the value for SpringCollab2020_ignoreLighting to the DynData for the strawberry. - new DynData(self)["SpringCollab2020_ignoreLighting"] = data.Bool("SpringCollab2020_ignoreLighting"); - } - - private static void onLightingBeforeRender(On.Celeste.LightingRenderer.orig_BeforeRender orig, LightingRenderer self, Scene scene) { - orig(self, scene); - - Draw.SpriteBatch.GraphicsDevice.SetRenderTarget(GameplayBuffers.Light); - Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, null, Matrix.Identity); - foreach (Entity entity in scene.Entities) { - if (entity is Strawberry berry) { - DynData berryData = new DynData(berry); - if (berryData.Get("SpringCollab2020_ignoreLighting")) { - Draw.SpriteBatch.Draw(strawberryCutoutTexture.Texture.Texture, berry.Position + berryData.Get("sprite").Position - (scene as Level).Camera.Position - - new Vector2(9, 8), Color.White); - } - } - } - Draw.SpriteBatch.End(); - } - } -} +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Monocle; +using MonoMod.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Celeste.Mod.SpringCollab2020.Entities { + /// + /// This class adds a custom attribute to vanilla strawberries. This is not a new entity. + /// + class StrawberryIgnoringLighting { + private static MTexture strawberryCutoutTexture; + + public static void Load() { + On.Celeste.Strawberry.ctor += onStrawberryConstructor; + On.Celeste.LightingRenderer.BeforeRender += onLightingBeforeRender; + } + + public static void LoadContent() { + strawberryCutoutTexture = GFX.Game["collectables/SpringCollab2020/strawberry/cutout"]; + } + + public static void Unload() { + On.Celeste.Strawberry.ctor -= onStrawberryConstructor; + On.Celeste.LightingRenderer.BeforeRender -= onLightingBeforeRender; + } + + private static void onStrawberryConstructor(On.Celeste.Strawberry.orig_ctor orig, Strawberry self, EntityData data, Vector2 offset, EntityID gid) { + orig(self, data, offset, gid); + + // save the value for SpringCollab2020_ignoreLighting to the DynData for the strawberry. + new DynData(self)["SpringCollab2020_ignoreLighting"] = data.Bool("SpringCollab2020_ignoreLighting"); + } + + private static void onLightingBeforeRender(On.Celeste.LightingRenderer.orig_BeforeRender orig, LightingRenderer self, Scene scene) { + orig(self, scene); + + Draw.SpriteBatch.GraphicsDevice.SetRenderTarget(GameplayBuffers.Light); + Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, null, Matrix.Identity); + foreach (Entity entity in scene.Entities) { + if (entity is Strawberry berry) { + DynData berryData = new DynData(berry); + if (berryData.Get("SpringCollab2020_ignoreLighting")) { + Draw.SpriteBatch.Draw(strawberryCutoutTexture.Texture.Texture, berry.Position + berryData.Get("sprite").Position - (scene as Level).Camera.Position + - new Vector2(9, 8), Color.White); + } + } + } + Draw.SpriteBatch.End(); + } + } +} diff --git a/Entities/TrollStrawberry.cs b/Entities/TrollStrawberry.cs index 68fecb1..2e1500f 100644 --- a/Entities/TrollStrawberry.cs +++ b/Entities/TrollStrawberry.cs @@ -1,10 +1,10 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; using System; using System.Collections; -namespace Celeste.Mod.SpringCollab2020.Entities { +namespace Celeste.Mod.SpringCollab2020.Entities { [CustomEntity("SpringCollab2020/trollStrawberry")] public class TrollStrawberry : Entity { @@ -28,8 +28,8 @@ public class TrollStrawberry : Entity { private Vector2 start; - private float collectTimer; - + private float collectTimer; + private bool collected; private bool flyingAway; @@ -72,7 +72,7 @@ public TrollStrawberry(EntityData data, Vector2 offset, EntityID gid) { } public override void Added(Scene scene) { - base.Added(scene); + base.Added(scene); Add(sprite = GFX.SpriteBank.Create("strawberry")); if (Winged) { sprite.Play("flap", false, false); @@ -192,84 +192,84 @@ public void OnPlayer(Player player) { } public void OnCollect() { - if (collected) { - return; - } - int collectIndex = 0; - collected = true; - if (Follower.Leader != null) { - Player p = Follower.Leader.Entity as Player; - collectIndex = p.StrawberryCollectIndex; - p.StrawberryCollectIndex++; - p.StrawberryCollectResetTimer = 2.5f; - Follower.Leader.LoseFollower(Follower); - } - Session session = (Scene as Level).Session; - session.DoNotLoad.Add(ID); - session.UpdateLevelStartDashes(); + if (collected) { + return; + } + int collectIndex = 0; + collected = true; + if (Follower.Leader != null) { + Player p = Follower.Leader.Entity as Player; + collectIndex = p.StrawberryCollectIndex; + p.StrawberryCollectIndex++; + p.StrawberryCollectResetTimer = 2.5f; + Follower.Leader.LoseFollower(Follower); + } + Session session = (Scene as Level).Session; + session.DoNotLoad.Add(ID); + session.UpdateLevelStartDashes(); Add(new Coroutine(CollectRoutine(collectIndex), true)); } - private IEnumerator FlyAwayRoutine() { - rotateWiggler.Start(); - flapSpeed = -200f; - Tween tween = Tween.Create(Tween.TweenMode.Oneshot, Ease.CubeOut, 0.25f, start: true); - tween.OnUpdate = delegate (Tween t) { - flapSpeed = MathHelper.Lerp(-200f, 0f, t.Eased); - }; - Add(tween); - yield return 0.1f; - Audio.Play("event:/game/general/strawberry_laugh", Position); - yield return 0.2f; - if (!Follower.HasLeader) { - Audio.Play("event:/game/general/strawberry_flyaway", Position); - } - tween = Tween.Create(Tween.TweenMode.Oneshot, null, 0.5f, start: true); - tween.OnUpdate = delegate (Tween t) { - flapSpeed = MathHelper.Lerp(0f, -200f, t.Eased); - }; - Add(tween); - } - - private IEnumerator CollectRoutine(int collectIndex) { - Tag = Tags.TransitionUpdate; - Depth = -2000010; - Audio.Play("event:/game/general/seed_poof", Position); - Collidable = false; - sprite.Scale = Vector2.One * 2f; - yield return 0.05f; - Input.Rumble(RumbleStrength.Medium, RumbleLength.Medium); - for (int i = 0; i < 6; i++) { - float num = Calc.Random.NextFloat((float) Math.PI * 2f); - SceneAs().ParticlesFG.Emit(StrawberrySeed.P_Burst, 1, Position + Calc.AngleToVector(num, 4f), Vector2.Zero, num); - } - Visible = false; - sprite.Play("collect"); - while (sprite.Animating) { - yield return null; - } - //Scene.Add(new StrawberryPoints(Position, isGhostBerry, collectIndex, Moon)); - RemoveSelf(); + private IEnumerator FlyAwayRoutine() { + rotateWiggler.Start(); + flapSpeed = -200f; + Tween tween = Tween.Create(Tween.TweenMode.Oneshot, Ease.CubeOut, 0.25f, start: true); + tween.OnUpdate = delegate (Tween t) { + flapSpeed = MathHelper.Lerp(-200f, 0f, t.Eased); + }; + Add(tween); + yield return 0.1f; + Audio.Play("event:/game/general/strawberry_laugh", Position); + yield return 0.2f; + if (!Follower.HasLeader) { + Audio.Play("event:/game/general/strawberry_flyaway", Position); + } + tween = Tween.Create(Tween.TweenMode.Oneshot, null, 0.5f, start: true); + tween.OnUpdate = delegate (Tween t) { + flapSpeed = MathHelper.Lerp(0f, -200f, t.Eased); + }; + Add(tween); + } + + private IEnumerator CollectRoutine(int collectIndex) { + Tag = Tags.TransitionUpdate; + Depth = -2000010; + Audio.Play("event:/game/general/seed_poof", Position); + Collidable = false; + sprite.Scale = Vector2.One * 2f; + yield return 0.05f; + Input.Rumble(RumbleStrength.Medium, RumbleLength.Medium); + for (int i = 0; i < 6; i++) { + float num = Calc.Random.NextFloat((float) Math.PI * 2f); + SceneAs().ParticlesFG.Emit(StrawberrySeed.P_Burst, 1, Position + Calc.AngleToVector(num, 4f), Vector2.Zero, num); + } + Visible = false; + sprite.Play("collect"); + while (sprite.Animating) { + yield return null; + } + //Scene.Add(new StrawberryPoints(Position, isGhostBerry, collectIndex, Moon)); + RemoveSelf(); } - private void OnLoseLeader() { - if (!collected && ReturnHomeWhenLost) { - Alarm.Set(this, 0.15f, delegate { - Vector2 displacement = (start - Position).SafeNormalize(); - float dist = Vector2.Distance(Position, start); - float scaleFactor = Calc.ClampedMap(dist, 16f, 120f, 16f, 96f); - Vector2 control = start + displacement * 16f + displacement.Perpendicular() * scaleFactor * Calc.Random.Choose(1, -1); - SimpleCurve curve = new SimpleCurve(Position, start, control); - Tween tween = Tween.Create(Tween.TweenMode.Oneshot, Ease.SineOut, MathHelper.Max(dist / 100f, 0.4f), start: true); - tween.OnUpdate = delegate (Tween f) { - Position = curve.GetPoint(f.Eased); - }; - tween.OnComplete = delegate { - Depth = 0; - }; - Add(tween); - }); - } + private void OnLoseLeader() { + if (!collected && ReturnHomeWhenLost) { + Alarm.Set(this, 0.15f, delegate { + Vector2 displacement = (start - Position).SafeNormalize(); + float dist = Vector2.Distance(Position, start); + float scaleFactor = Calc.ClampedMap(dist, 16f, 120f, 16f, 96f); + Vector2 control = start + displacement * 16f + displacement.Perpendicular() * scaleFactor * Calc.Random.Choose(1, -1); + SimpleCurve curve = new SimpleCurve(Position, start, control); + Tween tween = Tween.Create(Tween.TweenMode.Oneshot, Ease.SineOut, MathHelper.Max(dist / 100f, 0.4f), start: true); + tween.OnUpdate = delegate (Tween f) { + Position = curve.GetPoint(f.Eased); + }; + tween.OnComplete = delegate { + Depth = 0; + }; + Add(tween); + }); + } } } } diff --git a/Entities/UnderwaterSwitchController.cs b/Entities/UnderwaterSwitchController.cs index 3eb2be7..d5d62d7 100644 --- a/Entities/UnderwaterSwitchController.cs +++ b/Entities/UnderwaterSwitchController.cs @@ -1,70 +1,70 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System.Collections; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/UnderwaterSwitchController")] - class UnderwaterSwitchController : Entity { - private static FieldInfo waterFill = typeof(Water).GetField("fill", BindingFlags.NonPublic | BindingFlags.Instance); - - private string flag; - private Water water; - - public UnderwaterSwitchController(EntityData data, Vector2 offset) : base(data.Position + offset) { - flag = data.Attr("flag"); - - Add(new Coroutine(Routine())); - } - - public override void Added(Scene scene) { - base.Added(scene); - - // if the flag is already set, spawn water right away - Session session = SceneAs().Session; - if (session.GetFlag(flag)) { - spawnWater(session.LevelData.Bounds); - } - } - - public IEnumerator Routine() { - Session session = SceneAs().Session; - while (true) { - // wait until the flag is set. - while (!session.GetFlag(flag)) { - yield return null; - } - - // spawn water. - if (water == null) { - spawnWater(session.LevelData.Bounds); - } - - // wait until the flag is set. - while (session.GetFlag(flag)) { - yield return null; - } - - // make water go away. - water.Collidable = false; - Scene.Remove(water); - water = null; - } - } - - private void spawnWater(Rectangle levelBounds) { - // flood the room with water, make the water 10 pixels over the top to prevent a "splash" effect when going in a room above. - water = new Water(new Vector2(levelBounds.Left, levelBounds.Top - 10), - false, false, levelBounds.Width, levelBounds.Height + 10); - - // but we don't want the water to render off-screen, because it is visible on upwards transitions. - Rectangle fill = (Rectangle) waterFill.GetValue(water); - fill.Y += 10; - fill.Height -= 10; - waterFill.SetValue(water, fill); - - Scene.Add(water); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System.Collections; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/UnderwaterSwitchController")] + class UnderwaterSwitchController : Entity { + private static FieldInfo waterFill = typeof(Water).GetField("fill", BindingFlags.NonPublic | BindingFlags.Instance); + + private string flag; + private Water water; + + public UnderwaterSwitchController(EntityData data, Vector2 offset) : base(data.Position + offset) { + flag = data.Attr("flag"); + + Add(new Coroutine(Routine())); + } + + public override void Added(Scene scene) { + base.Added(scene); + + // if the flag is already set, spawn water right away + Session session = SceneAs().Session; + if (session.GetFlag(flag)) { + spawnWater(session.LevelData.Bounds); + } + } + + public IEnumerator Routine() { + Session session = SceneAs().Session; + while (true) { + // wait until the flag is set. + while (!session.GetFlag(flag)) { + yield return null; + } + + // spawn water. + if (water == null) { + spawnWater(session.LevelData.Bounds); + } + + // wait until the flag is set. + while (session.GetFlag(flag)) { + yield return null; + } + + // make water go away. + water.Collidable = false; + Scene.Remove(water); + water = null; + } + } + + private void spawnWater(Rectangle levelBounds) { + // flood the room with water, make the water 10 pixels over the top to prevent a "splash" effect when going in a room above. + water = new Water(new Vector2(levelBounds.Left, levelBounds.Top - 10), + false, false, levelBounds.Width, levelBounds.Height + 10); + + // but we don't want the water to render off-screen, because it is visible on upwards transitions. + Rectangle fill = (Rectangle) waterFill.GetValue(water); + fill.Y += 10; + fill.Height -= 10; + waterFill.SetValue(water, fill); + + Scene.Add(water); + } + } +} diff --git a/Entities/UpsideDownJumpThru.cs b/Entities/UpsideDownJumpThru.cs index 165a083..0432959 100644 --- a/Entities/UpsideDownJumpThru.cs +++ b/Entities/UpsideDownJumpThru.cs @@ -1,413 +1,413 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Mono.Cecil; -using Mono.Cecil.Cil; -using Monocle; -using MonoMod.Cil; -using MonoMod.RuntimeDetour; -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Linq; - -namespace Celeste.Mod.SpringCollab2020.Entities { - [CustomEntity("SpringCollab2020/UpsideDownJumpThru")] - [Tracked] - class UpsideDownJumpThru : JumpThru { - - private static FieldInfo actorMovementCounter = typeof(Actor).GetField("movementCounter", BindingFlags.Instance | BindingFlags.NonPublic); - private static FieldInfo playerVarJumpTimer = typeof(Player).GetField("varJumpTimer", BindingFlags.Instance | BindingFlags.NonPublic); - private static FieldInfo playerOnCollideV = typeof(Player).GetField("onCollideV", BindingFlags.Instance | BindingFlags.NonPublic); - - private static ILHook playerOrigUpdateHook; - - private static bool hooksActive = false; - - private static readonly Hitbox normalHitbox = new Hitbox(8f, 11f, -4f, -11f); - - public static void Load() { - On.Celeste.LevelLoader.ctor += onLevelLoad; - On.Celeste.OverworldLoader.ctor += onOverworldLoad; - } - - public static void Unload() { - On.Celeste.LevelLoader.ctor -= onLevelLoad; - On.Celeste.OverworldLoader.ctor -= onOverworldLoad; - deactivateHooks(); - } - - private static void onLevelLoad(On.Celeste.LevelLoader.orig_ctor orig, LevelLoader self, Session session, Vector2? startPosition) { - orig(self, session, startPosition); - - if (session.MapData?.Levels?.Any(level => level.Entities?.Any(entity => entity.Name == "SpringCollab2020/UpsideDownJumpThru") ?? false) ?? false) { - activateHooks(); - } else { - deactivateHooks(); - } - } - - private static void onOverworldLoad(On.Celeste.OverworldLoader.orig_ctor orig, OverworldLoader self, Overworld.StartMode startMode, HiresSnow snow) { - orig(self, startMode, snow); - - if (startMode != (Overworld.StartMode) (-1)) { // -1 = in-game overworld from the collab utils - deactivateHooks(); - } - } - - - public static void activateHooks() { - if (hooksActive) { - return; - } - hooksActive = true; - - Logger.Log(LogLevel.Info, "SpringCollab2020/UpsideDownJumpThru", "=== Activating upside-down jumpthru hooks"); - - // fix general actor/platform behavior to make them comply with jumpthrus. - IL.Celeste.Actor.MoveVExact += addUpsideDownJumpthrusInMoveVExact; - IL.Celeste.Platform.MoveVExactCollideSolids += addUpsideDownJumpthrusInCollideSolids; - - using (new DetourContext { After = { "*" } }) { - // fix player specific behavior allowing them to go through upside-down jumpthrus. - On.Celeste.Player.ctor += onPlayerConstructor; - } - - - // block player if they try to climb past an upside-down jumpthru. - IL.Celeste.Player.ClimbUpdate += patchPlayerClimbUpdate; - - // ignore upside-down jumpthrus in select places. - playerOrigUpdateHook = new ILHook(typeof(Player).GetMethod("orig_Update"), filterOutJumpThrusFromCollideChecks); - IL.Celeste.Player.DashUpdate += filterOutJumpThrusFromCollideChecks; - IL.Celeste.Player.RedDashUpdate += filterOutJumpThrusFromCollideChecks; - IL.Celeste.Actor.MoveVExact += filterOutJumpThrusFromCollideChecks; - - // listen for the player unducking, to knock the player down before they would go through upside down jumpthrus. - On.Celeste.Player.Update += onPlayerUpdate; - } - - public static void deactivateHooks() { - if (!hooksActive) { - return; - } - hooksActive = false; - - Logger.Log(LogLevel.Info, "SpringCollab2020/UpsideDownJumpThru", "=== Deactivating upside-down jumpthru hooks"); - - IL.Celeste.Actor.MoveVExact -= addUpsideDownJumpthrusInMoveVExact; - IL.Celeste.Platform.MoveVExactCollideSolids -= addUpsideDownJumpthrusInCollideSolids; - - On.Celeste.Player.ctor -= onPlayerConstructor; - IL.Celeste.Player.ClimbUpdate -= patchPlayerClimbUpdate; - - playerOrigUpdateHook?.Dispose(); - IL.Celeste.Player.DashUpdate -= filterOutJumpThrusFromCollideChecks; - IL.Celeste.Player.RedDashUpdate -= filterOutJumpThrusFromCollideChecks; - IL.Celeste.Actor.MoveVExact -= filterOutJumpThrusFromCollideChecks; - - On.Celeste.Player.Update -= onPlayerUpdate; - } - - private static void addUpsideDownJumpthrusInMoveVExact(ILContext il) { - ILCursor cursor = new ILCursor(il); - - VariableDefinition localPlatform = null; - foreach (VariableDefinition local in il.Method.Body.Variables) { - if (local.VariableType.FullName == "Celeste.Platform") { - localPlatform = local; - break; - } - } - - // position before the moveY > 0 check - if (cursor.TryGotoNext(MoveType.AfterLabel, instr => instr.MatchLdarg(1), instr => instr.MatchLdcI4(0))) { - // look for the code setting movementCounter - ILCursor cursor2 = cursor.Clone(); - if (cursor2.TryGotoNext(MoveType.AfterLabel, instr => instr.MatchLdarg(0), instr => instr.MatchLdflda("movementCounter"))) { - Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Injecting upside-down jumpthru check at {cursor.Index} in IL for {il.Method.Name}"); - - // insert our check for upside-down jumpthrus - cursor.Emit(OpCodes.Ldarg_0); - cursor.Emit(OpCodes.Ldarg_1); - cursor.EmitDelegate>((self, moveV) => { - int moveDirection = Math.Sign(moveV); - if (moveV < 0 && !self.IgnoreJumpThrus) { - JumpThru jumpthru = self.CollideFirstOutside(self.Position + Vector2.UnitY * moveDirection); - if (jumpthru != null) { - // hit upside-down jumpthru while going up - return jumpthru; - } - } - - return null; - }); - - // store the platform in the local variable dedicated to it: if it is non-null, the variable will be used - // to build the collision data. - cursor.Emit(OpCodes.Stloc, localPlatform); - cursor.Emit(OpCodes.Ldloc, localPlatform); - - // if non-null, jump to the same code vanilla uses when the actor hits something. - cursor.Emit(OpCodes.Brtrue, cursor2.Next); - } - } - } - - private static void addUpsideDownJumpthrusInCollideSolids(ILContext il) { - ILCursor cursor = new ILCursor(il); - - VariableDefinition localPlatform = null; - foreach (VariableDefinition local in il.Method.Body.Variables) { - if (local.VariableType.FullName == "Celeste.Platform") { - localPlatform = local; - break; - } - } - - // position before the moveY > 0 check - if (cursor.TryGotoNext(MoveType.AfterLabel, instr => instr.MatchLdarg(1), instr => instr.MatchLdcI4(0))) { - // look for the next platform != null check to figure out the target - ILCursor cursor2 = cursor.Clone(); - if (cursor2.TryGotoNext(MoveType.After, instr => instr.MatchLdloc(localPlatform.Index), instr => instr.OpCode == OpCodes.Brtrue_S)) { - - Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Injecting upside-down jumpthru check at {cursor.Index} in IL for {il.Method.Name}"); - - // inject our check for jumpthrus - cursor.Emit(OpCodes.Ldarg_0); - cursor.Emit(OpCodes.Ldarg_1); - cursor.EmitDelegate>((self, moveV) => { - int moveDirection = Math.Sign(moveV); - if (moveV < 0) { - return self.CollideFirstOutside(self.Position + Vector2.UnitY * moveDirection); - } - return null; - }); - - // store the platform in the local variable dedicated to it: if it is non-null, the variable will be used - // to build the collision data. - cursor.Emit(OpCodes.Stloc, localPlatform); - cursor.Emit(OpCodes.Ldloc, localPlatform); - - // if the check passes, jump to the same target as vanilla. - cursor.Emit(OpCodes.Brtrue_S, cursor2.Prev.Operand); - } - } - } - - private static void onPlayerConstructor(On.Celeste.Player.orig_ctor orig, Player self, Vector2 position, PlayerSpriteMode spriteMode) { - orig(self, position, spriteMode); - - Collision originalOnCollideV = (Collision) playerOnCollideV.GetValue(self); - - Collision patchedOnCollideV = collisionData => { - // we just want to kill a piece of code that executes in these conditions (supposed to push the player left or right when hitting a wall angle). - if (self.StateMachine.State != 19 && self.StateMachine.State != 3 && self.StateMachine.State != 9 && self.Speed.Y < 0 - && self.CollideCheckOutside(self.Position - Vector2.UnitY)) { - - // kill the player's vertical speed. - self.Speed.Y = 0; - - // reset varJumpTimer to prevent a weird "stuck on ceiling" effect. - playerVarJumpTimer.SetValue(self, 0); - } - - originalOnCollideV(collisionData); - }; - - playerOnCollideV.SetValue(self, patchedOnCollideV); - } - - private static void filterOutJumpThrusFromCollideChecks(ILContext il) { - ILCursor cursor = new ILCursor(il); - - // create a Vector2 temporary variable - VariableDefinition checkAtPositionStore = new VariableDefinition(il.Import(typeof(Vector2))); - il.Body.Variables.Add(checkAtPositionStore); - - while (cursor.Next != null) { - Instruction nextInstruction = cursor.Next; - if (nextInstruction.OpCode == OpCodes.Call || nextInstruction.OpCode == OpCodes.Callvirt) { - switch ((nextInstruction.Operand as MethodReference)?.FullName ?? "") { - case "T Monocle.Entity::CollideFirstOutside(Microsoft.Xna.Framework.Vector2)": - Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Patching CollideFirstOutside at {cursor.Index} in IL for {il.Method.Name}"); - cursor.Index++; - - // nullify if mod jumpthru. - cursor.EmitDelegate>(jumpThru => { - if (jumpThru?.GetType() == typeof(UpsideDownJumpThru)) - return null; - return jumpThru; - }); - break; - case "System.Boolean Monocle.Entity::CollideCheckOutside(Microsoft.Xna.Framework.Vector2)": - Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Patching CollideCheckOutside at {cursor.Index} in IL for {il.Method.Name}"); - - callOrigMethodKeepingEverythingOnStack(cursor, checkAtPositionStore); - - // check if colliding with a jumpthru but not an upside-down jumpthru. - cursor.EmitDelegate>((orig, self, at) => orig && !self.CollideCheckOutside(at)); - break; - case "System.Boolean Monocle.Entity::CollideCheck()": - Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Patching CollideCheck at {cursor.Index} in IL for {il.Method.Name}"); - - // we want stack to be: CollideCheck result, this - cursor.Index++; - cursor.Emit(OpCodes.Ldarg_0); - - // turn check to false if colliding with an upside-down jumpthru. - cursor.EmitDelegate>((vanillaCheck, self) => vanillaCheck && !self.CollideCheck()); - break; - - case "System.Collections.Generic.List`1 Monocle.Tracker::GetEntities()": - Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Patching GetEntities at {cursor.Index} in IL for {il.Method.Name}"); - - cursor.Index++; - - // remove all mod jumpthrus from the returned list. - cursor.EmitDelegate, List>>(matches => { - for (int i = 0; i < matches.Count; i++) { - if (matches[i].GetType() == typeof(UpsideDownJumpThru)) { - matches.RemoveAt(i); - i--; - } - } - return matches; - }); - break; - } - } - - cursor.Index++; - } - } - - private static void callOrigMethodKeepingEverythingOnStack(ILCursor cursor, VariableDefinition checkAtPositionStore) { - // store the position in the local variable - cursor.Emit(OpCodes.Stloc, checkAtPositionStore); - cursor.Emit(OpCodes.Ldloc, checkAtPositionStore); - - // let vanilla call CollideCheck - cursor.Index++; - - // reload the parameters - cursor.Emit(OpCodes.Ldarg_0); - cursor.Emit(OpCodes.Ldloc, checkAtPositionStore); - } - - private static void patchPlayerClimbUpdate(ILContext il) { - ILCursor cursor = new ILCursor(il); - - // in decompiled code, we want to get ourselves just before the last occurrence of "if (climbNoMoveTimer <= 0f)". - while (cursor.TryGotoNext( - instr => instr.MatchStfld("Y"), - instr => instr.MatchLdarg(0), - instr => instr.MatchLdfld("climbNoMoveTimer"), - instr => instr.MatchLdcR4(0f))) { - - cursor.Index += 2; - - FieldInfo f_lastClimbMove = typeof(Player).GetField("lastClimbMove", BindingFlags.NonPublic | BindingFlags.Instance); - - Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Injecting collide check to block climbing at {cursor.Index} in IL for {il.Method.Name}"); - - cursor.Emit(OpCodes.Ldarg_0); - cursor.Emit(OpCodes.Ldarg_0); - cursor.Emit(OpCodes.Ldfld, f_lastClimbMove); - - // if climbing is blocked by an upside down jumpthru, cancel the climb (lastClimbMove = 0 and Speed.Y = 0). - // injecting that at that point in the method allows it to drain stamina as if the player was not climbing. - cursor.EmitDelegate>((self, lastClimbMove) => { - if (Input.MoveY.Value == -1 && self.CollideCheckOutside(self.Position - Vector2.UnitY)) { - self.Speed.Y = 0; - return 0; - } - return lastClimbMove; - }); - - cursor.Emit(OpCodes.Stfld, f_lastClimbMove); - cursor.Emit(OpCodes.Ldarg_0); - } - } - - private static void onPlayerUpdate(On.Celeste.Player.orig_Update orig, Player self) { - bool unduckWouldGoThroughPlatform = self.Ducking && !self.CollideCheck(); - if (unduckWouldGoThroughPlatform) { - Collider bak = self.Collider; - self.Collider = normalHitbox; - unduckWouldGoThroughPlatform = self.CollideCheck(); - self.Collider = bak; - } - - orig(self); - - if (unduckWouldGoThroughPlatform && !self.Ducking) { - // we just unducked, and are now inside an upside-down jumpthru. - // knock the player down if possible! - while (self.CollideCheck() && !self.CollideCheck(self.Position + new Vector2(0f, 1f))) { - self.Position.Y++; - } - } - } - - - - private int columns; - private string overrideTexture; - private float animationDelay; - - public UpsideDownJumpThru(EntityData data, Vector2 offset) - : base(data.Position + offset, data.Width, false) { - - columns = data.Width / 8; - Depth = -60; - overrideTexture = data.Attr("texture", "default"); - animationDelay = data.Float("animationDelay", 0f); - - // shift the hitbox a bit to match the graphic - Collider.Top += 3; - } - - public override void Awake(Scene scene) { - if (animationDelay > 0f) { - for (int i = 0; i < columns; i++) { - Sprite jumpthruSprite = new Sprite(GFX.Game, "objects/jumpthru/" + overrideTexture); - jumpthruSprite.AddLoop("idle", "", animationDelay); - jumpthruSprite.X = i * 8; - jumpthruSprite.Y = 8; - jumpthruSprite.Scale.Y = -1; - jumpthruSprite.Play("idle"); - Add(jumpthruSprite); - } - } else { - AreaData areaData = AreaData.Get(scene); - string jumpthru = areaData.Jumpthru; - if (!string.IsNullOrEmpty(overrideTexture) && !overrideTexture.Equals("default")) { - jumpthru = overrideTexture; - } - - MTexture mTexture = GFX.Game["objects/jumpthru/" + jumpthru]; - int textureWidthInTiles = mTexture.Width / 8; - for (int i = 0; i < columns; i++) { - int xTilePosition; - int yTilePosition; - if (i == 0) { - xTilePosition = 0; - yTilePosition = ((!CollideCheck(Position + new Vector2(-1f, 0f))) ? 1 : 0); - } else if (i == columns - 1) { - xTilePosition = textureWidthInTiles - 1; - yTilePosition = ((!CollideCheck(Position + new Vector2(1f, 0f))) ? 1 : 0); - } else { - xTilePosition = 1 + Calc.Random.Next(textureWidthInTiles - 2); - yTilePosition = Calc.Random.Choose(0, 1); - } - - Image image = new Image(mTexture.GetSubtexture(xTilePosition * 8, yTilePosition * 8, 8, 8)); - image.X = i * 8; - image.Y = 8; - image.Scale.Y = -1; - Add(image); - } - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Monocle; +using MonoMod.Cil; +using MonoMod.RuntimeDetour; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; + +namespace Celeste.Mod.SpringCollab2020.Entities { + [CustomEntity("SpringCollab2020/UpsideDownJumpThru")] + [Tracked] + class UpsideDownJumpThru : JumpThru { + + private static FieldInfo actorMovementCounter = typeof(Actor).GetField("movementCounter", BindingFlags.Instance | BindingFlags.NonPublic); + private static FieldInfo playerVarJumpTimer = typeof(Player).GetField("varJumpTimer", BindingFlags.Instance | BindingFlags.NonPublic); + private static FieldInfo playerOnCollideV = typeof(Player).GetField("onCollideV", BindingFlags.Instance | BindingFlags.NonPublic); + + private static ILHook playerOrigUpdateHook; + + private static bool hooksActive = false; + + private static readonly Hitbox normalHitbox = new Hitbox(8f, 11f, -4f, -11f); + + public static void Load() { + On.Celeste.LevelLoader.ctor += onLevelLoad; + On.Celeste.OverworldLoader.ctor += onOverworldLoad; + } + + public static void Unload() { + On.Celeste.LevelLoader.ctor -= onLevelLoad; + On.Celeste.OverworldLoader.ctor -= onOverworldLoad; + deactivateHooks(); + } + + private static void onLevelLoad(On.Celeste.LevelLoader.orig_ctor orig, LevelLoader self, Session session, Vector2? startPosition) { + orig(self, session, startPosition); + + if (session.MapData?.Levels?.Any(level => level.Entities?.Any(entity => entity.Name == "SpringCollab2020/UpsideDownJumpThru") ?? false) ?? false) { + activateHooks(); + } else { + deactivateHooks(); + } + } + + private static void onOverworldLoad(On.Celeste.OverworldLoader.orig_ctor orig, OverworldLoader self, Overworld.StartMode startMode, HiresSnow snow) { + orig(self, startMode, snow); + + if (startMode != (Overworld.StartMode) (-1)) { // -1 = in-game overworld from the collab utils + deactivateHooks(); + } + } + + + public static void activateHooks() { + if (hooksActive) { + return; + } + hooksActive = true; + + Logger.Log(LogLevel.Info, "SpringCollab2020/UpsideDownJumpThru", "=== Activating upside-down jumpthru hooks"); + + // fix general actor/platform behavior to make them comply with jumpthrus. + IL.Celeste.Actor.MoveVExact += addUpsideDownJumpthrusInMoveVExact; + IL.Celeste.Platform.MoveVExactCollideSolids += addUpsideDownJumpthrusInCollideSolids; + + using (new DetourContext { After = { "*" } }) { + // fix player specific behavior allowing them to go through upside-down jumpthrus. + On.Celeste.Player.ctor += onPlayerConstructor; + } + + + // block player if they try to climb past an upside-down jumpthru. + IL.Celeste.Player.ClimbUpdate += patchPlayerClimbUpdate; + + // ignore upside-down jumpthrus in select places. + playerOrigUpdateHook = new ILHook(typeof(Player).GetMethod("orig_Update"), filterOutJumpThrusFromCollideChecks); + IL.Celeste.Player.DashUpdate += filterOutJumpThrusFromCollideChecks; + IL.Celeste.Player.RedDashUpdate += filterOutJumpThrusFromCollideChecks; + IL.Celeste.Actor.MoveVExact += filterOutJumpThrusFromCollideChecks; + + // listen for the player unducking, to knock the player down before they would go through upside down jumpthrus. + On.Celeste.Player.Update += onPlayerUpdate; + } + + public static void deactivateHooks() { + if (!hooksActive) { + return; + } + hooksActive = false; + + Logger.Log(LogLevel.Info, "SpringCollab2020/UpsideDownJumpThru", "=== Deactivating upside-down jumpthru hooks"); + + IL.Celeste.Actor.MoveVExact -= addUpsideDownJumpthrusInMoveVExact; + IL.Celeste.Platform.MoveVExactCollideSolids -= addUpsideDownJumpthrusInCollideSolids; + + On.Celeste.Player.ctor -= onPlayerConstructor; + IL.Celeste.Player.ClimbUpdate -= patchPlayerClimbUpdate; + + playerOrigUpdateHook?.Dispose(); + IL.Celeste.Player.DashUpdate -= filterOutJumpThrusFromCollideChecks; + IL.Celeste.Player.RedDashUpdate -= filterOutJumpThrusFromCollideChecks; + IL.Celeste.Actor.MoveVExact -= filterOutJumpThrusFromCollideChecks; + + On.Celeste.Player.Update -= onPlayerUpdate; + } + + private static void addUpsideDownJumpthrusInMoveVExact(ILContext il) { + ILCursor cursor = new ILCursor(il); + + VariableDefinition localPlatform = null; + foreach (VariableDefinition local in il.Method.Body.Variables) { + if (local.VariableType.FullName == "Celeste.Platform") { + localPlatform = local; + break; + } + } + + // position before the moveY > 0 check + if (cursor.TryGotoNext(MoveType.AfterLabel, instr => instr.MatchLdarg(1), instr => instr.MatchLdcI4(0))) { + // look for the code setting movementCounter + ILCursor cursor2 = cursor.Clone(); + if (cursor2.TryGotoNext(MoveType.AfterLabel, instr => instr.MatchLdarg(0), instr => instr.MatchLdflda("movementCounter"))) { + Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Injecting upside-down jumpthru check at {cursor.Index} in IL for {il.Method.Name}"); + + // insert our check for upside-down jumpthrus + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Ldarg_1); + cursor.EmitDelegate>((self, moveV) => { + int moveDirection = Math.Sign(moveV); + if (moveV < 0 && !self.IgnoreJumpThrus) { + JumpThru jumpthru = self.CollideFirstOutside(self.Position + Vector2.UnitY * moveDirection); + if (jumpthru != null) { + // hit upside-down jumpthru while going up + return jumpthru; + } + } + + return null; + }); + + // store the platform in the local variable dedicated to it: if it is non-null, the variable will be used + // to build the collision data. + cursor.Emit(OpCodes.Stloc, localPlatform); + cursor.Emit(OpCodes.Ldloc, localPlatform); + + // if non-null, jump to the same code vanilla uses when the actor hits something. + cursor.Emit(OpCodes.Brtrue, cursor2.Next); + } + } + } + + private static void addUpsideDownJumpthrusInCollideSolids(ILContext il) { + ILCursor cursor = new ILCursor(il); + + VariableDefinition localPlatform = null; + foreach (VariableDefinition local in il.Method.Body.Variables) { + if (local.VariableType.FullName == "Celeste.Platform") { + localPlatform = local; + break; + } + } + + // position before the moveY > 0 check + if (cursor.TryGotoNext(MoveType.AfterLabel, instr => instr.MatchLdarg(1), instr => instr.MatchLdcI4(0))) { + // look for the next platform != null check to figure out the target + ILCursor cursor2 = cursor.Clone(); + if (cursor2.TryGotoNext(MoveType.After, instr => instr.MatchLdloc(localPlatform.Index), instr => instr.OpCode == OpCodes.Brtrue_S)) { + + Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Injecting upside-down jumpthru check at {cursor.Index} in IL for {il.Method.Name}"); + + // inject our check for jumpthrus + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Ldarg_1); + cursor.EmitDelegate>((self, moveV) => { + int moveDirection = Math.Sign(moveV); + if (moveV < 0) { + return self.CollideFirstOutside(self.Position + Vector2.UnitY * moveDirection); + } + return null; + }); + + // store the platform in the local variable dedicated to it: if it is non-null, the variable will be used + // to build the collision data. + cursor.Emit(OpCodes.Stloc, localPlatform); + cursor.Emit(OpCodes.Ldloc, localPlatform); + + // if the check passes, jump to the same target as vanilla. + cursor.Emit(OpCodes.Brtrue_S, cursor2.Prev.Operand); + } + } + } + + private static void onPlayerConstructor(On.Celeste.Player.orig_ctor orig, Player self, Vector2 position, PlayerSpriteMode spriteMode) { + orig(self, position, spriteMode); + + Collision originalOnCollideV = (Collision) playerOnCollideV.GetValue(self); + + Collision patchedOnCollideV = collisionData => { + // we just want to kill a piece of code that executes in these conditions (supposed to push the player left or right when hitting a wall angle). + if (self.StateMachine.State != 19 && self.StateMachine.State != 3 && self.StateMachine.State != 9 && self.Speed.Y < 0 + && self.CollideCheckOutside(self.Position - Vector2.UnitY)) { + + // kill the player's vertical speed. + self.Speed.Y = 0; + + // reset varJumpTimer to prevent a weird "stuck on ceiling" effect. + playerVarJumpTimer.SetValue(self, 0); + } + + originalOnCollideV(collisionData); + }; + + playerOnCollideV.SetValue(self, patchedOnCollideV); + } + + private static void filterOutJumpThrusFromCollideChecks(ILContext il) { + ILCursor cursor = new ILCursor(il); + + // create a Vector2 temporary variable + VariableDefinition checkAtPositionStore = new VariableDefinition(il.Import(typeof(Vector2))); + il.Body.Variables.Add(checkAtPositionStore); + + while (cursor.Next != null) { + Instruction nextInstruction = cursor.Next; + if (nextInstruction.OpCode == OpCodes.Call || nextInstruction.OpCode == OpCodes.Callvirt) { + switch ((nextInstruction.Operand as MethodReference)?.FullName ?? "") { + case "T Monocle.Entity::CollideFirstOutside(Microsoft.Xna.Framework.Vector2)": + Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Patching CollideFirstOutside at {cursor.Index} in IL for {il.Method.Name}"); + cursor.Index++; + + // nullify if mod jumpthru. + cursor.EmitDelegate>(jumpThru => { + if (jumpThru?.GetType() == typeof(UpsideDownJumpThru)) + return null; + return jumpThru; + }); + break; + case "System.Boolean Monocle.Entity::CollideCheckOutside(Microsoft.Xna.Framework.Vector2)": + Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Patching CollideCheckOutside at {cursor.Index} in IL for {il.Method.Name}"); + + callOrigMethodKeepingEverythingOnStack(cursor, checkAtPositionStore); + + // check if colliding with a jumpthru but not an upside-down jumpthru. + cursor.EmitDelegate>((orig, self, at) => orig && !self.CollideCheckOutside(at)); + break; + case "System.Boolean Monocle.Entity::CollideCheck()": + Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Patching CollideCheck at {cursor.Index} in IL for {il.Method.Name}"); + + // we want stack to be: CollideCheck result, this + cursor.Index++; + cursor.Emit(OpCodes.Ldarg_0); + + // turn check to false if colliding with an upside-down jumpthru. + cursor.EmitDelegate>((vanillaCheck, self) => vanillaCheck && !self.CollideCheck()); + break; + + case "System.Collections.Generic.List`1 Monocle.Tracker::GetEntities()": + Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Patching GetEntities at {cursor.Index} in IL for {il.Method.Name}"); + + cursor.Index++; + + // remove all mod jumpthrus from the returned list. + cursor.EmitDelegate, List>>(matches => { + for (int i = 0; i < matches.Count; i++) { + if (matches[i].GetType() == typeof(UpsideDownJumpThru)) { + matches.RemoveAt(i); + i--; + } + } + return matches; + }); + break; + } + } + + cursor.Index++; + } + } + + private static void callOrigMethodKeepingEverythingOnStack(ILCursor cursor, VariableDefinition checkAtPositionStore) { + // store the position in the local variable + cursor.Emit(OpCodes.Stloc, checkAtPositionStore); + cursor.Emit(OpCodes.Ldloc, checkAtPositionStore); + + // let vanilla call CollideCheck + cursor.Index++; + + // reload the parameters + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Ldloc, checkAtPositionStore); + } + + private static void patchPlayerClimbUpdate(ILContext il) { + ILCursor cursor = new ILCursor(il); + + // in decompiled code, we want to get ourselves just before the last occurrence of "if (climbNoMoveTimer <= 0f)". + while (cursor.TryGotoNext( + instr => instr.MatchStfld("Y"), + instr => instr.MatchLdarg(0), + instr => instr.MatchLdfld("climbNoMoveTimer"), + instr => instr.MatchLdcR4(0f))) { + + cursor.Index += 2; + + FieldInfo f_lastClimbMove = typeof(Player).GetField("lastClimbMove", BindingFlags.NonPublic | BindingFlags.Instance); + + Logger.Log("SpringCollab2020/UpsideDownJumpThru", $"Injecting collide check to block climbing at {cursor.Index} in IL for {il.Method.Name}"); + + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Ldfld, f_lastClimbMove); + + // if climbing is blocked by an upside down jumpthru, cancel the climb (lastClimbMove = 0 and Speed.Y = 0). + // injecting that at that point in the method allows it to drain stamina as if the player was not climbing. + cursor.EmitDelegate>((self, lastClimbMove) => { + if (Input.MoveY.Value == -1 && self.CollideCheckOutside(self.Position - Vector2.UnitY)) { + self.Speed.Y = 0; + return 0; + } + return lastClimbMove; + }); + + cursor.Emit(OpCodes.Stfld, f_lastClimbMove); + cursor.Emit(OpCodes.Ldarg_0); + } + } + + private static void onPlayerUpdate(On.Celeste.Player.orig_Update orig, Player self) { + bool unduckWouldGoThroughPlatform = self.Ducking && !self.CollideCheck(); + if (unduckWouldGoThroughPlatform) { + Collider bak = self.Collider; + self.Collider = normalHitbox; + unduckWouldGoThroughPlatform = self.CollideCheck(); + self.Collider = bak; + } + + orig(self); + + if (unduckWouldGoThroughPlatform && !self.Ducking) { + // we just unducked, and are now inside an upside-down jumpthru. + // knock the player down if possible! + while (self.CollideCheck() && !self.CollideCheck(self.Position + new Vector2(0f, 1f))) { + self.Position.Y++; + } + } + } + + + + private int columns; + private string overrideTexture; + private float animationDelay; + + public UpsideDownJumpThru(EntityData data, Vector2 offset) + : base(data.Position + offset, data.Width, false) { + + columns = data.Width / 8; + Depth = -60; + overrideTexture = data.Attr("texture", "default"); + animationDelay = data.Float("animationDelay", 0f); + + // shift the hitbox a bit to match the graphic + Collider.Top += 3; + } + + public override void Awake(Scene scene) { + if (animationDelay > 0f) { + for (int i = 0; i < columns; i++) { + Sprite jumpthruSprite = new Sprite(GFX.Game, "objects/jumpthru/" + overrideTexture); + jumpthruSprite.AddLoop("idle", "", animationDelay); + jumpthruSprite.X = i * 8; + jumpthruSprite.Y = 8; + jumpthruSprite.Scale.Y = -1; + jumpthruSprite.Play("idle"); + Add(jumpthruSprite); + } + } else { + AreaData areaData = AreaData.Get(scene); + string jumpthru = areaData.Jumpthru; + if (!string.IsNullOrEmpty(overrideTexture) && !overrideTexture.Equals("default")) { + jumpthru = overrideTexture; + } + + MTexture mTexture = GFX.Game["objects/jumpthru/" + jumpthru]; + int textureWidthInTiles = mTexture.Width / 8; + for (int i = 0; i < columns; i++) { + int xTilePosition; + int yTilePosition; + if (i == 0) { + xTilePosition = 0; + yTilePosition = ((!CollideCheck(Position + new Vector2(-1f, 0f))) ? 1 : 0); + } else if (i == columns - 1) { + xTilePosition = textureWidthInTiles - 1; + yTilePosition = ((!CollideCheck(Position + new Vector2(1f, 0f))) ? 1 : 0); + } else { + xTilePosition = 1 + Calc.Random.Next(textureWidthInTiles - 2); + yTilePosition = Calc.Random.Choose(0, 1); + } + + Image image = new Image(mTexture.GetSubtexture(xTilePosition * 8, yTilePosition * 8, 8, 8)); + image.X = i * 8; + image.Y = 8; + image.Scale.Y = -1; + Add(image); + } + } + } + } +} diff --git a/Entities/VariableCrumbleBlock.cs b/Entities/VariableCrumbleBlock.cs index 024d5c1..9313c40 100644 --- a/Entities/VariableCrumbleBlock.cs +++ b/Entities/VariableCrumbleBlock.cs @@ -1,4 +1,4 @@ -using Celeste.Mod.Entities; +using Celeste.Mod.Entities; using Microsoft.Xna.Framework; using Monocle; using System; @@ -30,16 +30,16 @@ public class VariableCrumblePlatform : Solid { private string overrideTexture; - public VariableCrumblePlatform(Vector2 position, float width, string overrideTexture, float timer, float respawnTimer) - : base(position, width, 8f, false) { - EnableAssistModeChecks = false; - this.overrideTexture = overrideTexture; - crumbleTime = timer; - respawnTime = respawnTimer; - } - - public VariableCrumblePlatform(EntityData data, Vector2 offset) - : this(data.Position + offset, (float) data.Width, data.Attr("texture"), data.Float("timer", 0.4f), data.Float("respawnTimer", 2f)) { + public VariableCrumblePlatform(Vector2 position, float width, string overrideTexture, float timer, float respawnTimer) + : base(position, width, 8f, false) { + EnableAssistModeChecks = false; + this.overrideTexture = overrideTexture; + crumbleTime = timer; + respawnTime = respawnTimer; + } + + public VariableCrumblePlatform(EntityData data, Vector2 offset) + : this(data.Position + offset, (float) data.Width, data.Attr("texture"), data.Float("timer", 0.4f), data.Float("respawnTimer", 2f)) { } public override void Added(Scene scene) { @@ -47,51 +47,51 @@ public override void Added(Scene scene) { string crumbleBlock = areaData.CrumbleBlock; if (overrideTexture != null) { areaData.CrumbleBlock = overrideTexture; - } - base.Added(scene); - MTexture mTexture = GFX.Game["objects/crumbleBlock/outline"]; - outline = new List(); - if (base.Width <= 8f) { - Image image = new Image(mTexture.GetSubtexture(24, 0, 8, 8)); - image.Color = Color.White * 0f; - Add(image); - outline.Add(image); - } else { - for (int i = 0; (float) i < base.Width; i += 8) { - int num = (i != 0) ? ((i > 0 && (float) i < base.Width - 8f) ? 1 : 2) : 0; - Image image2 = new Image(mTexture.GetSubtexture(num * 8, 0, 8, 8)); - image2.Position = new Vector2(i, 0f); - image2.Color = Color.White * 0f; - Add(image2); - outline.Add(image2); - } - } - Add(outlineFader = new Coroutine()); - outlineFader.RemoveOnComplete = false; - images = new List(); - falls = new List(); - fallOrder = new List(); - MTexture mTexture2 = GFX.Game["objects/crumbleBlock/" + AreaData.Get(scene).CrumbleBlock]; - for (int j = 0; (float) j < base.Width; j += 8) { - int num2 = (int) ((Math.Abs(base.X) + (float) j) / 8f) % 4; - Image image3 = new Image(mTexture2.GetSubtexture(num2 * 8, 0, 8, 8)); - image3.Position = new Vector2(4 + j, 4f); - image3.CenterOrigin(); - Add(image3); - images.Add(image3); - Coroutine coroutine = new Coroutine(); - coroutine.RemoveOnComplete = false; - falls.Add(coroutine); - Add(coroutine); - fallOrder.Add(j / 8); - } - fallOrder.Shuffle(); - Add(new Coroutine(Sequence())); - Add(shaker = new ShakerList(images.Count, false, delegate (Vector2[] v) { - for (int k = 0; k < images.Count; k++) { - images[k].Position = new Vector2(4 + k * 8, 4f) + v[k]; - } - })); + } + base.Added(scene); + MTexture mTexture = GFX.Game["objects/crumbleBlock/outline"]; + outline = new List(); + if (base.Width <= 8f) { + Image image = new Image(mTexture.GetSubtexture(24, 0, 8, 8)); + image.Color = Color.White * 0f; + Add(image); + outline.Add(image); + } else { + for (int i = 0; (float) i < base.Width; i += 8) { + int num = (i != 0) ? ((i > 0 && (float) i < base.Width - 8f) ? 1 : 2) : 0; + Image image2 = new Image(mTexture.GetSubtexture(num * 8, 0, 8, 8)); + image2.Position = new Vector2(i, 0f); + image2.Color = Color.White * 0f; + Add(image2); + outline.Add(image2); + } + } + Add(outlineFader = new Coroutine()); + outlineFader.RemoveOnComplete = false; + images = new List(); + falls = new List(); + fallOrder = new List(); + MTexture mTexture2 = GFX.Game["objects/crumbleBlock/" + AreaData.Get(scene).CrumbleBlock]; + for (int j = 0; (float) j < base.Width; j += 8) { + int num2 = (int) ((Math.Abs(base.X) + (float) j) / 8f) % 4; + Image image3 = new Image(mTexture2.GetSubtexture(num2 * 8, 0, 8, 8)); + image3.Position = new Vector2(4 + j, 4f); + image3.CenterOrigin(); + Add(image3); + images.Add(image3); + Coroutine coroutine = new Coroutine(); + coroutine.RemoveOnComplete = false; + falls.Add(coroutine); + Add(coroutine); + fallOrder.Add(j / 8); + } + fallOrder.Shuffle(); + Add(new Coroutine(Sequence())); + Add(shaker = new ShakerList(images.Count, false, delegate (Vector2[] v) { + for (int k = 0; k < images.Count; k++) { + images[k].Position = new Vector2(4 + k * 8, 4f) + v[k]; + } + })); Add(occluder = new LightOcclude(0.2f)); areaData.CrumbleBlock = crumbleBlock; } diff --git a/GrandmasterHeartSideHelper.cs b/GrandmasterHeartSideHelper.cs index 1633be7..fc6d8a6 100755 --- a/GrandmasterHeartSideHelper.cs +++ b/GrandmasterHeartSideHelper.cs @@ -1,165 +1,165 @@ -using Microsoft.Xna.Framework; -using Mono.Cecil.Cil; -using Monocle; -using MonoMod.Cil; -using System; -using System.Reflection; -using System.Linq; -using MonoMod.RuntimeDetour; -using System.Collections.Generic; -using Mono.Cecil; -using MonoMod.Utils; - -namespace Celeste.Mod.SpringCollab2020 { - // The new Grandmaster Heart Side is called ZZ-NewHeartSide and the old one is hidden at ZZ-HeartSide. - // This requires some specific handling from the Collab Utils: - // - hiding old GMHS from the journals, and not making it behave like a heart side - // - making new GMHS behave like a heart side - // We also want the ending heart of old GMHS to send back to the new GMHS, instead of displaying an endscreen. - static class GrandmasterHeartSideHelper { - private static Hook hookIsHeartSide; - private static ILHook hookLobbyJournal; - private static ILHook hookOverworldJournal; - private static ILHook hookPoemColors; - - public static void Load() { - Assembly collabUtils = typeof(CollabUtils2.CollabModule).Assembly; - - hookIsHeartSide = new Hook( - collabUtils.GetType("Celeste.Mod.CollabUtils2.LobbyHelper").GetMethod("IsHeartSide"), - typeof(GrandmasterHeartSideHelper).GetMethod("modIsHeartSide", BindingFlags.NonPublic | BindingFlags.Static)); - - hookOverworldJournal = new ILHook( - collabUtils.GetType("Celeste.Mod.CollabUtils2.UI.OuiJournalCollabProgressInOverworld").GetConstructor(new Type[] { typeof(OuiJournal) }), - modOverworldJournal); - - hookLobbyJournal = new ILHook( - collabUtils.GetType("Celeste.Mod.CollabUtils2.UI.OuiJournalCollabProgressInLobby").GetMethod("GeneratePages"), - modLobbyJournal); - - IL.Celeste.Level.CompleteArea_bool_bool_bool += modLevelComplete; - IL.Celeste.OuiChapterPanel.Render += renderOldGMHSCompletionStamp; - - // we are looking for a lambda in Celeste.Mod.CollabUtils2.LobbyHelper.modJournalPoemHeartColors... - // except it is located in Celeste.Mod.CollabUtils2.LobbyHelper.<>c.b__{someRandomNumber}_0. - // find it by bruteforcing it a bit. - Type innerType = collabUtils.GetType("Celeste.Mod.CollabUtils2.LobbyHelper").GetNestedType("<>c", BindingFlags.NonPublic); - for (int i = 0; i < 100; i++) { - MethodInfo innerMethod = innerType.GetMethod($"b__{i}_0", BindingFlags.NonPublic | BindingFlags.Instance); - if (innerMethod != null) { - // found it! - hookPoemColors = new ILHook(innerMethod, modifyGMHSeartColor); - break; - } - } - } - - public static void Unload() { - hookIsHeartSide?.Dispose(); - hookIsHeartSide = null; - - hookLobbyJournal?.Dispose(); - hookLobbyJournal = null; - - hookOverworldJournal?.Dispose(); - hookOverworldJournal = null; - - IL.Celeste.Level.CompleteArea_bool_bool_bool -= modLevelComplete; - IL.Celeste.OuiChapterPanel.Render -= renderOldGMHSCompletionStamp; - - hookPoemColors?.Dispose(); - hookPoemColors = null; - } - - private static bool modIsHeartSide(Func orig, string sid) { - if (sid == "SpringCollab2020/5-Grandmaster/ZZ-HeartSide") { - return false; // old GMHS - } else if (sid == "SpringCollab2020/5-Grandmaster/ZZ-NewHeartSide") { - return true; // new GMHS - } - - return orig(sid); // ... not GMHS - } - - private static void modLobbyJournal(ILContext il) { - ILCursor cursor = new ILCursor(il); - - while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("get_Areas_Safe"))) { - Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Filtering out old GMHS from journal at {cursor.Index} in IL for OuiJournalProgressInLobby.GeneratePages"); - cursor.EmitDelegate, List>>(orig => orig.Where(area => area.SID != "SpringCollab2020/5-Grandmaster/ZZ-HeartSide").ToList()); - } - } - - private static void modOverworldJournal(ILContext il) { - ILCursor cursor = new ILCursor(il); - - while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdfld("Areas"))) { - Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Filtering out old GMHS from journal at {cursor.Index} in IL for OuiJournalCollabProgressInOverworld ctor"); - cursor.EmitDelegate, List>>(orig => orig.Where(area => area.SID != "SpringCollab2020/5-Grandmaster/ZZ-HeartSide").ToList()); - } - } - - - private static void modLevelComplete(ILContext il) { - ILCursor cursor = new ILCursor(il); - - while (cursor.TryGotoNext(MoveType.After, - instr => instr.OpCode == OpCodes.Ldftn && ((instr.Operand as MethodReference)?.Name.StartsWith("") ?? false), - instr => instr.MatchNewobj())) { - - Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Redirecting old GMHS ending to new GMHS at {cursor.Index} in IL for Level.CompleteArea"); - - cursor.Emit(OpCodes.Ldarg_0); - cursor.EmitDelegate>((orig, self) => { - if (self.Session.Area.GetSID() == "SpringCollab2020/5-Grandmaster/ZZ-HeartSide") { - // "return to lobby" (new GMHS) instead of returning to map. - return () => { - Engine.Scene = new CollabUtils2.UI.LevelExitToLobby(LevelExit.Mode.Completed, self.Session); - }; - } else { - // do the usual. - return orig; - } - }); - } - } - - private static void renderOldGMHSCompletionStamp(ILContext il) { - ILCursor cursor = new ILCursor(il); - - // draw the stamp just after the chapter card. - if (cursor.TryGotoNext(instr => instr.MatchStfld("card")) - && cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("Draw"))) { - - Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Injecting GMHS stamp rendering at {cursor.Index} in IL for OuiChapterPanel.Render"); - - cursor.Emit(OpCodes.Ldarg_0); - cursor.EmitDelegate>(self => { - // draw it only if the player beat old grandmaster heart side, and we're actually looking at it. - if (self.Area.GetSID() == "SpringCollab2020/5-Grandmaster/ZZ-NewHeartSide" - && (SaveData.Instance.GetAreaStatsFor(AreaData.Get("SpringCollab2020/5-Grandmaster/ZZ-HeartSide").ToKey())?.Modes[0].Completed ?? false)) { - - GFX.Gui["SpringCollab2020/OldGMHSStamp"].Draw(self.Position + new Vector2(40f, 150f)); - } - }); - } - } - - private static void modifyGMHSeartColor(ILContext il) { - ILCursor cursor = new ILCursor(il); - - while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdstr("_ZZ_HeartSide_A"), instr => instr.MatchCall("Concat"))) { - Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Modifying new GMHS heart color at {cursor.Index} in IL for {il.Method.FullName}"); - - cursor.Emit(OpCodes.Ldarg_2); // "poem" argument - cursor.EmitDelegate>((orig, poem) => { - // alter the check so that it matches on the new GMHS like it does for old GMHS. - if (orig == "poem_SpringCollab2020/5-Grandmaster_ZZ_HeartSide_A" && poem == Dialog.Clean("poem_SpringCollab2020_5_Grandmaster_ZZ_NewHeartSide_A")) { - return "poem_SpringCollab2020_5_Grandmaster_ZZ_NewHeartSide_A"; - } - return orig; - }); - } - } - } -} +using Microsoft.Xna.Framework; +using Mono.Cecil.Cil; +using Monocle; +using MonoMod.Cil; +using System; +using System.Reflection; +using System.Linq; +using MonoMod.RuntimeDetour; +using System.Collections.Generic; +using Mono.Cecil; +using MonoMod.Utils; + +namespace Celeste.Mod.SpringCollab2020 { + // The new Grandmaster Heart Side is called ZZ-NewHeartSide and the old one is hidden at ZZ-HeartSide. + // This requires some specific handling from the Collab Utils: + // - hiding old GMHS from the journals, and not making it behave like a heart side + // - making new GMHS behave like a heart side + // We also want the ending heart of old GMHS to send back to the new GMHS, instead of displaying an endscreen. + static class GrandmasterHeartSideHelper { + private static Hook hookIsHeartSide; + private static ILHook hookLobbyJournal; + private static ILHook hookOverworldJournal; + private static ILHook hookPoemColors; + + public static void Load() { + Assembly collabUtils = typeof(CollabUtils2.CollabModule).Assembly; + + hookIsHeartSide = new Hook( + collabUtils.GetType("Celeste.Mod.CollabUtils2.LobbyHelper").GetMethod("IsHeartSide"), + typeof(GrandmasterHeartSideHelper).GetMethod("modIsHeartSide", BindingFlags.NonPublic | BindingFlags.Static)); + + hookOverworldJournal = new ILHook( + collabUtils.GetType("Celeste.Mod.CollabUtils2.UI.OuiJournalCollabProgressInOverworld").GetConstructor(new Type[] { typeof(OuiJournal) }), + modOverworldJournal); + + hookLobbyJournal = new ILHook( + collabUtils.GetType("Celeste.Mod.CollabUtils2.UI.OuiJournalCollabProgressInLobby").GetMethod("GeneratePages"), + modLobbyJournal); + + IL.Celeste.Level.CompleteArea_bool_bool_bool += modLevelComplete; + IL.Celeste.OuiChapterPanel.Render += renderOldGMHSCompletionStamp; + + // we are looking for a lambda in Celeste.Mod.CollabUtils2.LobbyHelper.modJournalPoemHeartColors... + // except it is located in Celeste.Mod.CollabUtils2.LobbyHelper.<>c.b__{someRandomNumber}_0. + // find it by bruteforcing it a bit. + Type innerType = collabUtils.GetType("Celeste.Mod.CollabUtils2.LobbyHelper").GetNestedType("<>c", BindingFlags.NonPublic); + for (int i = 0; i < 100; i++) { + MethodInfo innerMethod = innerType.GetMethod($"b__{i}_0", BindingFlags.NonPublic | BindingFlags.Instance); + if (innerMethod != null) { + // found it! + hookPoemColors = new ILHook(innerMethod, modifyGMHSeartColor); + break; + } + } + } + + public static void Unload() { + hookIsHeartSide?.Dispose(); + hookIsHeartSide = null; + + hookLobbyJournal?.Dispose(); + hookLobbyJournal = null; + + hookOverworldJournal?.Dispose(); + hookOverworldJournal = null; + + IL.Celeste.Level.CompleteArea_bool_bool_bool -= modLevelComplete; + IL.Celeste.OuiChapterPanel.Render -= renderOldGMHSCompletionStamp; + + hookPoemColors?.Dispose(); + hookPoemColors = null; + } + + private static bool modIsHeartSide(Func orig, string sid) { + if (sid == "SpringCollab2020/5-Grandmaster/ZZ-HeartSide") { + return false; // old GMHS + } else if (sid == "SpringCollab2020/5-Grandmaster/ZZ-NewHeartSide") { + return true; // new GMHS + } + + return orig(sid); // ... not GMHS + } + + private static void modLobbyJournal(ILContext il) { + ILCursor cursor = new ILCursor(il); + + while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("get_Areas_Safe"))) { + Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Filtering out old GMHS from journal at {cursor.Index} in IL for OuiJournalProgressInLobby.GeneratePages"); + cursor.EmitDelegate, List>>(orig => orig.Where(area => area.SID != "SpringCollab2020/5-Grandmaster/ZZ-HeartSide").ToList()); + } + } + + private static void modOverworldJournal(ILContext il) { + ILCursor cursor = new ILCursor(il); + + while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdfld("Areas"))) { + Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Filtering out old GMHS from journal at {cursor.Index} in IL for OuiJournalCollabProgressInOverworld ctor"); + cursor.EmitDelegate, List>>(orig => orig.Where(area => area.SID != "SpringCollab2020/5-Grandmaster/ZZ-HeartSide").ToList()); + } + } + + + private static void modLevelComplete(ILContext il) { + ILCursor cursor = new ILCursor(il); + + while (cursor.TryGotoNext(MoveType.After, + instr => instr.OpCode == OpCodes.Ldftn && ((instr.Operand as MethodReference)?.Name.StartsWith("") ?? false), + instr => instr.MatchNewobj())) { + + Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Redirecting old GMHS ending to new GMHS at {cursor.Index} in IL for Level.CompleteArea"); + + cursor.Emit(OpCodes.Ldarg_0); + cursor.EmitDelegate>((orig, self) => { + if (self.Session.Area.GetSID() == "SpringCollab2020/5-Grandmaster/ZZ-HeartSide") { + // "return to lobby" (new GMHS) instead of returning to map. + return () => { + Engine.Scene = new CollabUtils2.UI.LevelExitToLobby(LevelExit.Mode.Completed, self.Session); + }; + } else { + // do the usual. + return orig; + } + }); + } + } + + private static void renderOldGMHSCompletionStamp(ILContext il) { + ILCursor cursor = new ILCursor(il); + + // draw the stamp just after the chapter card. + if (cursor.TryGotoNext(instr => instr.MatchStfld("card")) + && cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("Draw"))) { + + Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Injecting GMHS stamp rendering at {cursor.Index} in IL for OuiChapterPanel.Render"); + + cursor.Emit(OpCodes.Ldarg_0); + cursor.EmitDelegate>(self => { + // draw it only if the player beat old grandmaster heart side, and we're actually looking at it. + if (self.Area.GetSID() == "SpringCollab2020/5-Grandmaster/ZZ-NewHeartSide" + && (SaveData.Instance.GetAreaStatsFor(AreaData.Get("SpringCollab2020/5-Grandmaster/ZZ-HeartSide").ToKey())?.Modes[0].Completed ?? false)) { + + GFX.Gui["SpringCollab2020/OldGMHSStamp"].Draw(self.Position + new Vector2(40f, 150f)); + } + }); + } + } + + private static void modifyGMHSeartColor(ILContext il) { + ILCursor cursor = new ILCursor(il); + + while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdstr("_ZZ_HeartSide_A"), instr => instr.MatchCall("Concat"))) { + Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Modifying new GMHS heart color at {cursor.Index} in IL for {il.Method.FullName}"); + + cursor.Emit(OpCodes.Ldarg_2); // "poem" argument + cursor.EmitDelegate>((orig, poem) => { + // alter the check so that it matches on the new GMHS like it does for old GMHS. + if (orig == "poem_SpringCollab2020/5-Grandmaster_ZZ_HeartSide_A" && poem == Dialog.Clean("poem_SpringCollab2020_5_Grandmaster_ZZ_NewHeartSide_A")) { + return "poem_SpringCollab2020_5_Grandmaster_ZZ_NewHeartSide_A"; + } + return orig; + }); + } + } + } +} diff --git a/Graphics/SpringCollab2020/GlassBerry.xml b/Graphics/SpringCollab2020/GlassBerry.xml index acf4e45..af795bc 100644 --- a/Graphics/SpringCollab2020/GlassBerry.xml +++ b/Graphics/SpringCollab2020/GlassBerry.xml @@ -1,4 +1,4 @@ - +
diff --git a/README.md b/README.md index 95c1191..8af07eb 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,6 @@ Developing directly on the master branch is only allowed if you've forked the re - `cd` to your mods folder. - `git clone URL` - Make sure to replace `URL` with the URL to your fork. - - Click on the green "Clone or download" button to obtain the URL. + - Click on the green "Clone or download" button to obtain the URL. - Open the `.sln` in VS. - Submit pull requests (PRs). diff --git a/SpringCollab2020MapDataProcessor.cs b/SpringCollab2020MapDataProcessor.cs index da6a4f3..0919e3e 100644 --- a/SpringCollab2020MapDataProcessor.cs +++ b/SpringCollab2020MapDataProcessor.cs @@ -1,108 +1,108 @@ -using System; -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020 { - class SpringCollab2020MapDataProcessor : EverestMapDataProcessor { - - // the structure here is: FlagTouchSwitches[AreaID][ModeID][flagName] = list of entity ids for flag touch switches in this group on this map. - public static List>>> FlagTouchSwitches = new List>>>(); - private string levelName; - - // we want to match multi-room strawberry seeds with the strawberry that has the same name. - private Dictionary> multiRoomStrawberrySeedsByName = new Dictionary>(); - private Dictionary multiRoomStrawberryIDsByName = new Dictionary(); - private Dictionary multiRoomStrawberriesByName = new Dictionary(); - - public override Dictionary> Init() { - return new Dictionary> { - { - "level", level => { - // be sure to write the level name down. - levelName = level.Attr("name").Split(':')[0]; - if (levelName.StartsWith("lvl_")) { - levelName = levelName.Substring(4); - } - } - }, - { - "entity:SpringCollab2020/FlagTouchSwitch", flagTouchSwitch => { - string flag = flagTouchSwitch.Attr("flag"); - Dictionary> allTouchSwitchesInMap = FlagTouchSwitches[AreaKey.ID][(int) AreaKey.Mode]; - - // if no dictionary entry exists for this flag, create one. otherwise, get it. - List entityIDs; - if (!allTouchSwitchesInMap.ContainsKey(flag)) { - entityIDs = new List(); - allTouchSwitchesInMap[flag] = entityIDs; - } else { - entityIDs = allTouchSwitchesInMap[flag]; - } - - // add this flag touch switch to the dictionary. - entityIDs.Add(new EntityID(levelName, flagTouchSwitch.AttrInt("id"))); - } - }, - { - "entity:SpringCollab2020/MultiRoomStrawberrySeed", strawberrySeed => { - // auto-attribute indices for seeds, and save them. - string berryName = strawberrySeed.Attr("strawberryName"); - if (multiRoomStrawberrySeedsByName.ContainsKey(berryName)) { - if (strawberrySeed.AttrInt("index") < 0) { - strawberrySeed.SetAttr("index", multiRoomStrawberrySeedsByName[berryName].Count); - } - multiRoomStrawberrySeedsByName[berryName].Add(strawberrySeed); - } else { - if (strawberrySeed.AttrInt("index") < 0) { - strawberrySeed.SetAttr("index", 0); - } - multiRoomStrawberrySeedsByName[berryName] = new List() { strawberrySeed }; - } - } - }, - { - "entity:SpringCollab2020/MultiRoomStrawberry", strawberry => { - // save the strawberry IDs. - string berryName = strawberry.Attr("name"); - multiRoomStrawberryIDsByName[berryName] = new EntityID(levelName, strawberry.AttrInt("id")); - multiRoomStrawberriesByName[berryName] = strawberry; - } - } - }; - } - - public override void Reset() { - while (FlagTouchSwitches.Count <= AreaKey.ID) { - // fill out the empty space before the current map with empty dictionaries. - FlagTouchSwitches.Add(new List>>()); - } - while (FlagTouchSwitches[AreaKey.ID].Count <= (int) AreaKey.Mode) { - // fill out the empty space before the current map MODE with empty dictionaries. - FlagTouchSwitches[AreaKey.ID].Add(new Dictionary>()); - } - - // reset the dictionary for the current map and mode. - FlagTouchSwitches[AreaKey.ID][(int) AreaKey.Mode] = new Dictionary>(); - } - - public override void End() { - foreach (string strawberryName in multiRoomStrawberrySeedsByName.Keys) { - if (!multiRoomStrawberryIDsByName.ContainsKey(strawberryName)) { - Logger.Log(LogLevel.Warn, "SpringCollab2020MapDataProcessor", $"Multi-room strawberry seeds with name {strawberryName} didn't match any multi-room strawberry"); - } else { - // give the strawberry ID to the seeds. - EntityID strawberryID = multiRoomStrawberryIDsByName[strawberryName]; - foreach (BinaryPacker.Element strawberrySeed in multiRoomStrawberrySeedsByName[strawberryName]) { - strawberrySeed.SetAttr("berryLevel", strawberryID.Level); - strawberrySeed.SetAttr("berryID", strawberryID.ID); - } - - // and give the expected seed count to the strawberry. - multiRoomStrawberriesByName[strawberryName].SetAttr("seedCount", multiRoomStrawberrySeedsByName[strawberryName].Count); - } - } - - multiRoomStrawberrySeedsByName.Clear(); - multiRoomStrawberryIDsByName.Clear(); - } - } -} +using System; +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020 { + class SpringCollab2020MapDataProcessor : EverestMapDataProcessor { + + // the structure here is: FlagTouchSwitches[AreaID][ModeID][flagName] = list of entity ids for flag touch switches in this group on this map. + public static List>>> FlagTouchSwitches = new List>>>(); + private string levelName; + + // we want to match multi-room strawberry seeds with the strawberry that has the same name. + private Dictionary> multiRoomStrawberrySeedsByName = new Dictionary>(); + private Dictionary multiRoomStrawberryIDsByName = new Dictionary(); + private Dictionary multiRoomStrawberriesByName = new Dictionary(); + + public override Dictionary> Init() { + return new Dictionary> { + { + "level", level => { + // be sure to write the level name down. + levelName = level.Attr("name").Split(':')[0]; + if (levelName.StartsWith("lvl_")) { + levelName = levelName.Substring(4); + } + } + }, + { + "entity:SpringCollab2020/FlagTouchSwitch", flagTouchSwitch => { + string flag = flagTouchSwitch.Attr("flag"); + Dictionary> allTouchSwitchesInMap = FlagTouchSwitches[AreaKey.ID][(int) AreaKey.Mode]; + + // if no dictionary entry exists for this flag, create one. otherwise, get it. + List entityIDs; + if (!allTouchSwitchesInMap.ContainsKey(flag)) { + entityIDs = new List(); + allTouchSwitchesInMap[flag] = entityIDs; + } else { + entityIDs = allTouchSwitchesInMap[flag]; + } + + // add this flag touch switch to the dictionary. + entityIDs.Add(new EntityID(levelName, flagTouchSwitch.AttrInt("id"))); + } + }, + { + "entity:SpringCollab2020/MultiRoomStrawberrySeed", strawberrySeed => { + // auto-attribute indices for seeds, and save them. + string berryName = strawberrySeed.Attr("strawberryName"); + if (multiRoomStrawberrySeedsByName.ContainsKey(berryName)) { + if (strawberrySeed.AttrInt("index") < 0) { + strawberrySeed.SetAttr("index", multiRoomStrawberrySeedsByName[berryName].Count); + } + multiRoomStrawberrySeedsByName[berryName].Add(strawberrySeed); + } else { + if (strawberrySeed.AttrInt("index") < 0) { + strawberrySeed.SetAttr("index", 0); + } + multiRoomStrawberrySeedsByName[berryName] = new List() { strawberrySeed }; + } + } + }, + { + "entity:SpringCollab2020/MultiRoomStrawberry", strawberry => { + // save the strawberry IDs. + string berryName = strawberry.Attr("name"); + multiRoomStrawberryIDsByName[berryName] = new EntityID(levelName, strawberry.AttrInt("id")); + multiRoomStrawberriesByName[berryName] = strawberry; + } + } + }; + } + + public override void Reset() { + while (FlagTouchSwitches.Count <= AreaKey.ID) { + // fill out the empty space before the current map with empty dictionaries. + FlagTouchSwitches.Add(new List>>()); + } + while (FlagTouchSwitches[AreaKey.ID].Count <= (int) AreaKey.Mode) { + // fill out the empty space before the current map MODE with empty dictionaries. + FlagTouchSwitches[AreaKey.ID].Add(new Dictionary>()); + } + + // reset the dictionary for the current map and mode. + FlagTouchSwitches[AreaKey.ID][(int) AreaKey.Mode] = new Dictionary>(); + } + + public override void End() { + foreach (string strawberryName in multiRoomStrawberrySeedsByName.Keys) { + if (!multiRoomStrawberryIDsByName.ContainsKey(strawberryName)) { + Logger.Log(LogLevel.Warn, "SpringCollab2020MapDataProcessor", $"Multi-room strawberry seeds with name {strawberryName} didn't match any multi-room strawberry"); + } else { + // give the strawberry ID to the seeds. + EntityID strawberryID = multiRoomStrawberryIDsByName[strawberryName]; + foreach (BinaryPacker.Element strawberrySeed in multiRoomStrawberrySeedsByName[strawberryName]) { + strawberrySeed.SetAttr("berryLevel", strawberryID.Level); + strawberrySeed.SetAttr("berryID", strawberryID.ID); + } + + // and give the expected seed count to the strawberry. + multiRoomStrawberriesByName[strawberryName].SetAttr("seedCount", multiRoomStrawberrySeedsByName[strawberryName].Count); + } + } + + multiRoomStrawberrySeedsByName.Clear(); + multiRoomStrawberryIDsByName.Clear(); + } + } +} diff --git a/SpringCollab2020Module.cs b/SpringCollab2020Module.cs index d448b7d..c76cf5f 100644 --- a/SpringCollab2020Module.cs +++ b/SpringCollab2020Module.cs @@ -1,150 +1,150 @@ -using Celeste.Mod.SpringCollab2020.Effects; -using Celeste.Mod.SpringCollab2020.Entities; -using Celeste.Mod.SpringCollab2020.Triggers; -using Microsoft.Xna.Framework; -using Mono.Cecil.Cil; -using Monocle; -using MonoMod.Cil; -using System; - -namespace Celeste.Mod.SpringCollab2020 { - public class SpringCollab2020Module : EverestModule { - - public static SpringCollab2020Module Instance; - - public override Type SaveDataType => typeof(SpringCollab2020SaveData); - public SpringCollab2020SaveData SaveData => (SpringCollab2020SaveData) _SaveData; - - public override Type SessionType => typeof(SpringCollab2020Session); - public SpringCollab2020Session Session => (SpringCollab2020Session) _Session; - - public SpringCollab2020Module() { - Instance = this; - } - - public override void Load() { - Logger.SetLogLevel("SpringCollab2020", LogLevel.Info); - - NoRefillField.Load(); - FloatierSpaceBlock.Load(); - MoveBlockBarrier.Load(); - MoveBlockBarrierRenderer.Load(); - RemoveLightSourcesTrigger.Load(); - SafeRespawnCrumble.Load(); - UpsideDownJumpThru.Load(); - BubbleReturnBerry.Load(); - SidewaysJumpThru.Load(); - CrystalBombDetonatorRenderer.Load(); - FlagTouchSwitch.Load(); - DisableIcePhysicsTrigger.Load(); - MultiRoomStrawberrySeed.Load(); - MadelineSilhouetteTrigger.Load(); - BlockJellySpawnTrigger.Load(); - StrawberryIgnoringLighting.Load(); - SeekerCustomColors.Load(); - CameraCatchupSpeedTrigger.Load(); - ColorGradeFadeTrigger.Load(); - SpeedBasedMusicParamTrigger.Load(); - StaticPuffer.Load(); - LeaveTheoBehindTrigger.Load(); - BadelineBounceDirectionTrigger.Load(); - SpikeJumpThroughController.Load(); - Everest.Events.Level.OnLoadBackdrop += onLoadBackdrop; - - GrandmasterHeartSideHelper.Load(); - - IL.Celeste.Level.Reload += resetFlagsOnTimerResets; - - DecalRegistry.AddPropertyHandler("SpringCollab2020_scale", (decal, attrs) => { - Vector2 scale = decal.Scale; - if (attrs["multiply"] != null) { - scale *= float.Parse(attrs["multiply"].Value); - } - if (attrs["divide"] != null) { - scale /= float.Parse(attrs["divide"].Value); - } - decal.Scale = scale; - }); - } - - public override void LoadContent(bool firstLoad) { - base.LoadContent(firstLoad); - GlassBerry.LoadContent(); - StrawberryIgnoringLighting.LoadContent(); - } - - public override void Unload() { - NoRefillField.Unload(); - FloatierSpaceBlock.Unload(); - MoveBlockBarrier.Unload(); - MoveBlockBarrierRenderer.Unload(); - RemoveLightSourcesTrigger.Unload(); - SafeRespawnCrumble.Unload(); - GlassBerry.Unload(); - UpsideDownJumpThru.Unload(); - BubbleReturnBerry.Unload(); - SidewaysJumpThru.Unload(); - CrystalBombDetonatorRenderer.Unload(); - FlagTouchSwitch.Unload(); - DisableIcePhysicsTrigger.Unload(); - MultiRoomStrawberrySeed.Unload(); - MadelineSilhouetteTrigger.Unload(); - BlockJellySpawnTrigger.Unload(); - StrawberryIgnoringLighting.Unload(); - SeekerCustomColors.Unload(); - CameraCatchupSpeedTrigger.Unload(); - ColorGradeFadeTrigger.Unload(); - SpeedBasedMusicParamTrigger.Unload(); - StaticPuffer.Unload(); - LeaveTheoBehindTrigger.Unload(); - BadelineBounceDirectionTrigger.Unload(); - SpikeJumpThroughController.Unload(); - Everest.Events.Level.OnLoadBackdrop -= onLoadBackdrop; - - GrandmasterHeartSideHelper.Unload(); - - IL.Celeste.Level.Reload -= resetFlagsOnTimerResets; - } - - private Backdrop onLoadBackdrop(MapData map, BinaryPacker.Element child, BinaryPacker.Element above) { - if (child.Name.Equals("SpringCollab2020/HeatWaveNoColorGrade", StringComparison.OrdinalIgnoreCase)) { - return new HeatWaveNoColorGrade(); - } - if (child.Name.Equals("SpringCollab2020/CustomSnow", StringComparison.OrdinalIgnoreCase)) { - string[] colorsAsStrings = child.Attr("colors").Split(','); - Color[] colors = new Color[colorsAsStrings.Length]; - for (int i = 0; i < colors.Length; i++) { - colors[i] = Calc.HexToColor(colorsAsStrings[i]); - } - - return new CustomSnow(colors, child.AttrBool("foreground")); - } - if (child.Name.Equals("SpringCollab2020/BlackholeCustomColors", StringComparison.OrdinalIgnoreCase)) { - return BlackholeCustomColors.CreateBlackholeWithCustomColors(child); - } - return null; - } - - public override void PrepareMapDataProcessors(MapDataFixup context) { - base.PrepareMapDataProcessors(context); - - context.Add(); - } - - private void resetFlagsOnTimerResets(ILContext il) { - ILCursor cursor = new ILCursor(il); - - if (cursor.TryGotoNext(MoveType.After, instr => instr.MatchStfld("TimerStarted"))) { - Logger.Log("SpringCollab2020", $"Injecting call to reset flags along with timer at {cursor.Index} in IL for Level.Reload"); - - cursor.Emit(OpCodes.Ldarg_0); - cursor.EmitDelegate>(self => { - if (self.Session.Area.GetSID().StartsWith("SpringCollab2020/")) { - Logger.Log(LogLevel.Debug, "SpringCollab2020", "Resetting session flags along with chapter timer"); - self.Session.Flags.Clear(); - } - }); - } - } - } -} +using Celeste.Mod.SpringCollab2020.Effects; +using Celeste.Mod.SpringCollab2020.Entities; +using Celeste.Mod.SpringCollab2020.Triggers; +using Microsoft.Xna.Framework; +using Mono.Cecil.Cil; +using Monocle; +using MonoMod.Cil; +using System; + +namespace Celeste.Mod.SpringCollab2020 { + public class SpringCollab2020Module : EverestModule { + + public static SpringCollab2020Module Instance; + + public override Type SaveDataType => typeof(SpringCollab2020SaveData); + public SpringCollab2020SaveData SaveData => (SpringCollab2020SaveData) _SaveData; + + public override Type SessionType => typeof(SpringCollab2020Session); + public SpringCollab2020Session Session => (SpringCollab2020Session) _Session; + + public SpringCollab2020Module() { + Instance = this; + } + + public override void Load() { + Logger.SetLogLevel("SpringCollab2020", LogLevel.Info); + + NoRefillField.Load(); + FloatierSpaceBlock.Load(); + MoveBlockBarrier.Load(); + MoveBlockBarrierRenderer.Load(); + RemoveLightSourcesTrigger.Load(); + SafeRespawnCrumble.Load(); + UpsideDownJumpThru.Load(); + BubbleReturnBerry.Load(); + SidewaysJumpThru.Load(); + CrystalBombDetonatorRenderer.Load(); + FlagTouchSwitch.Load(); + DisableIcePhysicsTrigger.Load(); + MultiRoomStrawberrySeed.Load(); + MadelineSilhouetteTrigger.Load(); + BlockJellySpawnTrigger.Load(); + StrawberryIgnoringLighting.Load(); + SeekerCustomColors.Load(); + CameraCatchupSpeedTrigger.Load(); + ColorGradeFadeTrigger.Load(); + SpeedBasedMusicParamTrigger.Load(); + StaticPuffer.Load(); + LeaveTheoBehindTrigger.Load(); + BadelineBounceDirectionTrigger.Load(); + SpikeJumpThroughController.Load(); + Everest.Events.Level.OnLoadBackdrop += onLoadBackdrop; + + GrandmasterHeartSideHelper.Load(); + + IL.Celeste.Level.Reload += resetFlagsOnTimerResets; + + DecalRegistry.AddPropertyHandler("SpringCollab2020_scale", (decal, attrs) => { + Vector2 scale = decal.Scale; + if (attrs["multiply"] != null) { + scale *= float.Parse(attrs["multiply"].Value); + } + if (attrs["divide"] != null) { + scale /= float.Parse(attrs["divide"].Value); + } + decal.Scale = scale; + }); + } + + public override void LoadContent(bool firstLoad) { + base.LoadContent(firstLoad); + GlassBerry.LoadContent(); + StrawberryIgnoringLighting.LoadContent(); + } + + public override void Unload() { + NoRefillField.Unload(); + FloatierSpaceBlock.Unload(); + MoveBlockBarrier.Unload(); + MoveBlockBarrierRenderer.Unload(); + RemoveLightSourcesTrigger.Unload(); + SafeRespawnCrumble.Unload(); + GlassBerry.Unload(); + UpsideDownJumpThru.Unload(); + BubbleReturnBerry.Unload(); + SidewaysJumpThru.Unload(); + CrystalBombDetonatorRenderer.Unload(); + FlagTouchSwitch.Unload(); + DisableIcePhysicsTrigger.Unload(); + MultiRoomStrawberrySeed.Unload(); + MadelineSilhouetteTrigger.Unload(); + BlockJellySpawnTrigger.Unload(); + StrawberryIgnoringLighting.Unload(); + SeekerCustomColors.Unload(); + CameraCatchupSpeedTrigger.Unload(); + ColorGradeFadeTrigger.Unload(); + SpeedBasedMusicParamTrigger.Unload(); + StaticPuffer.Unload(); + LeaveTheoBehindTrigger.Unload(); + BadelineBounceDirectionTrigger.Unload(); + SpikeJumpThroughController.Unload(); + Everest.Events.Level.OnLoadBackdrop -= onLoadBackdrop; + + GrandmasterHeartSideHelper.Unload(); + + IL.Celeste.Level.Reload -= resetFlagsOnTimerResets; + } + + private Backdrop onLoadBackdrop(MapData map, BinaryPacker.Element child, BinaryPacker.Element above) { + if (child.Name.Equals("SpringCollab2020/HeatWaveNoColorGrade", StringComparison.OrdinalIgnoreCase)) { + return new HeatWaveNoColorGrade(); + } + if (child.Name.Equals("SpringCollab2020/CustomSnow", StringComparison.OrdinalIgnoreCase)) { + string[] colorsAsStrings = child.Attr("colors").Split(','); + Color[] colors = new Color[colorsAsStrings.Length]; + for (int i = 0; i < colors.Length; i++) { + colors[i] = Calc.HexToColor(colorsAsStrings[i]); + } + + return new CustomSnow(colors, child.AttrBool("foreground")); + } + if (child.Name.Equals("SpringCollab2020/BlackholeCustomColors", StringComparison.OrdinalIgnoreCase)) { + return BlackholeCustomColors.CreateBlackholeWithCustomColors(child); + } + return null; + } + + public override void PrepareMapDataProcessors(MapDataFixup context) { + base.PrepareMapDataProcessors(context); + + context.Add(); + } + + private void resetFlagsOnTimerResets(ILContext il) { + ILCursor cursor = new ILCursor(il); + + if (cursor.TryGotoNext(MoveType.After, instr => instr.MatchStfld("TimerStarted"))) { + Logger.Log("SpringCollab2020", $"Injecting call to reset flags along with timer at {cursor.Index} in IL for Level.Reload"); + + cursor.Emit(OpCodes.Ldarg_0); + cursor.EmitDelegate>(self => { + if (self.Session.Area.GetSID().StartsWith("SpringCollab2020/")) { + Logger.Log(LogLevel.Debug, "SpringCollab2020", "Resetting session flags along with chapter timer"); + self.Session.Flags.Clear(); + } + }); + } + } + } +} diff --git a/SpringCollab2020SaveData.cs b/SpringCollab2020SaveData.cs index 2d1cd91..2ceaa8e 100755 --- a/SpringCollab2020SaveData.cs +++ b/SpringCollab2020SaveData.cs @@ -1,8 +1,8 @@ - -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020 { - public class SpringCollab2020SaveData : EverestModuleSaveData { - public HashSet ModifiedThemeMaps = new HashSet() { }; - } -} + +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020 { + public class SpringCollab2020SaveData : EverestModuleSaveData { + public HashSet ModifiedThemeMaps = new HashSet() { }; + } +} diff --git a/SpringCollab2020Session.cs b/SpringCollab2020Session.cs index 1283d26..a14c855 100644 --- a/SpringCollab2020Session.cs +++ b/SpringCollab2020Session.cs @@ -1,32 +1,32 @@ - -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020 { - public class SpringCollab2020Session : EverestModuleSession { - public class MultiRoomStrawberrySeedInfo { - public int Index { get; set; } - public EntityID BerryID { get; set; } - public string Sprite { get; set; } - public bool IgnoreLighting { get; set; } - } - - public bool IcePhysicsDisabled { get; set; } = false; - - public List CollectedMultiRoomStrawberrySeeds { get; set; } = new List(); - - public bool MadelineIsSilhouette { get; set; } = false; - - public bool LightSourcesDisabled { get; set; } = false; - - public bool SpikeJumpThroughHooked { get; set; } = false; - - public class SpeedBasedMusicParamInfo { - public float MinimumSpeed { get; set; } - public float MaximumSpeed { get; set; } - public float MinimumParamValue { get; set; } - public float MaximumParamValue { get; set; } - } - - public Dictionary ActiveSpeedBasedMusicParams = new Dictionary(); - } -} + +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020 { + public class SpringCollab2020Session : EverestModuleSession { + public class MultiRoomStrawberrySeedInfo { + public int Index { get; set; } + public EntityID BerryID { get; set; } + public string Sprite { get; set; } + public bool IgnoreLighting { get; set; } + } + + public bool IcePhysicsDisabled { get; set; } = false; + + public List CollectedMultiRoomStrawberrySeeds { get; set; } = new List(); + + public bool MadelineIsSilhouette { get; set; } = false; + + public bool LightSourcesDisabled { get; set; } = false; + + public bool SpikeJumpThroughHooked { get; set; } = false; + + public class SpeedBasedMusicParamInfo { + public float MinimumSpeed { get; set; } + public float MaximumSpeed { get; set; } + public float MinimumParamValue { get; set; } + public float MaximumParamValue { get; set; } + } + + public Dictionary ActiveSpeedBasedMusicParams = new Dictionary(); + } +} diff --git a/Triggers/BadelineBounceDirectionTrigger.cs b/Triggers/BadelineBounceDirectionTrigger.cs index 7cadd52..b76271a 100755 --- a/Triggers/BadelineBounceDirectionTrigger.cs +++ b/Triggers/BadelineBounceDirectionTrigger.cs @@ -1,33 +1,33 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/BadelineBounceDirectionTrigger")] - [Tracked] - class BadelineBounceDirectionTrigger : Trigger { - public static void Load() { - On.Celeste.Player.FinalBossPushLaunch += onPlayerBadelinePushLaunch; - } - - public static void Unload() { - On.Celeste.Player.FinalBossPushLaunch -= onPlayerBadelinePushLaunch; - } - - private static void onPlayerBadelinePushLaunch(On.Celeste.Player.orig_FinalBossPushLaunch orig, Player self, int dir) { - // if the player is inside a Badeline Bounce Direction Trigger, mod the bounce direction to be the one we want. - BadelineBounceDirectionTrigger trigger = self.CollideFirst(); - if (trigger != null) { - dir = trigger.bounceLeft ? -1 : 1; - } - - orig(self, dir); - } - - private bool bounceLeft; - - public BadelineBounceDirectionTrigger(EntityData data, Vector2 offset) : base(data, offset) { - bounceLeft = data.Bool("bounceLeft"); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/BadelineBounceDirectionTrigger")] + [Tracked] + class BadelineBounceDirectionTrigger : Trigger { + public static void Load() { + On.Celeste.Player.FinalBossPushLaunch += onPlayerBadelinePushLaunch; + } + + public static void Unload() { + On.Celeste.Player.FinalBossPushLaunch -= onPlayerBadelinePushLaunch; + } + + private static void onPlayerBadelinePushLaunch(On.Celeste.Player.orig_FinalBossPushLaunch orig, Player self, int dir) { + // if the player is inside a Badeline Bounce Direction Trigger, mod the bounce direction to be the one we want. + BadelineBounceDirectionTrigger trigger = self.CollideFirst(); + if (trigger != null) { + dir = trigger.bounceLeft ? -1 : 1; + } + + orig(self, dir); + } + + private bool bounceLeft; + + public BadelineBounceDirectionTrigger(EntityData data, Vector2 offset) : base(data, offset) { + bounceLeft = data.Bool("bounceLeft"); + } + } +} diff --git a/Triggers/BlockJellySpawnTrigger.cs b/Triggers/BlockJellySpawnTrigger.cs index 4f6f9c9..2abc4ba 100644 --- a/Triggers/BlockJellySpawnTrigger.cs +++ b/Triggers/BlockJellySpawnTrigger.cs @@ -1,33 +1,33 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System.Linq; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/BlockJellySpawnTrigger")] - [Tracked] - class BlockJellySpawnTrigger : Trigger { - public static void Load() { - On.Celeste.Level.LoadLevel += onLoadLevel; - } - - public static void Unload() { - On.Celeste.Level.LoadLevel -= onLoadLevel; - } - - private static void onLoadLevel(On.Celeste.Level.orig_LoadLevel orig, Level self, Player.IntroTypes playerIntro, bool isFromLoader) { - orig(self, playerIntro, isFromLoader); - - // if the player spawned in a Block Jelly Spawn Trigger... - if (playerIntro == Player.IntroTypes.Respawn && (self.Tracker.GetEntity()?.CollideCheck() ?? false)) { - // remove all jellyfish from the room. - foreach (Glider jelly in self.Entities.OfType()) { - jelly.RemoveSelf(); - } - self.Entities.UpdateLists(); - } - } - - public BlockJellySpawnTrigger(EntityData data, Vector2 offset) : base(data, offset) { } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System.Linq; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/BlockJellySpawnTrigger")] + [Tracked] + class BlockJellySpawnTrigger : Trigger { + public static void Load() { + On.Celeste.Level.LoadLevel += onLoadLevel; + } + + public static void Unload() { + On.Celeste.Level.LoadLevel -= onLoadLevel; + } + + private static void onLoadLevel(On.Celeste.Level.orig_LoadLevel orig, Level self, Player.IntroTypes playerIntro, bool isFromLoader) { + orig(self, playerIntro, isFromLoader); + + // if the player spawned in a Block Jelly Spawn Trigger... + if (playerIntro == Player.IntroTypes.Respawn && (self.Tracker.GetEntity()?.CollideCheck() ?? false)) { + // remove all jellyfish from the room. + foreach (Glider jelly in self.Entities.OfType()) { + jelly.RemoveSelf(); + } + self.Entities.UpdateLists(); + } + } + + public BlockJellySpawnTrigger(EntityData data, Vector2 offset) : base(data, offset) { } + } +} diff --git a/Triggers/CameraCatchupSpeedTrigger.cs b/Triggers/CameraCatchupSpeedTrigger.cs index e7300c6..45cba2a 100644 --- a/Triggers/CameraCatchupSpeedTrigger.cs +++ b/Triggers/CameraCatchupSpeedTrigger.cs @@ -1,56 +1,56 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Mono.Cecil.Cil; -using Monocle; -using MonoMod.Cil; -using MonoMod.RuntimeDetour; -using System; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/CameraCatchupSpeedTrigger")] - [Tracked] - class CameraCatchupSpeedTrigger : Trigger { - private static ILHook playerOrigUpdateHook; - - public static void Load() { - playerOrigUpdateHook = new ILHook(typeof(Player).GetMethod("orig_Update"), modPlayerOrigUpdate); - } - - public static void Unload() { - playerOrigUpdateHook?.Dispose(); - } - - private static void modPlayerOrigUpdate(ILContext il) { - ILCursor cursor = new ILCursor(il); - - // we're looking for: 1f - (float)Math.Pow(0.01f / num2, Engine.DeltaTime) - if (cursor.TryGotoNext( - instr => instr.MatchLdcR4(0.01f), - instr => instr.OpCode == OpCodes.Ldloc_S, - instr => instr.MatchDiv())) { - - // and we want to position the cursor just after loading num2 - cursor.Index += 2; - - Logger.Log("SpringCollab2020/CameraCatchupSpeedTrigger", $"Inserting code to mod camera catchup speed at {cursor.Index} in IL for Player.orig_Update()"); - - // this delegate will allow us to turn num2 into something else. - cursor.Emit(OpCodes.Ldarg_0); - cursor.EmitDelegate>((orig, self) => { - CameraCatchupSpeedTrigger trigger = self.CollideFirst(); - if (trigger != null) { - return trigger.catchupSpeed; - } - return orig; - }); - } - } - - - private float catchupSpeed; - - public CameraCatchupSpeedTrigger(EntityData data, Vector2 offset) : base(data, offset) { - catchupSpeed = data.Float("catchupSpeed", 1f); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Mono.Cecil.Cil; +using Monocle; +using MonoMod.Cil; +using MonoMod.RuntimeDetour; +using System; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/CameraCatchupSpeedTrigger")] + [Tracked] + class CameraCatchupSpeedTrigger : Trigger { + private static ILHook playerOrigUpdateHook; + + public static void Load() { + playerOrigUpdateHook = new ILHook(typeof(Player).GetMethod("orig_Update"), modPlayerOrigUpdate); + } + + public static void Unload() { + playerOrigUpdateHook?.Dispose(); + } + + private static void modPlayerOrigUpdate(ILContext il) { + ILCursor cursor = new ILCursor(il); + + // we're looking for: 1f - (float)Math.Pow(0.01f / num2, Engine.DeltaTime) + if (cursor.TryGotoNext( + instr => instr.MatchLdcR4(0.01f), + instr => instr.OpCode == OpCodes.Ldloc_S, + instr => instr.MatchDiv())) { + + // and we want to position the cursor just after loading num2 + cursor.Index += 2; + + Logger.Log("SpringCollab2020/CameraCatchupSpeedTrigger", $"Inserting code to mod camera catchup speed at {cursor.Index} in IL for Player.orig_Update()"); + + // this delegate will allow us to turn num2 into something else. + cursor.Emit(OpCodes.Ldarg_0); + cursor.EmitDelegate>((orig, self) => { + CameraCatchupSpeedTrigger trigger = self.CollideFirst(); + if (trigger != null) { + return trigger.catchupSpeed; + } + return orig; + }); + } + } + + + private float catchupSpeed; + + public CameraCatchupSpeedTrigger(EntityData data, Vector2 offset) : base(data, offset) { + catchupSpeed = data.Float("catchupSpeed", 1f); + } + } +} diff --git a/Triggers/CancelLightningRemoveRoutineTrigger.cs b/Triggers/CancelLightningRemoveRoutineTrigger.cs index ba17336..c52cd2e 100755 --- a/Triggers/CancelLightningRemoveRoutineTrigger.cs +++ b/Triggers/CancelLightningRemoveRoutineTrigger.cs @@ -1,29 +1,29 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System.Linq; -using System.Reflection; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/CancelLightningRemoveRoutineTrigger")] - class CancelLightningRemoveRoutineTrigger : Trigger { - private static MethodInfo lightningSetBreakValue = typeof(Lightning).GetMethod("SetBreakValue", BindingFlags.Static | BindingFlags.NonPublic); - - public CancelLightningRemoveRoutineTrigger(EntityData data, Vector2 offset) : base(data, offset) { } - - public override void OnEnter(Player player) { - base.OnEnter(player); - - if (Scene.Entities.FirstOrDefault(entity => entity is LightningBreakerBox && entity.TagCheck(Tags.Persistent)) - is LightningBreakerBox box) { - - // break routine is in progress! delete the box running it, and reset the "break value" to zero. - box.Remove(box.Get()); - box.RemoveSelf(); - lightningSetBreakValue.Invoke(null /* static method */, new object[] { Scene as Level, 0f }); - } - - RemoveSelf(); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System.Linq; +using System.Reflection; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/CancelLightningRemoveRoutineTrigger")] + class CancelLightningRemoveRoutineTrigger : Trigger { + private static MethodInfo lightningSetBreakValue = typeof(Lightning).GetMethod("SetBreakValue", BindingFlags.Static | BindingFlags.NonPublic); + + public CancelLightningRemoveRoutineTrigger(EntityData data, Vector2 offset) : base(data, offset) { } + + public override void OnEnter(Player player) { + base.OnEnter(player); + + if (Scene.Entities.FirstOrDefault(entity => entity is LightningBreakerBox && entity.TagCheck(Tags.Persistent)) + is LightningBreakerBox box) { + + // break routine is in progress! delete the box running it, and reset the "break value" to zero. + box.Remove(box.Get()); + box.RemoveSelf(); + lightningSetBreakValue.Invoke(null /* static method */, new object[] { Scene as Level, 0f }); + } + + RemoveSelf(); + } + } +} diff --git a/Triggers/ChangeThemeTrigger.cs b/Triggers/ChangeThemeTrigger.cs index 26cfb4f..7ed3e55 100755 --- a/Triggers/ChangeThemeTrigger.cs +++ b/Triggers/ChangeThemeTrigger.cs @@ -1,128 +1,128 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - // a very hardcoded trigger that is here to persist theme choices between play sessions. - [CustomEntity("SpringCollab2020/ChangeThemeTrigger")] - public class ChangeThemeTrigger : Trigger { - private readonly bool enable; - - public ChangeThemeTrigger(EntityData data, Vector2 offset) : base(data, offset) { - enable = data.Bool("enable", false); - } - - public override void Added(Scene scene) { - base.Added(scene); - - // let's restore the saved theme... - Level level = Scene as Level; - string sid = level.Session.Area.GetSID(); - bool enabled = SpringCollab2020Module.Instance.SaveData.ModifiedThemeMaps.Contains(sid); - if (enabled) { - switch (sid) { - case "SpringCollab2020/3-Advanced/LinjKarma": - level.Session.SetFlag("skylinesoff"); - break; - case "SpringCollab2020/3-Advanced/NeoKat": - setBloom(level, -0.12f); - break; - case "SpringCollab2020/3-Advanced/RealVet": - level.Session.SetFlag("ignore_darkness_Room1"); - level.Session.SetFlag("ignore_darkness_Room2"); - level.Session.SetFlag("ignore_darkness_Room3"); - level.Session.SetFlag("ignore_darkness_Room4"); - level.Session.SetFlag("ignore_darkness_Room5"); - level.Session.SetFlag("ignore_darkness_Strawberry1"); - level.Session.SetFlag("ignore_darkness_Strawberry2"); - level.Session.SetFlag("ignore_darkness_Strawberry3"); - level.Session.SetFlag("ignore_darkness_BeginningRoom"); - level.Session.SetFlag("ignore_darkness_DebugRoom"); - level.Session.SetFlag("ignore_darkness_EndRoom"); - level.Session.LightingAlphaAdd = 0.3f; - - // and no, the current room is not dark. - level.Lighting.Alpha = level.BaseLightingAlpha + level.Session.LightingAlphaAdd; - level.DarkRoom = false; - break; - case "SpringCollab2020/4-Expert/Mun": - level.Session.SetFlag("darkmode"); - level.SnapColorGrade("panicattack"); - setBloom(level, 0.3f); - level.Session.LightingAlphaAdd = 0.25f; - level.Lighting.Alpha = level.BaseLightingAlpha + level.Session.LightingAlphaAdd; - break; - case "SpringCollab2020/4-Expert/Zerex": - level.Session.SetFlag("darker"); - setBloom(level, 0.65f); - break; - case "SpringCollab2020/5-Grandmaster/BobDole": - level.Session.SetFlag("boblight"); - level.SnapColorGrade("bobgrade"); - break; - } - } else { - switch (sid) { - case "SpringCollab2020/3-Advanced/NeoKat": - level.Session.SetFlag("sc2020_nyoom_normalmode"); - break; - case "SpringCollab2020/4-Expert/Zerex": - setBloom(level, 2.5f); - break; - } - } - } - - public override void Awake(Scene scene) { - base.Awake(scene); - - // let's restore the saved theme... - Level level = Scene as Level; - string sid = level.Session.Area.GetSID(); - bool enabled = SpringCollab2020Module.Instance.SaveData.ModifiedThemeMaps.Contains(sid); - if (enabled) { - switch (sid) { - case "SpringCollab2020/1-Beginner/DanTKO": - triggerTrigger("LightningColorTrigger", -456, 32); - triggerTrigger("ExtendedVariantTrigger", -456, 32); - - level.Session.LightingAlphaAdd = 0.095f; - level.Lighting.Alpha = level.BaseLightingAlpha + level.Session.LightingAlphaAdd; - break; - } - } else { - switch (sid) { - case "SpringCollab2020/1-Beginner/DanTKO": - triggerTrigger("ExtendedVariantTrigger", -448, 136); - break; - } - } - } - - private void triggerTrigger(string triggerName, int x, int y) { - foreach (Trigger trigger in Scene.Tracker.GetEntities()) { - if (trigger.GetType().Name.Contains(triggerName) && trigger.X == x && trigger.Y == y) { - trigger.OnEnter(Scene.Tracker.GetEntity()); - trigger.OnLeave(Scene.Tracker.GetEntity()); - break; - } - } - } - - private void setBloom(Level level, float bloomAdd) { - level.Session.BloomBaseAdd = bloomAdd; - level.Bloom.Base = AreaData.Get(level).BloomBase + bloomAdd; - } - - public override void OnEnter(Player player) { - base.OnEnter(player); - - if (enable) { - SpringCollab2020Module.Instance.SaveData.ModifiedThemeMaps.Add((Scene as Level).Session.Area.GetSID()); - } else { - SpringCollab2020Module.Instance.SaveData.ModifiedThemeMaps.Remove((Scene as Level).Session.Area.GetSID()); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + // a very hardcoded trigger that is here to persist theme choices between play sessions. + [CustomEntity("SpringCollab2020/ChangeThemeTrigger")] + public class ChangeThemeTrigger : Trigger { + private readonly bool enable; + + public ChangeThemeTrigger(EntityData data, Vector2 offset) : base(data, offset) { + enable = data.Bool("enable", false); + } + + public override void Added(Scene scene) { + base.Added(scene); + + // let's restore the saved theme... + Level level = Scene as Level; + string sid = level.Session.Area.GetSID(); + bool enabled = SpringCollab2020Module.Instance.SaveData.ModifiedThemeMaps.Contains(sid); + if (enabled) { + switch (sid) { + case "SpringCollab2020/3-Advanced/LinjKarma": + level.Session.SetFlag("skylinesoff"); + break; + case "SpringCollab2020/3-Advanced/NeoKat": + setBloom(level, -0.12f); + break; + case "SpringCollab2020/3-Advanced/RealVet": + level.Session.SetFlag("ignore_darkness_Room1"); + level.Session.SetFlag("ignore_darkness_Room2"); + level.Session.SetFlag("ignore_darkness_Room3"); + level.Session.SetFlag("ignore_darkness_Room4"); + level.Session.SetFlag("ignore_darkness_Room5"); + level.Session.SetFlag("ignore_darkness_Strawberry1"); + level.Session.SetFlag("ignore_darkness_Strawberry2"); + level.Session.SetFlag("ignore_darkness_Strawberry3"); + level.Session.SetFlag("ignore_darkness_BeginningRoom"); + level.Session.SetFlag("ignore_darkness_DebugRoom"); + level.Session.SetFlag("ignore_darkness_EndRoom"); + level.Session.LightingAlphaAdd = 0.3f; + + // and no, the current room is not dark. + level.Lighting.Alpha = level.BaseLightingAlpha + level.Session.LightingAlphaAdd; + level.DarkRoom = false; + break; + case "SpringCollab2020/4-Expert/Mun": + level.Session.SetFlag("darkmode"); + level.SnapColorGrade("panicattack"); + setBloom(level, 0.3f); + level.Session.LightingAlphaAdd = 0.25f; + level.Lighting.Alpha = level.BaseLightingAlpha + level.Session.LightingAlphaAdd; + break; + case "SpringCollab2020/4-Expert/Zerex": + level.Session.SetFlag("darker"); + setBloom(level, 0.65f); + break; + case "SpringCollab2020/5-Grandmaster/BobDole": + level.Session.SetFlag("boblight"); + level.SnapColorGrade("bobgrade"); + break; + } + } else { + switch (sid) { + case "SpringCollab2020/3-Advanced/NeoKat": + level.Session.SetFlag("sc2020_nyoom_normalmode"); + break; + case "SpringCollab2020/4-Expert/Zerex": + setBloom(level, 2.5f); + break; + } + } + } + + public override void Awake(Scene scene) { + base.Awake(scene); + + // let's restore the saved theme... + Level level = Scene as Level; + string sid = level.Session.Area.GetSID(); + bool enabled = SpringCollab2020Module.Instance.SaveData.ModifiedThemeMaps.Contains(sid); + if (enabled) { + switch (sid) { + case "SpringCollab2020/1-Beginner/DanTKO": + triggerTrigger("LightningColorTrigger", -456, 32); + triggerTrigger("ExtendedVariantTrigger", -456, 32); + + level.Session.LightingAlphaAdd = 0.095f; + level.Lighting.Alpha = level.BaseLightingAlpha + level.Session.LightingAlphaAdd; + break; + } + } else { + switch (sid) { + case "SpringCollab2020/1-Beginner/DanTKO": + triggerTrigger("ExtendedVariantTrigger", -448, 136); + break; + } + } + } + + private void triggerTrigger(string triggerName, int x, int y) { + foreach (Trigger trigger in Scene.Tracker.GetEntities()) { + if (trigger.GetType().Name.Contains(triggerName) && trigger.X == x && trigger.Y == y) { + trigger.OnEnter(Scene.Tracker.GetEntity()); + trigger.OnLeave(Scene.Tracker.GetEntity()); + break; + } + } + } + + private void setBloom(Level level, float bloomAdd) { + level.Session.BloomBaseAdd = bloomAdd; + level.Bloom.Base = AreaData.Get(level).BloomBase + bloomAdd; + } + + public override void OnEnter(Player player) { + base.OnEnter(player); + + if (enable) { + SpringCollab2020Module.Instance.SaveData.ModifiedThemeMaps.Add((Scene as Level).Session.Area.GetSID()); + } else { + SpringCollab2020Module.Instance.SaveData.ModifiedThemeMaps.Remove((Scene as Level).Session.Area.GetSID()); + } + } + } +} diff --git a/Triggers/ColorGradeFadeTrigger.cs b/Triggers/ColorGradeFadeTrigger.cs index e08d5dd..9139bf1 100644 --- a/Triggers/ColorGradeFadeTrigger.cs +++ b/Triggers/ColorGradeFadeTrigger.cs @@ -1,56 +1,56 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/ColorGradeFadeTrigger")] - [Tracked] - class ColorGradeFadeTrigger : Trigger { - public static void Load() { - On.Celeste.Level.Update += onLevelUpdate; - } - - public static void Unload() { - On.Celeste.Level.Update -= onLevelUpdate; - } - - private static void onLevelUpdate(On.Celeste.Level.orig_Update orig, Level self) { - orig(self); - - // check if the player is in a color grade fade trigger - Player player = self.Tracker.GetEntity(); - ColorGradeFadeTrigger trigger = player?.CollideFirst(); - if (trigger != null) { - DynData selfData = new DynData(self); - - // the game fades from lastColorGrade to Session.ColorGrade using colorGradeEase as a lerp value. - // let's hijack that! - float positionLerp = trigger.GetPositionLerp(player, trigger.direction); - if (positionLerp > 0.5f) { - // we are closer to B. let B be the target color grade when player exits the trigger / dies in it - selfData["lastColorGrade"] = trigger.colorGradeA; - self.Session.ColorGrade = trigger.colorGradeB; - selfData["colorGradeEase"] = positionLerp; - } else { - // we are closer to A. let A be the target color grade when player exits the trigger / dies in it - selfData["lastColorGrade"] = trigger.colorGradeB; - self.Session.ColorGrade = trigger.colorGradeA; - selfData["colorGradeEase"] = 1 - positionLerp; - } - selfData["colorGradeEaseSpeed"] = 1f; - } - } - - - private string colorGradeA; - private string colorGradeB; - private PositionModes direction; - - public ColorGradeFadeTrigger(EntityData data, Vector2 offset) : base(data, offset) { - colorGradeA = data.Attr("colorGradeA"); - colorGradeB = data.Attr("colorGradeB"); - direction = data.Enum("direction"); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/ColorGradeFadeTrigger")] + [Tracked] + class ColorGradeFadeTrigger : Trigger { + public static void Load() { + On.Celeste.Level.Update += onLevelUpdate; + } + + public static void Unload() { + On.Celeste.Level.Update -= onLevelUpdate; + } + + private static void onLevelUpdate(On.Celeste.Level.orig_Update orig, Level self) { + orig(self); + + // check if the player is in a color grade fade trigger + Player player = self.Tracker.GetEntity(); + ColorGradeFadeTrigger trigger = player?.CollideFirst(); + if (trigger != null) { + DynData selfData = new DynData(self); + + // the game fades from lastColorGrade to Session.ColorGrade using colorGradeEase as a lerp value. + // let's hijack that! + float positionLerp = trigger.GetPositionLerp(player, trigger.direction); + if (positionLerp > 0.5f) { + // we are closer to B. let B be the target color grade when player exits the trigger / dies in it + selfData["lastColorGrade"] = trigger.colorGradeA; + self.Session.ColorGrade = trigger.colorGradeB; + selfData["colorGradeEase"] = positionLerp; + } else { + // we are closer to A. let A be the target color grade when player exits the trigger / dies in it + selfData["lastColorGrade"] = trigger.colorGradeB; + self.Session.ColorGrade = trigger.colorGradeA; + selfData["colorGradeEase"] = 1 - positionLerp; + } + selfData["colorGradeEaseSpeed"] = 1f; + } + } + + + private string colorGradeA; + private string colorGradeB; + private PositionModes direction; + + public ColorGradeFadeTrigger(EntityData data, Vector2 offset) : base(data, offset) { + colorGradeA = data.Attr("colorGradeA"); + colorGradeB = data.Attr("colorGradeB"); + direction = data.Enum("direction"); + } + } +} diff --git a/Triggers/CustomBirdTutorialTrigger.cs b/Triggers/CustomBirdTutorialTrigger.cs index 46f8716..f27e8e7 100644 --- a/Triggers/CustomBirdTutorialTrigger.cs +++ b/Triggers/CustomBirdTutorialTrigger.cs @@ -1,30 +1,30 @@ -using Celeste.Mod.SpringCollab2020.Entities; -using Microsoft.Xna.Framework; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [Mod.Entities.CustomEntity("SpringCollab2020/CustomBirdTutorialTrigger")] - class CustomBirdTutorialTrigger : Trigger { - private string birdId; - private bool showTutorial; - - public CustomBirdTutorialTrigger(EntityData data, Vector2 offset) : base(data, offset) { - birdId = data.Attr("birdId"); - showTutorial = data.Bool("showTutorial"); - } - - public override void OnEnter(Player player) { - base.OnEnter(player); - - CustomBirdTutorial matchingBird = (CustomBirdTutorial) - Scene.Tracker.GetEntities().Find(entity => entity is CustomBirdTutorial bird && bird.BirdId == birdId); - - if (matchingBird != null) { - if (showTutorial) { - matchingBird.TriggerShowTutorial(); - } else { - matchingBird.TriggerHideTutorial(); - } - } - } - } -} +using Celeste.Mod.SpringCollab2020.Entities; +using Microsoft.Xna.Framework; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [Mod.Entities.CustomEntity("SpringCollab2020/CustomBirdTutorialTrigger")] + class CustomBirdTutorialTrigger : Trigger { + private string birdId; + private bool showTutorial; + + public CustomBirdTutorialTrigger(EntityData data, Vector2 offset) : base(data, offset) { + birdId = data.Attr("birdId"); + showTutorial = data.Bool("showTutorial"); + } + + public override void OnEnter(Player player) { + base.OnEnter(player); + + CustomBirdTutorial matchingBird = (CustomBirdTutorial) + Scene.Tracker.GetEntities().Find(entity => entity is CustomBirdTutorial bird && bird.BirdId == birdId); + + if (matchingBird != null) { + if (showTutorial) { + matchingBird.TriggerShowTutorial(); + } else { + matchingBird.TriggerHideTutorial(); + } + } + } + } +} diff --git a/Triggers/CustomSandwichLavaSettingsTrigger.cs b/Triggers/CustomSandwichLavaSettingsTrigger.cs index e32a4b2..a4609f3 100644 --- a/Triggers/CustomSandwichLavaSettingsTrigger.cs +++ b/Triggers/CustomSandwichLavaSettingsTrigger.cs @@ -1,33 +1,33 @@ -using Celeste.Mod.Entities; -using Celeste.Mod.SpringCollab2020.Entities; -using Microsoft.Xna.Framework; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/CustomSandwichLavaSettingsTrigger")] - class CustomSandwichLavaSettingsTrigger : Trigger { - - private bool onlyOnce; - private CustomSandwichLava.DirectionMode direction; - private float speed; - - public CustomSandwichLavaSettingsTrigger(EntityData data, Vector2 offset) : base(data, offset) { - onlyOnce = data.Bool("onlyOnce"); - direction = data.Enum("direction", CustomSandwichLava.DirectionMode.CoreModeBased); - speed = data.Float("speed", 20f); - } - - public override void OnEnter(Player player) { - base.OnEnter(player); - - CustomSandwichLava target = SceneAs().Tracker.GetEntity(); - if (target != null) { - target.Direction = direction; - target.Speed = speed; - } - - if (onlyOnce) { - RemoveSelf(); - } - } - } -} +using Celeste.Mod.Entities; +using Celeste.Mod.SpringCollab2020.Entities; +using Microsoft.Xna.Framework; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/CustomSandwichLavaSettingsTrigger")] + class CustomSandwichLavaSettingsTrigger : Trigger { + + private bool onlyOnce; + private CustomSandwichLava.DirectionMode direction; + private float speed; + + public CustomSandwichLavaSettingsTrigger(EntityData data, Vector2 offset) : base(data, offset) { + onlyOnce = data.Bool("onlyOnce"); + direction = data.Enum("direction", CustomSandwichLava.DirectionMode.CoreModeBased); + speed = data.Float("speed", 20f); + } + + public override void OnEnter(Player player) { + base.OnEnter(player); + + CustomSandwichLava target = SceneAs().Tracker.GetEntity(); + if (target != null) { + target.Direction = direction; + target.Speed = speed; + } + + if (onlyOnce) { + RemoveSelf(); + } + } + } +} diff --git a/Triggers/DisableIcePhysicsTrigger.cs b/Triggers/DisableIcePhysicsTrigger.cs index 6dddb9d..5bb89f3 100644 --- a/Triggers/DisableIcePhysicsTrigger.cs +++ b/Triggers/DisableIcePhysicsTrigger.cs @@ -1,45 +1,45 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using MonoMod.Cil; -using System; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/DisableIcePhysicsTrigger")] - class DisableIcePhysicsTrigger : Trigger { - public static void Load() { - IL.Celeste.Player.NormalUpdate += modPlayerNormalUpdate; - } - - public static void Unload() { - IL.Celeste.Player.NormalUpdate -= modPlayerNormalUpdate; - } - - private static void modPlayerNormalUpdate(ILContext il) { - ILCursor cursor = new ILCursor(il); - - while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("get_CoreMode"))) { - Logger.Log("SpringCollab2020/DisableIcePhysicsTrigger", $"Patching ice mode checking at {cursor.Index} in IL for Player.NormalUpdate"); - - cursor.EmitDelegate>(coreMode => { - if (SpringCollab2020Module.Instance.Session.IcePhysicsDisabled) { - // pretend there is no core mode, so that the ground is not slippery anymore. - return Session.CoreModes.None; - } - return coreMode; - }); - } - } - - private bool disableIcePhysics; - - public DisableIcePhysicsTrigger(EntityData data, Vector2 offset) : base(data, offset) { - disableIcePhysics = data.Bool("disableIcePhysics", true); - } - - public override void OnEnter(Player player) { - base.OnEnter(player); - - SpringCollab2020Module.Instance.Session.IcePhysicsDisabled = disableIcePhysics; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using MonoMod.Cil; +using System; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/DisableIcePhysicsTrigger")] + class DisableIcePhysicsTrigger : Trigger { + public static void Load() { + IL.Celeste.Player.NormalUpdate += modPlayerNormalUpdate; + } + + public static void Unload() { + IL.Celeste.Player.NormalUpdate -= modPlayerNormalUpdate; + } + + private static void modPlayerNormalUpdate(ILContext il) { + ILCursor cursor = new ILCursor(il); + + while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("get_CoreMode"))) { + Logger.Log("SpringCollab2020/DisableIcePhysicsTrigger", $"Patching ice mode checking at {cursor.Index} in IL for Player.NormalUpdate"); + + cursor.EmitDelegate>(coreMode => { + if (SpringCollab2020Module.Instance.Session.IcePhysicsDisabled) { + // pretend there is no core mode, so that the ground is not slippery anymore. + return Session.CoreModes.None; + } + return coreMode; + }); + } + } + + private bool disableIcePhysics; + + public DisableIcePhysicsTrigger(EntityData data, Vector2 offset) : base(data, offset) { + disableIcePhysics = data.Bool("disableIcePhysics", true); + } + + public override void OnEnter(Player player) { + base.OnEnter(player); + + SpringCollab2020Module.Instance.Session.IcePhysicsDisabled = disableIcePhysics; + } + } +} diff --git a/Triggers/FlagToggleCameraTargetTrigger.cs b/Triggers/FlagToggleCameraTargetTrigger.cs index 76d07e1..6d2a723 100644 --- a/Triggers/FlagToggleCameraTargetTrigger.cs +++ b/Triggers/FlagToggleCameraTargetTrigger.cs @@ -1,12 +1,12 @@ -using Celeste.Mod.Entities; -using Celeste.Mod.SpringCollab2020.Entities; -using Microsoft.Xna.Framework; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/FlagToggleCameraTargetTrigger")] - class FlagToggleCameraTargetTrigger : CameraTargetTrigger { - public FlagToggleCameraTargetTrigger(EntityData data, Vector2 offset) : base(data, offset) { - Add(new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"))); - } - } -} +using Celeste.Mod.Entities; +using Celeste.Mod.SpringCollab2020.Entities; +using Microsoft.Xna.Framework; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/FlagToggleCameraTargetTrigger")] + class FlagToggleCameraTargetTrigger : CameraTargetTrigger { + public FlagToggleCameraTargetTrigger(EntityData data, Vector2 offset) : base(data, offset) { + Add(new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"))); + } + } +} diff --git a/Triggers/FlagToggleSmoothCameraOffsetTrigger.cs b/Triggers/FlagToggleSmoothCameraOffsetTrigger.cs index e66f1c3..f06b307 100644 --- a/Triggers/FlagToggleSmoothCameraOffsetTrigger.cs +++ b/Triggers/FlagToggleSmoothCameraOffsetTrigger.cs @@ -1,12 +1,12 @@ -using Celeste.Mod.Entities; -using Celeste.Mod.SpringCollab2020.Entities; -using Microsoft.Xna.Framework; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/FlagToggleSmoothCameraOffsetTrigger")] - class FlagToggleSmoothCameraOffsetTrigger : SmoothCameraOffsetTrigger { - public FlagToggleSmoothCameraOffsetTrigger(EntityData data, Vector2 offset) : base(data, offset) { - Add(new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"))); - } - } -} +using Celeste.Mod.Entities; +using Celeste.Mod.SpringCollab2020.Entities; +using Microsoft.Xna.Framework; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/FlagToggleSmoothCameraOffsetTrigger")] + class FlagToggleSmoothCameraOffsetTrigger : SmoothCameraOffsetTrigger { + public FlagToggleSmoothCameraOffsetTrigger(EntityData data, Vector2 offset) : base(data, offset) { + Add(new FlagToggleComponent(data.Attr("flag"), data.Bool("inverted"))); + } + } +} diff --git a/Triggers/LeaveTheoBehindTrigger.cs b/Triggers/LeaveTheoBehindTrigger.cs index fc54813..8ed2de6 100644 --- a/Triggers/LeaveTheoBehindTrigger.cs +++ b/Triggers/LeaveTheoBehindTrigger.cs @@ -1,69 +1,69 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Cil; -using System; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/LeaveTheoBehindTrigger")] - class LeaveTheoBehindTrigger : Trigger { - private static bool leaveTheoBehind = false; - - public static void Load() { - IL.Celeste.Level.EnforceBounds += onLevelEnforceBounds; - On.Celeste.Level.TransitionTo += onLevelTransitionTo; - } - - public static void Unload() { - IL.Celeste.Level.EnforceBounds -= onLevelEnforceBounds; - On.Celeste.Level.TransitionTo -= onLevelTransitionTo; - } - - private static void onLevelEnforceBounds(ILContext il) { - ILCursor cursor = new ILCursor(il); - - while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("GetEntity"))) { - // the only usage of GetEntity gets the TheoCrystal entity on the screen. - Logger.Log("SpringCollab2020/LeaveTheoBehindTrigger", $"Adding hook to leave Theo behind at {cursor.Index} in IL for Level.EnforceBounds"); - cursor.EmitDelegate>(theo => { - if (leaveTheoBehind) { - return null; // pretend there is no Theo, so we can exit the room. - } - return theo; - }); - } - } - - private static void onLevelTransitionTo(On.Celeste.Level.orig_TransitionTo orig, Level self, LevelData next, Vector2 direction) { - if (leaveTheoBehind) { - // freeze all Theo Crystals that the player isn't carrying, to prevent them from crashing the game or being weird. - Player player = self.Tracker.GetEntity(); - foreach (TheoCrystal crystal in self.Tracker.GetEntities()) { - if (player?.Holding?.Entity != crystal) { - crystal.RemoveTag(Tags.TransitionUpdate); - } - } - } - - orig(self, next, direction); - } - - - public LeaveTheoBehindTrigger(EntityData data, Vector2 offset) : base(data, offset) { } - - public override void OnEnter(Player player) { - base.OnEnter(player); - leaveTheoBehind = true; - } - - public override void Removed(Scene scene) { - base.Removed(scene); - leaveTheoBehind = false; - } - - public override void SceneEnd(Scene scene) { - base.SceneEnd(scene); - leaveTheoBehind = false; - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Cil; +using System; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/LeaveTheoBehindTrigger")] + class LeaveTheoBehindTrigger : Trigger { + private static bool leaveTheoBehind = false; + + public static void Load() { + IL.Celeste.Level.EnforceBounds += onLevelEnforceBounds; + On.Celeste.Level.TransitionTo += onLevelTransitionTo; + } + + public static void Unload() { + IL.Celeste.Level.EnforceBounds -= onLevelEnforceBounds; + On.Celeste.Level.TransitionTo -= onLevelTransitionTo; + } + + private static void onLevelEnforceBounds(ILContext il) { + ILCursor cursor = new ILCursor(il); + + while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt("GetEntity"))) { + // the only usage of GetEntity gets the TheoCrystal entity on the screen. + Logger.Log("SpringCollab2020/LeaveTheoBehindTrigger", $"Adding hook to leave Theo behind at {cursor.Index} in IL for Level.EnforceBounds"); + cursor.EmitDelegate>(theo => { + if (leaveTheoBehind) { + return null; // pretend there is no Theo, so we can exit the room. + } + return theo; + }); + } + } + + private static void onLevelTransitionTo(On.Celeste.Level.orig_TransitionTo orig, Level self, LevelData next, Vector2 direction) { + if (leaveTheoBehind) { + // freeze all Theo Crystals that the player isn't carrying, to prevent them from crashing the game or being weird. + Player player = self.Tracker.GetEntity(); + foreach (TheoCrystal crystal in self.Tracker.GetEntities()) { + if (player?.Holding?.Entity != crystal) { + crystal.RemoveTag(Tags.TransitionUpdate); + } + } + } + + orig(self, next, direction); + } + + + public LeaveTheoBehindTrigger(EntityData data, Vector2 offset) : base(data, offset) { } + + public override void OnEnter(Player player) { + base.OnEnter(player); + leaveTheoBehind = true; + } + + public override void Removed(Scene scene) { + base.Removed(scene); + leaveTheoBehind = false; + } + + public override void SceneEnd(Scene scene) { + base.SceneEnd(scene); + leaveTheoBehind = false; + } + } +} diff --git a/Triggers/LightningStrikeTrigger.cs b/Triggers/LightningStrikeTrigger.cs index cb61995..df451ee 100644 --- a/Triggers/LightningStrikeTrigger.cs +++ b/Triggers/LightningStrikeTrigger.cs @@ -1,4 +1,4 @@ -using Celeste; +using Celeste; using Celeste.Mod.Entities; using Microsoft.Xna.Framework; using System.Linq; @@ -30,7 +30,7 @@ class LightningStrikeTrigger : Trigger { private int ConstantTimer = 10; private Random rand; - + public LightningStrikeTrigger(EntityData data, Vector2 offset) : this(data, offset, data.Float("playerOffset", 0f), data.Float("verticalOffset", 0), data.Float("strikeHeight", 0), data.Int("seed", 0), data.Float("delay", 0f), data.Bool("rain", true), data.Bool("flash", true), data.Bool("constant", false)) { } public LightningStrikeTrigger(EntityData data, Vector2 offset, float playerOffset, float verticalOffset, float height, int seed, float delay, bool raining, bool flash, bool constant) : base (data, offset) { diff --git a/Triggers/MadelineSilhouetteTrigger.cs b/Triggers/MadelineSilhouetteTrigger.cs index b8da6c6..42f11fb 100644 --- a/Triggers/MadelineSilhouetteTrigger.cs +++ b/Triggers/MadelineSilhouetteTrigger.cs @@ -1,115 +1,115 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Mono.Cecil.Cil; -using Monocle; -using MonoMod.Cil; -using System; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/MadelineSilhouetteTrigger")] - class MadelineSilhouetteTrigger : Trigger { - private static Color silhouetteOutOfStaminaZeroDashBlinkColor = Calc.HexToColor("348DC1"); - - public static void Load() { - On.Celeste.Player.Added += onPlayerAdded; - IL.Celeste.Player.Render += patchPlayerRender; - On.Celeste.Player.ResetSprite += onPlayerResetSprite; - } - - public static void Unload() { - On.Celeste.Player.Added -= onPlayerAdded; - IL.Celeste.Player.Render -= patchPlayerRender; - On.Celeste.Player.ResetSprite -= onPlayerResetSprite; - } - - private static void onPlayerAdded(On.Celeste.Player.orig_Added orig, Player self, Scene scene) { - orig(self, scene); - - if (SpringCollab2020Module.Instance.Session.MadelineIsSilhouette) { - refreshPlayerSpriteMode(self, true); - } - } - - private static void patchPlayerRender(ILContext il) { - ILCursor cursor = new ILCursor(il); - - // jump to the usage of the Red color - if (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCall("get_Red"))) { - Logger.Log("SpringCollab2020/MadelineSilhouetteTrigger", $"Patching silhouette hair color at {cursor.Index} in IL code for Player.Render()"); - - // when Madeline blinks red, make the hair blink red. - cursor.Emit(OpCodes.Ldarg_0); - cursor.EmitDelegate>((color, player) => { - if (SpringCollab2020Module.Instance.Session.MadelineIsSilhouette) { - if (player.Dashes == 0) { - // blink to another shade of blue instead, to avoid red/blue flashes that are hard on the eyes. - color = silhouetteOutOfStaminaZeroDashBlinkColor; - } - player.Hair.Color = color; - } - return color; - }); - } - - // jump to the usage of the White color - if (cursor.TryGotoNext(instr => instr.MatchCall("get_White"))) { - Logger.Log("SpringCollab2020/MadelineSilhouetteTrigger", $"Patching silhouette color at {cursor.Index} in IL code for Player.Render()"); - - // instead of calling Color.White, call getMadelineColor just below. - cursor.Emit(OpCodes.Ldarg_0); - cursor.Next.Operand = typeof(MadelineSilhouetteTrigger).GetMethod("GetMadelineColor"); - } - } - - public static Color GetMadelineColor(Player player) { - if (SpringCollab2020Module.Instance.Session.MadelineIsSilhouette) { - return player.Hair.Color; - } else { - return Color.White; - } - } - - private static void refreshPlayerSpriteMode(Player player, bool enableSilhouette) { - PlayerSpriteMode targetSpriteMode; - if (enableSilhouette) { - targetSpriteMode = PlayerSpriteMode.Playback; - } else { - targetSpriteMode = SaveData.Instance.Assists.PlayAsBadeline ? PlayerSpriteMode.MadelineAsBadeline : player.DefaultSpriteMode; - } - - if (player.Active) { - player.ResetSpriteNextFrame(targetSpriteMode); - } else { - player.ResetSprite(targetSpriteMode); - } - } - - private static void onPlayerResetSprite(On.Celeste.Player.orig_ResetSprite orig, Player self, PlayerSpriteMode mode) { - // filter all calls to ResetSprite when MadelineIsSilhouette is enabled, only the ones with Playback will go through. - // this prevents Madeline from turning back into normal when the Other Self variant is toggled. - if (!SpringCollab2020Module.Instance.Session.MadelineIsSilhouette || mode == PlayerSpriteMode.Playback) { - orig(self, mode); - } - } - - - private bool enable; - - public MadelineSilhouetteTrigger(EntityData data, Vector2 offset) : base(data, offset) { - enable = data.Bool("enable", true); - } - - public override void OnEnter(Player player) { - base.OnEnter(player); - - bool oldValue = SpringCollab2020Module.Instance.Session.MadelineIsSilhouette; - SpringCollab2020Module.Instance.Session.MadelineIsSilhouette = enable; - - // if the value changed... - if (oldValue != enable) { - // switch modes right now. this uses the same way as turning the "Other Self" variant on. - refreshPlayerSpriteMode(player, enable); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Mono.Cecil.Cil; +using Monocle; +using MonoMod.Cil; +using System; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/MadelineSilhouetteTrigger")] + class MadelineSilhouetteTrigger : Trigger { + private static Color silhouetteOutOfStaminaZeroDashBlinkColor = Calc.HexToColor("348DC1"); + + public static void Load() { + On.Celeste.Player.Added += onPlayerAdded; + IL.Celeste.Player.Render += patchPlayerRender; + On.Celeste.Player.ResetSprite += onPlayerResetSprite; + } + + public static void Unload() { + On.Celeste.Player.Added -= onPlayerAdded; + IL.Celeste.Player.Render -= patchPlayerRender; + On.Celeste.Player.ResetSprite -= onPlayerResetSprite; + } + + private static void onPlayerAdded(On.Celeste.Player.orig_Added orig, Player self, Scene scene) { + orig(self, scene); + + if (SpringCollab2020Module.Instance.Session.MadelineIsSilhouette) { + refreshPlayerSpriteMode(self, true); + } + } + + private static void patchPlayerRender(ILContext il) { + ILCursor cursor = new ILCursor(il); + + // jump to the usage of the Red color + if (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCall("get_Red"))) { + Logger.Log("SpringCollab2020/MadelineSilhouetteTrigger", $"Patching silhouette hair color at {cursor.Index} in IL code for Player.Render()"); + + // when Madeline blinks red, make the hair blink red. + cursor.Emit(OpCodes.Ldarg_0); + cursor.EmitDelegate>((color, player) => { + if (SpringCollab2020Module.Instance.Session.MadelineIsSilhouette) { + if (player.Dashes == 0) { + // blink to another shade of blue instead, to avoid red/blue flashes that are hard on the eyes. + color = silhouetteOutOfStaminaZeroDashBlinkColor; + } + player.Hair.Color = color; + } + return color; + }); + } + + // jump to the usage of the White color + if (cursor.TryGotoNext(instr => instr.MatchCall("get_White"))) { + Logger.Log("SpringCollab2020/MadelineSilhouetteTrigger", $"Patching silhouette color at {cursor.Index} in IL code for Player.Render()"); + + // instead of calling Color.White, call getMadelineColor just below. + cursor.Emit(OpCodes.Ldarg_0); + cursor.Next.Operand = typeof(MadelineSilhouetteTrigger).GetMethod("GetMadelineColor"); + } + } + + public static Color GetMadelineColor(Player player) { + if (SpringCollab2020Module.Instance.Session.MadelineIsSilhouette) { + return player.Hair.Color; + } else { + return Color.White; + } + } + + private static void refreshPlayerSpriteMode(Player player, bool enableSilhouette) { + PlayerSpriteMode targetSpriteMode; + if (enableSilhouette) { + targetSpriteMode = PlayerSpriteMode.Playback; + } else { + targetSpriteMode = SaveData.Instance.Assists.PlayAsBadeline ? PlayerSpriteMode.MadelineAsBadeline : player.DefaultSpriteMode; + } + + if (player.Active) { + player.ResetSpriteNextFrame(targetSpriteMode); + } else { + player.ResetSprite(targetSpriteMode); + } + } + + private static void onPlayerResetSprite(On.Celeste.Player.orig_ResetSprite orig, Player self, PlayerSpriteMode mode) { + // filter all calls to ResetSprite when MadelineIsSilhouette is enabled, only the ones with Playback will go through. + // this prevents Madeline from turning back into normal when the Other Self variant is toggled. + if (!SpringCollab2020Module.Instance.Session.MadelineIsSilhouette || mode == PlayerSpriteMode.Playback) { + orig(self, mode); + } + } + + + private bool enable; + + public MadelineSilhouetteTrigger(EntityData data, Vector2 offset) : base(data, offset) { + enable = data.Bool("enable", true); + } + + public override void OnEnter(Player player) { + base.OnEnter(player); + + bool oldValue = SpringCollab2020Module.Instance.Session.MadelineIsSilhouette; + SpringCollab2020Module.Instance.Session.MadelineIsSilhouette = enable; + + // if the value changed... + if (oldValue != enable) { + // switch modes right now. this uses the same way as turning the "Other Self" variant on. + refreshPlayerSpriteMode(player, enable); + } + } + } +} diff --git a/Triggers/MusicAreaChangeTrigger.cs b/Triggers/MusicAreaChangeTrigger.cs index 184f96e..3996774 100644 --- a/Triggers/MusicAreaChangeTrigger.cs +++ b/Triggers/MusicAreaChangeTrigger.cs @@ -1,53 +1,53 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/MusicAreaChangeTrigger")] - class MusicAreaChangeTrigger : Trigger { - private string musicParam1; - private string musicParam2; - private string musicParam3; - private float enterValue; - private float exitValue; - - // this is used specifically for the expert lobby. On enter, it sets all given parameter values - // to the enter value. On exit, it sets all parameter values to exit. Individual exit/enter values were not necessary so they were not added. - // this is hastily written code that could likely be refactored into a more widely useful trigger for FMOD'ers - - public MusicAreaChangeTrigger(EntityData data, Vector2 offset) : base(data, offset) { - musicParam1 = data.Attr("musicParam1"); - musicParam2 = data.Attr("musicParam2"); - musicParam3 = data.Attr("musicParam3"); - enterValue = data.Float("enterValue", 1f); - exitValue = data.Float("exitValue", 0f); - } - - public override void OnEnter(Player player) { - base.OnEnter(player); - - if (!string.IsNullOrEmpty(musicParam1)) { - Audio.SetMusicParam(musicParam1, enterValue); - } - if (!string.IsNullOrEmpty(musicParam2)) { - Audio.SetMusicParam(musicParam2, enterValue); - } - if (!string.IsNullOrEmpty(musicParam3)) { - Audio.SetMusicParam(musicParam3, enterValue); - } - } - - public override void OnLeave(Player player) { - base.OnLeave(player); - - if (!string.IsNullOrEmpty(musicParam1)) { - Audio.SetMusicParam(musicParam1, exitValue); - } - if (!string.IsNullOrEmpty(musicParam2)) { - Audio.SetMusicParam(musicParam2, exitValue); - } - if (!string.IsNullOrEmpty(musicParam3)) { - Audio.SetMusicParam(musicParam3, exitValue); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/MusicAreaChangeTrigger")] + class MusicAreaChangeTrigger : Trigger { + private string musicParam1; + private string musicParam2; + private string musicParam3; + private float enterValue; + private float exitValue; + + // this is used specifically for the expert lobby. On enter, it sets all given parameter values + // to the enter value. On exit, it sets all parameter values to exit. Individual exit/enter values were not necessary so they were not added. + // this is hastily written code that could likely be refactored into a more widely useful trigger for FMOD'ers + + public MusicAreaChangeTrigger(EntityData data, Vector2 offset) : base(data, offset) { + musicParam1 = data.Attr("musicParam1"); + musicParam2 = data.Attr("musicParam2"); + musicParam3 = data.Attr("musicParam3"); + enterValue = data.Float("enterValue", 1f); + exitValue = data.Float("exitValue", 0f); + } + + public override void OnEnter(Player player) { + base.OnEnter(player); + + if (!string.IsNullOrEmpty(musicParam1)) { + Audio.SetMusicParam(musicParam1, enterValue); + } + if (!string.IsNullOrEmpty(musicParam2)) { + Audio.SetMusicParam(musicParam2, enterValue); + } + if (!string.IsNullOrEmpty(musicParam3)) { + Audio.SetMusicParam(musicParam3, enterValue); + } + } + + public override void OnLeave(Player player) { + base.OnLeave(player); + + if (!string.IsNullOrEmpty(musicParam1)) { + Audio.SetMusicParam(musicParam1, exitValue); + } + if (!string.IsNullOrEmpty(musicParam2)) { + Audio.SetMusicParam(musicParam2, exitValue); + } + if (!string.IsNullOrEmpty(musicParam3)) { + Audio.SetMusicParam(musicParam3, exitValue); + } + } + } +} diff --git a/Triggers/MusicLayerFadeTrigger.cs b/Triggers/MusicLayerFadeTrigger.cs index 00811fd..b5fb4f4 100644 --- a/Triggers/MusicLayerFadeTrigger.cs +++ b/Triggers/MusicLayerFadeTrigger.cs @@ -1,41 +1,41 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/MusicLayerFadeTrigger")] - public class MusicLayerFadeTrigger : Trigger { - - private int[] layers; - private float fadeA; - private float fadeB; - private PositionModes direction; - - public MusicLayerFadeTrigger(EntityData data, Vector2 offset) - : base(data, offset) { - - // parse the "layers" attribute (for example "1,3,4") as an int array. - string[] layersAsStrings = data.Attr("layers").Split(','); - layers = new int[layersAsStrings.Length]; - for (int i = 0; i < layers.Length; i++) { - layers[i] = int.Parse(layersAsStrings[i]); - } - - // parse the other parameters for the trigger - fadeA = data.Float("fadeA", 0f); - fadeB = data.Float("fadeB", 1f); - direction = data.Enum("direction", PositionModes.LeftToRight); - } - - public override void OnStay(Player player) { - // compute the current fade value - float fade = MathHelper.Lerp(fadeA, fadeB, GetPositionLerp(player, direction)); - - // apply it to all the required layers - AudioState audio = SceneAs().Session.Audio; - foreach (int layer in layers) { - audio.Music.Layer(layer, fade); - } - audio.Apply(); - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/MusicLayerFadeTrigger")] + public class MusicLayerFadeTrigger : Trigger { + + private int[] layers; + private float fadeA; + private float fadeB; + private PositionModes direction; + + public MusicLayerFadeTrigger(EntityData data, Vector2 offset) + : base(data, offset) { + + // parse the "layers" attribute (for example "1,3,4") as an int array. + string[] layersAsStrings = data.Attr("layers").Split(','); + layers = new int[layersAsStrings.Length]; + for (int i = 0; i < layers.Length; i++) { + layers[i] = int.Parse(layersAsStrings[i]); + } + + // parse the other parameters for the trigger + fadeA = data.Float("fadeA", 0f); + fadeB = data.Float("fadeB", 1f); + direction = data.Enum("direction", PositionModes.LeftToRight); + } + + public override void OnStay(Player player) { + // compute the current fade value + float fade = MathHelper.Lerp(fadeA, fadeB, GetPositionLerp(player, direction)); + + // apply it to all the required layers + AudioState audio = SceneAs().Session.Audio; + foreach (int layer in layers) { + audio.Music.Layer(layer, fade); + } + audio.Apply(); + } + } +} diff --git a/Triggers/NoRefillField.cs b/Triggers/NoRefillField.cs index ba5dea4..be4db4f 100644 --- a/Triggers/NoRefillField.cs +++ b/Triggers/NoRefillField.cs @@ -1,44 +1,44 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - // Heavily based off the NoRefillTrigger class from vanilla, except reverting the state on leave. - [CustomEntity("SpringCollab2020/NoRefillField")] - [Tracked] - class NoRefillField : Trigger { - public NoRefillField(EntityData data, Vector2 offset): base(data, offset) { } - - public static void Load() { - Everest.Events.Level.OnExit += onLevelExit; - } - - public static void Unload() { - Everest.Events.Level.OnExit -= onLevelExit; - } - - public override void OnEnter(Player player) { - base.OnEnter(player); - SceneAs().Session.Inventory.NoRefills = true; - } - - public override void OnLeave(Player player) { - base.OnLeave(player); - - // re-enable refills if not colliding with another no refill field. - if (player.Dead || !player.CollideCheck()) { - SceneAs().Session.Inventory.NoRefills = false; - } - } - - private static void onLevelExit(Level level, LevelExit exit, LevelExit.Mode mode, Session session, HiresSnow snow) { - if (mode == LevelExit.Mode.SaveAndQuit) { - // if saving in a No Refill Field, be sure to restore the refills. - Player player = level.Tracker.GetEntity(); - if (player != null && player.CollideCheck()) { - level.Session.Inventory.NoRefills = false; - } - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + // Heavily based off the NoRefillTrigger class from vanilla, except reverting the state on leave. + [CustomEntity("SpringCollab2020/NoRefillField")] + [Tracked] + class NoRefillField : Trigger { + public NoRefillField(EntityData data, Vector2 offset): base(data, offset) { } + + public static void Load() { + Everest.Events.Level.OnExit += onLevelExit; + } + + public static void Unload() { + Everest.Events.Level.OnExit -= onLevelExit; + } + + public override void OnEnter(Player player) { + base.OnEnter(player); + SceneAs().Session.Inventory.NoRefills = true; + } + + public override void OnLeave(Player player) { + base.OnLeave(player); + + // re-enable refills if not colliding with another no refill field. + if (player.Dead || !player.CollideCheck()) { + SceneAs().Session.Inventory.NoRefills = false; + } + } + + private static void onLevelExit(Level level, LevelExit exit, LevelExit.Mode mode, Session session, HiresSnow snow) { + if (mode == LevelExit.Mode.SaveAndQuit) { + // if saving in a No Refill Field, be sure to restore the refills. + Player player = level.Tracker.GetEntity(); + if (player != null && player.CollideCheck()) { + level.Session.Inventory.NoRefills = false; + } + } + } + } +} diff --git a/Triggers/PauseBadelineBossesTrigger.cs b/Triggers/PauseBadelineBossesTrigger.cs index c6b5073..df2c45b 100644 --- a/Triggers/PauseBadelineBossesTrigger.cs +++ b/Triggers/PauseBadelineBossesTrigger.cs @@ -1,38 +1,38 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using MonoMod.Utils; -using System.Linq; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/PauseBadelineBossesTrigger")] - class PauseBadelineBossesTrigger : Trigger { - public PauseBadelineBossesTrigger(EntityData data, Vector2 offset) : base(data, offset) { } - - public override void OnStay(Player player) { - base.OnStay(player); - - // kick the attack coroutines from Badeline bosses so that they don't attack anymore. - foreach (FinalBoss badelineBoss in Scene.Tracker.GetEntities()) { - DynData bossData = new DynData(badelineBoss); - if (badelineBoss.Sprite.CurrentAnimationID == "idle") { - badelineBoss.Remove(bossData.Get("attackCoroutine")); - } - } - } - - public override void OnLeave(Player player) { - base.OnLeave(player); - - if (!player.Dead) { - // add the attack coroutines back. - foreach (FinalBoss badelineBoss in Scene.Tracker.GetEntities()) { - Coroutine attackCoroutine = new DynData(badelineBoss).Get("attackCoroutine"); - if (!badelineBoss.Components.Contains(attackCoroutine)) { - badelineBoss.Add(attackCoroutine); - } - } - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using MonoMod.Utils; +using System.Linq; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/PauseBadelineBossesTrigger")] + class PauseBadelineBossesTrigger : Trigger { + public PauseBadelineBossesTrigger(EntityData data, Vector2 offset) : base(data, offset) { } + + public override void OnStay(Player player) { + base.OnStay(player); + + // kick the attack coroutines from Badeline bosses so that they don't attack anymore. + foreach (FinalBoss badelineBoss in Scene.Tracker.GetEntities()) { + DynData bossData = new DynData(badelineBoss); + if (badelineBoss.Sprite.CurrentAnimationID == "idle") { + badelineBoss.Remove(bossData.Get("attackCoroutine")); + } + } + } + + public override void OnLeave(Player player) { + base.OnLeave(player); + + if (!player.Dead) { + // add the attack coroutines back. + foreach (FinalBoss badelineBoss in Scene.Tracker.GetEntities()) { + Coroutine attackCoroutine = new DynData(badelineBoss).Get("attackCoroutine"); + if (!badelineBoss.Components.Contains(attackCoroutine)) { + badelineBoss.Add(attackCoroutine); + } + } + } + } + } +} diff --git a/Triggers/RemoveLightSourcesTrigger.cs b/Triggers/RemoveLightSourcesTrigger.cs index 2c02613..5bb4586 100644 --- a/Triggers/RemoveLightSourcesTrigger.cs +++ b/Triggers/RemoveLightSourcesTrigger.cs @@ -1,14 +1,14 @@ -using Celeste.Mod.Entities; +using Celeste.Mod.Entities; using Monocle; using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System; - +using System.Collections.Generic; +using System; + namespace Celeste.Mod.SpringCollab2020.Triggers { [CustomEntity("SpringCollab2020/RemoveLightSourcesTrigger")] [Tracked] - class RemoveLightSourcesTrigger : Trigger { - + class RemoveLightSourcesTrigger : Trigger { + private static float alphaFade = 1f; public static void Load() { @@ -16,90 +16,90 @@ public static void Load() { On.Celeste.BloomRenderer.Apply += BloomRendererHook; On.Celeste.Level.LoadLevel += OnLoadLevel; On.Celeste.Level.Update += OnLevelUpdate; - } - + } + public static void Unload() { On.Celeste.LightingRenderer.BeforeRender -= LightHook; On.Celeste.BloomRenderer.Apply -= BloomRendererHook; On.Celeste.Level.LoadLevel -= OnLoadLevel; On.Celeste.Level.Update -= OnLevelUpdate; - } - - private static void OnLoadLevel(On.Celeste.Level.orig_LoadLevel orig, Level self, Player.IntroTypes playerIntro, bool isFromLoader) { - orig(self, playerIntro, isFromLoader); - - if (playerIntro != Player.IntroTypes.Transition) { - // do not fade and set the alpha right away when spawning into the level. - alphaFade = SpringCollab2020Module.Instance.Session.LightSourcesDisabled ? 0f : 1f; - } - } - - private static void OnLevelUpdate(On.Celeste.Level.orig_Update orig, Level self) { - orig(self); - - if (!self.Paused) { - // progressively fade in or out. - alphaFade = Calc.Approach(alphaFade, SpringCollab2020Module.Instance.Session.LightSourcesDisabled ? 0f : 1f, Engine.DeltaTime * 3f); - } + } + + private static void OnLoadLevel(On.Celeste.Level.orig_LoadLevel orig, Level self, Player.IntroTypes playerIntro, bool isFromLoader) { + orig(self, playerIntro, isFromLoader); + + if (playerIntro != Player.IntroTypes.Transition) { + // do not fade and set the alpha right away when spawning into the level. + alphaFade = SpringCollab2020Module.Instance.Session.LightSourcesDisabled ? 0f : 1f; + } + } + + private static void OnLevelUpdate(On.Celeste.Level.orig_Update orig, Level self) { + orig(self); + + if (!self.Paused) { + // progressively fade in or out. + alphaFade = Calc.Approach(alphaFade, SpringCollab2020Module.Instance.Session.LightSourcesDisabled ? 0f : 1f, Engine.DeltaTime * 3f); + } } private static void BloomRendererHook(On.Celeste.BloomRenderer.orig_Apply orig, BloomRenderer self, VirtualRenderTarget target, Scene scene) { - if (alphaFade < 1f) { - // multiply all alphas by alphaFade, and back up original values. - List affectedBloomPoints = new List(); - List originalAlpha = new List(); - foreach (BloomPoint bloomPoint in scene.Tracker.GetComponents().ToArray()) { - if (bloomPoint.Visible && !(bloomPoint.Entity is Payphone)) { - affectedBloomPoints.Add(bloomPoint); - originalAlpha.Add(bloomPoint.Alpha); - bloomPoint.Alpha *= alphaFade; - } - } - - // render the bloom. - orig(self, target, scene); - - // restore original alphas. - int index = 0; - foreach (BloomPoint bloomPoint in affectedBloomPoints) { - bloomPoint.Alpha = originalAlpha[index++]; - } - } else { - // alpha multiplier is 1: nothing to modify, go on with vanilla. - orig(self, target, scene); + if (alphaFade < 1f) { + // multiply all alphas by alphaFade, and back up original values. + List affectedBloomPoints = new List(); + List originalAlpha = new List(); + foreach (BloomPoint bloomPoint in scene.Tracker.GetComponents().ToArray()) { + if (bloomPoint.Visible && !(bloomPoint.Entity is Payphone)) { + affectedBloomPoints.Add(bloomPoint); + originalAlpha.Add(bloomPoint.Alpha); + bloomPoint.Alpha *= alphaFade; + } + } + + // render the bloom. + orig(self, target, scene); + + // restore original alphas. + int index = 0; + foreach (BloomPoint bloomPoint in affectedBloomPoints) { + bloomPoint.Alpha = originalAlpha[index++]; + } + } else { + // alpha multiplier is 1: nothing to modify, go on with vanilla. + orig(self, target, scene); } } private static void LightHook(On.Celeste.LightingRenderer.orig_BeforeRender orig, LightingRenderer self, Scene scene) { - if (alphaFade < 1f) { - // multiply all alphas by alphaFade, and back up original values. - List affectedVertexLights = new List(); - List originalAlpha = new List(); - foreach (VertexLight vertexLight in scene.Tracker.GetComponents().ToArray()) { - if (vertexLight.Visible && !vertexLight.Spotlight) { - affectedVertexLights.Add(vertexLight); - originalAlpha.Add(vertexLight.Alpha); - vertexLight.Alpha *= alphaFade; - } - } - - // render the lighting. - orig(self, scene); - - // restore original alphas. - int index = 0; - foreach (VertexLight vertexLight in affectedVertexLights) { - vertexLight.Alpha = originalAlpha[index++]; - } - } else { - // alpha multiplier is 1: nothing to modify, go on with vanilla. - orig(self, scene); + if (alphaFade < 1f) { + // multiply all alphas by alphaFade, and back up original values. + List affectedVertexLights = new List(); + List originalAlpha = new List(); + foreach (VertexLight vertexLight in scene.Tracker.GetComponents().ToArray()) { + if (vertexLight.Visible && !vertexLight.Spotlight) { + affectedVertexLights.Add(vertexLight); + originalAlpha.Add(vertexLight.Alpha); + vertexLight.Alpha *= alphaFade; + } + } + + // render the lighting. + orig(self, scene); + + // restore original alphas. + int index = 0; + foreach (VertexLight vertexLight in affectedVertexLights) { + vertexLight.Alpha = originalAlpha[index++]; + } + } else { + // alpha multiplier is 1: nothing to modify, go on with vanilla. + orig(self, scene); } - } - - private bool enableLightSources; - private bool fade; - + } + + private bool enableLightSources; + private bool fade; + public RemoveLightSourcesTrigger(EntityData data, Vector2 offset) : base(data, offset) { enableLightSources = data.Bool("enableLightSources", false); fade = data.Bool("fade", false); @@ -107,11 +107,11 @@ public RemoveLightSourcesTrigger(EntityData data, Vector2 offset) : base(data, o public override void OnEnter(Player player) { base.OnEnter(player); - SpringCollab2020Module.Instance.Session.LightSourcesDisabled = !enableLightSources; - - if (!fade) { - // don't fade; set the fade to its final value right away. - alphaFade = SpringCollab2020Module.Instance.Session.LightSourcesDisabled ? 0f : 1f; + SpringCollab2020Module.Instance.Session.LightSourcesDisabled = !enableLightSources; + + if (!fade) { + // don't fade; set the fade to its final value right away. + alphaFade = SpringCollab2020Module.Instance.Session.LightSourcesDisabled ? 0f : 1f; } } } diff --git a/Triggers/SmoothCameraOffsetTrigger.cs b/Triggers/SmoothCameraOffsetTrigger.cs index 11e6198..831900c 100644 --- a/Triggers/SmoothCameraOffsetTrigger.cs +++ b/Triggers/SmoothCameraOffsetTrigger.cs @@ -1,35 +1,35 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - // A Camera Offset Trigger lerping the offset depending on the player position. - [CustomEntity("SpringCollab2020/SmoothCameraOffsetTrigger")] - class SmoothCameraOffsetTrigger : Trigger { - - private Vector2 offsetFrom; - private Vector2 offsetTo; - private PositionModes positionMode; - private bool onlyOnce; - - public SmoothCameraOffsetTrigger(EntityData data, Vector2 offset) : base(data, offset) { - // parse the trigger attributes. Multiplying X dimensions by 48 and Y ones by 32 replicates the vanilla offset trigger behavior. - offsetFrom = new Vector2(data.Float("offsetXFrom") * 48f, data.Float("offsetYFrom") * 32f); - offsetTo = new Vector2(data.Float("offsetXTo") * 48f, data.Float("offsetYTo") * 32f); - positionMode = data.Enum("positionMode"); - onlyOnce = data.Bool("onlyOnce"); - } - - public override void OnStay(Player player) { - base.OnStay(player); - SceneAs().CameraOffset = Vector2.Lerp(offsetFrom, offsetTo, GetPositionLerp(player, positionMode)); - } - - public override void OnLeave(Player player) { - base.OnLeave(player); - - if (onlyOnce) { - RemoveSelf(); - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + // A Camera Offset Trigger lerping the offset depending on the player position. + [CustomEntity("SpringCollab2020/SmoothCameraOffsetTrigger")] + class SmoothCameraOffsetTrigger : Trigger { + + private Vector2 offsetFrom; + private Vector2 offsetTo; + private PositionModes positionMode; + private bool onlyOnce; + + public SmoothCameraOffsetTrigger(EntityData data, Vector2 offset) : base(data, offset) { + // parse the trigger attributes. Multiplying X dimensions by 48 and Y ones by 32 replicates the vanilla offset trigger behavior. + offsetFrom = new Vector2(data.Float("offsetXFrom") * 48f, data.Float("offsetYFrom") * 32f); + offsetTo = new Vector2(data.Float("offsetXTo") * 48f, data.Float("offsetYTo") * 32f); + positionMode = data.Enum("positionMode"); + onlyOnce = data.Bool("onlyOnce"); + } + + public override void OnStay(Player player) { + base.OnStay(player); + SceneAs().CameraOffset = Vector2.Lerp(offsetFrom, offsetTo, GetPositionLerp(player, positionMode)); + } + + public override void OnLeave(Player player) { + base.OnLeave(player); + + if (onlyOnce) { + RemoveSelf(); + } + } + } +} diff --git a/Triggers/SpeedBasedMusicParamTrigger.cs b/Triggers/SpeedBasedMusicParamTrigger.cs index a7179f6..c90cf0b 100644 --- a/Triggers/SpeedBasedMusicParamTrigger.cs +++ b/Triggers/SpeedBasedMusicParamTrigger.cs @@ -1,78 +1,78 @@ -using Celeste.Mod.Entities; -using Monocle; -using Microsoft.Xna.Framework; -using System.Collections.Generic; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - /** - * Port of the speed-based music param trigger from max480's Helping Hand: - * https://github.com/max4805/MaxHelpingHand/blob/master/Triggers/SpeedBasedMusicParamTrigger.cs - */ - [CustomEntity("SpringCollab2020/SpeedBasedMusicParamTrigger")] - class SpeedBasedMusicParamTrigger : Trigger { - public static void Load() { - On.Celeste.Player.Update += onPlayerUpdate; - } - - public static void Unload() { - On.Celeste.Player.Update -= onPlayerUpdate; - } - - private static void onPlayerUpdate(On.Celeste.Player.orig_Update orig, Player self) { - orig(self); - - if (SpringCollab2020Module.Instance.Session.ActiveSpeedBasedMusicParams.Count > 0) { - AudioState audio = self.SceneAs().Session.Audio; - float playerSpeed = self.Speed.Length(); - - // set all the speed-based music params to their corresponding values. - foreach (KeyValuePair musicParam - in SpringCollab2020Module.Instance.Session.ActiveSpeedBasedMusicParams) { - - audio.Music.Param(musicParam.Key, Calc.ClampedMap(playerSpeed, - musicParam.Value.MinimumSpeed, musicParam.Value.MaximumSpeed, - musicParam.Value.MinimumParamValue, musicParam.Value.MaximumParamValue)); - } - - audio.Apply(); - } - } - - private string paramName; - private float minSpeed; - private float maxSpeed; - private float minParamValue; - private float maxParamValue; - private bool activate; - - public SpeedBasedMusicParamTrigger(EntityData data, Vector2 offset) : base(data, offset) { - paramName = data.Attr("paramName"); - minSpeed = data.Float("minSpeed"); - maxSpeed = data.Float("maxSpeed"); - minParamValue = data.Float("minParamValue"); - maxParamValue = data.Float("maxParamValue"); - activate = data.Bool("activate"); - } - - public override void OnEnter(Player player) { - base.OnEnter(player); - - if (activate) { - // register the speed-based music param as active to keep it updated. - SpringCollab2020Module.Instance.Session.ActiveSpeedBasedMusicParams[paramName] = new SpringCollab2020Session.SpeedBasedMusicParamInfo() { - MinimumSpeed = minSpeed, - MaximumSpeed = maxSpeed, - MinimumParamValue = minParamValue, - MaximumParamValue = maxParamValue - }; - } else { - // unregister the param, and set the music param to the minimum value. - SpringCollab2020Module.Instance.Session.ActiveSpeedBasedMusicParams.Remove(paramName); - - AudioState audio = SceneAs().Session.Audio; - audio.Music.Param(paramName, minParamValue); - audio.Apply(); - } - } - } -} +using Celeste.Mod.Entities; +using Monocle; +using Microsoft.Xna.Framework; +using System.Collections.Generic; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + /** + * Port of the speed-based music param trigger from max480's Helping Hand: + * https://github.com/max4805/MaxHelpingHand/blob/master/Triggers/SpeedBasedMusicParamTrigger.cs + */ + [CustomEntity("SpringCollab2020/SpeedBasedMusicParamTrigger")] + class SpeedBasedMusicParamTrigger : Trigger { + public static void Load() { + On.Celeste.Player.Update += onPlayerUpdate; + } + + public static void Unload() { + On.Celeste.Player.Update -= onPlayerUpdate; + } + + private static void onPlayerUpdate(On.Celeste.Player.orig_Update orig, Player self) { + orig(self); + + if (SpringCollab2020Module.Instance.Session.ActiveSpeedBasedMusicParams.Count > 0) { + AudioState audio = self.SceneAs().Session.Audio; + float playerSpeed = self.Speed.Length(); + + // set all the speed-based music params to their corresponding values. + foreach (KeyValuePair musicParam + in SpringCollab2020Module.Instance.Session.ActiveSpeedBasedMusicParams) { + + audio.Music.Param(musicParam.Key, Calc.ClampedMap(playerSpeed, + musicParam.Value.MinimumSpeed, musicParam.Value.MaximumSpeed, + musicParam.Value.MinimumParamValue, musicParam.Value.MaximumParamValue)); + } + + audio.Apply(); + } + } + + private string paramName; + private float minSpeed; + private float maxSpeed; + private float minParamValue; + private float maxParamValue; + private bool activate; + + public SpeedBasedMusicParamTrigger(EntityData data, Vector2 offset) : base(data, offset) { + paramName = data.Attr("paramName"); + minSpeed = data.Float("minSpeed"); + maxSpeed = data.Float("maxSpeed"); + minParamValue = data.Float("minParamValue"); + maxParamValue = data.Float("maxParamValue"); + activate = data.Bool("activate"); + } + + public override void OnEnter(Player player) { + base.OnEnter(player); + + if (activate) { + // register the speed-based music param as active to keep it updated. + SpringCollab2020Module.Instance.Session.ActiveSpeedBasedMusicParams[paramName] = new SpringCollab2020Session.SpeedBasedMusicParamInfo() { + MinimumSpeed = minSpeed, + MaximumSpeed = maxSpeed, + MinimumParamValue = minParamValue, + MaximumParamValue = maxParamValue + }; + } else { + // unregister the param, and set the music param to the minimum value. + SpringCollab2020Module.Instance.Session.ActiveSpeedBasedMusicParams.Remove(paramName); + + AudioState audio = SceneAs().Session.Audio; + audio.Music.Param(paramName, minParamValue); + audio.Apply(); + } + } + } +} diff --git a/Triggers/StrawberryCollectionField.cs b/Triggers/StrawberryCollectionField.cs index a5574d5..6ebc6b5 100644 --- a/Triggers/StrawberryCollectionField.cs +++ b/Triggers/StrawberryCollectionField.cs @@ -1,66 +1,66 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; -using Monocle; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/StrawberryCollectionField")] - class StrawberryCollectionField : Trigger { - private float collectTimer = 0f; - private bool delayBetweenBerries; - private bool includeGoldens; - - public StrawberryCollectionField(EntityData data, Vector2 offset) : base(data, offset) { - delayBetweenBerries = data.Bool("delayBetweenBerries"); - includeGoldens = data.Bool("includeGoldens"); - } - - public override void OnStay(Player player) { - base.OnStay(player); - - if (collectTimer <= 0f) { - ReadOnlyCollection berryTypes = StrawberryRegistry.GetBerryTypes(); - List followingBerries = new List(); - foreach (Follower follower in player.Leader.Followers) { - if (berryTypes.Contains(follower.Entity.GetType()) - && follower.Entity is IStrawberry berry - && (includeGoldens || !isGolden(berry))) { - - followingBerries.Add(berry); - } - } - foreach (IStrawberry berry in followingBerries) { - berry.OnCollect(); - - if (delayBetweenBerries) { - collectTimer = 0.3f; - break; - } - } - } - } - - private bool isGolden(IStrawberry berry) { - if (berry.GetType() == typeof(Strawberry)) { - // vanilla berries are goldens if... they are goldens. - return (berry as Strawberry).Golden; - } else { - // mod berries are "goldens" if they block normal collection. - return StrawberryRegistry.GetRegisteredBerries().ToList() - .Find(berryDef => berryDef.berryClass == berry.GetType()) - .blocksNormalCollection; - } - } - - public override void Update() { - base.Update(); - - if (delayBetweenBerries) { - collectTimer -= Engine.DeltaTime; - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; +using Monocle; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/StrawberryCollectionField")] + class StrawberryCollectionField : Trigger { + private float collectTimer = 0f; + private bool delayBetweenBerries; + private bool includeGoldens; + + public StrawberryCollectionField(EntityData data, Vector2 offset) : base(data, offset) { + delayBetweenBerries = data.Bool("delayBetweenBerries"); + includeGoldens = data.Bool("includeGoldens"); + } + + public override void OnStay(Player player) { + base.OnStay(player); + + if (collectTimer <= 0f) { + ReadOnlyCollection berryTypes = StrawberryRegistry.GetBerryTypes(); + List followingBerries = new List(); + foreach (Follower follower in player.Leader.Followers) { + if (berryTypes.Contains(follower.Entity.GetType()) + && follower.Entity is IStrawberry berry + && (includeGoldens || !isGolden(berry))) { + + followingBerries.Add(berry); + } + } + foreach (IStrawberry berry in followingBerries) { + berry.OnCollect(); + + if (delayBetweenBerries) { + collectTimer = 0.3f; + break; + } + } + } + } + + private bool isGolden(IStrawberry berry) { + if (berry.GetType() == typeof(Strawberry)) { + // vanilla berries are goldens if... they are goldens. + return (berry as Strawberry).Golden; + } else { + // mod berries are "goldens" if they block normal collection. + return StrawberryRegistry.GetRegisteredBerries().ToList() + .Find(berryDef => berryDef.berryClass == berry.GetType()) + .blocksNormalCollection; + } + } + + public override void Update() { + base.Update(); + + if (delayBetweenBerries) { + collectTimer -= Engine.DeltaTime; + } + } + } +} diff --git a/Triggers/SwapDashTrigger.cs b/Triggers/SwapDashTrigger.cs index d38a837..646838e 100644 --- a/Triggers/SwapDashTrigger.cs +++ b/Triggers/SwapDashTrigger.cs @@ -1,29 +1,29 @@ -using Celeste.Mod.Entities; -using Microsoft.Xna.Framework; - -namespace Celeste.Mod.SpringCollab2020.Triggers { - [CustomEntity("SpringCollab2020/SwapDashTrigger")] - class SwapDashTrigger : Trigger { - private float enterX; - - public SwapDashTrigger(EntityData data, Vector2 offset) - : base(data, offset) { - } - - public override void OnEnter(Player player) { - enterX = player.X; - } - - public override void OnLeave(Player player) { - Session session = SceneAs().Session; - if (player.MaxDashes == 1 && (!(enterX + 16f >= player.X) || !(enterX - 16f <= player.X))) { - session.Inventory.Dashes = 2; - } else if (player.MaxDashes == 2 && (!(enterX + 16f >= player.X) || !(enterX - 16f <= player.X))) { - session.Inventory.Dashes = 1; - if (player.Dashes == 2) { - player.Dashes = 1; - } - } - } - } -} +using Celeste.Mod.Entities; +using Microsoft.Xna.Framework; + +namespace Celeste.Mod.SpringCollab2020.Triggers { + [CustomEntity("SpringCollab2020/SwapDashTrigger")] + class SwapDashTrigger : Trigger { + private float enterX; + + public SwapDashTrigger(EntityData data, Vector2 offset) + : base(data, offset) { + } + + public override void OnEnter(Player player) { + enterX = player.X; + } + + public override void OnLeave(Player player) { + Session session = SceneAs().Session; + if (player.MaxDashes == 1 && (!(enterX + 16f >= player.X) || !(enterX - 16f <= player.X))) { + session.Inventory.Dashes = 2; + } else if (player.MaxDashes == 2 && (!(enterX + 16f >= player.X) || !(enterX - 16f <= player.X))) { + session.Inventory.Dashes = 1; + if (player.Dashes == 2) { + player.Dashes = 1; + } + } + } + } +}