diff --git a/player.py b/player.py index 808e507..7bf47da 100644 --- a/player.py +++ b/player.py @@ -4,108 +4,221 @@ from settings import * class Player: def __init__(self, x, y): - # Position & Size + # ========================= + # POSITION + # ========================= + self.pos = pygame.Vector2(x, y) self.rect = pygame.Rect(x, y, PLAYER_WIDTH, PLAYER_HEIGHT) - # Movement + # ========================= + # MOVEMENT + # ========================= self.velocity = pygame.Vector2(0, 0) - self.acceleration = pygame.Vector2(0, 0) - - # State self.on_ground = False self.facing_right = True - # Stats + # Movement tuning + self.move_accel = 2800 + self.air_accel = 2200 + self.max_run_speed = 320 + self.ground_friction = -12 + self.air_control = 0.65 + + self.gravity = 2200 + self.max_fall_speed = 1000 + self.fast_fall_speed = 1400 + + self.jump_force = -700 + self.jump_cut_multiplier = 0.5 + + # ========================= + # JUMP TECH + # ========================= + self.coyote_time_max = 0.1 + self.jump_buffer_max = 0.12 + + self.coyote_timer = 0 + self.jump_buffer_timer = 0 + + # ========================= + # COMBAT + # ========================= self.health = MAX_HEALTH + self.max_health = MAX_HEALTH + + self.knockback_timer = 0 + self.invulnerable_timer = 0 + + # ========================= + # STATE + # ========================= + self.state = "idle" # ========================================================== # UPDATE # ========================================================== def update(self, dt, world): - self.handle_input() - self.apply_physics(dt) - self.handle_collisions(world) + dt = min(dt, 0.05) + + self.handle_timers(dt) + self.handle_input(dt) + self.apply_gravity(dt) + + # ---- Horizontal ---- + self.pos.x += self.velocity.x * dt + self.rect.x = round(self.pos.x) + self.handle_horizontal_collisions(world) + + # ---- Vertical ---- + self.pos.y += self.velocity.y * dt + self.rect.y = round(self.pos.y) + self.handle_vertical_collisions(world) + + self.update_state() + + # ========================================================== + # TIMERS + # ========================================================== + def handle_timers(self, dt): + if self.coyote_timer > 0: + self.coyote_timer -= dt + + if self.jump_buffer_timer > 0: + self.jump_buffer_timer -= dt + + if self.knockback_timer > 0: + self.knockback_timer -= dt + + if self.invulnerable_timer > 0: + self.invulnerable_timer -= dt # ========================================================== # INPUT # ========================================================== - def handle_input(self): + def handle_input(self, dt): keys = pygame.key.get_pressed() - self.acceleration.x = 0 + if self.knockback_timer > 0: + return - # Horizontal movement + move = 0 if keys[pygame.K_a]: - self.acceleration.x = -PLAYER_ACCELERATION + move -= 1 self.facing_right = False - if keys[pygame.K_d]: - self.acceleration.x = PLAYER_ACCELERATION + move += 1 self.facing_right = True - # Apply friction - self.acceleration.x += self.velocity.x * PLAYER_FRICTION + accel = self.move_accel if self.on_ground else self.air_accel * self.air_control - # Jump - if keys[pygame.K_SPACE] and self.on_ground: - self.velocity.y = JUMP_FORCE + if move != 0: + self.velocity.x += move * accel * dt + else: + if self.on_ground: + self.velocity.x += self.velocity.x * self.ground_friction * dt + + # Clamp horizontal speed + self.velocity.x = max(-self.max_run_speed, + min(self.max_run_speed, self.velocity.x)) + + # Jump buffering + if keys[pygame.K_SPACE]: + self.jump_buffer_timer = self.jump_buffer_max + + # Variable jump height (jump cut) + if not keys[pygame.K_SPACE] and self.velocity.y < 0: + self.velocity.y *= self.jump_cut_multiplier + + # Fast fall + if keys[pygame.K_s] and not self.on_ground: + self.velocity.y = min(self.velocity.y + 3000 * dt, + self.fast_fall_speed) + + # ========================================================== + # GRAVITY + # ========================================================== + def apply_gravity(self, dt): + self.velocity.y += self.gravity * dt + + if self.velocity.y > self.max_fall_speed: + self.velocity.y = self.max_fall_speed + + if self.on_ground: + self.coyote_timer = self.coyote_time_max + + # Perform jump if allowed + if self.jump_buffer_timer > 0 and self.coyote_timer > 0: + self.velocity.y = self.jump_force + self.jump_buffer_timer = 0 + self.coyote_timer = 0 self.on_ground = False - # ========================================================== - # PHYSICS - # ========================================================== - def apply_physics(self, dt): - # Apply gravity - self.acceleration.y = GRAVITY - - # Update velocity - self.velocity += self.acceleration * dt - - # Clamp fall speed - if self.velocity.y > MAX_FALL_SPEED: - self.velocity.y = MAX_FALL_SPEED - - # Move horizontally - self.rect.x += self.velocity.x * dt - - # Move vertically - self.rect.y += self.velocity.y * dt - # ========================================================== # COLLISIONS # ========================================================== - def handle_collisions(self, world): - self.on_ground = False - - # Get nearby tiles once - nearby_tiles = world.get_nearby_tiles(self.rect) - - # Horizontal collisions - for tile in nearby_tiles: - if tile["rect"].colliderect(self.rect) and tile["solid"]: - if self.velocity.x > 0: # Moving right + def handle_horizontal_collisions(self, world): + for tile in world.get_nearby_tiles(self.rect): + if tile["solid"] and self.rect.colliderect(tile["rect"]): + if self.velocity.x > 0: self.rect.right = tile["rect"].left - elif self.velocity.x < 0: # Moving left + elif self.velocity.x < 0: self.rect.left = tile["rect"].right + + self.pos.x = self.rect.x self.velocity.x = 0 - # Vertical collisions - for tile in nearby_tiles: - if tile["rect"].colliderect(self.rect) and tile["solid"]: - if self.velocity.y > 0: # Falling + def handle_vertical_collisions(self, world): + self.on_ground = False + + for tile in world.get_nearby_tiles(self.rect): + if tile["solid"] and self.rect.colliderect(tile["rect"]): + if self.velocity.y > 0: self.rect.bottom = tile["rect"].top self.on_ground = True - elif self.velocity.y < 0: # Jumping up + elif self.velocity.y < 0: self.rect.top = tile["rect"].bottom + + self.pos.y = self.rect.y self.velocity.y = 0 + # ========================================================== + # STATE MACHINE + # ========================================================== + def update_state(self): + if not self.on_ground: + self.state = "jump" if self.velocity.y < 0 else "fall" + elif abs(self.velocity.x) > 10: + self.state = "run" + else: + self.state = "idle" + + # ========================================================== + # DAMAGE / KNOCKBACK + # ========================================================== + def take_damage(self, amount, direction): + if self.invulnerable_timer > 0: + return + + self.health -= amount + self.invulnerable_timer = 0.5 + + self.velocity.x = direction * 500 + self.velocity.y = -400 + self.knockback_timer = 0.2 + # ========================================================== # DRAW # ========================================================== def draw(self, screen, camera): draw_rect = camera.apply(self.rect) - pygame.draw.rect(screen, (255, 50, 50), draw_rect) + # Flash when invulnerable + if self.invulnerable_timer > 0: + color = (255, 255, 255) + else: + color = (255, 50, 50) + + pygame.draw.rect(screen, color, draw_rect) if SHOW_COLLIDERS: - pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2) + pygame.draw.rect(screen, (0, 255, 0), draw_rect, 2) \ No newline at end of file