Level End Menu

Motivation

At the end of each level, our team wanted to give the player a score in order to reinforce that speed and collection were important. We decided that the best way to implement this idea was to have a separate “end level” that would present the score.

Design

My focus in the design of this class was flexibility. This class was created midway through the project, and I knew we were going to have to make changes to this menu over time.

There are four main elements to the score menu: the title bar, the score table, a star rating, and a short poem based on that rating. Each of these four elements, as well as each element inside the table, can have its font, font size, position, and icons changed independently. We didn’t end up using some of this flexibility in the final product, bug having it available was helpful.

Code

End-of-Level Menu

    
        
--JS: File load and execution priority
priority = -1000

dofile( self, APP_PATH .. "scripts/include/AnimationBase.lua")
dofile( self, APP_PATH .. "scripts/include/Time.lua")

-------- UI Variables --------
--General
displayStartX   = 230
displayStartY   = 140
backgroundStr   = "x.lua"

--Title
titleFontString = "arial"
titleFontSize   = 24
titleOffsetX    = 400
titleOffsetY    = 140

--Table
tableFontString = "arial"
tableFontSize   = 12
tableOffsetX    = 150
tableOffsetY    = 180
tableSpacingX1  = 40
tableSpacingX2  = 100
tableSpacingX3  = 80
tableSpacingX4  = 80
tableSpacingY   = 40
tableRowIcoStr1 = "x.lua"
tableRowIcoStr2 = "x.lua"
tableRowIcoStr3 = "x.lua"

--Total Score
totalFontString = "arial"
totalFontSize   = 30
totalOffsetX    = 175
totalOffsetY    = 300

--Ratings
ratingFontStr   = "arial"
ratingFontSize  = 14
rateTextOffsetX = 500
rateTextOffsetY = 300
rateAnimOffsetX = 550
rateAnimOffsetY = 200
rateYGapOffset  = 20
ratingAnimStr1  = "x.lua"
ratingAnimStr2  = "x.lua"
ratingAnimStr3  = "x.lua"

-------- In-game Variables --------
--General
displayPosition = nil
backgroundAnim  = nil

--Title
titleTextString = "Title:"
titleFontObject = nil
titlePosition   = nil

--Table
tableFontObject = nil
tablePosition   = nil
tableRow1Icon   = nil
tableRow2Icon   = nil
tableRow3Icon   = nil

--Total Score
totalTextString = "Final Score: "
totalFontObject = nil
totalPosition   = nil

--Ratings
ratingFontObj   = nil
ratingAnimPos   = nil
ratingTextPos   = nil
ratingAnim1     = nil
ratingAnim2     = nil
ratingAnim3     = nil
ratingText11    = "Rating Haiku:"
ratingText12    = "You seem new at this"
ratingText13    = "You made too many mistakes"
ratingText14    = "Play level again"
ratingText21    = "Rating Haiku:"
ratingText22    = "You played this quite well"
ratingText23    = "It was a skillful display"
ratingText24    = "There is more to learn"
ratingText31    = "Rating Haiku:"
ratingText32    = "Your play was the best"
ratingText33    = "Your actions are without peer"
ratingText34    = "Your title: master"
scoreForRating2 = 20
scoreForRating3 = 35

--Row Enablers
showTitle       = true
showTableHeads  = true
showPlayerCoins = true
showTargetTime  = true
showPlayerTime  = true
showTotalScore  = true
showScoreRating = true

--Multipliers
coinMultiplier  = 1
timeMultiplier  = 1

--Score Components
playerCoins     = 0
playerCoinScore = 0
playerTime      = 0
targetTime      = 60
playerTimeScore = 0
totalScore      = 0

--Distance
displayDistance = 20

--Animation
animationAngle  = 0
animationTime   = 0

scoreSaveName   = "score"

