# =============================================================================
# File:    sergiosUpdatedSlimePythonSketch.py
# Date:    2026-10-01  (ported from sergiosUpdatedSlimeJavaSketch.pde, 2026-10-01)
# Author:  Sergio Z & klp — Python Processing port by klp
# Folder:  CreativeCrossTraining/SergioZSlimeSprite2023-12-09/scripts/
# Purpose: Python Processing port of the refactored slime sprite teaching sketch.
#          Keyboard-controlled animated slime on a tiled grass background.
#
# === Running This Sketch in the Processing Desktop IDE ===
#   1. Install Python Mode: Processing IDE > Tools > Manage Modes > Python Mode.
#   2. Select Python Mode from the mode selector (top-right drop-down).
#   3. File > Open... this file.
#   4. Add images via Sketch > Add File... (copies them into data/ automatically):
#        slimeSprite.png, slimeBack.png, Grass_Sample.png
#   5. Click Run (Ctrl+R). WASD or arrow keys move the slime.
#
# === What Is Different from the Java Version ===
#   - NO PVector: Python Processing can access Java's PVector via Jython interop
#     but using plain float variables (pos_x, pos_y, vel_x, vel_y) is cleaner
#     and more Pythonic. The math is identical; the container is simpler.
#     This is itself a cross-training lesson — the same velocity-based movement
#     concept works with or without a dedicated vector class.
#   - NO HashSet<Integer>: Python's built-in set() does the same job.
#     held_keys.add(keyCode) and held_keys.discard(keyCode) are behaviorally
#     identical to Java's heldKeys.add(keyCode) / heldKeys.remove(keyCode).
#     discard() is preferred over remove() — it does not raise a KeyError if
#     the key is absent (which can happen when the sketch loses focus mid-hold).
#   - GLOBAL declarations: Python requires the 'global' keyword when reassigning
#     a module-level variable inside a function. In setup(), 'global idle, back,
#     bg, player' is needed because we assign new objects to those names.
#     Simply reading or calling methods on module-level objects does NOT need
#     global — draw() can use player.vel_x without a global declaration.
#   - UPPER_CASE constants: Python has no 'final' keyword. UPPER_CASE is the
#     community convention that signals "treat this as a constant."
#   - SELF vs THIS: every attribute access is self.pos_x, self.vel_x, etc.
#   - SNAKE_CASE methods: draw_player() and update() follow Python style.
#     Processing built-ins (image, pushMatrix, constrain, etc.) stay lowercase
#     because they are inherited from the underlying Java Processing API.
#   - int() cast: int(self.frame) replaces Java's (int)frame and JS's floor().
#
# === The 9-Parameter image() Call — Same Corner Convention as Java ===
#   Python Processing is Jython calling Java Processing directly, so it uses
#   Java's convention — NOT p5.js's:
#     p5.js:          image(img, dx, dy, dw, dh, sx, sy, sw, sh)  ← offset + SIZE
#     Java / Python:  image(img, dx, dy, dw, dh, u1, v1, u2, v2)  ← top-left + BOTTOM-RIGHT CORNER
#   Fix: src_x2 = src_x + idle.height  (same calculation as the Java version)
#
# === Concepts Demonstrated ===
#   - Sprite-sheet animation: the two-frame breathing trick (same algorithm)
#   - Multi-key input using Python's built-in set()
#   - Velocity-based movement without PVector — plain float components
#   - OOP in Python: class, __init__, self, snake_case methods
#   - Python idioms: global, in-operator, discard(), UPPER_CASE constants
# =============================================================================

# ── Constants (UPPER_CASE by convention — Python has no 'final') ──────────────
CANVAS_SIZE       = 480    # square canvas
FRAME_RATE_TARGET = 60     # target frames per second
PLAYER_SPEED      = 3.0    # pixels added to velocity per frame while a key is held

PLAYER_SIZE    = 50.0  # logical player hitbox size in pixels
SPRITE_SCALE   = 1.2   # sprite drawn 20% larger than the hitbox for visual weight
ANIM_FRAME_MAX = 1.9   # reset threshold: keeps frame index within 0..1
ANIM_SPEED     = 0.05  # frame counter increment per update() call
                       # cycle: 2.0 / 0.05 = 40 frames ≈ 0.67 s at 60 fps
VEL_THRESHOLD  = 0.09  # velocities smaller than this are snapped to zero

