import pyxel
# ==============================================================================
# --- GLOBAL CONFIGURATION CONSTANTS (ENGINE SETTINGS) ---
# ==============================================================================
# Display Options
WIDTH, HEIGHT = 160, 120
GAME_TITLE = "Workshop: Low Level OO Engine"
RESOURCE_FILE = "my_resource.pyxres"
# Player Physics & Mechanics
PLAYER_START_X = 80
PLAYER_START_Y = 0
PLAYER_WALK_SPEED = 2
PLAYER_GRAVITY = 0.3
PLAYER_JUMP_FORCE = -5
PLAYER_SIDE_PIXELS = 8
PLAYER_START_AMMO = 3
# Weapon & Combat Settings
LASER_DURATION_FRAMES = 5
LASER_Y_OFFSET = 4
ENEMY_SIDE_PIXELS = 8
# Grenade Physics Balance
GRENADE_SPEED_X = 3
GRENADE_LAUNCH_VELOCITY_Y = -4
GRENADE_GRAVITY = 0.2
GRENADE_RADIUS = 2
# Input Mapping (Low-Level Keys)
KEY_MOVE_LEFT = pyxel.KEY_LEFT
KEY_MOVE_RIGHT = pyxel.KEY_RIGHT
KEY_ACTION_JUMP = pyxel.KEY_SPACE
KEY_ACTION_FIRE = pyxel.KEY_X
KEY_ACTION_GRENADE = pyxel.KEY_Z
# Audio Settings
JUMP_SOUND_INDEX = 0
LANDING_SOUND_INDEX = 1
AUDIO_CHANNEL = 0
# Color Palette (Pyxel 16-Color Index)
COLOR_BACKGROUND = 0
COLOR_PLATFORM = 11
COLOR_ENEMY = 8
COLOR_GRENADE = 10
COLOR_TEXT_UI = 7
COLOR_SPRITE_TRANSPARENCY = 2
FLOOR_HEIGHT = 10
ALTURA_PLATAFORMA = 2
FLOOR_RECT = [0, HEIGHT - FLOOR_HEIGHT, WIDTH, FLOOR_HEIGHT]
# ==============================================================================
# --- MULTI-LEVEL DESIGN DATA ---
# ==============================================================================
ALL_LEVELS_DATA = [
# --- NIVEL 1 ---
{
"door": {"room_index": 1, "x": 140, "y": 72}, # Puerta en la última habitación
"rooms": [
{
"platforms": [
FLOOR_RECT,
[40, 80, 40, ALTURA_PLATAFORMA],
[100, 60, 40, ALTURA_PLATAFORMA],
],
"enemies": [{"x": 60, "y": 72}, {"x": 120, "y": 52}]
},
{
"platforms": [
FLOOR_RECT,
[20, 90, 50, ALTURA_PLATAFORMA],
[100, 80, 50, ALTURA_PLATAFORMA],
],
"enemies": [{"x": 35, "y": 82}]
}
]
},
# --- NIVEL 2 (¡NUEVO!) ---
{
"door": {"room_index": 1, "x": 140, "y": 42}, # Puerta en una plataforma elevada
"rooms": [
{
"platforms": [
FLOOR_RECT,
[10, 95, 30, ALTURA_PLATAFORMA],
[50, 75, 40, ALTURA_PLATAFORMA],
[110, 60, 40, ALTURA_PLATAFORMA],
],
"enemies": [{"x": 60, "y": 67}]
},
{
"platforms": [
FLOOR_RECT,
[20, 90, 40, ALTURA_PLATAFORMA],
[80, 70, 40, ALTURA_PLATAFORMA],
[130, 50, 25, ALTURA_PLATAFORMA], # Plataforma de la puerta
],
"enemies": [{"x": 90, "y": 62}]
}
]
}
]
# ==============================================================================
class Platform:
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
def to_rect_args(self):
return (self.x, self.y, self.width, self.height)
def check_player_landing(self, px, py, p_width, p_height, next_py):
if px + p_width > self.x and px < self.x + self.width:
if py + p_height <= self.y and next_py + p_height >= self.y:
return True
return False
def check_projectile_collision(self, bx, by, bradius):
if self.x <= bx <= self.x + self.width:
if self.y <= by + bradius <= self.y + self.height:
return True
return False
class Enemy:
def __init__(self, x, y):
self.x = x
self.y = y
self.width = ENEMY_SIDE_PIXELS
self.height = ENEMY_SIDE_PIXELS
self.is_alive = True
def check_hitscan(self, player_x, player_y, facing_sign):
if not self.is_alive:
return False
laser_y = player_y + LASER_Y_OFFSET
if self.y <= laser_y <= self.y + self.height:
if facing_sign == 1 and self.x > player_x:
return True
if facing_sign == -1 and self.x < player_x:
return True
return False
def check_grenade_explosion(self, gx, gy, gradius):
if not self.is_alive:
return False
if self.x <= gx + gradius and gx - gradius <= self.x + self.width:
if self.y <= gy + gradius and gy - gradius <= self.y + self.height:
return True
return False
def draw(self):
if self.is_alive:
pyxel.rect(self.x, self.y, self.width, self.height, COLOR_ENEMY)
class Grenade:
def __init__(self, x, y, direction_sign):
self.x = x
self.y = y
self.dx = direction_sign * GRENADE_SPEED_X
self.dy = GRENADE_LAUNCH_VELOCITY_Y
self.radius = GRENADE_RADIUS
self.is_active = True
def update(self, level):
self.x += self.dx
self.dy += GRENADE_GRAVITY
self.y += self.dy
if self.x < 0 or self.x > WIDTH:
self.is_active = False
return
for plat in level.current_room.platforms:
if plat.check_projectile_collision(self.x, self.y, self.radius):
self.explode(level)
return
for enemy in level.current_room.enemies:
if enemy.check_grenade_explosion(self.x, self.y, self.radius):
self.explode(level)
return
def explode(self, level):
self.is_active = False
for enemy in level.current_room.enemies:
if enemy.check_grenade_explosion(self.x, self.y, self.radius + 6):
enemy.is_alive = False
def draw(self):
if self.is_active:
pyxel.circ(self.x, self.y, self.radius, COLOR_GRENADE)
class Door:
def __init__(self, room_index, x, y):
self.room_index = room_index
self.x = x
self.y = y
self.has_collided = False
def update_collision(self, player_x, player_y, active_room, is_jumping):
if active_room != self.room_index or is_jumping:
self.has_collided = False
return
# Ajuste de bounding box para que el jugador tenga que tocar el sprite de la puerta
bounding_box_side = 8
x_collision = player_x < self.x + bounding_box_side and player_x + 8 > self.x
y_collision = player_y < self.y + bounding_box_side and player_y + 8 > self.y
self.has_collided = x_collision and y_collision
def draw(self):
pyxel.blt(self.x, self.y, 0, 8, 8, 8, 8, COLOR_SPRITE_TRANSPARENCY)
class Room:
def __init__(self, room_dict):
self.platforms = []
for p in room_dict["platforms"]:
self.platforms.append(Platform(x=p[0], y=p[1], width=p[2], height=p[3]))
self.enemies = []
for e in room_dict["enemies"]:
self.enemies.append(Enemy(x=e["x"], y=e["y"]))
class Level:
def __init__(self, raw_data):
door_info = raw_data["door"]
self.door = Door(room_index=door_info["room_index"], x=door_info["x"], y=door_info["y"])
self.rooms = []
for room_cfg in raw_data["rooms"]:
self.rooms.append(Room(room_cfg))
self.active_room_idx = 0
@property
def current_room(self):
return self.rooms[self.active_room_idx]
def change_room(self, direction):
self.active_room_idx = (self.active_room_idx + direction) % len(self.rooms)
def draw(self):
for plat in self.current_room.platforms:
pyxel.rect(*plat.to_rect_args(), COLOR_PLATFORM)
for enemy in self.current_room.enemies:
enemy.draw()
if self.active_room_idx == self.door.room_index:
self.door.draw()
class Player:
def __init__(self, x, y):
self.reset_position(x, y)
self.facing_sign = 1
self.laser_timer = 0
self.side = PLAYER_SIDE_PIXELS
self.grenades = []
self.ammo = PLAYER_START_AMMO
def reset_position(self, x, y):
"""Fuerza al jugador a una coordenada fija (útil para el cambio de nivel)"""
self.x = x
self.y = y
self.dy = 0
self.is_jumping = True
self.on_platform = False
def update(self, level):
# 1. Horizontal Movement & Room Switching
if pyxel.btn(KEY_MOVE_LEFT):
self.x -= PLAYER_WALK_SPEED
self.facing_sign = -1
if pyxel.btn(KEY_MOVE_RIGHT):
self.x += PLAYER_WALK_SPEED
self.facing_sign = 1
if self.x < 0:
level.change_room(-1)
self.x %= WIDTH
self.grenades.clear()
elif self.x >= WIDTH:
level.change_room(1)
self.x %= WIDTH
self.grenades.clear()
# 2. Apply Gravity Intent
self.dy += PLAYER_GRAVITY
self.on_platform = False
next_y = self.y + self.dy
# 3. Environment Collision Handling
for plat in level.current_room.platforms:
if plat.check_player_landing(self.x, self.y, self.side, self.side, next_y):
if self.is_jumping:
pyxel.play(AUDIO_CHANNEL, LANDING_SOUND_INDEX)
self.y = plat.y - self.side
self.dy = 0
self.is_jumping = False
self.on_platform = True
break
if not self.on_platform:
self.y = next_y
self.is_jumping = True
# 4. Jump Action Input
if pyxel.btnp(KEY_ACTION_JUMP) and not self.is_jumping:
self.dy = PLAYER_JUMP_FORCE
self.is_jumping = True
pyxel.play(AUDIO_CHANNEL, JUMP_SOUND_INDEX)
# 5. Laser Hitscan Combat Logic
if self.laser_timer > 0:
self.laser_timer -= 1
if pyxel.btnp(KEY_ACTION_FIRE):
self.laser_timer = LASER_DURATION_FRAMES
for enemy in level.current_room.enemies:
if enemy.check_hitscan(self.x, self.y, self.facing_sign):
enemy.is_alive = False
# 6. Projectile Grenade Launcher Logic
if pyxel.btnp(KEY_ACTION_GRENADE) and self.ammo > 0:
self.ammo -= 1
spawn_x = self.x + (self.side // 2)
spawn_y = self.y + (self.side // 2)
self.grenades.append(Grenade(spawn_x, spawn_y, self.facing_sign))
for grenade in self.grenades:
grenade.update(level)
self.grenades = [g for g in self.grenades if g.is_active]
def draw(self):
if self.laser_timer > 0:
color_tick = pyxel.frame_count % 16
lx = self.x + (self.side // 2)
ly = self.y + LASER_Y_OFFSET
target_x = WIDTH if self.facing_sign == 1 else 0
pyxel.line(lx, ly, target_x, ly, 10 if color_tick < 8 else 7)
for grenade in self.grenades:
grenade.draw()
if self.is_jumping:
u, v = (0, self.side) if self.dy < 0 else (self.side, 0)
else:
u, v = 0, 0
pyxel.blt(self.x, self.y, 0, u, v, self.facing_sign * self.side, self.side, COLOR_SPRITE_TRANSPARENCY)
class SimplePlatformer:
def __init__(self):
pyxel.init(WIDTH, HEIGHT, title=GAME_TITLE)
pyxel.load(RESOURCE_FILE)
pyxel.sounds[JUMP_SOUND_INDEX].set("c3c4", "S", "7", "N", 5)
pyxel.sounds[LANDING_SOUND_INDEX].set("g1", "N", "5", "F", 10)
# Control del estado global de la campaña/niveles
self.current_level_idx = 0
self.level = Level(ALL_LEVELS_DATA[self.current_level_idx])
self.player = Player(x=PLAYER_START_X, y=PLAYER_START_Y)
pyxel.run(self.update, self.draw)
def advance_level(self):
"""Lógica de transicion de nivel: Incrementa el mapa y reposiciona al jugador"""
self.current_level_idx += 1
# Si superamos los niveles existentes, hacemos un bucle al primero (o podrías poner pantalla de Game Over)
if self.current_level_idx >= len(ALL_LEVELS_DATA):
self.current_level_idx = 0
# Instanciar el nuevo nivel desde el diccionario de configuración
self.level = Level(ALL_LEVELS_DATA[self.current_level_idx])
# --- NUEVA REGLA: Caer desde el medio superior de la pantalla ---
middle_top_x = WIDTH // 2
middle_top_y = 0
self.player.reset_position(middle_top_x, middle_top_y)
# Opcional: Rellenar granadas al cambiar de nivel
self.player.ammo = PLAYER_START_AMMO
def update(self):
self.player.update(self.level)
self.level.door.update_collision(
self.player.x,
self.player.y,
self.level.active_room_idx,
self.player.is_jumping
)
# --- NEW: Disparador del cambio de nivel real ---
if self.level.door.has_collided:
self.advance_level()
def draw(self):
pyxel.cls(COLOR_BACKGROUND)
self.level.draw()
self.player.draw()
# UI Dinámica reflejando tanto las granadas como el nivel en curso
ui_text = f"LEVEL: {self.current_level_idx + 1} | GRENADES: {self.player.ammo}"
pyxel.text(4, 4, ui_text, COLOR_TEXT_UI)
SimplePlatformer()