1-- Breakout for Golly
2-- Author: Chris Rowett (crowett@gmail.com), November 2016
3-- Use F12 to save a screenshot
4
5local build     = 84
6local g         = golly()
7-- require "gplus.strict"
8local gp        = require "gplus"
9local split     = gp.split
10local op        = require "oplus"
11local ov        = g.overlay
12local ovt       = g.ovtable
13local rand      = math.random
14local maketext  = op.maketext
15local pastetext = op.pastetext
16
17math.randomseed(os.time())  -- init seed for math.random
18
19-- text alignment
20local text = {
21    alignleft   = 0,
22    aligncenter = 1,
23    alignright  = 2,
24    aligntop    = 3,
25    alignbottom = 4,
26    fontscale   = 1
27}
28
29-- overlay width and height
30local wd, ht   = g.getview(g.getlayer())
31local minwd    = 400
32local minht    = 400
33local edgegapl = 0
34local edgegapr = 0
35
36-- background settings
37local bgclip = "bg"
38
39-- shadow settings
40local shadow = {
41    x     = -wd // 100,
42    y     = ht // 100,
43    alpha = 128,      -- default alpha for shadows
44    delta = 8,        -- brick fade rate when hit
45    txtx  = -2,
46    txty  = 2,
47}
48shadow.color     = {"rgba", 0, 0, 0, shadow.alpha}
49shadow.fadecolor = {"rgba", 0, 0, 0, shadow.alpha}
50
51-- colors as tables
52local colors = {
53    white     = {"rgba", 255, 255, 255, 255},
54    black     = {"rgba", 0, 0, 0, 255},
55    red       = {"rgba", 255, 0, 0, 255},
56    green     = {"rgba", 0, 255, 0, 255},
57    blue      = {"rgba", 0, 0, 255, 255},
58    cyan      = {"rgba", 0, 255, 255, 255},
59    magenta   = {"rgba", 255, 0, 255, 255},
60    yellow    = {"rgba", 255, 255, 0, 255}
61}
62
63-- brick settings
64local brick = {
65    numrows     = 6,
66    numcols     = 20,
67    rows        = {},
68    wd,
69    ht          = ht // 40,
70    maxoffsety  = 20,
71    offsety     = 0,
72    startoffset = 0,
73    movedown    = 0,
74    movesteps   = 24,
75    cols  = {
76        [1] = colors.red,
77        [2] = colors.yellow,
78        [3] = colors.magenta,
79        [4] = colors.green,
80        [5] = colors.cyan,
81        [6] = colors.blue
82    },
83    bricksleft  = 0,
84    totalbricks = 0,
85    x           = 0,
86    y           = 0,
87    fading      = {},
88    fadecols    = {}
89}
90brick.wd = wd // brick.numcols
91brick.bricksleft = brick.numrows * brick.numcols
92brick.totalbricks = brick.bricksleft
93
94-- bat settings
95local bat = {
96    x      = 0,
97    y      = 0,
98    wd     = wd // 10,
99    ht     = brick.ht,
100    lastx  = 0,
101    fade   = 128
102}
103
104-- ball settings
105local ball = {
106    size      = wd // 80,
107    x         = 0,
108    y         = 0,
109    numsteps  = 80
110}
111
112-- particle settings
113local particle = {
114    particles       = {},
115    brickparticles  = brick.wd * brick.ht // 10,
116    ballparticles   = 1,
117    ballpartchance  = 0.25,
118    wallparticles   = 20,
119    batparticles    = 20,
120    highparticles   = 4,
121    comboparticles  = 4,
122    bonusparticles  = 4,
123    lostparticles   = 1024,
124    bonusparticlesg = 6,
125    bonusparticlesy = 3
126}
127
128-- points settings
129local points = {}
130
131-- game settings
132local game = {
133    level      = 1,
134    newball    = true,
135    pause      = false,
136    hiscore    = 0,
137    score      = 0,
138    combo      = 1,
139    combomult  = 1,
140    combofact  = 1.04,
141    comboraw   = 0,
142    comboextra = 0,
143    gamecombo  = 1,
144    maxcombo   = 2,
145    balls      = 3,
146    newhigh    = false,
147    newcombo   = false,
148    newbonus   = false,
149    offoverlay = false,
150    finished   = false,
151    again      = true
152}
153
154-- timing settings
155local timing = {
156    times         = {},
157    timenum       = 1,
158    numtimes      = 8,
159    framemult     = 1,
160    framecap      = 100,
161    sixtyhz       = 16.7
162}
163
164-- game options
165local options = {
166    brickscore    = 1,
167    showtiming    = 0,
168    showparticles = 1,
169    autopause     = 0,
170    autostart     = 0,
171    showmouse     = 1,
172    showshadows   = 1,
173    confirmquit   = 0,
174    showoptions   = false,
175    confirming    = false,
176    comboscore    = 1,
177    soundvol      = 1,
178    musicvol      = 1,
179    fullscreen    = 0
180}
181
182-- settings are saved in this file
183local settingsfile = g.getdir("data").."breakout.ini"
184
185-- notifications
186local notification = {
187    duration = 300,
188    trans    = 20,
189    current  = 0,
190    message  = ""
191}
192
193-- bonus level
194local bonus = {
195    bricks = {
196        805203,
197        699732,
198        830290,
199        698705,
200        698705,
201        805158
202    },
203    level    = false,
204    interval = 3,
205    time     = 60,
206    current  = 0,
207    green    = 10,
208    yellow   = 20,
209    best     = 0
210}
211
212-- key highlight color and names
213local keys       = "keys"
214local buttons    = "buttons"
215local keycolor   = {"rgba", 32, 32, 32, 255}
216local mousecolor = {"rgba", 48, 0, 0, 255}
217local keynames   = {
218     [keys]      = { "Esc", "Tab", "Enter" },
219     [buttons]   = { "Click", "Right Click", "Mouse" }
220}
221local keycols    = {
222    [keys]       = keycolor,
223    [buttons]    = mousecolor
224}
225
226-- static messages and clip names
227local optcol = {"rgba", 192, 192, 192, 255}
228local messages = {
229    ["gameover"]   = { text = "Game Over", size = 30, color = colors.red },
230    ["newball"]    = { text = "Click or Enter to launch ball", size = 10, color = colors.white },
231    ["control"]    = { text = "Mouse to move bat", size = 10, color = colors.white },
232    ["askquit"]    = { text = "Quit Game?", size = 15, color = colors.yellow },
233    ["pause"]      = { text = "Paused", size = 15, color = colors.yellow },
234    ["askleft"]    = { text = "Click or Enter to Confirm", size = 10, color = colors.white },
235    ["askright"]   = { text = "Right Click to Cancel", size = 10, color = colors.white },
236    ["resume"]     = { text = "Click or Enter to continue", size = 10, color = colors.white },
237    ["focus"]      = { text = "Move mouse onto game window to continue", size = 10, color = colors.white },
238    ["manfocus"]   = { text = "Move mouse onto game window and", size = 10, color = colors.white },
239    ["quitgame"]   = { text = "Right Click to quit game", size = 10, color = colors.white },
240    ["option"]     = { text = "Tab for Game Settings", size = 10, color = colors.white },
241    ["restart"]    = { text = "Click or Enter to start again", size = 10, color = colors.white },
242    ["quit"]       = { text = "Right Click or Esc to exit", size = 10, color = colors.white },
243    ["continue"]   = { text = "Click or Enter for next level", size = 10, color = colors.white },
244    ["newhigh"]    = { text = "New High Score!", size = 10, color = colors.green },
245    ["newcombo"]   = { text = "New Best Combo!", size = 10, color = colors.green },
246    ["newbonus"]   = { text = "New Best Bonus!", size = 10, color = colors.green },
247    ["close"]      = { text = "Click or Tab to close Game Settings", size = 10, color = colors.white },
248    ["autopause"]  = { text = "Autopause", size = 10, color = optcol },
249    ["brickscore"] = { text = "Brick Score", size = 10, color = optcol },
250    ["comboscore"] = { text = "Combo Score", size = 10, color = optcol },
251    ["shadows"]    = { text = "Shadows", size = 10, color = optcol },
252    ["mouse"]      = { text = "Mouse Pointer", size = 10, color = optcol },
253    ["particles"]  = { text = "Particles", size = 10, color = optcol },
254    ["confirm"]    = { text = "Confirm Quit", size = 10, color = optcol },
255    ["autostart"]  = { text = "Autostart", size = 10, color = optcol },
256    ["timing"]     = { text = "Timing", size = 10, color = optcol },
257    ["fullscreen"] = { text = "Fullscreen", size = 10, color = optcol },
258    ["function"]   = { text = "Function", size = 10, color = colors.white },
259    ["on"]         = { text = "On", size = 10, color = colors.green },
260    ["off"]        = { text = "Off", size = 10, color = colors.red },
261    ["state"]      = { text = "State", size = 10, color = colors.white },
262    ["key"]        = { text = "Key", size = 10, color = colors.white },
263    ["a"]          = { text = "A", size = 10, color = optcol },
264    ["b"]          = { text = "B", size = 10, color = optcol },
265    ["c"]          = { text = "C", size = 10, color = optcol },
266    ["d"]          = { text = "D", size = 10, color = optcol },
267    ["m"]          = { text = "M", size = 10, color = optcol },
268    ["p"]          = { text = "P", size = 10, color = optcol },
269    ["q"]          = { text = "Q", size = 10, color = optcol },
270    ["s"]          = { text = "S", size = 10, color = optcol },
271    ["t"]          = { text = "T", size = 10, color = optcol },
272    ["f11"]        = { text = "F11", size = 10, color = optcol },
273    ["-"]          = { text = "-", size = 10, color = optcol },
274    ["="]          = { text = "+", size = 10, color = optcol },
275    ["["]          = { text = "[", size = 10, color = optcol },
276    ["]"]          = { text = "]", size = 10, color = optcol },
277    ["sound"]      = { text = "Sound Volume", size = 10, color = optcol },
278    ["music"]      = { text = "Music Volume", size = 10, color = optcol },
279    ["fxvol"]      = { text = "100%", size = 10, color = colors.green },
280    ["musicvol"]   = { text = "100%", size = 10, color = colors.green },
281    ["level"]      = { text = "Level ", size = 15, color = colors.white },
282    ["bonus"]      = { text = "Bonus Level", size = 15, color = colors.white },
283    ["bcomplete"]  = { text = "Bonus Level Complete", size = 15, color = colors.white },
284    ["remain"]     = { text = "Bricks left", size = 10, color = colors.green },
285    ["time"]       = { text = "Time", size = 15, color = colors.green },
286    ["left"]       = { text = "3 balls left", size = 15, color = colors.yellow },
287    ["score"]      = { text = "Score", size = 10, color = colors.white },
288    ["high"]       = { text = "High Score", size = 10, color = colors.white },
289    ["balls"]      = { text = "Balls", size = 10, color = colors.white },
290    ["combo"]      = { text = "Combo", size = 10, color = colors.white },
291    ["notify"]     = { text = "Notify", size = 7, color = colors.white },
292    ["ms"]         = { text = "1 ms", size = 7, color = colors.white },
293    ["complete"]   = { text = "Level Complete", size = 20, color = colors.green },
294    ["awarded"]    = { text = "No Bonus", size = 15, color = colors.red }
295}
296
297-- music
298local music = {
299    currenttrack = "",
300    fade         = 1,
301    faderate     = -0.01,
302    gameovertime = 1000 * 64,
303    folder       = "oplus/sounds/breakout/"
304}
305
306--------------------------------------------------------------------------------
307
308local function showcursor()
309    if options.showmouse == 0 then
310        ov("cursor hidden")
311    else
312        ov("cursor arrow")
313    end
314end
315
316--------------------------------------------------------------------------------
317
318local function setfullscreen()
319    g.setoption("fullscreen", options.fullscreen)
320    showcursor()
321end
322
323--------------------------------------------------------------------------------
324
325local function readsettings()
326    local f = io.open(settingsfile, "r")
327    if f then
328        game.hiscore          = tonumber(f:read("*l")) or 0
329        options.fullscreen    = tonumber(f:read("*l")) or 0
330        options.showtiming    = tonumber(f:read("*l")) or 0
331        options.showparticles = tonumber(f:read("*l")) or 1
332        options.autopause     = tonumber(f:read("*l")) or 0
333        options.autostart     = tonumber(f:read("*l")) or 0
334        options.showmouse     = tonumber(f:read("*l")) or 1
335        options.showshadows   = tonumber(f:read("*l")) or 1
336        game.maxcombo         = tonumber(f:read("*l")) or 2
337        options.brickscore    = tonumber(f:read("*l")) or 1
338        options.confirmquit   = tonumber(f:read("*l")) or 1
339        bonus.best            = tonumber(f:read("*l")) or 0
340        options.comboscore    = tonumber(f:read("*l")) or 1
341        options.soundvol      = tonumber(f:read("*l")) or 100
342        options.musicvol      = tonumber(f:read("*l")) or 70
343        f:close()
344
345        if options.soundvol == 1 then
346           options.soundvol = 100
347        end
348        if options.musicvol == 1 then
349           options.musicvol = 100
350        end
351    end
352end
353
354--------------------------------------------------------------------------------
355
356local function writesettings()
357    local f = io.open(settingsfile, "w")
358    if f then
359        f:write(tostring(game.hiscore).."\n")
360        f:write(tostring(options.fullscreen).."\n")
361        f:write(tostring(options.showtiming).."\n")
362        f:write(tostring(options.showparticles).."\n")
363        f:write(tostring(options.autopause).."\n")
364        f:write(tostring(options.autostart).."\n")
365        f:write(tostring(options.showmouse).."\n")
366        f:write(tostring(options.showshadows).."\n")
367        f:write(tostring(game.maxcombo).."\n")
368        f:write(tostring(options.brickscore).."\n")
369        f:write(tostring(options.confirmquit).."\n")
370        f:write(tostring(bonus.best).."\n")
371        f:write(tostring(options.comboscore).."\n")
372        f:write(tostring(options.soundvol).."\n")
373        f:write(tostring(options.musicvol).."\n")
374        f:close()
375    end
376end
377
378--------------------------------------------------------------------------------
379
380local function updatemessage(name, s, color)
381    -- lookup the message
382    local message = messages[name]
383    if color ~= nil then
384        message.color = color
385    end
386    -- get the font size for this message
387    ov("font "..((message.size * text.fontscale) // 1 | 0))
388    -- create the text message clips
389    message.text = s
390    local textcol = message.color
391    if type(textcol) == "table" then textcol = table.concat(textcol, " ") end
392    local w, h = maketext(message.text, name, textcol, shadow.txtx, shadow.txty)
393    -- save the clip width and height
394    message.width  = w
395    message.height = h
396end
397
398--------------------------------------------------------------------------------
399
400local function createstatictext()
401    -- create each static text clip
402    for clipname, message in pairs(messages) do
403        updatemessage(clipname, message.text)
404    end
405end
406
407--------------------------------------------------------------------------------
408
409local function soundstate(sound)
410    return ov("sound state "..music.folder..sound..".ogg")
411end
412
413--------------------------------------------------------------------------------
414
415local function setvolume(sound, vol)
416    ov("sound volume "..music.folder..sound..".ogg "..vol)
417end
418
419--------------------------------------------------------------------------------
420
421local function setchannelvolume(channel, vol)
422    if vol == 0 then
423        updatemessage(channel.."vol", "Off", colors.red)
424    else
425        updatemessage(channel.."vol", vol.."%", colors.green)
426    end
427    -- update the music volume immediately since it may be playing
428    if channel == "music" then
429        if music.currenttrack ~= "" then
430            setvolume(music.currenttrack, (options.musicvol * music.fade / 100))
431        end
432    end
433end
434
435--------------------------------------------------------------------------------
436
437local function stopallsound()
438    ov("sound stop")
439end
440
441--------------------------------------------------------------------------------
442
443local function stopmusic()
444    if music.currenttrack ~= "" then
445        ov("sound stop "..music.folder..music.currenttrack..".ogg")
446    end
447end
448--------------------------------------------------------------------------------
449
450local function playsound(name, loop)
451    if options.soundvol > 0 then
452        loop = loop or false
453        if loop then
454            ov("sound loop "..music.folder..name..".ogg "..(options.soundvol / 100))
455        else
456            ov("sound play "..music.folder..name..".ogg "..(options.soundvol / 100))
457        end
458    end
459end
460
461--------------------------------------------------------------------------------
462
463local function playmusic(name, loop)
464    loop = loop or false
465    stopmusic()
466    music.currenttrack = name
467    if loop then
468        ov("sound loop "..music.folder..name..".ogg "..(options.musicvol / 100))
469    else
470        ov("sound play "..music.folder..name..".ogg "..(options.musicvol / 100))
471    end
472end
473
474--------------------------------------------------------------------------------
475
476local function updatelevel(value)
477    game.level = value
478    updatemessage("level", "Level "..game.level)
479    updatemessage("complete", "Level "..game.level.." complete!")
480end
481
482--------------------------------------------------------------------------------
483
484local function updatescore(value)
485    game.score = value
486    updatemessage("score", "Score "..game.score)
487end
488
489--------------------------------------------------------------------------------
490
491local function updatehighscore(value)
492    local color = colors.white
493    if game.newhigh then
494        color = colors.green
495    end
496    game.hiscore = value
497    updatemessage("high", "High Score "..game.hiscore, color)
498end
499
500--------------------------------------------------------------------------------
501
502local function updateballs(value)
503    game.balls = value
504    local color = colors.white
505    if game.balls == 1 then
506        updatemessage("left", "Last ball!", colors.red)
507        color = colors.red
508    elseif game.balls == 2 then
509        color = colors.yellow
510        updatemessage("left", game.balls.." balls left", colors.yellow)
511    else
512        updatemessage("left", game.balls.." balls left", colors.green)
513    end
514    updatemessage("balls", "Balls "..game.balls, color)
515end
516
517--------------------------------------------------------------------------------
518
519local function updatecombo(value)
520    local color = colors.white
521    game.combo = value
522    if game.combo == game.maxcombo then
523        color = colors.green
524    end
525    updatemessage("combo", "Combo x"..game.combo - 1, color)
526end
527
528--------------------------------------------------------------------------------
529
530local function highlightkey(textstr, x, y, w, h, token, color)
531    local t1, t2 = textstr:find(token)
532    if t1 ~= nil then
533        local charw = w / textstr:len()
534        local x1 = x + (t1 - 1) * charw
535        local oldblend = ov("blend 0")
536        local oldrgba = ovt(colors.black)
537        ovt{"fill", x1 + shadow.txtx, y + shadow.txty, (charw * (t2 - t1 + 1) + 5), h - 4}
538        ovt(color)
539        ovt{"fill", x1, y, (charw * (t2 - t1 + 1) + 5), h - 4}
540        ovt(oldrgba)
541        ov("blend "..oldblend)
542    end
543end
544
545--------------------------------------------------------------------------------
546
547local function drawtextclip(name, x, y, xalign, yalign, highlight)
548    xalign    = xalign or text.alignleft
549    yalign    = yalign or text.aligntop
550    highlight = highlight or false
551    -- lookup the message
552    local message = messages[name]
553    local w = message.width
554    local h = message.height
555    local xoffset = 0
556    local yoffset = 0
557    if xalign == text.aligncenter then
558        xoffset = (wd - w) / 2
559    elseif xalign == text.alignright then
560        xoffset = wd - w - edgegapr - edgegapl
561    end
562    if yalign == text.aligncenter then
563        yoffset = (ht - h) / 2
564    elseif yalign == text.alignbottom then
565        yoffset = ht - h
566    end
567    -- check for highlight text
568    if highlight == true then
569        for colkey, list in pairs(keynames) do
570            for _, textstr in pairs(list) do
571                highlightkey(message.text, x + xoffset + edgegapl, y + yoffset, w, h, textstr, keycols[colkey])
572            end
573        end
574    end
575    -- draw the text clip
576    pastetext(x + xoffset + edgegapl, y + yoffset, nil, name)
577    -- return clip dimensions
578    return w, h
579end
580
581--------------------------------------------------------------------------------
582
583local function updatenotification()
584    -- check if there is a message to display
585    if notification.message ~= "" then
586        local y
587        -- check if notification finished
588        if notification.current >= notification.duration then
589            notification.message = ""
590            notification.current = 0
591        else
592            -- check which phase
593            if notification.current < notification.trans then
594                -- appear
595                y = (notification.current / notification.trans) * (8 * text.fontscale)
596            elseif notification.current > notification.duration - notification.trans then
597                -- disappear
598                y = (notification.duration - notification.current) / notification.trans * (8 * text.fontscale)
599            else
600                -- hold
601                y = (8 * text.fontscale)
602            end
603            -- draw notification
604            drawtextclip("notify", 4, (8 * text.fontscale) - y, text.alignleft, text.alignbottom)
605            notification.current = notification.current + timing.framemult
606        end
607    end
608end
609
610--------------------------------------------------------------------------------
611
612local function notify(message, flag)
613    flag = flag or -1
614    notification.message = message
615    if flag == 0 then
616        notification.message = message.." off"
617    elseif flag == 1 then
618        notification.message = message.." on"
619    end
620    -- create the text clip
621    updatemessage("notify", notification.message)
622    if notification.current ~= 0 then
623        notification.current = notification.trans
624    end
625end
626
627--------------------------------------------------------------------------------
628
629local function initparticles()
630    particle.particles = {}
631end
632
633--------------------------------------------------------------------------------
634
635local function createparticles(x, y, areawd, areaht, howmany, color)
636    color = color or colors.white
637    -- find the first free slot
638    local i = 1
639    while i <= #particle.particles and particle.particles[i].alpha > 0 do
640        i = i + 1
641    end
642    for _ = 1, howmany do
643        local item = { alpha = 255, x = x - rand(areawd // 1), y = y + rand(areaht // 1), dx = rand() - 0.5, dy = rand() - 0.5, color = color }
644        particle.particles[i] = item
645        i = i + 1
646        -- find the next free slot
647        while i <= #particle.particles and particle.particles[i].alpha > 0 do
648            i = i + 1
649        end
650    end
651end
652
653--------------------------------------------------------------------------------
654
655local function drawparticles()
656    ov("blend 2")
657    local xy = {"fill"}
658    local m = 2
659    local color = {"rgba", 0, 0, 0, -1}
660
661    for i = 1, #particle.particles do
662        local item  = particle.particles[i]
663        local scale = ht / 1000
664        -- check if particle is still alive
665        local alpha = item.alpha
666        if alpha > 0 then
667            if options.showparticles ~= 0 then
668                local itemcol = item.color
669                -- check if this item has a different color than the current batch
670                if alpha ~= color[5] or itemcol[2] ~= color[2] or itemcol[3] ~= color[3] or itemcol[4] ~= color[4] then
671                    -- draw the current batch
672                    if m > 2 then
673                        ovt(xy)
674                        m = 2
675                        xy = {"fill"}
676                    end
677                    -- start a new batch with the new color
678                    color = {"rgba", itemcol[2], itemcol[3], itemcol[4], alpha}
679                    ovt(color)
680                end
681                -- add the item to the batch to draw
682                xy[m] = item.x
683                xy[m + 1] = item.y
684                xy[m + 2] = 2
685                xy[m + 3] = 2
686                m = m + 4
687            end
688            -- fade item
689            alpha = alpha - 4 * timing.framemult
690            if alpha < 0 then
691                alpha = 0
692            end
693            item.alpha = alpha
694            item.x = item.x + item.dx * timing.framemult * scale
695            item.y = item.y + item.dy * timing.framemult * scale
696            item.dx = item.dx * 0.99
697            if item.dy < 0 then
698                item.dy = item.dy + 0.05
699            else
700                item.dy = (item.dy + 0.02) * 1.02
701            end
702        end
703    end
704    -- draw any unfinished batch
705    if m > 2 then
706        ovt(xy)
707    end
708end
709
710--------------------------------------------------------------------------------
711
712local function initpoints()
713    points = {}
714end
715
716--------------------------------------------------------------------------------
717
718local function createpoints(x, y, value)
719    -- find the first free slot
720    local i = 1
721    while i <= #points and points[i].duration > 0 do
722        i = i + 1
723    end
724    -- create the clip
725    ov("font "..((7 * text.fontscale) // 1 | 0).." mono")
726    local w, h = maketext(value, "point"..i, op.white, shadow.txtx, shadow.txty)
727
728    -- save the item
729    local item = { duration = 60, x = (x + brick.wd / 2 - w / 2), y = (y + brick.ht / 2 - h / 2) }
730    points[i] = item
731end
732
733--------------------------------------------------------------------------------
734
735local function drawpoints()
736    ov("blend 2")
737    for i = 1, #points do
738        local item = points[i]
739        -- check if item is still alive
740        if item.duration > 0 then
741            if options.brickscore == 1 then
742                local y = item.y + brick.offsety * brick.ht
743                if item.duration < 8 then
744                    -- fade out by replacing clip alpha
745                    ov("target point"..i)
746                    ov("replace *# *# *# *#-16")
747                    ov("target")
748                end
749                -- draw item
750                ovt{"paste", item.x, y, "point"..i}
751            end
752            item.duration = item.duration - 1 * timing.framemult
753            if item.duration < 0 then
754                item.duration = 0
755            end
756        end
757    end
758end
759
760--------------------------------------------------------------------------------
761
762local function createfadingbrick(x, y)
763    local fading = brick.fading
764    local i = 1
765    while i <= #fading and fading[i].alpha > 0 do
766        i = i + 1
767    end
768    fading[i] = { alpha = shadow.alpha, x = x, y = y }
769end
770
771--------------------------------------------------------------------------------
772
773local function drawfadingbricks(pass, xoff, yoff)
774    ov("blend 2")
775    -- get the list of fading bricks
776    local fading = brick.fading
777    local fadecols = brick.fadecols
778    local fadecolor = shadow.fadecolor
779    for i = 1, #fading do
780        local alpha = fading[i].alpha
781        -- find each shadow that hasn't fully faded
782        if alpha > 0 then
783            -- increase transparency if this is the brick (not shadow)
784            if pass == 2 then
785                alpha = alpha - shadow.delta
786                if alpha < 0 then alpha = 0 end
787                fading[i].alpha = alpha
788            end
789            if alpha > 0 then
790                -- draw brick or shadow
791                local fy = fading[i].y
792                local y = (fy + brick.offsety) * brick.ht
793                local x = (fading[i].x - 1) * brick.wd + edgegapl
794                -- pick the color depending on drawing brick or shadow
795                if (pass == 1) then
796                    fadecolor[5] = alpha
797                    ovt(fadecolor)
798                else
799                    fadecols[fy][5] = alpha * 2
800                    ovt(fadecols[fy])
801                end
802                ovt{"fill", (x + xoff), (y + yoff), (brick.wd - 1), (brick.ht - 1)}
803            end
804        end
805    end
806end
807
808--------------------------------------------------------------------------------
809
810local function createbackground()
811    -- create background clip
812    ov("create "..wd.." "..ht.." "..bgclip)
813    ov("target "..bgclip)
814    -- create background gradient
815    ov("blend 0")
816    local c
817    local level = 96
818    local cmd = {"line", 0, 0, wd - 1, 0}
819    for y = 0, ht - 1 do
820        c = (y * level) // ht
821        ovt{"rgba", 0, (level - c), c, 255}
822        cmd[3] = y
823        cmd[5] = y
824        ovt(cmd)
825    end
826
827    -- add borders if required
828    if edgegapl > 0 then
829        ovt(colors.black)
830        ovt{"fill", 0, 0, edgegapl, (ht - 1)}
831    end
832    if edgegapr > 0 then
833        ovt(colors.black)
834        ovt{"fill", (wd - edgegapr), 0 , edgegapr, (ht - 1)}
835    end
836
837    -- reset target
838    ov("target")
839end
840
841--------------------------------------------------------------------------------
842
843local function drawbackground()
844    ov("blend 0")
845    ovt{"paste", 0, 0, bgclip}
846end
847
848--------------------------------------------------------------------------------
849
850local function drawbricks()
851    local xoff = shadow.x
852    local yoff = shadow.y
853    local bwd = brick.wd
854    local bht = brick.ht
855    local bwdm1 = bwd - 1
856    local bhtm1 = bht - 1
857    local ncols = brick.numcols
858    local startpass = 1
859    -- check whether to draw shadows
860    if options.showshadows == 0 then
861        startpass = 2
862        xoff = 0
863        yoff = 0
864    end
865    for pass = startpass, 2 do
866        drawfadingbricks(pass, xoff, yoff)
867        if pass == 1 then
868            ov("blend 2")
869            ovt(shadow.color)
870        else
871            ov("blend 0")
872        end
873        for y = 1, brick.numrows do
874            local bricks = brick.rows[y]
875            if pass == 2 then
876                ovt(brick.cols[y])
877            end
878            local by = ((y + brick.offsety) * bht) // 1 + yoff
879            local bx = edgegapl + xoff
880            for x = 1, ncols do
881                if bricks[x] then
882                    ovt{"fill", bx, by, bwdm1, bhtm1}
883                end
884                bx = bx + bwd
885            end
886        end
887        xoff = 0
888        yoff = 0
889    end
890end
891
892--------------------------------------------------------------------------------
893
894local function drawball()
895    local oldwidth = ov("lineoption width "..(ball.size // 2))
896    ov("blend 2")
897    if options.showshadows == 1 then
898        ovt(shadow.color)
899        ov("ellipse "..(((ball.x - ball.size // 2) + shadow.x) // 1 | 0).." "..(((ball.y - ball.size // 2) + shadow.y) // 1 | 0).." "..ball.size.." "..ball.size)
900    end
901    ovt(colors.white)
902    ov("ellipse "..((ball.x - ball.size // 2) // 1 | 0).." "..((ball.y - ball.size // 2) // 1 | 0).." "..ball.size.." "..ball.size)
903    if rand() < particle.ballpartchance * timing.framemult then
904        createparticles(ball.x + ball.size // 2, ball.y - ball.size // 2, ball.size, ball.size, particle.ballparticles)
905    end
906    ov("lineoption width "..oldwidth)
907end
908
909--------------------------------------------------------------------------------
910
911local function drawbat(alpha)
912    alpha = alpha or 256
913    ov("blend 2")
914    if options.showshadows == 1 then
915        shadow.fadecolor[5] = alpha // 2
916        ovt(shadow.fadecolor)
917        ovt{"fill", bat.x + shadow.x, bat.y + shadow.y, bat.wd, bat.ht}
918    end
919    -- draw the bat in red if mouse is off the overlay
920    if game.offoverlay then
921        ovt(colors.red)
922    else
923        if alpha == 256 then alpha = 255 end
924        ovt{"rgba", 192, 192, 192, alpha}
925    end
926    ovt{"fill", bat.x, bat.y, bat.wd, bat.ht}
927end
928
929--------------------------------------------------------------------------------
930
931local function initbricks()
932    brick.rows        = {}
933    brick.wd          = wd // brick.numcols
934    brick.ht          = ht // 40
935    brick.bricksleft  = 0
936    brick.totalbricks = 0
937    brick.offsety     = game.level + 1
938    if brick.offsety > brick.maxoffsety then
939        brick.offsety = brick.maxoffsety
940    end
941    brick.movedown    = 0
942
943    -- check for bonus level
944    bonus.current = bonus.time
945    bonus.level   = false
946    if (game.level % bonus.interval) == 0 then
947       bonus.level = true
948    end
949
950    -- distribute any gap left and right
951    local edgegap = wd - brick.wd * brick.numcols
952    edgegapl = edgegap // 2
953    edgegapr = edgegap - edgegapl
954
955    -- clear the fading bricks (used when brick hit)
956    brick.fading = {}
957
958    -- create the brick fade colors from the brick colors
959    for i = 1, #brick.cols do
960        local col = brick.cols[i]
961        brick.fadecols[i] = {"rgba", col[2], col[3], col[4], col[5]}
962    end
963
964    -- set the required bricks alive
965    local match
966    for y = 1, brick.numrows do
967        local bricks = {}
968        if bonus.level then
969            local bonusrow = bonus.bricks[y]
970            match = 1
971            for x = brick.numcols - 1, 0, -1 do
972                if (bonusrow & match) == match then
973                    bricks[x + 1] = true
974                    brick.bricksleft = brick.bricksleft + 1
975                else
976                    bricks[x + 1] = false
977                end
978                match = match + match
979            end
980        else
981            for x = 1, brick.numcols do
982                bricks[x] = true
983                brick.bricksleft = brick.bricksleft + 1
984            end
985        end
986        brick.rows[y] = bricks
987    end
988    brick.totalbricks = brick.bricksleft
989end
990
991--------------------------------------------------------------------------------
992
993local function initbat()
994    bat.wd = wd // 10
995    bat.x  = (wd - bat.wd) // 2
996    bat.ht = brick.ht
997    bat.y  = ht - bat.ht * 4
998end
999
1000--------------------------------------------------------------------------------
1001
1002local function initball()
1003    ball.size = wd // 80
1004    ball.x    = (wd - ball.size) / 2
1005    ball.y    = bat.y - ball.size
1006end
1007
1008--------------------------------------------------------------------------------
1009
1010local function initshadow()
1011    shadow.x      = -wd // 100
1012    shadow.y      = ht // 100
1013    shadow.color = {"rgba", 0, 0, 0, shadow.alpha}
1014end
1015
1016--------------------------------------------------------------------------------
1017
1018local function togglefullscreen()
1019    options.fullscreen = 1 - options.fullscreen
1020    writesettings()
1021    setfullscreen()
1022    initpoints()
1023end
1024
1025--------------------------------------------------------------------------------
1026
1027local function toggletiming()
1028    options.showtiming = 1 - options.showtiming
1029    writesettings()
1030    notify("Timing", options.showtiming)
1031end
1032
1033--------------------------------------------------------------------------------
1034
1035local function toggleparticles()
1036    options.showparticles = 1 - options.showparticles
1037    writesettings()
1038    notify("Particles", options.showparticles)
1039end
1040
1041--------------------------------------------------------------------------------
1042
1043local function toggleautopause()
1044    options.autopause = 1 - options.autopause
1045    writesettings()
1046    notify("Autopause", options.autopause)
1047end
1048
1049--------------------------------------------------------------------------------
1050
1051local function toggleautostart()
1052    options.autostart = 1 - options.autostart
1053    writesettings()
1054    notify("Autostart", options.autostart)
1055end
1056
1057--------------------------------------------------------------------------------
1058
1059local function togglemouse()
1060    options.showmouse = 1 - options.showmouse
1061    writesettings()
1062    showcursor()
1063    notify("Mouse pointer", options.showmouse)
1064end
1065
1066--------------------------------------------------------------------------------
1067
1068local function toggleshadowdisplay()
1069    options.showshadows = 1 - options.showshadows
1070    writesettings()
1071    notify("Shadows", options.showshadows)
1072end
1073
1074--------------------------------------------------------------------------------
1075
1076local function togglebrickscore()
1077    options.brickscore = 1 - options.brickscore
1078    writesettings()
1079    notify("Brick Score", options.brickscore)
1080end
1081
1082--------------------------------------------------------------------------------
1083
1084local function savescreenshot()
1085    local filename = g.getdir("data").."shot"..os.date("%y%m%d%H%M%S", os.time())..".png"
1086    ov("save 0 0 0 0 "..filename)
1087    notify("Saved screenshot "..filename)
1088end
1089
1090--------------------------------------------------------------------------------
1091
1092local function toggleconfirmquit()
1093    options.confirmquit = 1 - options.confirmquit
1094    writesettings()
1095    notify("Confirm Quit", options.confirmquit)
1096end
1097
1098--------------------------------------------------------------------------------
1099
1100local function togglecomboscore()
1101    options.comboscore = 1 - options.comboscore
1102    writesettings()
1103    notify("Combo Score", options.comboscore)
1104end
1105
1106--------------------------------------------------------------------------------
1107
1108local function adjustsoundvol(delta)
1109    options.soundvol = options.soundvol + delta
1110    if options.soundvol > 100 then
1111        options.soundvol = 100
1112    end
1113    if options.soundvol < 0 then
1114        options.soundvol = 0
1115    end
1116    writesettings()
1117    notify("Sound Volume "..options.soundvol.."%")
1118    setchannelvolume("fx", options.soundvol)
1119end
1120
1121--------------------------------------------------------------------------------
1122
1123local function adjustmusicvol(delta)
1124    options.musicvol = options.musicvol + delta
1125    if options.musicvol > 100 then
1126        options.musicvol = 100
1127    end
1128    if options.musicvol < 0 then
1129        options.musicvol = 0
1130    end
1131    writesettings()
1132    notify("Music Volume "..options.musicvol.."%")
1133    setchannelvolume("music", options.musicvol)
1134end
1135
1136--------------------------------------------------------------------------------
1137
1138local function processstandardkeys(event)
1139    if event:find("^key") then
1140        if event == "key f11 none" then
1141            -- toggle fullscreen
1142            togglefullscreen()
1143        elseif event == "key a none" then
1144            -- toggle autopause when mouse moves off overlay
1145            toggleautopause()
1146        elseif event == "key b none" then
1147            -- toggle brick score display
1148            togglebrickscore()
1149        elseif event == "key c none" then
1150            -- toggle combo score display
1151            togglecomboscore()
1152        elseif event == "key d none" then
1153            -- toggle shadow display
1154            toggleshadowdisplay()
1155        elseif event == "key = none" then
1156            -- increase sound volume
1157            adjustsoundvol(10)
1158        elseif event == "key - none" then
1159            -- decrease sound volume
1160            adjustsoundvol(-10)
1161        elseif event == "key m none" then
1162            -- toggle mouse cursor display when not fullscreen
1163            togglemouse()
1164        elseif event == "key p none" then
1165            -- toggle particle display
1166            toggleparticles()
1167        elseif event == "key q none" then
1168            -- toggle confirm quit
1169            toggleconfirmquit()
1170        elseif event == "key s none" then
1171            -- toggle autostart when mouse moves onto overlay
1172            toggleautostart()
1173        elseif event == "key t none" then
1174            -- toggle timing display
1175            toggletiming()
1176        elseif event == "key [ none" then
1177            -- decrease music volume
1178            adjustmusicvol(-10)
1179        elseif event == "key ] none" then
1180            -- increase music volume
1181            adjustmusicvol(10)
1182        elseif event == "key tab none" then
1183            -- show options
1184            options.showoptions = not options.showoptions
1185        elseif event == "key f12 none" then
1186            -- save screenshot
1187            savescreenshot()
1188        end
1189    end
1190end
1191
1192--------------------------------------------------------------------------------
1193
1194local function pausegame(paused)
1195    if paused ~= game.pause then
1196        game.pause = paused
1197        if music.currenttrack ~= "" then
1198            if game.pause then
1199                music.fade = 1
1200                music.faderate  = -0.05
1201            else
1202                music.faderate = 0.05
1203                ov("sound resume")
1204            end
1205        end
1206    end
1207end
1208
1209--------------------------------------------------------------------------------
1210
1211local function updatemusic()
1212    if music.currenttrack ~= "" then
1213        if game.pause then
1214            if music.fade > 0 then
1215                music.fade = music.fade + music.faderate
1216                if music.fade <= 0 then
1217                    music.fade = 0
1218                    ov("sound pause")
1219                else
1220                    setvolume(music.currenttrack, (options.musicvol * music.fade / 100))
1221                end
1222            end
1223        else
1224            if music.fade < 1 then
1225                music.fade = music.fade + music.faderate
1226                if music.fade > 1 then
1227                    music.fade = 1
1228                end
1229                setvolume(music.currenttrack, (options.musicvol * music.fade / 100))
1230            end
1231        end
1232    end
1233end
1234
1235--------------------------------------------------------------------------------
1236
1237local function processinput()
1238    -- check for click, enter or return
1239    local event = g.getevent()
1240    if #event > 0 then
1241        local button, _
1242        button = ""
1243        if event:find("^oclick") then
1244            _, _, _, button, _= split(event)
1245        end
1246        -- right click quits game
1247        if button == "right" then
1248            if options.confirmquit == 0 then
1249                updateballs(0)
1250                options.showoptions = false
1251            else
1252                options.confirming = not options.confirming
1253            end
1254        elseif button == "left" or event == "key return none" or event == "key space none" then
1255            -- left click, enter or space starts game, toggles pause or dismisses settings
1256            if options.confirming then
1257                updateballs(0)
1258                options.showoptions = false
1259                options.confirming = false
1260            elseif options.showoptions then
1261                options.showoptions = false
1262            elseif game.newball then
1263                if bonus.level then
1264                    playmusic("bonusloop", true)
1265                else
1266                    playmusic("gameloop", true)
1267                end
1268                game.newball = false
1269                pausegame(false)
1270            else
1271                -- do not unpause if off overlay
1272                if not (game.pause and game.offoverlay and options.autopause ~= 0) then
1273                    pausegame(not game.pause)
1274                end
1275            end
1276        else
1277            processstandardkeys(event)
1278        end
1279    end
1280end
1281
1282--------------------------------------------------------------------------------
1283
1284local function processendinput()
1285    local event = g.getevent()
1286    if #event > 0 then
1287        local button, _
1288        button = ""
1289        if event:find("^oclick") then
1290            _, _, _, button, _ = split(event)
1291        end
1292        -- right click quits application
1293        if button == "right" then
1294            -- quit application
1295            game.again          = false
1296            game.finished       = true
1297            options.showoptions = false
1298        elseif button == "left" or event == "key return none" or event == "key space none" then
1299            -- left click, enter or space restarts game or dismisses settings
1300            if options.showoptions then
1301                options.showoptions = false
1302            else
1303                game.finished = true
1304            end
1305        else
1306            processstandardkeys(event)
1307        end
1308    end
1309end
1310
1311--------------------------------------------------------------------------------
1312
1313local function resizegame(newwd, newht)
1314    -- check minimum size
1315    if newwd < minwd then
1316        newwd = minwd
1317    end
1318    if newht < minht then
1319        newht = minht
1320    end
1321    local xscale = newwd / wd
1322    local yscale = newht / ht
1323
1324    wd = newwd
1325    ht = newht
1326    text.fontscale = wd / minwd
1327    if (ht / minht) < text.fontscale then
1328        text.fontscale = ht / minht
1329    end
1330
1331    -- scale bat, ball and bricks
1332    brick.wd                = wd // brick.numcols
1333    brick.ht                = ht // 40
1334    particle.brickparticles = brick.wd * brick.ht // 10
1335    bat.wd                  = wd // 10
1336    bat.ht                  = brick.ht
1337    ball.size               = wd // 80
1338    local edgegap           = wd - brick.wd * brick.numcols
1339    edgegapl                = edgegap // 2
1340    edgegapr                = edgegap - edgegapl
1341
1342    -- reposition the bat and ball
1343    bat.x  = bat.x * xscale
1344    bat.y  = ht - bat.ht * 4
1345    ball.x = ball.x * xscale
1346    ball.y = ball.y * yscale
1347
1348    -- reposition particles
1349    for i = 1, #particle.particles do
1350        local item = particle.particles[i]
1351        item.x = item.x * xscale
1352        item.y = item.y * yscale
1353    end
1354
1355    -- resize shadow
1356    initshadow()
1357
1358    -- recreate background
1359    createbackground()
1360
1361    -- recreate static text
1362    createstatictext()
1363
1364    -- resize overlay
1365    ov("resize "..wd.." "..ht)
1366end
1367
1368--------------------------------------------------------------------------------
1369
1370local function drawscoreline()
1371    ov("blend 2")
1372    drawtextclip("score", 4, 4, text.alignleft)
1373    drawtextclip("balls", -4, 4, text.alignright)
1374    drawtextclip("high", 0, 4, text.aligncenter)
1375    if game.combo > 2 then
1376        drawtextclip("combo", 0, 0, text.aligncenter, text.alignbottom)
1377    end
1378    if not game.newball and not game.pause and not options.showoptions and bonus.level and bonus.current >= 0 then
1379        local color = colors.green
1380        if bonus.current < 10 then
1381            color = colors.red
1382        elseif bonus.current < 20 then
1383            color = colors.yellow
1384        end
1385        updatemessage("time", "Time "..string.format("%.1f", bonus.current), color)
1386        drawtextclip("time", 0, ht / 2, text.aligncenter)
1387        color = colors.green
1388        if brick.bricksleft > bonus.yellow then
1389            color = colors.red
1390        elseif brick.bricksleft > bonus.green then
1391            color = colors.yellow
1392        end
1393        updatemessage("remain", "Bricks left "..brick.bricksleft, color)
1394        drawtextclip("remain", 0, ht / 2 + 25 * text.fontscale, text.aligncenter)
1395    end
1396end
1397
1398--------------------------------------------------------------------------------
1399
1400local function drawgameover()
1401    ov("blend 2")
1402    if game.newhigh then
1403        local highscorew = drawtextclip("newhigh", 0, ht / 2 + 96 * text.fontscale, text.aligncenter)
1404        createparticles(edgegapl + (wd / 2 + highscorew / 2), (ht / 2 + 96 * text.fontscale), highscorew, 1, particle.highparticles)
1405    end
1406    updatecombo(game.gamecombo)
1407    if game.newcombo then
1408        local combow = drawtextclip("newcombo", 0, ht / 2 + 118 * text.fontscale, text.aligncenter)
1409        createparticles(edgegapl + (wd / 2 + combow / 2), (ht / 2 + 118 * text.fontscale), combow, 1, particle.comboparticles)
1410    end
1411    drawtextclip("gameover", 0, ht / 2 - 30 * text.fontscale, text.aligncenter)
1412    drawtextclip("restart", 0, ht / 2 + 30 * text.fontscale, text.aligncenter, nil, true)
1413    drawtextclip("quit", 0, ht / 2 + 52 * text.fontscale, text.aligncenter, nil, true)
1414    drawtextclip("option", 0, ht / 2 + 74 * text.fontscale, text.aligncenter, nil, true)
1415    if bat.fade > 0 then
1416        bat.fade = bat.fade - shadow.delta
1417        if bat.fade < 0 then bat.fade = 0 end
1418        drawbat(bat.fade)
1419    end
1420end
1421
1422--------------------------------------------------------------------------------
1423
1424local function drawlevelcomplete()
1425    ov("blend 2")
1426    drawtextclip("complete", 0, ht / 2 - 30 * text.fontscale, text.aligncenter)
1427    drawtextclip("continue", 0, ht / 2 + 30 * text.fontscale, text.aligncenter, nil, true)
1428    drawtextclip("quitgame", 0, ht / 2 + 52 * text.fontscale, text.aligncenter, nil, true)
1429    drawtextclip("option", 0, ht / 2 + 74 * text.fontscale, text.aligncenter, nil, true)
1430end
1431
1432--------------------------------------------------------------------------------
1433
1434local function drawbonuscomplete()
1435    ov("blend 2")
1436    drawtextclip("bcomplete", 0, ht / 2 - 30 * text.fontscale, text.aligncenter)
1437
1438    local w = drawtextclip("awarded", 0, ht / 2, text.aligncenter)
1439    if brick.bricksleft <= bonus.green then
1440        createparticles(edgegapl + (wd / 2 + w / 2), ht / 2, w, 1, particle.bonusparticlesg)
1441    elseif brick.bricksleft <= bonus.yellow then
1442        createparticles(edgegapl + (wd / 2 + w / 2), ht / 2, w, 1, particle.bonusparticlesy)
1443    end
1444    drawtextclip("continue", 0, ht / 2 + 30 * text.fontscale, text.aligncenter, nil, true)
1445    drawtextclip("quitgame", 0, ht / 2 + 52 * text.fontscale, text.aligncenter, nil, true)
1446    drawtextclip("option", 0, ht / 2 + 74 * text.fontscale, text.aligncenter, nil, true)
1447    if game.newbonus then
1448        local bonusw = drawtextclip("newbonus", 0, ht / 2 + 96 * text.fontscale, text.aligncenter)
1449        createparticles(edgegapl + (wd / 2 + bonusw / 2), (ht / 2 + 96 * text.fontscale), bonusw, 1, particle.bonusparticles)
1450    end
1451end
1452
1453--------------------------------------------------------------------------------
1454
1455local function drawconfirm()
1456    ov("blend 2")
1457    drawtextclip("askquit", 0, ht / 2 - 15 * text.fontscale, text.aligncenter, nil, true)
1458    drawtextclip("askleft", 0, ht / 2 + 22 * text.fontscale, text.aligncenter, nil, true)
1459    drawtextclip("askright", 0, ht / 2 + 44 * text.fontscale, text.aligncenter, nil, true)
1460end
1461
1462--------------------------------------------------------------------------------
1463
1464local function drawpause()
1465    ov("blend 2")
1466    drawtextclip("pause", 0, ht / 2 - 15 * text.fontscale, text.aligncenter)
1467    if game.offoverlay and options.autopause ~= 0 then
1468        if options.autostart ~= 0 then
1469            drawtextclip("focus", 0, ht / 2 + 22 * text.fontscale, text.aligncenter)
1470        else
1471            drawtextclip("manfocus", 0, ht / 2 + 22 * text.fontscale, text.aligncenter)
1472            drawtextclip("resume", 0, ht / 2 + 44 * text.fontscale, text.aligncenter, nil, true)
1473            drawtextclip("quitgame", 0, ht / 2 + 66 * text.fontscale, text.aligncenter, nil, true)
1474            drawtextclip("option", 0, ht / 2 + 88 * text.fontscale, text.aligncenter, nil, true)
1475        end
1476    else
1477        drawtextclip("resume", 0, ht / 2 + 22 * text.fontscale, text.aligncenter, nil, true)
1478        drawtextclip("quitgame", 0, ht / 2 + 44 * text.fontscale, text.aligncenter, nil, true)
1479        drawtextclip("option", 0, ht / 2 + 66 * text.fontscale, text.aligncenter, nil, true)
1480    end
1481end
1482
1483--------------------------------------------------------------------------------
1484
1485local function drawnewball()
1486    ov("blend 2")
1487    drawtextclip("newball", 0, ht / 2 + 22 * text.fontscale, text.aligncenter, nil, true)
1488    drawtextclip("control", 0, ht / 2 + 44 * text.fontscale, text.aligncenter, nil, true)
1489    drawtextclip("quitgame", 0, ht / 2 + 66 * text.fontscale, text.aligncenter, nil, true)
1490    drawtextclip("option", 0,  ht / 2 + 88 * text.fontscale, text.aligncenter, nil, true)
1491    drawtextclip("left", 0, ht / 2 - 15 * text.fontscale, text.aligncenter)
1492    if bonus.level then
1493        drawtextclip("bonus", 0, ht / 2 - 52 * text.fontscale, text.aligncenter)
1494    else
1495        drawtextclip("level", 0, ht / 2 - 52 * text.fontscale, text.aligncenter)
1496    end
1497end
1498
1499--------------------------------------------------------------------------------
1500
1501local function drawtiming(t)
1502    timing.times[timing.timenum] = t
1503    timing.timenum = timing.timenum + 1
1504    if timing.timenum > timing.numtimes then
1505        timing.timenum = 1
1506    end
1507    local average = 0
1508    for i = 1, #timing.times do
1509        average = average + timing.times[i]
1510    end
1511    average = average / #timing.times
1512    local oldblend = ov("blend 2")
1513    updatemessage("ms", string.format("%.1fms", average))
1514    drawtextclip("ms", -4, 0, text.alignright, text.alignbottom)
1515    ov("blend "..oldblend)
1516end
1517
1518--------------------------------------------------------------------------------
1519
1520local function drawoption(key, setting, state, leftx, h, y)
1521    if key ~= "key" then
1522        ovt(colors.black)
1523        ovt{"fill", (leftx + edgegapl + shadow.txtx), (y + shadow.txty), (messages[key].width + 3), (messages[key].height - 4)}
1524        ovt(keycolor)
1525        ovt{"fill", (leftx + edgegapl), y, (messages[key].width + 3), (messages[key].height - 4)}
1526    end
1527    drawtextclip(key, leftx, y, text.alignleft)
1528    drawtextclip(setting, 0, y, text.aligncenter)
1529    drawtextclip(state, -leftx, y, text.alignright)
1530    return y + h
1531end
1532
1533--------------------------------------------------------------------------------
1534
1535local function drawpercent(downkey, upkey, setting, valname, leftx, h, y)
1536    local width  = messages[downkey].width
1537    local height = messages[downkey].height
1538    ovt(colors.black)
1539    ovt{"fill", (leftx + edgegapl + shadow.txtx), (y + shadow.txty), (width + 3), (height - 4)}
1540    ovt{"fill", (leftx + width * 2 + edgegapl + shadow.txtx), (y + shadow.txty), (width + 3), (height - 4)}
1541    ovt(keycolor)
1542    ovt{"fill", (leftx + edgegapl), y, (width + 3), (height - 4)}
1543    ovt{"fill", (leftx + width * 2 + edgegapl), y, (width + 3), (height - 4)}
1544    drawtextclip(downkey, leftx, y, text.alignleft)
1545    drawtextclip(upkey, leftx + width * 2, y, text.alignleft)
1546    drawtextclip(setting, 0, y, text.aligncenter)
1547    drawtextclip(valname, -leftx, y, text.alignright)
1548    return y + h
1549end
1550
1551--------------------------------------------------------------------------------
1552
1553local function drawoptions()
1554    local leftx = wd // 6
1555    local state = {[0] = "off", [1] = "on"}
1556
1557    -- draw header
1558    ov("blend 2")
1559    local h = messages["key"].height
1560    local y = (ht - 14 * h) // 2
1561    y = drawoption("key", "function", "state", leftx, h, y)
1562
1563    -- draw options
1564    y = drawoption("a", "autopause", state[options.autopause], leftx, h, y)
1565    y = drawoption("b", "brickscore", state[options.brickscore], leftx, h, y)
1566    y = drawoption("c", "comboscore", state[options.comboscore], leftx, h, y)
1567    y = drawoption("d", "shadows", state[options.showshadows], leftx, h, y)
1568    y = drawoption("m", "mouse", state[options.showmouse], leftx, h, y)
1569    y = drawoption("p", "particles", state[options.showparticles], leftx, h, y)
1570    y = drawoption("q", "confirm", state[options.confirmquit], leftx, h, y)
1571    y = drawoption("s", "autostart", state[options.autostart], leftx, h, y)
1572    y = drawoption("t", "timing", state[options.showtiming], leftx, h, y)
1573    y = drawoption("f11", "fullscreen", state[options.fullscreen], leftx, h, y)
1574    y = drawpercent("-", "=", "sound", "fxvol", leftx, h, y)
1575    y = drawpercent("[", "]", "music", "musicvol", leftx, h, y)
1576
1577    -- draw close options
1578    drawtextclip("close", 0, y + h, text.aligncenter, nil, true)
1579    if game.balls == 0 then
1580        drawtextclip("quit", 0, y + h * 2.5, text.aligncenter, nil, true)
1581    else
1582        drawtextclip("quitgame", 0, y + h * 2.5, text.aligncenter, nil, true)
1583    end
1584end
1585
1586--------------------------------------------------------------------------------
1587
1588local function checkforsystemevent()
1589    -- check for resize
1590    local newwd, newht = g.getview(g.getlayer())
1591    if newwd ~= wd or newht ~= ht then
1592        resizegame(newwd, newht)
1593    end
1594    -- check for overlay hidden
1595    if not game.pause then
1596        if g.getoption("showoverlay") == 0 then
1597            pausegame(true)
1598        end
1599    end
1600end
1601
1602--------------------------------------------------------------------------------
1603
1604local function updatebatposition()
1605    local mousepos = ov("xy")
1606    if mousepos ~= "" then
1607        local mousex, _ = split(mousepos)
1608        if mousex ~= bat.lastx then
1609            bat.lastx = mousex
1610            bat.x = tonumber(mousex) - bat.wd / 2
1611            if bat.x < edgegapl then
1612                bat.x = edgegapl
1613            elseif bat.x > wd - edgegapr - bat.wd then
1614                bat.x = wd - edgegapr - bat.wd
1615            end
1616        end
1617        -- check if mouse was off overlay
1618        if game.offoverlay then
1619            -- check if paused
1620            if game.pause and options.autostart ~= 0 and options.autopause ~= 0 then
1621                pausegame(false)
1622            end
1623        end
1624        game.offoverlay = false
1625    else
1626        -- mouse off overlay
1627        game.offoverlay = true
1628        -- check for autopause if in game
1629        if options.autopause ~= 0 and not game.newball then
1630            pausegame(true)
1631        end
1632    end
1633end
1634
1635--------------------------------------------------------------------------------
1636
1637local function clearbonusbricks()
1638    local bricks
1639    local clearparticles = particle.brickparticles / 4
1640    for y = 1, brick.numrows do
1641        bricks = brick.rows[y]
1642        for x = 1, brick.numcols do
1643            if bricks[x] then
1644                bricks[x] = false
1645                createparticles(x * brick.wd + edgegapl, ((y + brick.offsety) * brick.ht), brick.wd, brick.ht, clearparticles, brick.cols[y])
1646                createfadingbrick(x, y)
1647            end
1648        end
1649    end
1650end
1651
1652--------------------------------------------------------------------------------
1653
1654local function computebonus()
1655    local bonusscore = 0
1656    if brick.bricksleft <= bonus.green then
1657        bonusscore = (brick.totalbricks - brick.bricksleft) * (100 + (game.level - 1) * 10)
1658        updatemessage("awarded", "Bricks left "..brick.bricksleft.." = "..bonusscore, colors.green)
1659    elseif brick.bricksleft <= bonus.yellow then
1660        bonusscore = (brick.totalbricks - brick.bricksleft) * (50 + (game.level - 1) * 10)
1661        updatemessage("awarded", "Bricks left "..brick.bricksleft.." = "..bonusscore, colors.yellow)
1662    else
1663        updatemessage("awarded", "Bricks left "..brick.bricksleft.." = ".."No Bonus", colors.red)
1664    end
1665    playmusic("levelcompleteloop", true)
1666    updatescore(game.score + bonusscore)
1667    if game.score > game.hiscore then
1668        game.newhigh = true
1669        updatehighscore(game.score)
1670    end
1671    if bonusscore > bonus.best then
1672        game.newbonus = true
1673        bonus.best = bonusscore
1674    end
1675end
1676
1677--------------------------------------------------------------------------------
1678
1679local function resetcombo()
1680    updatecombo(1)
1681    game.combomult  = 1
1682    game.comboraw   = 0
1683    game.comboextra = 0
1684end
1685
1686--------------------------------------------------------------------------------
1687
1688local function playexit()
1689    local box = {}
1690    local n = 1
1691    local tx, ty
1692    local tilesize = wd // 32
1693    ov("blend 0")
1694
1695    -- copy the screen into tiles
1696    for y = 0, ht, tilesize do
1697        for x = 0, wd, tilesize do
1698            tx = x + rand(0, wd // 8) - wd / 16
1699            ty = ht + rand(0, ht // 2)
1700            local entry = {}
1701            entry[1] = x
1702            entry[2] = y
1703            entry[3] = tx
1704            entry[4] = ty
1705            box[n] = entry
1706            ov("copy "..x.." "..y.." "..tilesize.." "..tilesize.." sprite"..n)
1707            n = n + 1
1708        end
1709    end
1710    local fadestart = music.fade
1711    ovt(colors.black)
1712    for i = 0, 100 do
1713        local t = g.millisecs()
1714        local a = i / 100
1715        local x, y
1716        ovt{"fill"}
1717        -- update each tile
1718        for j = 1, #box do
1719            x = box[j][1]
1720            y = box[j][2]
1721            tx = box[j][3]
1722            ty = box[j][4]
1723            ovt{"paste", (x * (1 - a) + tx * a), (y * (1 - a) + ty * a), "sprite"..j}
1724        end
1725        -- draw timing if on
1726        if options.showtiming == 1 then
1727            drawtiming(g.millisecs() - t)
1728        end
1729        -- fade the music
1730        music.fade = fadestart * ((100 - i) / 100)
1731        setvolume(music.currenttrack, (options.musicvol * music.fade / 100))
1732        ov("update")
1733        while g.millisecs() - t < 15 do end
1734    end
1735    -- delete tiles
1736    for i = 1, #box do
1737        ov("delete sprite"..i)
1738    end
1739end
1740
1741--------------------------------------------------------------------------------
1742
1743local function breakout()
1744    -- set font
1745    local oldfont   = ov("font 16 mono")
1746    local oldbg     = ov("textoption background 0 0 0 0")
1747    local oldblend  = ov("blend 0")
1748    local oldcursor = ov("cursor arrow")
1749
1750    -- read saved settings
1751    readsettings()
1752    setfullscreen()
1753
1754    -- set sound and music volume
1755    setchannelvolume("fx", options.soundvol)
1756    setchannelvolume("music", options.musicvol)
1757
1758    -- play games until finished
1759    game.balls    = 3
1760    game.score    = 0
1761    game.level    = 1
1762    game.again    = true
1763    game.newhigh  = false
1764    game.newcombo = false
1765    game.newbonus = false
1766
1767    -- initialise the bat and ball
1768    initbat()
1769    initball()
1770
1771    -- welcome message
1772    notify("Golly Breakout build "..build)
1773
1774    -- create static text
1775    createstatictext()
1776
1777    -- initialize dynamic text
1778    updatescore(game.score)
1779    updatehighscore(game.hiscore)
1780    updateballs(game.balls)
1781    updatelevel(game.level)
1782
1783    -- main loop
1784    while game.again do
1785        music.fade = 1
1786
1787        -- initialize the bricks
1788        initbricks()
1789
1790        -- create the background
1791        createbackground()
1792
1793        -- intiialize the bat
1794        local bathits = 0
1795        local maxhits = 7
1796        bat.lastx     = -1
1797
1798        -- initialize the ball
1799        local balldx   = 0.5
1800        local balldy   = -1
1801        local maxspeed = 2.2 + (game.level - 1) * 0.1
1802        if maxspeed > 3 then
1803            maxspeed = 3
1804        end
1805        local speedinc  = 0.02
1806        local speeddef  = 1 + (game.level - 1) * speedinc * 4
1807        if speeddef > maxspeed then
1808            speeddef = maxspeed
1809        end
1810        local ballspeed = speeddef
1811        local speeddiv  = 3
1812
1813        -- initialize shadow
1814        initshadow()
1815
1816        -- initialize particles
1817        initparticles()
1818
1819        -- initialise points
1820        initpoints()
1821
1822        -- whether alive
1823        game.newball       = true
1824        options.confirming = false
1825        pausegame(false)
1826
1827        -- whether mouse off overlay
1828        game.offoverlay = false
1829
1830        -- reset combo
1831        resetcombo()
1832
1833        -- game loop
1834        playmusic("gamestart")
1835        while game.balls > 0 and brick.bricksleft > 0 and bonus.current > 0 do
1836            -- time frame
1837            local frametime = g.millisecs()
1838
1839            -- check for mouse click or key press
1840            processinput()
1841
1842            -- check if size of overlay has changed or overlay is hidden
1843            checkforsystemevent()
1844
1845            -- draw the background
1846            drawbackground()
1847
1848            -- process next game step unless paused or game finished due to user quit
1849            if not game.pause and not options.confirming and not options.showoptions and bonus.current > 0 and game.balls > 0 then
1850                -- check for new ball
1851                if not game.newball then
1852                    -- update ball position incrementally
1853                    local framesteps = (ball.numsteps * timing.framemult) // 1
1854                    local i = 1
1855                    while i <= framesteps and not game.newball do
1856                        i = i + 1
1857                        local stepx = ((balldx * ballspeed * ball.size) / speeddiv) / ball.numsteps
1858                        local stepy = ((balldy * ballspeed * ball.size) / speeddiv) / ball.numsteps
1859                        ball.x = ball.x + stepx
1860                        ball.y = ball.y + stepy
1861
1862                        -- check for ball hitting left or right boundary
1863                        if ball.x < ball.size / 2 + edgegapl or ball.x >= wd - edgegapr - ball.size / 2 then
1864                            createparticles(ball.x, ball.y, 1, 1, particle.wallparticles)
1865                            -- invert x direction
1866                            balldx = -balldx
1867                            ball.x  = ball.x - stepx
1868                            playsound("edge")
1869                        end
1870
1871                        -- check for ball hitting top boundary
1872                        if ball.y < ball.size / 2 then
1873                            createparticles(ball.x, (ball.y - ball.size / 2), 1, 1, particle.wallparticles)
1874                            -- ball hit top so speed up a little bit
1875                            balldy    = -balldy
1876                            ball.y     = ball.y - stepy
1877                            ballspeed = ballspeed + speedinc / 2
1878                            if ballspeed > maxspeed then
1879                                ballspeed = maxspeed
1880                            end
1881                            playsound("top")
1882
1883                        -- check for ball hitting bottom boundary
1884                        elseif ball.y >= ht then
1885                            -- check for bonus level
1886                            if bonus.level then
1887                                -- end bonus level
1888                                bonus.current = 0
1889                            else
1890                                -- ball lost!
1891                                updateballs(game.balls - 1)
1892                                balldy       = -1
1893                                balldx       = 0.5
1894                                ballspeed    = speeddef
1895                                game.newball = true
1896                                -- reset combo
1897                                if game.comboextra - game.comboraw > 0 then
1898                                    if options.comboscore == 1 then
1899                                        notify("Combo x "..(game.combo - 1).." Score "..game.comboextra - game.comboraw.." (+"..(((100 * game.comboextra // game.comboraw) - 100) // 1 | 0).."%)")
1900                                    end
1901                                end
1902                                resetcombo()
1903                                playmusic("lostball")
1904                            end
1905                            -- exit loop
1906                            i = framesteps + 1
1907
1908                        -- check for ball hitting bat
1909                        elseif ball.y >= bat.y and ball.y <= bat.y + bat.ht - 1 and ball.x >= bat.x and ball.x < bat.x + bat.wd then
1910                            -- set dx from where ball hit bat
1911                            balldx = (3 * (ball.x - bat.x) / bat.wd) - 1.5
1912                            if balldx >= 0 and balldx < 0.1 then
1913                                balldx = 0.1
1914                            end
1915                            if balldx > -0.1 and balldx <= 0 then
1916                                balldx = -0.1
1917                            end
1918                            balldy  = -balldy
1919                            ball.y  = bat.y
1920                            bathits = bathits + 1
1921                            -- move the bricks down after a number of bat hits
1922                            if bathits == maxhits then
1923                                bathits = 0
1924                                if brick.offsety < brick.maxoffsety then
1925                                    brick.movedown = brick.movesteps
1926                                    brick.startoffset = brick.offsety
1927                                end
1928                            end
1929                            createparticles(ball.x, ball.y - ball.size / 2, 1, 1, particle.batparticles)
1930                            -- reset combo
1931                            if game.comboextra - game.comboraw > 0 then
1932                                if options.comboscore == 1 then
1933                                    notify("Combo x "..(game.combo - 1).." Score "..game.comboextra - game.comboraw.." (+"..(((100 * game.comboextra / game.comboraw) - 100) // 1 | 0).."%)")
1934                                end
1935                            end
1936                            resetcombo()
1937                            playsound("bat")
1938                        end
1939
1940                        -- check for ball hitting brick
1941                        brick.y = (ball.y - (brick.offsety * brick.ht)) // brick.ht
1942                        if brick.y >= 1 and brick.y <= brick.numrows then
1943                            brick.x = ((ball.x - edgegapl) // brick.wd) + 1
1944                            if brick.rows[brick.y][brick.x] then
1945                                -- hit a brick!
1946                                brick.rows[brick.y][brick.x] = false
1947                                -- adjust score
1948                                local pointval = ((game.level + 9) * (brick.numrows - brick.y + 1) * game.combomult) // 1 | 0
1949                                local rawpoints = ((game.level + 9) * (brick.numrows - brick.y + 1)) // 1 | 0
1950                                if game.combo > 1 then
1951                                    game.comboraw = game.comboraw + rawpoints
1952                                    game.comboextra = game.comboextra + pointval
1953                                end
1954                                updatescore(game.score + pointval)
1955                                if game.score > game.hiscore then
1956                                    game.newhigh = true
1957                                    updatehighscore(game.score)
1958                                end
1959                                createpoints((brick.x - 1) * brick.wd + edgegapl, brick.y * brick.ht, pointval)
1960                                createfadingbrick(brick.x, brick.y)
1961                                -- increment combo
1962                                game.combomult = game.combomult * game.combofact
1963                                if game.combo + 1 > game.maxcombo then
1964                                    game.maxcombo = game.combo + 1
1965                                    game.newcombo = true
1966                                end
1967                                updatecombo(game.combo + 1)
1968                                if game.combo > game.gamecombo then
1969                                    game.gamecombo = game.combo
1970                                end
1971                                -- work out which axis to invert
1972                                local lastbricky = ((ball.y - stepy) - (brick.offsety * brick.ht)) // brick.ht
1973                                if lastbricky == brick.y then
1974                                    balldx = -balldx
1975                                else
1976                                    balldy = -balldy
1977                                end
1978                                -- speed the ball up
1979                                ballspeed = ballspeed + speedinc
1980                                if ballspeed > maxspeed then
1981                                    ballspeed = maxspeed
1982                                end
1983                                -- create particles
1984                                createparticles(brick.x * brick.wd + edgegapl, ((brick.y + brick.offsety) * brick.ht), brick.wd, brick.ht, particle.brickparticles, brick.cols[brick.y])
1985                                -- one less brick
1986                                brick.bricksleft = brick.bricksleft - 1
1987                                playsound("brick"..(brick.y | 0))
1988                            end
1989                        end
1990                    end
1991                end
1992            end
1993
1994            -- update brick position
1995            if brick.movedown > 0 then
1996                brick.movedown = brick.movedown - 1
1997                brick.offsety = brick.offsety + (1 / brick.movesteps)
1998                if brick.movedown <= 0 then
1999                    brick.movedown = 0
2000                    brick.offsety = brick.startoffset + 1
2001                end
2002            end
2003
2004            -- update bat position
2005            updatebatposition()
2006
2007            -- if new ball then set ball to sit on bat
2008            if game.newball then
2009                ball.x = bat.x + bat.wd / 2
2010                ball.y = bat.y - ball.size
2011            end
2012
2013            -- draw the particles
2014            drawparticles()
2015
2016            -- draw the bricks
2017            drawbricks()
2018
2019            -- draw the points
2020            drawpoints()
2021
2022            -- draw the ball
2023            if game.balls > 0 then
2024                drawball()
2025            end
2026
2027            -- draw the bat
2028            drawbat()
2029
2030            -- draw the score, high score and lives
2031            drawscoreline()
2032
2033            -- check for text overlay
2034            if options.confirming then
2035                drawconfirm()
2036            elseif options.showoptions then
2037                drawoptions()
2038            elseif game.pause then
2039                drawpause()
2040            elseif game.newball and game.balls > 0 then
2041                drawnewball()
2042            end
2043
2044            -- update music volume (used for pause fade/resume)
2045            updatemusic()
2046
2047            -- draw timing if on
2048            if options.showtiming == 1 then
2049                drawtiming(g.millisecs() - frametime)
2050            end
2051
2052            -- update notification
2053            updatenotification()
2054
2055            -- update the display
2056            ov("update")
2057
2058            -- pause until frame time reached
2059            while g.millisecs() - frametime < 16 do end
2060
2061            -- check what the actual frame time was and scale speed accordingly
2062            timing.framemult = 1
2063            local finaltime = g.millisecs() - frametime
2064            if finaltime > timing.sixtyhz then
2065                -- cap to maximum frame time in case external event took control for a long time
2066                if finaltime > timing.framecap then
2067                    finaltime = timing.framecap
2068                end
2069                timing.framemult = finaltime / timing.sixtyhz
2070            end
2071
2072            -- update bonus time if on bonus level
2073            if bonus.level and not game.pause and not game.newball and not options.showoptions then
2074                bonus.current = bonus.current - (finaltime / 1000)
2075            end
2076        end
2077
2078        -- check for bonus level complete
2079        if bonus.level then
2080            computebonus()
2081            clearbonusbricks()
2082        else
2083            if brick.bricksleft == 0 then
2084                playmusic("levelcompleteloop", true)
2085            end
2086        end
2087
2088        -- save high score, max combo and best bonus
2089        if game.newhigh or game.newcombo or game.newbonus then
2090            writesettings()
2091        end
2092
2093        -- if game is over destroy bat and display best game combo
2094        if game.balls == 0 then
2095            -- destroy bat
2096            createparticles(bat.x + bat.wd, bat.y, bat.wd, bat.ht, particle.lostparticles)
2097            bat.fade = shadow.alpha
2098            notify("Best Combo x"..game.maxcombo - 1)
2099        end
2100
2101        -- loop until mouse button clicked or enter pressed
2102        bonus.current       = -1
2103        game.finished       = false
2104        local fading        = false
2105        local musicplaytime = g.millisecs()
2106
2107        while not game.finished do
2108            -- time frame
2109            local frametime = g.millisecs()
2110
2111            -- check if size of overlay has changed or overlay hidden
2112            checkforsystemevent()
2113
2114            -- draw background
2115            drawbackground()
2116
2117            -- update bat position if game is not over
2118            if game.balls > 0 then
2119                updatebatposition()
2120            end
2121
2122            -- draw particles
2123            drawparticles()
2124
2125            -- draw bricks
2126            drawbricks()
2127
2128            -- draw brick score
2129            drawpoints()
2130
2131            -- check why game finished
2132            if options.showoptions then
2133                drawoptions()
2134            else
2135                if game.balls == 0 then
2136                    -- game over
2137                    drawgameover()
2138                else
2139                    -- draw bat
2140                    drawbat()
2141                    if bonus.level then
2142                        -- end of bonus level
2143                        drawbonuscomplete()
2144                    else
2145                        -- level complete
2146                        drawlevelcomplete()
2147                    end
2148                end
2149            end
2150
2151            -- handle music during game over
2152            if game.balls == 0 then
2153                if fading then
2154                    -- fade music after one play through
2155                    if frametime - musicplaytime > music.gameovertime then
2156                        music.fade = music.fade + music.faderate
2157                        if music.fade < 0 then
2158                            music.fade = 0
2159                        end
2160                        setvolume("gamelostloop", (options.musicvol * music.fade / 100))
2161                    end
2162                else
2163                    -- wait for ball lost music to finish
2164                    if soundstate("lostball") ~= "playing" then
2165                        fading         = true
2166                        musicplaytime  = g.millisecs()
2167                        music.fade     = 1
2168                        music.faderate = -0.001
2169                        playmusic("gamelostloop", true)
2170                    else
2171                        updatemusic()
2172                    end
2173                end
2174            end
2175
2176            -- draw score line
2177            drawscoreline()
2178
2179            -- get key or mouse event
2180            processendinput()
2181
2182            -- draw timing if on
2183            if options.showtiming == 1 then
2184                drawtiming(g.millisecs() - frametime)
2185            end
2186
2187            -- update notification
2188            updatenotification()
2189
2190            -- update the display
2191            ov("update")
2192
2193            -- pause until frame time reached
2194            while g.millisecs() - frametime < 16 do end
2195
2196            -- check what the actual frame time was and scale speed accordingly
2197            timing.framemult = 1
2198            local finaltime = g.millisecs() - frametime
2199            if finaltime > timing.sixtyhz then
2200                -- cap to maximum frame time in case external event took control for a long time
2201                if finaltime > timing.framecap then
2202                    finaltime = timing.framecap
2203                end
2204                timing.framemult = finaltime / timing.sixtyhz
2205            end
2206        end
2207
2208        -- check why game finished
2209        if game.balls == 0 then
2210            -- reset
2211            updatescore(0)
2212            updateballs(3)
2213            updatelevel(1)
2214            game.newhigh   = false
2215            updatehighscore(game.hiscore)
2216            game.newcombo  = false
2217            game.gamecombo = 1
2218        else
2219            -- level complete
2220            updatelevel(game.level + 1)
2221        end
2222        game.newbonus = false
2223    end
2224
2225    -- exit animation
2226    playexit()
2227
2228    -- free clips and restore settings
2229    ov("delete "..bgclip)
2230    ov("blend "..oldblend)
2231    ov("font "..oldfont)
2232    ov("textoption background "..oldbg)
2233    ov("cursor "..oldcursor)
2234end
2235
2236--------------------------------------------------------------------------------
2237
2238local function main()
2239    -- get size of overlay
2240    wd, ht = g.getview(g.getlayer())
2241    if wd < minwd then
2242        wd = minwd
2243    end
2244    if ht < minht then
2245        ht = minht
2246    end
2247
2248    -- create overlay
2249    ov("create "..wd.." "..ht)
2250    text.fontscale = wd / minwd
2251    if (ht / minht) < text.fontscale then
2252        text.fontscale = ht / minht
2253    end
2254
2255    -- run breakout
2256    breakout()
2257end
2258
2259--------------------------------------------------------------------------------
2260
2261local oldoverlay = g.setoption("showoverlay", 1)
2262local oldbuttons = g.setoption("showbuttons", 0) -- disable translucent buttons
2263local oldscroll  = g.setoption("showscrollbars", 0)
2264local oldfs      = g.getoption("fullscreen")
2265
2266local status, err = xpcall(main, gp.trace)
2267if err then g.continue(err) end
2268-- the following code is always executed
2269
2270g.check(false)
2271stopallsound()
2272ov("delete")
2273g.setoption("showoverlay", oldoverlay)
2274g.setoption("showbuttons", oldbuttons)
2275g.setoption("showscrollbars", oldscroll)
2276g.setoption("fullscreen", oldfs)
2277