# Copyright (c) 2011-2020 Eric Froemling
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Various classes for bombs, mines, tnt, etc."""

# FIXME
# pylint: disable=too-many-lines

from __future__ import annotations

import random
from typing import TYPE_CHECKING

import ba

if TYPE_CHECKING:
    from typing import Any, Sequence, Optional, Callable, List, Tuple


class BombFactory:
    """Wraps up media and other resources used by ba.Bombs.

    category: Gameplay Classes

    A single instance of this is shared between all bombs
    and can be retrieved via bastd.actor.bomb.get_factory().

    Attributes:

       bomb_model
          The ba.Model of a standard or ice bomb.

       sticky_bomb_model
          The ba.Model of a sticky-bomb.

       impact_bomb_model
          The ba.Model of an impact-bomb.

       land_mine_model
          The ba.Model of a land-mine.

       tnt_model
          The ba.Model of a tnt box.

       regular_tex
          The ba.Texture for regular bombs.

       ice_tex
          The ba.Texture for ice bombs.

       sticky_tex
          The ba.Texture for sticky bombs.

       impact_tex
          The ba.Texture for impact bombs.

       impact_lit_tex
          The ba.Texture for impact bombs with lights lit.

       land_mine_tex
          The ba.Texture for land-mines.

       land_mine_lit_tex
          The ba.Texture for land-mines with the light lit.

       tnt_tex
          The ba.Texture for tnt boxes.

       hiss_sound
          The ba.Sound for the hiss sound an ice bomb makes.

       debris_fall_sound
          The ba.Sound for random falling debris after an explosion.

       wood_debris_fall_sound
          A ba.Sound for random wood debris falling after an explosion.

       explode_sounds
          A tuple of ba.Sounds for explosions.

       freeze_sound
          A ba.Sound of an ice bomb freezing something.

       fuse_sound
          A ba.Sound of a burning fuse.

       activate_sound
          A ba.Sound for an activating impact bomb.

       warn_sound
          A ba.Sound for an impact bomb about to explode due to time-out.

       bomb_material
          A ba.Material applied to all bombs.

       normal_sound_material
          A ba.Material that generates standard bomb noises on impacts, etc.

       sticky_material
          A ba.Material that makes 'splat' sounds and makes collisions softer.

       land_mine_no_explode_material
          A ba.Material that keeps land-mines from blowing up.
          Applied to land-mines when they are created to allow land-mines to
          touch without exploding.

       land_mine_blast_material
          A ba.Material applied to activated land-mines that causes them to
          explode on impact.

       impact_blast_material
          A ba.Material applied to activated impact-bombs that causes them to
          explode on impact.

       blast_material
          A ba.Material applied to bomb blast geometry which triggers impact
          events with what it touches.

       dink_sounds
          A tuple of ba.Sounds for when bombs hit the ground.

       sticky_impact_sound
          The ba.Sound for a squish made by a sticky bomb hitting something.

       roll_sound
          ba.Sound for a rolling bomb.
    """

    def random_explode_sound(self) -> ba.Sound:
        """Return a random explosion ba.Sound from the factory."""
        return self.explode_sounds[random.randrange(len(self.explode_sounds))]

    def __init__(self) -> None:
        """Instantiate a BombFactory.

        You shouldn't need to do this; call bastd.actor.bomb.get_factory()
        to get a shared instance.
        """

        self.bomb_model = ba.getmodel('bomb')
        self.sticky_bomb_model = ba.getmodel('bombSticky')
        self.impact_bomb_model = ba.getmodel('impactBomb')
        self.land_mine_model = ba.getmodel('landMine')
        self.tnt_model = ba.getmodel('tnt')

        self.regular_tex = ba.gettexture('bombColor')
        self.ice_tex = ba.gettexture('bombColorIce')
        self.sticky_tex = ba.gettexture('bombStickyColor')
        self.impact_tex = ba.gettexture('impactBombColor')
        self.impact_lit_tex = ba.gettexture('impactBombColorLit')
        self.land_mine_tex = ba.gettexture('landMine')
        self.land_mine_lit_tex = ba.gettexture('landMineLit')
        self.tnt_tex = ba.gettexture('tnt')

        self.hiss_sound = ba.getsound('hiss')
        self.debris_fall_sound = ba.getsound('debrisFall')
        self.wood_debris_fall_sound = ba.getsound('woodDebrisFall')

        self.explode_sounds = (ba.getsound('explosion01'),
                               ba.getsound('explosion02'),
                               ba.getsound('explosion03'),
                               ba.getsound('explosion04'),
                               ba.getsound('explosion05'))

        self.freeze_sound = ba.getsound('freeze')
        self.fuse_sound = ba.getsound('fuse01')
        self.activate_sound = ba.getsound('activateBeep')
        self.warn_sound = ba.getsound('warnBeep')

        # set up our material so new bombs don't collide with objects
        # that they are initially overlapping
        self.bomb_material = ba.Material()
        self.normal_sound_material = ba.Material()
        self.sticky_material = ba.Material()

        self.bomb_material.add_actions(
            conditions=((('we_are_younger_than', 100), 'or',
                         ('they_are_younger_than', 100)),
                        'and', ('they_have_material',
                                ba.sharedobj('object_material'))),
            actions=('modify_node_collision', 'collide', False))

        # we want pickup materials to always hit us even if we're currently not
        # colliding with their node (generally due to the above rule)
        self.bomb_material.add_actions(
            conditions=('they_have_material', ba.sharedobj('pickup_material')),
            actions=('modify_part_collision', 'use_node_collide', False))

        self.bomb_material.add_actions(actions=('modify_part_collision',
                                                'friction', 0.3))

        self.land_mine_no_explode_material = ba.Material()
        self.land_mine_blast_material = ba.Material()
        self.land_mine_blast_material.add_actions(
            conditions=(('we_are_older_than',
                         200), 'and', ('they_are_older_than',
                                       200), 'and', ('eval_colliding', ),
                        'and', (('they_dont_have_material',
                                 self.land_mine_no_explode_material), 'and',
                                (('they_have_material',
                                  ba.sharedobj('object_material')), 'or',
                                 ('they_have_material',
                                  ba.sharedobj('player_material'))))),
            actions=('message', 'our_node', 'at_connect', ImpactMessage()))

        self.impact_blast_material = ba.Material()
        self.impact_blast_material.add_actions(
            conditions=(('we_are_older_than',
                         200), 'and', ('they_are_older_than',
                                       200), 'and', ('eval_colliding', ),
                        'and', (('they_have_material',
                                 ba.sharedobj('footing_material')), 'or',
                                ('they_have_material',
                                 ba.sharedobj('object_material')))),
            actions=('message', 'our_node', 'at_connect', ImpactMessage()))

        self.blast_material = ba.Material()
        self.blast_material.add_actions(
            conditions=(('they_have_material',
                         ba.sharedobj('object_material'))),
            actions=(('modify_part_collision', 'collide', True),
                     ('modify_part_collision', 'physical',
                      False), ('message', 'our_node', 'at_connect',
                               ExplodeHitMessage())))

        self.dink_sounds = (ba.getsound('bombDrop01'),
                            ba.getsound('bombDrop02'))
        self.sticky_impact_sound = ba.getsound('stickyImpact')
        self.roll_sound = ba.getsound('bombRoll01')

        # collision sounds
        self.normal_sound_material.add_actions(
            conditions=('they_have_material',
                        ba.sharedobj('footing_material')),
            actions=(('impact_sound', self.dink_sounds, 2, 0.8),
                     ('roll_sound', self.roll_sound, 3, 6)))

        self.sticky_material.add_actions(actions=(('modify_part_collision',
                                                   'stiffness', 0.1),
                                                  ('modify_part_collision',
                                                   'damping', 1.0)))

        self.sticky_material.add_actions(
            conditions=(('they_have_material',
                         ba.sharedobj('player_material')),
                        'or', ('they_have_material',
                               ba.sharedobj('footing_material'))),
            actions=('message', 'our_node', 'at_connect', SplatMessage()))


# noinspection PyTypeHints
def get_factory() -> BombFactory:
    """Get/create a shared bastd.actor.bomb.BombFactory object."""
    activity = ba.getactivity()

    # FIXME: Need to figure out an elegant way to store
    #  shared actor data with an activity.
    factory: BombFactory
    try:
        factory = activity.shared_bomb_factory  # type: ignore
    except Exception:
        factory = activity.shared_bomb_factory = BombFactory()  # type: ignore
    assert isinstance(factory, BombFactory)
    return factory


class SplatMessage:
    """Tells an object to make a splat noise."""


class ExplodeMessage:
    """Tells an object to explode."""


class ImpactMessage:
    """Tell an object it touched something."""


class ArmMessage:
    """Tell an object to become armed."""


class WarnMessage:
    """Tell an object to issue a warning sound."""


class ExplodeHitMessage:
    """Tell an object it was hit by an explosion."""

    def __init__(self) -> None:
        pass


class Blast(ba.Actor):
    """An explosion, as generated by a bomb or some other object.

    category: Gameplay Classes
    """

    def __init__(self,
                 position: Sequence[float] = (0.0, 1.0, 0.0),
                 velocity: Sequence[float] = (0.0, 0.0, 0.0),
                 blast_radius: float = 2.0,
                 blast_type: str = "normal",
                 source_player: ba.Player = None,
                 hit_type: str = 'explosion',
                 hit_subtype: str = 'normal'):
        """Instantiate with given values."""

        # bah; get off my lawn!
        # pylint: disable=too-many-locals
        # pylint: disable=too-many-statements

        super().__init__()

        factory = get_factory()

        self.blast_type = blast_type
        self.source_player = source_player
        self.hit_type = hit_type
        self.hit_subtype = hit_subtype
        self.radius = blast_radius

        # set our position a bit lower so we throw more things upward
        rmats = (factory.blast_material, ba.sharedobj('attack_material'))
        self.node = ba.newnode('region',
                               delegate=self,
                               attrs={
                                   'position': (position[0], position[1] - 0.1,
                                                position[2]),
                                   'scale':
                                       (self.radius, self.radius, self.radius),
                                   'type': 'sphere',
                                   'materials': rmats
                               })

        ba.timer(0.05, self.node.delete)

        # throw in an explosion and flash
        evel = (velocity[0], max(-1.0, velocity[1]), velocity[2])
        explosion = ba.newnode("explosion",
                               attrs={
                                   'position': position,
                                   'velocity': evel,
                                   'radius': self.radius,
                                   'big': (self.blast_type == 'tnt')
                               })
        if self.blast_type == "ice":
            explosion.color = (0, 0.05, 0.4)

        ba.timer(1.0, explosion.delete)

        if self.blast_type != 'ice':
            ba.emitfx(position=position,
                      velocity=velocity,
                      count=int(1.0 + random.random() * 4),
                      emit_type='tendrils',
                      tendril_type='thin_smoke')
        ba.emitfx(position=position,
                  velocity=velocity,
                  count=int(4.0 + random.random() * 4),
                  emit_type='tendrils',
                  tendril_type='ice' if self.blast_type == 'ice' else 'smoke')
        ba.emitfx(position=position,
                  emit_type='distortion',
                  spread=1.0 if self.blast_type == 'tnt' else 2.0)

        # and emit some shrapnel..
        if self.blast_type == 'ice':

            def emit() -> None:
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=30,
                          spread=2.0,
                          scale=0.4,
                          chunk_type='ice',
                          emit_type='stickers')

            # looks better if we delay a bit
            ba.timer(0.05, emit)

        elif self.blast_type == 'sticky':

            def emit() -> None:
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=int(4.0 + random.random() * 8),
                          spread=0.7,
                          chunk_type='slime')
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=int(4.0 + random.random() * 8),
                          scale=0.5,
                          spread=0.7,
                          chunk_type='slime')
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=15,
                          scale=0.6,
                          chunk_type='slime',
                          emit_type='stickers')
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=20,
                          scale=0.7,
                          chunk_type='spark',
                          emit_type='stickers')
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=int(6.0 + random.random() * 12),
                          scale=0.8,
                          spread=1.5,
                          chunk_type='spark')

            # looks better if we delay a bit
            ba.timer(0.05, emit)

        elif self.blast_type == 'impact':  # regular bomb shrapnel

            def emit() -> None:
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=int(4.0 + random.random() * 8),
                          scale=0.8,
                          chunk_type='metal')
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=int(4.0 + random.random() * 8),
                          scale=0.4,
                          chunk_type='metal')
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=20,
                          scale=0.7,
                          chunk_type='spark',
                          emit_type='stickers')
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=int(8.0 + random.random() * 15),
                          scale=0.8,
                          spread=1.5,
                          chunk_type='spark')

            # looks better if we delay a bit
            ba.timer(0.05, emit)

        else:  # regular or land mine bomb shrapnel

            def emit() -> None:
                if self.blast_type != 'tnt':
                    ba.emitfx(position=position,
                              velocity=velocity,
                              count=int(4.0 + random.random() * 8),
                              chunk_type='rock')
                    ba.emitfx(position=position,
                              velocity=velocity,
                              count=int(4.0 + random.random() * 8),
                              scale=0.5,
                              chunk_type='rock')
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=30,
                          scale=1.0 if self.blast_type == 'tnt' else 0.7,
                          chunk_type='spark',
                          emit_type='stickers')
                ba.emitfx(position=position,
                          velocity=velocity,
                          count=int(18.0 + random.random() * 20),
                          scale=1.0 if self.blast_type == 'tnt' else 0.8,
                          spread=1.5,
                          chunk_type='spark')

                # tnt throws splintery chunks
                if self.blast_type == 'tnt':

                    def emit_splinters() -> None:
                        ba.emitfx(position=position,
                                  velocity=velocity,
                                  count=int(20.0 + random.random() * 25),
                                  scale=0.8,
                                  spread=1.0,
                                  chunk_type='splinter')

                    ba.timer(0.01, emit_splinters)

                # every now and then do a sparky one
                if self.blast_type == 'tnt' or random.random() < 0.1:

                    def emit_extra_sparks() -> None:
                        ba.emitfx(position=position,
                                  velocity=velocity,
                                  count=int(10.0 + random.random() * 20),
                                  scale=0.8,
                                  spread=1.5,
                                  chunk_type='spark')

                    ba.timer(0.02, emit_extra_sparks)

            # looks better if we delay a bit
            ba.timer(0.05, emit)

        lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else
                  (1, 0.3, 0.1))
        light = ba.newnode('light',
                           attrs={
                               'position': position,
                               'volume_intensity_scale': 10.0,
                               'color': lcolor
                           })

        scl = random.uniform(0.6, 0.9)
        scorch_radius = light_radius = self.radius
        if self.blast_type == 'tnt':
            light_radius *= 1.4
            scorch_radius *= 1.15
            scl *= 3.0

        iscale = 1.6
        ba.animate(
            light, "intensity", {
                0: 2.0 * iscale,
                scl * 0.02: 0.1 * iscale,
                scl * 0.025: 0.2 * iscale,
                scl * 0.05: 17.0 * iscale,
                scl * 0.06: 5.0 * iscale,
                scl * 0.08: 4.0 * iscale,
                scl * 0.2: 0.6 * iscale,
                scl * 2.0: 0.00 * iscale,
                scl * 3.0: 0.0
            })
        ba.animate(
            light, "radius", {
                0: light_radius * 0.2,
                scl * 0.05: light_radius * 0.55,
                scl * 0.1: light_radius * 0.3,
                scl * 0.3: light_radius * 0.15,
                scl * 1.0: light_radius * 0.05
            })
        ba.timer(scl * 3.0, light.delete)

        # make a scorch that fades over time
        scorch = ba.newnode('scorch',
                            attrs={
                                'position': position,
                                'size': scorch_radius * 0.5,
                                'big': (self.blast_type == 'tnt')
                            })
        if self.blast_type == 'ice':
            scorch.color = (1, 1, 1.5)

        ba.animate(scorch, "presence", {3.000: 1, 13.000: 0})
        ba.timer(13.0, scorch.delete)

        if self.blast_type == 'ice':
            ba.playsound(factory.hiss_sound, position=light.position)

        lpos = light.position
        ba.playsound(factory.random_explode_sound(), position=lpos)
        ba.playsound(factory.debris_fall_sound, position=lpos)

        ba.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0)

        # tnt is more epic..
        if self.blast_type == 'tnt':
            ba.playsound(factory.random_explode_sound(), position=lpos)

            def _extra_boom() -> None:
                ba.playsound(factory.random_explode_sound(), position=lpos)

            ba.timer(0.25, _extra_boom)

            def _extra_debris_sound() -> None:
                ba.playsound(factory.debris_fall_sound, position=lpos)
                ba.playsound(factory.wood_debris_fall_sound, position=lpos)

            ba.timer(0.4, _extra_debris_sound)

    def handlemessage(self, msg: Any) -> Any:
        if __debug__:
            self._handlemessage_sanity_check()

        if isinstance(msg, ba.DieMessage):
            if self.node:
                self.node.delete()

        elif isinstance(msg, ExplodeHitMessage):
            node = ba.get_collision_info("opposing_node")
            if node:
                assert self.node
                nodepos = self.node.position

                # new
                mag = 2000.0
                if self.blast_type == 'ice':
                    mag *= 0.5
                elif self.blast_type == 'land_mine':
                    mag *= 2.5
                elif self.blast_type == 'tnt':
                    mag *= 2.0

                node.handlemessage(
                    ba.HitMessage(pos=nodepos,
                                  velocity=(0, 0, 0),
                                  magnitude=mag,
                                  hit_type=self.hit_type,
                                  hit_subtype=self.hit_subtype,
                                  radius=self.radius,
                                  source_player=self.source_player))
                if self.blast_type == "ice":
                    ba.playsound(get_factory().freeze_sound,
                                 10,
                                 position=nodepos)
                    node.handlemessage(ba.FreezeMessage())

        else:
            super().handlemessage(msg)


class Bomb(ba.Actor):
    """A standard bomb and its variants such as land-mines and tnt-boxes.

    category: Gameplay Classes
    """

    # Ew; should try to clean this up later
    # pylint: disable=too-many-locals
    # pylint: disable=too-many-branches
    # pylint: disable=too-many-statements

    def __init__(self,
                 position: Sequence[float] = (0.0, 1.0, 0.0),
                 velocity: Sequence[float] = (0.0, 0.0, 0.0),
                 bomb_type: str = 'normal',
                 blast_radius: float = 2.0,
                 source_player: ba.Player = None,
                 owner: ba.Node = None):
        """Create a new Bomb.

        bomb_type can be 'ice','impact','land_mine','normal','sticky', or
        'tnt'. Note that for impact or land_mine bombs you have to call arm()
        before they will go off.
        """
        super().__init__()

        factory = get_factory()

        if bomb_type not in ('ice', 'impact', 'land_mine', 'normal', 'sticky',
                             'tnt'):
            raise Exception("invalid bomb type: " + bomb_type)
        self.bomb_type = bomb_type

        self._exploded = False

        self.texture_sequence: Optional[ba.Node] = None

        if self.bomb_type == 'sticky':
            self._last_sticky_sound_time = 0.0

        self.blast_radius = blast_radius
        if self.bomb_type == 'ice':
            self.blast_radius *= 1.2
        elif self.bomb_type == 'impact':
            self.blast_radius *= 0.7
        elif self.bomb_type == 'land_mine':
            self.blast_radius *= 0.7
        elif self.bomb_type == 'tnt':
            self.blast_radius *= 1.45

        self._explode_callbacks: List[Callable[[Bomb, Blast], Any]] = []

        # the player this came from
        self.source_player = source_player

        # by default our hit type/subtype is our own, but we pick up types of
        # whoever sets us off so we know what caused a chain reaction
        self.hit_type = 'explosion'
        self.hit_subtype = self.bomb_type

        # if no owner was provided, use an unconnected node ref
        # (nevermind; trying to use None in these type cases instead)
        # if owner is None:
        #     owner = ba.Node(None)

        # the node this came from
        self.owner = owner

        # adding footing-materials to things can screw up jumping and flying
        # since players carrying those things
        # and thus touching footing objects will think they're on solid
        # ground.. perhaps we don't wanna add this even in the tnt case?..
        materials: Tuple[ba.Material, ...]
        if self.bomb_type == 'tnt':
            materials = (factory.bomb_material,
                         ba.sharedobj('footing_material'),
                         ba.sharedobj('object_material'))
        else:
            materials = (factory.bomb_material,
                         ba.sharedobj('object_material'))

        if self.bomb_type == 'impact':
            materials = materials + (factory.impact_blast_material, )
        elif self.bomb_type == 'land_mine':
            materials = materials + (factory.land_mine_no_explode_material, )

        if self.bomb_type == 'sticky':
            materials = materials + (factory.sticky_material, )
        else:
            materials = materials + (factory.normal_sound_material, )

        if self.bomb_type == 'land_mine':
            fuse_time = None
            self.node = ba.newnode('prop',
                                   delegate=self,
                                   attrs={
                                       'position': position,
                                       'velocity': velocity,
                                       'model': factory.land_mine_model,
                                       'light_model': factory.land_mine_model,
                                       'body': 'landMine',
                                       'shadow_size': 0.44,
                                       'color_texture': factory.land_mine_tex,
                                       'reflection': 'powerup',
                                       'reflection_scale': [1.0],
                                       'materials': materials
                                   })

        elif self.bomb_type == 'tnt':
            fuse_time = None
            self.node = ba.newnode('prop',
                                   delegate=self,
                                   attrs={
                                       'position': position,
                                       'velocity': velocity,
                                       'model': factory.tnt_model,
                                       'light_model': factory.tnt_model,
                                       'body': 'crate',
                                       'shadow_size': 0.5,
                                       'color_texture': factory.tnt_tex,
                                       'reflection': 'soft',
                                       'reflection_scale': [0.23],
                                       'materials': materials
                                   })

        elif self.bomb_type == 'impact':
            fuse_time = 20.0
            self.node = ba.newnode('prop',
                                   delegate=self,
                                   attrs={
                                       'position': position,
                                       'velocity': velocity,
                                       'body': 'sphere',
                                       'model': factory.impact_bomb_model,
                                       'shadow_size': 0.3,
                                       'color_texture': factory.impact_tex,
                                       'reflection': 'powerup',
                                       'reflection_scale': [1.5],
                                       'materials': materials
                                   })
            self.arm_timer = ba.Timer(
                0.2, ba.WeakCall(self.handlemessage, ArmMessage()))
            self.warn_timer = ba.Timer(
                fuse_time - 1.7, ba.WeakCall(self.handlemessage,
                                             WarnMessage()))

        else:
            fuse_time = 3.0
            if self.bomb_type == 'sticky':
                sticky = True
                model = factory.sticky_bomb_model
                rtype = 'sharper'
                rscale = 1.8
            else:
                sticky = False
                model = factory.bomb_model
                rtype = 'sharper'
                rscale = 1.8
            if self.bomb_type == 'ice':
                tex = factory.ice_tex
            elif self.bomb_type == 'sticky':
                tex = factory.sticky_tex
            else:
                tex = factory.regular_tex
            self.node = ba.newnode('bomb',
                                   delegate=self,
                                   attrs={
                                       'position': position,
                                       'velocity': velocity,
                                       'model': model,
                                       'shadow_size': 0.3,
                                       'color_texture': tex,
                                       'sticky': sticky,
                                       'owner': owner,
                                       'reflection': rtype,
                                       'reflection_scale': [rscale],
                                       'materials': materials
                                   })

            sound = ba.newnode('sound',
                               owner=self.node,
                               attrs={
                                   'sound': factory.fuse_sound,
                                   'volume': 0.25
                               })
            self.node.connectattr('position', sound, 'position')
            ba.animate(self.node, 'fuse_length', {0.0: 1.0, fuse_time: 0.0})

        # Light the fuse!!!
        if self.bomb_type not in ('land_mine', 'tnt'):
            assert fuse_time is not None
            ba.timer(fuse_time,
                     ba.WeakCall(self.handlemessage, ExplodeMessage()))

        ba.animate(self.node, "model_scale", {0: 0, 0.2: 1.3, 0.26: 1})

    def get_source_player(self) -> Optional[ba.Player]:
        """Returns a ba.Player representing the source of this bomb.

        Be prepared for values of None or invalid Player refs."""
        return self.source_player

    def on_expire(self) -> None:
        super().on_expire()
        # release callbacks/refs so we don't wind up with dependency loops..
        self._explode_callbacks = []

    def _handle_die(self) -> None:
        if self.node:
            self.node.delete()

    def _handle_oob(self) -> None:
        self.handlemessage(ba.DieMessage())

    def _handle_impact(self) -> None:
        node = ba.get_collision_info("opposing_node")
        # if we're an impact bomb and we came from this node, don't explode...
        # alternately if we're hitting another impact-bomb from the same
        # source, don't explode...
        try:
            node_delegate = node.getdelegate()
        except Exception:
            node_delegate = None
        if node:
            if (self.bomb_type == 'impact'
                    and (node is self.owner or
                         (isinstance(node_delegate, Bomb)
                          and node_delegate.bomb_type == 'impact'
                          and node_delegate.owner is self.owner))):
                return
            self.handlemessage(ExplodeMessage())

    def _handle_dropped(self) -> None:
        if self.bomb_type == 'land_mine':
            self.arm_timer = ba.Timer(
                1.25, ba.WeakCall(self.handlemessage, ArmMessage()))

        # once we've thrown a sticky bomb we can stick to it..
        elif self.bomb_type == 'sticky':

            def _safesetattr(node: Optional[ba.Node], attr: str,
                             value: Any) -> None:
                if node:
                    setattr(node, attr, value)

            ba.timer(0.25,
                     lambda: _safesetattr(self.node, 'stick_to_owner', True))

    def _handle_splat(self) -> None:
        node = ba.get_collision_info("opposing_node")
        if (node is not self.owner
                and ba.time() - self._last_sticky_sound_time > 1.0):
            self._last_sticky_sound_time = ba.time()
            assert self.node
            ba.playsound(get_factory().sticky_impact_sound,
                         2.0,
                         position=self.node.position)

    def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None:
        """Add a call to be run when the bomb has exploded.

        The bomb and the new blast object are passed as arguments.
        """
        self._explode_callbacks.append(call)

    def explode(self) -> None:
        """Blows up the bomb if it has not yet done so."""
        if self._exploded:
            return
        self._exploded = True
        activity = self.getactivity()
        if activity is not None and self.node:
            blast = Blast(position=self.node.position,
                          velocity=self.node.velocity,
                          blast_radius=self.blast_radius,
                          blast_type=self.bomb_type,
                          source_player=self.source_player,
                          hit_type=self.hit_type,
                          hit_subtype=self.hit_subtype).autoretain()
            for callback in self._explode_callbacks:
                callback(self, blast)

        # we blew up so we need to go away
        # FIXME; was there a reason we need this delay?
        ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage()))

    def _handle_warn(self) -> None:
        if self.texture_sequence and self.node:
            self.texture_sequence.rate = 30
            ba.playsound(get_factory().warn_sound,
                         0.5,
                         position=self.node.position)

    def _add_material(self, material: ba.Material) -> None:
        if not self.node:
            return
        materials = self.node.materials
        if material not in materials:
            assert isinstance(materials, tuple)
            self.node.materials = materials + (material, )

    def arm(self) -> None:
        """Arm the bomb (for land-mines and impact-bombs).

        These types of bombs will not explode until they have been armed.
        """
        if not self.node:
            return
        factory = get_factory()
        intex: Sequence[ba.Texture]
        if self.bomb_type == 'land_mine':
            intex = (factory.land_mine_lit_tex, factory.land_mine_tex)
            self.texture_sequence = ba.newnode('texture_sequence',
                                               owner=self.node,
                                               attrs={
                                                   'rate': 30,
                                                   'input_textures': intex
                                               })
            ba.timer(0.5, self.texture_sequence.delete)
            # We now make it explodable.
            ba.timer(
                0.25,
                ba.WeakCall(self._add_material,
                            factory.land_mine_blast_material))
        elif self.bomb_type == 'impact':
            intex = (factory.impact_lit_tex, factory.impact_tex,
                     factory.impact_tex)
            self.texture_sequence = ba.newnode('texture_sequence',
                                               owner=self.node,
                                               attrs={
                                                   'rate': 100,
                                                   'input_textures': intex
                                               })
            ba.timer(
                0.25,
                ba.WeakCall(self._add_material,
                            factory.land_mine_blast_material))
        else:
            raise Exception('arm() should only be called '
                            'on land-mines or impact bombs')
        self.texture_sequence.connectattr('output_texture', self.node,
                                          'color_texture')
        ba.playsound(factory.activate_sound, 0.5, position=self.node.position)

    def _handle_hit(self, msg: ba.HitMessage) -> None:
        ispunch = (msg.srcnode and msg.srcnode.getnodetype() == 'spaz')

        # Normal bombs are triggered by non-punch impacts;
        # impact-bombs by all impacts.
        if (not self._exploded and not ispunch
                or self.bomb_type in ['impact', 'land_mine']):
            # Also lets change the owner of the bomb to whoever is setting
            # us off. (this way points for big chain reactions go to the
            # person causing them).
            if msg.source_player not in [None]:
                self.source_player = msg.source_player

                # Also inherit the hit type (if a landmine sets off by a bomb,
                # the credit should go to the mine)
                # the exception is TNT.  TNT always gets credit.
                if self.bomb_type != 'tnt':
                    self.hit_type = msg.hit_type
                    self.hit_subtype = msg.hit_subtype

            ba.timer(100 + int(random.random() * 100),
                     ba.WeakCall(self.handlemessage, ExplodeMessage()),
                     timeformat=ba.TimeFormat.MILLISECONDS)
        assert self.node
        self.node.handlemessage("impulse", msg.pos[0], msg.pos[1], msg.pos[2],
                                msg.velocity[0], msg.velocity[1],
                                msg.velocity[2], msg.magnitude,
                                msg.velocity_magnitude, msg.radius, 0,
                                msg.velocity[0], msg.velocity[1],
                                msg.velocity[2])

        if msg.srcnode:
            pass

    def handlemessage(self, msg: Any) -> Any:
        if isinstance(msg, ExplodeMessage):
            self.explode()
        elif isinstance(msg, ImpactMessage):
            self._handle_impact()
        elif isinstance(msg, ba.PickedUpMessage):
            # change our source to whoever just picked us up *only* if its None
            # this way we can get points for killing bots with their own bombs
            # hmm would there be a downside to this?...
            if self.source_player is not None:
                self.source_player = msg.node.source_player
        elif isinstance(msg, SplatMessage):
            self._handle_splat()
        elif isinstance(msg, ba.DroppedMessage):
            self._handle_dropped()
        elif isinstance(msg, ba.HitMessage):
            self._handle_hit(msg)
        elif isinstance(msg, ba.DieMessage):
            self._handle_die()
        elif isinstance(msg, ba.OutOfBoundsMessage):
            self._handle_oob()
        elif isinstance(msg, ArmMessage):
            self.arm()
        elif isinstance(msg, WarnMessage):
            self._handle_warn()
        else:
            super().handlemessage(msg)


class TNTSpawner:
    """Regenerates TNT at a given point in space every now and then.

    category: Gameplay Classes
    """

    def __init__(self, position: Sequence[float], respawn_time: float = 30.0):
        """Instantiate with given position and respawn_time (in seconds)."""
        self._position = position
        self._tnt: Optional[Bomb] = None
        self._update()
        # (go with slightly more than 1 second to avoid timer stacking)
        self._update_timer = ba.Timer(1.1,
                                      ba.WeakCall(self._update),
                                      repeat=True)
        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
        self._wait_time = 0.0

    def _update(self) -> None:
        tnt_alive = self._tnt is not None and self._tnt.node
        if not tnt_alive:
            # respawn if its been long enough.. otherwise just increment our
            # how-long-since-we-died value
            if self._tnt is None or self._wait_time >= self._respawn_time:
                self._tnt = Bomb(position=self._position, bomb_type='tnt')
                self._wait_time = 0.0
            else:
                self._wait_time += 1.1