# Letter key ASCII codes — same values as Java and JavaScript
KEY_A = 65   # A
KEY_D = 68   # D
KEY_W = 87   # W
KEY_S = 83   # S
# LEFT, RIGHT, UP, DOWN are built-in Processing constants inherited from Java.

# ── Module-level globals — assigned in setup() ────────────────────────────────
# Declared as None here so Python knows these names exist at module scope before
# setup() runs. This mirrors Java's uninitialized PImage/Player declarations.
idle   = None   # two-frame sprite sheet: left = squished, right = rounder
back   = None   # single back-facing sprite, shown when moving straight up
bg     = None   # tiled grass background
player = None   # the single Player instance

# ── Held-keys set (Python multi-key input pattern) ───────────────────────────
# Python's set() replaces Java's HashSet<Integer>.
# Methods are called on the existing set object, so 'global' is not needed in
# keyPressed() / keyReleased() — we are not reassigning held_keys itself.
held_keys = set()


# =============================================================================
# SETUP — called once when the sketch starts
# =============================================================================
def setup():
    global idle, back, bg, player   # global required: we reassign these names

    size(480, 480)                          # fixed square canvas — no HTML layout
    frameRate(FRAME_RATE_TARGET)
    colorMode(HSB, 360, 100, 100, 100)      # Hue 0-360, S/B/A 0-100
    imageMode(CENTER)

    # loadImage() is synchronous — no preload() or await.
    # Processing finds files in the sketch's data/ folder automatically.
    # Use Sketch > Add File... in the IDE to place images there.
    idle = loadImage("slimeSprite.png")
    back = loadImage("slimeBack.png")
    bg   = loadImage("Grass_Sample.png")

    player = Player(width / 2.0, height / 2.0)
# end setup


# =============================================================================
# DRAW — called FRAME_RATE_TARGET times per second
#
# Layer order (painter's algorithm — identical logic to Java and JS versions):
#   Layer 1: background() — clears the previous frame
#   Layer 2: bg image     — tiles the grass texture
#   Layer 3: keyboard → velocity → player.update() → draw sprite
#
# BRAKING: vel_x and vel_y are zeroed at the end of every frame.
# The slime moves only while a key is actively held.
# =============================================================================
def draw():
    # Layer 1: clear canvas to light gray
    background(0, 0, 90, 100)

    # Layer 2: tiled grass background
    imageMode(CENTER)
    if bg is not None:
        image(bg, width / 2.0, height / 2.0, width * 2, height)
    else:
        background(120, 60, 30)   # HSB dark-green fallback if image is missing

    # Layer 3: apply held-key velocity, respecting canvas boundaries.
    # 'in' operator replaces Java's heldKeys.contains() / p5.js keyIsDown().

    # ── right (D or →) ───────────────────────────────────────────────────────
    if (KEY_D in held_keys or RIGHT in held_keys) and \
            player.pos_x <= width - player.size / 2:
        player.vel_x += PLAYER_SPEED

    # ── left (A or ←) ────────────────────────────────────────────────────────
    if (KEY_A in held_keys or LEFT in held_keys) and \
            player.pos_x >= player.size / 2:
        player.vel_x -= PLAYER_SPEED

    # ── up (W or ↑) ──────────────────────────────────────────────────────────
    if (KEY_W in held_keys or UP in held_keys) and \
            player.pos_y >= player.size / 2:
        player.vel_y -= PLAYER_SPEED

    # ── down (S or ↓) ────────────────────────────────────────────────────────
    if (KEY_S in held_keys or DOWN in held_keys) and \
            player.pos_y <= height - player.size / 2:
        player.vel_y += PLAYER_SPEED

    # Clamp velocity — constrain() has the same name and semantics in all three languages
    player.vel_x = constrain(player.vel_x, -PLAYER_SPEED, PLAYER_SPEED)
    player.vel_y = constrain(player.vel_y, -PLAYER_SPEED, PLAYER_SPEED)

    player.update()

    # BRAKING: stop instantly when key is released
    player.vel_x = 0
    player.vel_y = 0
# end draw


# =============================================================================
# KEYBOARD EVENT HANDLERS
#
# Processing calls these as global functions.
# No 'global held_keys' needed — we call .add()/.discard() on the existing set,
# not reassign held_keys itself.
# discard() vs remove(): discard() is silent when the key is not in the set.
# This matters because keyReleased() can fire without a matching keyPressed()
# if the sketch window gains focus while a key is already held down.
# =============================================================================
def keyPressed():
    held_keys.add(keyCode)

