diff --git a/config.lua b/config.lua index b7d3350..0b8acf9 100644 --- a/config.lua +++ b/config.lua @@ -78,3 +78,22 @@ skin = "native" -- textos.record_label = "Record" -- textos.name_label = "Nom:" -- textos.name_help = "(A-Z)" + +-- ==================================================================== +-- TEMPS I DURACIONS +-- ==================================================================== +-- Tots els valors van en *frames* (a 60 fps, 60 frames = 1 segon). +-- Pots ajustar la sensacio del joc sense tocar el .lua. + +-- temps.score_step = 3 -- frames per cada +1 del comptador +-- -- animat del HUD. 3 = ~20 punts/segon. +-- temps.fade_frames = 18 -- duracio del fade out (i del fade in) +-- -- entre pantalles. 18 = 0.3 s per fase. +-- temps.mort_anim_frames = 30 -- duracio de l'animacio visual de mort +-- -- (Pepe parpadejant + careta trista). +-- temps.respawn_delay_frames = 120 -- temps que Pepe queda invisible abans +-- -- del respawn (els malos segueixen). +-- temps.invuln_frames = 180 -- temps que Pepe es invulnerable al +-- -- respawn, parpadejant blanc/groc. +-- temps.escala_step_frames = 6 -- frames entre cada cel·la nova de +-- -- l'escala lateral quan creix. diff --git a/pepe_runner_dx.lua b/pepe_runner_dx.lua index 3bf6a0d..efe2921 100644 --- a/pepe_runner_dx.lua +++ b/pepe_runner_dx.lua @@ -30,6 +30,10 @@ 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 @@ -113,6 +117,17 @@ 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" @@ -122,9 +137,11 @@ 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 } +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 @@ -132,6 +149,26 @@ 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) @@ -160,12 +197,63 @@ function definir_marc() 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 @@ -204,6 +292,10 @@ function carregar_mapa(num) 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 @@ -226,8 +318,33 @@ function pintar_mapa(offset_x) end function pintar_pepe() - color(pepe.color, colors.bg) - print(chr(glif[pepe.dibuix]), pepe.x, pepe.y) + 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) @@ -277,7 +394,8 @@ function tic_pepe() -- Final pantalla: si arriba a la fila 1, passa al nivel seguent if pepe.y == 1 then - fase_nova() + sfx_level() + transicio(function() fase_nova() end) return end @@ -313,33 +431,47 @@ function tic_pepe() 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() - pepe.vides = pepe.vides - 1 + 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 - sfx_die() + 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) +-- 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() - sfx_level() end -- ==================================================================== @@ -403,17 +535,18 @@ function init_title() end function update_title() - if btnp(KEY_SPACE) then - inicialitzacio() - set_estat(ESTAT_PLAYING) - return + 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) - if flr(cnt() / 30) % 2 == 0 then + -- 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) @@ -436,16 +569,20 @@ function update_gameover() pintar_hud() - -- Despres de 2 segons (120 frames), transicio - if temps_estat() > 120 then + -- 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 - hi_score = score - nom_hi_score = "AAA" - enter_name_idx = 1 - set_estat(ESTAT_ENTERNAME) + transicio(function() + hi_score = score + nom_hi_score = "AAA" + enter_name_idx = 1 + set_estat(ESTAT_ENTERNAME) + end) else - init_title() - set_estat(ESTAT_TITLE) + transicio(function() + init_title() + set_estat(ESTAT_TITLE) + end) end end end @@ -466,6 +603,8 @@ function update_entername() 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) @@ -473,46 +612,74 @@ function update_entername() string.sub(nom_hi_score, enter_name_idx+1) enter_name_idx = enter_name_idx + 1 if enter_name_idx > 3 then - guardar_records() - init_title() - set_estat(ESTAT_TITLE) + transicio(function() + guardar_records() + init_title() + set_estat(ESTAT_TITLE) + end) end end end -- ----- PLAYING ----- function update_playing() - -- Abandonar partida → flux de game over (como en el RUNNER.PAS amb ESC) - if btnp(keys.quit) then - sfx_gameover() - set_estat(ESTAT_GAMEOVER) - return - end + -- 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() - -- Cavar es immediat (un sol forat per pulsacio) - if 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 - tic_pepe() - check_mort_per_malos() - if (game_tic % MALO_RATIO) == 0 then - tic_malos() - check_mort_per_malos() + -- 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 - check_mapa() - if pepe.vides < 0 then + 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 + -- Render (sempre, fins i tot durant el fade — el dither es pinta encima) neteja_fons() pintar_mapa() pintar_malos() @@ -673,6 +840,7 @@ function tic_malos() 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() @@ -681,17 +849,36 @@ function check_mort_per_malos() end end --- Si Pepe ha recollit tots els diners, fa apareixer una escala a la columna 0 --- des de la fila 1 cap avall, parant si troba pedra. (CheckMapaComplet) +-- 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 - for j = 1, MAP_H-2 do - if mapa[0][j].tipo == PEDRA then break end - mapa[0][j].tipo = ESCALA - mapa[0][j].color = colors.escala + 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() @@ -744,7 +931,17 @@ function pintar_hud() -- Text dins del marc print(textos.level_label.." "..string.format("%02d", level), 3, 26) - print(textos.score_label.." "..string.format("%03d", score), 16, 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) @@ -757,19 +954,20 @@ end -- 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() - local saved_colors, saved_keys, saved_textos = {}, {}, {} - for k, v in pairs(colors) do saved_colors[k] = v end - for k, v in pairs(keys) do saved_keys[k] = v end - for k, v in pairs(textos) do saved_textos[k] = v end + -- 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 k, v in pairs(saved_colors) do - if colors[k] == nil then colors[k] = v end - end - for k, v in pairs(saved_keys) do - if keys[k] == nil then keys[k] = v end - end - for k, v in pairs(saved_textos) do - if textos[k] == nil then textos[k] = v end + 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 @@ -788,9 +986,19 @@ function init() 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