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 # White text for the ammo display
COLOR_SPRITE_TRANSPARENCY = 2
# ==============================================================================
# --- LEVEL DESIGN DATA (CAN BE MIGRATED TO A JSON FILE LATER) ---
# ==============================================================================
FLOOR_HEIGHT = 10
ALTURA_PLATAFORMA = 2
FLOOR_RECT = [0, HEIGHT - FLOOR_HEIGHT, WIDTH, FLOOR_HEIGHT]
LEVEL_DATA = {
"door": {
"room_index": 0,
"x": 150,
"y": 7
},
"rooms": [
{
"platforms": [
FLOOR_RECT,
[40, 80, 40, ALTURA_PLATAFORMA],
[100, 60, 40, ALTURA_PLATAFORMA],
[150, 15, 10, ALTURA_PLATAFORMA]
],
"enemies": [
{"x": 60, "y": 72},
{"x": 120, "y": 52}
]
},
{
"platforms": [
FLOOR_RECT,
[50, 90, 80, ALTURA_PLATAFORMA],
[40, 50, 20, ALTURA_PLATAFORMA]
],
"enemies": [
{"x": 80, "y": 82}
]
}
]
}
# ==============================================================================
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
bounding_box_side = 4
x_collision = player_x < self.x + bounding_box_side and player_x > self.x - bounding_box_side
y_collision = player_y < self.y + bounding_box_side and player_y > self.y - bounding_box_side
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.x = x
self.y = y
self.dy = 0
self.facing_sign = 1
self.is_jumping = True
self.on_platform = False
self.laser_timer = 0
self.side = PLAYER_SIDE_PIXELS
self.grenades = []
self.ammo = PLAYER_START_AMMO # --- NEW: Tracks remaining ammo state
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 with Stock Checking
if pyxel.btnp(KEY_ACTION_GRENADE) and self.ammo > 0:
self.ammo -= 1 # Deduct ammunition resource
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):
# Draw Laser Beam
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)
# Draw Grenades
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)
self.level = Level(LEVEL_DATA)
self.player = Player(x=PLAYER_START_X, y=PLAYER_START_Y)
pyxel.run(self.update, self.draw)
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
)
def draw(self):
pyxel.cls(COLOR_BACKGROUND)
self.level.draw()
self.player.draw()
# --- NEW: Heads-Up Display (UI Overlay) ---
# Draw a low-level text string reading the current player status value
ammo_text = f"GRENADES: {self.player.ammo}"
pyxel.text(4, 4, ammo_text, COLOR_TEXT_UI)
if self.level.door.has_collided:
pyxel.text(20, 20, "Wiiii!!!", pyxel.rndi(1, 15))
SimplePlatformer()