blob: 63ac6b11fa69eb4a829697c6278555d10dd5caf3 [file] [log] [blame]
--[[
__________ __ ___.
Open \______ \ ____ ____ | | _\_ |__ _______ ___
Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
\/ \/ \/ \/ \/
$Id$
Port of Chain Reaction (which is based on Boomshine) to Rockbox in Lua.
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
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
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
local levels = {
-- {GOAL, TOTAL_BALLS},
{1, 5},
{2, 10},
{4, 15},
{6, 20},
{10, 25},
{15, 30},
{18, 35},
{22, 40},
{30, 45},
{37, 50},
{48, 55},
{59, 60},
{29, 30},
{24, 25},
{19, 20},
{14, 15},
{9, 10},
{10, 10},
{4, 5},
{5, 5}
}
local Ball = {
size = DEFAULT_BALL_SIZE,
exploded = false,
implosion = false
}
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(level),
right_speed = Ball:generateSpeed(level),
explosion_size = math.random(2*self.size, 4*self.size),
life_duration = math.random(rb.HZ / level, rb.HZ*5),
}
end
setmetatable(o, self)
self.__index = self
return o
end
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
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()
--[[
--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()
self.x = self.x + self.right_speed
self.y = self.y + self.up_speed
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.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
function Ball:checkHit(other)
if (other.x + other.size >= self.x) and (self.x + self.size >= other.x) and
(other.y + other.size >= self.y) and (self.y + self.size >= other.y) then
assert(not self.exploded)
self.exploded = true
self.death_time = rb.current_tick() + self.life_duration
if not other.exploded then
other.exploded = true
other.death_time = rb.current_tick() + other.life_duration
end
return true
end
return false
end
local Cursor = {
size = DEFAULT_BALL_SIZE*2,
x = rb.LCD_WIDTH/2,
y = rb.LCD_HEIGHT/2,
image = create_cursor(DEFAULT_BALL_SIZE*2)
}
function Cursor:new()
return self
end
function Cursor:do_action(action)
if action == actions_pla.ACTION_TOUCHSCREEN and HAS_TOUCHSCREEN then
_, self.x, self.y = rb.action_get_touchscreen_press()
return true
elseif action == actions_pla.PLA_SELECT then
return true
elseif (action == actions_pla.PLA_RIGHT or action == actions_pla.PLA_RIGHT_REPEAT) then
self.x = self.x + self.size
elseif (action == actions_pla.PLA_LEFT or action == actions_pla.PLA_LEFT_REPEAT) then
self.x = self.x - self.size
elseif (action == actions_pla.PLA_UP or action == actions_pla.PLA_UP_REPEAT) then
self.y = self.y - self.size
elseif (action == actions_pla.PLA_DOWN or action == actions_pla.PLA_DOWN_REPEAT) then
self.y = self.y + self.size
end
if self.x > rb.LCD_WIDTH then
self.x = 0
elseif self.x < 0 then
self.x = rb.LCD_WIDTH
end
if self.y > rb.LCD_HEIGHT then
self.y = 0
elseif self.y < 0 then
self.y = rb.LCD_HEIGHT
end
return false
end
function Cursor:draw()
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)
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)
if rb.lcd_set_foreground ~= nil then
rb.lcd_set_foreground(color)
end
end
function random_color()
if rb.lcd_rgbpack ~= nil then --color target
return rb.lcd_rgbpack(math.random(1,255), math.random(1,255), math.random(1,255))
end
return math.random(1, rb.LCD_DEPTH)
end
function start_round(level, goal, nrBalls, total)
local player_added, score, exit, nrExpendedBalls = false, 0, false, 0
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(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
endtick = rb.current_tick() + CYCLETIME
-- Check if the round is over
if player_added and #explodedBalls == 0 then
break
end
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,
y = cursor.y,
color = DEFAULT_FOREGROUND_COLOR,
size = 10,
explosion_size = 3*DEFAULT_BALL_SIZE,
exploded = true,
death_time = rb.current_tick() + rb.HZ * 3
})
explodedBalls[1] = player
player_added = true
cursor = nil
end
-- Check for hits
for i, Ball in ipairs(Balls) do
for _, explodedBall in ipairs(explodedBalls) do
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 explodedBall.death_time < tick then
if explodedBall.size > 0 then
explodedBall.implosion = true -- We should be dying
else
table.remove(explodedBalls, i) -- We're imploded!
end
end
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()
if not (player_added or HAS_TOUCHSCREEN) then
cursor:draw()
end
for _, Ball in ipairs(Balls) do
Ball:step()
Ball:draw()
end
for _, explodedBall in ipairs(explodedBalls) do
explodedBall:step_exploded()
explodedBall:draw_exploded()
end
-- Push framebuffer to the LCD
rb.lcd_update()
-- Check for actions
if rb.current_tick() < endtick then
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
-- Helper function to display a message
function display_message(to, ...)
local message = string.format(...)
local _, w, h = rb.font_getstringsize(message, rb.FONT_UI)
local x, y = (rb.LCD_WIDTH - w) / 2, (rb.LCD_HEIGHT - h) / 2
rb.lcd_clear_display()
set_foreground(DEFAULT_FOREGROUND_COLOR)
if w > rb.LCD_WIDTH then
rb.lcd_puts_scroll(0, y/h, message)
else
rb.lcd_putsxy(x, y, message)
end
if to == -1 then
local msg = "Press button to exit"
w = rb.font_getstringsize(msg, rb.FONT_UI)
x = (rb.LCD_WIDTH - w) / 2
if x < 0 then
rb.lcd_puts_scroll(0, y/h + 1, msg)
else
rb.lcd_putsxy(x, y + h, msg)
end
end
rb.lcd_update()
if to == -1 then
rb.sleep(rb.HZ/2)
rb.button_clear_queue()
rb.button_get(rb.HZ * 60)
else
rb.sleep(to)
end
rb.lcd_scroll_stop() -- Stop our scrolling message
end
if HAS_TOUCHSCREEN then
rb.touchscreen_set_mode(rb.TOUCHSCREEN_POINT)
end
--[[MAIN PROGRAM]]
rb.backlight_force_on()
math.randomseed(os.time())
local idx, highscore = 1, 0
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
if nrExpendedBalls >= goal then
display_message(rb.HZ*2, "You won!")
idx = idx + 1
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 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)
end
-- Restore user backlight settings
rb.backlight_use_settings()