Skip to content

Thunderstorm

Demo

Quick Start

thunderstorm.py
from terminaltexteffects.effects.effect_thunderstorm import Thunderstorm

effect = Thunderstorm("YourTextHere")
with effect.terminal_output() as terminal:
    for frame in effect:
        terminal.print(frame)

Create a Thunderstorm in the terminal.

Classes:

Name Description
- Thunderstorm

Effect class for the Thunderstorm effect.

- ThunderstormConfig

Configuration for the Thunderstorm effect.

- ThunderstormIterator

Iterates over the effect. Does not normally need to be called directly.

Thunderstorm

Bases: BaseEffect[ThunderstormConfig]

Create a thunderstorm in the terminal.

Rain falls across the canvas. Lightning strikes illuminate the scene and cause sparks at the point of impact. Characters struck by lightning glow.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
class Thunderstorm(BaseEffect[ThunderstormConfig]):
    """Create a thunderstorm in the terminal.

    Rain falls across the canvas. Lightning strikes illuminate the scene and
    cause sparks at the point of impact. Characters struck by lightning glow.
    """

    @property
    def _config_cls(self) -> type[ThunderstormConfig]:
        return ThunderstormConfig

    @property
    def _iterator_cls(self) -> type[ThunderstormIterator]:
        return ThunderstormIterator

ThunderstormConfig dataclass

Bases: BaseConfig

Effect configuration dataclass.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
@dataclass
class ThunderstormConfig(BaseConfig):
    """Effect configuration dataclass."""

    parser_spec: argutils.ParserSpec = argutils.ParserSpec(
        name="thunderstorm",
        help="Create a thunderstorm in the terminal.",
        description="thunderstorm | Create a thunderstorm in the terminal.",
        epilog=(
            "terminaltexteffects thunderstorm --lightning-color 68A3E8 "
            "--glowing-text-color EF5411 --text-glow-time 6 "
            "--raindrop-symbols '\\\\' '.' ',' --spark-symbols '*' '.' \"'\" "
            "--spark-glow-color ff4d00 --spark-glow-time 18 "
            "--storm-time 12 --final-gradient-stops 8A008A 00D1FF ffffff "
            "--final-gradient-steps 12 --final-gradient-frames 3 --final-gradient-direction vertical"
        ),
    )

    lightning_color: tte.Color = argutils.ArgSpec(
        name="--lightning-color",
        type=argutils.ColorArg.type_parser,
        default=tte.Color("#68A3E8"),
        metavar=argutils.ColorArg.METAVAR,
        help="Color for the lightning strike.",
    )  # pyright: ignore[reportAssignmentType]
    "Color: Color for the lightning strike."

    glowing_text_color: tte.Color = argutils.ArgSpec(
        name="--glowing-text-color",
        type=argutils.ColorArg.type_parser,
        default=tte.Color("#EF5411"),
        metavar=argutils.ColorArg.METAVAR,
        help="Color for the text when glowing after a lightning strike.",
    )  # pyright: ignore[reportAssignmentType]
    "Color: Color for the text when glowing after a lightning strike."

    text_glow_time: int = argutils.ArgSpec(
        name="--text-glow-time",
        type=argutils.PositiveInt.type_parser,
        default=6,
        metavar=argutils.PositiveInt.METAVAR,
        help=(
            "Number of frames to display each color in the post-lightning text glow cooling gradient. "
            "Increase to slow down the cooling animation."
        ),
    )  # pyright: ignore[reportAssignmentType]
    (
        "int: Number of frames to display each color in the post-lightning text glow cooling gradient. "
        "Increase to slow down the cooling animation."
    )

    raindrop_symbols: tuple[str, ...] = argutils.ArgSpec(
        name="--raindrop-symbols",
        type=argutils.Symbol.type_parser,
        default=("\\", ".", ","),
        nargs="+",
        action=argutils.TupleAction,
        metavar=argutils.Symbol.METAVAR,
        help="Symbols to use for the raindrops.",
    )  # pyright: ignore[reportAssignmentType]
    "tuple[str, ...]: Symbols to use for the raindrops."

    spark_symbols: tuple[str, ...] = argutils.ArgSpec(
        name="--spark-symbols",
        type=argutils.Symbol.type_parser,
        default=("*", ".", "'"),
        nargs="+",
        action=argutils.TupleAction,
        metavar=argutils.Symbol.METAVAR,
        help="Symbols to use for the lightning impact sparks.",
    )  # pyright: ignore[reportAssignmentType]
    "tuple[str, ...]: Symbols to use for the lightning impact sparks."

    spark_glow_color: tte.Color = argutils.ArgSpec(
        name="--spark-glow-color",
        type=argutils.ColorArg.type_parser,
        default=tte.Color("#ff4d00"),
        metavar=argutils.ColorArg.METAVAR,
        help="Color for the spark glow after a lightning strike.",
    )  # pyright: ignore[reportAssignmentType]
    "Color: Color for the spark glow after a lightning strike."

    spark_glow_time: int = argutils.ArgSpec(
        name="--spark-glow-time",
        type=argutils.PositiveInt.type_parser,
        default=18,
        metavar=argutils.PositiveInt.METAVAR,
        help=(
            "Number of frames to display each color in the post-lightning spark cooling gradient. "
            "Increase to slow down the cooling animation."
        ),
    )  # pyright: ignore[reportAssignmentType]
    (
        "int: Number of frames to display each color in the post-lightning spark cooling gradient. "
        "Increase to slow down the cooling animation."
    )

    storm_time: float = argutils.ArgSpec(
        name="--storm-time",
        type=argutils.PositiveFloat.type_parser,
        default=12,
        metavar=argutils.PositiveFloat.METAVAR,
        help="Duration, in seconds, the storm will occur.",
    )  # pyright: ignore[reportAssignmentType]
    "float: Duration, in seconds, the storm will occur."

    final_gradient_stops: tuple[tte.Color, ...] = FinalGradientStopsArg(
        default=(tte.Color("#8A008A"), tte.Color("#00D1FF"), tte.Color("#FFFFFF")),
    )  # pyright: ignore[reportAssignmentType]
    (
        "tuple[Color, ...]: Space separated, unquoted, list of colors for the character gradient "
        "(applied across the canvas). If only one color is provided, the characters will be displayed in that color."
    )

    final_gradient_steps: tuple[int, ...] = FinalGradientStepsArg(
        default=(12,),
    )  # pyright: ignore[reportAssignmentType]
    (
        "tuple[int, ...]: Space separated, unquoted, list of the number of gradient steps to use. More "
        "steps will create a smoother and longer gradient animation."
    )

    final_gradient_frames: int = FinalGradientFramesArg(
        default=3,
    )  # pyright: ignore[reportAssignmentType]
    "int: Number of frames to display each gradient step. Increase to slow down the gradient animation."

    final_gradient_direction: tte.Gradient.Direction = FinalGradientDirectionArg(
        default=tte.Gradient.Direction.VERTICAL,
    )  # pyright: ignore[reportAssignmentType]
    "Gradient.Direction : Direction of the final gradient."

