Files

1222 lines
44 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Pepe Runner DX — versio millorada del port amb QoL i extres.
-- Tots els valors editables del joc viuen a `config.lua` (skin, tecles,
-- textos, temps, vides, audio). Aquest fitxer nomes declara les globals
-- buides; `carregar_config()` les ompli al iniciar.
-- ====================================================================
-- GLOBALS DE CONFIGURACIO (els ompli config.lua via carregar_config())
-- ====================================================================
-- Declarats aci nomes per a documentar que existeixen. El valor real
-- arriba sempre de config.lua. Si config.lua no defineix algun, el joc
-- petara al usar-lo — millor que un default amagat darrere.
skin = nil
keys = nil
textos = nil
temps = nil
musica = nil
music_on = nil
VIDES_INI = nil
LEVEL_INI = nil
version = nil -- string que es mostra centrat baix a la pantalla de titol
-- 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 = 247 -- (fora del rang ASCII basic per a no pisar la lletra 'X')
-- 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.
-- Tots fora del rang ASCII basic per a no pisar lletres / signes.
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 = 248 -- enemic 2 (cada malos[i] usa el seu propi sprite)
MALO_C3 = 249 -- enemic 3
-- Glifs del marc del HUD. Codis triats fora del rang ASCII basic per a
-- no pisar lletres ni signes quan s'imprimeix text.
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/<nom>.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 = {}
-- 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)
-- 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: la inicialitzacio real (vides=VIDES_INI, posicio inicial...) la
-- fa inicialitzacio() despres de carregar config.lua. Aci nomes declarem
-- el shape per a que existeixca durant title/gameover (no es pinta).
pepe = { x=19, y=23, dibuix=PEPE_C, vides=0, 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=... }
-- Id del jingle reproduit actualment (nil = silenci). Les melodies
-- (musica.title, musica.entername) i music_on viuen a config.lua.
music_actual = nil
-- Buffer d'edges d'input direccional (esquerra/dreta/dalt/baix).
--
-- La logica del joc avanca cada TICS=6 frames pero el bucle d'update() corre
-- a 60 fps. Bufferitzem nomes les *transicions* (btnp = edge de pulsacio):
-- aixi un tap rapid (1-9 frames) genera UN sol moviment, encara que caiga
-- entre dos tics. El moviment "mantingut" funciona per btn() directe al tic
-- (vore tic_pepe): si la tecla segueix premuda quan arriba el tic, mou.
--
-- Per que no bufferitzar el btn()? Perque un tap de 7-9 frames (un tap humà
-- "normal" de ~130 ms) podria abarcar 2 tics consecutius i Pepe avançaria
-- 2 caselles d'un sol toc. Bufferitzar nomes l'edge garanteix 1 = 1.
input_buf = { up=false, down=false, left=false, right=false }
function sample_input()
if btnp(keys.up) then input_buf.up = true end
if btnp(keys.down) then input_buf.down = true end
if btnp(keys.left) then input_buf.left = true end
if btnp(keys.right) then input_buf.right = true end
end
function reset_input()
input_buf.up, input_buf.down, input_buf.left, input_buf.right = false, false, false, false
end
-- 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/<skin>.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
function sfx_typewriter() sound(2500, 2) end -- tic agut i molt curt per a cada lletra
-- 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]
-- En game over, Pepe queda visible amb la cara trista alternant A/B
-- (parpadeig amb cnt() perque mort_t ja no es decrementa fora de playing).
if estat_joc == ESTAT_GAMEOVER then
if (cnt() % 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
color(col, colors.bg)
print(chr(g), pepe.x, pepe.y)
return
end
if pepe.mort_t > 0 then
-- En l'ultima vida no hi ha respawn_delay: tot el countdown es anim.
local mort_t_max
if pepe.vides == 0 then
mort_t_max = temps.mort_anim_frames
else
mort_t_max = temps.mort_anim_frames + temps.respawn_delay_frames
end
-- Fase d'espera (Pepe invisible, mapa segueix viu): no pintem res.
local fase = mort_t_max - pepe.mort_t -- 0..mort_t_max-1
if fase >= temps.mort_anim_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 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).
-- Edge bufferitzat (input_buf, per a taps curts) OR tecla mantinguda (btn).
if input_buf.up or btn(keys.up) then
if actual == ESCALA and not es_paret(pepe.x, pepe.y-1) then
pepe.y = pepe.y - 1
end
elseif input_buf.down or 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 input_buf.left or 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 input_buf.right or 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)
-- Si es l'ultima vida (vides==0), saltem el delay: l'animacio termina i
-- anem directament a game over, sense pausa invisible.
-- 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
if pepe.vides == 0 then
-- Ultima vida: directament sona sfx_gameover (no sfx_die seguit de
-- sfx_gameover, eren molt semblants i es solapaven com a doble eco).
pepe.mort_t = temps.mort_anim_frames
sfx_gameover()
else
pepe.mort_t = temps.mort_anim_frames + temps.respawn_delay_frames
sfx_die()
end
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 = LEVEL_INI
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
-- Descarta qualsevol edge que haguera quedat al input_buf des de la
-- partida anterior (si vam morir entre dos tics, el reset programat
-- no s'arribava a executar perque salirem amb un return prematur).
reset_input()
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), 23)
end
-- Versio del joc, centrada a la part baixa (es desvaneix amb el fade)
color(colors.version, colors.bg)
print(version, flr((40 - strlen(version)) / 2), 28)
end
-- ----- GAME OVER -----
function update_gameover()
set_music(nil)
-- Render congelat: mapa + malos + Pepe trist (pintar_pepe detecta l'estat)
neteja_fons()
pintar_mapa()
pintar_malos()
pintar_pepe()
-- Typewriter: les lletres apareixen una a una despres d'un petit delay
-- inicial perque no es solapin amb la sonoritat de sfx_gameover().
-- Aban apareix un cuadre de fons (strlen+2 × 3) per a aillar el text
-- del mapa que queda darrere.
local g = textos.game_over
local t = temps_estat() - temps.gameover_delay_frames
if t >= 0 then
local total_chars = strlen(g)
local frames_typewriter = total_chars * temps.typewriter_step_frames
local text_x = flr((40 - total_chars) / 2)
local text_y = 12
-- Cuadre de fons: una fila per damunt i una per davall del text,
-- i una columna a cada costat. Es pinta primer per a que el text
-- quede a sobre.
local box_w = total_chars + 2
color(colors.gameover_box, colors.gameover_box)
local blank = string.rep(" ", box_w)
for dy = -1, 1 do print(blank, text_x - 1, text_y + dy) end
-- Lletres del typewriter
local nletres = flr(t / temps.typewriter_step_frames) + 1
if nletres > total_chars then nletres = total_chars end
color(colors.title, colors.gameover_box)
print(substr(g, 0, nletres), text_x, text_y)
-- Tic nomes en el frame en que apareix una lletra nova, i nomes
-- mentre el typewriter encara escriu (no en bucle infinit).
if t < frames_typewriter and (t % temps.typewriter_step_frames) == 0 then
sfx_typewriter()
end
end
pintar_hud()
-- Quan el typewriter ha acabat i el text ha estat visible un temps,
-- transicionem a title o a la pantalla d'entrar nom (si hi ha record).
local total = temps.gameover_delay_frames
+ strlen(g) * temps.typewriter_step_frames
+ temps.gameover_show_frames
if not fade_actiu() and temps_estat() > total 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()
-- Titol amb asteriscos a banda i banda per a donar "feel" arcade
color(colors.title, colors.bg)
local t = "* * "..textos.new_record.." * *"
print(t, flr((40 - strlen(t)) / 2), 10)
-- Puntuacio amb dos punts (consistencia amb "Nom:")
color(colors.diners, colors.bg)
local s = textos.score_label..": "..string.format("%03d", hi_score)
print(s, flr((40 - strlen(s)) / 2), 12)
-- Bloc del nom dins d'un marc decoratiu (reutilitzem els FRAME_* del HUD)
color(COLOR_WHITE, colors.bg)
local n = textos.name_label.." "..nom_hi_score -- "Nom: AAA"
local inner_w = strlen(n) + 2 -- 1 espai de padding cada costat
local frame_w = inner_w + 2 -- + 2 esquines
local frame_x = flr((40 - frame_w) / 2)
local top = chr(FRAME_TL)..string.rep(chr(FRAME_H), inner_w)..chr(FRAME_TR)
local mid = chr(FRAME_V).." "..n.." "..chr(FRAME_V)
local bot = chr(FRAME_BL)..string.rep(chr(FRAME_H), inner_w)..chr(FRAME_BR)
print(top, frame_x, 15)
print(mid, frame_x, 16)
print(bot, frame_x, 17)
-- Ajuda just sota el marc — 10 chars = exactament l'amplada interna del
-- marc (cols 15-24), per a evitar l'asimetria del "(A-Z)" original.
print(textos.name_help, flr((40 - strlen(textos.name_help)) / 2), 18)
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
-- Sample d'input cada frame (acumulat fins al proxim tic_pepe)
sample_input()
-- 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 ja s'ha tocat al inici de l'animacio (mort_pepe)
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 reset_input(); return end
check_mort_per_malos()
end
-- Reset incondicional del buffer al tic: si Pepe estava muriguent,
-- descartem l'input acumulat durant l'animacio (no volem que el
-- primer moviment despres del respawn siga una tecla pulsada fa 2 s).
reset_input()
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
-- config.lua es l'unica fuent de verdat per a tot lo configurable
-- (skin, keys, textos, temps, vides, audio). El carreguem amb dofile
-- directe — si peta o falta una clau, millor un error visible ara que
-- un valor sorpresa enmig d'una partida.
function carregar_config()
dofile("config.lua")
end
function init()
wintitle("© 2026 Pepe Runner DX — JailDesigner")
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()
if btnp(KEY_ESCAPE) then os.exit(0) end
-- 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