### ----------------------------------------------------
### -- Terminal::Print
### -- Licenses: Artistic-2.0
### -- Authors: John Haltiwanger, japhb
### -- File: examples/rpg-ui.p6
### ----------------------------------------------------
### -- Chunk 2 of 3
party-width - 1 - $.edge-border;
        my $v-break     = $.h - $log-height  - 1 - $.edge-border;

        # Let Terminal::Print know this will be a new screen
        T.add-grid('main', :new-grid($.grid));
        $.bar.add-progress(5);

        # Draw viewport borders
        my $style = $.ascii ?? 'ascii' !! 'double';
        self.draw-box(0, 0, $.w - 1, $.h - 1, :$style) if $.edge-border;
        self.draw-hline(+$.edge-border, $.w - 1 - $.edge-border, $v-break, :$style);
        self.draw-vline($h-break, +$.edge-border, $v-break - 1, :$style);

        # Draw intersections if in full Unicode mode
        unless $.ascii {
            $.grid.set-span-text(0, $v-break,        '╠') if $.edge-border;
            $.grid.set-span-text($.w - 1, $v-break,  '╣') if $.edge-border;
            $.grid.set-span-text($h-break, 0,        '╦') if $.edge-border;
            $.grid.set-span-text($h-break, $v-break, '╩');
        }
        $.bar.add-progress(5);

        # Add map, party, and log viewers, compositing them to the new UI grid
        # but not printing them yet (just updating the progress bar)
        $!mv = MapViewer.new(:x(+$.edge-border), :y(+$.edge-border),
                             :w($h-break - $.edge-border),
                             :h($v-break - $.edge-border),
                             :map($.game.map), :map-x(3), :map-y(3),
                             :full-width(!$.narrow),
                             :party($.game.party), :parent(self));
        $!mv.do-frame(Terminal::Print::FrameInfo.new);
        $.bar.add-progress(5);

        $!pv = PartyViewer.new(:x($h-break + 1), :y(+$.edge-border),
                               :w($party-width),
                               :h($v-break - 1 - $.edge-border),
                               :party($.game.party), :parent(self));
        $!pv.show-state(:!print);
        $.bar.add-progress(5);

        # Log/input
        $!lv = LogViewer.new(:x(+$.edge-border), :y($v-break + 1),
                             :w(w - 2 * $.edge-border),
                             :h($log-height), :parent(self));
        $!lv.add-entry('Game state loaded.', :!print);
        $.bar.add-progress(5);
    }
}


#
# ATTACK ANIMATIONS
#


#| Convert an rgb triplet (each in the 0..1 range) to a valid cell color
sub rgb-color(Real $r, Real $g, Real $b) {
    # Just use the 6x6x6 color cube, ignoring the hi-res gray ramp
    my $c = 16 + 36 * (5e0 * $r + .5e0).floor
               +  6 * (5e0 * $g + .5e0).floor
               +      (5e0 * $b + .5e0).floor;

    # Cell colors must be stringified
    ~$c
}


#| Convert a grayscale value (in the 0..1 range) to a valid cell color
sub gray-color(Real $gray) {
    # Use the hi-res gray ramp plus true black and white
    my $c = $gray <= .012e0 ?? 'black' !!
            $gray >= .953e0 ?? 'white' !!
                               232 + (24e0 * $gray).floor;

    # Cell colors must be stringified
    ~$c
}

#| Uniform random numbers centered on $c (or 0) and up to $n distant
multi sub myrand($n)     {      ($n * 2e0).rand - $n }
multi sub myrand($c, $n) { $c + ($n * 2e0).rand - $n }