final_gradient_direction = FinalGradientDirectionArg(default=tte.Gradient.Direction.VERTICAL) class-attribute instance-attribute

Gradient.Direction : Direction of the final gradient.

final_gradient_frames = FinalGradientFramesArg(default=3) class-attribute instance-attribute

int: Number of frames to display each gradient step. Increase to slow down the gradient animation.

final_gradient_steps = FinalGradientStepsArg(default=(12,)) class-attribute instance-attribute

tuple[int, ...]: Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation.

final_gradient_stops = FinalGradientStopsArg(default=(tte.Color('#8A008A'), tte.Color('#00D1FF'), tte.Color('#FFFFFF'))) class-attribute instance-attribute

tuple[Color, ...]: Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color.

glowing_text_color = argutils.ArgSpec(name='--glowing-text-color', type=argutils.ColorArg.type_parser, default=tte.Color('#EF5411'), metavar=argutils.ColorArg.METAVAR, help='Color for the text when glowing after a lightning strike.') class-attribute instance-attribute

Color: Color for the text when glowing after a lightning strike.

lightning_color = argutils.ArgSpec(name='--lightning-color', type=argutils.ColorArg.type_parser, default=tte.Color('#68A3E8'), metavar=argutils.ColorArg.METAVAR, help='Color for the lightning strike.') class-attribute instance-attribute

Color: Color for the lightning strike.

raindrop_symbols = argutils.ArgSpec(name='--raindrop-symbols', type=argutils.Symbol.type_parser, default=('\\', '.', ','), nargs='+', action=argutils.TupleAction, metavar=argutils.Symbol.METAVAR, help='Symbols to use for the raindrops.') class-attribute instance-attribute

tuple[str, ...]: Symbols to use for the raindrops.

spark_glow_color = argutils.ArgSpec(name='--spark-glow-color', type=argutils.ColorArg.type_parser, default=tte.Color('#ff4d00'), metavar=argutils.ColorArg.METAVAR, help='Color for the spark glow after a lightning strike.') class-attribute instance-attribute

Color: Color for the spark glow after a lightning strike.

spark_glow_time = argutils.ArgSpec(name='--spark-glow-time', type=argutils.PositiveInt.type_parser, default=18, metavar=argutils.PositiveInt.METAVAR, help='Number of frames to display each color in the post-lightning spark cooling gradient. Increase to slow down the cooling animation.') class-attribute instance-attribute

int: Number of frames to display each color in the post-lightning spark cooling gradient. Increase to slow down the cooling animation.

spark_symbols = argutils.ArgSpec(name='--spark-symbols', type=argutils.Symbol.type_parser, default=('*', '.', "'"), nargs='+', action=argutils.TupleAction, metavar=argutils.Symbol.METAVAR, help='Symbols to use for the lightning impact sparks.') class-attribute instance-attribute

tuple[str, ...]: Symbols to use for the lightning impact sparks.

storm_time = argutils.ArgSpec(name='--storm-time', type=argutils.PositiveFloat.type_parser, default=12, metavar=argutils.PositiveFloat.METAVAR, help='Duration, in seconds, the storm will occur.') class-attribute instance-attribute

float: Duration, in seconds, the storm will occur.

text_glow_time = argutils.ArgSpec(name='--text-glow-time', type=argutils.PositiveInt.type_parser, default=6, metavar=argutils.PositiveInt.METAVAR, help='Number of frames to display each color in the post-lightning text glow cooling gradient. Increase to slow down the cooling animation.') class-attribute instance-attribute

int: Number of frames to display each color in the post-lightning text glow cooling gradient. Increase to slow down the cooling animation.

ThunderstormIterator

Bases: BaseEffectIterator[ThunderstormConfig]