ui = {
    --General
    uiHeadingMain   = { order =  1,  type = "boolean", label = "GENERAL SETTINGS:",             default = false                             },
    displayStartX   = { order =  2,  type = "number",  label = "Display Top Left Corner X:",    default = 230                               },
    displayStartY   = { order =  3,  type = "number",  label = "Display Top Left Corner Y:",    default = 140                               },
    backgroundStr   = { order =  4,  type = "anim",    label = "Background Image:",             default = "x.lua"                           },
    displayDistance = { order =  5,  type = "number",  label = "Rendering Distance Threshold:", default = 20                                },
    scoreSaveName   = { order =  6,  type = "string",  label = "Save Session Variable:",        default = "score"                           },

    --Title
    uiHeadingTitle  = { order = 10,  type = "boolean", label = "TITLE SETTINGS:",               default = false                             },
    titleTextString = { order = 11,  type = "string",  label = "Title Text:",                   default = "Title:"                          },
    titleFontString = { order = 12,  type = "string",  label = "Title Font:",                   default = "arial"                           },
    titleFontSize   = { order = 13,  type = "number",  label = "Title Font Size:",              default = 20                                },
    titleOffsetX    = { order = 14,  type = "number",  label = "Title X Offset:",               default = 400                               },
    titleOffsetY    = { order = 15,  type = "number",  label = "Title Y Offset:",               default = 140                               },

    --Table
    uiHeadingTable  = { order = 20,  type = "boolean", label = "TABLE SETTINGS:",               default = false                             },
    tableFontString = { order = 21,  type = "string",  label = "Table Font:",                   default = "arial"                           },
    tableFontSize   = { order = 22,  type = "number",  label = "Table Font Size:",              default = 12                                },
    tableOffsetX    = { order = 23,  type = "number",  label = "Table X Offset:",               default = 150                               },
    tableOffsetY    = { order = 24,  type = "number",  label = "Table Y Offset:",               default = 180                               },
    tableSpacingX1  = { order = 25,  type = "number",  label = "Table X Spacing(Icon Column):", default = 40                                },
    tableSpacingX2  = { order = 26,  type = "number",  label = "Table X Spacing(2nd Column):",  default = 100                               },
    tableSpacingX3  = { order = 27,  type = "number",  label = "Table X Spacing(3rd Column):",  default = 80                                },
    tableSpacingX4  = { order = 28,  type = "number",  label = "Table X Spacing(4th Column):",  default = 80                                },
    tableSpacingY   = { order = 29,  type = "number",  label = "Table Y Spacing:",              default = 40                                },
    tableRowIcoStr1 = { order = 30,  type = "anim",    label = "Table Coin Animation:",         default = "x.lua"                           },
    tableRowIcoStr2 = { order = 31,  type = "anim",    label = "Table Target Time Animation:",  default = "x.lua"                           },
    tableRowIcoStr3 = { order = 32,  type = "anim",    label = "Table Player Time Animation:",  default = "x.lua"                           },

    --Data  
    uiHeadingData   = { order = 35,  type = "boolean", label = "DATA SETTINGS:",                default = false                             },
    coinMultiplier  = { order = 36,  type = "number",  label = "Score Multiplier (Coins):",     default = 1                                 },
    timeMultiplier  = { order = 37,  type = "number",  label = "Score Multiplier (Time):",      default = 1                                 },
    targetTime      = { order = 38,  type = "number",  label = "Target Time (Seconds):",        default = 60                                },

    --Total
    totalTextString = { order = 40,  type = "string",  label = "Total Score Prefix Text:",      default = "Final Score: "                   },
    totalFontString = { order = 41,  type = "string",  label = "Total Score Font:",             default = "arial"                           },
    totalFontSize   = { order = 42,  type = "number",  label = "Total Score Font Size:",        default = 30                                },
    totalOffsetX    = { order = 43,  type = "number",  label = "Total Score X Offset:",         default = 175                               },
    totalOffsetY    = { order = 44,  type = "number",  label = "Total Score Y Offset:",         default = 300                               },

    --Ratings
    uiHeadingRate   = { order = 50,  type = "boolean", label = "RATINGS (Low to High):",        default = false                             },
    ratingFontStr   = { order = 51,  type = "string",  label = "Rating Font:",                  default = "arial"                           },
    ratingFontSize  = { order = 52,  type = "number",  label = "Rating Font Size:",             default = 14                                },
    rateTextOffsetX = { order = 53,  type = "number",  label = "Rating Text X Offset:",         default = 500                               },
    rateTextOffsetY = { order = 54,  type = "number",  label = "Rating Text Y Offset:",         default = 300                               },
    rateAnimOffsetX = { order = 55,  type = "number",  label = "Rating Anim X Offset:",         default = 550                               },
    rateAnimOffsetY = { order = 56,  type = "number",  label = "Rating Anim Y Offset:",         default = 200                               },
    rateYGapOffset  = { order = 57,  type = "number",  label = "Rating Y Gap Offset:",          default = 20                                },

    ratingAnimStr1  = { order = 61,  type = "anim",    label = "Low Rating Animation:",         default = "x.lua"                           },
    ratingText11    = { order = 62,  type = "string",  label = "Low Rating Text 1:",            default = "Rating Haiku:"                   },
    ratingText12    = { order = 63,  type = "string",  label = "Low Rating Text 2:",            default = "You seem new at this"            },
    ratingText13    = { order = 64,  type = "string",  label = "Low Rating Text 3:",            default = "You made too many mistakes"      },
    ratingText14    = { order = 65,  type = "string",  label = "Low Rating Text 4:",            default = "Play level again"                },

    scoreForRating2 = { order = 71,  type = "number",  label = "Medium Score Threshold:",       default = 20                                },
    ratingAnimStr2  = { order = 72,  type = "anim",    label = "Medium Rating Animation:",      default = "x.lua"                           },
    ratingText21    = { order = 73,  type = "string",  label = "Medium Rating Text 1:",         default = "Rating Haiku:"                   },
    ratingText22    = { order = 74,  type = "string",  label = "Medium Rating Text 2:",         default = "You played this quite well"      },
    ratingText23    = { order = 75,  type = "string",  label = "Medium Rating Text 3:",         default = "It was a skillful display"       },
    ratingText24    = { order = 76,  type = "string",  label = "Medium Rating Text 4:",         default = "There is more to learn"          },

    scoreForRating3 = { order = 81,  type = "number",  label = "Best Rating Threshold:",        default = 35                                },
    ratingAnimStr3  = { order = 82,  type = "anim",    label = "Best Rating Animation:",        default = "x.lua"                           },
    ratingText31    = { order = 83,  type = "string",  label = "Best Rating Text 1:",           default = "Rating Haiku:"                   },
    ratingText32    = { order = 84,  type = "string",  label = "Best Rating Text 2:",           default = "Your play was the best"          },
    ratingText33    = { order = 85,  type = "string",  label = "Best Rating Text 3:",           default = "Your actions are without peer"   },
    ratingText34    = { order = 86,  type = "string",  label = "Best Rating Text 4:",           default = "Your title: master"              },

    --Row Enabling
    uiHeadingEnable = { order = 90,  type = "boolean", label = "ROW DISABLE SETTINGS:",         default = false                             },
    showTitle       = { order = 91,  type = "boolean", label = "Show Title?",                   default = true                              },
    showTableHeads  = { order = 92,  type = "boolean", label = "Show Table Headings?",          default = true                              },
    showPlayerCoins = { order = 93,  type = "boolean", label = "Show Player Coins?",            default = true                              },
    showTargetTime  = { order = 94,  type = "boolean", label = "Show Target Time?",             default = true                              },
    showPlayerTime  = { order = 95,  type = "boolean", label = "Show Player's Time?",           default = true                              },
    showTotalScore  = { order = 96,  type = "boolean", label = "Show Total Score?",             default = true                              },
    showScoreRating = { order = 97,  type = "boolean", label = "Show Rating for Player?",       default = true                              }
}

