0,]']>S}Wup8d%qy)6o;]AsVl3=؍88 ݌  "" """""""!"" "   ݌ݍ݈ݍ      ,"""" ``VUUeffff , NLKKLOOOOOOOOOOOOOOOOOOOOOOOOOc    d888888899>????????????????????HHHHHHOOOOOOOOOOOOOOOOOOOOOOOO  P0?????????????????????????????e !#&'),/////////////// 0000000 1 1 1 1RRR R P P@PP P@P2P$ ; Pf  ef  Pff (eff ) Ɍ̬*"3 3 "3 ̜  ̜         ݐ ݪ ̠ ݽ ݺ Н Эڽ , Р нн ڽ۽ڭݽݭ """"  Ш ݭڭݭнڭ   ݽ۽۽Э ݭ  ݽ  ݰ   ݠ    ݌ ݌   ݍݍ ݈  + `   +; ` "" *"  VUUe *""3 ffff   """" *   "!"     " "      ݠ  :   2  ":8 " ":3*:   8 *8   : *3                 , """"               2 2  "23  P "23 e 2Pf 2 ef Pff      P   e2   Pf 2 ef"23  Pff"23 P  %2 e Pf2  Pfef 2 efPffY 2Pffe "23Pi "23hf (2Pf   ej     "3  "3 "3 "3  2 2  "23 "23   2"3 2 " 3 "3  D 0" D̠   HD9"3 P 233>  "#32)#2e&22 * 2 2   = #"33#2#")Rh)23"33 *32"23"3"3""3؍83"33"2#)ef&233*"3)"3 ""23"23"33 " "383"33"2#"Zff&233*"3Ù)3%*323,2#"9#,,"3̙̙#"33 2#"*hf23"3"3Yf3*22"33 ""3 "#2)#Zff&22 *ef) 2 *       `` )    ``   ,,VUUeVUUeŒ,, """"""""ffffffff""""""""   p-- title: HEAPS LLC -- author: OsloOMA -- desc: An incremental game about trash disposal -- script: lua -- -- ASSET & FLAG CONSTANTS -- local SPRITE_BULLDOZER=16; local SPRITE_CANNON_BASE=17; local SPRITE_CANNON_TURRET=18 local SPRITE_HELI_BODY=19; local SPRITE_HELI_ROTOR=21; local SPRITE_MAGNET=23 local SPRITE_TRASH_RED=5; local SPRITE_TRASH_BLUE=6; local SPRITE_TRASH_GREEN=7 -- (v0.9.8): Set firework sprites to user's 16x16 frames at 24, 26, 28 local SPRITE_FIREWORK_1=24; local SPRITE_FIREWORK_2=26; local SPRITE_FIREWORK_3=28 local SFX_SCORE=0; local SFX_UPGRADE=1; local SFX_CANNON_FIRE=2 local SFX_MENU_TOGGLE=3; local SFX_MENU_NAV=4 local SFX_MAGNET_ACTIVATE=5; local SFX_MAGNET_DEACTIVATE=6 local FLAG_SOLID_GROUND=0; local FLAG_TARGET_RED=1 local FLAG_TARGET_BLUE=2; local FLAG_TARGET_GREEN=3 local TRANSPARENT_COLOR = 0 -- -- GAME CONSTANTS -- GRAVITY=0.08; SPAWN_INTERVAL=4 HELI_THRUST_FORCE=0.22 HELI_TILT_FORCE=0.2 HELI_MAX_VX=1.5 MAGNET_PULL_RADIUS_SQ=400 MAGNET_PULL_FORCE = 0.4 local MAX_PARTICLES=100 local BOUNCE_DAMPENING=0.5; local BULLDOZER_INITIAL_SPEED=1.5 local SPAWN_X_LIMIT=160; local MAX_FX_PARTICLES=200 local SCORE_SHAKE_AMOUNT=2; local CANNON_SUCK_RADIUS_SQ=900 local CANNON_BASE_COOLDOWN=120 local CANNON_FIRE_VELOCITY_Y=-3.5 local CANNON_FIRE_VELOCITY_X_MULT=0.05 local CANNON_SPREAD=0.5 local HELI_DAMPENING=0.98 local MAX_MENU_ITEMS_DISPLAYED = 5 TRASH_VALUE_MULTIPLIER = 1.0 next_firework_milestone = 1000 -- -- GAME STATE & OBJECTS -- -- (v0.9.5): Added new states for the menu system local STATE_INTRO=0; local STATE_TITLE=1; local STATE_PLAY=2; local STATE_PAUSE=3; local STATE_MENU=4; local STATE_HOW_TO_PLAY=5 local particles,fx_particles={},{}; local timer,score,screen_shake=0,0,0 local camera={x=0}; local world_bounds={left=0,right=240} local all_targets={}; local cannon_update_idx = 1 -- FIX (v1.0.0): Added variables for multi-burst firework show local firework_show_active = false local firework_burst_count = 0 local firework_burst_timer = 0 local heli={x=120,y=60,w=16,h=16, full_h=24, vx=0,vy=0, magnet_active=false, rotor_frame=0} local bulldozer={x=120,y=104,w=16,h=8,speed=BULLDOZER_INITIAL_SPEED} local cannons={}; local upgrades={ bulldozer_speed={level=1,cost=50,name="BULLDOZER SPEED"}, heli_speed={level=1, cost=250, name="HELI SPEED"}, magnet_strength={level=1, cost=300, name="MAGNET STRENGTH"}, trash_frequency={level=1, cost=500, name="TRASH FREQUENCY"}, cannon_hire={level=0,cost=200,name="HIRE CANNON"}, cannon_rate={level=1,cost=400,name="CANNON FIRE RATE"}, cannon_capacity={level=1,cost=800,name="CANNON CAPACITY"}, trash_value={level=1, cost=1000, name="TRASH VALUE"}, map={level=1,cost=500,name="EXPAND MAP"} }; local menu_selection=1 -- -- STATE MANAGER -- StateManager = { activeState = nil, change = function(self, newState, params) if self.activeState and self.activeState.exit then self.activeState:exit() end self.activeState = newState if self.activeState and self.activeState.enter then self.activeState:enter(params or {}) end end, update = function(self) if self.activeState and self.activeState.update then self.activeState:update() end end, draw = function(self) if self.activeState and self.activeState.draw then self.activeState:draw() end end } -- -- HELPER FUNCTIONS -- function is_solid(map_x, map_y) return fget(mget(map_x, map_y), FLAG_SOLID_GROUND) end -- -- INITIALIZATION -- local function find_all_targets() for y=0,16 do for x=0,59 do local id=mget(x,y) if fget(id,FLAG_TARGET_RED) or fget(id,FLAG_TARGET_BLUE) or fget(id,FLAG_TARGET_GREEN) then table.insert(all_targets,{x=x*8+4,y=y*8+4})end end end end local function init() for i=1,MAX_PARTICLES do particles[i]={active=false}end; for i=1,MAX_FX_PARTICLES do fx_particles[i]={active=false}end; find_all_targets() StateManager:change(IntroState) end -- -- PARTICLE & FX SPAWNING -- function spawn_particle(is_cannon_shot, source_particle, cannon) for i=1,MAX_PARTICLES do local p=particles[i] if not p.active then p.active=true if is_cannon_shot then p.sprite=source_particle.sprite; p.type_flag=source_particle.type_flag; p.score_value=source_particle.score_value p.x=cannon.x; p.y=cannon.y - 4 p.is_cannon_shot = true; p.life = 180 local nearest_target=nil; local min_dist_sq=999999 for _,target in ipairs(all_targets) do local dSq=(p.x-target.x)^2+(p.y-target.y)^2;if dSq HELI_MAX_VX then heli.vx = HELI_MAX_VX end if heli.vx < -HELI_MAX_VX then heli.vx = -HELI_MAX_VX end heli.x = heli.x + heli.vx; heli.y = heli.y + heli.vy if heli.x < world_bounds.left then heli.x = world_bounds.left; heli.vx = 0 end if heli.x + heli.w > world_bounds.right then heli.x = world_bounds.right - heli.w; heli.vx = 0 end if heli.y < 0 then heli.y = 0; heli.vy = 0 end if heli.y + heli.full_h > 120 then heli.y = 120 - heli.full_h; heli.vy = 0 end if btnp(4) then heli.magnet_active = not heli.magnet_active if heli.magnet_active then sfx(SFX_MAGNET_ACTIVATE) else sfx(SFX_MAGNET_DEACTIVATE) local magnet_x = heli.x + 8; local magnet_y = heli.y + 21 for i=1,MAX_PARTICLES do local p = particles[i] if p.active then local dist_sq = (p.x - magnet_x)^2 + (p.y - magnet_y)^2 if dist_sq < MAGNET_PULL_RADIUS_SQ then p.vx = 0 end end end end end heli.rotor_frame = (heli.rotor_frame + 0.25) % 2 end function update_bulldozer() bulldozer.x = bulldozer.x + bulldozer.speed if bulldozer.x < world_bounds.left then bulldozer.x = world_bounds.left; bulldozer.speed = -bulldozer.speed end if bulldozer.x + bulldozer.w > world_bounds.right then bulldozer.x = world_bounds.right - bulldozer.w; bulldozer.speed = -bulldozer.speed end end function update_particles() local magnet_x, magnet_y if heli.magnet_active then magnet_x = heli.x + 8; magnet_y = heli.y + 21 end for i=1,MAX_PARTICLES do local p=particles[i] if p.active then if heli.magnet_active then local dist_sq = (p.x - magnet_x)^2 + (p.y - magnet_y)^2 if dist_sq < MAGNET_PULL_RADIUS_SQ then local angle = math.atan2(magnet_y - p.y, magnet_x - p.x) p.vx = p.vx + math.cos(angle) * MAGNET_PULL_FORCE p.vy = p.vy + math.sin(angle) * MAGNET_PULL_FORCE end end p.vy=p.vy+GRAVITY;p.x=p.x+p.vx;p.y=p.y+p.vy local cX=math.max(bulldozer.x,math.min(p.x,bulldozer.x+bulldozer.w)) local cY=math.max(bulldozer.y,math.min(p.y,bulldozer.y+bulldozer.h)) if((p.x-cX)^2+(p.y-cY)^2)<(4*4)then p.vx=p.vx+(p.x-(bulldozer.x+bulldozer.w/2))*0.05; p.vy=-1 end local mx,my=math.floor(p.x/8),math.floor((p.y)/8) if is_solid(mx,my)then p.y=my*8;p.vy=-p.vy*BOUNCE_DAMPENING if math.abs(p.vx)>0.1 then p.vx=p.vx*0.95 else p.vx=0 end end if p.x<0 then p.x=0;p.vx=-p.vx*BOUNCE_DAMPENING end local target_tile_id=mget(math.floor(p.x/8),math.floor((p.y+4)/8)) local tile_flag_red=fget(target_tile_id,FLAG_TARGET_RED) local tile_flag_blue=fget(target_tile_id,FLAG_TARGET_BLUE) local tile_flag_green=fget(target_tile_id,FLAG_TARGET_GREEN) if tile_flag_red or tile_flag_blue or tile_flag_green then if (tile_flag_red and p.type_flag==FLAG_TARGET_RED)or (tile_flag_blue and p.type_flag==FLAG_TARGET_BLUE)or (tile_flag_green and p.type_flag==FLAG_TARGET_GREEN)then p.active=false score=score+p.score_value sfx(SFX_SCORE);screen_shake=SCORE_SHAKE_AMOUNT spawn_poof(p.x,p.y) end end if p.is_cannon_shot then p.life = p.life - 1 if p.life <= 0 then p.active = false end end if p.y > 136 then p.active = false end end end end function update_fx() for i=1,MAX_FX_PARTICLES do local p=fx_particles[i] if p.active then p.x=p.x+p.vx; p.y=p.y+p.vy local grav = GRAVITY if p.gravity_mult then grav = grav * p.gravity_mult end p.vy = p.vy + grav -- (v0.9.5): Reworked update logic for sprite-based fireworks if p.is_firework then p.anim_timer = p.anim_timer + 1 if p.anim_timer >= p.life then p.active = false end else -- Logic for poof particles p.life=p.life-1 if p.life<=0 then p.active=false end end end end end function update_cannons() if #cannons == 0 then return end local c = cannons[cannon_update_idx] c.cooldown=c.cooldown-1 if c.cooldown<=0 then local particles_to_fire={}; for j=1,MAX_PARTICLES do if #particles_to_fire>=c.capacity then break end; local p=particles[j]; if p.active and not p.is_cannon_shot then local dSq=(p.x-c.x)^2+(p.y-c.y)^2; if dSq0 then for _,p_old in ipairs(particles_to_fire)do spawn_particle(true,p_old,c); p_old.active=false end; sfx(SFX_CANNON_FIRE); c.cooldown=c.fire_rate end end cannon_update_idx = (cannon_update_idx % #cannons) + 1 end -- -- GAME STATE DEFINITIONS -- IntroState = { timer = 0, enter = function(self) self.timer = 360 for i=1,MAX_FX_PARTICLES do local p = fx_particles[i] p.active = true p.angle = math.random() * math.pi * 2 p.radius = math.random(10, 80) p.target_radius = 0 p.x = 120 + math.cos(p.angle) * p.radius p.y = 68 + math.sin(p.angle) * p.radius p.color = math.random(3,12) p.life = 999 p.gravity_mult = 0; p.is_firework = false end end, update = function(self) self.timer = self.timer - 1 if self.timer <= 0 or btnp(4) or btnp(5) then StateManager:change(TitleState) end for i=1,MAX_FX_PARTICLES do local p = fx_particles[i] if p.active and p.life == 999 then p.angle = p.angle + 0.02 -- Rotational speed p.radius = p.radius + (p.target_radius - p.radius) * 0.01 -- Pulsing speed p.x = 120 + math.cos(p.angle) * p.radius p.y = 68 + math.sin(p.angle) * p.radius if p.radius < 1 then p.radius = 80 end end end end, draw = function(self) cls(0) for i=1,MAX_FX_PARTICLES do local p = fx_particles[i] if p.active and p.life == 999 then circ(p.x, p.y, 1, p.color) end end print("A GAME BY OSLOOMA", 70, 66, 12) end, exit = function(self) for i=1,MAX_FX_PARTICLES do fx_particles[i].active = false end end } TitleState = { angle = 0, vtx = {}, tris = {}, enter = function(self) self.angle = 0 self.vtx = {} self.tris = {} self:generate_cup_model() music(0) end, -- (v0.9.6): Corrected 1-based indexing for 3D model generate_cup_model = function(self) local segments = 16 local top_radius = 15 local inner_top_radius = top_radius * 0.8 -- Rim thickness local bottom_radius = 10 local height = 30 local y_top = -height / 2 local y_bottom = height / 2 -- Add bottom center vertex (INDEX 1) table.insert(self.vtx, {x=0, y=y_bottom, z=0}) local bottom_center_idx = 1 -- Was 0, now 1-based -- Generate vertices for bottom ring and outer top ring (INDICES 2 to 1 + segments*2) for i=0, segments-1 do local angle = (i / segments) * math.pi * 2 local x = math.cos(angle) local z = math.sin(angle) -- Bottom ring vertex table.insert(self.vtx, {x = x * bottom_radius, y = y_bottom, z = z * bottom_radius}) -- Outer Top ring vertex table.insert(self.vtx, {x = x * top_radius, y = y_top, z = z * top_radius}) end -- Generate vertices for inner top ring (INDICES 2 + segments*2 to 1 + segments*4) for i=0, segments-1 do local angle = (i / segments) * math.pi * 2 local x = math.cos(angle) local z = math.sin(angle) -- Inner Top ring vertex table.insert(self.vtx, {x = x * inner_top_radius, y = y_top, z = z * inner_top_radius}) end -- Generate side, bottom, and rim faces for i=0, segments-1 do local next_i = (i + 1) % segments -- Indices for vertices (all 1-based) -- vtx layout: [1:bottom_center, 2:b0, 3:t_outer0, 4:b1, 5:t_outer1, ... ] local b1_idx = 2 + i*2 local t_outer1_idx = 2 + i*2 + 1 local b2_idx = 2 + next_i*2 local t_outer2_idx = 2 + next_i*2 + 1 -- Side faces (2 triangles) table.insert(self.tris, {b1_idx, t_outer1_idx, t_outer2_idx, 14}) table.insert(self.tris, {b1_idx, t_outer2_idx, b2_idx, 14}) -- Bottom face (1 triangle) table.insert(self.tris, {bottom_center_idx, b2_idx, b1_idx, 14}) -- Uses 1-based bottom_center_idx -- Rim faces (2 triangles) -- Find inner top indices local inner_ring_start_idx = 2 + (segments * 2) local t_inner1_idx = inner_ring_start_idx + i local t_inner2_idx = inner_ring_start_idx + next_i table.insert(self.tris, {t_outer1_idx, t_inner1_idx, t_inner2_idx, 12}) table.insert(self.tris, {t_outer1_idx, t_inner2_idx, t_outer2_idx, 12}) end end, -- (v0.9.5): Change to new MenuState update = function(self) if btnp(4) then StateManager:change(MenuState) end self.angle = (self.angle + 0.02) % (math.pi * 2) end, draw_3d_model = function(self) local projected = {} local cam_z = 100; local fov = 64 local tilt_angle = math.rad(15) local cos_z = math.cos(tilt_angle) local sin_z = math.sin(tilt_angle) for i,v in ipairs(self.vtx) do local zrx = v.x * cos_z - v.y * sin_z local zry = v.x * sin_z + v.y * cos_z local zrz = v.z local rx = zrx * math.cos(self.angle) - zrz * math.sin(self.angle) local rz = zrx * math.sin(self.angle) + zrz * math.cos(self.angle) local ry = zry local z_dist = rz + cam_z if z_dist <= 0 then z_dist = 0.1 end local scale = fov / z_dist local sx = rx * scale + 120 local sy = ry * scale + 60 projected[i] = {x=sx, y=sy, z=z_dist} end for i,t in ipairs(self.tris) do -- (v0.9.6): Accessing projected table with 1-based indices from tris table local v1 = projected[t[1]] local v2 = projected[t[2]] local v3 = projected[t[3]] local col = t[4] local cross_prod = (v2.x-v1.x)*(v3.y-v1.y) - (v2.y-v1.y)*(v3.x-v1.x) if cross_prod > 0 then tri(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y, col) end end end, -- (v0.9.5): Cleaned up text draw = function(self) cls(0) self:draw_3d_model() print("HEAPS LLC", 66, 90, 12,true,2) print("PRESS (A) TO START", 72, 105, 10) end, exit = function(self) music(-1) end } -- (v0.9.5): New game state for the main menu MenuState = { selection = 1, enter = function(self) self.selection = 1 end, update = function(self) if btnp(0) or btnp(2) then -- Up self.selection = self.selection - 1 if self.selection < 1 then self.selection = 2 end sfx(SFX_MENU_NAV) end if btnp(1) or btnp(3) then -- Down self.selection = self.selection + 1 if self.selection > 2 then self.selection = 1 end sfx(SFX_MENU_NAV) end if btnp(4) then -- (A) button if self.selection == 1 then StateManager:change(GameState) elseif self.selection == 2 then StateManager:change(HowToPlayState) end sfx(SFX_UPGRADE) end end, draw = function(self) cls(0) print("HEAPS LLC", 66, 66, 12,true,2) local col1 = 12 local col2 = 12 if self.selection == 1 then col1 = 10 else col2 = 10 end if self.selection == 1 then print("> NEW GAME", 84, 90, 12) print(" HOW TO PLAY", 84, 100, col1) else print(" NEW GAME", 84, 90, col2) print("> HOW TO PLAY", 84, 100, 12) end end, exit = function(self) end } -- (v0.9.5): New game state for instructions HowToPlayState = { enter = function(self) end, update = function(self) -- Go back to menu on (A) or (B) if btnp(4) or btnp(5) then StateManager:change(MenuState) sfx(SFX_MENU_TOGGLE) end end, draw = function(self) cls(0) print("HOW TO PLAY", 84, 30, 12, true) print("ARROW KEYS", 60, 50, 14) print("TILT HELICOPTER", 120, 50, 12) print("UP ARROW", 60, 60, 14) print("APPLY THRUST", 120, 60, 12) print("(A) BUTTON", 60, 70, 14) print("TOGGLE MAGNET", 120, 70, 12) print("(B) BUTTON", 60, 80, 14) print("PAUSE & UPGRADE", 120, 80, 12) print("PRESS (A) OR (B) TO RETURN", 48, 110, 12) end, exit = function(self) end } GameState = { sx=0, sy=0, enter = function(self) timer = 0; self.sx=0; self.sy=0 end, update = function(self) timer=timer+1; if timer%math.floor(SPAWN_INTERVAL)==0 then spawn_particle()end update_heli(); update_bulldozer(); update_particles() update_cannons(); update_fx() -- FIX (v1.0.0): Reworked milestone to trigger a 3-burst show if score >= next_firework_milestone and not firework_show_active then firework_show_active = true firework_burst_count = 3 -- Set to 3 bursts firework_burst_timer = 0 -- Start first burst immediately next_firework_milestone = next_firework_milestone + 1000 end -- FIX (v1.0.0): Handle the multi-burst firework show if firework_show_active then firework_burst_timer = firework_burst_timer - 1 if firework_burst_timer <= 0 then if firework_burst_count > 0 then -- Fire a burst! local firework_x_min = math.floor(camera.x) local firework_x_max = math.floor(camera.x + 240) spawn_firework(math.random(firework_x_min, firework_x_max), 30) spawn_firework(math.random(firework_x_min, firework_x_max), 40) spawn_firework(math.random(firework_x_min, firework_x_max), 30) sfx(SFX_CANNON_FIRE) -- Set up for next burst firework_burst_count = firework_burst_count - 1 firework_burst_timer = 45 -- Wait 30 frames for next burst else -- Show is over firework_show_active = false end end end if heli.x>camera.x+180 then camera.x=heli.x-180 elseif heli.x world_bounds.right - 240 then camera.x = world_bounds.right - 240 end if btnp(5)then StateManager:change(PauseState); sfx(SFX_MENU_TOGGLE) end if screen_shake>0 then local s=math.floor(screen_shake) self.sx,self.sy=math.random(-s,s),math.random(-s,s) screen_shake=screen_shake*0.9 if screen_shake<0.5 then screen_shake=0; self.sx=0; self.sy=0 end else self.sx,self.sy=0,0 end end, -- (v0.9.9): Consolidated ALL fx particles into ONE loop AFTER the map draw = function(self) cls();local sx,sy=self.sx,self.sy -- 1. Draw Map map(0,0,60,17,-camera.x+sx,sy) -- 2. Draw World Objects spr(SPRITE_BULLDOZER,bulldozer.x-camera.x+sx,bulldozer.y+sy,TRANSPARENT_COLOR) for i,c in ipairs(cannons)do spr(SPRITE_CANNON_BASE,c.x-camera.x+sx,c.y+sy,TRANSPARENT_COLOR) spr(SPRITE_CANNON_TURRET,c.x-camera.x+sx,c.y-4+sy,TRANSPARENT_COLOR,1,c.flip,0) end -- 3. Draw Trash Particles for i=1,MAX_PARTICLES do local p=particles[i] if p.active then spr(p.sprite,p.x-camera.x-4+sx,p.y-4+sy,TRANSPARENT_COLOR) end end -- 4. Draw ALL FX (Poofs and Fireworks) for i=1,MAX_FX_PARTICLES do local p=fx_particles[i] if p.active then -- Draw relative to camera (NO PARALLAX) local px = p.x - camera.x + sx local py = p.y + sy if p.is_firework then -- FIX (v1.0.0): Slowed down animation (total 90 frames) local sprite_id = SPRITE_FIREWORK_1 if p.anim_timer > 60 then sprite_id = SPRITE_FIREWORK_3 -- Frame 3 (frames 61-90) elseif p.anim_timer > 10 then sprite_id = SPRITE_FIREWORK_2 -- Frame 2 (frames 11-60) end -- Frame 1 is (frames 0-10) spr(sprite_id, px, py, TRANSPARENT_COLOR, 1, 0, 0, 2, 2) else -- Draw poof particle circ(px, py, p.radius, p.color) end end end -- 5. Draw Player and UI if heli.magnet_active and timer % 4 < 2 then rect(heli.x+7-camera.x+sx, heli.y+12+sy, 2, 9, 14) end spr(SPRITE_HELI_BODY, heli.x-camera.x+sx, heli.y+sy, TRANSPARENT_COLOR, 1, 0, 0, 2, 2) spr(SPRITE_HELI_ROTOR + math.floor(heli.rotor_frame), heli.x+4-camera.x+sx, heli.y-4+sy, TRANSPARENT_COLOR) spr(SPRITE_MAGNET, heli.x+4-camera.x+sx, heli.y+13+sy, TRANSPARENT_COLOR) print("SCORE: "..score,5,5,12) print("PRESS (B) FOR UPGRADES",5,15,13) end, exit = function(self) end } PauseState = { menu_scroll_top = 1, items = {}, enter = function(self) menu_selection = 1 self.menu_scroll_top = 1 self.items={"bulldozer_speed", "heli_speed", "magnet_strength", "trash_frequency", "cannon_hire"} if upgrades.cannon_hire.level>0 then table.insert(self.items,"cannon_rate") table.insert(self.items,"cannon_capacity") end table.insert(self.items, "trash_value") table.insert(self.items, "map") end, update = function(self) local item_count = #self.items -- (v0.9.7): Improved menu wrapping logic if btnp(0)then -- UP menu_selection=menu_selection-1;sfx(SFX_MENU_NAV) if menu_selection < 1 then menu_selection = item_count -- Wrap to bottom -- Set scroll to show the last page self.menu_scroll_top = math.max(1, item_count - MAX_MENU_ITEMS_DISPLAYED + 1) elseif menu_selection < self.menu_scroll_top then self.menu_scroll_top = menu_selection end end if btnp(1)then -- DOWN menu_selection=menu_selection+1;sfx(SFX_MENU_NAV) if menu_selection > item_count then menu_selection = 1 -- Wrap to top -- Set scroll to top self.menu_scroll_top = 1 elseif menu_selection >= self.menu_scroll_top + MAX_MENU_ITEMS_DISPLAYED then self.menu_scroll_top = menu_selection - MAX_MENU_ITEMS_DISPLAYED + 1 end end if self.menu_scroll_top > item_count - MAX_MENU_ITEMS_DISPLAYED + 1 then self.menu_scroll_top = math.max(1, item_count - MAX_MENU_ITEMS_DISPLAYED + 1) end if btnp(4)then local k=self.items[menu_selection] local u=upgrades[k] if score>=u.cost then score=score-u.cost;u.level=u.level+1 if k=="bulldozer_speed"then bulldozer.speed=bulldozer.speed+0.5 u.cost=math.floor(u.cost*1.8) elseif k=="heli_speed" then HELI_TILT_FORCE = HELI_TILT_FORCE * 1.2 HELI_MAX_VX = HELI_MAX_VX * 1.2 u.cost=math.floor(u.cost*1.9) elseif k=="magnet_strength" then MAGNET_PULL_FORCE = MAGNET_PULL_FORCE * 1.2 MAGNET_PULL_RADIUS_SQ = MAGNET_PULL_RADIUS_SQ * 1.2 u.cost=math.floor(u.cost*2.0) elseif k=="trash_frequency" then SPAWN_INTERVAL = math.max(1, SPAWN_INTERVAL - 1) u.cost=math.floor(u.cost*2.2) elseif k=="cannon_hire"then table.insert(cannons,{x=60,y=104-(#cannons*16),cooldown=0,fire_rate=CANNON_BASE_COOLDOWN,flip=0,capacity=upgrades.cannon_capacity.level}) u.cost=math.floor(u.cost*2.2) self:enter() elseif k=="cannon_rate"then CANNON_BASE_COOLDOWN=CANNON_BASE_COOLDOWN*0.8 u.cost=math.floor(u.cost*1.9) for i,c in ipairs(cannons)do c.fire_rate=c.fire_rate*0.8 end elseif k=="cannon_capacity"then u.cost=math.floor(u.cost*2.5) for i,c in ipairs(cannons)do c.capacity=c.capacity+1 end elseif k=="trash_value" then TRASH_VALUE_MULTIPLIER = TRASH_VALUE_MULTIPLIER * 1.5 u.cost=math.floor(u.cost*3.0) elseif k=="map"then world_bounds.right=world_bounds.right+240 u.cost=999999;u.name="MAP EXPANDED" end sfx(SFX_UPGRADE) end end if btnp(5)then StateManager:change(GameState); sfx(SFX_MENU_TOGGLE) end end, draw = function(self) GameState:draw() cls() rect(10,10,220,116,0);rectb(10,10,220,116,12) print("UPGRADE MENU",80,15,12,true) print("SCORE: "..score,28,28,5) local item_count = #self.items for i=self.menu_scroll_top, self.menu_scroll_top + MAX_MENU_ITEMS_DISPLAYED - 1 do if i <= item_count then local k = self.items[i] local u = upgrades[k] local y_pos = 45 + (i - self.menu_scroll_top) * 12 local col = 12 local text = string.format("%s L%d - COST:%d",u.name,u.level,u.cost) if score < u.cost then col = 15 end -- Red if i == menu_selection then print("> "..text, 20, y_pos, col) print(">", 20, y_pos, 12) else print(" "..text, 20, y_pos, col) end end end if self.menu_scroll_top > 1 then if timer % 30 < 15 then print("^ SCROLL ^", 88, 36, 12) end end if self.menu_scroll_top + MAX_MENU_ITEMS_DISPLAYED - 1 < item_count then if timer % 30 < 15 then print("v SCROLL v", 88, 100, 12) end end print("PRESS (B) TO RETURN",66,110,13) end, exit = function(self) end } -- -- TIC-80 MAIN LOOP -- function BOOT() init() end function TIC() StateManager:update() StateManager:draw() end