Effect iterator for the NamedEffect effect.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
class ThunderstormIterator(BaseEffectIterator[ThunderstormConfig]):
    """Effect iterator for the NamedEffect effect."""

    DYNAMIC_NEUTRAL_GRAY = tte.Color("#808080")

    def __init__(self, effect: Thunderstorm) -> None:
        """Initialize the effect iterator.

        Args:
            effect (NamedEffect): The effect to iterate over.

        """
        super().__init__(effect)
        self.character_final_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {}
        self.character_visible_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {}
        self.character_storm_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {}
        self.delay = 0
        self.strike_progression_delay = 0
        self.rain_pool = self.build_rain_pool()
        self.pending_strike_chars: list[EffectCharacter] = []
        self.available_strike_chars: list[EffectCharacter] = []
        self.active_strike_chars: list[EffectCharacter] = []
        self.spark_pool = self.build_spark_pool()
        self.pending_glow_chars: list[EffectCharacter] = []
        self.strike_in_progress: bool = False
        self.flashing: bool = False
        self.strike_branch_chance = 0.05
        self.phase: str = "pre-storm"
        self.storm_start_time = self.terminal.clock()
        self.build()

    @staticmethod
    def _adjust_color_pair_brightness(colors: tte.ColorPair, brightness: float) -> tte.ColorPair:
        return tte.ColorPair(
            fg=(tte.Animation.adjust_color_brightness(colors.fg, brightness) if colors.fg is not None else None),
            bg=(tte.Animation.adjust_color_brightness(colors.bg, brightness) if colors.bg is not None else None),
        )

    @staticmethod
    def _add_color_pair_gradient_frames(
        scene: tte.Scene,
        symbol: str,
        start_colors: tte.ColorPair,
        end_colors: tte.ColorPair,
        *,
        steps: int,
        duration: int,
    ) -> None:
        fg_steps = (
            list(
                tte.Gradient(
                    typing.cast("tte.Color", start_colors.fg),
                    typing.cast("tte.Color", end_colors.fg),
                    steps=steps,
                ),
            )
            if start_colors.fg is not None and end_colors.fg is not None
            else [end_colors.fg if end_colors.fg is not None else start_colors.fg] * steps
        )
        bg_steps = (
            list(
                tte.Gradient(
                    typing.cast("tte.Color", start_colors.bg),
                    typing.cast("tte.Color", end_colors.bg),
                    steps=steps,
                ),
            )
            if start_colors.bg is not None and end_colors.bg is not None
            else [end_colors.bg if end_colors.bg is not None else start_colors.bg] * steps
        )
        for index in range(steps):
            scene.add_frame(
                symbol=symbol,
                colors=tte.ColorPair(
                    fg=fg_steps[index],
                    bg=bg_steps[index],
                ),
                duration=duration,
            )

    def build(self) -> None:
        """Build the effect."""
        final_gradient = tte.Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps)
        final_gradient_mapping = final_gradient.build_coordinate_color_mapping(
            self.terminal.canvas.text_bottom,
            self.terminal.canvas.text_top,
            self.terminal.canvas.text_left,
            self.terminal.canvas.text_right,
            self.config.final_gradient_direction,
        )
        self.build_strike_characters()

        # setup scenes on text characters
        all_chars = self.terminal.get_characters()
        for text_char in all_chars:
            if self.terminal.config.existing_color_handling == "dynamic":
                visible_colors = tte.ColorPair(
                    fg=(
                        text_char.animation.input_fg_color
                        if text_char.animation.input_fg_color is not None
                        else self.DYNAMIC_NEUTRAL_GRAY
                    ),
                    bg=text_char.animation.input_bg_color,
                )
                restore_colors = tte.ColorPair(
                    fg=text_char.animation.input_fg_color,
                    bg=text_char.animation.input_bg_color,
                )
            else:
                visible_colors = tte.ColorPair(
                    fg=final_gradient_mapping[text_char.input_coord],
                )
                restore_colors = visible_colors
            storm_colors = self._adjust_color_pair_brightness(visible_colors, brightness=0.5)
            self.character_visible_color_map[text_char] = visible_colors
            self.character_storm_color_map[text_char] = storm_colors
            self.character_final_color_map[text_char] = restore_colors

            # post-strike glow and cool scene
            glow_scn = text_char.animation.new_scene(scene_id="glow")
            glow_fg_gradient = tte.Gradient(
                self.config.glowing_text_color,
                typing.cast("tte.Color", storm_colors.fg),
                steps=7,
            )
            for color in glow_fg_gradient:
                glow_scn.add_frame(
                    symbol=text_char.input_symbol,
                    colors=tte.ColorPair(fg=color, bg=storm_colors.bg),
                    duration=self.config.text_glow_time,
                )
            if self.terminal.config.existing_color_handling == "dynamic":
                glow_scn.add_frame(
                    symbol=text_char.input_symbol,
                    colors=storm_colors,
                    duration=self.config.text_glow_time,
                )

            # fade before storm scene
            fade_scn = text_char.animation.new_scene(scene_id="fade")
            if self.terminal.config.existing_color_handling == "dynamic":
                self._add_color_pair_gradient_frames(
                    fade_scn,
                    text_char.input_symbol,
                    visible_colors,
                    storm_colors,
                    steps=7,
                    duration=12,
                )
                fade_scn.add_frame(symbol=text_char.input_symbol, colors=storm_colors, duration=12)
            else:
                fade_gradient = tte.Gradient(
                    typing.cast("tte.Color", visible_colors.fg),
                    typing.cast("tte.Color", storm_colors.fg),
                    steps=7,
                )
                for color in fade_gradient:
                    fade_scn.add_frame(symbol=text_char.input_symbol, colors=tte.ColorPair(fg=color), duration=12)

            unfade_scn = text_char.animation.new_scene(scene_id="unfade")
            if self.terminal.config.existing_color_handling == "dynamic":
                self._add_color_pair_gradient_frames(
                    unfade_scn,
                    text_char.input_symbol,
                    storm_colors,
                    visible_colors,
                    steps=7,
                    duration=12,
                )
                unfade_scn.add_frame(symbol=text_char.input_symbol, colors=visible_colors, duration=12)
                if restore_colors != visible_colors:
                    unfade_scn.add_frame(symbol=text_char.input_symbol, colors=restore_colors, duration=12)
            else:
                unfade_gradient = list(
                    tte.Gradient(
                        typing.cast("tte.Color", visible_colors.fg),
                        typing.cast("tte.Color", storm_colors.fg),
                        steps=7,
                    ),
                )[::-1]
                for color in unfade_gradient:
                    unfade_scn.add_frame(symbol=text_char.input_symbol, colors=tte.ColorPair(fg=color), duration=12)

            # lightning flash scene
            lightning_flash_color = tte.Animation.adjust_color_brightness(
                typing.cast("tte.Color", visible_colors.fg),
                brightness=1.7,
            )
            strike_scn = text_char.animation.new_scene(scene_id="flash")
            flash_gradient = tte.Gradient(
                typing.cast("tte.Color", storm_colors.fg),
                lightning_flash_color,
                steps=7,
                loop=True,
            )
            for color in flash_gradient:
                strike_scn.add_frame(
                    symbol=text_char.input_symbol,
                    colors=tte.ColorPair(fg=color, bg=storm_colors.bg),
                    duration=6,
                )

            self.terminal.set_character_visibility(text_char, is_visible=True)

        # setup a reference character callback to indicate when the pre-storm fade has completed
        def fade_complete(*_: typing.Any) -> None:
            self.phase = "storm"
            self.storm_start_time = self.terminal.clock()

        reference_char = all_chars[0]
        reference_char.event_handler.register_event(
            event=tte.Event.SCENE_COMPLETE,
            caller="fade",
            action=tte.Action.CALLBACK,
            target=tte.EventHandler.Callback(fade_complete),
        )

    def build_rain_pool(self) -> tte.ParticlePool:
        """Build and return the raindrop particle pool."""

        def initialize_raindrop(rain_char: EffectCharacter) -> None:
            rain_char.layer = 1
            rain_char.animation.set_appearance(
                symbol=rain_char.input_symbol,
                colors=tte.ColorPair(fg=tte.Color("#aaaaff")),
            )

        return tte.ParticlePool(
            terminal=self.terminal,
            active_characters=self.active_characters,
            symbols=self.config.raindrop_symbols,
            initial_count=50,
            initializer=initialize_raindrop,
        )

    def build_spark_pool(self) -> tte.ParticlePool:
        """Build and return the spark particle pool."""
        spark_gradient = tte.Gradient(
            self.config.spark_glow_color,
            self.terminal.config.terminal_background_color,
            steps=7,
        )

        def _build_spark_characters(new_spark_char: EffectCharacter) -> None:
            """Build spark characters for the lightning strike effect."""
            new_spark_char.layer = 2
            spark_scn = new_spark_char.animation.new_scene(scene_id="glow", ease=tte.easing.in_circ)
            for color in spark_gradient:
                spark_scn.add_frame(
                    symbol=new_spark_char.input_symbol,
                    colors=tte.ColorPair(fg=color),
                    duration=self.config.spark_glow_time,
                )

        return tte.ParticlePool(
            terminal=self.terminal,
            active_characters=self.active_characters,
            symbols=self.config.spark_symbols,
            initial_count=200,
            max_size=2000,
            initializer=_build_spark_characters,
        )

    def build_strike_characters(self, count: int = 200) -> None:
        """Build strike characters for the lightning strike effect."""
        for _ in range(count):
            strike_char = self.terminal.add_character(
                symbol="|",
                coord=tte.Coord(1, 1),
            )
            self.available_strike_chars.append(strike_char)

    def hide_character(self, character: tte.EffectCharacter, *_: typing.Any) -> None:
        """Hide a helper character."""
        self.terminal.set_character_visibility(character, is_visible=False)

    def return_strike_to_pool(self, character: tte.EffectCharacter, *_: typing.Any) -> None:
        """Return a strike character to the available pool."""
        self.available_strike_chars.append(character)

    def get_next_strike_char(self) -> tte.EffectCharacter:
        """Get the next available strike character, creating more when needed."""
        if not self.available_strike_chars:
            self.build_strike_characters(20)
        strike_char = self.available_strike_chars.pop()
        strike_char.animation.scenes.clear()
        strike_char.event_handler.registered_events.clear()
        return strike_char

    def set_strike_in_progress_false(self, *_: typing.Any) -> None:
        """Reset the strike in progress flag."""
        self.strike_in_progress = False

    def make_char_glow(self, strike_char: tte.EffectCharacter) -> None:
        """Activate the 'cool' scene on any text character behind a strike character.

        Args:
            strike_char (tte.EffectCharacter): Strike character.

        """
        input_char = self.terminal.get_character_by_input_coord(strike_char.motion.current_coord)
        if input_char and input_char.is_visible:
            input_char.animation.activate_scene("glow")
            self.pending_glow_chars.append(input_char)

    def _setup_sparks_for_impact(self, spark_char: EffectCharacter) -> None:
        """Configure sparks for the impact of a lightning strike."""
        impact_coord = spark_char.motion.current_coord
        spark_path = spark_char.motion.new_path(
            speed=random.uniform(0.1, 0.25),
            ease=tte.easing.out_quint,
            hold_time=30,
        )
        spark_target = tte.Coord(
            column=impact_coord.column + random.randint(4, 20) * random.choice((1, -1)),
            row=self.terminal.canvas.bottom,
        )
        bezier_column = impact_coord.column - ((impact_coord.column - spark_target.column) // 2)
        spark_path.new_waypoint(
            coord=spark_target,
            bezier_control=tte.Coord(column=bezier_column, row=random.randint(1, self.terminal.canvas.top)),
        )
        self.spark_pool.reclaim_on_event(
            spark_char,
            spark_char.animation.query_scene("glow"),
            event=tte.Event.SCENE_COMPLETE,
        )
        spark_char.animation.activate_scene("glow")
        spark_char.motion.activate_path(spark_path)

    def setup_lightning_strike(self, branch_neighbor: tte.EffectCharacter | None = None) -> None:
        """Build a lightning strike effect."""
        if branch_neighbor is not None:
            column, row = branch_neighbor.motion.current_coord
            row = branch_neighbor.motion.current_coord.row
        else:
            column = random.randint(1, self.terminal.canvas.right)
            row = self.terminal.canvas.top

        while row >= self.terminal.canvas.bottom:
            if not self.available_strike_chars:
                self.build_strike_characters(20)
            if branch_neighbor is not None:
                if branch_neighbor.input_symbol == "/":
                    column += 1
                    symbol = random.choice(("|", "\\"))
                elif branch_neighbor.input_symbol == "\\":
                    column -= 1
                    symbol = random.choice(("|", "/"))
                else:
                    delta = random.choice((-1, 1))
                    column += delta
                    symbol = "\\" if delta == 1 else "/"
            else:
                symbol = random.choice(("\\", "/", "|"))

            strike_char = self.get_next_strike_char()
            strike_char.motion.set_coordinate(tte.Coord(column, row))
            strike_char.animation.set_appearance(symbol=symbol, colors=tte.ColorPair(fg=self.config.lightning_color))
            row -= 1
            if symbol == "\\":
                column += 1
            elif symbol == "/":
                column -= 1

            self.pending_strike_chars.append(strike_char)
            if random.random() < self.strike_branch_chance and branch_neighbor is None:
                self.strike_branch_chance -= 0.01
                self.setup_lightning_strike(branch_neighbor=strike_char)
            branch_neighbor = None
        self.strike_branch_chance = 0.05

    def lightning_strike(self) -> None:
        """Trigger a lightning strike effect."""
        self.setup_lightning_strike()
        strike_base_color = self.config.lightning_color
        strike_flash_color = tte.Animation.adjust_color_brightness(strike_base_color, 1.7)
        strike_gradient = tte.Gradient(
            strike_base_color,
            strike_flash_color,
            steps=7,
            loop=True,
        )
        fade_gradient = tte.Gradient(strike_base_color, self.terminal.config.terminal_background_color, steps=6)
        layer = 1
        flash_ease = tte.easing.make_easing(0, 1.6, 1, random.uniform(-0.6, 0.4))
        for strike_char in self.pending_strike_chars:
            flash_scn = strike_char.animation.new_scene(scene_id="flash", ease=flash_ease)
            for color in strike_gradient:
                flash_scn.add_frame(
                    symbol=strike_char.animation.current_character_visual.symbol,
                    colors=tte.ColorPair(fg=color),
                    duration=6,
                )
            fade_scn = strike_char.animation.new_scene(scene_id="fade")
            for color in fade_gradient:
                fade_scn.add_frame(
                    symbol=strike_char.animation.current_character_visual.symbol,
                    colors=tte.ColorPair(fg=color),
                    duration=2,
                )
            strike_char.layer = layer
            strike_char.event_handler.register_event(
                event=tte.Event.SCENE_COMPLETE,
                caller=flash_scn,
                action=tte.Action.ACTIVATE_SCENE,
                target=fade_scn,
            )
            strike_char.event_handler.register_event(
                event=tte.Event.SCENE_COMPLETE,
                caller=fade_scn,
                action=tte.Action.CALLBACK,
                target=tte.EventHandler.Callback(self.hide_character),
            )
            strike_char.event_handler.register_event(
                event=tte.Event.SCENE_COMPLETE,
                caller=fade_scn,
                action=tte.Action.CALLBACK,
                target=tte.EventHandler.Callback(self.make_char_glow),
            )
            strike_char.event_handler.register_event(
                event=tte.Event.SCENE_COMPLETE,
                caller=fade_scn,
                action=tte.Action.CALLBACK,
                target=tte.EventHandler.Callback(self.return_strike_to_pool),
            )

        for text_char in self.terminal.get_characters():
            flash_scene = text_char.animation.query_scene("flash")
            flash_scene.ease = flash_ease  # pyright: ignore[reportOptionalMemberAccess]

    def step_lightning_strike(self) -> None:
        """Progress the lightning strike effect."""
        if self.strike_progression_delay:
            self.strike_progression_delay -= 1
            return
        if self.pending_strike_chars:
            for _ in range(random.randint(1, 3)):
                if not self.pending_strike_chars:
                    break
                next_strike_char = self.pending_strike_chars.pop(0)
                self.active_strike_chars.append(next_strike_char)
                self.terminal.set_character_visibility(next_strike_char, is_visible=True)
                self.strike_progression_delay = 1

                # if the last strike_char was activated, activate the sparks
                # and setup the post-fade callback to indicate the strike has
                # ended
                if not self.pending_strike_chars:
                    for _ in range(random.randint(12, 18)):
                        self.spark_pool.emit(
                            self.active_strike_chars[-1].motion.current_coord,
                            on_emit=self._setup_sparks_for_impact,
                            visible=True,
                            reset=tte.ParticleReset(clear_events=True),
                        )
                    next_strike_char.event_handler.register_event(
                        event=tte.Event.SCENE_COMPLETE,
                        caller="fade",
                        action=tte.Action.CALLBACK,
                        target=tte.EventHandler.Callback(self.set_strike_in_progress_false),
                    )

                    # activate the flash scene on all strike chars and text
                    for strike_char in self.active_strike_chars:
                        strike_char.animation.activate_scene("flash")
                        self.active_characters.add(strike_char)
                    self.active_strike_chars.clear()

                    for text_char in self.terminal.get_characters():
                        text_char.animation.activate_scene("flash")
                        self.active_characters.add(text_char)

    def _setup_raindrop(self, rain_char: EffectCharacter) -> None:
        """Configure a raindrop for one fall across the canvas."""
        origin = rain_char.motion.current_coord
        fall_path = rain_char.motion.new_path(
            speed=random.uniform(0.5, 1.5),
        )
        fall_path.new_waypoint(
            tte.Coord(
                column=origin.column + self.terminal.canvas.top + 1,
                row=self.terminal.canvas.bottom - 1,
            ),
        )
        self.rain_pool.reclaim_on_event(
            rain_char,
            fall_path,
            event=tte.Event.PATH_COMPLETE,
        )
        rain_char.motion.activate_path(fall_path)

    def rain(self) -> None:
        """Handle the rain effect."""
        if self.delay:
            self.delay -= 1
            return

        for _ in range(random.randint(1, 6)):
            spawn_column = random.randint(1 - self.terminal.canvas.top, self.terminal.canvas.right)
            self.rain_pool.emit(
                tte.Coord(column=spawn_column - 1, row=self.terminal.canvas.top + 1),
                on_emit=self._setup_raindrop,
                reset=tte.ParticleReset(clear_events=True),
            )
        self.delay = random.randint(1, 7)

    def pre_storm_text_fade(self) -> None:
        """Activate the fade effect for all text characters before the storm."""
        for char in self.terminal.get_characters():
            char.animation.activate_scene("fade")
            self.active_characters.add(char)

    def post_storm_text_fade_in(self) -> None:
        """Active the fade in scene for all text characters after the storm clears."""
        for char in self.terminal.get_characters():
            char.animation.activate_scene("unfade")
            self.active_characters.add(char)

    def __next__(self) -> str:
        """Return the next frame of the effect."""
        if self.active_characters or self.phase != "complete":
            if self.phase == "pre-storm":
                self.pre_storm_text_fade()
                self.phase = "waiting"
            elif self.phase == "storm":
                self.rain()
                if not self.strike_in_progress and random.random() < 0.008:
                    self.strike_in_progress = True
                    self.lightning_strike()
                if self.strike_in_progress:
                    self.step_lightning_strike()

                for char in self.pending_glow_chars:
                    self.active_characters.add(char)
                self.pending_glow_chars.clear()
                if (
                    self.terminal.clock() - self.storm_start_time
                ) >= self.config.storm_time and not self.strike_in_progress:
                    self.post_storm_text_fade_in()
                    self.phase = "complete"
            self.update()
            return self.frame
        raise StopIteration

__init__(effect)

Initialize the effect iterator.

Parameters:

Name Type Description Default
effect NamedEffect

The effect to iterate over.

required
Source code in terminaltexteffects/effects/effect_thunderstorm.py
def __init__(self, effect: Thunderstorm) -> None:
    """Initialize the effect iterator.

    Args:
        effect (NamedEffect): The effect to iterate over.

    """
    super().__init__(effect)
    self.character_final_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {}
    self.character_visible_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {}
    self.character_storm_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {}
    self.delay = 0
    self.strike_progression_delay = 0
    self.rain_pool = self.build_rain_pool()
    self.pending_strike_chars: list[EffectCharacter] = []
    self.available_strike_chars: list[EffectCharacter] = []
    self.active_strike_chars: list[EffectCharacter] = []
    self.spark_pool = self.build_spark_pool()
    self.pending_glow_chars: list[EffectCharacter] = []
    self.strike_in_progress: bool = False
    self.flashing: bool = False
    self.strike_branch_chance = 0.05
    self.phase: str = "pre-storm"
    self.storm_start_time = self.terminal.clock()
    self.build()

__next__()

Return the next frame of the effect.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def __next__(self) -> str:
    """Return the next frame of the effect."""
    if self.active_characters or self.phase != "complete":
        if self.phase == "pre-storm":
            self.pre_storm_text_fade()
            self.phase = "waiting"
        elif self.phase == "storm":
            self.rain()
            if not self.strike_in_progress and random.random() < 0.008:
                self.strike_in_progress = True
                self.lightning_strike()
            if self.strike_in_progress:
                self.step_lightning_strike()

            for char in self.pending_glow_chars:
                self.active_characters.add(char)
            self.pending_glow_chars.clear()
            if (
                self.terminal.clock() - self.storm_start_time
            ) >= self.config.storm_time and not self.strike_in_progress:
                self.post_storm_text_fade_in()
                self.phase = "complete"
        self.update()
        return self.frame
    raise StopIteration

build()

Build the effect.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def build(self) -> None:
    """Build the effect."""
    final_gradient = tte.Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps)
    final_gradient_mapping = final_gradient.build_coordinate_color_mapping(
        self.terminal.canvas.text_bottom,
        self.terminal.canvas.text_top,
        self.terminal.canvas.text_left,
        self.terminal.canvas.text_right,
        self.config.final_gradient_direction,
    )
    self.build_strike_characters()

    # setup scenes on text characters
    all_chars = self.terminal.get_characters()
    for text_char in all_chars:
        if self.terminal.config.existing_color_handling == "dynamic":
            visible_colors = tte.ColorPair(
                fg=(
                    text_char.animation.input_fg_color
                    if text_char.animation.input_fg_color is not None
                    else self.DYNAMIC_NEUTRAL_GRAY
                ),
                bg=text_char.animation.input_bg_color,
            )
            restore_colors = tte.ColorPair(
                fg=text_char.animation.input_fg_color,
                bg=text_char.animation.input_bg_color,
            )
        else:
            visible_colors = tte.ColorPair(
                fg=final_gradient_mapping[text_char.input_coord],
            )
            restore_colors = visible_colors
        storm_colors = self._adjust_color_pair_brightness(visible_colors, brightness=0.5)
        self.character_visible_color_map[text_char] = visible_colors
        self.character_storm_color_map[text_char] = storm_colors
        self.character_final_color_map[text_char] = restore_colors

        # post-strike glow and cool scene
        glow_scn = text_char.animation.new_scene(scene_id="glow")
        glow_fg_gradient = tte.Gradient(
            self.config.glowing_text_color,
            typing.cast("tte.Color", storm_colors.fg),
            steps=7,
        )
        for color in glow_fg_gradient:
            glow_scn.add_frame(
                symbol=text_char.input_symbol,
                colors=tte.ColorPair(fg=color, bg=storm_colors.bg),
                duration=self.config.text_glow_time,
            )
        if self.terminal.config.existing_color_handling == "dynamic":
            glow_scn.add_frame(
                symbol=text_char.input_symbol,
                colors=storm_colors,
                duration=self.config.text_glow_time,
            )

        # fade before storm scene
        fade_scn = text_char.animation.new_scene(scene_id="fade")
        if self.terminal.config.existing_color_handling == "dynamic":
            self._add_color_pair_gradient_frames(
                fade_scn,
                text_char.input_symbol,
                visible_colors,
                storm_colors,
                steps=7,
                duration=12,
            )
            fade_scn.add_frame(symbol=text_char.input_symbol, colors=storm_colors, duration=12)
        else:
            fade_gradient = tte.Gradient(
                typing.cast("tte.Color", visible_colors.fg),
                typing.cast("tte.Color", storm_colors.fg),
                steps=7,
            )
            for color in fade_gradient:
                fade_scn.add_frame(symbol=text_char.input_symbol, colors=tte.ColorPair(fg=color), duration=12)

        unfade_scn = text_char.animation.new_scene(scene_id="unfade")
        if self.terminal.config.existing_color_handling == "dynamic":
            self._add_color_pair_gradient_frames(
                unfade_scn,
                text_char.input_symbol,
                storm_colors,
                visible_colors,
                steps=7,
                duration=12,
            )
            unfade_scn.add_frame(symbol=text_char.input_symbol, colors=visible_colors, duration=12)
            if restore_colors != visible_colors:
                unfade_scn.add_frame(symbol=text_char.input_symbol, colors=restore_colors, duration=12)
        else:
            unfade_gradient = list(
                tte.Gradient(
                    typing.cast("tte.Color", visible_colors.fg),
                    typing.cast("tte.Color", storm_colors.fg),
                    steps=7,
                ),
            )[::-1]
            for color in unfade_gradient:
                unfade_scn.add_frame(symbol=text_char.input_symbol, colors=tte.ColorPair(fg=color), duration=12)

        # lightning flash scene
        lightning_flash_color = tte.Animation.adjust_color_brightness(
            typing.cast("tte.Color", visible_colors.fg),
            brightness=1.7,
        )
        strike_scn = text_char.animation.new_scene(scene_id="flash")
        flash_gradient = tte.Gradient(
            typing.cast("tte.Color", storm_colors.fg),
            lightning_flash_color,
            steps=7,
            loop=True,
        )
        for color in flash_gradient:
            strike_scn.add_frame(
                symbol=text_char.input_symbol,
                colors=tte.ColorPair(fg=color, bg=storm_colors.bg),
                duration=6,
            )

        self.terminal.set_character_visibility(text_char, is_visible=True)

    # setup a reference character callback to indicate when the pre-storm fade has completed
    def fade_complete(*_: typing.Any) -> None:
        self.phase = "storm"
        self.storm_start_time = self.terminal.clock()

    reference_char = all_chars[0]
    reference_char.event_handler.register_event(
        event=tte.Event.SCENE_COMPLETE,
        caller="fade",
        action=tte.Action.CALLBACK,
        target=tte.EventHandler.Callback(fade_complete),
    )