function removeCollision()
    if ( rigidBody ~= nil ) then
        g.physics:remove( rigidBody )
    end
    rigidBody = g.physics:createBox( x, y, RigidBody_STATIC, iconW/pixelsPerUnit, iconH/pixelsPerUnit, 0 )
    rigidBody:setCollisionCategory( CollisionCategory_NONE )
    rigidBody:setCollisionMask( CollisionMask_NONE )
end

function init()

    removeCollision()

    --Turn off Base icon
    iconVisible = false

    updateAlways = false

    --Initialize animations first ( we need their sizes during position creation )
    backgroundAnim  = initializeAnimationOrReturnNil( backgroundStr )
    ratingAnim1     = initializeAnimationOrReturnNil( ratingAnimStr1 )
    ratingAnim2     = initializeAnimationOrReturnNil( ratingAnimStr2 )
    ratingAnim3     = initializeAnimationOrReturnNil( ratingAnimStr3 )
    tableRow1Icon   = initializeAnimationOrReturnNil( tableRowIcoStr1 )
    tableRow2Icon   = initializeAnimationOrReturnNil( tableRowIcoStr2 )
    tableRow3Icon   = initializeAnimationOrReturnNil( tableRowIcoStr3 )

    --Now figure out our display positions
    displayPosition = { x = displayStartX,                   y = displayStartY                   }
    titlePosition   = { x = displayStartX + titleOffsetX,    y = displayStartY + titleOffsetY    }
    tablePosition   = { x = displayStartX + tableOffsetX,    y = displayStartY + tableOffsetY    }
    totalPosition   = { x = displayStartX + totalOffsetX,    y = displayStartY + totalOffsetY    }
    ratingAnimPos   = { x = displayStartX + rateAnimOffsetX, y = displayStartY + rateAnimOffsetY }
    ratingTextPos   = { x = displayStartX + rateTextOffsetX, y = displayStartY + rateTextOffsetY }

    --Finish it up by initializing the fonts
    titleFontObject = Font( titleFontString, titleFontSize )
    tableFontObject = Font( tableFontString, tableFontSize )
    totalFontObject = Font( totalFontString, totalFontSize )
    ratingFontObj   = Font( ratingFontStr,   ratingFontSize )

