Implementing Tool Usage Mechanics in a Stardew Valley-like Pygame Project

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

Tags: pygame stardew-valley-clone game-mechanics python sprite-animation

Posted on Mon, 03 Aug 2026 16:43:47 +0000 by SBukoski