build_rain_pool()

Build and return the raindrop particle pool.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def build_rain_pool(self) -> tte.ParticlePool:
    """Build and return the raindrop particle pool."""

    def initialize_raindrop(rain_char: EffectCharacter) -> None:
        rain_char.layer = 1
        rain_char.animation.set_appearance(
            symbol=rain_char.input_symbol,
            colors=tte.ColorPair(fg=tte.Color("#aaaaff")),
        )

    return tte.ParticlePool(
        terminal=self.terminal,
        active_characters=self.active_characters,
        symbols=self.config.raindrop_symbols,
        initial_count=50,
        initializer=initialize_raindrop,
    )

build_spark_pool()

Build and return the spark particle pool.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def build_spark_pool(self) -> tte.ParticlePool:
    """Build and return the spark particle pool."""
    spark_gradient = tte.Gradient(
        self.config.spark_glow_color,
        self.terminal.config.terminal_background_color,
        steps=7,
    )

    def _build_spark_characters(new_spark_char: EffectCharacter) -> None:
        """Build spark characters for the lightning strike effect."""
        new_spark_char.layer = 2
        spark_scn = new_spark_char.animation.new_scene(scene_id="glow", ease=tte.easing.in_circ)
        for color in spark_gradient:
            spark_scn.add_frame(
                symbol=new_spark_char.input_symbol,
                colors=tte.ColorPair(fg=color),
                duration=self.config.spark_glow_time,
            )

    return tte.ParticlePool(
        terminal=self.terminal,
        active_characters=self.active_characters,
        symbols=self.config.spark_symbols,
        initial_count=200,
        max_size=2000,
        initializer=_build_spark_characters,
    )

