Source code for bruhanimate.bruheffect.snow_effect

"""
Copyright 2023 Ethan Christensen

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import random

from bruhcolor import bruhcolored

from ..bruhutil import FLAKE_WEIGHT_CHARS, SNOWFLAKE_COLORS, SNOWFLAKE_TYPES, Buffer
from .base_effect import BaseEffect
from .settings import SnowSettings


[docs] class SnowEffect(BaseEffect): """ A class to represent a snow effect. """
[docs] def __init__(self, buffer: Buffer, background: str, settings: SnowSettings = None): """ Initializes the SnowEffect. Args: buffer (Buffer): Effect buffer to render changes to. background (str): Character or string used for the background of the effect. settings (SnowSettings, optional): Configuration for the snow effect. Defaults to None. """ super(SnowEffect, self).__init__(buffer, background) s = settings or SnowSettings() self.image_present = False self.collision = s.collision self.show_info = s.show_info self.snow_intensity = max(0.01, min(1.0, s.intensity)) self.wind = max(-1.0, min(1.0, s.wind)) self.flakes = [] self.ground_flakes = [ [0 for _ in range(buffer.width())] for _ in range(buffer.height()) ] self.image_flakes = [None for _ in range(self.buffer.width())] self.smart_transparent = False self.total_ground_flakes = 0
[docs] def set_snow_intensity(self, intensity: float): """ Sets the probability of a new snowflake spawning per column per frame. Args: intensity (float): Value between 0.01 and 1.0. """ if 0.01 <= intensity <= 1.0: self.snow_intensity = intensity
[docs] def set_wind(self, wind: float): """ Sets the horizontal wind bias that influences snowflake drift. Args: wind (float): Wind strength from -1.0 (hard left) to 1.0 (hard right). """ self.wind = max(-1.0, min(1.0, wind))
[docs] def set_show_info(self, show_info: bool): """ Toggles the snowflake debug info overlay. Args: show_info (bool): Whether to show flake counts on screen. """ self.show_info = show_info
[docs] def update_collision( self, img_start_x: int, img_start_y: int, img_width: int, img_height: int, collision: bool, smart_transparent: bool, image_buffer: Buffer = None, ): """ Configures collision detection with an image. Args: img_start_x (int): Image x position on screen. img_start_y (int): Image y position on screen. img_width (int): Width of the image. img_height (int): Height of the image. collision (bool): Whether to enable collision. smart_transparent (bool): Smart transparency flag. image_buffer (Buffer, optional): Buffer containing the image. Defaults to None. """ self.image_present = ( True if img_start_x and img_start_y and img_width and img_height else False ) self.collision = collision if self.image_present: self.img_start_x = img_start_x self.img_start_y = img_start_y self.img_height = img_height self.img_width = img_width self.img_end_y = img_start_y + img_height self.image_buffer = image_buffer self.image_x_boundaries = (img_start_x, img_start_x + img_width) self.image_y_boundaries = (img_start_y, img_start_y + img_height) else: self.image_buffer = None
[docs] def generate_snowflake(self, x: int): """ Generates a new snowflake at the given x position with a wind-influenced drift bias. Args: x (int): The x position to spawn the snowflake at. """ snowflake_type = random.choice(list(SNOWFLAKE_TYPES.keys())) snowflake_char = bruhcolored(snowflake_type, SNOWFLAKE_COLORS[snowflake_type]) drift_bias = self.wind + random.uniform(-0.35, 0.35) self.flakes.append( { "x": x, "y": 0, "type": snowflake_type, "colored": snowflake_char, "fall_delay": 0, "drift_bias": drift_bias, } )
[docs] def can_stack(self, x: int, y: int) -> bool: """ Checks whether accumulated snow at (x, y) is dense enough to stack onto the row above. Args: x (int): Column to check. y (int): Row to check. Returns: bool: True if snow should stack upward. """ if y >= self.buffer.height(): return False return self.ground_flakes[y][x] >= 18
[docs] def is_colliding(self, x: int, y: int) -> bool: """ Checks whether a snowflake at (x, y) would collide with an image. Args: x (int): Column to check. y (int): Row to check. Returns: bool: True if the position is inside the image boundary. """ if self.image_present: if ( self.image_x_boundaries[0] < x < self.image_x_boundaries[1] and self.image_y_boundaries[0] < y < self.image_y_boundaries[1] ): return True return False
[docs] def handle_snowflake_landing(self, x: int, y: int): """ Accumulates snow at the landing position and updates the ground character. Args: x (int): Column where the flake landed. y (int): Row where the flake landed. """ self.ground_flakes[y][x] += 1 weight = self.ground_flakes[y][x] for w, char in sorted(FLAKE_WEIGHT_CHARS.items(), reverse=True): if weight >= w: self.buffer.put_char(x, y, char) break if weight > 18 and y > 0: self.ground_flakes[y - 1][x] += 1
[docs] def add_info(self): """ Renders debug info (flake counts) at the top of the buffer. """ self.buffer.put_at(0, 0, f"Total Snow Flakes: {len(self.flakes)}") self.buffer.put_at( 0, 1, f"Total Flakes on Ground: {sum(1 for row in self.ground_flakes for v in row if v > 0)}", )
[docs] def render_frame(self, frame_number: int): """ Renders a single frame of the snow effect. Args: frame_number (int): The current frame number. """ for idx in range(self.buffer.width()): if random.random() < self.snow_intensity: self.generate_snowflake(idx) new_flakes = [] for snowflake in self.flakes: x, y, flake_type = snowflake["x"], snowflake["y"], snowflake["type"] speed = SNOWFLAKE_TYPES[flake_type]["speed"] self.buffer.put_char(x, y, " ") if snowflake["fall_delay"] < speed: snowflake["fall_delay"] += 1 new_flakes.append(snowflake) continue else: snowflake["fall_delay"] = 0 if y + 1 >= self.buffer.height() or self.can_stack(x, y + 1): self.handle_snowflake_landing(x, y) else: snowflake["drift_bias"] = max( -2.0, min(2.0, snowflake["drift_bias"] + random.uniform(-0.08, 0.08)), ) bias = snowflake["drift_bias"] left_w = max(0.05, 1.0 - bias) right_w = max(0.05, 1.0 + bias) dx = random.choices([-1, 0, 1], weights=[left_w, 1.2, right_w])[0] new_x = max(0, min(self.buffer.width() - 1, x + dx)) new_y = y + 1 if ( new_y < self.buffer.height() and not self.is_colliding(new_x, new_y) and 0 <= new_x < self.buffer.width() ): snowflake["x"], snowflake["y"] = new_x, new_y new_flakes.append(snowflake) else: self.handle_snowflake_landing(x, y) self.flakes = new_flakes for snowflake in self.flakes: self.buffer.put_char( snowflake["x"], snowflake["y"], snowflake["colored"].colored ) if self.show_info: self.add_info()