0,]']>S}Wup8d%qy)6o;]AsVl3>>>????????????????`        =     """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""     ""wwffffww""""wwffffww""""wwffffww""""wwffffww""""""""wwww""""""DDD3CD""""""wwww""""""DDD3CD""""""wwww""""""DDD3CD""""""wwww""""""i@DD43DDDD""""""""""""""i@DD43DDDD""""""""""""""i@DD43DDDD""""""""""""""iDDD3CDDDD3CD"""""""""""""" DDD3CDDDD3CD"""""""""""""" DDD3CDDDD3CD"""""""""""""" D43DDD3333CD"""""""""""""" D43DDD3333CD"""""""""""""" D43DDD3333CD"""""""""""" ""wwffffww""3CDDDD333DDD"""""""""""" ""wwffffww""3CDDDD333DDD"""""""""""" ""wwffffww""3CDDDD333DDDi""""""""""""i""wwffffww""DDD333333DDDi""""""""""i""""""wwww""""""DDD333333DDDi""""""""""i""""""wwww""""""DDD333333DDD鹻i""""""""""i""""""wwww""""""@D333DDDD """""""""" """"""wwww""""""@D333DDDD雙  """"""""""""""@D333DDDD  """"""""""""""DDDDDD陙  """"""""""""""DDDDDD  """"""""""""""DDDDDD陙  @DD43D""""""""""""""  @DD43D""""""""""""""陙  @DD43D""""""""""""""wwwwwDDD3CDDDD""""""""""""""`ffffffwwwwwDDD3CDDDD""""""""""""`ffffffwwwwwDDD3CDDDD""""""""""""`ffffffwwwwwODD43DDDD43D""""""""""""`ffffffwwwwwODD43DDDD43D""""""""""""`ffffffpwwODD43DDDD43D""""""""""`ffffffpwwOD3CDD43333D""""""""""`ffffffpwwOD3CDD43333D""""""""""3333333333cffffffpwwOD3CDD43333D""""""""""3333333333cfffffpww?3DDDD433CDD3333333333cffffff?3DDDD433CDD3333333333cfffff?3DDDD433CDD3333333333cffffffODD433333CDD3333333333cfffffODD433333CDD3333333333cffffffODD433333CDD0333333CDDDDDD4333ffffff D433CDDDDwwwww0333333CDDDDDD4333fffffff D433CDDDDwwwww0333333CDDDDDD4333ffffff D433CDDDDwwwww0333333CDDDDDD4333fffffff @DDDDDwwwww0333333CDDDDDD4333ffffff @DDDDDwwwww0333333CDDDDDD4333fffffff @DDDDDpww0333333CDDDDDD4333ffffff pww3333333DDD43333333333333cfff pww3333333DDD4333333333333cfff pww3333333DDD43333333333333cfff pww3333333DDD4333333333333cfffpww 3333333DDD43333333333333cfffpww 3333333DDD4333333333333cfffpww 3333333DDD43333333333333cfffpww 333CDDD3333333333333333pww `ff333CDDD33333333333333333wwwwf 333CDDD3333333333333333wwww 333CDDD33333333333333333wwww`  333CDDD3333333333333333wwww "" 333CDDD33333333333333333wwww""! 333CDDD3333333333333333! 333CDDD33333333333333333 333CDDD3333333333333333 333CDDD33333333333333333i 333CDDD3333333333333333fi 333CDDD33333333333333333fipww 333CDDD3333333333333333fipww 333CDDD33333333333333333f pww 333333333333333333333 pww 333333333333333333333 pww 333333333333333333333 wwww333333333333333333333 wwww333333333333333333333 wwww333333333333333333333 wwww333333333333333333333 wwww033333333333330333333333333303333333333333033333333333330333333333333303333333333333033333333333332\-- title: Fruit Pop! (Sand Edition) -- author: OsloOMA -- desc: Suika game where rot turns to clearing sand -- script: lua -- 1. CONFIGURATION local HIGHSCORE = pmem(0) -- Fruit Definitions FRUITS = { {id=1, r=5, col=2, score=1, spr_id=1, scale=5/4}, {id=2, r=8, col=1, score=2, spr_id=2, scale=8/4}, {id=3, r=11, col=9, score=3, spr_id=3, scale=11/4}, {id=4, r=14, col=3, score=5, spr_id=4, scale=14/4}, {id=5, r=18, col=4, score=8, spr_id=5, scale=18/4}, {id=6, r=22, col=5, score=13, spr_id=6, scale=22/4}, {id=7, r=26, col=6, score=21, spr_id=7, scale=26/4}, {id=8, r=30, col=10, score=34, spr_id=8, scale=30/4}, {id=9, r=35, col=11, score=55, spr_id=9, scale=35/4}, {id=10, r=40, col=13, score=89, spr_id=10, scale=40/4}, {id=11, r=48, col=12, score=144, spr_id=11, scale=48/4}, } MAX_FRUITS_ID = #FRUITS -- Standard Game Box Dimensions GAME_BOX_L = 68 GAME_BOX_R = 228 GAME_BOX_B = 130 GAME_BOX_T = 25 GAME_BOX_CENTER = GAME_BOX_L + (GAME_BOX_R - GAME_BOX_L) / 2 DANGER_Y = GAME_BOX_T + 5 -- SAND CONFIGURATION SAND_SCALE = 2 -- 1 sand pixel = 2x2 screen pixels SAND_W = (GAME_BOX_R - GAME_BOX_L) // SAND_SCALE SAND_H = (GAME_BOX_B - GAME_BOX_T) // SAND_SCALE SAND_PALETTE = {2, 3, 13, 4} -- Red, Orange, L-Grey, Brown (Simplified Colors) sand_grid = {} -- The grid array -- Dynamic Wall Variables WALL_L = GAME_BOX_L; WALL_R = GAME_BOX_R WALL_B = GAME_BOX_B; WALL_T = GAME_BOX_T -- Physics Constants GRAVITY = 0.85 PHYSICS_STEPS = 8 WALL_BOUNCE = -0.3 FRICTION = 0.98 CORRECTION_PERCENT = 0.2 GROWTH_AMOUNT = 0.40 ROTTEN_TIME = 45000 COLORKEY = 0 -- Global State dropper = {} fruits = {} particles = {} game_events = {} float_text = {} score = 0 game_over = false next_fruit_id = 1 sim_t = 0 -- Juice variables cam_x = 0; cam_y = 0 shake_timer = 0; shake_intensity = 0 hit_stop_timer = 0 combo_count = 0; combo_timer = 0 -- EXTENDED DANGER TIME (approx 10 seconds at 60fps) danger_timer = 0; MAX_DANGER_TIME = 600 -- 2. STATE MANAGER StateManager = { current = nil, update = nil, draw = nil } function StateManager:change(state) if state.init then state:init() end self.update = function() state:update() end self.draw = function() state:draw() end end -- 3. INTRO STATE IntroState = {} function IntroState:init() self.timer = 0 self.particles = {} for i=1, 40 do table.insert(self.particles, { spr_id = math.random(1, MAX_FRUITS_ID), angle = math.random() * 2 * math.pi, z = math.random() * 2 - 1, angle_speed = (math.random() - 0.5) * 0.02 + 0.01, z_speed = 0.0025 + math.random() * 0.0025 }) end end function IntroState:update() self.timer = self.timer + 1 if self.timer > 240 then StateManager:change(TitleState) return end for i=1, #self.particles do local p = self.particles[i] p.angle = p.angle + p.angle_speed; p.z = p.z + p.z_speed if p.z > 1 then p.z = 1; p.z_speed = -math.abs(p.z_speed) elseif p.z < -1 then p.z = -1; p.z_speed = math.abs(p.z_speed) end local z_norm = (p.z + 1) / 2 local radius = 20 + z_norm * 80 p.screen_x = 120 + math.cos(p.angle) * radius p.screen_y = 68 + math.sin(p.angle) * radius * 0.4 - (z_norm * 10) p.scale = z_norm * 2.5 end table.sort(self.particles, function(a,b) return a.z < b.z end) end function IntroState:draw() cls(0) for i=1, #self.particles do local p = self.particles[i] if p.scale > 0.1 then local r = p.scale * 4 spr(p.spr_id, p.screen_x - r, p.screen_y - r+5, COLORKEY, p.scale, 0, 0, 1, 1) end end if self.timer > 30 then print("OsloOMA", 76, 56, 12, false, 2) print("PRESENTS", 95, 72, 13, false, 1) end end -- 4. TITLE STATE TitleState = {} function TitleState:init() WALL_L = 0; WALL_R = 240; WALL_B = 136; WALL_T = -20 game_over = false; score = 0; fruits = {}; particles = {}; game_events = {} init_sand_grid() -- Clear sand self.dropper_timer = 0 spawn_dropper_fruit(math.random(1, 3), 120) end function TitleState:update() if btnp(4) then StateManager:change(GameState) return end self.dropper_timer = self.dropper_timer + 1 dropper.x = 120 + math.sin(self.dropper_timer * 0.03) * 110 if self.dropper_timer % 20 == 0 then drop_fruit() end for i=1, PHYSICS_STEPS do update_physics(1/PHYSICS_STEPS) end process_game_events(); update_particles() if #fruits > 60 then table.remove(fruits, 1) end for i=#fruits, 1, -1 do if fruits[i].y > 200 then table.remove(fruits, i) end end end function TitleState:draw() cls(0); draw_particles() for i=1, #fruits do local f = fruits[i] spr(f.spr_id, f.x - f.r, f.y - f.r, COLORKEY, f.scale, 0, 0, 1, 1) end print("FRUIT POP!", 69, 46, 12, false, 2) print("PRESS (Z) TO START", 68, 76, 4 + ((time() // 500) % 2) * 8, true) print("HI-SCORE: "..HIGHSCORE, 82, 95, 13, true) end -- 5. GAME STATE GameState = {} function GameState:init() WALL_L = GAME_BOX_L; WALL_R = GAME_BOX_R WALL_B = GAME_BOX_B; WALL_T = GAME_BOX_T fruits = {}; particles = {}; game_events = {}; float_text = {} init_sand_grid() score = 0; combo_count = 0; combo_timer = 0 danger_timer = 0; shake_timer = 0; hit_stop_timer = 0 sim_t = 0 spawn_dropper_fruit(math.random(1, 3)) next_fruit_id = math.random(1, 3) end function GameState:update() if hit_stop_timer > 0 then hit_stop_timer = hit_stop_timer - 1 return end if shake_timer > 0 then shake_timer = shake_timer - 1 cam_x = (math.random() * 2 - 1) * shake_intensity cam_y = (math.random() * 2 - 1) * shake_intensity else cam_x = 0; cam_y = 0 end if btn(2) then dropper.x = dropper.x - 2 end if btn(3) then dropper.x = dropper.x + 2 end local r = FRUITS[dropper.id].r if dropper.x - r < WALL_L then dropper.x = WALL_L + r end if dropper.x + r > WALL_R then dropper.x = WALL_R - r end -- Prevent spawning if stack is too high or blocked local can_drop = true -- 1. Check fruits overlap for i=1, #fruits do local f = fruits[i] if math.abs(dropper.x - f.x) < r + f.r and math.abs(dropper.y - f.y) < r + f.r then can_drop = false end end -- 2. Check sand overlap (New Edge Case Fix) if can_drop then local sand_h = get_sand_height_at(dropper.x, r) if dropper.y + r > sand_h then can_drop = false end end if btnp(4) and can_drop then drop_fruit() end if combo_timer > 0 then combo_timer = combo_timer - 1 else combo_count = 0 end -- UPDATE SAND (Sub-game) sim_t = sim_t + 1 update_sand() for i=1, PHYSICS_STEPS do update_physics(1 / PHYSICS_STEPS) end process_game_events() update_particles() update_float_text() -- DANGER ZONE CHECK local danger = false for i=1, #fruits do local f = fruits[i] if not f.target_r and f.y - f.r < DANGER_Y then if math.abs(f.vy) < 1.0 then danger = true; break end end end if danger then danger_timer = danger_timer + 1 if danger_timer > MAX_DANGER_TIME then StateManager:change(GameOverState) end else if danger_timer > 0 then danger_timer = danger_timer - 2 end end end function GameState:draw() poke(0x3FF9, cam_x); poke(0x3FFA, cam_y) cls(0) -- UI print("SCORE", 5, 5, 5, true) print(score, 5, 14, 12, true) print("HI:", 5, 22, 4, true) print(""..HIGHSCORE, 5, 30, 12, true) print("NEXT", 5, 40, 13, true) rectb(4, 49, 40, 40, 15) local next_f = FRUITS[next_fruit_id] spr(next_f.spr_id, 24 - next_f.r, 69 - next_f.r, COLORKEY, next_f.scale, 0, 0, 1, 1) -- IMPROVED COMBO METER if combo_count > 1 then local cx = 185 local cy = 5 -- Draw bar local max_time = 150 -- match logic local bar_w = (combo_timer / max_time) * 50 rect(cx, cy+18, bar_w, 3, 11) rectb(cx-1, cy+17, 52, 5, 13) -- Pulse effect local pulse = 1 + math.sin(time()*0.4)*0.2 print("COMBO!", cx, cy, 6, true) local w = print("x"..combo_count, 0, -20) print("x"..combo_count, cx + 25 - (w*pulse)/2, cy + 8, 12, true, pulse) end local box_col = 15 -- Flash box when danger is high if danger_timer > MAX_DANGER_TIME * 0.7 then box_col = (time() % 20 < 10) and 15 or 2 end rectb(WALL_L - 2, WALL_T, (WALL_R - WALL_L) + 4, (WALL_B - WALL_T) + 2, box_col) -- Draw Sand (Behind fruits) draw_sand() -- Danger Line Visuals if danger_timer > 0 then local alpha = (danger_timer / MAX_DANGER_TIME) * 100 for x = WALL_L, WALL_R, 4 do pix(x, DANGER_Y, 2) end line(WALL_L, DANGER_Y, WALL_L + (WALL_R-WALL_L)*(danger_timer/MAX_DANGER_TIME), DANGER_Y, 2) end draw_particles() -- Aim Assist Line local f_def = FRUITS[dropper.id] for y = dropper.y, WALL_B, 6 do pix(dropper.x, y, 13) end -- Draw Fruits for i=1, #fruits do local f = fruits[i] local draw_spr = f.spr_id if f.is_rotten then draw_spr = f.spr_id + 16 end -- Pop Effect Logic (Scale Pulse) local draw_scale = f.scale if f.pop_scale and f.pop_scale > 1 then draw_scale = draw_scale * f.pop_scale f.pop_scale = f.pop_scale * 0.9 if f.pop_scale < 1.01 then f.pop_scale = 1 end end -- Rotten Shake local ox, oy = 0, 0 if f.is_rotten then ox = math.random(-1,1); oy = math.random(-1,1) end spr(draw_spr, f.x - f.r + ox, f.y - f.r + oy, COLORKEY, draw_scale, 0, 0, 1, 1) -- Fallback Visuals (if sprites missing) if f.is_rotten then line(f.x-3, f.y-3, f.x+3, f.y+3, 5) line(f.x+3, f.y-3, f.x-3, f.y+3, 5) end -- Danger Indicator if f.y - f.r < DANGER_Y and not f.target_r and math.abs(f.vy) < 1.0 then if time() % 500 < 250 then pix(f.x, f.y - f.r - 2, 12); pix(f.x+2, f.y - f.r - 4, 12) end end end draw_float_text() -- Draw Dropper spr(f_def.spr_id, dropper.x - f_def.r, dropper.y - f_def.r, COLORKEY, f_def.scale, 0, 0, 1, 1) poke(0x3FF9, 0); poke(0x3FFA, 0) end -- 6. GAME OVER STATE GameOverState = {} function GameOverState:init() game_over = true sfx(0, 24, 30, 0, 12, 0) trace("DidGameEnd:"..score) if score > HIGHSCORE then HIGHSCORE = score; pmem(0, HIGHSCORE) end end function GameOverState:update() if btnp(4) then StateManager:change(TitleState) end end function GameOverState:draw() poke(0x3FF9, 0); poke(0x3FFA, 0) rect(60, 40, 120, 60, 15); rectb(60, 40, 120, 60, 12) print("GAME OVER", 90, 50, 2, true) print("SCORE: "..score, 92, 65, 4, true) if score == HIGHSCORE and score > 0 then print("NEW RECORD!", 85, 75, 12, true) end print("PRESS (Z)", 94, 90, 5, true) end -- 7. PHYSICS & LOGIC function spawn_dropper_fruit(id, x_pos) local f_def = FRUITS[id] local new_x = x_pos or (WALL_L + (WALL_R - WALL_L)/2) dropper = { id = id, x = new_x, y = WALL_T - f_def.r - 5 } end function drop_fruit() if not dropper or not dropper.id then return end local f_def = FRUITS[dropper.id] local r = f_def.r; local m = r * r local new_fruit = { id = dropper.id, r = r, col = f_def.col, spr_id = f_def.spr_id, scale = f_def.scale, x = dropper.x + (math.random() - 0.5), y = dropper.y, vx = 0, vy = 0, mass = m, inv_mass = 1/m, spawn_time = time(), is_rotten = false, pop_scale = 1.0 -- Init pop scale } table.insert(fruits, new_fruit) -- Pitch shift SFX based on size local note = math.max(10, 60 - (dropper.id * 3)) sfx(1, note, 10, 1, 5, 0) spawn_dropper_fruit(next_fruit_id, dropper.x) next_fruit_id = math.random(1, 3) end function update_physics(dt) for i=#fruits, 1, -1 do local f = fruits[i] -- Check Rot Time if not f.is_rotten and (time() - f.spawn_time > ROTTEN_TIME) then f.is_rotten = true; add_float_text(f.x, f.y, "ROT!", 5) f.rot_timer = 0 end -- Process Rotten Dissolving if f.is_rotten then if not f.rot_timer then f.rot_timer = 0 end f.rot_timer = f.rot_timer + 1 f.pop_scale = 1.0 + (math.sin(f.rot_timer * 0.2) * 0.1) -- Dissolve into sand after 4 seconds (Extended from 2s) if f.rot_timer > 240 then dissolve_fruit_to_sand(f) table.remove(fruits, i) goto continue end end if f.target_r then f.r = f.r + GROWTH_AMOUNT if f.r >= f.target_r then f.r = f.target_r; f.target_r = nil end f.mass = f.r*f.r; f.inv_mass = 1/f.mass; f.scale = f.r/4 end f.vy = f.vy + (GRAVITY * dt) f.vx = f.vx * (1 - (FRICTION * dt)) f.vy = f.vy * (1 - (FRICTION * dt)) f.x = f.x + f.vx * dt f.y = f.y + f.vy * dt -- Sand Collision (Floor push up) local sand_y = get_sand_height_at(f.x, f.r) if f.y + f.r > sand_y then f.y = sand_y - f.r f.vy = f.vy * WALL_BOUNCE f.vx = f.vx * 0.9 -- High friction on sand end -- Hard Clamp (Prevent tunneling) if f.y+f.r > WALL_B then f.y=WALL_B-f.r; f.vy=f.vy*WALL_BOUNCE end if f.x-f.r < WALL_L then f.x=WALL_L+f.r; f.vx=math.abs(f.vx)*0.5 end if f.x+f.r > WALL_R then f.x=WALL_R-f.r; f.vx=-math.abs(f.vx)*0.5 end ::continue:: end -- Collision Check for i=1, #fruits do local f1 = fruits[i] for j=i+1, #fruits do local f2 = fruits[j] if math.abs(f1.x - f2.x) < (f1.r + f2.r) and math.abs(f1.y - f2.y) < (f1.r + f2.r) then local dx = f2.x - f1.x; local dy = f2.y - f1.y local dist_sq = dx*dx + dy*dy; local total_r = f1.r + f2.r if dist_sq < total_r * total_r then resolve_collision(i, j, f1, f2, dx, dy, dist_sq, total_r) end end end end end function resolve_collision(idx1, idx2, f1, f2, dx, dy, dist_sq, total_r) -- MERGE LOGIC: Rotten fruits CANNOT merge if f1.id == f2.id and f1.id < MAX_FRUITS_ID and not f1.target_r and not f2.target_r and not f1.merged and not f2.merged and not f1.is_rotten and not f2.is_rotten then local event = { type = "MERGE", i1 = idx1, i2 = idx2, x = (f1.x+f2.x)/2, y = (f1.y+f2.y)/2, new_id = f1.id + 1, score = FRUITS[f1.id].score } table.insert(game_events, event) f1.merged = true; f2.merged = true return end local dist = math.sqrt(dist_sq) local overlap = total_r - dist local nx, ny = dx/dist, dy/dist local tot_inv_mass = f1.inv_mass + f2.inv_mass if tot_inv_mass == 0 then tot_inv_mass = 1 end local corr = (overlap * CORRECTION_PERCENT) / tot_inv_mass f1.x = f1.x - nx * corr * f1.inv_mass f1.y = f1.y - ny * corr * f1.inv_mass f2.x = f2.x + nx * corr * f2.inv_mass f2.y = f2.y + ny * corr * f2.inv_mass local dvx = f1.vx - f2.vx; local dvy = f1.vy - f2.vy local vn = dvx*nx + dvy*ny if vn < 0 then local impulse = -(1 + 0.1) * vn / tot_inv_mass f1.vx = f1.vx - nx * impulse * f1.inv_mass f1.vy = f1.vy - ny * impulse * f1.inv_mass f2.vx = f2.vx + nx * impulse * f2.inv_mass f2.vy = f2.vy + ny * impulse * f2.inv_mass end end -- 8. EVENTS, PARTICLES & TEXT function process_game_events() local remove_map = {}; local add_list = {} for _, e in ipairs(game_events) do if e.type == "MERGE" then if not remove_map[e.i1] and not remove_map[e.i2] then remove_map[e.i1] = true; remove_map[e.i2] = true local next_def = FRUITS[e.new_id]; local start_r = 1 table.insert(add_list, { id = e.new_id, r = start_r, target_r = next_def.r, col = next_def.col, spr_id = next_def.spr_id, scale=0.25, x = e.x, y = e.y, vx=0, vy=0, mass=1, inv_mass=1, spawn_time=time(), is_rotten=false, pop_scale = 2.0 }) -- NEW COMBO LOGIC: EXTENDED TIME combo_count = combo_count + 1 combo_timer = 150 -- Increased from 90 to 150 (2.5 seconds) local mult = 1 + (combo_count * 0.5) local final_score = math.floor(e.score * mult) score = score + final_score -- SIMPLIFIED SCORE TEXT: Just show "+SCORE" local txt = "+"..final_score.."" add_float_text(e.x, e.y, txt, 9 + math.min(3, combo_count)) local note = math.max(10, 70 - (e.new_id * 2)) sfx(2, note, 10, 2, 14, 0) shake_screen(4 + combo_count, 1 + combo_count/2) spawn_particles(e.x, e.y, next_def.col, 10 + combo_count*2) end end end local new_fruits = {} for i, f in ipairs(fruits) do if not remove_map[i] then table.insert(new_fruits, f) end end for _, f in ipairs(add_list) do table.insert(new_fruits, f) end fruits = new_fruits game_events = {} end function spawn_particles(x, y, col, count) for i=1, count do local angle = math.random() * 6.28 local speed = 1.5 + math.random() * 2.5 table.insert(particles, { type = "chunk", x = x, y = y, vx = math.cos(angle) * speed, vy = math.sin(angle) * speed, life = 30 + math.random(10), col = col, size = math.random(20, 40) / 10 }) end table.insert(particles, { type = "ring", x = x, y = y, r = 5, width = 2, life = 15, col = 12 }) end function update_particles() for i=#particles, 1, -1 do local p = particles[i] p.life = p.life - 1 if p.type == "chunk" then p.x = p.x + p.vx; p.y = p.y + p.vy p.vy = p.vy + 0.2; p.vx = p.vx * 0.95; p.vy = p.vy * 0.95 p.size = p.size * 0.9 elseif p.type == "ring" then p.r = p.r + 3 end if p.life <= 0 then table.remove(particles, i) end end end function draw_particles() for _, p in ipairs(particles) do if p.type == "chunk" then if p.size > 1 then circ(p.x, p.y, p.size, p.col) pix(p.x - p.size/2, p.y - p.size/2, 12) else pix(p.x, p.y, p.col) end elseif p.type == "ring" then circb(p.x, p.y, p.r, p.col) end end end function add_float_text(x, y, txt, col) table.insert(float_text, {x=x+math.random(-5,5), y=y, txt=txt, col=col, life=40, max_life=40}) end function update_float_text() for i=#float_text, 1, -1 do local t = float_text[i]; t.life = t.life - 1; t.y = t.y - 0.5 if t.life <= 0 then table.remove(float_text, i) end end end function draw_float_text() for _, t in ipairs(float_text) do local scale = 2; local age = t.max_life - t.life if age < 5 then scale = 1 + (5-age)/5 end local w = print(t.txt, 0, -20) print(t.txt, t.x - w/2, t.y, t.col, true, scale, true) end end function shake_screen(dur, inten) shake_timer = dur; shake_intensity = inten end -- 9. SAND SYSTEMS (New Feature) function init_sand_grid() sand_grid = {} for x = 1, SAND_W do sand_grid[x] = {} for y = 1, SAND_H do sand_grid[x][y] = 0 end end end function dissolve_fruit_to_sand(f) sfx(3, 30, 15, 3, 15, 0) -- Sand crumble sfx add_float_text(f.x, f.y, "CRUMBLE!", 4) -- Simplified Colors: Map fruit color to Sand Palette local sand_col = SAND_PALETTE[(f.id % #SAND_PALETTE) + 1] -- Convert screen coords to grid coords local cx = math.floor((f.x - GAME_BOX_L) / SAND_SCALE) local cy = math.floor((f.y - GAME_BOX_T) / SAND_SCALE) local cr = math.floor(f.r / SAND_SCALE) for x = cx - cr, cx + cr do for y = cy - cr, cy + cr do local dist = (x-cx)^2 + (y-cy)^2 if dist <= cr^2 then if x >= 1 and x <= SAND_W and y >= 1 and y <= SAND_H then -- Randomize sand color slightly for texture if math.random() > 0.8 then sand_grid[x][y] = sand_col + 1 -- highlight speck else sand_grid[x][y] = sand_col end end end end end end function update_sand() local moved = false -- Scan bottom-up for y = SAND_H - 1, 1, -1 do -- Alternate scan direction local start_x, end_x, step_x = 1, SAND_W, 1 if sim_t % 2 == 0 then start_x, end_x, step_x = SAND_W, 1, -1 end for x = start_x, end_x, step_x do local cell = sand_grid[x][y] if cell ~= 0 then -- Down if sand_grid[x][y+1] == 0 then sand_grid[x][y+1] = cell; sand_grid[x][y] = 0 moved = true else -- Diagonals (Slide) local d1, d2 = 1, -1 if math.random() > 0.5 then d1 = -1; d2 = 1 end local moved_diag = false -- Try first diagonal local nx = x + d1 if nx >= 1 and nx <= SAND_W and sand_grid[nx][y+1] == 0 then sand_grid[nx][y+1] = cell; sand_grid[x][y] = 0 moved_diag = true end -- Try second diagonal if not moved_diag then nx = x + d2 if nx >= 1 and nx <= SAND_W and sand_grid[nx][y+1] == 0 then sand_grid[nx][y+1] = cell; sand_grid[x][y] = 0 end end end end end end -- Check for Lines (Tetris Style) if sim_t % 4 == 0 then -- Don't check every frame check_sand_lines() end end function check_sand_lines() local lines_cleared = 0 for y = SAND_H, 1, -1 do local full = true for x = 1, SAND_W do if sand_grid[x][y] == 0 then full = false; break end end if full then -- Shift everything above down for k = y, 2, -1 do for x = 1, SAND_W do sand_grid[x][k] = sand_grid[x][k-1] end end -- Clear top row for x = 1, SAND_W do sand_grid[x][1] = 0 end lines_cleared = lines_cleared + 1 -- Re-check same row since everything shifted down y = y + 1 end end if lines_cleared > 0 then sfx(0, 60, 30, 2, 10, 0) -- Clear SFX score = score + (lines_cleared * 100) add_float_text(GAME_BOX_CENTER, GAME_BOX_B - 20, "SAND CLEAR!", 11) shake_screen(10, 2) end end function get_sand_height_at(world_x, radius) -- Convert world X to grid X range local center_gx = math.floor((world_x - GAME_BOX_L) / SAND_SCALE) local gr = math.ceil(radius / SAND_SCALE) local max_height_y = SAND_H + 1 -- Default: Floor -- Scan the area under the fruit for gx = center_gx - gr, center_gx + gr do if gx >= 1 and gx <= SAND_W then -- Find highest sand pixel in this column for gy = 1, SAND_H do if sand_grid[gx][gy] ~= 0 then if gy < max_height_y then max_height_y = gy end break end end end end -- Convert grid Y back to world Y local world_y = GAME_BOX_T + (max_height_y - 1) * SAND_SCALE -- If no sand found, return the box floor if max_height_y > SAND_H then return WALL_B end return world_y end function draw_sand() for x = 1, SAND_W do for y = 1, SAND_H do local c = sand_grid[x][y] if c ~= 0 then rect(GAME_BOX_L + x*SAND_SCALE, GAME_BOX_T + y*SAND_SCALE, SAND_SCALE, SAND_SCALE, c) -- Simple lighting texture if SAND_SCALE >= 2 then pix(GAME_BOX_L + x*SAND_SCALE + 1, GAME_BOX_T + y*SAND_SCALE + 1, 15) -- Highlight end end end end end function TIC() if StateManager.update == nil then StateManager:change(IntroState) end StateManager:update() StateManager:draw() end