build_strike_characters(count=200)

Build strike characters for the lightning strike effect.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def build_strike_characters(self, count: int = 200) -> None:
    """Build strike characters for the lightning strike effect."""
    for _ in range(count):
        strike_char = self.terminal.add_character(
            symbol="|",
            coord=tte.Coord(1, 1),
        )
        self.available_strike_chars.append(strike_char)

get_next_strike_char()

Get the next available strike character, creating more when needed.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def get_next_strike_char(self) -> tte.EffectCharacter:
    """Get the next available strike character, creating more when needed."""
    if not self.available_strike_chars:
        self.build_strike_characters(20)
    strike_char = self.available_strike_chars.pop()
    strike_char.animation.scenes.clear()
    strike_char.event_handler.registered_events.clear()
    return strike_char

hide_character(character, *_)

Hide a helper character.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def hide_character(self, character: tte.EffectCharacter, *_: typing.Any) -> None:
    """Hide a helper character."""
    self.terminal.set_character_visibility(character, is_visible=False)

lightning_strike()

Trigger a lightning strike effect.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def lightning_strike(self) -> None:
    """Trigger a lightning strike effect."""
    self.setup_lightning_strike()
    strike_base_color = self.config.lightning_color
    strike_flash_color = tte.Animation.adjust_color_brightness(strike_base_color, 1.7)
    strike_gradient = tte.Gradient(
        strike_base_color,
        strike_flash_color,
        steps=7,
        loop=True,
    )
    fade_gradient = tte.Gradient(strike_base_color, self.terminal.config.terminal_background_color, steps=6)
    layer = 1
    flash_ease = tte.easing.make_easing(0, 1.6, 1, random.uniform(-0.6, 0.4))
    for strike_char in self.pending_strike_chars:
        flash_scn = strike_char.animation.new_scene(scene_id="flash", ease=flash_ease)
        for color in strike_gradient:
            flash_scn.add_frame(
                symbol=strike_char.animation.current_character_visual.symbol,
                colors=tte.ColorPair(fg=color),
                duration=6,
            )
        fade_scn = strike_char.animation.new_scene(scene_id="fade")
        for color in fade_gradient:
            fade_scn.add_frame(
                symbol=strike_char.animation.current_character_visual.symbol,
                colors=tte.ColorPair(fg=color),
                duration=2,
            )
        strike_char.layer = layer
        strike_char.event_handler.register_event(
            event=tte.Event.SCENE_COMPLETE,
            caller=flash_scn,
            action=tte.Action.ACTIVATE_SCENE,
            target=fade_scn,
        )
        strike_char.event_handler.register_event(
            event=tte.Event.SCENE_COMPLETE,
            caller=fade_scn,
            action=tte.Action.CALLBACK,
            target=tte.EventHandler.Callback(self.hide_character),
        )
        strike_char.event_handler.register_event(
            event=tte.Event.SCENE_COMPLETE,
            caller=fade_scn,
            action=tte.Action.CALLBACK,
            target=tte.EventHandler.Callback(self.make_char_glow),
        )
        strike_char.event_handler.register_event(
            event=tte.Event.SCENE_COMPLETE,
            caller=fade_scn,
            action=tte.Action.CALLBACK,
            target=tte.EventHandler.Callback(self.return_strike_to_pool),
        )

    for text_char in self.terminal.get_characters():
        flash_scene = text_char.animation.query_scene("flash")
        flash_scene.ease = flash_ease  # pyright: ignore[reportOptionalMemberAccess]

