Project Script


 
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

# Weapon & Combat Settings
LASER_DURATION_FRAMES = 5
LASER_Y_OFFSET = 4
ENEMY_SIDE_PIXELS = 8

# 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

# 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_SPRITE_TRANSPARENCY = 2


# ==============================================================================
# --- LEVEL DESIGN DATA (CAN BE MIGRATED TO A JSON FILE LATER) ---
# ==============================================================================

# Common variables reused across the raw layout data
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


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 draw(self):
        if self.is_alive:
            pyxel.rect(self.x, self.y, self.width, self.height, COLOR_ENEMY)


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):
        # Dynamically build internal engine entities from the data dictionaries
        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):
        # Configure Goal Door
        door_info = raw_data["door"]
        self.door = Door(room_index=door_info["room_index"], x=door_info["x"], y=door_info["y"])
        
        # Parse all rooms from raw dictionary lists
        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

    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
        elif self.x >= WIDTH:
            level.change_room(1)
            self.x %= WIDTH

        # 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

    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)
        
        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)

        # Dependency Injection: Pass the data configuration into the system loader
        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()
        
        if self.level.door.has_collided:
            pyxel.text(20, 20, "Wiiii!!!", pyxel.rndi(1, 15))


SimplePlatformer()