1222 lines
44 KiB
Lua
1222 lines
44 KiB
Lua
-- 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
|