make_char_glow(strike_char)

Activate the 'cool' scene on any text character behind a strike character.

Parameters:

Name Type Description Default
strike_char EffectCharacter

Strike character.

required
Source code in terminaltexteffects/effects/effect_thunderstorm.py
def make_char_glow(self, strike_char: tte.EffectCharacter) -> None:
    """Activate the 'cool' scene on any text character behind a strike character.

    Args:
        strike_char (tte.EffectCharacter): Strike character.

    """
    input_char = self.terminal.get_character_by_input_coord(strike_char.motion.current_coord)
    if input_char and input_char.is_visible:
        input_char.animation.activate_scene("glow")
        self.pending_glow_chars.append(input_char)

post_storm_text_fade_in()

Active the fade in scene for all text characters after the storm clears.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def post_storm_text_fade_in(self) -> None:
    """Active the fade in scene for all text characters after the storm clears."""
    for char in self.terminal.get_characters():
        char.animation.activate_scene("unfade")
        self.active_characters.add(char)

pre_storm_text_fade()

Activate the fade effect for all text characters before the storm.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def pre_storm_text_fade(self) -> None:
    """Activate the fade effect for all text characters before the storm."""
    for char in self.terminal.get_characters():
        char.animation.activate_scene("fade")
        self.active_characters.add(char)

