Files
pepe-runner-ascii/pepe_runner_dx.lua
T

1005 lines
34 KiB
Lua

-- 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
-- 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 fora dels SKINS per a que
-- funcione igual en custom i native, sense dependre del ROM nadiu.
FADE_BLOCK = 246
-- Skins: cada skin es una taula tile_id → codi_de_glif_a_pintar.
-- "custom" usa els mateixos codis CP437 (redibuixats amb setchar).
-- "native" remapeja als glifs que ja existeixen al ROM d'ascii
-- (mira chuleta_font_ascii.png per a identificar-los).
SKINS = {
custom = {
[BUIT]=0, [DINERS]=36, [PEDRA]=219, [ESCALA]=205, [CORDA]=196,
[BLOC1]=176, [BLOC2]=177, [BLOC3]=178,
[PEPE_C]=2, [MALO_C]=88,
},
native = {
[BUIT]=0, [DINERS]=36, [PEDRA]=233, [ESCALA]=61, [CORDA]=45,
[BLOC1]=216, [BLOC2]=218, [BLOC3]=220,
[PEPE_C]=224,[MALO_C]=88,
},
}
glif = SKINS.custom -- s'actualitza a init() segons la config
-- 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
}
-- Paleta — tots els colors son sobreescriuibles per config.lua.
-- bg es el color del fons del nivell (pintat tant a les cel·les buides com
-- darrere de cada glif), per a que tot quede integrat en lloc de tindre
-- negre darrere dels caracters.
colors = {
bg = COLOR_BLUE, -- fons del nivell
pedra = COLOR_BROWN,
diners = COLOR_YELLOW,
escala = COLOR_LIGHT_CYAN,
corda = COLOR_LIGHT_GRAY,
pepe = COLOR_WHITE,
malo = COLOR_LIGHT_RED,
malo_carrega = COLOR_LIGHT_MAGENTA, -- enemic portant diners
hud_text = COLOR_WHITE,
hud_bg = COLOR_BLACK,
border = COLOR_BLUE,
title = COLOR_LIGHT_RED,
}
-- 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, color=colors.pepe, 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=... }
-- 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 },
}
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)
-- Nomes la skin "custom" redibuixa els glifs CP437. La "native" usa
-- els que ja existeixen al ROM d'ascii (cap setchar mes).
if skin ~= "custom" then return end
setchar(219, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF) -- PEDRA █
setchar(36, 0x18,0x3E,0x60,0x3C,0x06,0x7C,0x18,0x00) -- DINERS $
setchar(205, 0x00,0x00,0xFF,0x00,0x00,0xFF,0x00,0x00) -- ESCALA ═
setchar(196, 0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00) -- CORDA ─
setchar(176, 0x44,0x11,0x44,0x11,0x44,0x11,0x44,0x11) -- BLOC1 ░
setchar(177, 0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55) -- BLOC2 ▒
setchar(178, 0xBB,0xEE,0xBB,0xEE,0xBB,0xEE,0xBB,0xEE) -- BLOC3 ▓
setchar(2, 0x7E,0x81,0xA5,0x81,0xBD,0x99,0x81,0x7E) -- PEPE ☻
setchar(88, 0x00,0xC3,0x66,0x3C,0x18,0x3C,0x66,0xC3) -- MALO X
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
-- ====================================================================
-- 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
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
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 = pepe.color
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: glif passa a la careta trista (codi 225 del ROM
-- d'ascii, no redefinit per cap skin) parpadejant roig/bg.
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
g = 225
col = ((fase - tram_curt) % 12 < 6) and colors.malo or colors.bg
end
elseif pepe.invuln_t > 0 then
-- Invulnerabilitat: parpadeja entre blanc i groc cada 6 frames.
col = (pepe.invuln_t % 12 < 6) and colors.pepe or colors.diners
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 tipo_a(pepe.x+dx, pepe.y) ~= PEDRA
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)
if btn(keys.up) then
if actual == ESCALA then pepe.y = pepe.y - 1 end
elseif btn(keys.down) then
if 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 tipo_a(pepe.x-1, pepe.y) ~= PEDRA and pepe.estat ~= CAENT then
pepe.x = pepe.x - 1
end
elseif btn(keys.right) then
if tipo_a(pepe.x+1, pepe.y) ~= PEDRA 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 es escala/pedra → cau
actual = tipo_a(pepe.x, pepe.y)
sotto = tipo_a(pepe.x, pepe.y+1)
if (sotto ~= ESCALA and sotto ~= PEDRA)
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
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()
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()
-- 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()
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()
-- 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()
malos = {
{ x= 9, y=2, color=colors.malo, estat=ESQUERRA, iaclock=0, carrega={ok=false, x=0, y=0} },
{ x=20, y=2, color=colors.malo, estat=ESQUERRA, iaclock=0, carrega={ok=false, x=0, y=0} },
{ x=39, y=2, color=colors.malo, estat=ESQUERRA, iaclock=0, carrega={ok=false, x=0, y=0} },
}
end
function pintar_malos()
for i = 1, NUM_MALOS do
local m = malos[i]
color(m.color, colors.bg)
print(chr(glif[MALO_C]), 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 tipo_a(m.x+1, m.y) ~= PEDRA then nou = nou | DRETA end
if tipo_a(m.x-1, m.y) ~= PEDRA 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)
local sotto = tipo_a(m.x, m.y+1)
if sotto ~= PEDRA and sotto ~= ESCALA 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)
-- El comptador diners_pantalla NO canvia: l'enemic agafant/soltant es
-- transitori, sols compta el que el Pepe recull definitivament.
if m.carrega.ok then
local c = mapa[m.carrega.x][m.carrega.y]
c.tipo = DINERS
c.color = colors.diners
end
m.x = 39; m.y = 1
m.color = colors.malo
m.estat = CAENT
m.iaclock = 0
m.carrega.ok = false
m.carrega.x = 0
m.carrega.y = 0
sfx_malo_die()
end
function tic_malos()
for i = 1, NUM_MALOS do
local m = malos[i]
if m.iaclock == 0 then m.estat = select_estat(m) end
m.estat = agafar_escala(m)
local actual = tipo_a(m.x, m.y)
local sotto = tipo_a(m.x, m.y+1)
-- caiguda (aquesta SI comprova corda — els malos s'agafen a la corda)
if sotto ~= PEDRA and sotto ~= ESCALA and actual ~= CORDA then
m.estat = CAENT
end
-- si toca terra i venia caent → reconsidera
if (sotto == PEDRA or sotto == ESCALA) 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 pedra sota → reconsidera
if sotto == PEDRA and m.estat == BAIXAR then
m.estat = select_estat(m)
end
-- aplicar moviment
if m.estat == DRETA then m.x = m.x + 1
elseif m.estat == ESQUERRA then m.x = m.x - 1
elseif m.estat == PUJAR then m.y = m.y - 1
elseif m.estat == BAIXAR then m.y = m.y + 1
elseif m.estat == CAENT 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
-- emparedat → mort
if tipo_a(m.x, m.y) == PEDRA then
mort_malo(m)
end
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
if malos[i].x == pepe.x and malos[i].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.diners, 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 = { "colors", "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()
glif = SKINS[skin] or SKINS.custom
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