From 48d6ec2c9031a8483ecf4342b3c17b0e537c8bf4 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 16 May 2026 08:18:33 +0200 Subject: [PATCH] =?UTF-8?q?Enemics,=20vides=20i=20animaci=C3=B3=20de=20mor?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pepe_pintor.lua | 395 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 334 insertions(+), 61 deletions(-) diff --git a/pepe_pintor.lua b/pepe_pintor.lua index 66eae90..f5e186d 100644 --- a/pepe_pintor.lua +++ b/pepe_pintor.lua @@ -1,48 +1,72 @@ -- Pepe el Pintor — port a ascii/Lua del joc original en Turbo Pascal -- (Sergi Valor Martínez, 1999). Base: PINTOR3.PAS. -- --- ITER 1: motor mínim — fase única (la 3 = rectangle gran), Pepe es mou amb --- O P Q A, pinta el fons al pas, gasta el pot de pintura i el recarrega al --- tornar a la posició inicial. Sense enemics, vides ni transicions. +-- ITER 2: motor base + enemics + vides. +-- - Fase única (la 3 = rectangle gran) +-- - O P Q A per a moure, pinta al pas, gasta el pot +-- - Recàrrega al tornar al pot (fidel al Pascal: sound(i*10)) +-- - 3 enemics amb spawn diferit per llindar de blocs pintats +-- - Moviment IA dels enemics fidel a moure_malos (random eix + perseguir) +-- - 5 vides, animació de mort, game over, victòria -- ==================================================================== -- CONSTANTS -- ==================================================================== MAP_W, MAP_H = 40, 25 -HUD_Y0 = 25 -- HUD ocupa files 25..29 +HUD_Y0 = 25 --- Glifs (codis CP437 originals). Els redefinim amb setchar per a fidelitat. -PEPE_PLE = 2 -- ☻ Pepe amb pintura (cara plena) -PEPE_BUIT = 1 -- ☺ Pepe sense pintura (cara buida) +-- Glifs (codis CP437). Els redefinim amb setchar per a fidelitat. +PEPE_PLE = 2 -- ☻ Pepe amb pintura +PEPE_BUIT = 1 -- ☺ Pepe sense pintura PARED = 0xB1 -- ▒ paret -FONS = 0xDB -- █ fons (sense pintar i pintat, distingit per atribut) +FONS = 0xDB -- █ fons (sense pintar / pintat = distingit per `pintat`) POT_GLIF = 232 -- ◘ marca visual del pot +MALO_GLIF = 88 -- 'X' enemic -- Constants del joc POT_MAX = 90 -PEPE_INI_X = 37 -- cambra del pot, 1 cel·la sortint del marc a la dreta (fidel al PINTOR3) +PEPE_INI_X = 37 -- cambra del pot (sortint 1 cel·la del marc a la dreta) PEPE_INI_Y = 11 -TICS_MOVIMENT = 5 -- frames entre intents de moviment (60/5 = 12 Hz) +TICS_MOVIMENT = 5 -- frames per tic de moviment (60/5 = 12 Hz) TICS_OMPLIR = 3 -- frames entre +1 de pot al recarregar +NUM_MALOS = 3 +VIDES_INI = 5 + +-- Llindars d'aparició dels enemics (proporcionals al PINTOR3 escalat als +-- nostres ~829 blocs). Apareixen quan total_blocs baixa per sota d'aquests. +BLOCS_SPAWN = { 750, 520, 370 } + +-- Animació de mort: 10000 Hz → 1000 Hz, restant 250 cada pas +MORT_FREQ_INI = 10000 +MORT_FREQ_END = 1000 +MORT_FREQ_STEP = 250 +MORT_FRAMES = 36 -- ~600 ms d'animació de mort + +-- Estats +ESTAT_PLAYING = "playing" +ESTAT_MURIGUENT = "muriguent" +ESTAT_GAMEOVER = "gameover" +ESTAT_VICTORIA = "victoria" + -- ==================================================================== --- ESTAT +-- ESTAT GLOBAL -- ==================================================================== -mapa = {} -- mapa[x][y] = { tipo=, pintat=bool } +mapa = {} pepe = { x=PEPE_INI_X, y=PEPE_INI_Y, pinta=true } pot = POT_MAX -total_blocs = 0 -- es calcula al carregar mapa -omplint = false -- en mig d'una recàrrega? -omplir_i = 0 -- comptador del bucle de recàrrega (fidel al Pascal) -omplir_max = 0 -- valor de (90-pot) en el moment d'entrar al pot +total_blocs = 0 +omplint = false +omplir_i = 0 +omplir_max = 0 --- Buffer d'edges d'input direccional. --- update() corre a 60 fps però la lògica del Pepe avança cada TICS_MOVIMENT --- frames. Si l'usuari fa un tap curt entre dos tics, btn() al moment del tic --- ja no detecta la pulsació. Bufferitzem només l'edge (btnp), així un tap --- ràpid sempre genera UN moviment. El moviment "mantingut" segueix funcionant --- per btn() directe al tic. Buffer binari: si poses 3 taps en un tic, només --- compta com 1 — no se sobren moviments al pròxim tic. +malos = {} -- { {x, y, viu=bool, aparegut=bool}, ... } +vides = VIDES_INI + +estat_joc = ESTAT_PLAYING +estat_t = 0 -- contador de frames dins de l'estat actual + +-- Buffer d'edges d'input direccional (vore explicació a l'iter 1). input_buf = { up=false, down=false, left=false, right=false } function sample_input() @@ -60,16 +84,14 @@ end -- GLIFS CUSTOM (bitmaps CP437) -- ==================================================================== function definir_glifs() - -- Pepe ple (CP437 chr 2 — smiley relleno) setchar(PEPE_PLE, 0x7E, 0xFF, 0xDB, 0xFF, 0xC3, 0xE7, 0xFF, 0x7E) - -- Pepe buit (CP437 chr 1 — smiley contorn) setchar(PEPE_BUIT, 0x7E, 0x81, 0xA5, 0x81, 0xBD, 0x99, 0x81, 0x7E) - -- Paret ▒ (CP437 chr 177 — half-tone gris) setchar(PARED, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11) - -- Fons █ ple setchar(FONS, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF) - -- Pot de pintura: un trapezoide amb pintura dins (sprite propi) setchar(POT_GLIF, 0x00, 0x7E, 0x7E, 0x42, 0x66, 0x7E, 0x3C, 0x00) + -- L'enemic 'X' del CP437 ja és coherent al ROM nadiu d'ascii, però el + -- redefinim igualment per a controlar-ho. + setchar(MALO_GLIF, 0x00, 0xC3, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0xC3) end -- ==================================================================== @@ -80,6 +102,21 @@ function tipo_a(x, y) return mapa[x][y].tipo end +-- Una cel·la és "pintada" si Pepe o un malo hi han passat. Els malos només +-- es poden moure per cel·les pintades (fidel al Pascal: a_v(destí) == bg). +function pintat_a(x, y) + if x < 0 or x >= MAP_W or y < 0 or y >= MAP_H then return false end + return mapa[x][y].pintat +end + +function hi_ha_malo(x, y) + for i = 1, NUM_MALOS do + local m = malos[i] + if m.viu and m.x == x and m.y == y then return true end + end + return false +end + function carregar_mapa(num) filein("maps/"..tostr(num)..".map", 0, MAP_W*MAP_H) total_blocs = 0 @@ -91,8 +128,7 @@ function carregar_mapa(num) if b == FONS then total_blocs = total_blocs + 1 end end end - -- La cel·la del pot no es pinta: la marquem com "ja pintada" i la traiem - -- del compte, així `total_blocs == 0` és assolible com a condició de victòria. + -- La cel·la del pot no es pinta: ja "pintada" per a la condició de victòria mapa[PEPE_INI_X][PEPE_INI_Y].pintat = true total_blocs = total_blocs - 1 end @@ -108,13 +144,11 @@ function pintar_mapa() color(COLOR_BROWN, COLOR_BLACK) print(chr(FONS), x, y) end - -- FONS no pintat: deixem la cel·la negra (cls); no es dibuixa res end end end function pintar_pot() - -- Dibuixem la marca visual del pot només si Pepe NO hi és damunt if not (pepe.x == PEPE_INI_X and pepe.y == PEPE_INI_Y) then color(COLOR_YELLOW, COLOR_BROWN) print(chr(POT_GLIF), PEPE_INI_X, PEPE_INI_Y) @@ -124,7 +158,9 @@ end function pintar_pepe() local glif = pepe.pinta and PEPE_PLE or PEPE_BUIT local fons_color - if mapa[pepe.x][pepe.y].pintat then + if pepe.x == PEPE_INI_X and pepe.y == PEPE_INI_Y then + fons_color = COLOR_BROWN -- damunt del pot + elseif mapa[pepe.x][pepe.y].pintat then fons_color = COLOR_BROWN else fons_color = COLOR_BLACK @@ -133,6 +169,18 @@ function pintar_pepe() print(chr(glif), pepe.x, pepe.y) end +function pintar_malos() + for i = 1, NUM_MALOS do + local m = malos[i] + if m.viu then + -- Color enemic: cian clar (107 = 0x6B → bg 6 marró + fg 11 cian) + color(COLOR_LIGHT_CYAN, COLOR_BROWN) + print(chr(MALO_GLIF), m.x, m.y) + end + end +end + +-- HUD: pintura, blocs restants, vides (com a ☻), controls function pintar_hud() color(COLOR_LIGHT_GRAY, COLOR_BLUE) local blank = " " @@ -140,31 +188,40 @@ function pintar_hud() color(COLOR_WHITE, COLOR_BLUE) print("PINTURA "..string.format("%02d", pot).."/"..tostr(POT_MAX), 1, 26) + color(COLOR_YELLOW, COLOR_BLUE) - print("BLOCS "..string.format("%04d", total_blocs), 22, 26) + print("BLOCS "..string.format("%04d", total_blocs), 17, 26) + + color(COLOR_LIGHT_RED, COLOR_BLUE) + for i = 1, vides do + print(chr(PEPE_PLE), 32 + i, 26) + end color(COLOR_LIGHT_CYAN, COLOR_BLUE) print("O P Q A: moure", 1, 28) end -- ==================================================================== --- LÒGICA DEL JOC +-- LÒGICA DEL PEPE -- ==================================================================== - --- Intent de moviment en (dx, dy). Si la cel·la destí és paret, no es mou. --- Si és fons no pintat i hi ha pintura, la pinta i descompta del pot. function intent_moviment(dx, dy) local nx, ny = pepe.x + dx, pepe.y + dy local t = tipo_a(nx, ny) if t == PARED then return end if t ~= FONS then return end + -- Detecció de mort: si la cel·la destí té un malo (fidel a "a_v=bicho") + if hi_ha_malo(nx, ny) then + mort_pepe() + return + end + pepe.x, pepe.y = nx, ny local c = mapa[nx][ny] - -- La cel·la del pot no es pinta mai (es queda neta per a la recàrrega) + -- El pot no es pinta if nx == PEPE_INI_X and ny == PEPE_INI_Y then - sound(2000, 4) + sound(2000, 3) return end @@ -176,30 +233,29 @@ function intent_moviment(dx, dy) sound(5000, 3) pepe.pinta = (pot > 0) else - sound(800, 3) + sound(1000, 3) -- so més greu quan no hi ha pintura (fidel: nota=1000) end + else + sound(5000, 3) end end function tic_pepe() - if omplint then return end -- bloquejat durant l'animació de recàrrega - -- Cada direcció: edge bufferitzat (tap curt) OR tecla mantinguda al moment del tic. + if omplint then return end if input_buf.up or btn(KEY_Q) then intent_moviment( 0, -1) elseif input_buf.down or btn(KEY_A) then intent_moviment( 0, 1) elseif input_buf.left or btn(KEY_O) then intent_moviment(-1, 0) elseif input_buf.right or btn(KEY_P) then intent_moviment( 1, 0) end - -- Arribar al pot dispara la recàrrega if pepe.x == PEPE_INI_X and pepe.y == PEPE_INI_Y and pot < POT_MAX then omplint = true omplir_i = 0 - omplir_max = POT_MAX - pot -- (90 - pot) en el moment d'entrar + omplir_max = POT_MAX - pot end end --- Fidel al Pascal original: `for i:=0 to 90-pot do sound(i*10); pot:=pot+1;` --- El so depèn del comptador del bucle (sempre arranca a 0 Hz), no del nivell del pot. +-- Fidel al Pascal: sound(i*10) on i és el comptador del bucle function tic_omplir() if not omplint then return end sound(omplir_i * 10, 2) @@ -213,6 +269,231 @@ function tic_omplir() pepe.pinta = true end +-- ==================================================================== +-- ENEMICS — moure_malos del PINTOR3.PAS +-- ==================================================================== +function init_malos() + malos = {} + for i = 1, NUM_MALOS do + malos[i] = { x=0, y=0, viu=false, aparegut=false } + end +end + +-- naiximent: busca totes les cel·les pintades (= chr 219 en el Pascal) i +-- en tria una aleatòria. Reservem el pot perquè no naixi el malo damunt. +function naiximent() + local llocs = {} + for x = 0, MAP_W-1 do + for y = 0, MAP_H-1 do + if mapa[x][y].pintat + and not (x == PEPE_INI_X and y == PEPE_INI_Y) + and not (x == pepe.x and y == pepe.y) then + llocs[#llocs+1] = { x=x, y=y } + end + end + end + if #llocs == 0 then return PEPE_INI_X, PEPE_INI_Y end -- fallback (impossible si jugues) + local pick = llocs[rnd(#llocs) + 1] + return pick.x, pick.y +end + +-- Jingle "música malos" del Pascal (sound creixent i decreixent en bucle) +function jingle_malo() + play("l0o3cdefgab>cl0o3cdefgab>c") +end + +-- Check spawn: si total_blocs ha baixat per sota del llindar i el malo no +-- ha aparegut encara, el fem aparèixer. +function check_spawn() + for i = 1, NUM_MALOS do + local m = malos[i] + if not m.aparegut and total_blocs <= BLOCS_SPAWN[i] then + m.x, m.y = naiximent() + m.viu = true + m.aparegut = true + jingle_malo() + end + end +end + +-- Decisió de direcció del malo (fidel a moure_malos del Pascal): +-- random(2): 0 = perseguir en eix X, 1 = perseguir en eix Y. La direcció és: +-- meneja=0 esquerra, 1 dreta, 2 dalt, 3 baix. +-- Retorna una de les 4 direccions (o nil si no s'aplica cap). +function decidir_direccio(m) + local meneja = nil + if rnd(2) == 0 then + if pepe.x - m.x > 0 then meneja = 1 -- pepe a la dreta del malo + elseif pepe.x - m.x < 0 then meneja = 0 -- pepe a l'esquerra + end + else + if pepe.y - m.y > 0 then meneja = 3 -- pepe avall + elseif pepe.y - m.y < 0 then meneja = 2 -- pepe amunt + end + end + return meneja +end + +-- Intenta moure un malo en la direcció `meneja`. Fidel al Pascal: +-- requereix que la cel·la destí (a) no siga paret, (b) no tinga un altre +-- malo, i (c) siga "bg" (= cel·la pintada). Aquesta tercera condició limita +-- els malos a la zona ja pintada pel Pepe. +function moure_malo(m, meneja) + if meneja == nil then return end + local dx, dy = 0, 0 + if meneja == 0 then dx = -1 + elseif meneja == 1 then dx = 1 + elseif meneja == 2 then dy = -1 + elseif meneja == 3 then dy = 1 + end + local nx, ny = m.x + dx, m.y + dy + if tipo_a(nx, ny) == PARED then return end + if hi_ha_malo(nx, ny) then return end + if not pintat_a(nx, ny) then return end + m.x, m.y = nx, ny +end + +function tic_malos() + for i = 1, NUM_MALOS do + local m = malos[i] + if m.viu then + moure_malo(m, decidir_direccio(m)) + end + end +end + +function check_mort_per_malo() + for i = 1, NUM_MALOS do + local m = malos[i] + if m.viu and m.x == pepe.x and m.y == pepe.y then + mort_pepe() + return + end + end +end + +-- ==================================================================== +-- MORT + ESTATS DE TRANSICIÓ +-- ==================================================================== +function set_estat(nou) + estat_joc = nou + estat_t = 0 +end + +function mort_pepe() + vides = vides - 1 + set_estat(ESTAT_MURIGUENT) +end + +function reset_partida() + pot = POT_MAX + total_blocs = 0 + vides = VIDES_INI + omplint = false + pepe.x, pepe.y = PEPE_INI_X, PEPE_INI_Y + pepe.pinta = true + init_malos() + carregar_mapa(3) + reset_input() + set_estat(ESTAT_PLAYING) +end + +-- Respawn després de mort (sense reset complet, sols Pepe a casa i pot ple) +function respawn_pepe() + pepe.x, pepe.y = PEPE_INI_X, PEPE_INI_Y + pepe.pinta = true + pot = POT_MAX + omplint = false + reset_input() +end + +-- ==================================================================== +-- LÒGICA PER ESTAT +-- ==================================================================== +function update_playing() + sample_input() + + if omplint and (cnt() % TICS_OMPLIR) == 0 then tic_omplir() end + + if not omplint and (cnt() % TICS_MOVIMENT) == 0 then + tic_pepe() + check_mort_per_malo() + if estat_joc ~= ESTAT_PLAYING then reset_input(); return end + tic_malos() + check_mort_per_malo() + check_spawn() + reset_input() + + if total_blocs <= 0 then + set_estat(ESTAT_VICTORIA) + play("l3o4ceg>c") + return + end + end + + cls() + pintar_mapa() + pintar_pot() + pintar_malos() + pintar_pepe() + pintar_hud() +end + +function update_muriguent() + -- Animació de mort: so descendent + parpadeig del Pepe + local pas = flr(estat_t / 3) + local freq = MORT_FREQ_INI - pas * MORT_FREQ_STEP + if freq < MORT_FREQ_END then freq = MORT_FREQ_END end + sound(freq, 2) + + cls() + pintar_mapa() + pintar_pot() + pintar_malos() + if (estat_t // 4) % 2 == 0 then pintar_pepe() end + pintar_hud() + + if estat_t >= MORT_FRAMES then + nosound() + if vides <= 0 then + set_estat(ESTAT_GAMEOVER) + else + respawn_pepe() + set_estat(ESTAT_PLAYING) + end + end +end + +function update_gameover() + cls() + pintar_mapa() + pintar_pot() + pintar_malos() + pintar_hud() + color(COLOR_WHITE, COLOR_RED) + print(" A D E U ", 15, 11) + + if estat_t > 120 and btnp(KEY_SPACE) then + reset_partida() + end +end + +function update_victoria() + cls() + pintar_mapa() + pintar_pot() + pintar_pepe() + pintar_hud() + if (estat_t // 10) % 2 == 0 then + color(COLOR_GREEN, COLOR_BLACK) + print("ENHORABONA!", 14, 11) + end + + if estat_t > 180 and btnp(KEY_SPACE) then + reset_partida() + end +end + -- ==================================================================== -- BUCLE PRINCIPAL -- ==================================================================== @@ -220,24 +501,16 @@ function init() mode(1) border(COLOR_BLUE) definir_glifs() + init_malos() carregar_mapa(3) cls() end function update() - -- Sample d'edges d'input cada frame (mentre el joc està actiu i no en recàrrega) - if not omplint then sample_input() end - - if omplint and (cnt() % TICS_OMPLIR) == 0 then tic_omplir() end - - if not omplint and (cnt() % TICS_MOVIMENT) == 0 then - tic_pepe() - reset_input() -- descartem edges acumulats després de processar el tic + estat_t = estat_t + 1 + if estat_joc == ESTAT_PLAYING then update_playing() + elseif estat_joc == ESTAT_MURIGUENT then update_muriguent() + elseif estat_joc == ESTAT_GAMEOVER then update_gameover() + elseif estat_joc == ESTAT_VICTORIA then update_victoria() end - - cls() - pintar_mapa() - pintar_pot() - pintar_pepe() - pintar_hud() end