Implementation Logic
First, we need to track which tool the player currently holds. We define a variable to store the active tool type, defaulting to a watering tool for now (tool switching will be added later):
self.current_tool = 'water'
When the player presses the spacebar, we want to trigger a tool usage animation lasting 350ms. To manage this timed action, we create a reusable timer class. Create a new file named countdown.py with the following implementation:
import pygame
class Countdown:
def __init__(self, milliseconds, callback=None):
self.milliseconds = milliseconds # Total duration to count
self.callback = callback # Function to run when timer ends
self.start_timestamp = 0 # Time when timer was started
self.is_running = False # Tracks if timer is active
def start(self):
self.is_running = True
# pygame.time.get_ticks() returns milliseconds since program launch
self.start_timestamp = pygame.time.get_ticks()
def stop(self):
self.is_running = False
self.start_timestamp = 0
def update(self):
if not self.is_running:
return
current_timestamp = pygame.time.get_ticks()
if current_timestamp - self.start_timestamp >= self.milliseconds:
if self.callback and self.start_timestamp != 0:
self.callback()
self.stop()
The timer accepts a duration in milliseconds and an optional callback function that runs when the countdown finishes. Each frame, we check the elapsed time by comparing the current timestamp with the start timestamp.
Integrating Timer into Player Logic
Import the Countdown class into player.py and store all timers in a dictionary for easy management:
self.cooldowns = {
'tool_action': Countdown(350, self.execute_tool_action)
}
Define the callback function placeholder for now, as tool effects will be added later:
def execute_tool_action(self):
pass
We need to update all timers every frame, so add a helper method:
def refresh_cooldowns(self):
for cooldown in self.cooldowns.values():
cooldown.update()
Call this method in the player's main update loop:
def update(self, delta_time):
self.handle_input()
self.update_state()
self.refresh_cooldowns()
self.process_movement(delta_time)
self.play_animation(delta_time)
Input Handling During Tool Usage
While the tool action timer is active, the player should not accept movement input or trigger another tool action. Modify the input handling method:
def handle_input(self):
pressed_keys = pygame.key.get_pressed()
# Block input if tool action is in progress
if not self.cooldowns['tool_action'].is_running:
if pressed_keys[pygame.K_UP]:
self.direction.y = -1
self.state = 'up'
elif pressed_keys[pygame.K_DOWN]:
self.direction.y = 1
self.state = 'down'
else:
self.direction.y = 0
if pressed_keys[pygame.K_RIGHT]:
self.direction.x = 1
self.state = 'right'
elif pressed_keys[pygame.K_LEFT]:
self.direction.x = -1
self.state = 'left'
else:
self.direction.x = 0
if pressed_keys[pygame.K_SPACE]:
self.cooldowns['tool_action'].start()
# Stop movement during tool usage
self.direction = pygame.math.Vector2()
# Reset animation frame to start from first frame
self.sprite_index = 0
Resetting sprite_index to 0 is necessary because tool animations often have fewer frames than movement animations. If we don't reset it, we might try to access a non-existent frame index (e.g., endex 3 for a 2-frame tool animation) and crash the program.
Updating Player State
We need to update the player's state string to reflect tool usage when the timer is active. The order of checks matters here: idle state check must come before tool state check, becuase tool usage implies the player is stationary, and reversing the order would incorrectly override the tool state with idle.
def update_state(self):
# Set idle state if no movement
if self.direction.magnitude() == 0:
base_state = self.state.split('_')[0]
self.state = f"{base_state}_idle"
# Override with tool state if tool action is active
if self.cooldowns['tool_action'].is_running:
base_state = self.state.split('_')[0]
self.state = f"{base_state}_{self.current_tool}"
Full Code Samples
player.py:
import pygame
from settings import *
from support import *
from countdown import Countdown
class Player(pygame.sprite.Sprite):
def __init__(self, position, sprite_group):
super().__init__(sprite_group)
self.load_assets()
self.state = 'down_idle'
self.sprite_index = 0
self.image = self.animations[self.state][self.sprite_index]
self.rect = self.image.get_rect(center=position)
self.direction = pygame.math.Vector2()
self.world_pos = pygame.math.Vector2(self.rect.center)
self.move_speed = 200
self.cooldowns = {
'tool_action': Countdown(350, self.execute_tool_action)
}
self.current_tool = 'water'
def execute_tool_action(self):
pass
def load_assets(self):
self.animations = {
'up': [], 'down': [], 'left': [], 'right',
'up_idle': [], 'down_idle': [], 'left_idle': [], 'right_idle',
'up_hoe': [], 'down_hoe': [], 'left_hoe': [], 'right_hoe',
'up_axe': [], 'down_axe': [], 'left_axe': [], 'right_axe',
'up_water': [], 'down_water': [], 'left_water': [], 'right_water'
}
for anim_key in self.animations.keys():
asset_path = f'../graphics/character/{anim_key}'
self.animations[anim_key] = import_folder(asset_path)
def play_animation(self, delta_time):
self.sprite_index += 4 * delta_time
if self.sprite_index >= len(self.animations[self.state]):
self.sprite_index = 0
self.image = self.animations[self.state][int(self.sprite_index)]
def handle_input(self):
pressed_keys = pygame.key.get_pressed()
if not self.cooldowns['tool_action'].is_running:
if pressed_keys[pygame.K_UP]:
self.direction.y = -1
self.state = 'up'
elif pressed_keys[pygame.K_DOWN]:
self.direction.y = 1
self.state = 'down'
else:
self.direction.y = 0
if pressed_keys[pygame.K_RIGHT]:
self.direction.x = 1
self.state = 'right'
elif pressed_keys[pygame.K_LEFT]:
self.direction.x = -1
self.state = 'left'
else:
self.direction.x = 0
if pressed_keys[pygame.K_SPACE]:
self.cooldowns['tool_action'].start()
self.direction = pygame.math.Vector2()
self.sprite_index = 0
def update_state(self):
if self.direction.magnitude() == 0:
base = self.state.split('_')[0]
self.state = f"{base}_idle"
if self.cooldowns['tool_action'].is_running:
base = self.state.split('_')[0]
self.state = f"{base}_{self.current_tool}"
def refresh_cooldowns(self):
for cd in self.cooldowns.values():
cd.update()
def process_movement(self, delta_time):
if self.direction.magnitude() > 0:
self.direction = self.direction.normalize()
self.world_pos.x += self.direction.x * self.move_speed * delta_time
self.rect.centerx = self.world_pos.x
self.world_pos.y += self.direction.y * self.move_speed * delta_time
self.rect.centery = self.world_pos.y
def update(self, delta_time):
self.handle_input()
self.update_state()
self.refresh_cooldowns()
self.process_movement(delta_time)
self.play_animation(delta_time)
countdown.py:
import pygame
class Countdown:
def __init__(self, milliseconds, callback=None):
self.milliseconds = milliseconds
self.callback = callback
self.start_timestamp = 0
self.is_running = False
def start(self):
self.is_running = True
self.start_timestamp = pygame.time.get_ticks()
def stop(self):
self.is_running = False
self.start_timestamp = 0
def update(self):
if not self.is_running:
return
current_time = pygame.time.get_ticks()
if current_time - self.start_timestamp >= self.milliseconds:
if self.callback and self.start_timestamp != 0:
self.callback()
self.stop()