def keyReleased():
    held_keys.discard(keyCode)   # safe even if keyCode is not in the set


# =============================================================================
# PLAYER CLASS
# ─────────────────────────────────────────────────────────────────────────────
# Direct translation of the Java Player class and JS Player.js.
# The breathing trick algorithm is identical across all three languages.
#
# === The Primary Teaching Point of the Python Port ===
# Java uses PVector for pos and vel. Python uses plain float pairs (pos_x/pos_y,
# vel_x/vel_y). The movement math is exactly the same:
#     Java:   pos.add(vel)           → pos.x += vel.x; pos.y += vel.y
#     Python: pos_x += vel_x; pos_y += vel_y
# The concept (velocity-based movement) is language-independent. The library
# support (PVector) is language-dependent. Students learn to recognize which is
# which by seeing the same behavior written both ways.
#
# === Module Scope Access ===
# draw_player() reads 'idle', 'back', and all UPPER_CASE constants from the
# module's global scope. This is Python's equivalent of Java's inner-class
# access to the outer PApplet's fields. Reading globals from a class method
# is standard Python — no import, no parameter passing needed.
#
# === THE BREATHING TRICK (identical to Java and JS) ===
# self.frame advances by ANIM_SPEED each update().
# int(self.frame) floors it: 0.0-0.99 → 0,  1.0-1.89 → 1.
# src_x selects the left edge of the frame in the sprite sheet.
# src_x2 = src_x + idle.height is the right edge (Java/Python corner convention).
# =============================================================================
class Player:

    def __init__(self, start_x, start_y):
        # Position — plain floats replace PVector pos
        self.pos_x = float(start_x)
        self.pos_y = float(start_y)

        # Velocity — plain floats replace PVector vel
        self.vel_x = 0.0
        self.vel_y = 0.0

        # Acceleration — always (0, 0); documented hook for future physics
        self.accel_x = 0.0
        self.accel_y = 0.0

        self.frame = 0.0      # animation counter; int(frame) gives the frame index
        self.size  = PLAYER_SIZE


    def draw_player(self):
        # Direction logic (identical to Java and JS):
        #   vel_y < 0 AND vel_x == 0 → moving straight up → back sprite
        #   vel_x < 0                → moving left        → idle, mirrored
        #   all other cases          → idle (right-facing or stationary breathing)
        #   NOTE: moving straight DOWN falls through to idle — no down sprite.
        imageMode(CENTER)

        display_size = self.size * SPRITE_SCALE
        frame_index  = int(self.frame)          # Python's floor for positive floats
        src_x        = idle.height * frame_index
        src_x2       = src_x + idle.height      # bottom-right corner (Java/Python convention)

        if self.vel_y < 0 and self.vel_x == 0:
            # ── Moving straight up ────────────────────────────────────────────
            image(back,
                  self.pos_x, self.pos_y,
                  display_size, display_size)

        elif self.vel_x < 0:
            # ── Moving left: mirror idle horizontally ─────────────────────────
            # scale(-1, 1) flips around x = 0; negate pos_x to re-center.
            pushMatrix()
            scale(-1, 1)
            image(idle,
                  -self.pos_x, self.pos_y,
                  display_size, display_size,
                  src_x, 0, src_x2, idle.height)
            popMatrix()

        else:
            # ── Moving right, moving down, or stationary (breathing) ──────────
            image(idle,
                  self.pos_x, self.pos_y,
                  display_size, display_size,
                  src_x, 0, src_x2, idle.height)
    # end draw_player


    def update(self):
        # Apply acceleration (always 0; hook for future physics)
        self.vel_x += self.accel_x
        self.vel_y += self.accel_y

        # Move by current velocity
        self.pos_x += self.vel_x
        self.pos_y += self.vel_y

        self.draw_player()

        # Advance the animation counter and loop it when it exceeds ANIM_FRAME_MAX
        self.frame += ANIM_SPEED
        if self.frame > ANIM_FRAME_MAX:
            self.frame = 0

        # Snap near-zero velocities to zero — prevents infinite micro-drift
        if abs(self.vel_x) < VEL_THRESHOLD:
            self.vel_x = 0
        if abs(self.vel_y) < VEL_THRESHOLD:
            self.vel_y = 0
    # end update

# end class Player
