"""
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.
"""
from bruhcolor import bruhcolored
from ..bruhutil import WIND_DIRECTIONS, Buffer
from ..resize import _size
from .base_effect import BaseEffect
from .settings import RainSettings, validated_settings
[docs]
class RainEffect(BaseEffect):
"""
Effect to simulate the look of rain.
"""
[docs]
def __init__(
self, buffer: Buffer, background: str, settings: RainSettings | None = 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.
"""
s = validated_settings(settings, RainSettings)
super(RainEffect, self).__init__(buffer, background, seed=s.seed)
self.image_present = False
self.collision = s.collision
self.intensity = max(0, min(999, 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.lightning_threshold = s.lightning_threshold
self.lightning_color = s.lightning_color
self.lightning_color_type = s.lightning_color_type
self._bolts = [] # list of (cells, frames_remaining)
self._set_rain()
self._tick = 0
[docs]
def resize(self, size: tuple[int, int]) -> None:
"""Adapt to (width, height), preserving visible rain and lightning age.
Drops, image splashes and bolt cells keep absolute coordinates; visible
bottom-row splashes move to the new floor. Cropped cells are discarded.
New space starts empty and receives rain during normal updates. Explicit
collision masks remain at their configured coordinates and dimensions.
Intensity, wind, swell direction, tick and random streams are unchanged.
"""
_size(size)
width, height = size
old_width, old_height = self.buffer.width(), self.buffer.height()
if size == (old_width, old_height):
super().resize(size)
return
columns = min(width, old_width)
raster = Buffer(height, width).clear_buffer(val=self.background)
for y in range(min(height, old_height)):
raster.buffer[y][:columns] = self.buffer.buffer[y][:columns]
if old_height:
for x in range(columns):
if self.buffer.get_char(x, old_height - 1) == "v":
raster.put_char(x, old_height - 1, self.background)
raster.put_char(x, height - 1, "v")
bolts = []
for cells, remaining in self._bolts:
visible = [
(x, y, char)
for x, y, char in cells
if 0 <= x < width and 0 <= y < height
]
if visible:
bolts.append([visible, remaining])
self.buffer = raster
self._bolts = bolts
super().resize(size)
[docs]
def set_multiplier(self, val: int):
"""
Sets the scroll multiplier (controls shift amount per simulation update).
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 = max(0, min(999, intensity))
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,
threshold: int = None,
color: bool = None,
color_type: int = None,
):
"""
Enables or disables lightning strikes.
Args:
lightning (bool): Whether to show lightning.
chance (float, optional): Per-update probability of a new bolt (0.0–1.0).
threshold (int, optional): Intensity threshold for lightning.
color (bool, optional): Whether to color the lightning.
color_type (int, optional): Color code to use for the lightning.
"""
self.lightning = lightning
if chance is not None:
self.lightning_chance = chance
if threshold is not None:
self.lightning_threshold = threshold
if color is not None:
self.lightning_color = color
if color_type is not None:
self.lightning_color_type = color_type
def _spawn_bolt(self):
w = self.buffer.width()
h = self.buffer.height()
if w == 0 or h == 0:
return
bolt_height = h
x = self.rng.randint(2, w - 3) if w >= 5 else self.rng.randint(0, w - 1)
cells = []
chars = ["|", "/", "\\"]
for y in range(bolt_height):
cells.append((x, y, self.rng.choice(chars)))
x = max(0, min(w - 1, x + self.rng.choice([-1, 0, 0, 1])))
self._bolts.append([cells, self.rng.randint(10, 20)])
def _set_rain(self):
self.rain = f"{' ' * (1000 - self.intensity)}"
if self.intensity > 0:
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 update(self, dt: float) -> None:
"""Advance one simulation tick; dt is measured in seconds."""
tick = self._tick
self._tick += 1
if self.swells:
self.set_intensity(None)
if self.lightning:
for bolt, _ in self._bolts:
for bx, by, _ in bolt:
self.buffer.put_char(bx, by, " ")
self._bolts = [
[bolt, remaining - 1]
for bolt, remaining in self._bolts
if remaining > 1
]
if tick == 0:
self.buffer.put_at(
0,
0,
"".join(
self.rain[self.rng.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[self.rng.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.get_char(x, y + 1)
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 (
self.rng.random() < self.lightning_chance
and self.intensity >= self.lightning_threshold
):
self._spawn_bolt()
for bolt, remaining in self._bolts:
for bx, by, ch in bolt:
if self.lightning_color:
colored_ch = bruhcolored(
ch, color=self.lightning_color_type
).colored
self.buffer.put_char(bx, by, colored_ch)
else:
self.buffer.put_char(bx, by, ch)