Lua Boomshine Update with circles + levels
added a few more levels and increased difficulty based on level
Rliimage allows us to use circles in boomshine
Optimized some of the code to exceed the speed of the original
Change-Id: I68eb74f8b68fcdc8a9d022cd172374744425f15d
diff --git a/apps/plugins/boomshine.lua b/apps/plugins/boomshine.lua
index 1729cbb..63ac6b1 100644
--- a/apps/plugins/boomshine.lua
+++ b/apps/plugins/boomshine.lua
@@ -11,6 +11,7 @@
See http://www.yvoschaap.com/chainrxn/ and http://www.k2xl.com/games/boomshine/
Copyright (C) 2009 by Maurus Cuelenaere
+ Copyright (C) 2018 William Wilgus -- Added circles, blit cursor, hard levels
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
@@ -21,14 +22,32 @@
KIND, either express or implied.
]]--
-
require "actions"
+-- [[only save the actions we are using]]
+actions_pla = {}
+for key, val in pairs(rb.actions) do
+ for _, v in ipairs({"PLA_", "TOUCHSCREEN"}) do
+ if string.find (key, v) then
+ actions_pla[key] = val
+ break
+ end
+ end
+end
+rb.actions = nil
+rb.contexts = nil
+-------------------------------------
+
+local _LCD = rb.lcd_framebuffer()
+local BSAND = 0x8
+local rocklib_image = getmetatable(rb.lcd_framebuffer())
+local _ellipse = rocklib_image.ellipse
local CYCLETIME = rb.HZ / 50
local HAS_TOUCHSCREEN = rb.action_get_touchscreen_press ~= nil
local DEFAULT_BALL_SIZE = rb.LCD_HEIGHT > rb.LCD_WIDTH and rb.LCD_WIDTH / 30
or rb.LCD_HEIGHT / 30
local MAX_BALL_SPEED = DEFAULT_BALL_SIZE / 2
+local DEC_BALL_SPEED = DEFAULT_BALL_SIZE / 8
local DEFAULT_FOREGROUND_COLOR = rb.lcd_get_foreground ~= nil
and rb.lcd_get_foreground()
or 0
@@ -46,7 +65,15 @@
{30, 45},
{37, 50},
{48, 55},
- {55, 60}
+ {59, 60},
+ {29, 30},
+ {24, 25},
+ {19, 20},
+ {14, 15},
+ {9, 10},
+ {10, 10},
+ {4, 5},
+ {5, 5}
}
local Ball = {
@@ -55,16 +82,44 @@
implosion = false
}
-function Ball:new(o)
+local function create_cursor(size)
+ local cursor
+ if not HAS_TOUCHSCREEN then
+ cursor = rb.new_image(size, size)
+ cursor:clear(0)
+ local sz2 = size / 2
+ local sz4 = size / 4
+
+ cursor:line(1, 1, sz4, 1, 1)
+ cursor:line(1, 1, 1, sz4, 1)
+
+ cursor:line(1, size, sz4, size, 1)
+ cursor:line(1, size, 1, size - sz4, 1)
+
+ cursor:line(size, size, size - sz4, size, 1)
+ cursor:line(size, size, size, size - sz4, 1)
+
+ cursor:line(size, 1, size - sz4, 1, 1)
+ cursor:line(size, 1, size, sz4, 1)
+
+ --crosshairs
+ cursor:line(sz2 - sz4, sz2, sz2 + sz4, sz2, 1)
+ cursor:line(sz2, sz2 - sz4, sz2, sz2 + sz4, 1)
+ end
+ return cursor
+end
+
+function Ball:new(o, level)
+ level = level or 1
if o == nil then
o = {
x = math.random(0, rb.LCD_WIDTH - self.size),
y = math.random(0, rb.LCD_HEIGHT - self.size),
color = random_color(),
- up_speed = Ball:generateSpeed(),
- right_speed = Ball:generateSpeed(),
+ up_speed = Ball:generateSpeed(level),
+ right_speed = Ball:generateSpeed(level),
explosion_size = math.random(2*self.size, 4*self.size),
- life_duration = math.random(rb.HZ, rb.HZ*5)
+ life_duration = math.random(rb.HZ / level, rb.HZ*5),
}
end
@@ -73,8 +128,10 @@
return o
end
-function Ball:generateSpeed()
- local speed = math.random(-MAX_BALL_SPEED, MAX_BALL_SPEED)
+function Ball:generateSpeed(level)
+ local ballspeed = MAX_BALL_SPEED - (DEC_BALL_SPEED * (level - 1))
+ if ballspeed < 2 then ballspeed = 2 end
+ local speed = math.random(-ballspeed, ballspeed)
if speed == 0 then
speed = 1 -- Make sure all balls move
end
@@ -82,39 +139,48 @@
return speed
end
+function Ball:draw_exploded()
+ --[[
+ --set_foreground(self.color)
+ --rb.lcd_fillrect(self.x, self.y, self.size, self.size)
+ ]]
+
+ _ellipse(_LCD, self.x, self.y,
+ self.x + self.size, self.y + self.size , self.color, nil, true)
+end
+
function Ball:draw()
--[[
- I know these aren't circles, but as there's no current circle
- implementation in Rockbox, rectangles will just do fine (drawing
- circles from within Lua is far too slow).
- ]]--
- set_foreground(self.color)
- rb.lcd_fillrect(self.x, self.y, self.size, self.size)
+ --set_foreground(self.color)
+ --rb.lcd_fillrect(self.x, self.y, self.size, self.size)
+ ]]
+
+ _ellipse(_LCD, self.x, self.y,
+ self.x + self.size, self.y + self.size , self.color, self.color, true)
+end
+
+function Ball:step_exploded()
+ if self.implosion and self.size > 0 then
+ self.size = self.size - 2
+ self.x = self.x + 1 -- We do this because we want to stay centered
+ self.y = self.y + 1
+ elseif self.size < self.explosion_size then
+ self.size = self.size + 2
+ self.x = self.x - 1 -- We do this for the same reasons as above
+ self.y = self.y - 1
+ end
end
function Ball:step()
- if self.exploded then
- if self.implosion and self.size > 0 then
- self.size = self.size - 2
- self.x = self.x + 1 -- We do this because we want to stay centered
- self.y = self.y + 1
- elseif self.size < self.explosion_size then
- self.size = self.size + 2
- self.x = self.x - 1 -- We do this for the same reasons as above
- self.y = self.y - 1
- end
- return
- end
-
self.x = self.x + self.right_speed
self.y = self.y + self.up_speed
- if (self.right_speed > 0 and self.x + self.size >= rb.LCD_WIDTH) or
- (self.right_speed < 0 and self.x <= 0) then
+ if (self.x <= 0 or self.x >= rb.LCD_WIDTH - self.size) then
self.right_speed = -self.right_speed
+ self.x = self.x + self.right_speed
end
- if (self.up_speed > 0 and self.y + self.size >= rb.LCD_HEIGHT) or
- (self.up_speed < 0 and self.y <= 0) then
+ if (self.y <= 0 or self.y >= rb.LCD_HEIGHT - self.size ) then
self.up_speed = -self.up_speed
+ self.y = self.y + self.up_speed
end
end
@@ -137,7 +203,8 @@
local Cursor = {
size = DEFAULT_BALL_SIZE*2,
x = rb.LCD_WIDTH/2,
- y = rb.LCD_HEIGHT/2
+ y = rb.LCD_HEIGHT/2,
+ image = create_cursor(DEFAULT_BALL_SIZE*2)
}
function Cursor:new()
@@ -145,18 +212,18 @@
end
function Cursor:do_action(action)
- if action == rb.actions.ACTION_TOUCHSCREEN and HAS_TOUCHSCREEN then
+ if action == actions_pla.ACTION_TOUCHSCREEN and HAS_TOUCHSCREEN then
_, self.x, self.y = rb.action_get_touchscreen_press()
return true
- elseif action == rb.actions.PLA_SELECT then
+ elseif action == actions_pla.PLA_SELECT then
return true
- elseif (action == rb.actions.PLA_RIGHT) then
+ elseif (action == actions_pla.PLA_RIGHT or action == actions_pla.PLA_RIGHT_REPEAT) then
self.x = self.x + self.size
- elseif (action == rb.actions.PLA_LEFT) then
+ elseif (action == actions_pla.PLA_LEFT or action == actions_pla.PLA_LEFT_REPEAT) then
self.x = self.x - self.size
- elseif (action == rb.actions.PLA_UP) then
+ elseif (action == actions_pla.PLA_UP or action == actions_pla.PLA_UP_REPEAT) then
self.y = self.y - self.size
- elseif (action == rb.actions.PLA_DOWN) then
+ elseif (action == actions_pla.PLA_DOWN or action == actions_pla.PLA_DOWN_REPEAT) then
self.y = self.y + self.size
end
@@ -176,25 +243,16 @@
end
function Cursor:draw()
- set_foreground(DEFAULT_FOREGROUND_COLOR)
- rb.lcd_hline(self.x - self.size/2, self.x - self.size/4, self.y - self.size/2)
- rb.lcd_hline(self.x + self.size/4, self.x + self.size/2, self.y - self.size/2)
- rb.lcd_hline(self.x - self.size/2, self.x - self.size/4, self.y + self.size/2)
- rb.lcd_hline(self.x + self.size/4, self.x + self.size/2, self.y + self.size/2)
- rb.lcd_vline(self.x - self.size/2, self.y - self.size/2, self.y - self.size/4)
- rb.lcd_vline(self.x - self.size/2, self.y + self.size/4, self.y + self.size/2)
- rb.lcd_vline(self.x + self.size/2, self.y - self.size/2, self.y - self.size/4)
- rb.lcd_vline(self.x + self.size/2, self.y + self.size/4, self.y + self.size/2)
-
- rb.lcd_hline(self.x - self.size/4, self.x + self.size/4, self.y)
- rb.lcd_vline(self.x, self.y - self.size/4, self.y + self.size/4)
+ rocklib_image.copy(_LCD, self.image, self.x - self.size/2, self.y - self.size/2,
+ _NIL, _NIL, _NIL, _NIL, true, BSAND, DEFAULT_FOREGROUND_COLOR)
end
function draw_positioned_string(bottom, right, str)
local _, w, h = rb.font_getstringsize(str, rb.FONT_UI)
-
- rb.lcd_putsxy((rb.LCD_WIDTH-w)*right, (rb.LCD_HEIGHT-h)*bottom, str)
+ local x = not right or (rb.LCD_WIDTH-w)*right - 1
+ local y = not bottom or (rb.LCD_HEIGHT-h)*bottom - 1
+ rb.lcd_putsxy(x, y, str)
end
function set_foreground(color)
@@ -213,31 +271,43 @@
function start_round(level, goal, nrBalls, total)
local player_added, score, exit, nrExpendedBalls = false, 0, false, 0
- local balls, explodedBalls = {}, {}
+ local Balls, explodedBalls = {}, {}
+ local ball_ct, ball_el
+ local tick, endtick
+ local action = 0
+ local hit_detected = false
local cursor = Cursor:new()
-- Initialize the balls
for _=1,nrBalls do
- table.insert(balls, Ball:new())
+ table.insert(Balls, Ball:new(nil, level))
+ end
+
+ local function draw_stats()
+ draw_positioned_string(0, 0, string.format("%d balls expended", nrExpendedBalls))
+ draw_positioned_string(0, 1, string.format("Level %d", level))
+ draw_positioned_string(1, 1, string.format("%d level points", score))
+ draw_positioned_string(1, 0, string.format("%d total points", total + score))
end
-- Make sure there are no unwanted touchscreen presses
rb.button_clear_queue()
+ set_foreground(DEFAULT_FOREGROUND_COLOR) -- color for text
+
while true do
- local endtick = rb.current_tick() + CYCLETIME
+ endtick = rb.current_tick() + CYCLETIME
-- Check if the round is over
- if #explodedBalls == 0 and player_added then
+ if player_added and #explodedBalls == 0 then
break
end
- -- Check for actions
- local action = rb.get_plugin_action(0)
- if action == rb.actions.PLA_EXIT or action == rb.actions.PLA_CANCEL then
+ if action == actions_pla.PLA_EXIT or action == actions_pla.PLA_CANCEL then
exit = true
break
end
+
if not player_added and cursor:do_action(action) then
local player = Ball:new({
x = cursor.x,
@@ -248,26 +318,28 @@
exploded = true,
death_time = rb.current_tick() + rb.HZ * 3
})
- table.insert(explodedBalls, player)
+ explodedBalls[1] = player
player_added = true
+ cursor = nil
end
- -- Check for hits
- for i, ball in ipairs(balls) do
+ -- Check for hits
+ for i, Ball in ipairs(Balls) do
for _, explodedBall in ipairs(explodedBalls) do
- if ball:checkHit(explodedBall) then
- score = score + 100*level
- nrExpendedBalls = nrExpendedBalls + 1
- table.insert(explodedBalls, ball)
- table.remove(balls, i)
+ if Ball:checkHit(explodedBall) then
+ explodedBalls[#explodedBalls + 1] = Ball
+ --table.remove(Balls, i)
+ Balls[i] = false
+ hit_detected = true
break
end
end
end
-- Check if we're dead yet
+ tick = rb.current_tick()
for i, explodedBall in ipairs(explodedBalls) do
- if rb.current_tick() >= explodedBall.death_time then
+ if explodedBall.death_time < tick then
if explodedBall.size > 0 then
explodedBall.implosion = true -- We should be dying
else
@@ -277,38 +349,64 @@
end
-- Drawing phase
+ if hit_detected then
+ hit_detected = false
+ -- same as table.remove(Balls, i) but more efficient
+ ball_el = 1
+ ball_ct = #Balls
+ for i = 1, ball_ct do
+ if Balls[i] then
+ Balls[ball_el] = Balls[i]
+ ball_el = ball_el + 1
+ end
+ end
+ -- remove any remaining exploded balls
+ for i = ball_el, ball_ct do
+ Balls[i] = nil
+ end
+ -- Calculate score
+ nrExpendedBalls = nrBalls - ball_el + 1
+ score = nrExpendedBalls * level * 100
+ end
+
rb.lcd_clear_display()
+ draw_stats()
- set_foreground(DEFAULT_FOREGROUND_COLOR)
- draw_positioned_string(0, 0, string.format("%d balls expended", nrExpendedBalls))
- draw_positioned_string(0, 1, string.format("Level %d", level))
- draw_positioned_string(1, 1, string.format("%d level points", score))
- draw_positioned_string(1, 0, string.format("%d total points", total+score))
+ if not (player_added or HAS_TOUCHSCREEN) then
+ cursor:draw()
+ end
- for _, ball in ipairs(balls) do
- ball:step()
- ball:draw()
+ for _, Ball in ipairs(Balls) do
+ Ball:step()
+ Ball:draw()
end
for _, explodedBall in ipairs(explodedBalls) do
- explodedBall:step()
- explodedBall:draw()
- end
-
- if not HAS_TOUCHSCREEN and not player_added then
- cursor:draw()
+ explodedBall:step_exploded()
+ explodedBall:draw_exploded()
end
-- Push framebuffer to the LCD
rb.lcd_update()
+ -- Check for actions
if rb.current_tick() < endtick then
- rb.sleep(endtick - rb.current_tick())
+ action = rb.get_plugin_action(endtick - rb.current_tick())
else
rb.yield()
+ action = rb.get_plugin_action(0)
end
+
end
+ --splash the final stats for a moment at end
+ rb.lcd_clear_display()
+ for _, Ball in ipairs(Balls) do Ball:draw() end
+ _LCD:clear(nil, nil, nil, nil, nil, nil, 2, 2)
+ draw_stats()
+ rb.lcd_update()
+ rb.sleep(rb.HZ * 2)
+
return exit, score, nrExpendedBalls
end
@@ -320,6 +418,7 @@
rb.lcd_clear_display()
set_foreground(DEFAULT_FOREGROUND_COLOR)
+
if w > rb.LCD_WIDTH then
rb.lcd_puts_scroll(0, y/h, message)
else
@@ -340,7 +439,7 @@
if to == -1 then
rb.sleep(rb.HZ/2)
rb.button_clear_queue()
- rb.button_get(1)
+ rb.button_get(rb.HZ * 60)
else
rb.sleep(to)
end
@@ -351,6 +450,8 @@
if HAS_TOUCHSCREEN then
rb.touchscreen_set_mode(rb.TOUCHSCREEN_POINT)
end
+
+--[[MAIN PROGRAM]]
rb.backlight_force_on()
math.randomseed(os.time())
@@ -359,9 +460,12 @@
while levels[idx] ~= nil do
local goal, nrBalls = levels[idx][1], levels[idx][2]
+ collectgarbage("collect") --run gc now to hopefully prevent interruption later
+
display_message(rb.HZ*2, "Level %d: get %d out of %d balls", idx, goal, nrBalls)
local exit, score, nrExpendedBalls = start_round(idx, goal, nrBalls, highscore)
+
if exit then
break -- Exiting..
else
@@ -371,11 +475,14 @@
highscore = highscore + score
else
display_message(rb.HZ*2, "You lost!")
+ highscore = highscore - score - idx * 100
+ if highscore < 0 then break end
end
end
end
-
-if idx > #levels then
+if highscore <= 0 then
+ display_message(-1, "You lost at level %d", idx)
+elseif idx > #levels then
display_message(-1, "You finished the game with %d points!", highscore)
else
display_message(-1, "You made it till level %d with %d points!", idx, highscore)
@@ -383,3 +490,4 @@
-- Restore user backlight settings
rb.backlight_use_settings()
+