Pardon the mess, Play My Code is in beta!

READY TO PLAY?
CLICK TO LOG IN!

sign up - lost password

Raycaster - Zombies Attack2 »

You do not own this project, so changes will not be saved

/**
 * Raycaster
 * 
 * A technical demo of a small raycaster engine.
 * This is built using the (excellent) RayCaster tutorial
 * here: http://lodev.org/cgtutor/raycasting.html
 * 
 * Feel free to look through the code,
 * steal ideas, or re-use for your own projects!
 * 
 * It includes a simple raycasting engine (the Level class),
 * a bobbing hand to represent the player moving (the Hand),
 * and a day/night system (implemented in the Background class).
 * 
 * For movement is uses a 'ConcanicalMover' class. Conanical
 * movement is where you are fixed to tiles. You move forward
 * and back one whole tile at a time, and turn in 90 degree chunks.
 * 
 * But you can also move freely if you interface with the Level's
 * 'move' and 'turn' methods directly (that gives you Wolfenstein 3D
 * style movement).
 */
/*
 * TODO fix Quby bug with recursively calling a def
 * TODO index walls next to 0 map positions, so they are tinted when drawn
 * TODO write light tinting code
 */

showFPS()
textures = new Image("wallsSheet.png")

// move speed in squares per second
$MOVE_SPEED = 0.05
// turn speed in radians per second
$TURN_SPEED = 0.15
$ZOOM_SPEED = 0.005

$BOBBING_SPEED  = 0.17
$BOBBING_HEIGHT = 0.00 // 0.10

// how much to draw darkness
$SHADOW_ALPHA = 0.25

// increase/decrease these values to make the hand move
// more/less along the x and y axis
$HAND_UPDATE_SPEED = 2 / 13.0
$HAND_SWAY_X = 5
$HAND_SWAY_Y = 16


