Ladder Man

As a fun exercise I decided to make a clone to Timberman. Of course, making a straight up clone would be kind of lame, so I took the gameplay and I put it in a different scenario.

Ladder Man - you tap on one side of the screen to climb the ladder on that side. if you climb into a gap, you fall and die. if you hold on to one rung for too long, you fall and die.

Here are some screenshots, what do you guys think?


@JakAttak Looks fun, I’d play it anyway! Are you planning on publishing it or just doing it for practice/fun? Also, as a sidebar question, are you using sprites/meshes for your graphics or making them out of shapes?

@Staples, I don’t have plans to publish it, I might but it is doubtful. I may release the code (though it is kind of messy because it was just a playing around) as an example for newbies.

The graphics are all from platformer art by the incredibly talented kenny.nl

Here’s the code as it stands if you just wanna give it a go:


--# Main
-- Ladderman

displayMode(FULLSCREEN)
supportedOrientations(PORTRAIT)
function setup()
    PLAYING, LOST = 1, 2
    MODE = PLAYING
    
    stuffs()
    initialise()
end

function draw()
    background(255)
    
    if MODE == PLAYING or MODE == LOST then
        drawGame()
    end
    
    if MODE == LOST then
        drawLost()
    end
end

function touched(t)
    if t.state == ENDED then
        if MODE == PLAYING then
            touchPlaying(t)
        elseif MODE == LOST then
            touchLost(t)
        end
    end
end

--# Playing
-- Just holds most of the functions for the playing state

-- Resets the game
function initialise()
    ladders = {}
    player = { x = rightX, y = sizes.ladder.y * 3.5, iid = 1, angle = 0 }
    sign = { x = 0, y = sizes.ladder.y + sizes.sign.y / 2, alpha = 0 }
    
    for r = 1, math.ceil(HEIGHT / sizes.ladder.y) + 1 do
        addRow(true)
    end
    
    groundO = sizes.ladder.y
    score = 0
    highscore = readLocalData("highscore") or 0
    time = .5
    if timeT ~= nil then tween.stop(timeT) end
    canMove = true
end


-- Draws everything for the playing state
function drawGame()
    -- Draw the 'bulding' background
    for bi = #ladders, 1, -2 do
        sprite(imgs.brick, WIDTH / 2, ladders[bi].y, WIDTH, sizes.ladder.y)
    end
        
    -- Draw the ladders
    for li = #ladders, 1, -1 do
        local ladder = ladders[li]
        if not ladder.hole then
            sprite(imgs.ladder, ladder.x, ladder.y, sizes.ladder.x, sizes.ladder.y)
        end
    end

    -- Draw the player
    pushMatrix() translate(player.x, player.y) rotate(player.angle)
    sprite(imgs.player[player.iid], 0, 0, sizes.player.x, sizes.player.y)
    popMatrix()
        
    -- Draw the ground if it is still onscreen
    if groundO < sizes.ladder.y then
        sprite(imgs.ground, WIDTH / 2, sizes.ladder.y / 2 - groundO, WIDTH, sizes.ladder.y)
    end
        
    -- Draw the timer bar
    local o = WIDTH / 200
    local a = (rightX - sizes.ladder.x / 2) - (leftX + sizes.ladder.x / 2)
    sprite(imgs.bar[1], WIDTH / 2, HEIGHT - HEIGHT / 40, a, HEIGHT / 20)
    sprite(imgs.bar[2], WIDTH / 2, HEIGHT - HEIGHT / 40, (a - o*1.8) * time, HEIGHT / 20 - o*1.8)
        
    
    -- Draws the score and then the highscore right below it
    sprite(imgs.panel, WIDTH / 2, HEIGHT / 2, a / 1.5)
    font(GAMEFONT)
    fill(0) if score >= highscore then fill(255,0,0) end fontSize(WIDTH / 10)
    text(score, WIDTH / 2, HEIGHT / 2)
    fill(255,0,0) fontSize(WIDTH / 20)
    text(math.max(highscore, score), WIDTH / 2, HEIGHT / 2 - fontSize() * 1.5)
    fill(0)
    text("SCORE", WIDTH / 2, HEIGHT / 2 + fontSize() * 1.5)
    stroke(0, 155, 255, 153) strokeWidth(oneS * 2)
    line(WIDTH / 2 - a / 3.27, HEIGHT / 2 - fontSize(), WIDTH / 2 + a / 3.27, HEIGHT / 2 - fontSize())
    line(WIDTH / 2 - a / 3.27, HEIGHT / 2 + fontSize(), WIDTH / 2 + a / 3.27, HEIGHT / 2 + fontSize())
    
    
    -- Lost / died sign
    tint(255, sign.alpha)
    sprite(imgs.sign, sign.x, sign.y, sizes.sign.x, sizes.sign.y)
    noTint()
