diff options
Diffstat (limited to 'apps/plugins/boomshine.lua')
-rw-r--r-- | apps/plugins/boomshine.lua | 669 |
1 files changed, 386 insertions, 283 deletions
diff --git a/apps/plugins/boomshine.lua b/apps/plugins/boomshine.lua index 63ac6b11fa..224708d065 100644 --- a/apps/plugins/boomshine.lua +++ b/apps/plugins/boomshine.lua | |||
@@ -11,7 +11,7 @@ | |||
11 | See http://www.yvoschaap.com/chainrxn/ and http://www.k2xl.com/games/boomshine/ | 11 | See http://www.yvoschaap.com/chainrxn/ and http://www.k2xl.com/games/boomshine/ |
12 | 12 | ||
13 | Copyright (C) 2009 by Maurus Cuelenaere | 13 | Copyright (C) 2009 by Maurus Cuelenaere |
14 | Copyright (C) 2018 William Wilgus -- Added circles, blit cursor, hard levels | 14 | Copyright (C) 2018 William Wilgus -- Added circles, logic rewrite, hard levels |
15 | 15 | ||
16 | This program is free software; you can redistribute it and/or | 16 | This program is free software; you can redistribute it and/or |
17 | modify it under the terms of the GNU General Public License | 17 | modify it under the terms of the GNU General Public License |
@@ -23,34 +23,44 @@ | |||
23 | 23 | ||
24 | ]]-- | 24 | ]]-- |
25 | require "actions" | 25 | require "actions" |
26 | -- [[only save the actions we are using]] | 26 | --[[only save the actions we are using]] |
27 | actions_pla = {} | 27 | pla = {} |
28 | for key, val in pairs(rb.actions) do | 28 | for key, val in pairs(rb.actions) do |
29 | for _, v in ipairs({"PLA_", "TOUCHSCREEN"}) do | 29 | for _, v in ipairs({"PLA_", "TOUCHSCREEN", "_NONE"}) do |
30 | if string.find (key, v) then | 30 | if string.find (key, v) then |
31 | actions_pla[key] = val | 31 | pla[key] = val; break |
32 | break | ||
33 | end | 32 | end |
34 | end | 33 | end |
35 | end | 34 | end |
36 | rb.actions = nil | 35 | rb.actions, rb.contexts = nil, nil |
37 | rb.contexts = nil | 36 | -------------------------------------- |
38 | ------------------------------------- | ||
39 | 37 | ||
40 | local _LCD = rb.lcd_framebuffer() | 38 | local _LCD = rb.lcd_framebuffer() |
41 | local BSAND = 0x8 | 39 | local rocklib_image = getmetatable(_LCD) |
42 | local rocklib_image = getmetatable(rb.lcd_framebuffer()) | ||
43 | local _ellipse = rocklib_image.ellipse | 40 | local _ellipse = rocklib_image.ellipse |
41 | local BSAND = 0x8 | ||
42 | local irand = math.random | ||
44 | 43 | ||
45 | local CYCLETIME = rb.HZ / 50 | ||
46 | local HAS_TOUCHSCREEN = rb.action_get_touchscreen_press ~= nil | 44 | local HAS_TOUCHSCREEN = rb.action_get_touchscreen_press ~= nil |
47 | local DEFAULT_BALL_SIZE = rb.LCD_HEIGHT > rb.LCD_WIDTH and rb.LCD_WIDTH / 30 | 45 | local LCD_H, LCD_W = rb.LCD_HEIGHT, rb.LCD_WIDTH |
48 | or rb.LCD_HEIGHT / 30 | 46 | local DEFAULT_BALL_SZ = LCD_H > LCD_W and LCD_W / 30 or LCD_H / 30 |
49 | local MAX_BALL_SPEED = DEFAULT_BALL_SIZE / 2 | 47 | DEFAULT_BALL_SZ = DEFAULT_BALL_SZ - bit.band(DEFAULT_BALL_SZ, 1) |
50 | local DEC_BALL_SPEED = DEFAULT_BALL_SIZE / 8 | 48 | local MAX_BALL_SPEED = DEFAULT_BALL_SZ / 2 + 1 |
51 | local DEFAULT_FOREGROUND_COLOR = rb.lcd_get_foreground ~= nil | 49 | -- Ball states |
52 | and rb.lcd_get_foreground() | 50 | local B_DEAD, B_MOVE, B_EXPLODE, B_DIE, B_WAIT, B_IMPLODE = 5, 4, 3, 2, 1, 0 |
53 | or 0 | 51 | |
52 | local DEFAULT_FG_CLR = 1 | ||
53 | local DEFAULT_BG_CLR = 0 | ||
54 | if rb.lcd_get_foreground ~= nil then | ||
55 | DEFAULT_FG_CLR = rb.lcd_get_foreground() | ||
56 | DEFAULT_BG_CLR = rb.lcd_get_background() | ||
57 | elseif rb.LCD_DEFAULT_FG ~= nil then | ||
58 | DEFAULT_FG_CLR = rb.LCD_DEFAULT_FG | ||
59 | DEFAULT_BG_CLR = rb.LCD_DEFAULT_BG | ||
60 | end | ||
61 | |||
62 | local FMT_EXPEND, FMT_LEVEL = "%d balls expended", "Level %d" | ||
63 | local FMT_TOTPTS, FMT_LVPTS = "%d total points", "%d level points" | ||
54 | 64 | ||
55 | local levels = { | 65 | local levels = { |
56 | -- {GOAL, TOTAL_BALLS}, | 66 | -- {GOAL, TOTAL_BALLS}, |
@@ -65,134 +75,103 @@ local levels = { | |||
65 | {30, 45}, | 75 | {30, 45}, |
66 | {37, 50}, | 76 | {37, 50}, |
67 | {48, 55}, | 77 | {48, 55}, |
68 | {59, 60}, | 78 | {58, 60}, |
69 | {29, 30}, | 79 | {28, 30}, |
70 | {24, 25}, | 80 | {23, 25}, |
71 | {19, 20}, | 81 | {18, 20}, |
72 | {14, 15}, | 82 | {13, 15}, |
83 | {8, 10}, | ||
73 | {9, 10}, | 84 | {9, 10}, |
74 | {10, 10}, | ||
75 | {4, 5}, | 85 | {4, 5}, |
76 | {5, 5} | 86 | {5, 5} |
77 | } | 87 | } |
78 | |||
79 | local Ball = { | ||
80 | size = DEFAULT_BALL_SIZE, | ||
81 | exploded = false, | ||
82 | implosion = false | ||
83 | } | ||
84 | |||
85 | local function create_cursor(size) | ||
86 | local cursor | ||
87 | if not HAS_TOUCHSCREEN then | ||
88 | cursor = rb.new_image(size, size) | ||
89 | cursor:clear(0) | ||
90 | local sz2 = size / 2 | ||
91 | local sz4 = size / 4 | ||
92 | 88 | ||
93 | cursor:line(1, 1, sz4, 1, 1) | 89 | local function getstringsize(str) |
94 | cursor:line(1, 1, 1, sz4, 1) | 90 | local _, w, h = rb.font_getstringsize(str, rb.FONT_UI) |
91 | return w, h | ||
92 | end | ||
95 | 93 | ||
96 | cursor:line(1, size, sz4, size, 1) | 94 | local function set_foreground(color) |
97 | cursor:line(1, size, 1, size - sz4, 1) | 95 | if rb.lcd_set_foreground ~= nil then |
96 | rb.lcd_set_foreground(color) | ||
97 | end | ||
98 | end | ||
98 | 99 | ||
99 | cursor:line(size, size, size - sz4, size, 1) | 100 | local function random_color() |
100 | cursor:line(size, size, size, size - sz4, 1) | 101 | if rb.lcd_rgbpack ~= nil then -- color target |
102 | return rb.lcd_rgbpack(irand(1,255), irand(1,255), irand(1,255)) | ||
103 | end | ||
101 | 104 | ||
102 | cursor:line(size, 1, size - sz4, 1, 1) | 105 | local color = irand(1, rb.LCD_DEPTH) |
103 | cursor:line(size, 1, size, sz4, 1) | 106 | color = (rb.LCD_DEPTH == 2) and (3 - color) or color -- invert for 2-bit screens |
104 | 107 | ||
105 | --crosshairs | 108 | return color |
106 | cursor:line(sz2 - sz4, sz2, sz2 + sz4, sz2, 1) | ||
107 | cursor:line(sz2, sz2 - sz4, sz2, sz2 + sz4, 1) | ||
108 | end | ||
109 | return cursor | ||
110 | end | 109 | end |
111 | 110 | ||
111 | local Ball = { | ||
112 | -- Ball defaults | ||
113 | sz = DEFAULT_BALL_SZ, | ||
114 | next_tick = 0, | ||
115 | state = B_MOVE | ||
116 | } | ||
117 | |||
112 | function Ball:new(o, level) | 118 | function Ball:new(o, level) |
113 | level = level or 1 | ||
114 | if o == nil then | 119 | if o == nil then |
120 | level = level or 1 | ||
121 | local maxdelay = (level <= 5) and 10 or level | ||
115 | o = { | 122 | o = { |
116 | x = math.random(0, rb.LCD_WIDTH - self.size), | 123 | x = irand(1, LCD_W - self.sz), |
117 | y = math.random(0, rb.LCD_HEIGHT - self.size), | 124 | y = irand(1, LCD_H - self.sz), |
118 | color = random_color(), | 125 | color = random_color(), |
119 | up_speed = Ball:generateSpeed(level), | 126 | xi = Ball:genSpeed(), |
120 | right_speed = Ball:generateSpeed(level), | 127 | yi = Ball:genSpeed(), |
121 | explosion_size = math.random(2*self.size, 4*self.size), | 128 | step_delay = irand(3, maxdelay / 2), |
122 | life_duration = math.random(rb.HZ / level, rb.HZ*5), | 129 | explosion_sz = irand(2 * self.sz, 4 * self.sz), |
130 | life_ticks = irand(rb.HZ / level, rb.HZ * (maxdelay / 5)) | ||
123 | } | 131 | } |
124 | end | 132 | end |
125 | 133 | o.life_ticks = o.life_ticks + DEFAULT_BALL_SZ -- extra time for larger screens | |
126 | setmetatable(o, self) | 134 | setmetatable(o, self) |
127 | self.__index = self | 135 | self.__index = self |
128 | return o | 136 | return o |
129 | end | 137 | end |
130 | 138 | ||
131 | function Ball:generateSpeed(level) | 139 | function Ball:genSpeed() |
132 | local ballspeed = MAX_BALL_SPEED - (DEC_BALL_SPEED * (level - 1)) | 140 | local speed = irand(-MAX_BALL_SPEED, MAX_BALL_SPEED) |
133 | if ballspeed < 2 then ballspeed = 2 end | 141 | return speed ~= 0 and speed or MAX_BALL_SPEED -- Make sure all balls move |
134 | local speed = math.random(-ballspeed, ballspeed) | ||
135 | if speed == 0 then | ||
136 | speed = 1 -- Make sure all balls move | ||
137 | end | ||
138 | |||
139 | return speed | ||
140 | end | ||
141 | |||
142 | function Ball:draw_exploded() | ||
143 | --[[ | ||
144 | --set_foreground(self.color) | ||
145 | --rb.lcd_fillrect(self.x, self.y, self.size, self.size) | ||
146 | ]] | ||
147 | |||
148 | _ellipse(_LCD, self.x, self.y, | ||
149 | self.x + self.size, self.y + self.size , self.color, nil, true) | ||
150 | end | 142 | end |
151 | 143 | ||
152 | function Ball:draw() | 144 | function Ball:draw() |
153 | --[[ | ||
154 | --set_foreground(self.color) | ||
155 | --rb.lcd_fillrect(self.x, self.y, self.size, self.size) | ||
156 | ]] | ||
157 | |||
158 | _ellipse(_LCD, self.x, self.y, | 145 | _ellipse(_LCD, self.x, self.y, |
159 | self.x + self.size, self.y + self.size , self.color, self.color, true) | 146 | self.x + self.sz, self.y + self.sz , self.color, self.color, true) |
160 | end | 147 | end |
161 | 148 | ||
162 | function Ball:step_exploded() | 149 | function Ball:step(tick) |
163 | if self.implosion and self.size > 0 then | 150 | self.next_tick = tick + self.step_delay |
164 | self.size = self.size - 2 | 151 | self.x = self.x + self.xi |
165 | self.x = self.x + 1 -- We do this because we want to stay centered | 152 | self.y = self.y + self.yi |
166 | self.y = self.y + 1 | ||
167 | elseif self.size < self.explosion_size then | ||
168 | self.size = self.size + 2 | ||
169 | self.x = self.x - 1 -- We do this for the same reasons as above | ||
170 | self.y = self.y - 1 | ||
171 | end | ||
172 | end | ||
173 | 153 | ||
174 | function Ball:step() | 154 | if (self.x <= 0 or self.x >= (LCD_W - self.sz)) then |
175 | self.x = self.x + self.right_speed | 155 | self.xi = -self.xi |
176 | self.y = self.y + self.up_speed | 156 | self.x = self.x + self.xi |
177 | if (self.x <= 0 or self.x >= rb.LCD_WIDTH - self.size) then | ||
178 | self.right_speed = -self.right_speed | ||
179 | self.x = self.x + self.right_speed | ||
180 | end | 157 | end |
181 | if (self.y <= 0 or self.y >= rb.LCD_HEIGHT - self.size ) then | 158 | |
182 | self.up_speed = -self.up_speed | 159 | if (self.y <= 0 or self.y >= (LCD_H - self.sz)) then |
183 | self.y = self.y + self.up_speed | 160 | self.yi = -self.yi |
161 | self.y = self.y + self.yi | ||
184 | end | 162 | end |
185 | end | 163 | end |
186 | 164 | ||
187 | function Ball:checkHit(other) | 165 | function Ball:checkHit(other) |
188 | if (other.x + other.size >= self.x) and (self.x + self.size >= other.x) and | 166 | if (other.xi >= self.x) and (other.yi >= self.y) and |
189 | (other.y + other.size >= self.y) and (self.y + self.size >= other.y) then | 167 | (self.x + self.sz >= other.x) and (self.y + self.sz >= other.y) then |
190 | assert(not self.exploded) | 168 | self.state = B_EXPLODE |
191 | self.exploded = true | 169 | -- x/y increment no longer needed it is now impact region |
192 | self.death_time = rb.current_tick() + self.life_duration | 170 | self.xi = self.x + self.sz |
193 | if not other.exploded then | 171 | self.yi = self.y + self.sz |
194 | other.exploded = true | 172 | |
195 | other.death_time = rb.current_tick() + other.life_duration | 173 | if other.state < B_EXPLODE then -- add duration to the ball that got hit |
174 | other.next_tick = other.next_tick + self.life_ticks | ||
196 | end | 175 | end |
197 | return true | 176 | return true |
198 | end | 177 | end |
@@ -200,236 +179,332 @@ function Ball:checkHit(other) | |||
200 | return false | 179 | return false |
201 | end | 180 | end |
202 | 181 | ||
182 | function Ball:draw_exploded() | ||
183 | _ellipse(_LCD, self.x, self.y, self.xi, self.yi, self.color, nil, true) | ||
184 | end | ||
185 | |||
186 | function Ball:step_exploded(tick) | ||
187 | -- exploding ball state machine | ||
188 | -- B_EXPLODE >> B_DIE >> BWAIT >> B_IMPLODE >> B_DEAD | ||
189 | if self.state == B_EXPLODE and self.sz < self.explosion_sz then | ||
190 | self.sz = self.sz + 2 | ||
191 | self.x = self.x - 1 -- We do this because we want to stay centered | ||
192 | self.y = self.y - 1 | ||
193 | elseif self.state == B_DIE then | ||
194 | self.state = B_WAIT | ||
195 | self.next_tick = tick + self.life_ticks | ||
196 | return | ||
197 | elseif self.state == B_IMPLODE and self.sz > 0 then | ||
198 | self.sz = self.sz - 2 | ||
199 | self.x = self.x + 1 -- We do this because we want to stay centered | ||
200 | self.y = self.y + 1 | ||
201 | elseif self.state <= B_IMPLODE then | ||
202 | self.state = B_DEAD | ||
203 | return | ||
204 | elseif self.next_tick < tick then | ||
205 | self.state = self.state - 1 | ||
206 | return | ||
207 | end | ||
208 | -- fell through, update next_tick and impact region | ||
209 | self.next_tick = tick + self.step_delay | ||
210 | self.xi = self.x + self.sz | ||
211 | self.yi = self.y + self.sz | ||
212 | end | ||
213 | |||
203 | local Cursor = { | 214 | local Cursor = { |
204 | size = DEFAULT_BALL_SIZE*2, | 215 | sz = (DEFAULT_BALL_SZ * 2), |
205 | x = rb.LCD_WIDTH/2, | 216 | x = (LCD_W / 2), |
206 | y = rb.LCD_HEIGHT/2, | 217 | y = (LCD_H / 2), |
207 | image = create_cursor(DEFAULT_BALL_SIZE*2) | 218 | color = DEFAULT_FG_CLR |
208 | } | 219 | } |
209 | 220 | ||
210 | function Cursor:new() | 221 | function Cursor:new() |
222 | if rb.LCD_DEPTH == 2 then -- invert for 2 - bit screens | ||
223 | self.color = 3 - DEFAULT_FG_CLR | ||
224 | end | ||
225 | self:create_image(DEFAULT_BALL_SZ * 2) | ||
211 | return self | 226 | return self |
212 | end | 227 | end |
213 | 228 | ||
229 | function Cursor:create_image(sz) | ||
230 | if not HAS_TOUCHSCREEN then | ||
231 | sz = sz + 1 | ||
232 | local img = rb.new_image(sz, sz) | ||
233 | local sz2 = (sz / 2) + 1 | ||
234 | local sz4 = (sz / 4) | ||
235 | |||
236 | img:clear(0) | ||
237 | img:line(1, 1, sz4 + 1, 1, 1) | ||
238 | img:line(1, 1, 1, sz4 + 1, 1) | ||
239 | |||
240 | img:line(1, sz, sz4 + 1, sz, 1) | ||
241 | img:line(1, sz, 1, sz - sz4, 1) | ||
242 | |||
243 | img:line(sz, sz, sz - sz4, sz, 1) | ||
244 | img:line(sz, sz, sz, sz - sz4, 1) | ||
245 | |||
246 | img:line(sz, 1, sz - sz4, 1, 1) | ||
247 | img:line(sz, 1, sz, sz4 + 1, 1) | ||
248 | |||
249 | -- crosshairs | ||
250 | img:line(sz2 - sz4, sz2, sz2 + sz4, sz2, 1) | ||
251 | img:line(sz2, sz2 - sz4, sz2, sz2 + sz4, 1) | ||
252 | self.image = img | ||
253 | end | ||
254 | end | ||
255 | |||
256 | local function clamp_roll(iVal, iMin, iMax) | ||
257 | if iVal < iMin then | ||
258 | iVal = iMax | ||
259 | elseif iVal > iMax then | ||
260 | iVal = iMin | ||
261 | end | ||
262 | |||
263 | return iVal | ||
264 | end | ||
265 | |||
214 | function Cursor:do_action(action) | 266 | function Cursor:do_action(action) |
215 | if action == actions_pla.ACTION_TOUCHSCREEN and HAS_TOUCHSCREEN then | 267 | local xi, yi = 0, 0 |
268 | |||
269 | if HAS_TOUCHSCREEN and action == pla.ACTION_TOUCHSCREEN then | ||
216 | _, self.x, self.y = rb.action_get_touchscreen_press() | 270 | _, self.x, self.y = rb.action_get_touchscreen_press() |
217 | return true | 271 | return true |
218 | elseif action == actions_pla.PLA_SELECT then | 272 | elseif action == pla.PLA_SELECT then |
219 | return true | 273 | return true |
220 | elseif (action == actions_pla.PLA_RIGHT or action == actions_pla.PLA_RIGHT_REPEAT) then | 274 | elseif (action == pla.PLA_RIGHT or action == pla.PLA_RIGHT_REPEAT) then |
221 | self.x = self.x + self.size | 275 | xi = self.sz |
222 | elseif (action == actions_pla.PLA_LEFT or action == actions_pla.PLA_LEFT_REPEAT) then | 276 | elseif (action == pla.PLA_LEFT or action == pla.PLA_LEFT_REPEAT) then |
223 | self.x = self.x - self.size | 277 | xi = -self.sz |
224 | elseif (action == actions_pla.PLA_UP or action == actions_pla.PLA_UP_REPEAT) then | 278 | elseif (action == pla.PLA_UP or action == pla.PLA_UP_REPEAT) then |
225 | self.y = self.y - self.size | 279 | yi = -self.sz |
226 | elseif (action == actions_pla.PLA_DOWN or action == actions_pla.PLA_DOWN_REPEAT) then | 280 | elseif (action == pla.PLA_DOWN or action == pla.PLA_DOWN_REPEAT) then |
227 | self.y = self.y + self.size | 281 | yi = self.sz |
228 | end | 282 | end |
229 | 283 | ||
230 | if self.x > rb.LCD_WIDTH then | 284 | self.x = clamp_roll(self.x + xi, 1, LCD_W - self.sz) |
231 | self.x = 0 | 285 | self.y = clamp_roll(self.y + yi, 1, LCD_H - self.sz) |
232 | elseif self.x < 0 then | ||
233 | self.x = rb.LCD_WIDTH | ||
234 | end | ||
235 | |||
236 | if self.y > rb.LCD_HEIGHT then | ||
237 | self.y = 0 | ||
238 | elseif self.y < 0 then | ||
239 | self.y = rb.LCD_HEIGHT | ||
240 | end | ||
241 | 286 | ||
242 | return false | 287 | return false |
243 | end | 288 | end |
244 | 289 | ||
245 | function Cursor:draw() | 290 | function Cursor:draw() |
291 | rocklib_image.copy(_LCD, self.image, self.x, self.y, _NIL, _NIL, | ||
292 | _NIL, _NIL, true, BSAND, self.color) | ||
293 | end | ||
246 | 294 | ||
247 | rocklib_image.copy(_LCD, self.image, self.x - self.size/2, self.y - self.size/2, | 295 | local function calc_score(total, level, goal, expended) |
248 | _NIL, _NIL, _NIL, _NIL, true, BSAND, DEFAULT_FOREGROUND_COLOR) | 296 | local score = (expended * level) * 100 |
297 | if expended < goal then | ||
298 | score = -(score + (level * 100)) | ||
299 | end | ||
300 | total = total + score | ||
301 | return total | ||
249 | end | 302 | end |
250 | 303 | ||
251 | function draw_positioned_string(bottom, right, str) | 304 | local function draw_pos_str(bottom, right, str) |
252 | local _, w, h = rb.font_getstringsize(str, rb.FONT_UI) | 305 | local w, h = getstringsize(str) |
253 | local x = not right or (rb.LCD_WIDTH-w)*right - 1 | 306 | local x = (right > 0) and ((LCD_W - w) * right - 1) or 1 |
254 | local y = not bottom or (rb.LCD_HEIGHT-h)*bottom - 1 | 307 | local y = (bottom > 0) and ((LCD_H - h) * bottom - 1) or 1 |
255 | rb.lcd_putsxy(x, y, str) | 308 | rb.lcd_putsxy(x, y, str) |
256 | end | 309 | end |
257 | 310 | ||
258 | function set_foreground(color) | 311 | local function wait_anykey(to_secs) |
259 | if rb.lcd_set_foreground ~= nil then | 312 | rb.sleep(rb.HZ / 2) |
260 | rb.lcd_set_foreground(color) | 313 | rb.button_clear_queue() |
261 | end | 314 | rb.button_get_w_tmo(rb.HZ * to_secs) |
262 | end | 315 | end |
263 | 316 | ||
264 | function random_color() | 317 | local function start_round(level, goal, nrBalls, total) |
265 | if rb.lcd_rgbpack ~= nil then --color target | 318 | local player_added, score = false, 0 |
266 | return rb.lcd_rgbpack(math.random(1,255), math.random(1,255), math.random(1,255)) | 319 | local last_expend, nrBalls_expend = 0, 0 |
320 | local balls_exploded = 1 -- keep looping when player_added == false | ||
321 | local action = 0 | ||
322 | local Balls = {} | ||
323 | local str_level = string.format(FMT_LEVEL, level) -- static | ||
324 | local str_totpts = string.format(FMT_TOTPTS, total) -- static | ||
325 | local str_expend, str_lvlpts | ||
326 | local tick, cursor | ||
327 | local test_spd = false | ||
328 | |||
329 | local function update_stats() | ||
330 | -- we only create a new string when a hit is detected | ||
331 | str_expend = string.format(FMT_EXPEND, nrBalls_expend) | ||
332 | str_lvlpts = string.format(FMT_LVPTS, score) | ||
267 | end | 333 | end |
268 | 334 | ||
269 | return math.random(1, rb.LCD_DEPTH) | 335 | local function draw_stats() |
270 | end | 336 | draw_pos_str(0, 0, str_expend) |
337 | draw_pos_str(0, 1, str_level) | ||
338 | draw_pos_str(1, 1, str_lvlpts) | ||
339 | draw_pos_str(1, 0, str_totpts) | ||
340 | end | ||
271 | 341 | ||
272 | function start_round(level, goal, nrBalls, total) | 342 | local function add_player() |
273 | local player_added, score, exit, nrExpendedBalls = false, 0, false, 0 | 343 | -- cursor becomes exploded ball |
274 | local Balls, explodedBalls = {}, {} | 344 | local player = Ball:new({ |
275 | local ball_ct, ball_el | 345 | x = cursor.x, |
276 | local tick, endtick | 346 | y = cursor.y, |
277 | local action = 0 | 347 | color = cursor.color, |
278 | local hit_detected = false | 348 | step_delay = 3, |
279 | local cursor = Cursor:new() | 349 | explosion_sz = (3 * DEFAULT_BALL_SZ), |
350 | life_ticks = (test_time == true) and (100) or | ||
351 | irand(rb.HZ * 2, rb.HZ * DEFAULT_BALL_SZ), | ||
352 | sz = 10, | ||
353 | state = B_EXPLODE | ||
354 | }) | ||
355 | -- set x/y impact region | ||
356 | player.xi = player.x + player.sz | ||
357 | player.yi = player.y + player.sz | ||
358 | |||
359 | table.insert(Balls, player) | ||
360 | balls_exploded = 1 | ||
361 | player_added = true | ||
362 | cursor = nil | ||
363 | end | ||
280 | 364 | ||
281 | -- Initialize the balls | 365 | if level < 1 then |
282 | for _=1,nrBalls do | 366 | -- check speed of target |
283 | table.insert(Balls, Ball:new(nil, level)) | 367 | set_foreground(DEFAULT_BG_CLR) --hide text during test |
368 | local bkcolor = (rb.LCD_DEPTH == 2) and (3) or 0 | ||
369 | level = 1 | ||
370 | nrBalls = 20 | ||
371 | cursor = { x = LCD_W * 2, y = LCD_H * 2, color = bkcolor} | ||
372 | table.insert(Balls, Ball:new({ | ||
373 | x = 1, y = 1, xi = 1, yi = 1, | ||
374 | color = bkcolor, step_delay = 1, | ||
375 | explosion_sz = 0, life_ticks = 0, | ||
376 | step = function() test_spd = test_spd + 1 end | ||
377 | }) | ||
378 | ) | ||
379 | add_player() | ||
380 | test_spd = 0 | ||
381 | else | ||
382 | set_foreground(DEFAULT_FG_CLR) -- color for text | ||
383 | cursor = Cursor:new() | ||
284 | end | 384 | end |
285 | 385 | ||
286 | local function draw_stats() | 386 | -- Initialize the balls |
287 | draw_positioned_string(0, 0, string.format("%d balls expended", nrExpendedBalls)) | 387 | for i=1, nrBalls do |
288 | draw_positioned_string(0, 1, string.format("Level %d", level)) | 388 | table.insert(Balls, Ball:new(nil, level)) |
289 | draw_positioned_string(1, 1, string.format("%d level points", score)) | ||
290 | draw_positioned_string(1, 0, string.format("%d total points", total + score)) | ||
291 | end | 389 | end |
292 | 390 | ||
293 | -- Make sure there are no unwanted touchscreen presses | 391 | -- Make sure there are no unwanted touchscreen presses |
294 | rb.button_clear_queue() | 392 | rb.button_clear_queue() |
295 | 393 | ||
296 | set_foreground(DEFAULT_FOREGROUND_COLOR) -- color for text | 394 | update_stats() -- load status strings |
297 | |||
298 | while true do | ||
299 | endtick = rb.current_tick() + CYCLETIME | ||
300 | 395 | ||
301 | -- Check if the round is over | 396 | -- Check if the round is over |
302 | if player_added and #explodedBalls == 0 then | 397 | while balls_exploded > 0 do |
303 | break | 398 | tick = rb.current_tick() |
304 | end | ||
305 | 399 | ||
306 | if action == actions_pla.PLA_EXIT or action == actions_pla.PLA_CANCEL then | 400 | if action ~= pla.ACTION_NONE and (action == pla.PLA_EXIT or |
307 | exit = true | 401 | action == pla.PLA_CANCEL) then |
402 | action = pla.PLA_EXIT | ||
308 | break | 403 | break |
309 | end | 404 | end |
310 | 405 | ||
311 | if not player_added and cursor:do_action(action) then | 406 | rb.lcd_clear_display() |
312 | local player = Ball:new({ | ||
313 | x = cursor.x, | ||
314 | y = cursor.y, | ||
315 | color = DEFAULT_FOREGROUND_COLOR, | ||
316 | size = 10, | ||
317 | explosion_size = 3*DEFAULT_BALL_SIZE, | ||
318 | exploded = true, | ||
319 | death_time = rb.current_tick() + rb.HZ * 3 | ||
320 | }) | ||
321 | explodedBalls[1] = player | ||
322 | player_added = true | ||
323 | cursor = nil | ||
324 | end | ||
325 | 407 | ||
326 | -- Check for hits | 408 | if not player_added then |
327 | for i, Ball in ipairs(Balls) do | 409 | if action ~= pla.ACTION_NONE and cursor:do_action(action) then |
328 | for _, explodedBall in ipairs(explodedBalls) do | 410 | add_player() |
329 | if Ball:checkHit(explodedBall) then | 411 | elseif not HAS_TOUCHSCREEN then |
330 | explodedBalls[#explodedBalls + 1] = Ball | 412 | cursor:draw() |
331 | --table.remove(Balls, i) | ||
332 | Balls[i] = false | ||
333 | hit_detected = true | ||
334 | break | ||
335 | end | ||
336 | end | 413 | end |
337 | end | 414 | end |
338 | 415 | ||
339 | -- Check if we're dead yet | 416 | for _, Ball in ipairs(Balls) do |
340 | tick = rb.current_tick() | 417 | if Ball.state == B_MOVE then |
341 | for i, explodedBall in ipairs(explodedBalls) do | 418 | if tick > Ball.next_tick then |
342 | if explodedBall.death_time < tick then | 419 | Ball:step(tick) |
343 | if explodedBall.size > 0 then | 420 | for i = #Balls, 1, -1 do |
344 | explodedBall.implosion = true -- We should be dying | 421 | if Balls[i].state < B_MOVE and |
345 | else | 422 | Ball:checkHit(Balls[i]) then -- exploded? |
346 | table.remove(explodedBalls, i) -- We're imploded! | 423 | balls_exploded = balls_exploded + 1 |
424 | nrBalls_expend = nrBalls_expend + 1 | ||
425 | break | ||
426 | end | ||
427 | end | ||
347 | end | 428 | end |
348 | end | 429 | -- check if state changed draw ball if still moving |
349 | end | 430 | if Ball.state == B_MOVE then |
350 | 431 | Ball:draw() | |
351 | -- Drawing phase | 432 | end |
352 | if hit_detected then | 433 | elseif Ball.state ~= B_DEAD then |
353 | hit_detected = false | 434 | if tick > Ball.next_tick then |
354 | -- same as table.remove(Balls, i) but more efficient | 435 | Ball:step_exploded(tick) |
355 | ball_el = 1 | 436 | end |
356 | ball_ct = #Balls | 437 | if Ball.state ~= B_DEAD then |
357 | for i = 1, ball_ct do | 438 | Ball:draw_exploded() |
358 | if Balls[i] then | 439 | else |
359 | Balls[ball_el] = Balls[i] | 440 | balls_exploded = balls_exploded - 1 |
360 | ball_el = ball_el + 1 | ||
361 | end | 441 | end |
362 | end | 442 | end |
363 | -- remove any remaining exploded balls | ||
364 | for i = ball_el, ball_ct do | ||
365 | Balls[i] = nil | ||
366 | end | ||
367 | -- Calculate score | ||
368 | nrExpendedBalls = nrBalls - ball_el + 1 | ||
369 | score = nrExpendedBalls * level * 100 | ||
370 | end | 443 | end |
371 | 444 | ||
372 | rb.lcd_clear_display() | 445 | draw_stats() -- stats redrawn every frame |
373 | draw_stats() | 446 | rb.lcd_update() -- Push framebuffer to the LCD |
374 | 447 | ||
375 | if not (player_added or HAS_TOUCHSCREEN) then | 448 | if nrBalls_expend ~= last_expend then -- hit detected? |
376 | cursor:draw() | 449 | last_expend = nrBalls_expend |
450 | score = (nrBalls_expend * level) * 100 | ||
451 | update_stats() -- only update stats when not current | ||
452 | if nrBalls_expend == nrBalls then break end -- round is over? | ||
377 | end | 453 | end |
378 | 454 | ||
379 | for _, Ball in ipairs(Balls) do | 455 | rb.yield() -- yield to other tasks |
380 | Ball:step() | ||
381 | Ball:draw() | ||
382 | end | ||
383 | 456 | ||
384 | for _, explodedBall in ipairs(explodedBalls) do | 457 | action = rb.get_plugin_action(0) -- Check for actions |
385 | explodedBall:step_exploded() | 458 | end |
386 | explodedBall:draw_exploded() | ||
387 | end | ||
388 | 459 | ||
389 | -- Push framebuffer to the LCD | 460 | if test_spd and test_spd > 0 then return test_spd end |
390 | rb.lcd_update() | ||
391 | 461 | ||
392 | -- Check for actions | 462 | -- splash the final stats for a moment at end |
393 | if rb.current_tick() < endtick then | 463 | rb.lcd_clear_display() |
394 | action = rb.get_plugin_action(endtick - rb.current_tick()) | 464 | for _, Ball in ipairs(Balls) do |
395 | else | 465 | -- move balls back to their initial exploded positions |
396 | rb.yield() | 466 | if Ball.state == B_DEAD then |
397 | action = rb.get_plugin_action(0) | 467 | Ball.sz = Ball.explosion_sz + 2 |
468 | Ball.x = Ball.x - (Ball.explosion_sz / 2) | ||
469 | Ball.y = Ball.y - (Ball.explosion_sz / 2) | ||
398 | end | 470 | end |
471 | Ball:draw() | ||
472 | end | ||
399 | 473 | ||
474 | if DEFAULT_BALL_SZ > 3 then | ||
475 | _LCD:clear(nil, nil, nil, nil, nil, nil, 2, 2) | ||
400 | end | 476 | end |
401 | 477 | ||
402 | --splash the final stats for a moment at end | 478 | total = calc_score(total, level, goal, nrBalls_expend) |
403 | rb.lcd_clear_display() | 479 | str_totpts = string.format(FMT_TOTPTS, total) |
404 | for _, Ball in ipairs(Balls) do Ball:draw() end | ||
405 | _LCD:clear(nil, nil, nil, nil, nil, nil, 2, 2) | ||
406 | draw_stats() | 480 | draw_stats() |
407 | rb.lcd_update() | 481 | rb.lcd_update() |
408 | rb.sleep(rb.HZ * 2) | 482 | wait_anykey(2) |
409 | 483 | ||
410 | return exit, score, nrExpendedBalls | 484 | return action == pla.PLA_EXIT, score, nrBalls_expend |
411 | end | 485 | end |
412 | 486 | ||
413 | -- Helper function to display a message | 487 | -- Helper function to display a message |
414 | function display_message(to, ...) | 488 | local function disp_msg(to, ...) |
415 | local message = string.format(...) | 489 | local message = string.format(...) |
416 | local _, w, h = rb.font_getstringsize(message, rb.FONT_UI) | 490 | local w, h = getstringsize(message) |
417 | local x, y = (rb.LCD_WIDTH - w) / 2, (rb.LCD_HEIGHT - h) / 2 | 491 | local x, y = (LCD_W - w) / 2, (LCD_H - h) / 2 |
418 | 492 | ||
419 | rb.lcd_clear_display() | 493 | rb.lcd_clear_display() |
420 | set_foreground(DEFAULT_FOREGROUND_COLOR) | 494 | set_foreground(DEFAULT_FG_CLR) |
421 | 495 | ||
422 | if w > rb.LCD_WIDTH then | 496 | if w > LCD_W then |
423 | rb.lcd_puts_scroll(0, y/h, message) | 497 | rb.lcd_puts_scroll(0, (y / h), message) |
424 | else | 498 | else |
425 | rb.lcd_putsxy(x, y, message) | 499 | rb.lcd_putsxy(x, y, message) |
426 | end | 500 | end |
501 | |||
427 | if to == -1 then | 502 | if to == -1 then |
428 | local msg = "Press button to exit" | 503 | local msg = "Press button to exit" |
429 | w = rb.font_getstringsize(msg, rb.FONT_UI) | 504 | w, h = getstringsize(msg) |
430 | x = (rb.LCD_WIDTH - w) / 2 | 505 | x = (LCD_W - w) / 2 |
431 | if x < 0 then | 506 | if x < 0 then |
432 | rb.lcd_puts_scroll(0, y/h + 1, msg) | 507 | rb.lcd_puts_scroll(0, (y / h) + 1, msg) |
433 | else | 508 | else |
434 | rb.lcd_putsxy(x, y + h, msg) | 509 | rb.lcd_putsxy(x, y + h, msg) |
435 | end | 510 | end |
@@ -437,9 +512,7 @@ function display_message(to, ...) | |||
437 | rb.lcd_update() | 512 | rb.lcd_update() |
438 | 513 | ||
439 | if to == -1 then | 514 | if to == -1 then |
440 | rb.sleep(rb.HZ/2) | 515 | wait_anykey(60) |
441 | rb.button_clear_queue() | ||
442 | rb.button_get(rb.HZ * 60) | ||
443 | else | 516 | else |
444 | rb.sleep(to) | 517 | rb.sleep(to) |
445 | end | 518 | end |
@@ -447,47 +520,77 @@ function display_message(to, ...) | |||
447 | rb.lcd_scroll_stop() -- Stop our scrolling message | 520 | rb.lcd_scroll_stop() -- Stop our scrolling message |
448 | end | 521 | end |
449 | 522 | ||
523 | --[[MAIN PROGRAM]] | ||
524 | do -- attempt to get stats to fit on screen | ||
525 | local function getwidth(str) | ||
526 | local w, _ = getstringsize(str) | ||
527 | return w | ||
528 | end | ||
529 | local w0, w1 = getwidth(FMT_EXPEND), getwidth(FMT_LEVEL) | ||
530 | if (w0 + w1) > LCD_W then | ||
531 | FMT_EXPEND, FMT_LEVEL = "%d balls", "Lv %d" | ||
532 | end | ||
533 | w0, w1 = getwidth(FMT_TOTPTS), getwidth(FMT_LVPTS) | ||
534 | if (w0 + w1 + getwidth("0000000")) > LCD_W then | ||
535 | FMT_TOTPTS, FMT_LVPTS = "%d total", "%d lv" | ||
536 | end | ||
537 | end | ||
538 | |||
450 | if HAS_TOUCHSCREEN then | 539 | if HAS_TOUCHSCREEN then |
451 | rb.touchscreen_set_mode(rb.TOUCHSCREEN_POINT) | 540 | rb.touchscreen_set_mode(rb.TOUCHSCREEN_POINT) |
452 | end | 541 | end |
453 | 542 | ||
454 | --[[MAIN PROGRAM]] | ||
455 | rb.backlight_force_on() | 543 | rb.backlight_force_on() |
456 | 544 | ||
457 | math.randomseed(os.time()) | 545 | math.randomseed(os.time()) |
458 | 546 | ||
459 | local idx, highscore = 1, 0 | 547 | local idx, highscore = 1, 0 |
548 | |||
549 | local test_spd = start_round(0, 0, 0, 0) -- test speed of target | ||
550 | |||
551 | if test_spd < 100 then | ||
552 | rb.splash(100, "Slow Target..") | ||
553 | |||
554 | if test_spd < 25 then | ||
555 | MAX_BALL_SPEED = MAX_BALL_SPEED + MAX_BALL_SPEED | ||
556 | elseif test_spd < 50 then | ||
557 | MAX_BALL_SPEED = MAX_BALL_SPEED + MAX_BALL_SPEED / 2 | ||
558 | elseif test_spd < 100 then | ||
559 | MAX_BALL_SPEED = MAX_BALL_SPEED + MAX_BALL_SPEED / 4 | ||
560 | end | ||
561 | end | ||
562 | |||
460 | while levels[idx] ~= nil do | 563 | while levels[idx] ~= nil do |
461 | local goal, nrBalls = levels[idx][1], levels[idx][2] | 564 | local goal, nrBalls = levels[idx][1], levels[idx][2] |
462 | 565 | ||
463 | collectgarbage("collect") --run gc now to hopefully prevent interruption later | 566 | collectgarbage("collect") -- run gc now to hopefully prevent interruption later |
464 | 567 | ||
465 | display_message(rb.HZ*2, "Level %d: get %d out of %d balls", idx, goal, nrBalls) | 568 | disp_msg(rb.HZ * 2, "Level %d: get %d out of %d balls", idx, goal, nrBalls) |
466 | 569 | ||
467 | local exit, score, nrExpendedBalls = start_round(idx, goal, nrBalls, highscore) | 570 | local exit, score, nrBalls_expend = start_round(idx, goal, nrBalls, highscore) |
468 | 571 | ||
469 | if exit then | 572 | if exit then |
470 | break -- Exiting.. | 573 | break -- Exiting.. |
471 | else | 574 | else |
472 | if nrExpendedBalls >= goal then | 575 | highscore = calc_score(highscore, idx, goal, nrBalls_expend) |
473 | display_message(rb.HZ*2, "You won!") | 576 | if nrBalls_expend >= goal then |
577 | disp_msg(rb.HZ * 2, "You won!") | ||
578 | levels[idx] = nil | ||
474 | idx = idx + 1 | 579 | idx = idx + 1 |
475 | highscore = highscore + score | ||
476 | else | 580 | else |
477 | display_message(rb.HZ*2, "You lost!") | 581 | disp_msg(rb.HZ * 2, "You lost %d points!", score + (idx * 100)) |
478 | highscore = highscore - score - idx * 100 | ||
479 | if highscore < 0 then break end | 582 | if highscore < 0 then break end |
480 | end | 583 | end |
481 | end | 584 | end |
482 | end | 585 | end |
586 | |||
483 | if highscore <= 0 then | 587 | if highscore <= 0 then |
484 | display_message(-1, "You lost at level %d", idx) | 588 | disp_msg(-1, "You lost at level %d", idx) |
485 | elseif idx > #levels then | 589 | elseif idx > #levels then |
486 | display_message(-1, "You finished the game with %d points!", highscore) | 590 | disp_msg(-1, "You finished the game with %d points!", highscore) |
487 | else | 591 | else |
488 | display_message(-1, "You made it till level %d with %d points!", idx, highscore) | 592 | disp_msg(-1, "You made it till level %d with %d points!", idx, highscore) |
489 | end | 593 | end |
490 | 594 | ||
491 | -- Restore user backlight settings | 595 | -- Restore user backlight settings |
492 | rb.backlight_use_settings() | 596 | rb.backlight_use_settings() |
493 | |||