rain()

Handle the rain effect.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def rain(self) -> None:
    """Handle the rain effect."""
    if self.delay:
        self.delay -= 1
        return

    for _ in range(random.randint(1, 6)):
        spawn_column = random.randint(1 - self.terminal.canvas.top, self.terminal.canvas.right)
        self.rain_pool.emit(
            tte.Coord(column=spawn_column - 1, row=self.terminal.canvas.top + 1),
            on_emit=self._setup_raindrop,
            reset=tte.ParticleReset(clear_events=True),
        )
    self.delay = random.randint(1, 7)

return_strike_to_pool(character, *_)

Return a strike character to the available pool.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def return_strike_to_pool(self, character: tte.EffectCharacter, *_: typing.Any) -> None:
    """Return a strike character to the available pool."""
    self.available_strike_chars.append(character)

set_strike_in_progress_false(*_)

Reset the strike in progress flag.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def set_strike_in_progress_false(self, *_: typing.Any) -> None:
    """Reset the strike in progress flag."""
    self.strike_in_progress = False

setup_lightning_strike(branch_neighbor=None)

Build a lightning strike effect.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def setup_lightning_strike(self, branch_neighbor: tte.EffectCharacter | None = None) -> None:
    """Build a lightning strike effect."""
    if branch_neighbor is not None:
        column, row = branch_neighbor.motion.current_coord
        row = branch_neighbor.motion.current_coord.row
    else:
        column = random.randint(1, self.terminal.canvas.right)
        row = self.terminal.canvas.top

    while row >= self.terminal.canvas.bottom:
        if not self.available_strike_chars:
            self.build_strike_characters(20)
        if branch_neighbor is not None:
            if branch_neighbor.input_symbol == "/":
                column += 1
                symbol = random.choice(("|", "\\"))
            elif branch_neighbor.input_symbol == "\\":
                column -= 1
                symbol = random.choice(("|", "/"))
            else:
                delta = random.choice((-1, 1))
                column += delta
                symbol = "\\" if delta == 1 else "/"
        else:
            symbol = random.choice(("\\", "/", "|"))

        strike_char = self.get_next_strike_char()
        strike_char.motion.set_coordinate(tte.Coord(column, row))
        strike_char.animation.set_appearance(symbol=symbol, colors=tte.ColorPair(fg=self.config.lightning_color))
        row -= 1
        if symbol == "\\":
            column += 1
        elif symbol == "/":
            column -= 1

        self.pending_strike_chars.append(strike_char)
        if random.random() < self.strike_branch_chance and branch_neighbor is None:
            self.strike_branch_chance -= 0.01
            self.setup_lightning_strike(branch_neighbor=strike_char)
        branch_neighbor = None
    self.strike_branch_chance = 0.05