end

-- Losing sequence
function fall()
    canMove = false
    
    if timeT ~= nil then tween.stop(timeT) end
    
    tween(.75, _G, { groundO = 0 })
    falling = tween(.75, player, { y = 0, angle = 230 }, tween.easing.linear, function()
        sign.x = player.x + sizes.ladder.x / 2 + sizes.sign.x / 1.75
        if player.x == leftX then sign.x = player.x - sizes.ladder.x / 2 - sizes.sign.x / 1.75 end
        falling = tween(.5, sign, { alpha = 255 }, tween.easing.linear, function() 
            animateEndMenu()
            MODE = LOST
        end)
    end)
end

-- Adds another row to the top of the ladder
function addRow(rnok)
    local y
    if #ladders == 0 then y = sizes.ladder.y / 2
    else y = ladders[#ladders].y + sizes.ladder.y end
    
    local rand = math.random(1, 5)
    local rhole, lhole = false, false
    if not rnok then
        if rand == 2 and lastRand ~= 3 then rhole = true
        elseif rand == 3 and lastRand ~= 2 then lhole = true end
    end
    lastRand = rand
    
    table.insert(ladders, { x = rightX, y = y, hole = rhole })
    table.insert(ladders, { x = leftX, y = y, hole = lhole })
end

-- Checks if the player is in a hole
function checkLoss()
    for li = #ladders, 1, -1 do
        local ladder = ladders[li]
        if ladder.hole and ladder.x == player.x and math.abs(ladder.y - player.y) < sizes.ladder.y / 2 then
            if score > highscore then
                saveLocalData("highscore", score)
            end
            
            fall()
            return true
        end
    end
    
    return false
end

-- Moves ladders and player, and calls to check if player is in a hole
function moveObjects(right)
    canMove = false
    player.iid = 2
    
    if right then
        player.x = rightX
    else
        player.x = leftX
    end
    
    checkLoss()
    
    for li = #ladders, 1, -1 do
        local ladder = ladders[li]
        tween(0.04, ladder, { y = ladder.y - sizes.ladder.y })
        
        if ladder.y < 0 then
            table.remove(ladders, li)
        end
    end
    
    tween.delay(0.04, function()
        addRow()
    
        if not checkLoss() then
            player.iid = 1
    
            score = score + 1
            canMove = true
        end
    end)
end

-- Adds a bit of time to the timer
function addToTimer()
    if timeT ~= nil then
        tween.stop(timeT)
        timeT = nil
    end
    
    time = math.min(time + .04, 1)
    timeT = tween(time * 8, _G, { time = 0 }, tween.easing.linear, fall) 
end

-- All the touch code for the playing state
function touchPlaying(t)
    if canMove then
        moveObjects(t.x > WIDTH / 2)
        addToTimer()
    end
end

--# Lost
-- Just holds most of the functions for the lost state
function animateEndMenu()
    tween(.5, replay, { y = HEIGHT / 2 })
end

function animateHideEndMenu()
    tween(.5, replay, { y = HEIGHT + sizes.replay.y / 2 }, tween.easing.linear, function()
        initialise()
        MODE = PLAYING
    end)
end

function drawLost()
    sprite(imgs.panel, replay.x, replay.y, sizes.replay.x, sizes.replay.y)
    stroke(0, 155, 255, 153) strokeWidth(oneS * 2)
    local lof, sof, tof = sizes.replay.x / 2.17, sizes.replay.y / 4, sizes.replay.y / 9
    line(replay.x - lof, replay.y - sof, replay.x + lof, replay.y - sof)
    line(replay.x - lof, replay.y + sof, replay.x + lof, replay.y + sof)
    
    fill(0) fontSize(WIDTH / 8)
    text("GAME OVER", replay.x, replay.y + sof + tof)
    text("PLAY AGAIN?", replay.x, replay.y - sof - tof)
end

function touchLost(t)
    if t.x >= replay.x - sizes.replay.x / 2 and t.x <= replay.x + sizes.replay.x / 2
    and t.y >= replay.y - sizes.replay.y / 2 and t.y <= replay.y - sizes.replay.y / 4 then
        animateHideEndMenu()
    end
end

--# Variables
function stuffs()
    GAMEFONT = "Futura-CondensedMedium"
    
    oneS = WIDTH / 768
    
    
    local bimg, gimg = makeImgs()
    imgs = {
        ladder = readImage("Dropbox:ladder_mid"),
        player = { readImage("Dropbox:alienGreen_climb1"), readImage("Dropbox:alienGreen_climb2") },
        ground = gimg,
        sign = readImage("Dropbox:signRip"),
        brick = bimg,
        bar = { readImage("Dropbox:barHorizontal_shadow_dark_full"), readImage("Dropbox:barHorizontal_red_full") },
        panel = readImage("Dropbox:glassPanel_corners")
    }
    
    sizes = {
        ladder = vec2(WIDTH / 9, HEIGHT / 9),
        player = vec2(WIDTH / 10, HEIGHT / 9.5),
        sign = vec2(WIDTH / 8, HEIGHT / 9),
        replay = vec2(WIDTH / 1.5, WIDTH / 1.5)
    }
    
    replay = { x = WIDTH / 2, y = HEIGHT + sizes.replay.y / 2 }
    
    leftX = WIDTH / 3.5
    rightX = WIDTH * 2.5/3.5
end

function makeImgs()
    local brickImg = image(WIDTH, HEIGHT / 9) setContext(brickImg)
    for i = 1, 9 do
        sprite("Dropbox:stoneCenter", -WIDTH / 18 + (WIDTH / 9) * i, HEIGHT / 18, WIDTH / 9, HEIGHT / 9)
    end
    setContext()
    
    local groundImg = image(WIDTH, HEIGHT / 9) setContext(groundImg)
    for i = 1, 9 do
        sprite("Dropbox:grassMid", -WIDTH / 18 + (WIDTH / 9) * i, HEIGHT / 18, WIDTH / 9, HEIGHT / 9)
    end
    setContext()
    
    
    return brickImg, groundImg
end

You will have to use placeholder sprited though… (they are all defined in the variables tab, under fairly clear names so you could pick something that fits)

I plan to add a formal menu screen at the beginning and redo the replay screen as I’m not super pleased with it.

thank you @jakattak.
Once again, the images have to be replaced…
That make me wonder… what about creating a public repo for shared game images? Maybe we could use some already existing web service? twitter, or else? The equivalent of gist on github.

@JakAttak @Jmv38 you could share your images in a link with your code using Dropbox, imgur, etc.
NEW IDEA: there could be one public shared Codea folder on Dropbox for people to upload their images (though this may be very memory consuming).

@JakAttak thanks for sharing the code. Though it looks a bit wonky with my replacement images, it works well. Nice job!

Well done! I’ll try the code as soon as I can :wink:

@Staples what would be the api for sharing via dropbox? Could you post a working example? Each time i tried from the ipad, i could not even get a usable link from propbox (it goes through their touch interface first, instead of accessing directly the image)

@Jmv38, @Staples, I wrote about this

http://coolcodea.wordpress.com/2014/02/17/150-where-to-store-images-for-downloading-by-codea/

Yes, I have code to download images to Dropbox, guess its time to break it out :slight_smile:

@ignatz wow, great post thanks a lot! PhotoBucket seems great, do you know how to send there an image from codea?

The user and all related content has been deleted.

@Jmv38: http://codea.io/talk/discussion/4679/uploading-images-using-codea/p1

scroll down and you’ll see some toffer magic

@NatTheCoder that’s because you don’t have the images used by @JakAttak. You have to replace them with default images built in to Codea for the game to run.

The user and all related content has been deleted.

Here we go: https://gist.github.com/JakAttak/cce08f9c984ed81440dd

Added image loading code, redid replay screen, move player down a rung.

Enjoy!

i’ve just tried your code, but i get

error: [string "Loader = class()..."]:91: attempt to perform arithmetic on local 'b' (a nil value)

@Jmv38, so sorry about that, I guess I already had the images so did not do a ‘full’ test. Let me have a look.

EDIT: I found the problem: sneaky Dropbox changed the site code on me :stuck_out_tongue:

I have updated the code in the gist, so this: https://gist.github.com/JakAttak/cce08f9c984ed81440dd should work now.

Thanks!

@JakAttak This brings back memories of a game I played on my Apple II. It was called Lode Runner. There is an app called Lode Runner classic that looks just like what I played.