Something went wrong. Try again.
A modern, network-enabled music player platform built on Rockbox technology. rockboxd.tsiry-sandratraina.com
rust deno navidrome airplay libadwaita zig mpris snapcast mpd rockbox audio subsonic
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768--[[ __________ __ ___. Open \______ \ ____ ____ | | _\_ |__ _______ ___ Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ \/ \/ \/ \/ \/
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) 2020 William Wilgus -- Added circles, logic rewrite, 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"local DEBUG = false
--[[only save the actions we are using]]----------------------------------------pla = {}for key, val in pairs(rb.actions) do for _, v in ipairs({"PLA_", "TOUCHSCREEN", "_NONE"}) do if string.find (key, v) then pla[key] = val; break end endendrb.actions, rb.contexts = nil, nil--------------------------------------------------------------------------------
--[[ Profiling ]]---------------------------------------------------------------local screen_redraw_count = 0local screen_redraw_duration = 0--------------------------------------------------------------------------------
--[[ References ]]--------------------------------------------------------------local _LCD = rb.lcd_framebuffer()local rocklib_image = getmetatable(_LCD)local _ellipse = rocklib_image.ellipse--local irand = math.random--local lcd_putsxy = rb.lcd_putsxy--local strfmt = string.format--local Cursor_Ref = nillocal Ball_Ref = {}--------------------------------------------------------------------------------
--[[ 'Constants' ]]-------------------------------------------------------------local SCORE_MULTIPLY = 10local BSAND = 0x8--local HAS_TOUCHSCREEN = rb.action_get_touchscreen_press ~= nillocal Empty_fn = function() endlocal LCD_H, LCD_W = rb.LCD_HEIGHT, rb.LCD_WIDTHlocal DEFAULT_BALL_SZ = LCD_H > LCD_W and LCD_W / 30 or LCD_H / 30 DEFAULT_BALL_SZ = DEFAULT_BALL_SZ - bit.band(DEFAULT_BALL_SZ, 1)local MAX_BALL_SPEED = DEFAULT_BALL_SZ / 2 + 1-- Ball stateslocal B_DEAD, B_MOVE, B_EXPLODE, B_DIE, B_WAIT, B_IMPLODE = 5, 4, 3, 2, 1, 0
local DEFAULT_FG_CLR = 1local DEFAULT_BG_CLR = 0if rb.lcd_get_foreground ~= nil then DEFAULT_FG_CLR = rb.lcd_get_foreground() DEFAULT_BG_CLR = rb.lcd_get_background()elseif rb.LCD_DEFAULT_FG ~= nil then DEFAULT_FG_CLR = rb.LCD_DEFAULT_FG DEFAULT_BG_CLR = rb.LCD_DEFAULT_BGend
local FMT_EXPEND, FMT_LEVEL = "%d balls expended", "Level %d"local FMT_TOTPTS, FMT_LVPTS = "%d total points", "%d level points"--------------------------------------------------------------------------------
--[[ Other ]]-------------------------------------------------------------------local Player_Addedlocal Action_Evt = pla.ACTION_NONE
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}, {58, 60}, {28, 30}, {23, 25}, {18, 20}, {13, 15}, {8, 10}, {9, 10}, {4, 5}, {5, 5} }--------------------------------------------------------------------------------
--[[ Event Callback ]]----------------------------------------------------------function action_event(action) if action == pla.PLA_CANCEL then Action_Evt = pla.PLA_EXIT else Action_Evt = action endend--------------------------------------------------------------------------------
--[[ Helper functions ]]--------------------------------------------------------local function getstringsize(str) local _, w, h = rb.font_getstringsize(str, rb.FONT_UI) return w, hend
local function set_foreground(color) if rb.lcd_set_foreground ~= nil then rb.lcd_set_foreground(color) endend
local function calc_score(total, level, goal, expended) local bonus = 0 local score = (expended * level) * SCORE_MULTIPLY if expended < goal then score = -(score + (level * SCORE_MULTIPLY)) elseif expended > goal then bonus = (expended - goal) * (level * SCORE_MULTIPLY) end total = total + score + bonus return total, score, bonusend
local function wait_anykey(to_secs) rb.sleep(rb.HZ / 2) rb.button_clear_queue() rb.button_get_w_tmo(rb.HZ * to_secs)end
local function disp_msg(to, ...) local message = strfmt(...) local w, h = getstringsize(message) local x, y = (LCD_W - w) / 2, (LCD_H - h) / 2
rb.lcd_clear_display() set_foreground(DEFAULT_FG_CLR)
if w > LCD_W then rb.lcd_puts_scroll(0, (y / h), message) else lcd_putsxy(x, y, message) end
if to == -1 then local msg = "Press button to exit" w, h = getstringsize(msg) x = (LCD_W - w) / 2 if x < 0 then rb.lcd_puts_scroll(0, (y / h) + 1, msg) else lcd_putsxy(x, y + h, msg) end end rb.lcd_update()
if to == -1 then wait_anykey(60) else rb.sleep(to) end
rb.lcd_scroll_stop() -- Stop our scrolling messageend
local function test_speed() local test_spd, redraw, dur = start_round(0, 0, 0, 0) -- test speed of target --Logic speed, screen redraw, duration if DEBUG == true then disp_msg(rb.HZ * 5, "Spd: %d, Redraw: %d Dur: %d Ms", test_spd, redraw, dur) end if test_spd < 25 or redraw < 10 then rb.splash(rb.HZ, "Slow Target..")
if test_spd < 10 then MAX_BALL_SPEED = MAX_BALL_SPEED + MAX_BALL_SPEED elseif test_spd < 15 then MAX_BALL_SPEED = MAX_BALL_SPEED + MAX_BALL_SPEED / 2 elseif test_spd < 25 then MAX_BALL_SPEED = MAX_BALL_SPEED + MAX_BALL_SPEED / 4 end endend--------------------------------------------------------------------------------
--[[ Ball Functions ]]----------------------------------------------------------local Ball = { -- Ball defaults sz = DEFAULT_BALL_SZ, next_tick = 0, state = B_MOVE, }
function Ball:new(o, level, color) local function random_color() if color == nil then if rb.lcd_rgbpack ~= nil then -- color target return rb.lcd_rgbpack(irand(1,255), irand(1,255), irand(1,255)) end color = irand(1, rb.LCD_DEPTH) color = (rb.LCD_DEPTH == 2) and (3 - color) or color -- invert for 2-bit screens end return color end
if o == nil then level = level or 1 local maxdelay = (level <= 5) and 15 or level * 2 o = { x = irand(1, LCD_W - self.sz), y = irand(1, LCD_H - self.sz), color = random_color(), xi = self.genSpeed(), -- x increment; x + sz after explode yi = self.genSpeed(), -- y increment; y + sz after explode step_delay = irand(3, maxdelay / 2), explosion_sz = irand(2 * self.sz, 4 * self.sz), life_ticks = irand(rb.HZ / level, rb.HZ * (maxdelay / 5)), draw_fn = nil, step_fn = self.step -- reference to current stepping function } end local Ball = o or {} -- so we can use Ball. instead of self -- these functions end up in a closure; faster to call, use more RAM function Ball.draw() _ellipse(_LCD, o.x, o.y, o.x + o.sz, o.y + o.sz, o.color, o.color, true) end
function Ball.draw_exploded() _ellipse(_LCD, o.x, o.y, o.xi, o.yi, o.color, nil, true) end
if Ball.draw_fn == nil then Ball.draw_fn = Ball.draw -- reference to current drawing function end
setmetatable(o, self) self.__index = self return oend
-- these functions are shared by reference, slower to call, use less RAMfunction Ball:genSpeed() local speed = irand(-MAX_BALL_SPEED, MAX_BALL_SPEED) return speed ~= 0 and speed or MAX_BALL_SPEED -- Make sure all balls moveend
function Ball:step(tick) local function rndspeed(cur) local speed = cur + irand(-2, 2) if speed < -MAX_BALL_SPEED then speed = -MAX_BALL_SPEED elseif speed > MAX_BALL_SPEED then speed = MAX_BALL_SPEED elseif speed == 0 then speed = cur end return speed end
self.next_tick = tick + self.step_delay self.x = self.x + self.xi self.y = self.y + self.yi local max_x = LCD_W - self.sz local max_y = LCD_H - self.sz
if self.x <= 0 then self.xi = -self.xi self.x = 1 self.yi = rndspeed(self.yi) elseif self.x >= max_x then self.xi = -self.xi self.x = max_x self.yi = rndspeed(self.yi) end
if self.y <= 0 then self.yi = -self.yi self.y = 1 self.xi = rndspeed(self.xi) elseif self.y >= max_y then self.yi = -self.yi self.y = max_y self.xi = rndspeed(self.xi) endend
function Ball:step_exploded(tick) -- exploding ball state machine -- B_EXPLODE >> B_DIE >> BWAIT >> B_IMPLODE >> B_DEAD if self.state == B_EXPLODE and self.sz < self.explosion_sz then self.sz = self.sz + 2 self.x = self.x - 1 -- stay centered self.y = self.y - 1 elseif self.state == B_DIE then self.state = B_WAIT self.next_tick = tick + self.life_ticks return elseif self.state == B_IMPLODE then if self.sz > 0 then self.sz = self.sz - 2 self.x = self.x + 1 -- stay centered self.y = self.y + 1 else self.state = B_DEAD self.draw_fn = Empty_fn self.step_fn = Empty_fn return B_DEAD end elseif self.next_tick < tick then -- decay to next lower state self.state = self.state - 1 return end -- fell through, update next_tick and impact region self.next_tick = tick + self.step_delay self.xi = self.x + self.sz self.yi = self.y + self.szend
function Ball:checkHit(other) local x, y = self.x, self.y if (other.xi >= x) and (other.yi >= y) then local sz = self.sz local xi, yi = x + sz, y + sz if (xi >= other.x) and (yi >= other.y) then -- update impact region self.xi = xi self.yi = yi -- change to exploded state self.state = B_EXPLODE self.draw_fn = self.draw_exploded self.step_fn = self.step_exploded
if other.state < B_EXPLODE then -- add duration to the ball that got hit other.next_tick = other.next_tick + self.life_ticks end return true end end
return falseend--------------------------------------------------------------------------------
--[[ Cursor Functions ]]--------------------------------------------------------local Cursor = { sz = (DEFAULT_BALL_SZ * 2), x = (LCD_W / 2), y = (LCD_H / 2), color = DEFAULT_FG_CLR --image = nil }
function Cursor:new() if rb.LCD_DEPTH == 2 then -- invert for 2 - bit screens self.color = 3 - DEFAULT_FG_CLR end if not HAS_TOUCHSCREEN and not self.image then local function create_image(sz) sz = sz + 1 local img = rb.new_image(sz, sz) local sz2 = (sz / 2) + 1 local sz4 = (sz / 4)
img:clear(0) img:line(1, 1, sz4 + 1, 1, 1) img:line(1, 1, 1, sz4 + 1, 1)
img:line(1, sz, sz4 + 1, sz, 1) img:line(1, sz, 1, sz - sz4, 1)
img:line(sz, sz, sz - sz4, sz, 1) img:line(sz, sz, sz, sz - sz4, 1)
img:line(sz, 1, sz - sz4, 1, 1) img:line(sz, 1, sz, sz4 + 1, 1)
-- crosshairs img:line(sz2 - sz4, sz2, sz2 + sz4, sz2, 1) img:line(sz2, sz2 - sz4, sz2, sz2 + sz4, 1) return img end self.image = create_image(DEFAULT_BALL_SZ * 2) end
function Cursor.draw() rocklib_image.copy(_LCD, self.image, self.x, self.y, _NIL, _NIL, _NIL, _NIL, true, BSAND, self.color) end
return selfend
function Cursor:do_action(action) local function clamp_roll(iVal, iMin, iMax) if iVal < iMin then iVal = iMax elseif iVal > iMax then iVal = iMin end return iVal end local xi, yi = 0, 0
if HAS_TOUCHSCREEN and action == pla.ACTION_TOUCHSCREEN then _, self.x, self.y = rb.action_get_touchscreen_press() return true elseif action == pla.PLA_SELECT then return true elseif (action == pla.PLA_RIGHT or action == pla.PLA_RIGHT_REPEAT) then xi = self.sz elseif (action == pla.PLA_LEFT or action == pla.PLA_LEFT_REPEAT) then xi = -self.sz elseif (action == pla.PLA_UP or action == pla.PLA_UP_REPEAT) then yi = -self.sz elseif (action == pla.PLA_DOWN or action == pla.PLA_DOWN_REPEAT) then yi = self.sz else Action_Evt = pla.ACTION_NONE return false end
self.x = clamp_roll(self.x + xi, 1, LCD_W - self.sz) self.y = clamp_roll(self.y + yi, 1, LCD_H - self.sz) Action_Evt = pla.ACTION_NONE return falseend--------------------------------------------------------------------------------
--[[ Game function ]]-----------------------------------------------------------function start_round(level, goal, nrBalls, total) Player_Added = false --[[ References ]]-- local current_tick = rb.current_tick local lcd_update = rb.lcd_update local lcd_clear_display = rb.lcd_clear_display
local test_spd = false local is_exit = false; local score = 0 local last_expend, nrBalls_expend = 0, 0 local Balls = {} local balls_exploded = 1 -- to keep looping when player_added == false
local tick_next = 0 local cursor = nil local draw_cursor = Empty_fn local refresh = rb.HZ/20
local function level_stats(level, total) return strfmt(FMT_LEVEL, level), strfmt(FMT_TOTPTS, total) end
local str_level, str_totpts = level_stats(level, total) -- static for lvl local str_expend, str_lvlpts
local function update_stats() -- we only create a new string when a hit is detected str_expend = strfmt(FMT_EXPEND, nrBalls_expend) str_lvlpts = strfmt(FMT_LVPTS, score) end
function draw_stats() local function draw_pos_str(bottom, right, str) local w, h = getstringsize(str) local x = (right > 0) and ((LCD_W - w) * right - 1) or 1 local y = (bottom > 0) and ((LCD_H - h) * bottom - 1) or 1 lcd_putsxy(x, y, str) end -- pos(B, R) [B/R]=0 => [y/x]=1, [B/R]=1 => [y/x]=lcd[H/W]-string[H/W] draw_pos_str(0, 0, str_expend) draw_pos_str(0, 1, str_level) draw_pos_str(1, 1, str_lvlpts) draw_pos_str(1, 0, str_totpts) end
-- all Balls share same function, will by changed by player_add() local checkhit_fn = Empty_fn
local function checkhit(Ball) if Ball.state == B_MOVE then for i = #Balls, 1, -1 do if Balls[i].state < B_MOVE and Ball:checkHit(Balls[i]) then -- exploded? balls_exploded = balls_exploded + 1 nrBalls_expend = nrBalls_expend + 1 break end end end end
local function add_player() -- cursor becomes exploded ball local player = Ball:new({ x = cursor.x - cursor.sz, y = cursor.y - cursor.sz, color = cursor.color, step_delay = 3, explosion_sz = (3 * DEFAULT_BALL_SZ), life_ticks = (test_spd) and (rb.HZ) or irand(rb.HZ * 2, rb.HZ * DEFAULT_BALL_SZ), sz = 10, state = B_EXPLODE }) -- set x/y impact region -->[] player.xi = player.x + player.sz player.yi = player.y + player.sz player.draw_fn = player.draw_exploded player.step_fn = player.step_exploded
table.insert(Balls, player) balls_exploded = 1 Player_Added = true cursor = nil draw_cursor = Empty_fn -- only need collision detection after player add checkhit_fn = checkhit end
local function speedtest() -- check speed of target set_foreground(DEFAULT_BG_CLR) --hide text during test level = 1 nrBalls = 100 cursor = {x = LCD_W * 2, y = LCD_H * 2, color = 0, sz = 1} table.insert(Balls, Ball:new({ x = 1, y = 1, xi = 1, yi = 1, color = 0, step_delay = 0, explosion_sz = 0, life_ticks = 0, draw_fn = Empty_fn, step_fn = function() test_spd = test_spd + 1 end }) ) test_spd = 0 add_player() end
local function screen_redraw() -- (draw_fn changes dynamically at runtime) for i = 1, #Balls do Balls[i].draw_fn() end
draw_stats() draw_cursor()
lcd_update() lcd_clear_display() end
local function game_loop() local tick = current_tick() for _, Ball in ipairs(Balls) do if tick > Ball.next_tick then -- (step_fn changes dynamically at runtime) if Ball:step_fn(tick) == B_DEAD then balls_exploded = balls_exploded - 1 else checkhit_fn(Ball) end end end return tick end
if level < 1 then speedtest() local bkcolor = (rb.LCD_DEPTH == 2) and (3) or 0 -- Initialize the balls if DEBUG then bkcolor = nil end for i=1, nrBalls do table.insert(Balls, Ball:new(nil, level, bkcolor)) end else speedtest = nil set_foreground(DEFAULT_FG_CLR) -- color for text cursor = Cursor:new() if not HAS_TOUCHSCREEN then draw_cursor = cursor.draw end -- Initialize the balls for i=1, nrBalls do table.insert(Balls, Ball:new(nil, level)) end end
-- Make sure there are no unwanted touchscreen presses rb.button_clear_queue()
Action_Evt = pla.ACTION_NONE
update_stats() -- load status strings
if rb.cpu_boost then rb.cpu_boost(true) end collectgarbage("collect") -- run gc now to hopefully prevent interruption later
local duration = current_tick() -- Game loop >> Check if the round is over while balls_exploded > 0 do if Action_Evt == pla.PLA_EXIT then is_exit = true break end
if game_loop() > tick_next then tick_next = current_tick() + refresh if not Player_Added then if Action_Evt ~= pla.ACTION_NONE and cursor:do_action(Action_Evt) then add_player() end end
if nrBalls_expend ~= last_expend then -- hit detected? last_expend = nrBalls_expend score = (nrBalls_expend * level) * SCORE_MULTIPLY update_stats() -- only update stats when not current if nrBalls_expend == nrBalls then break end -- round is over? end
screen_redraw_count = screen_redraw_count + 1 screen_redraw() end rb.yield() -- yield to other tasks end
screen_redraw_duration = screen_redraw_duration + (rb.current_tick() - duration) if rb.cpu_boost then rb.cpu_boost(false) end
if test_spd and test_spd > 0 then return test_spd, screen_redraw_count, screen_redraw_duration *10 --ms end
-- splash the final stats for a moment at end lcd_clear_display() for _, Ball in ipairs(Balls) do -- move balls back to their initial exploded positions if Ball.state == B_DEAD then Ball.sz = Ball.explosion_sz + 2 Ball.x = Ball.x - (Ball.explosion_sz / 2) Ball.y = Ball.y - (Ball.explosion_sz / 2) end Ball:draw() end
if DEFAULT_BALL_SZ > 3 then -- dither _LCD:clear(nil, nil, nil, nil, nil, nil, 2, 2) end
total = calc_score(total, level, goal, nrBalls_expend) str_level, str_totpts = level_stats(level, total) draw_stats() lcd_update() wait_anykey(2)
return is_exit, score, nrBalls_expendend--------------------------------------------------------------------------------
--[[MAIN PROGRAM]]--------------------------------------------------------------do -- attempt to get stats to fit on screen local function getwidth(str) local w, _ = getstringsize(str) return w end local w0, w1 = getwidth(FMT_EXPEND), getwidth(FMT_LEVEL) if (w0 + w1) > LCD_W then FMT_EXPEND, FMT_LEVEL = "%d balls", "Lv %d" end w0, w1 = getwidth(FMT_TOTPTS), getwidth(FMT_LVPTS) if (w0 + w1 + getwidth("0000000")) > LCD_W then FMT_TOTPTS, FMT_LVPTS = "%d total", "%d lv" endend
if HAS_TOUCHSCREEN then rb.touchscreen_mode(rb.TOUCHSCREEN_POINT)end
rb.backlight_force_on()
local eva = rockev.register("action", action_event, rb.HZ / 10)
math.randomseed(os.time() or 1)
local idx, highscore = 1, 0
disp_msg(rb.HZ, "BoomShine")test_speed()rb.sleep(rb.HZ * 2)test_speed = nil
while levels[idx] ~= nil do local goal, nrBalls = levels[idx][1], levels[idx][2]
collectgarbage("collect") -- run gc now to hopefully prevent interruption later
disp_msg(rb.HZ * 2, "Level %d: get %d out of %d balls", idx, goal, nrBalls)
local is_exit, score, nrBalls_expend = start_round(idx, goal, nrBalls, highscore) if DEBUG == true then local fps = screen_redraw_count * 100 / screen_redraw_duration disp_msg(rb.HZ * 5, "Redraw: %d fps", fps) end
if is_exit then break -- Exiting.. else highscore, score, bonus = calc_score(highscore, idx, goal, nrBalls_expend) if nrBalls_expend >= goal then if bonus == 0 then disp_msg(rb.HZ * 2, "You won!") else disp_msg(rb.HZ * 2, "You won BONUS!") end levels[idx] = nil idx = idx + 1 else disp_msg(rb.HZ * 2, "You lost %d points!", -score) if highscore < 0 then break end end endend
if highscore <= 0 then disp_msg(-1, "You lost at level %d", idx)elseif idx > #levels then disp_msg(-1, "You finished the game with %d points!", highscore)else disp_msg(-1, "You made it till level %d with %d points!", idx, highscore)end
-- Restore user backlight settingsrb.backlight_use_settings()if rb.cpu_boost then rb.cpu_boost(false) end
os.exit()--------------------------------------------------------------------------------