step_lightning_strike()

Progress the lightning strike effect.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def step_lightning_strike(self) -> None:
    """Progress the lightning strike effect."""
    if self.strike_progression_delay:
        self.strike_progression_delay -= 1
        return
    if self.pending_strike_chars:
        for _ in range(random.randint(1, 3)):
            if not self.pending_strike_chars:
                break
            next_strike_char = self.pending_strike_chars.pop(0)
            self.active_strike_chars.append(next_strike_char)
            self.terminal.set_character_visibility(next_strike_char, is_visible=True)
            self.strike_progression_delay = 1

            # if the last strike_char was activated, activate the sparks
            # and setup the post-fade callback to indicate the strike has
            # ended
            if not self.pending_strike_chars:
                for _ in range(random.randint(12, 18)):
                    self.spark_pool.emit(
                        self.active_strike_chars[-1].motion.current_coord,
                        on_emit=self._setup_sparks_for_impact,
                        visible=True,
                        reset=tte.ParticleReset(clear_events=True),
                    )
                next_strike_char.event_handler.register_event(
                    event=tte.Event.SCENE_COMPLETE,
                    caller="fade",
                    action=tte.Action.CALLBACK,
                    target=tte.EventHandler.Callback(self.set_strike_in_progress_false),
                )

                # activate the flash scene on all strike chars and text
                for strike_char in self.active_strike_chars:
                    strike_char.animation.activate_scene("flash")
                    self.active_characters.add(strike_char)
                self.active_strike_chars.clear()

                for text_char in self.terminal.get_characters():
                    text_char.animation.activate_scene("flash")
                    self.active_characters.add(text_char)

get_effect_resources()

Get the command, effect class, and configuration class for the effect.

Returns:

Type Description
tuple[str, type[BaseEffect], type[BaseConfig]]

tuple[str, type[BaseEffect], type[BaseConfig]]: The command name, effect class, and configuration class.

Source code in terminaltexteffects/effects/effect_thunderstorm.py
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
    """Get the command, effect class, and configuration class for the effect.

    Returns:
        tuple[str, type[BaseEffect], type[BaseConfig]]: The command name, effect class, and configuration class.

    """
    return "thunderstorm", Thunderstorm, ThunderstormConfig