-- Pepe Runner DX — versio millorada del port amb QoL i extres. -- Carrega config.lua si existeix per a sobreescriure els valors per defecte. -- ==================================================================== -- VALORS PER DEFECTE DE CONFIGURACIO (sobreescriuibles per config.lua) -- ==================================================================== skin = "custom" -- "custom" | "native" (vore config.lua per descripcio) -- Codis CP437 dels sprites del joc original (de TIPOS.PAS). -- Els usem com a IDs logics de tipus de cel·la (i coincideixen amb el -- format binari dels .map). El codi de glif real per a pintar surt -- de la taula glif[] segons la skin activa. BUIT = 0 DINERS = 36 PEDRA = 219 ESCALA = 205 CORDA = 196 BLOC1 = 176 BLOC2 = 177 BLOC3 = 178 PEPE_C = 2 MALO_C = 88 -- Code-points reservats addicionals (no venen del .map binari, son -- elements visuals introduits per la versio DX). Tots els skins els han -- de definir en estos mateixos cp per a ser intercanviables. PEPE_MORT_A = 226 -- sprite A de l'animacio de mort de Pepe PEPE_MORT_B = 227 -- sprite B (alterna amb A frame per frame) MALO_C2 = 89 -- enemic 2 (cada malos[i] usa el seu propi sprite) MALO_C3 = 90 -- enemic 3 -- Glifs del marc del HUD. Codis triats per a no col·lidir amb cap glif -- de cap skin (la native usa 224, 233, 216-220, etc.). FRAME_H = 240 FRAME_V = 241 FRAME_TL = 242 FRAME_TR = 243 FRAME_BL = 244 FRAME_BR = 245 -- Glif dedicat al fade (bloc 100% solid). Definit en definir_marc() i no -- en cap skin: el fade ha de funcionar igual en totes les skins. FADE_BLOCK = 246 -- La definicio de cada skin viu en `skins/.lua` (bitmaps, colors). -- En arrancar el joc, `carregar_skin()` carrega el fitxer corresponent al -- valor de `skin` i ompli les variables globals seguents: -- skin_data → tabla amb { elements = {...}, state_colors = {...} } -- glif → mapa tipo_logic_numeric → cp_a_pintar (BUIT→0, PEDRA→219, ...) -- colors → mapa nom_logic → COLOR_X (colors.pedra, colors.bg, ...) skin_data = nil glif = {} colors = {} -- Tecles del joc — sobreescriuibles per config.lua. -- Nota: KEY_ESCAPE no es pot usar perque el interpret ascii la captura -- per a la seva consola de debug abans que Lua la veja. keys = { up = KEY_UP, down = KEY_DOWN, left = KEY_LEFT, right = KEY_RIGHT, dig_left = KEY_Z, dig_right = KEY_X, quit = KEY_Q, -- abandona la partida → game over } -- Textos del joc — sobreescriuibles per config.lua (taula `textos`). -- Tots en sentence case (Inicial Majuscula, resta minuscula). textos = { title_press_play = "Prem l'espai per a jugar", game_over = "Fi de joc", new_record = "Nou record !", score_label = "Punts", -- HUD i pantalla de nou record level_label = "Nivell", lives_label = "Vides", record_label = "Record", name_label = "Nom:", name_help = "(A-Z)", } -- Estats (de TIPOS.PAS — son bitflags per a SelectEstat dels enemics) NORMAL = 0 PUJAR = 0x01 BAIXAR = 0x02 CAENT = 4 ESQUERRA = 0x10 DRETA = 0x20 -- Constants del joc MAP_W = 40 MAP_H = 25 BLOC_OUT = 100 -- temps que dura un forat obert (de TIPOS.PAS) TICS = 6 -- frames per tick de joc (60fps / 6 = 10 Hz) NUM_MALOS = 3 TEMPS_IA = 30 -- iteracions del malo entre canvis de direccio MALO_RATIO = 4 -- els malos van 1/4 del ritme del Pepe (com en RUNNER.PAS) NUM_FASES = 10 -- mapes 1..10 (el 0 esta reservat per al titol) VIDES_INI = 3 -- l'original arrancava amb 0 (1 vida); 3 es mes raonable -- Tots els temps i durations de les animacions del DX. Sobreescriuibles -- per config.lua (en frames a 60 fps; 60 = 1 segon). temps = { score_step = 3, -- frames per cada +1 del comptador animat (60/3 = 20 pts/s) fade_frames = 18, -- duracio de cada fase del fade (out i in son simetrics) mort_anim_frames = 30, -- duracio de l'animacio de mort (Pepe visible, 0.5 s) respawn_delay_frames = 120, -- temps que Pepe queda invisible abans del respawn (2 s) invuln_frames = 180, -- temps invulnerable despres del respawn (3 s) escala_step_frames = 6, -- frames entre cada cel·la nova de l'escala lateral (0.1 s) } -- Estats del joc (maquina d'estats global) ESTAT_TITLE = "title" ESTAT_PLAYING = "playing" ESTAT_GAMEOVER = "gameover" ESTAT_ENTERNAME = "entername" -- Estat global mapa = {} -- mapa[x][y] = { tipo=, color=, temps= } level = 1 pepe = { x=19, y=23, dibuix=PEPE_C, vides=VIDES_INI, estat=NORMAL, mort_t=0, invuln_t=0 } malos = {} score = 0 score_display = 0 -- valor pintat al HUD; va alcançant a score animadament diners_pantalla = 0 game_tic = 0 hi_score = 0 nom_hi_score = "AAA" estat_joc = ESTAT_TITLE estat_inici = 0 enter_name_idx = 1 fade = nil -- nil = sense transicio; o { phase="out"|"in", t=0, on_mid=... } -- Jingles per als estats no-jugables: una sola reproduccio per entrada al -- estat (no es repeteixen en bucle). El motor d'ascii nomes te un canal -- d'audio, per aixo no posem musica in-game (les SFX la tallarien). music_on = true -- false desactiva tots els jingles (les SFX continuen) musica = { title = "l4o4cegfedcceg", entername = "l2o5cegcegced", } music_actual = nil -- id del jingle reproduit actualment (nil = silenci) -- Estat de l'animacio de l'escala lateral (apareix quan no queden diners). -- "idle" → encara no s'ha disparat per a este mapa -- "growing" → en curs: cada escala_step_frames apareix una cel·la nova -- "done" → ja arribada al final (col 0 plena fins a pedra o fila 23) escala_state = "idle" escala_y = 1 -- proxima fila a omplir escala_t = 0 -- comptador de frames entre cel·les -- Matriu 4x4 de Bayer (0..15). El llindar es la proporcio coberta del fade: -- una cel·la (x,y) es cobrix amb bloc solid bg si bayer[x%4][y%4] < progress (0..16). -- Al ser 4x4 i tindre tots els valors 0..15, dona 16 nivells de cobertura -- amb un patro de dither ordenat (no aleatori), molt mes natural visualment. BAYER4 = { { 0, 8, 2, 10 }, { 12, 4, 14, 6 }, { 3, 11, 1, 9 }, { 15, 7, 13, 5 }, } -- Mapeja id_logic_string (com surten en skins/*.lua) → constant numerica -- interna del .lua (BUIT, PEDRA, ...). S'usa per a omplir glif[] des de -- l'estructura declarativa del fitxer de skin. local ELEMENT_TO_TIPO = { diners = DINERS, pedra = PEDRA, escala = ESCALA, corda = CORDA, bloc1 = BLOC1, bloc2 = BLOC2, bloc3 = BLOC3, pepe = PEPE_C, pepe_mort_a = PEPE_MORT_A, pepe_mort_b = PEPE_MORT_B, malo1 = MALO_C, malo2 = MALO_C2, malo3 = MALO_C3, } -- Carrega skins/.lua i ompli glif[] + colors{} a partir de la taula -- declarativa. Si el fitxer no existeix o falla, fallback silenciós a custom. function carregar_skin() local ok, data = pcall(dofile, "skins/"..skin..".lua") if not ok or type(data) ~= "table" then data = dofile("skins/custom.lua") end skin_data = data glif = {} glif[BUIT] = 0 for name, elem in pairs(skin_data.elements) do local tipo = ELEMENT_TO_TIPO[name] if tipo then glif[tipo] = elem.cp end end colors = {} for name, elem in pairs(skin_data.elements) do colors[name] = elem.color end -- Alias per a codi historic que encara mira `colors.malo` (ex: pintar_pepe -- al tram curt de l'animacio de mort). colors.malo = skin_data.elements.malo1.color for k, v in pairs(skin_data.state_colors) do colors[k] = v end end function definir_glifs() -- Char 0 sempre buit (el ROM d'ascii hi te una caixa que taparia el mapa) setchar(0, 0,0,0,0,0,0,0,0) -- Aplica els bitmaps de la skin activa. Si un element te bitmap=nil, -- es deixa el cp tal com estiga al ROM nadiu (cas no usat ara mateix). for _, elem in pairs(skin_data.elements) do local b = elem.bitmap if b then setchar(elem.cp, b[1],b[2],b[3],b[4],b[5],b[6],b[7],b[8]) end end end -- Glifs del marc del HUD (sempre, independent de la skin del joc) function definir_marc() setchar(FRAME_H, 0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00) -- horitzontal centrada setchar(FRAME_V, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18) -- vertical centrada setchar(FRAME_TL, 0x00,0x00,0x00,0x1F,0x1F,0x18,0x18,0x18) -- ┌ setchar(FRAME_TR, 0x00,0x00,0x00,0xF8,0xF8,0x18,0x18,0x18) -- ┐ setchar(FRAME_BL, 0x18,0x18,0x18,0x1F,0x1F,0x00,0x00,0x00) -- └ setchar(FRAME_BR, 0x18,0x18,0x18,0xF8,0xF8,0x00,0x00,0x00) -- ┘ setchar(FADE_BLOCK, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF) -- █ solid per al fade end -- ==================================================================== -- TRANSICIONS (fade out + canvi d'estat + fade in) -- ==================================================================== -- El "fade" es un dither ordenat (Bayer 4x4) en color de fons: cada cel·la -- es cobrix amb un bloc solid quan el seu llindar es supera. Aixi simula -- un fade out sense necessitar paleta runtime (que ascii no te). function fade_actiu() return fade ~= nil end -- Inicia una transicio: fade_out → executa on_mid (canvi d'estat) → fade_in. -- Mentre fade_actiu(), els estats no han de processar input ni logica; -- nomes renderitzen el seu frame, i el dither es pinta a sobre. function transicio(on_mid) fade = { phase="out", t=0, on_mid=on_mid } end function update_fade() if not fade then return end fade.t = fade.t + 1 if fade.phase == "out" and fade.t >= temps.fade_frames then local cb = fade.on_mid fade = { phase="in", t=0 } if cb then cb() end elseif fade.phase == "in" and fade.t >= temps.fade_frames then fade = nil end end -- Pinta el dither encima del render actual. progress va de 0 (descobert) -- a 16 (totalment cobert). En fade_out puja, en fade_in baixa. function pintar_fade() if not fade then return end local progress if fade.phase == "out" then progress = flr(fade.t * 16 / temps.fade_frames) else progress = 16 - flr(fade.t * 16 / temps.fade_frames) end if progress <= 0 then return end color(colors.bg, colors.bg) for y = 0, 29 do for x = 0, 39 do if BAYER4[(x % 4) + 1][(y % 4) + 1] < progress then print(chr(FADE_BLOCK), x, y) end end end end -- ==================================================================== -- JINGLES (una reproduccio per entrada a l'estat, sense bucle) -- ==================================================================== -- Es crida set_music(id) al inici de cada estat: si l'id canvia, dispara -- el jingle una vegada i ja esta. set_music(nil) silencia el canal d'audio. function set_music(id) if music_actual == id then return end music_actual = id if not music_on then return end if id then play(musica[id]) else nosound() end end -- ==================================================================== -- SFX (l'original no tenia so — afegim els minims raonables) -- ==================================================================== function sfx_coin() sound(2000, 40) end function sfx_escala_step(y) sound(400 + y * 40, 4) end -- "uic uic uic..." quatre pulsos aguts amb volum decreixent, com un eco function sfx_respawn() play("o6v9c0r0v6c0r0v3c0r0v1c0") end function sfx_dig() play("l0o2c") end function sfx_die() play("l1o4cl0o3bagfed") end function sfx_malo_die() play("l0o3ao4c") end function sfx_level() play("l1o4ceg") end function sfx_gameover() play("l1o3bal0gfedco2c") end -- Neteja la pantalla amb el color de fons configurat (en lloc de negre). function neteja_fons() color(colors.bg, colors.bg) cls() end function color_de(tipo) if tipo == PEDRA then return colors.pedra end if tipo == DINERS then return colors.diners end if tipo == ESCALA then return colors.escala end if tipo == CORDA then return colors.corda end if tipo == BLOC1 or tipo == BLOC2 or tipo == BLOC3 then return colors.pedra end return colors.bg end -- Helper segur per llegir el tipus d'una cel·la (fora de mapa = pedra virtual) function tipo_a(x, y) if x < 0 or x >= MAP_W or y < 0 or y >= MAP_H then return PEDRA end return mapa[x][y].tipo end -- Es comporta com una paret (impedeix passar lateralment, fa de suport): -- PEDRA, qualsevol fase de forat tancant-se (BLOC1/2/3) o un malo atrapat. -- Aço unifica el comportament: "rellenat" o "rellenant" → solid. function es_paret(x, y) local t = tipo_a(x, y) if t == PEDRA or t == BLOC1 or t == BLOC2 or t == BLOC3 then return true end for i = 1, NUM_MALOS do if malos[i].atrapat and malos[i].x == x and malos[i].y == y then return true end end return false end -- Es comporta com a suport per a no caure (paret + escala). function es_suport(x, y) return es_paret(x, y) or tipo_a(x, y) == ESCALA end function carregar_mapa(num) filein("maps/"..tostr(num)..".map", 0, MAP_W*MAP_H) diners_pantalla = 0 for x = 0, MAP_W-1 do mapa[x] = {} for y = 0, MAP_H-1 do local tipo = peek(x*MAP_H + y) mapa[x][y] = { tipo=tipo, color=color_de(tipo), temps=-1 } if tipo == DINERS then diners_pantalla = diners_pantalla + 1 end end end -- Reset de l'animacio de l'escala per al nou mapa escala_state = "idle" escala_y = 1 escala_t = 0 end -- offset_x permet desplacar tot el mapa horitzontalment al pintar -- (s'usa al title per a centrar millor el logo, que esta lleugerament -- esbiaixat a l'esquerra en el 0.map original). function pintar_mapa(offset_x) offset_x = offset_x or 0 for x = 0, MAP_W-1 do local px = x + offset_x if px >= 0 and px < MAP_W then for y = 0, MAP_H-1 do local c = mapa[x][y] if c.tipo ~= BUIT then color(c.color, colors.bg) print(chr(glif[c.tipo]), px, y) end end end end end function pintar_pepe() local col = colors.pepe local g = glif[pepe.dibuix] if pepe.mort_t > 0 then -- Fase d'espera (Pepe invisible, mapa segueix viu): no pintem res. if pepe.mort_t <= temps.respawn_delay_frames then return end -- Animacio visual de mort, en dos trams desproporcionats (1/4 + 3/4): -- tram curt: parpadeja blanc/roig amb la cara normal del Pepe. -- tram llarg: alterna entre els dos sprites de mort de la skin -- (pepe_mort_a / pepe_mort_b), cada un amb el seu color propi. local fase = (temps.mort_anim_frames + temps.respawn_delay_frames) - pepe.mort_t -- 0..mort_anim_frames-1 local tram_curt = flr(temps.mort_anim_frames / 4) if fase < tram_curt then col = (fase % 6 < 3) and colors.malo or colors.pepe else if ((fase - tram_curt) % 12) < 6 then g = glif[PEPE_MORT_A] col = colors.pepe_mort_a else g = glif[PEPE_MORT_B] col = colors.pepe_mort_b end end elseif pepe.invuln_t > 0 then -- Invulnerabilitat: parpadeja entre colors.pepe i colors.pepe_invuln -- cada 6 frames. Es vol un color brillant per a que no quede estela -- visual al moure's (negre o foscos donen la sensacio que la silueta -- blanca persisteix en la retina). col = (pepe.invuln_t % 12 < 6) and colors.pepe or colors.pepe_invuln end color(col, colors.bg) print(chr(g), pepe.x, pepe.y) end -- Marca una cel·la com a forat (sols si actualment es pedra) function foradar(x, y) if tipo_a(x, y) == PEDRA then mapa[x][y].temps = BLOC_OUT end end -- Pot Pepe cavar a esquerra/dreta? Condicions del MouPepe original: -- - la cel·la diagonal-baix ha de ser pedra (per a obrir-hi forat) -- - la cel·la lateral no pot ser pedra (per a que Pepe s'hi puga assomar) -- - Pepe ha d'estar en estat normal (no caent) function pot_cavar(dx) return pepe.estat == NORMAL and tipo_a(pepe.x+dx, pepe.y+1) == PEDRA and not es_paret(pepe.x+dx, pepe.y) end -- Tic de joc del Pepe: input de moviment, gravetat, recollir diners, emparedat function tic_pepe() local actual = tipo_a(pepe.x, pepe.y) local sotto = tipo_a(pepe.x, pepe.y+1) -- Moviment vertical (com en RUNNER.PAS, son if/else). -- Bloquegem entrar dins de qualsevol cel·la "paret" (inclou malos atrapats). if btn(keys.up) then if actual == ESCALA and not es_paret(pepe.x, pepe.y-1) then pepe.y = pepe.y - 1 end elseif btn(keys.down) then if not es_paret(pepe.x, pepe.y+1) and (sotto == ESCALA or sotto == BUIT or sotto == DINERS) then pepe.y = pepe.y + 1 end end -- Moviment horitzontal (no es pot moure si esta caent) if btn(keys.left) then if not es_paret(pepe.x-1, pepe.y) and pepe.estat ~= CAENT then pepe.x = pepe.x - 1 end elseif btn(keys.right) then if not es_paret(pepe.x+1, pepe.y) and pepe.estat ~= CAENT then pepe.x = pepe.x + 1 end end -- Si no passa res especial, estat = normal (gravetat pot canviar-ho mes avall) pepe.estat = NORMAL -- Final pantalla: si arriba a la fila 1, passa al nivel seguent if pepe.y == 1 then sfx_level() transicio(function() fase_nova() end) return end -- Emparedat: si la cel·la actual s'ha tornat pedra, Pepe mor if tipo_a(pepe.x, pepe.y) == PEDRA then mort_pepe() return end -- Recollir diners if tipo_a(pepe.x, pepe.y) == DINERS then mapa[pepe.x][pepe.y].tipo = BUIT score = score + 1 diners_pantalla = diners_pantalla - 1 sfx_coin() end -- Bordes X if pepe.x < 0 then pepe.x = 0 end if pepe.x > MAP_W-1 then pepe.x = MAP_W-1 end -- Gravetat: si la cel·la actual es buit/diners i la de baix no fa de suport → cau actual = tipo_a(pepe.x, pepe.y) if not es_suport(pepe.x, pepe.y+1) and (actual == BUIT or actual == DINERS) then pepe.y = pepe.y + 1 pepe.estat = CAENT end -- Bordes Y if pepe.y < 0 then pepe.y = 0 end if pepe.y > MAP_H-1 then pepe.y = MAP_H-1 end end -- Inicia l'animacio de mort. El timer mort_t cobreix dos trams seguits: -- [delay+1 .. delay+anim] → animacio visual de Pepe morint -- [1 .. delay] → Pepe invisible, mapa segueix viu (pausa) -- Al arribar a 0, update_playing descompta vida i fa el respawn. -- Guarda contra reentrada per a no reiniciar si ja esta morint. function mort_pepe() if pepe.mort_t > 0 then return end pepe.mort_t = temps.mort_anim_frames + temps.respawn_delay_frames sfx_die() end -- Reposiciona Pepe al spawn i activa la invulnerabilitat. function respawn_pepe() pepe.x = 19 pepe.y = 23 pepe.estat = NORMAL pepe.invuln_t = temps.invuln_frames sfx_respawn() end -- Inicialitza tot per a una nova partida (reset complet) function inicialitzacio() level = 1 score = 0 score_display = 0 pepe.vides = VIDES_INI pepe.x = 19; pepe.y = 23; pepe.estat = NORMAL pepe.mort_t = 0 pepe.invuln_t = 0 carregar_mapa(level) init_malos() game_tic = 0 end -- Avanca al nivell seguent (sense reset de score ni vides). -- sfx_level() es dispara abans de la transicio, no aci dins. function fase_nova() level = level + 1 if level > NUM_FASES then level = 1 end pepe.x = 19; pepe.y = 23; pepe.estat = NORMAL carregar_mapa(level) init_malos() end -- ==================================================================== -- ESTATS DEL JOC (title / playing / game over / enter name) -- ==================================================================== function set_estat(nou) estat_joc = nou estat_inici = cnt() end function temps_estat() return cnt() - estat_inici end -- Records I/O. Usem io.open (estandard de Lua) en lloc de filein/fileout -- per a poder gestionar el cas de fitxer inexistent sense petar. -- Format: 6 bytes = 3 (centenes, desenes, unitats del score) + 3 (lletres nom) function carregar_records() local f = io.open("records", "rb") if not f then return end local data = f:read(6) f:close() if not data or #data < 6 then return end local b = { string.byte(data, 1, 6) } hi_score = b[1]*100 + b[2]*10 + b[3] -- Validar que les lletres del nom siguen imprimibles if b[4] >= 32 and b[4] < 127 and b[5] >= 32 and b[5] < 127 and b[6] >= 32 and b[6] < 127 then nom_hi_score = string.char(b[4], b[5], b[6]) end end function guardar_records() local f = io.open("records", "wb") if not f then return end f:write(string.char( flr(hi_score / 100), flr((hi_score % 100) / 10), hi_score % 10, string.byte(nom_hi_score, 1) or 65, string.byte(nom_hi_score, 2) or 65, string.byte(nom_hi_score, 3) or 65 )) f:close() end -- Quina lletra A-Z s'ha pulsat este frame (escaneig manual amb btnp, -- perque ascii no exposa whichbtn() al Lua malgrat estar al ascii.h). function lletra_pulsada() for sc = KEY_A, KEY_Z do if btnp(sc) then return string.char(65 + sc - KEY_A) end end return nil end -- ----- TITLE ----- function init_title() carregar_mapa(0) -- mapa 0 es l'art del titol end function update_title() set_music("title") if not fade_actiu() and btnp(KEY_SPACE) then transicio(function() inicialitzacio() set_estat(ESTAT_PLAYING) end) end neteja_fons() pintar_mapa(1) -- desplacem +1 col per a centrar el logo de map 0 -- Missatge parpadejant (textos.title_press_play); ocult durant el fade if not fade_actiu() and flr(cnt() / 30) % 2 == 0 then color(COLOR_WHITE, colors.bg) local t = textos.title_press_play print(t, flr((40 - strlen(t)) / 2), 22) end pintar_hud() end -- ----- GAME OVER ----- function update_gameover() set_music(nil) -- Render congelat: ultim estat del joc + overlay "GAME OVER" neteja_fons() pintar_mapa() pintar_malos() pintar_pepe() color(colors.title, colors.bg) local g = textos.game_over print(g, flr((40 - strlen(g)) / 2), 12) pintar_hud() -- Despres de 2 segons (120 frames), transicio a title o entername if not fade_actiu() and temps_estat() > 120 then if score > hi_score then transicio(function() hi_score = score nom_hi_score = "AAA" enter_name_idx = 1 set_estat(ESTAT_ENTERNAME) end) else transicio(function() init_title() set_estat(ESTAT_TITLE) end) end end end -- ----- ENTER NAME ----- function update_entername() set_music("entername") neteja_fons() color(colors.title, colors.bg) local t = textos.new_record print(t, flr((40 - strlen(t)) / 2), 10) color(colors.diners, colors.bg) local s = textos.score_label.." "..string.format("%03d", hi_score) print(s, flr((40 - strlen(s)) / 2), 12) color(COLOR_WHITE, colors.bg) local n = textos.name_label.." "..nom_hi_score print(n, flr((40 - strlen(n)) / 2), 15) print(textos.name_help, flr((40 - strlen(textos.name_help)) / 2), 17) pintar_hud() if fade_actiu() then return end local lletra = lletra_pulsada() if lletra then nom_hi_score = string.sub(nom_hi_score, 1, enter_name_idx-1) ..lletra.. string.sub(nom_hi_score, enter_name_idx+1) enter_name_idx = enter_name_idx + 1 if enter_name_idx > 3 then transicio(function() guardar_records() init_title() set_estat(ESTAT_TITLE) end) end end end -- ----- PLAYING ----- function update_playing() set_music(nil) -- Durant un fade (cambi de fase), no processem input ni logica del joc: -- nomes render (el dither es pinta a sobre des de update() global). -- Nota: la transicio a game over es directa (sense fade), perque el -- frame d'arribada es el mateix que el de sortida + overlay. if not fade_actiu() then -- Animacio independent de l'escala lateral (per frame, no per tic) tic_escala_creixent() -- Timers per frame (animacio de mort, invulnerabilitat post-respawn) if pepe.mort_t > 0 then pepe.mort_t = pepe.mort_t - 1 if pepe.mort_t == 0 then pepe.vides = pepe.vides - 1 if pepe.vides < 0 then sfx_gameover() set_estat(ESTAT_GAMEOVER) return end respawn_pepe() end end if pepe.invuln_t > 0 then pepe.invuln_t = pepe.invuln_t - 1 end -- Mentre muriguent, ignorem input i logica del joc (pero els malos -- segueixen movent-se per a no congelar l'escena). local muriguent = pepe.mort_t > 0 if not muriguent and btnp(keys.quit) then -- Abandonar partida → flux de game over (com en RUNNER.PAS amb ESC) sfx_gameover() set_estat(ESTAT_GAMEOVER) return end -- Cavar es immediat (un sol forat per pulsacio) if not muriguent and pepe.estat == NORMAL then if btnp(keys.dig_left) and pot_cavar(-1) then foradar(pepe.x-1, pepe.y+1); sfx_dig() end if btnp(keys.dig_right) and pot_cavar( 1) then foradar(pepe.x+1, pepe.y+1); sfx_dig() end end -- Logica del joc: cada TICS frames if (cnt() % TICS) == 0 then game_tic = game_tic + 1 if not muriguent then tic_pepe() if fade_actiu() then return end -- canvi de fase iniciat check_mort_per_malos() end if (game_tic % MALO_RATIO) == 0 then tic_malos() if not muriguent then check_mort_per_malos() end end check_mapa() end end -- Render (sempre, fins i tot durant el fade — el dither es pinta encima) neteja_fons() pintar_mapa() pintar_malos() pintar_pepe() pintar_hud() end -- ==================================================================== -- ENEMICS -- ==================================================================== function init_malos() -- Cada malo te el seu propi sprite (1, 2 o 3) i guardem el seu color -- base per a poder restaurar-lo despres de respawn o de soltar carrega. local function novo(x, y, sprite) local cname = "malo"..sprite -- "malo1" | "malo2" | "malo3" return { x=x, y=y, sprite=sprite, color=colors[cname], color_base=colors[cname], estat=ESQUERRA, iaclock=0, carrega={ok=false, x=0, y=0}, atrapat=false, dropped_x=nil, dropped_y=nil } end malos = { novo(9, 2, 1), novo(20, 2, 2), novo(39, 2, 3) } end function pintar_malos() local cps = { glif[MALO_C], glif[MALO_C2], glif[MALO_C3] } for i = 1, NUM_MALOS do local m = malos[i] color(m.color, colors.bg) print(chr(cps[m.sprite]), m.x, m.y) end end -- Tria una nova direccio per a un enemic (port de SelectEstat de RUNNER.PAS). -- 50% prob persegueix Pepe, 50% prob direccio aleatoria entre les valides. -- Si no te suport sota els peus, override a CAENT. function select_estat(m) local nou = 0 if not es_paret(m.x+1, m.y) then nou = nou | DRETA end if not es_paret(m.x-1, m.y) then nou = nou | ESQUERRA end if tipo_a(m.x, m.y) == ESCALA then nou = nou | PUJAR end if tipo_a(m.x, m.y+1) == ESCALA then nou = nou | BAIXAR end local sestat = 0 if nou == 0 then sestat = 10 end -- atrapat: valor que cap case reconeix local pX = (m.x > pepe.x) and ESQUERRA or DRETA local pY = (m.y > pepe.y) and PUJAR or BAIXAR if rnd(100) < 50 and (((nou & pX) == pX) or ((nou & pY) == pY)) then if (nou & pX) == pX then sestat = pX else sestat = pY end else local x = rnd(4) while sestat == 0 do if x == 0 and (nou & DRETA) == DRETA then sestat = DRETA elseif x == 1 and (nou & ESQUERRA) == ESQUERRA then sestat = ESQUERRA elseif x == 2 and (nou & PUJAR) == PUJAR then sestat = PUJAR elseif x == 3 and (nou & BAIXAR) == BAIXAR then sestat = BAIXAR end x = (x + 1) & 3 end end -- override de caiguda (igual que en el Pascal — no comprova corda aci, -- la comprovacio amb corda es fa al tic_malos) if not es_suport(m.x, m.y+1) then sestat = CAENT end return sestat end -- Si va horitzontal i hi ha escala adalt/abaix, 80% prob s'enganxa function agafar_escala(m) if m.estat == DRETA or m.estat == ESQUERRA then if rnd(100) < 80 then if tipo_a(m.x, m.y) == ESCALA then return PUJAR elseif tipo_a(m.x, m.y+1) == ESCALA then return BAIXAR end end end return m.estat end -- Mort d'un enemic (per emparedat). Respawn a (39, 1). -- A diferencia del Pascal, sols solta diners si en duia (l'original sempre -- escrivia diners a (carrega.x, carrega.y), deixant un $ a (0,0) com a bug). function mort_malo(m) -- Cas A: encara duu carrega (no era atrapat) → solta al lloc original. -- Cas B: era atrapat i ja havia soltat la carrega sobre la cabeça → -- si l'has sepultat (cel·la torna a ser pedra), restaura al -- lloc original perque el mapa segueix sent acabable. if m.carrega.ok then local c = mapa[m.carrega.x][m.carrega.y] c.tipo = DINERS c.color = colors.diners elseif m.dropped_x then local t = mapa[m.dropped_x][m.dropped_y].tipo -- BUIT = Pepe la va recollir; DINERS = encara hi es; pedra/bloc = sepultada if t ~= BUIT and t ~= DINERS then local c = mapa[m.carrega.x][m.carrega.y] c.tipo = DINERS c.color = colors.diners end end m.x = 39; m.y = 1 m.color = m.color_base m.estat = CAENT m.iaclock = 0 m.atrapat = false m.carrega.ok = false m.carrega.x = 0 m.carrega.y = 0 m.dropped_x = nil m.dropped_y = nil sfx_malo_die() end -- Marca un enemic com a atrapat en un forat obert. Es queda quiet fins -- que la cel·la es rellena (i llavors mor per emparedat). Solta la carrega -- de forma immediata sobre la cabeça (o al lloc original si la cabeça no -- pot acollir-la). function atrapar(m) if m.atrapat then return end m.atrapat = true m.estat = NORMAL m.color = colors.malo_atrapat if m.carrega.ok then local cy = m.y - 1 if cy >= 0 and mapa[m.x][cy].tipo == BUIT and mapa[m.x][cy].temps <= 0 then mapa[m.x][cy].tipo = DINERS mapa[m.x][cy].color = colors.diners m.dropped_x = m.x m.dropped_y = cy else -- cabeça ocupada o fora de mapa: fallback al lloc original mapa[m.carrega.x][m.carrega.y].tipo = DINERS mapa[m.carrega.x][m.carrega.y].color = colors.diners m.dropped_x = m.carrega.x m.dropped_y = m.carrega.y end m.carrega.ok = false end end function tic_malos() for i = 1, NUM_MALOS do local m = malos[i] -- Malo atrapat: nomes comprovem si la cel·la s'ha rellenat (emparedat). -- No es mou, no agafa diners, no reaccionara fins que muiga. if m.atrapat then if tipo_a(m.x, m.y) == PEDRA then mort_malo(m) end goto seguent end if m.iaclock == 0 then m.estat = select_estat(m) end m.estat = agafar_escala(m) do local actual = tipo_a(m.x, m.y) -- caiguda (aquesta SI comprova corda — els malos s'agafen a la corda) if not es_suport(m.x, m.y+1) and actual ~= CORDA then m.estat = CAENT end -- si toca terra i venia caent → reconsidera if es_suport(m.x, m.y+1) and m.estat == CAENT then m.estat = select_estat(m) end -- si vol pujar pero no esta en escala → reconsidera if actual == BUIT and m.estat == PUJAR then m.estat = select_estat(m) end -- si vol baixar pero te paret sota → reconsidera if es_paret(m.x, m.y+1) and m.estat == BAIXAR then m.estat = select_estat(m) end end -- aplicar moviment (nomes si la cel·la desti no es paret; -- aixi els malos no es fiquen dins d'altres malos atrapats) if m.estat == DRETA and not es_paret(m.x+1, m.y) then m.x = m.x + 1 elseif m.estat == ESQUERRA and not es_paret(m.x-1, m.y) then m.x = m.x - 1 elseif m.estat == PUJAR and not es_paret(m.x, m.y-1) then m.y = m.y - 1 elseif m.estat == BAIXAR and not es_paret(m.x, m.y+1) then m.y = m.y + 1 elseif m.estat == CAENT and not es_paret(m.x, m.y+1) then m.y = m.y + 1 end -- bordes X (rebot) if m.x < 0 then m.x = 0; m.estat = DRETA end if m.x > MAP_W-1 then m.x = MAP_W-1; m.estat = ESQUERRA end -- bordes Y (clamp) if m.y < 0 then m.y = 0 end if m.y > MAP_H-1 then m.y = MAP_H-1 end -- agafar diners if tipo_a(m.x, m.y) == DINERS and not m.carrega.ok then mapa[m.x][m.y].tipo = BUIT m.color = colors.malo_carrega m.carrega.ok = true m.carrega.x = m.x m.carrega.y = m.y end -- caure dins d'un forat obert (BUIT cavat) → atrapat. No comprovem -- suport sota: en el Lode Runner classic el malo queda agarrat al -- vora del forat encara que sota hi haja aire (forats "flotants" -- a una planta amb buit a sota també atrapen el malo que cau). if mapa[m.x][m.y].tipo == BUIT and mapa[m.x][m.y].temps > 0 then atrapar(m) end -- emparedat → mort if tipo_a(m.x, m.y) == PEDRA then mort_malo(m) end ::seguent:: m.iaclock = m.iaclock + 1 if m.iaclock == TEMPS_IA then m.iaclock = 0 end end end function check_mort_per_malos() if pepe.invuln_t > 0 or pepe.mort_t > 0 then return end for i = 1, NUM_MALOS do local m = malos[i] if not m.atrapat and m.x == pepe.x and m.y == pepe.y then mort_pepe() return end end end -- Quan Pepe ha recollit tots els diners, dispara l'animacio de creixement -- de l'escala a la columna 0 (la posa l'efectua tic_escala_creixent cada -- frame, no aci de cop). Aci nomes activa l'estat la primera vegada. function check_mapa_complet() if diners_pantalla > 0 then return end if escala_state == "idle" then escala_state = "growing" escala_y = 1 escala_t = 0 end end -- Tic de creixement de l'escala lateral, cridat des de update_playing. -- Cada escala_step_frames afig una cel·la d'escala a (0, escala_y) i sona -- un beep ascendent. Para quan troba pedra o arriba al final del mapa. function tic_escala_creixent() if escala_state ~= "growing" then return end escala_t = escala_t + 1 if escala_t < temps.escala_step_frames then return end escala_t = 0 if escala_y > MAP_H - 2 or tipo_a(0, escala_y) == PEDRA then escala_state = "done" return end mapa[0][escala_y].tipo = ESCALA mapa[0][escala_y].color = colors.escala sfx_escala_step(escala_y) escala_y = escala_y + 1 end -- Anima els forats: decrementa temps i cambia el tipus segons la fase -- (idem case statement de CheckMapa al RUNNER.PAS) function check_mapa() for x = 0, MAP_W-1 do for y = 0, MAP_H-1 do local c = mapa[x][y] local t = c.temps if t == 0 then c.temps = -1 c.tipo = PEDRA c.color = colors.pedra elseif t == 1 or t == BLOC_OUT-1 then c.tipo = BLOC3; c.color = colors.pedra; c.temps = t - 1 elseif t == 2 or t == BLOC_OUT-2 then c.tipo = BLOC2; c.color = colors.pedra; c.temps = t - 1 elseif t == 3 or t == BLOC_OUT-3 then c.tipo = BLOC1; c.color = colors.pedra; c.temps = t - 1 elseif t == 4 or t == BLOC_OUT-4 then c.tipo = BUIT; c.color = colors.bg; c.temps = t - 1 elseif t > 0 then c.temps = t - 1 end -- t == -1 → idle, no fer res end end check_mapa_complet() end -- HUD: rotul inferior amb level/score/vides/hi-score sobre banda blava function pintar_hud() color(colors.hud_text, colors.hud_bg) local blank = " " -- Banda inferior completa (5 files, rows 25..29) amb marc decoratiu -- al voltant (cantoneres + linies fines) i text als rows 26 i 28. for y = 25, 29 do print(blank, 0, y) end -- Marc print(chr(FRAME_TL), 0, 25) print(chr(FRAME_TR), 39, 25) print(chr(FRAME_BL), 0, 29) print(chr(FRAME_BR), 39, 29) for x = 1, 38 do print(chr(FRAME_H), x, 25) print(chr(FRAME_H), x, 29) end for y = 26, 28 do print(chr(FRAME_V), 0, y) print(chr(FRAME_V), 39, y) end -- Text dins del marc print(textos.level_label.." "..string.format("%02d", level), 3, 26) -- Punts: etiqueta sempre en hud_text, digits en groc mentre el contador -- animat encara no ha alcançat el valor real (sensació de coin pickup). local score_label = textos.score_label.." " print(score_label, 16, 26) if score_display < score then color(colors.score_flash, colors.hud_bg) end print(string.format("%03d", score_display), 16 + strlen(score_label), 26) color(colors.hud_text, colors.hud_bg) -- max(0, ...) evita que la pantalla mostre "Vides /" en el game over: -- l'implementacio de tostr() d'ascii torna "/" per a -1 (bug intern). print(textos.lives_label.." "..tostr(max(0, pepe.vides)), 29, 26) local r = textos.record_label.." "..string.format("%03d", hi_score).." "..nom_hi_score print(r, flr((40 - strlen(r)) / 2), 28) end -- Carrega config.lua si existeix. Si falta o te errors, queden els defaults. -- Despres del dofile, si l'usuari ha fet "colors = {bg=X}" en lloc d'assignar -- camp a camp, hi haura claus que falten. Fem un merge amb les defaults -- guardades per a que no quede res a nil. function carregar_config() -- Accedim per nom (_G) per a que aço seguisca funcionant fins i tot si -- l'usuari reasigna tota la taula en config.lua (ex: `colors = {bg=...}` -- en lloc de mutar camps individuals). local noms = { "keys", "textos", "temps" } local saved = {} for _, nom in ipairs(noms) do saved[nom] = {} for k, v in pairs(_G[nom]) do saved[nom][k] = v end end pcall(dofile, "config.lua") for _, nom in ipairs(noms) do for k, v in pairs(saved[nom]) do if _G[nom][k] == nil then _G[nom][k] = v end end end end function init() carregar_config() carregar_skin() mode(1) border(colors.border) color(colors.bg, colors.bg) -- ink no importa, paper=bg perque el cls() final empleni amb bg definir_glifs() definir_marc() carregar_records() init_title() set_estat(ESTAT_TITLE) cls() end function update() -- Contador animat del score: independent de l'estat, així segueix pujant -- sobre el overlay del game over (no es queda congelat a meitat camí). if score_display < score and (cnt() % temps.score_step) == 0 then score_display = score_display + 1 end if estat_joc == ESTAT_TITLE then update_title() elseif estat_joc == ESTAT_PLAYING then update_playing() elseif estat_joc == ESTAT_GAMEOVER then update_gameover() elseif estat_joc == ESTAT_ENTERNAME then update_entername() end -- Overlay del fade encima de tot, i avanc del seu temporitzador pintar_fade() update_fade() end