"""
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 ..bruhutil import WIND_DIRECTIONS, Buffer
from .base_effect import BaseEffect
from .settings import RainSettings
[docs]
class RainEffect(BaseEffect):
"""
Effect to simulate the look of rain.
"""
[docs]
def __init__(self, buffer: Buffer, background: str, settings: RainSettings = None):
"""
Initializes the RainEffect.
Args:
buffer (Buffer): Effect buffer to push updates to.
background (str): Character or string to use for the background.
settings (RainSettings, optional): Configuration for the rain effect. Defaults to None.
"""
super(RainEffect, self).__init__(buffer, background)
s = settings or RainSettings()
self.image_present = False
self.collision = s.collision
self.intensity = s.intensity
self.swells = s.swells
self.swell_direction = 1
self.multiplier = 1
self.wind_direction = (
s.wind_direction if s.wind_direction in WIND_DIRECTIONS else "none"
)
self.wind_mappings = {
"east": [".\\", -1, ["\\", "."]],
"none": [".|", 0, ["|", "."]],
"west": ["./", 1, ["/", "."]],
}
self.lightning = s.lightning
self.lightning_chance = s.lightning_chance
self._bolts = [] # list of (cells, frames_remaining)
self._set_rain()
[docs]
def set_multiplier(self, val: int):
"""
Sets the scroll multiplier (controls shift amount per frame).
Args:
val (int): Multiplier value.
"""
self.multiplier = val
[docs]
def set_wind_direction(self, direction: str):
"""
Sets the direction the rain falls.
Args:
direction (str): Wind direction ("none", "east", or "west").
"""
if direction in WIND_DIRECTIONS:
self.wind_direction = direction
self._set_rain()
[docs]
def set_intensity(self, intensity: int):
"""
Sets the intensity of the rain.
Args:
intensity (int): Intensity value between 0 and 999.
"""
if self.swells:
if self.intensity == 900:
self.swell_direction = -1
if self.intensity == 0:
self.swell_direction = 1
self.intensity += self.swell_direction
else:
self.intensity = intensity if intensity < 1000 else 999
self._set_rain()
[docs]
def set_swells(self, swells: bool):
"""
Enables or disables automatic intensity oscillation.
Args:
swells (bool): Whether the rain intensity should oscillate.
"""
self.swells = swells
[docs]
def set_lightning(self, lightning: bool, chance: float = None):
"""
Enables or disables lightning strikes.
Args:
lightning (bool): Whether to show lightning.
chance (float, optional): Per-frame probability of a new bolt (0.0–1.0).
"""
self.lightning = lightning
if chance is not None:
self.lightning_chance = chance
def _spawn_bolt(self):
w = self.buffer.width()
h = self.buffer.height()
bolt_height = random.randint(h // 3, h - 2)
x = random.randint(2, w - 3)
cells = []
chars = ["|", "/", "\\"]
for y in range(bolt_height):
cells.append((x, y, random.choice(chars)))
x = max(0, min(w - 1, x + random.choice([-1, 0, 0, 1])))
self._bolts.append([cells, random.randint(2, 4)])
def _set_rain(self):
self.rain = f"{' ' * (1000 - self.intensity)}"
if self.intensity > 50:
self.rain += "."
if self.intensity > 250:
self.rain += "."
if self.intensity > 500:
self.rain += self.wind_mappings[self.wind_direction][0]
self.rain_length = len(self.rain)
[docs]
def update_collision(
self,
img_start_x: int,
img_start_y: int,
img_width: int,
img_height: int,
collision: bool,
smart_transparent: bool = False,
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, optional): Smart transparency flag. Defaults to False.
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.smart_transparent = smart_transparent
self.image_buffer = image_buffer
else:
self.image_buffer = None
[docs]
def render_frame(self, frame_number: int):
"""
Renders a single frame of the rain effect.
Args:
frame_number (int): The current frame number.
"""
if self.swells:
self.set_intensity(None)
if frame_number == 0:
self.buffer.put_at(
0,
0,
"".join(
self.rain[random.randint(0, self.rain_length - 1)]
for _ in range(self.buffer.width())
),
)
else:
self.buffer.shift(
self.wind_mappings[self.wind_direction][1] * self.multiplier
)
self.buffer.scroll(-1 * self.multiplier)
self.buffer.put_at(
0,
0,
"".join(
self.rain[random.randint(0, self.rain_length - 1)]
for _ in range(self.buffer.width())
),
)
if self.collision:
for y in range(self.buffer.height()):
for x in range(self.buffer.width()):
if self.buffer.get_char(x, y) == "v":
self.buffer.put_char(x, y, " ")
else:
if self.image_present and self.image_buffer:
if 0 <= y + 1 < self.buffer.height():
if (
self.image_buffer.buffer[y + 1][x]
not in [" ", None]
and self.buffer.get_char(x, y)
in self.wind_mappings[self.wind_direction][2]
):
self.buffer.put_char(x, y, "v")
if y == self.buffer.height() - 1:
if (
self.buffer.get_char(x, y)
in self.wind_mappings[self.wind_direction][2]
):
self.buffer.put_char(x, y, "v")
if self.lightning:
if random.random() < self.lightning_chance:
self._spawn_bolt()
next_bolts = []
for bolt, remaining in self._bolts:
for bx, by, ch in bolt:
self.buffer.put_char(bx, by, ch)
if remaining > 1:
next_bolts.append([bolt, remaining - 1])
self._bolts = next_bolts