worldMap = [
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,0,0,0,1,0,0,0,0,1],
        [1,1,1,1,1,1,1,1,1,0,8,0,0,0,1,0,1,0,1,0,8,8,0,1],
        [1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,0,1,0,1,0,0,0,0,1],
        [1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,0,0,1],
        [1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1],
        [1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1,0,0,2,0,0,0,0,0,0,0,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1,0,0,2,0,1,1,1,0,0,0,0,0,1,1,1,1],
        [1,1,1,1,2,2,2,2,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,1],
        [1,1,1,1,2,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1],
        [1,1,1,1,2,8,0,0,0,0,8,0,1,1,1,1,0,0,0,0,0,1,1,0],
        [1,1,1,1,2,0,0,0,0,0,0,0,0,1,0,1,1,1,1,0,0,0,1,1],
        [1,1,1,1,2,2,2,2,0,0,0,0,0,1,0,1,1,1,1,0,0,0,1,1],
        [1,1,1,1,1,1,1,1,0,0,2,2,0,1,0,1,1,1,1,0,8,0,1,1],
        [1,1,1,1,1,1,1,1,0,0,2,0,0,1,0,1,1,1,1,0,0,0,1,1],
        [1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
        [1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
        [1,0,0,0,1,0,0,0,0,0,2,0,0,1,1,1,1,1,1,1,1,0,0,1],
        [1,0,0,0,1,1,1,1,0,0,2,0,0,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,0,0,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,1],
        [1,0,0,0,0,0,2,2,2,2,2,0,0,0,2,0,2,2,2,2,0,0,0,1],
        [1,0,0,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,1],
        [1,0,0,0,0,0,2,0,8,0,2,0,0,0,2,0,0,8,0,2,0,0,0,1],
        [1,0,0,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,0,2,0,0,0,1],
        [1,0,0,0,0,0,2,2,0,2,2,0,0,0,2,2,0,0,2,2,0,0,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,2,0,2,0,0,0,0,0,0,2,2,2,2,2,2,2,2,0,2,2,2,2,1],
        [1,2,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,2,0,0,0,1],
        [1,2,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,2,0,8,0,1],
        [1,2,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,2,0,0,0,1],
        [1,2,0,2,2,2,2,2,2,0,2,0,0,0,0,0,0,2,0,2,0,0,0,1],
        [1,2,0,0,0,0,0,0,2,0,2,0,0,0,0,0,2,2,0,2,2,2,0,1],
        [1,2,0,0,0,0,0,0,2,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,2,0,0,0,0,0,0,2,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,2,2,2,2,2,2,2,2,0,2,0,8,0,0,0,0,0,0,0,0,0,0,1],
        [1,2,9,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,2,2,2,2,2,2,2,2,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
]

// NOTE: 
// 9 = player start position
// 8 = enemy position
// 0 = empty
// the rest are walls


sprites =[]

 // x and y start position
startX = 41.0
startY = 12.0


worldmap.eachIndex() do |row_index|  
    worldmap[row_index].eachIndex() do |col_index|
        if(worldmap[row_index][col_index] == 8) 
            sprites.push(new Sprite( new Image('sprite.png'), row_index, col_index))
        end
        if(worldmap[row_index][col_index] == 9) 
            startY = col_index
            startX = row_index
        end
        if(worldmap[row_index][col_index] > 7) 
            worldmap[row_index][col_index] = 0 // remove them from the map leaving only walls
        end
    end
end


 // x and y start position
startAngle = 180.toRadians()

back = new Background(
        new Image( 'ceiling.jpg' ),
        new Image( 'floor.jpg' ),
        190, 29, 42,
        -10, 160, 200,
        0.0003
)
    
level = new Level( worldMap, textures, $SHADOW_ALPHA, startX, startY, startAngle , sprites)
hand  = new Hand(new Image('fist.png'), new Image('clawHand.png'), 300, 180, $HAND_SWAY_X, $HAND_SWAY_Y )
controls = new FreeMover( level, $MOVE_SPEED, $TURN_SPEED/4, hand)

bob = 1

setFont( 'Arial', 16 )

onEachFrame() do |delta|
    // update ceiling and floor
    back.update( delta )
    
    c = getControls()
    if c.isKeyDown('c')
        level.zoom(  $ZOOM_SPEED*delta )
    else if c.isKeyDown('z')
        level.zoom( -$ZOOM_SPEED*delta )
    else if c.isKeyDown('x')
        level.setZoom( 0 )
    end
        
    bobHeight = bob.sin() * $BOBBING_HEIGHT
    if c.isKeyDown('control')
        level.setHeight( bobHeight - 0.2 )
    else if
        level.setHeight( bobHeight )
    end

    hand.update_fist(delta)

    if controls.update(delta)
        hand.update( delta * $HAND_UPDATE_SPEED )
        
        bob = bob + delta*$BOBBING_SPEED
    end
    
    // render the scene
    back.draw()
    level.draw( back.getBrightness() )
    hand.draw()
    sprites.each() do |sprite|
        sprite.update(delta)
    end
end

class FreeMover
    def new( level, moveSpeed, turnSpeed ,hand)
        @level = level
        @moveSpeed = moveSpeed
        @turnSpeed = turnSpeed
        @hand = hand
    end
    
    def update( delta )
        move = @moveSpeed*delta
        turn = @turnSpeed*delta
        
        moved   = true
        strafed = true
        turned  = true
        
        c = getControls()
        
        // turning
        if c.isKeyDown('right')
            @level.turn( -turn )
        else if c.isKeyDown('left')
            @level.turn(  turn )
        else
            turned = false
        end
        
        // strafing
        if c.isKeyDown('d')
            @level.move( 0,  move )
        else if c.isKeyDown('a')
            @level.move( 0, -move )
        else
            strafed = false
        end
        
        // forward/back
        if c.isKeyDown('w')
            @level.move(  move, 0 )
        else if c.isKeyDown('s')
            @level.move( -move, 0 )
        else
            moved = false
        end
        
        if c.isKeyDown('enter')
            @hand.fist()
        end

return moved or strafed or turned
    end
end


/**
 * This runs the day/night cycle.
 * The sky, and the floor, slowly get brighter and darker
 * over time.
 */
class Background
    def new(
            // images for top and bottom
            sky, floor,
            // standard color
            r, g, b,
            // offset colors
            offsetR, offsetG, offsetB,
            speed
    )
        @tick = 0
        
        @offsetR = offsetR
        @offsetG = offsetG
        @offsetB = offsetB
        
        @r = r
        @g = g
        @b = b
        
        @speed = speed
        
        @sky = sky
        @floor = floor
    end
    
    def update( delta )
        @tick = @tick + delta*@speed
    end
    
    /**
     * Returns a value that represents the percentage
     * between night and dark.
     * 
     * This is a value from 0 to 1.
     * 
     * 0.0 is the darkest point of night time, whilst
     * 1.0 it the lightest point of day time.
     * 
     * @return a Value from 0.0 to 1.0 that represents the night to day percentage.
     */
    def getBrightness()
        return (@tick.sin() + 1) / 2
    end
    
    def getColor()
        brightness = @tick.sin()
        yield @r + brightness*@offsetR,
                @g + brightness*@offsetG,
                @b + brightness*@offsetB
    end
    
    def draw()
        brightness = @tick.sin()
        
        this.getColor() do |r, g, b|
            setColor( r, g, b )
        end
        drawImage( @sky, 0, 0 )
        
        avg = ( @r + @g + @b ) / 2.5
        avgOffset = ( @offsetR + @offsetG + @offsetB ) / 3
        floorColor = avg + avgOffset*(brightness/3)
        setColor( floorColor, floorColor, floorColor )
        drawImage( @floor, 0, getScreenHeight()-@floor.getHeight() )
    end
end

class Sprite
    def new( img, x, y )
        @img = img
        @x = x
        @y = y
        @spriteWidth = 65
        @spriteHeight = 65
        @elapsed_time = 0
        @time_per_frame = 5
        number_of_frames = 8
        @animation_length = number_of_frames * @time_per_frame
        @frame = 0
    end
    
    
    def update(delta)
        
        @elapsed_time = (@elapsed_time + delta) % @animation_length 
        @frame = (@elapsed_time / @time_per_frame).floor()
    end
    
    def x
        return @x
    end

    def width
        return @spriteWidth
    end

    def y
        return @y
    end    
    
    def drawStripe(spriteX, spriteXMax, screenX, drawStartY, drawEndY)
        frame_offsetX  =  @spriteWidth * @frame

        texX = (spriteX * @spriteWidth)/spriteXMax
        setColor( 255, 255, 255 ,1)
        // draw the pixels of the stripe as a vertical line
        drawImage( @img, 0 + texX.floor() + frame_offsetX,0, 1, @spriteHeight, screenX , drawStartY,1,drawEndY - drawStartY)

    end

end




/**
 * This provides the hand,
 * which shows the player.
 * 
 * As it updates the hand bobs up and down,
 * and drifts from side to side.
 * 
 * This is to help give a walking sensation.
 */
class Hand
    def new( fist_img, img, x, y, xSway, ySway )
        @img = img
        @current_image = img
        @fist = fist_img
        @x = x
        @y = y
        @angle = 0
        @elapsed_time = 0
        
        @xSway = xSway
        @ySway = ySway
    end
    def fist()
        @current_image = @fist
        @elapsed_time = 0
    end
    def open()
        @current_image = @img
    end
    
    def update( delta )
        @angle = @angle + delta
    end

    def update_fist( delta )
        @elapsed_time = @elapsed_time + delta
        if (@elapsed_time > 15)
            open()
        end
    end


    def draw()
        setColor( 255, 255, 255, 1 )
        drawImage( @current_image,
                @x + @angle.sin()*@xSway,
                @y + @angle.cos()*@ySway
                 )
    end
end



/**
 * This class is the actual raycaster.
 * You provde a map, textures,
 * and a starting position and angle.
 * 
 * The Level stores a camera inside,
 * and this can be moved around with the
 * 'move' and 'turn' methods.
 * 
 * They automatically prevent walking into walls.
 * 
 * Call 'draw' to draw the level from the current
 * camera position.
 * 
 * Each tile in the world is 1.0 wide, so if you move
 * by 1.0 then you move a whole tile in one step.
 * 
 * All rotations are in radians.
 * 
 * The lighting is put together based on several values,
 * these include:
 *  = the brightness given when you call draw,
 *    this allows you to have night-time and
 *    day-time sections.
 *  = the level of the fog-shadow,
 *    this affects walls in the background
 *    getting darker (like a fog).
 *    Walls close up also get a tiny bit lighter.
 *  = walls working in one direction are darker,
 *    this is to stop corners from looking flat.
 * 
 * You can also set a shadow color which is used when
 * drawing the shading. By default this is black,
 * but other colours can be used to give different effects.
 */
class Level
    /**
     * 
     * 
     * @param map An array describing the layout of the map.
     * @param textures Textures to use for rendering the map.
     * @param x The starting x location for the camera
     * @param y The starting y location for the camera.
     * @param angle The starting angle for the camera.
     */
    def new(
            map, textures, shadowAlpha,
            x, y, angle, sprites
    )
        @map = map
        @textures = textures
        @sprites = sprites
        
        @posX = x.floor() + 0.5
        @posY = y.floor() + 0.5

        this.setAngle( angle )
        
        @zoom  = 0
        @zoomX = 0
        @zoomY = 0
        @stretch = 0
        @stretchFactor = 250
        
        @fogShadow = .5
        @fogR = 0
        @fogG = 0
        @fogB = 0
        
        @onCollision = null
        
        @shadowAlpha = shadowAlpha
        
        @height = 0
        
        @lightMap = null
        @lightStrengthMap = null
        @lights = []
        
        @zbuffer = new Array(getScreenWidth()).fill(1000)
    end
    
    def addLights( lights )
        lights.each() do |light|
            @lights.add( light )
        end
        
        indexLights()
    end

    def addLight( light )
        @lights.add( light )
        
        indexLights()
    end
    
    def indexLights()
        lightMap = {}
        strengthMap = {}
        
        @lights.each() do |light|
            x = light.getX()
            y = light.getY()
            
            dist = light.getDist()
            lightSearch = def( i, j, count )
                lightXY = this.getWorldMap( i, j )
                
                if lightXY.is( :number ) and lightXY == 0
                    index = i + '_' + j
                    
                    lights = lightMap[ index ]
                    if lights == null
                        lightMap[ index ] = [ light ]
                        strengthMap[ index ] = [ count ]
                    else if ! lights.contains( light )
                        lightMap[ index ].add( light )
                        strengthMap[ index ].add( count )
                    end
                    
                    if count > 0
//                        lightSearch.call([ i+1, j  , count-1 ])
//                        lightSearch.call([ i-1, j  , count-1 ])
//                        lightSearch.call([ i  , j+1, count-1 ])
//                        lightSearch.call([ i  , j-1, count-1 ])
                    end
                end
            end
        end
        
        @lightMap = lightMap
        @lightStrengthMap = strengthMap
    end
    
    def setFog( alpha )
        @fogShadow = alpha
    end
    
    def getFog()
        return @fogShadow
    end
    
    def setShadow( r, g, b )
        @fogR = r
        @fogG = g
        @fogB = b
    end
    
    def setHeight( height )
        @height = height.limit( -1, 1 )
    end
    
    def getHeight()
        return @height
    end
    
    /**
     * A function to call when this collides with a wall.
     * The action should take two parameters,
     * the x and y location of the wall being collided with.
     * 
     * This x/y location is rounded to the x/y location of
     * the wall in the map.
     * 
     * Note that this x and y location is not the location
     * of the camera, but the location of the wall.
     * 
     * Optionally, the function can have a third parameter
     * this will be this level object.
     */
//    def onCollision( action )
//        @onCollision = action
//    end
    
    /**
     * Moves the camera along the x and y axis given.
     * 
     * The movement is in relation to the camera's
     * current angle in the level.
     * 
     * Passing in a positive value for forward, moves the
     * camera forward, and a negative value moves you back.
     * 
     * A positive value for strafe moves you to the right,
     * whilst a negative value moves you to the left.
     * 
     * For any value you don't want to use, like if you
     * don't want to strafe, pass in '0'.
     * 
     * @param forward The amount to move forward or back.
     * @param strafe The amount to move the camera to the side.
     */
    def move( forward, strafe )
        if forward != 0
            this.moveCamera( forward * @dirX, forward * @dirY )
        end
        
        if strafe != 0
            this.moveCamera( strafe * @strafeX, strafe * @strafeY )
        end
    end
    
    /**
     * Moves the camera directly by the amounts given.
     * 
     * The difference between 'moveCamera' and the
     * other 'move' method is that this moves the camera
     * directly, whilst 'move' moves the camera in relation
     * to it's current angle.
     * 
     * For most movement you should use the other
     * move method instead of this one. This method
     * is for absolute direct control over camera
     * movement.
     * 
     * @param moveX The amount to move the camera along the x-axis.
     * @param moveY The amount to move the camera along the y-axis.
     */
    def moveCamera( moveX, moveY )
        newX = @posX + moveX
        newY = @posY + moveY
        
        isCollision = false
        
        if getWorldMap( @posX.floor(), newY.floor() ) == 0
            @posY = newY
        else
            isCollision = true
        end
        
        if getWorldMap( newX.floor(), @posY.floor() ) == 0
            @posX = newX
        else
            isCollision = true
        end
        
        if @onCollision and isCollision
            @onCollision.call([
                    @posX.floor(),
                    @posY.floor(),
                    this
            ])
        end
    end
    
    /**
     * Sets a new location for the camera to be
     * positioned in.
     * 
     * @param x The new x location for the camera.
     * @param y The new y location for the camera.
     */
    def setXY( x, y )
        if getWorldMap( x.floor(), y.floor() ) == 0
            @posX = x
            @posY = y
        end
    end
    
    /**
     * The level is split into tiles,
     * and the cameara moves across these.
     * 
     * Calling this method will center the camera
     * to be in the centre of which ever tile it
     * is currently within.
     */
    def centerXY()
        this.setXY(
                this.getX().floor() + 0.5,
                this.getY().floor() + 0.5
        )
    end
    
    /**
     * @return The y location of the camera.
     */
    def getX()
        return @posX
    end
    
    /**
     * @return The x location of the camera.
     */
    def getY()
        return @posY
    end
    
    /**
     * Turns the camera by the amount given.
     */
    def turn( rot )
        // rotate
        if rot != 0
            rotateXY( @dirX, @dirY, rot - 90.toRadians() ) do |newX, newY|
                @strafeX = newX
                @strafeY = newY
            end
            
            rotateXY( @dirX, @dirY, rot ) do |newX, newY|
                @dirX = newX
                @dirY = newY
            end
            
            rotateXY( @planeX, @planeY, rot ) do |newX, newY|
                @planeX = newX
                @planeY = newY
            end
            
            rotateXY( @zoomX, @zoomY, rot ) do |newX, newY|
                @zoomX = newX
                @zoomY = newY
            end
            
            @angle = @angle + rot
        end
    end
    
    def setAngle( angle )
        // initial direction vector
        @dirX = angle.cos()
        @dirY = angle.sin()
        
        strafeAngle = angle - 90.toRadians()
        @strafeX = strafeAngle.cos()
        @strafeY = strafeAngle.sin()
        
        // the 2d raycaster version of camera plane
        rotateXY( 0.0, -0.66, angle ) do |newX, newY|
            @planeX = newX
            @planeY = newY
        end
        
        @angle = angle
    end
    
    def getAngle()
        return @angle
    end
    
    /**
     * Increments the zoom by the amount given.
     * 
     * The total zoom can only be between -1 and 1,
     * so if the increment pushes the zoom outside of
     * this range then it will be capped.
     */
    def zoom( inc )
        setZoom( getZoom() + inc )
    end
    
    /**
     * Sets the zoom level for the camera for when the level
     * is rendered.
     * 
     * The zoom is expressed as a value from -1 to 1.
     * Negative values are a negative zoom,
     * positive values are zooming in,
     * and 0 is no zoom at all.
     * 
     * @param zoom The zoom level, a value from -1 to 1.
     */
    def setZoom( zoom )
        zoom = zoom.limit( -1.0, 1.0 ) * 0.65
        
        @zoom = zoom
        
        @zoomX = zoom*@dirY
        @zoomY = zoom*@dirX
        
        @stretch = zoom*@stretchFactor
    end
    
    /**
     * @return The current zoom level, a value from -1.0 to 1.0.
     */
    def getZoom()
        return @zoom / 0.66
    end

    /**
     * Draws the scene with no brightness set.
     * 
     * See the other draw method for details on the level drawing.
     */
    def draw()
        return draw( 1.0 )
    end
    
    /**
     * Draws the level, based on the current location of
     * the camera.
     * 
     * To draw the level differently, you can alter the cameras
     * location, angle or zoom using the other methods.
     * 
     * You can also set a brightness for the scene, for
     * when it is rendered. This is so you can have day/night
     * cycles, and sections in darkness.
     * 
     * @param brightness A value from 0.0 (dark) to 1.0 (bright) stating the brightness of the level.
     */
    def draw( brightness )
        brightness = (1 - brightness.limit(0, 1)) * 0.45
        shadowBrightness = (brightness+@shadowAlpha).limit(0, 1)
        
        // get these values here to cache them,
        // for faster lookup later
        worldMap = @map
        textures = @textures
        textureSize = @textures.getHeight()
        
        posX = @posX
        posY = @posY
        dirX = @dirX
        dirY = @dirY
        planeX = @planeX + @zoomX
        planeY = @planeY + @zoomY
        stretch = @stretch
        
        rayPosX = @posX
        rayPosY = @posY
        
        drawHeight = @height
        
        fogShadow = @fogShadow
        fogR = @fogR
        fogG = @fogG
        fogB = @fogB
        
        width  = getScreenWidth()
        height = getScreenHeight()
        width.times() do |x|
            // calculate ray position and direction
            cameraX = 2.0 * x / (1.0*width) - 1.0 // x-coordinate in camera space
            rayDirX = dirX + planeX * cameraX
            rayDirY = dirY + planeY * cameraX
            
            // which box of the map we're in 
            mapX = rayPosX.floor()
            mapY = rayPosY.floor()
            
            // length of ray from current position to next x or y-side
            sideDistX = 0
            sideDistY = 0
            
            // length of ray from one x or y-side to next x or y-side
            deltaDistX = (1.0 + ((rayDirY * rayDirY) / (rayDirX * rayDirX))).sqrt()
            deltaDistY = (1.0 + ((rayDirX * rayDirX) / (rayDirY * rayDirY))).sqrt()
            perpWallDist = 0
            
            // what direction to step in x or y-direction (+1 or -1)
            stepX = 0
            stepY = 0
    
            hit  = 0 // was there a wall hit?
            side = 0 // was a NS or a EW wall hit?
            
            // calculate step and initial sideDist
            if rayDirX < 0
                stepX = -1
                sideDistX = deltaDistX * (rayPosX - mapX)
            else
                stepX =  1
                sideDistX = deltaDistX * (mapX + 1.0 - rayPosX)
            end
            
            if rayDirY < 0
                stepY = -1
                sideDistY = deltaDistY * (rayPosY - mapY)
            else
                stepY =  1
                sideDistY = deltaDistY * (mapY + 1.0 - rayPosY)
            end
            
            // perform DDA
            while hit == 0
                // Jump to next map square,
                // OR in x-direction, OR in y-direction.
                if sideDistX < sideDistY
                    sideDistX = sideDistX + deltaDistX
                    mapX = mapX + stepX
                    side = 0
                else
                    sideDistY = sideDistY + deltaDistY
                    mapY = mapY + stepY
                    side = 1
                end
                
                // Check if ray has hit a wall
                wall = getWorldMap( mapX, mapY )
                if wall == null || wall > 0
                    hit = 1
                end
            end
            
            // Calculate distance projected on camera direction
            if side == 0
                temp = (mapX - rayPosX + ((1.0 - stepX) / 2.0)) / rayDirX        
                wallX = rayPosY + ( temp * rayDirY )
            else
                temp = (mapY - rayPosY + ((1.0 - stepY) / 2.0)) / rayDirY
                wallX = rayPosX + ( temp * rayDirX )
            end
            perpWallDist = temp.abs()
            
            @zbuffer[x] = perpWallDist

            wallX = wallX - wallX.floor()
            
            offset = ((height / perpWallDist) / 44) * drawHeight
            
            // Calculate height of line to draw on screen
            lineHeight = (height / perpWallDist).floor().abs()
            percentHeight = lineHeight / textureSize
            if stretch < 0 && lineHeight < -stretch
                lineHeight = 1
            else
                lineHeight = lineHeight + stretch
            end
            
            if lineHeight > 0
                // calculate lowest and highest pixel to fill in current stripe
                drawStart = ( -lineHeight / 2 + height / 2 ) + offset*20
                
                //x coordinate on the texture
                texX = wallX * textureSize
                if side == 0 && rayDirX > 0
                    texX = textureSize - texX
                end
                if side == 1 && rayDirY < 0
                    texX = textureSize - texX
                end
                
                // choose wall tex
                color = getWorldMap( mapX, mapY )
                if color == null
                    color = 0
                end
                
                texX = texX + textureSize*(color-1).min(7)
                if texX < 0
                    texX = texX + textureSize
                else if texX >= textures.getWidth()
                    texX = textures.getWidth() - textureSize
                end
                
                setColor( 255, 255, 255 )
                // draw the pixels of the stripe as a vertical line
                drawImage( textures,
                        texX.floor(), 0,
                        1, textureSize,
                        x, drawStart,
                        1, lineHeight
                )
                
                // give x and y sides different brightness
                distShadow = (1-percentHeight) * fogShadow
                
                // When your close to something, you make it brighter
                // but your not a walking light, so tone it down a little.
                if distShadow < 0
                    distShadow = distShadow/3
                end
                
                if side == 1
                    drawAlpha = shadowBrightness+distShadow
                else
                    drawAlpha = brightness+distShadow
                end
                
                if drawAlpha > 0
                    setColor( fogR, fogG, fogB, drawAlpha )
                    // draw the pixels of the stripe as a vertical line
                    fillRect( x, drawStart-1, 1, lineHeight+1 )
                end
            end
        end

/*
 *  Draw Sprites!!!
 */
        w = getScreenWidth()
        h = getScreenHeight()
        

        // calculate distance to each sprite
        // order sprites furthest to closest
      @sprites.map() do |sprite|

        spriteDistanceSquared = ((posX - sprite.x()) * (posX - sprite.x()) + (posY - sprite.y()) * (posY - sprite.y())); //sqrt not taken, unneeded
     //after sorting the sprites, do the projection and draw them
         return [spriteDistanceSquared, sprite]
      end
      
      
      
      @sprites.each() do |sprite|
    //    for(int i = 0; i < numSprites; i++) <-- one sprite for now
    //    {
          //translate sprite position to relative to camera
          spriteX = sprite.x() - posX;
          spriteY = sprite.y() - posY;
          
    
    
          //transform sprite with the inverse camera matrix
          // [ planeX   dirX ] -1                                       [ dirY      -dirX ]
          // [               ]       =  1/(planeX*dirY-dirX*planeY) *   [                 ]
          // [ planeY   dirY ]                                          [ -planeY  planeX ]
          
          invDet = 1.0 / (planeX * dirY - dirX * planeY); //required for correct matrix multiplication
          
          transformX = invDet * (dirY * spriteX - dirX * spriteY);
          transformY = invDet * (-planeY * spriteX + planeX * spriteY); //this is actually the depth inside the screen, that what Z is in 3D       
                
    
                
          spriteScreenX = ((w / 2) * (1 + transformX / transformY)).floor(); //<<< NWT : Should this be round?
                    
          
          //parameters for scaling and moving the sprites
          uDiv =1
          vDiv =1
          vMove =60.0
          
          vMoveScreen = (vMove / transformY).round(); //<<< NWT Should this be round?
          
          //calculate height of the sprite on screen
          spriteHeight = ((h / transformY).round()).abs() / vDiv; //using "transformY" instead of the real distance prevents fisheye
    
          //calculate lowest and highest pixel to fill in current stripe
          drawStartY = -spriteHeight / 2.0 + h / 2.0 + vMoveScreen;
          drawEndY = spriteHeight / 2.0 + h / 2.0 + vMoveScreen;
          
          //calculate width of the sprite
          spriteWidth = ( (h / transformY).round()).abs() / uDiv;
          drawStartX = spriteScreenX -(spriteWidth / 2.0) ;
          
          //loop through every vertical stripe of the sprite on screen
    //      fillText( "y: " + spriteScreenY, 30, 40 )
          
          0.upTo(spriteWidth) do |i|
            stripe = drawStartX + i
            //the conditions in the if are:
            //1) it's in front of camera plane so you don't see things behind you
            //2) it's on the screen (left)
            //3) it's on the screen (right)
            //4) ZBuffer, with perpendicular distance
            if(transformY > 0 && stripe > 0 && stripe < w && transformY < @zbuffer[stripe]) 
                sprite.drawStripe(i, spriteWidth, stripe, drawStartY, drawEndY)
            end
        end
    end
/*    
    drawBuffer(buffer[0]);
    for(int x = 0; x < w; x++) for(int y = 0; y < h; y++) buffer[x][y] = 0; //clear the buffer instead of cls()
*/
    end

    def getWorldMap( x, y )
        row = @map[x]
        
        if row != null
            return row[y]
        else
            return null
        end
    end
end

class Light
    def new(x, y, dist, strength)
        @x = x
        @y = y
        @dist = dist
        @strength = strength
        @r = 255
        @g = 255
        @b = 255
    end
    
    def new(x, y, dist, strength, r, g, b)
        @x = x
        @y = y
        @dist = dist
        @strength = strength
        @r = r
        @g = g
        @b = b
    end
    
    def getX()
        return @x
    end
    
    def getY()
        return @y
    end
    
    def getDist()
        return @dist
    end
end

def rotateXY( x, y, rot )
    yield x * rot.cos() - y * rot.sin(),
          x * rot.sin() + y * rot.cos()
end

ERRORS

YOUR BROWSER DOES NOT SUPPORT HTML5!

Please use one of these instead

Our games cannot run in your browser