extends Node var activated = false var effect_running = false var slowness_remaining = 0 var iceland_remaining = 0 var atbash_remaining = 0 var queued_iceland = 0 var skip_available = false var puzzle_focused = false var solve_mode = false var not_behind_wall = false var puzzle_to_skip = "" var text_dirty = true var orig_env var orig_walk var orig_run var wallcast func _ready(): orig_env = get_tree().get_root().get_node("Spatial/player/pivot/camera").environment orig_walk = get_tree().get_root().get_node("Spatial/player").walk_speed orig_run = get_tree().get_root().get_node("Spatial/player").run_speed wallcast = get_tree().get_root().get_node("Spatial/player/pivot/camera/wallcast") var label = Label.new() label.set_name("label") label.margin_right = 1920.0 - 20.0 label.margin_top = 20.0 label.align = Label.ALIGN_RIGHT label.valign = Label.VALIGN_TOP var dynamic_font = DynamicFont.new() dynamic_font.font_data = load("res://fonts/Lingo.ttf") dynamic_font.size = 36 dynamic_font.outline_color = Color(0, 0, 0, 1) dynamic_font.outline_size = 2 label.add_font_override("font", dynamic_font) add_child(label) var slowness_timer = Timer.new() slowness_timer.name = "SlownessTimer" slowness_timer.wait_time = 1.0 add_child(slowness_timer) slowness_timer.connect("timeout", self, "_tick_slowness") var iceland_timer = Timer.new() iceland_timer.name = "IcelandTimer" iceland_timer.wait_time = 1.0 add_child(iceland_timer) iceland_timer.connect("timeout", self, "_tick_iceland") func activate(): activated = true if queued_iceland > 0: trigger_iceland_trap(queued_iceland) queued_iceland = 0 func trigger_slowness_trap(length = 30): if slowness_remaining == 0: var player = get_tree().get_root().get_node("Spatial/player") player.walk_speed = orig_walk / 2.0 player.run_speed = orig_run / 2.0 $SlownessTimer.start() slowness_remaining += length text_dirty = true var apclient = global.get_node("Archipelago") apclient.saveLocaldata() func trigger_iceland_trap(length = 60): if not activated: queued_iceland += length return if iceland_remaining == 0: get_tree().get_root().get_node("Spatial/player/pivot/camera").set_environment( load("res://environments/level_iceland.tres") ) $IcelandTimer.start() iceland_remaining += length text_dirty = true var apclient = global.get_node("Archipelago") apclient.saveLocaldata() func trigger_atbash_trap(): var newly_atbash = (atbash_remaining == 0) atbash_remaining += 1 if newly_atbash: var apclient = global.get_node("Archipelago") apclient.evaluateSolvability() text_dirty = true var apclient = global.get_node("Archipelago") apclient.saveLocaldata() func deactivate_atbash_trap(): if atbash_remaining > 0: atbash_remaining -= 1 if atbash_remaining == 0: var apclient = global.get_node("Archipelago") apclient.evaluateSolvability() text_dirty = true var apclient = global.get_node("Archipelago") apclient.saveLocaldata() func show_puzzle_skip_message(node_path): if puzzle_focused and node_path != puzzle_to_skip: hide_puzzle_skip_message() var panel_input = get_tree().get_root().get_node(node_path) if not panel_input.visible: return var ap_panel = panel_input.get_parent().get_parent().get_parent().get_parent().get_node_or_null( "AP_Panel" ) if ap_panel == null or not ap_panel.solvable: return puzzle_focused = true wallcast.enabled = true not_behind_wall = false text_dirty = true puzzle_to_skip = node_path _evaluate_puzzle_skip() func hide_puzzle_skip_message(): puzzle_focused = false wallcast.enabled = false not_behind_wall = false text_dirty = true _evaluate_puzzle_skip() func enter_solve_mode(): solve_mode = true _evaluate_puzzle_skip() func exit_solve_mode(): solve_mode = false _evaluate_puzzle_skip() func skip_puzzle(): if not solve_mode and puzzle_focused: var apclient = global.get_node("Archipelago") if apclient.getAvailablePuzzleSkips() > 0: apclient.usePuzzleSkip() get_tree().get_root().get_node(puzzle_to_skip).complete() func _evaluate_puzzle_skip(): if puzzle_focused and not solve_mode: skip_available = true else: skip_available = false text_dirty = true func _tick_slowness(): slowness_remaining -= 1 text_dirty = true if slowness_remaining == 0: var player = get_tree().get_root().get_node("Spatial/player") player.walk_speed = orig_walk player.run_speed = orig_run $SlownessTimer.stop() if slowness_remaining % 5 == 0: var apclient = global.get_node("Archipelago") apclient.saveLocaldata() func _tick_iceland(): iceland_remaining -= 1 text_dirty = true if iceland_remaining == 0: get_tree().get_root().get_node("Spatial/player/pivot/camera").set_environment( orig_env ) $IcelandTimer.stop() if iceland_remaining % 5 == 0: var apclient = global.get_node("Archipelago") apclient.saveLocaldata() func _process(_delta): if puzzle_focused: var should_nbw = false if wallcast.is_colliding(): var player = get_tree().get_root().get_node("Spatial/player") var puzzlecast = player.get_node("pivot/camera/RayCast_sight") var distance = puzzlecast.get_collision_point().distance_to(wallcast.get_collision_point()) should_nbw = (distance < 0.05) if should_nbw != not_behind_wall: not_behind_wall = should_nbw text_dirty = true if text_dirty: text_dirty = false var text = "" if atbash_remaining == 1: text += "Atbash Trap lasts until you solve a puzzle" if atbash_remaining > 1: text += ("Atbash Trap lasts until you solve %d puzzles" % atbash_remaining) if slowness_remaining > 0: if not text.empty(): text += "\n" text += "Slowness: %d seconds" % slowness_remaining if iceland_remaining > 0: if not text.empty(): text += "\n" text += "Iceland: %d seconds" % iceland_remaining if skip_available and not_behind_wall: var apclient = global.get_node("Archipelago") if apclient.getAvailablePuzzleSkips() > 0: if not text.empty(): text += "\n" text += "Press P to skip puzzle (%d available)" % apclient.getAvailablePuzzleSkips() self.get_node("label").text = text olor: #888888 } /* Generic.Output */ .highlight .gp { color: #555555 } /* Generic.Prompt */ .highlight .gs { font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #666666 } /* Generic.Subheading */ .highlight .gt { color: #aa0000 } /* Generic.Traceback */ .highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #008800 } /* Keyword.Pseudo */ .highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */ .highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */ .highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ .highlight .na { color: #336699 } /* Name.Attribute */ .highlight .nb { color: #003388 } /* Name.Builtin */ .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
The Relentless is complicated because it makes heavy use of the keyholder
mechanic. There are three rooms, and you are expected to enter each room missing
certain letters. Solving the available puzzles in each room opens the doors
between the rooms, which lets you cross into them with a different set of
missing letters.

There currently isn't a way to represent "is missing certain letters" in our map
data or randomizer state. Instead, we use rooms to emulate knowing which letters
are available. There is a room for each of the three entrances, containing the
puzzles solvable with the expected missing letters. There's a room for each of
the inner pairs of rooms, representing what becomes available when one of the
doors is opened, and a room representing what is solvable when both doors are
opened.

This is all done with the expectation that you are always entering The
Relentless with the correct letters in the Control Center's keyholders. Because
of this, the warps to The Relentless are not randomizable. The Control Center
keywords that open these warps are also not randomizable. It'd be nice to find a
way to randomize this at a later point.

Also note that in order to keep this functioning properly, if the player
receives a letter item while in The Relentless, the mod should hold off on
adding it to the player's keyboard. We may want to overhaul how keyholders work
entirely and just have some kind of thing in the Archipelago client's global
state.