role TempCompositing {
    has $.temp = Terminal::Print::Grid.new(self.grid.w, self.grid.h);

    my sub grid-string($grid, :$framed) {
        ('╔' ~ '═' x $grid.w ~ "╗\n║" if $framed)
        ~ $grid.grid.map(*.map({$_ // '?'}).join).join( $framed ?? "║\n║" !! "\n")
        ~ ("║\n╚" ~ '═' x $grid.w ~ '╝' if $framed)
    }

    method debug-string(|) {
        self.^name ~ ":\n"
            ~ "TEMP\n" ~ grid-string($.temp, :framed).indent(4) ~ "\n"
            ~ "GRID\n" ~ grid-string($.grid, :framed).indent(4) ~ "\n"
    }

    method refresh-background(:$from = self.target-grid, |) {
        my $fg = $!temp.grid;
        my $bg = $from.grid;
        my $x1 = $.x;
        my $x2 = $.x + $.w - 1;

        $fg[$_] = [ $bg[$.y + $_][$x1 .. $x2] ] for ^$.h;  # ++
    }

    method composite(:$to = self.target-grid, :$print, |) {
        self.refresh-background(:from($to));
        my &move := &($.grid.move-cursor);
        for ^$.h -> $y {
            my $grid-row = $.grid.grid[$y];
            my $temp-row = $.temp.grid[$y];
            for ^$.w -> $x {
                $temp-row[$x] = $grid-row[$x] if $grid-row[$x] ne ' ';
            }
            print (^$.w .map: { $temp-row[$_] ?? move($_ + $.x, $y + $.y) ~ $temp-row[$_] !! '' }).join if $print;  # )
        }
    }

    method uncomposite(:$to = self.target-grid, |) {
        self.refresh-background(:from($to));
        print &($.grid.move-cursor)($.x, $_ + $.y) ~ $.temp.grid[$_].join for ^$.h;  # ))
    }
}


class SimpleParticle is Terminal::Print::Particle {
    has $.dx;
    has $.dy;
}


class DragonBreath is Terminal::Print::ParticleEffect
 does TempCompositing {
    has $.life;
    has $.gen-frac = .4e0;                 #= fraction of life that generation occurs
    has $.fly-frac = 1e0 - self.gen-frac;  #= fraction of life that last particle lives

    method generate-particles(Num $dt) {
        my $w-scale  = $.w / ($.life * $.fly-frac);
        my $h-scale  = $.h / ($.life * $.fly-frac);

        return if $.rel.time > $.life * $.gen-frac;

        for ^($dt * 100) {
            @.particles.push: SimpleParticle.new:
                age   => 0e0,
                life  => $.life,
                color => rgb-color(1e0, 1e0, 0e0),  # Saturated yellow
                x     => myrand($.w - 1e0, .3e0),
                y     => 1e0,
                dx    => myrand(-1.1e0, .1e0) * $w-scale,
                dy    => myrand(.5e0,  .25e0) * $h-scale;
        }
    }

    method update-particles(Num $dt) {
        for @.particles {
            .x += $dt * .dx;
            .y += $dt * .dy;  # ++

            my $fade = 1e0 - .age / (.life * .8e0);
            .color = rgb-color(1e0, $fade < 0e0 ?? 0e0 !! $fade, 0e0)  # Fade to red
        }
    }
}


class Missiles is Terminal::Print::ParticleEffect
 does TempCompositing {
    has $.life;
    has $.count;

    method generate-particles(Num $dt) {
        return if @.particles;

        for ^$.count {
            @.particles.push: SimpleParticle.new:
                age   => 0e0,
                life  => $.life,
                color => rgb-color(.6e0, 0e0, 1e0),  # Blueish purple
                x     => 0e0,
                y     => $.h / 2e0,
                dx    => myrand(1e0, .15e0) * $.w / $.life,
                dy    => myrand(($_ + .5e0) / $.count - .6e0, .05e0) * 1.5e0 * $.h / $.life;
        }
    }

    method update-particles(Num $dt) {
        for @.particles {
            .x += $dt * .dx;
            .y += $dt * .dy;  # ++
        }
    }
}


class Projectile is Animation
 does TempCompositing {
    has $.life;

    method composite(:$to = self.target-grid, :$print) {
        return unless $print;

        my $x = round $.w * $.rel.time / $.life;
        my $y = $.y + $.h div 2;  # ++
        my $glyph = @.frames[$x % @.frames];

        print $to.span-string($.x, $.x + $x - 1 - $x % 2, $y) if $x >= 2;
        print ' ' if $x % 2;
        print &($.grid.move-cursor)($x + $.x, $y) ~ $glyph if $x < $.w;
    }
}


class Arrow is Projectile {
    has @.frames = < → >;
}


class Dagger is Projectile {
    has @.frames = < ↑ ↗ → ↘ ↓ ↙ ← ↖ >;
}


class SolarBeam is Terminal::Print::PixelAnimation
 does TempCompositing {
    has $.life;

    method compute-pixels() {
        my $t = $.rel.time.Num;
        return () if $t >= $.life;

        my $cy    = $.h - 1;
        my $w     = $.w.Num;
        my $left  = ($w * max(0e0, 1e0 - 5e0 * ($.life - $t))).floor;
        my $right = ($w * min(1e0, 5e0 * $t)).floor;

        my @colors;
        for $left..$right -> $x {
            my $shape = sin(.1e0 + $x / $w * π / 2e0) ** .15e0;  # Beam, narrower at left end
            my $wave  = .1e0 * sin(.5e0 * $x - 15e0 * $t);       # Wavy pulses traveling left to right
            my $beam  = $shape * (1.8e0 + .1e0.rand + $wave);    # Small variations

            for -2 .. 2 -> $dy {
                my $bright = $beam - .5e0 * $dy.abs;
                next if $bright < .5e0;

                @colors[$cy + $dy][$x] = $bright > 1e0 ?? rgb-color(1e0, 1e0, $bright - 1e0)
                                                       !! rgb-color($bright, $bright, 0e0);
            }
        }

        @colors;
    }
}


class ColdCone is Terminal::Print::PixelAnimation
 does TempCompositing {
    has $.life;

    method compute-pixels() {
        my $t = $.rel.time.Num;
        return () if $t >= $.life;

        my $cy    = $.h - 1;
        my $w     = $.w.Num;
        my $left  = ($w * max(0e0, 1e0 - 5e0 * ($.life - $t))).floor;
        my $right = ($w * min(1e0, 5e0 * $t)).floor;

        my @colors;
        for $left..$right -> $x {
            my $ramp  = sin(.1e0 + $x / $w * π / 2e0) ** .15e0;  # Subtly dimmer on left
            my $cone  = $ramp * (1.9e0 + .1e0.rand);             # Small variations
            my $width = $x div 3;

            for -$width .. $width -> $dy {
                my $hyp = ($dy * $dy + $x * $x).sqrt;
                my $cos = $x / ($hyp || 1);
                my $bright = $cone * $cos ** 6e0;
                next if $bright < .5e0;

                @colors[$cy + $dy][$x - 1]
                    = $bright > 1e0
                      ?? rgb-color($bright - 1e0, $bright - 1e0, 1e0)
                      !! rgb-color(0e0, 0e0, $bright);
            }
        }

        @colors;
    }
}


class LightningBolt is Terminal::Print::PixelAnimation
 does TempCompositing {
    has $.life;

    method compute-pixels() {
        my $t = $.rel.time.Num;
        return () if $t >= $.life;

        my $cy    = $.h - 1;
        my $dy    = 2e0.rand - 1e0;
        my $left  = ($.w * max(0e0, 1e0 - 5e0 * ($.life - $t))).floor;
        my $right = ($.w * min(1e0, 5e0 * $t)).floor;

        my @colors;
        for $left..$right -> $x {
            my $top  = ($cy + $dy).floor;
            my $dist = $dy - $dy.floor;

            @colors[$top    ][$x] = gray-color(       $dist  ** .1e0) if $dist < .5e0;
            @colors[$top + 1][$x] = gray-color((1e0 - $dist) ** .1e0) if $dist > .5e0;

            $dy = $dy * .9e0 + 1.0e0.rand - .5e0;
        }

        @colors;
    }
}


class WaveFront is Terminal::Print::PixelAnimation
 does TempCompositing {
    has $.life;

    method compute-pixels() {
        my $w     = $.w;
        my $h     = $.h * 2;
        my $cx    = $w div 2;
        my $cy    = $h div 2;
        my $r     = min($cx, $cy).Num;
        my $t     = $.rel.time.Num / $.life;
        my $rt    = max(1e0, $r * $t);

        my @colors;
        for ^$h -> $y {
            my $row = @colors[$y] //= [];
            my $dy  = ($y - $cy).Num;
            my $dy2 = $dy * $dy;

            for ^$w -> $x {
                my $dx  = ($x - $cx).Num;
                my $dx2 = $dx * $dx;
                my $d   = ($dx2 + $dy2).sqrt;
                my $rd  = $d / $rt;

                if .3e0 < $rd < 1e0 {
                    my $tint = $rd * (.9e0 + .1e0.rand);
                    my $fade = max(.5e0, min(1e0, 5e0 * (1e0 - $t)));
                    $row[$x] = rgb-color($fade, $tint * $fade, 0e0);
                }
            }
        }

        @colors;
    }
}


class Teleport is Animation
 does Terminal::Print::Pixelated
 does TempCompositing {
    has $.cx = self.w div 2;
    has $.cy = self.h div 2;
    has $.r  = min((self.w + 1) div 2, $!cy).Num;

    has @.symbols = (0x263F .. 0x2653).pick(*)».chr;

    #| Figure out what phase of a multi-phase animation is active, and how far that phase has progressed
    method phase($time, @phase-times) {
        my $t = 0e0;
        for @phase-times.kv -> $i, $phase {
            return ($i, ($time - $t) / $phase) if $time < $t + $phase;
            $t += $phase;
        }
        (+@phase-times, $time - $t)
    }

    #| Compute coordinates for hexagon corners, relative to its center
    method hexagon-coords($pct) {
        my $rt    = $!r * $pct;
        my $x     = cos(π / 6);

        my $xrt   = (1e0 + $.parent.full-width) * $x * $rt;
        my $hrt   = .5e0 * $rt;

        ( $xrt, -$hrt),
        ( 0   , -$rt ),
        (-$xrt, -$hrt),
        (-$xrt,  $hrt),
        ( 0   ,  $rt ),
        ( $xrt,  $hrt);
    }

    #| Spread the symbols into the right shape for the tessaract
    method spread-symbols($pct) {
        my @coords = self.hexagon-coords($pct);

        for @coords.kv -> $i, ($x, $y) {
            $.grid.set-span($!cx + ($x / 2).round, $!cy + ($y / 2).round, @.symbols[$i],     'green');
            $.grid.set-span($!cx + $x.round,       $!cy + $y.round,       @.symbols[$i + 6], 'green');
        }

        $.grid.set-span($!cx, $!cy, @.symbols[12], 'green');
    }

    method draw-partial-line(@pixels, $color, $pct, $x1, $y1, $x2, $y2) {
        my $dx = $x2 - $x1;
        my $dy = $y2 - $y1;

        # X-major
        if $dx.abs > $dy.abs {
            my $x = $x1;
            while (my $frac = ($x - $x1) / $dx) <= $pct {
                my $y = $y1 + $dy * $frac;
                @pixels[$y.round][$x.round] = $color;

                $x += $dx.sign;
            }
        }
        # Y-major
        elsif $dx.abs < $dy.abs {
            my $y = $y1;
            while (my $frac = ($y - $y1) / $dy) <= $pct {
                my $x = $x1 + $dx * $frac;
                @pixels[$y.round][$x.round] = $color;

                $y += $dy.sign;
            }
        }
        # Single point
        else {
            @pixels[$y1.round][$x1.round] = $color if $pct > .5e0;
        }
    }

    method form-tesseract($pct) {
        my @coords = self.hexagon-coords(1e0);
        my @pixels;

        my $cy = $!cy * 2 + .5e0;
        my $p1 = min(1e0,           $pct           * 3e0);
        my $p2 = min(1e0, max(0e0, ($pct - .333e0) * 3e0));
        my $p3 =          max(0e0, ($pct - .666e0) * 3e0);

        for 0, 2, 4 -> $i {
            self.draw-partial-line(@pixels, 'magenta', $p1, $!cx, $cy,
                                   $!cx + @coords[$i][0],
                                   $cy + (@coords[$i][1] * 2e0));

            self.draw-partial-line(@pixels, 'magenta', $p2,
                                   $!cx + @coords[$i][0],
                                   $cy + (@coords[$i][1] * 2e0),
                                   $!cx + @coords[$i + 1][0],
                                   $cy + (@coords[$i + 1][1] * 2e0));

            self.draw-partial-line(@pixels, 'magenta', $p2,
                                   $!cx + @coords[$i][0],
                                   $cy + (@coords[$i][1] * 2e0),
                                   $!cx + @coords[($i - 1) % 6][0],
                                   $cy + (@coords[($i - 1) % 6][1] * 2e0));

            self.draw-partial-line(@pixels, 'magenta', $p3,
                                   $!cx + (@coords[$i][0] * .5e0),
                                   $cy +   @coords[$i][1],
                                   $!cx + (@coords[$i + 1][0] * .5e0),
                                   $cy +   @coords[$i + 1][1]);

            self.draw-partial-line(@pixels, 'magenta', $p2,
                                   $!cx + (@coords[$i][0] * .5e0),
                                   $cy +   @coords[$i][1],
                                   $!cx + (@coords[($i - 1) % 6][0] * .5e0),
                                   $cy +   @coords[($i - 1) % 6][1]);
        }

        self.composite-pixels(@pixels);

        self.spread-symbols(1e0);
    }

    method flash($pct) {
        self.form-tesseract(1e0);
        next unless ($pct * 23).round % 2;

        my $ratio = $.parent.full-width + 1e0;
        my $rtx   = ($!r * $pct * $ratio).round;
        my $rty   = ($!r * $pct / 2e0).round;

        my $color = gray-color(.75e0 + $pct / 4e0);

        for -$rty .. $rty -> $dy {
            $.grid.set-span($!cx - $rtx, $!cy + $dy, '█' x ($rtx * 2 + 1), $color);
        }

        for 1 .. $rty -> $dy {
            my $xw = ($rtx * (1 - $dy / $rty)).ceiling;
            $.grid.set-span($!cx - $xw, $!cy + $dy + $rty, '█' x ($xw * 2 + 1), $color);
            $.grid.set-span($!cx - $xw, $!cy - $dy - $rty, '█' x ($xw * 2 + 1), $color);
        }
    }

    method draw-frame() {
        my @times = .8e0, .8e0, 1e0;
        my ($phase, $pct) = self.phase($.rel.time + .5e0, @times);

        given $phase {
            when 0  { self.spread-symbols($pct) }
            when 1  { self.form-tesseract($pct) }
            when 2  { self.flash($pct) }
            default { }
        }
    }
}



#
# DEMO EVENTS
#

#| Play out the turns of the climactic red dragon battle
sub dragon-battle(UI $ui, Game $game) {
    #| Do damage to a character and show it in the UI
    my sub do-damage($member) {
        $game.party.members[$member]<hp>--;
        $ui.pv.children[$member].injured;
    }

    #| Use up one of the character's magic points and show it in the UI
    my sub use-spell($member) {
        $game.party.members[$member]<mp>--;
        $ui.pv.show-state(:expand($member));
    }

    #| Convert a rect in party-relative map coords to absolute viewer coords
    my sub maprel-to-viewer(:$dx!, :$dy!, :$w!, :$h!) {
        my $fw = 1 + $ui.mv.full-width;
        my $x  = ($game.party.map-x - $ui.mv.map-x + $dx) * $fw;
        my $y  =  $game.party.map-y - $ui.mv.map-y + $dy;  # -

        ($x, $y, $w * $fw, $h)
    }

    #| Add an attack animation
    my sub show-attack($attack, $life, :$dx!, :$dy!, :$w! is copy, :$h! is copy, |c) {
        my $t0 = now;
        (my $x, my $y, $w, $h) = maprel-to-viewer(:$dx, :$dy, :$w, :$h);

        my $animation = $attack.new(:$x, :$y, :$w, :$h,
                                    :$life, :parent($ui.mv), |c);

        my $start = now;
        repeat {
            $animation.do-frame(Terminal::Print::FrameInfo.new);
            $animation.composite(:print);
        } while now - $start < $life;

        $animation.uncomposite;
        $ui.mv.remove-child($animation);
        record-time("Render $animation.w() x $animation.h() {$animation.^name}", $t0);
    }

    #| Add the dragon to the screen
    my sub add-dragon(:$dx, :$dy) {
        my $dragon-wide = q:to/DRAGON/;
                 __     
            <,  /\      
             `=<###>.   
               ]   ] `~+
            DRAGON

        my $dragon-narrow = q:to/DRAGON/;
                _   
            <, /\   
             `<##>. 
              ]  ]`+
            DRAGON

        # /
        my $dragon = $ui.mv.full-width ?? $dragon-wide !! $dragon-narrow;
        my $grid   = make-text-grid($dragon);
        $grid.set-span-color(0, $grid.w - 1, $_, 'red') for ^$grid.h;

        my $fw   = 1 + $ui.mv.full-width;
        my ($x, $y, $w, $h)
                 = maprel-to-viewer(:$dx, :$dy, :w($grid.w / $fw), :h($grid.h));

        Widget.new-from-grid($grid, :$x, :$y, :parent($ui.mv));
    }

    #| Refresh MapViewer
    my sub refresh-mv() {
        $ui.mv.do-frame(Terminal::Print::FrameInfo.new);
        $ui.mv.composite(:print);
    }

    # Add dragon and make sure the battle area is completely visible
    $ui.lv.add-entry("The party encounters a red dragon.");
    $ui.mv.ensure-visible(:x($game.party.map-x - 1), :y($game.party.map-y - 2),  # ,
                          :w(11), :h(5));
    my $dragon = add-dragon(:dx(+4), :dy(-2));
    refresh-mv();

    # Dragon turn #1
    $ui.lv.add-entry("The dragon is enraged by Torfin's dragon hide armor and immediately attacks.");
    $ui.lv.add-entry("The dragon breathes a great blast of fire!");
    show-attack(DragonBreath, 1e0, :dx(-1), :dy(-2), :w(5), :h(5));
    $ui.lv.add-entry("--> Fennic performs a diving roll and dodges the fire blast.");
    $ui.lv.add-entry("--> Galtar is partially shielded but still takes minor damage.");
    await do-damage(1);
    $ui.lv.add-entry("--> Salnax melts into the dancing shadows, avoiding the brunt of the blast.");
    $ui.lv.add-entry("--> Torfin's dragon hide armor shrugs off the fire.");
    $ui.lv.add-entry("--> Trentis hid behind Torfin and is untouched.");

    # Party turn #1
    $ui.pv.show-state(:expand(0));
    $ui.lv.user-input('[Fennic]>', 'fire bow');
    $ui.lv.add-entry("--> Fennic fires a glowing arrow from the longbow and pierces the dragon's hide.");
    show-attack(Arrow, .5e0, :dx(+1), :dy(0), :w(5), :h(1));

    $ui.pv.show-state(:expand(1));
    $ui.lv.user-input('[Galtar]>', 'cast solar blast');
    $ui.lv.add-entry("--> Galtar calls upon the power of the sun and bathes the dragon in searing golden light.");
    show-attack(SolarBeam, 1e0, :dx(+1), :dy(-2), :w(7), :h(5));
    use-spell(1);
    $ui.lv.add-entry("--> The dragon is blinded!");

    $ui.pv.show-state(:expand(2));
    $ui.lv.user-input('[Salnax]>', 'trigger ice cone');
    $ui.lv.add-entry("--> Salnax calls a cone of ice from the staff.");
    show-attack(ColdCone, 1e0, :dx(+1), :dy(-2), :w(7), :h(5));
    $ui.lv.add-entry("--> The dragon is encased in ice!");

    $ui.pv.show-state(:expand(3));
    $ui.lv.user-input('[Torfin]>', 'swing sword');
    $ui.lv.add-entry("--> Torfin swings the fearsome sword, biting deep into the dragon's flesh.");
    $ui.lv.add-entry("--> The dragon howls in pain!");

    $ui.pv.show-state(:expand(4));
    $ui.lv.user-input('[Trentis]>', 'throw dagger');
    $ui.lv.add-entry("--> Trentis throws a dagger towards the dragon's underbelly but misses.");
    show-attack(Dagger, .75e0, :dx(+1), :dy(0), :w(5), :h(1));

    # Dragon turn #2
    $ui.pv.show-state;
    $ui.lv.add-entry("The dragon smashes through its icy shell.");
    $ui.lv.add-entry("The dragon blindly swings its mighty tail.");
    $ui.lv.add-entry("--> Galtar and Torfin are painfully knocked down!");
    await do-damage(1), do-damage(3);

    # Party turn #2
    $ui.pv.show-state(:expand(0));
    $ui.lv.user-input('[Fennic]>', 'fire bow');
    $ui.lv.add-entry("--> Fennic fires the longbow again, embedding a second arrow in the dragon's neck.");
    show-attack(Arrow, .5e0, :dx(+1), :dy(0), :w(5), :h(1));

    $ui.pv.show-state(:expand(1));
    $ui.lv.user-input('[Galtar]>', 'swing mace');
    $ui.lv.add-entry("--> Galtar swings the mace in a perfect arc, slamming it into the dragon's left foreleg with a resounding crunch.");
    $ui.lv.add-entry("--> The dragon staggers from the blow!");

    $ui.pv.show-state(:expand(2));
    $ui.lv.user-input('[Salnax]>', 'cast lightning bolt');
    $ui.lv.add-entry("--> Salnax ionizes the air with a white-hot bolt of electricity.");
    show-attack(LightningBolt, 1e0, :dx(+1), :dy(-2), :w(7), :h(5));
    use-spell(2);
    $ui.lv.add-entry("--> The dragon shudders as electric arcs course through it.");

    $ui.pv.show-state(:expand(3));
    $ui.lv.user-input('[Torfin]>', 'rise');
    $ui.lv.add-entry("--> Torfin staggers upright, ready to fight again.");

    $ui.pv.show-state(:expand(4));
    $ui.lv.user-input('[Trentis]>', 'throw dagger');
    $ui.lv.add-entry("--> Trentis throws a dagger and impales the dragon's throat.");
    show-attack(Dagger, .5e0, :dx(+1), :dy(0), :w(5), :h(1));

    # Dragon turn #3
    $ui.pv.show-state;
    $ui.lv.add-entry("The dragon blindly casts explosive fireball.");
    $ui.lv.add-entry("--> The fiery blast knocks everyone back, singeing cloth and heating metal.");
    show-attack(WaveFront, .5e0, :dx(-1), :dy(-2), :w(6), :h(5));
    await (^5).map: *.&do-damage;

    # Party turn #3
    $ui.pv.show-state(:expand(0));
    $ui.lv.user-input('[Fennic]>', 'fire bow');
    $ui.lv.add-entry("--> Fennic fires a third arrow into the dragon.");
    show-attack(Arrow, .5e0, :dx(+1), :dy(0), :w(5), :h(1));

    $ui.pv.show-state(:expand(1));
    $ui.lv.user-input('[Galtar]>', 'swing mace');
    $ui.lv.add-entry("--> Galtar swings the mace and lands a solid blow to the dragon's right foreleg.");
    $ui.lv.add-entry("--> The dragon remains staggered.");

    $ui.pv.show-state(:expand(2));
    $ui.lv.user-input('[Salnax]>', 'cast magic missile');
    $ui.lv.add-entry("--> Salnax launches a quintet of octarine missiles, scattering them across the dragon's massive frame.");
    show-attack(Missiles, 1e0, :dx(+1), :dy(-2), :w(7), :h(5), :count(5));
    use-spell(2);
    $ui.lv.add-entry("--> The dragon howls with growing rage!");

    $ui.pv.show-state(:expand(3));
    $ui.lv.user-input('[Torfin]>', 'sword charge');
    $ui.lv.add-entry("--> Torfin charges the dragon at full speed, focusing battle rage into a massive swing.");
    $ui.lv.add-entry("--> The dragon is critically wounded!");

    $ui.pv.show-state(:expand(4));
    $ui.lv.user-input('[Trentis]>', 'mount dragon');
    $ui.lv.add-entry("--> Trentis leaps acrobatically onto the dragon's back, scrambling for purchase on the thick scales.");

    # Dragon turn #4
    $ui.pv.show-state;
    $ui.lv.add-entry("The dragon's vision clears.");
    $ui.lv.add-entry("Beaten and bleeding and realizing all party members are still fighting, the dragon decides to flee.  Shimmering symbols appear in the air around it and reality twists as the dragon teleports to safety.");
    $ui.mv.remove-child($dragon);
    refresh-mv();
    show-attack(Teleport, 2.1e0, :dx(+3), :dy(-3), :w(7), :h(7));
    $ui.lv.add-entry("--> Trentis falls to the floor with a thud.");
    await do-damage(4);
}


#
# MAIN PROGRAM
#

#| Simulate a CRPG or Roguelike interface
sub MAIN(
    Bool :$ascii, #= Use only ASCII characters, no >127 codepoints
    Bool :$bench, #= Benchmark mode (run as fast as possible, with no sleeps or rate limiting)
    Int  :$color-bits = 8, #= Set color support (4 = 16-color, 8 = 256-color, 24 = 24-bit RGB)
    Bool :$narrow, #= Use narrow map