end

function getDistanceToPlayer()
    playerLocation  = { x = g.player.rigidBody:posX(),  y = g.player.rigidBody:posY() }
    ourLocation     = { x = rigidBody:posX(),           y = rigidBody:posY() }

    return ( ( ourLocation.x - playerLocation.x ) * ( ourLocation.x - playerLocation.x ) ) + ( ( ourLocation.y - playerLocation.y ) * ( ourLocation.y - playerLocation.y ) )
end

function update( dt )

    if ( getDistanceToPlayer() < ( displayDistance * displayDistance ) ) then
        g.player.timerRunning = false
    else
        g.player.timerRunning = true
    end

    if ( showPlayerCoins ) then
        playerCoins = g.player.score
        playerCoinScore = playerCoins * coinMultiplier
    end

    if ( showPlayerTime ) then
        playerTime  = g.player.timeInLevel
        playerTimeScore = g.math.floor( ( targetTime - playerTime ) * timeMultiplier )
    end

    if ( showTotalScore ) then
        totalScore = playerCoinScore + playerTimeScore
        SetSessionVariable( scoreSaveName, totalScore )
    end

    animationTime = animationTime + dt

end

function render( dt )

    if ( getDistanceToPlayer() > ( displayDistance * displayDistance ) ) then
        return
    end

    if ( backgroundAnim ~= nil ) then
        backgroundAnim:draw( animationTime, displayPosition.x, displayPosition.y, animationAngle, Image_COORDS_SCREEN_TOPLEFT )
    end

    if ( showTitle ) then
        titleFontObject:draw( titlePosition.x, titlePosition.y, titleTextString )
    end

    rowPositionY = tablePosition.y
    if ( showTableHeads ) then
        renderTableRow( tableFontObject, tablePosition.x, rowPositionY, nil, "", "Collected", " Multiplier", "Score" )
    end

    if ( showPlayerCoins ) then
        rowPositionY = rowPositionY + tableSpacingY
        renderTableRow( tableFontObject, tablePosition.x, rowPositionY, tableRow1Icon, "Coins:", g.tostring( playerCoins ), g.tostring( coinMultiplier ), g.tostring( playerCoinScore ) )
    end

    if ( showTargetTime ) then
        rowPositionY = rowPositionY + tableSpacingY
        renderTableRow( tableFontObject, tablePosition.x, rowPositionY, tableRow2Icon, "Target Time:", rawTimeToString( targetTime ), " ", "" )
    end

    if ( showPlayerTime ) then
        rowPositionY = rowPositionY + tableSpacingY
        renderTableRow( tableFontObject, tablePosition.x, rowPositionY, tableRow3Icon, "Time:", rawTimeToString( targetTime - playerTime ), g.tostring( timeMultiplier ), g.tostring( playerTimeScore ) )
    end

    if ( showTotalScore ) then
        totalFontObject:draw( totalPosition.x, totalPosition.y, totalTextString .. g.tostring( totalScore ) )
    end

    if ( showScoreRating ) then
        
        if ( totalScore < scoreForRating2 ) then

            if ( ratingAnim1 ~= nil ) then
                ratingAnim1:draw( animationTime, ratingAnimPos.x, ratingAnimPos.y, animationAngle, Image_COORDS_SCREEN_TOPLEFT )
            end
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y,                           ratingText11 )
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + rateYGapOffset,          ratingText12 )
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 2 *  rateYGapOffset ), ratingText13 )
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 3 *  rateYGapOffset ), ratingText14 )

        elseif ( totalScore < scoreForRating3 ) then

            if ( ratingAnim2 ~= nil ) then
                ratingAnim2:draw( animationTime, ratingAnimPos.x, ratingAnimPos.y, animationAngle, Image_COORDS_SCREEN_TOPLEFT )
            end
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y,                           ratingText21 )
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + rateYGapOffset,          ratingText22 )
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 2 *  rateYGapOffset ), ratingText23 )
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 3 *  rateYGapOffset ), ratingText24 )

        else

            if ( ratingAnim3 ~= nil ) then
                ratingAnim3:draw( animationTime, ratingAnimPos.x, ratingAnimPos.y, animationAngle, Image_COORDS_SCREEN_TOPLEFT )
            end

            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y,                           ratingText31 )
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + rateYGapOffset,          ratingText32 )
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 2 *  rateYGapOffset ), ratingText33 )
            ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 3 *  rateYGapOffset ), ratingText34 )

        end

    end

end

function renderTableRow( fontObject, rowStartX, rowStartY, leftIconAnimation, leadingString, rawScoreString, multiplierString, multipliedScoreString )
    
    drawPositionX = rowStartX
    if ( leftIconAnimation ~= nil ) then
        leftIconAnimation:draw( animationTime, drawPositionX, rowStartY, animationAngle, Image_COORDS_SCREEN )
    end

    drawPositionX = rowStartX + tableSpacingX1
    fontObject:draw( drawPositionX, rowStartY - 10, leadingString )

    drawPositionX = drawPositionX + tableSpacingX2
    fontObject:draw( drawPositionX, rowStartY - 10, rawScoreString )

    drawPositionX = drawPositionX + tableSpacingX3
    --If the first character is a space, don't draw the multiplier. Instead, draw the string without the space.
    if ( multiplierString:sub( 1, 1 ) ~= " ") then
        fontObject:draw( drawPositionX, rowStartY - 10, "x" .. multiplierString )
    else
        fontObject:draw( drawPositionX, rowStartY - 10, multiplierString:sub( 2 ) )
    end

    drawPositionX = drawPositionX + tableSpacingX4
    fontObject:draw( drawPositionX, rowStartY - 10, multipliedScoreString )
end