[GAME MODE] Bomb Defusal

Recently a friend of mine from college said he’ll do some programming work for me and I had him redo the defusal game mode (http://aloha.pk/index.php?topic=9452.0)

So far I briefly tested it on piqueserver with openspades and it looks like it works somewhat.

ATM there is only one bombsite and the base gamemode is CTF. According to my friend, two bombsites could be possible (along with with progression bar) if base game mode was TC). The code can probably be improved upon by others who are more familiar with aos/piqueserver mechanics.

Code

"""
Bombdefusal: Game mode based on Counter Strike


Green Team is Terrorist
Blue Team is Counter-Terrorist

Green Team starts with enemy intel (BOMB) amd must take it to their base (BOMB SITE) and plant it there.
The have to hold the bomb site in order to prevent CT's from defusing the bomb.
The game is won by either killing all members of enemy team or successfully detonating the bomb.

Blue Team's main objective is to prevent Green Team from planting the bomb.
The game is won by either killing all members of enemy team or successfully defusing the bomb.

Spawn and Bomb Site locations must be configured via extensions in the map's
map_name.txt metadata:
>>> extensions = {
...     'ct_spawn': (235, 310, 58),
...     't_spawn': (275, 180, 49),
...     'bomb_site': (205, 307, 52),
... }

Optionally, the following variables can also be changed:

-> bomb_duration
-> bomb_radius
-> defuse_time
-> plant_time

"""
from __future__ import division
import random
import time

from twisted.internet.error import AlreadyCalled, AlreadyCancelled
from twisted.internet.reactor import callLater
from twisted.internet.task import LoopingCall
from pyspades.collision import distance_3d
from pyspades.common import Vertex3
from pyspades.constants import *
from pyspades.contained import ChatMessage, GrenadePacket
from pyspades.world import Grenade


chat_message, grenade_packet = ChatMessage(), GrenadePacket()

out_of_map = (0, 0, 63) # Location to hide unwanted elements of the game

ROUND_TIME = 120  # In seconds

BOMB_TIME = 40 # bomb duration

def bomb_calc(bomb_pos, player_pos, blast_radius): # calculation of bomb damage based on distance
    distance = distance_3d(bomb_pos, player_pos)
    if distance > blast_radius:
        return 0
    return 1.2 - distance / blast_radius


def all_dead(team):  # check if entire team is dead
    for player in team.get_players():
        if not player.world_object.dead:
            return False
    return True


def send_chat(connection, value, global_message):  # sending chat to other team members
    chat_message.player_id = connection.player_id
    chat_message.value = value
    chat_message.chat_type = [CHAT_TEAM, CHAT_ALL][int(global_message)]
    connection.protocol.send_contained(chat_message, team=None if global_message else connection.team)


def apply_script(protocol, connection, config):

    class BombdefusalConnection(connection):
        defusing = False
        planting = False

        def cancel_actions(self):  # cancel all actions that a player can perform
            if self.defusing:
                self.protocol.stop_defusing(canceled=True, connection=self)
            if self.planting:
                self.protocol.stop_planting(canceled=True, connection=self)

        def on_disconnect(self):
            if self.protocol.match_running:
                self.cancel_actions()
                if self.world_object is not None:
                    self.world_object.dead = True    # kill players on disconnect
                    self.protocol.check_game_status()
            return connection.on_disconnect(self)

        def on_team_join(self, team):
            ret = connection.on_team_join(self, team)
            if ret is False:
                return ret
            if self.protocol.match_running:
                self.cancel_actions()
                if self.world_object is not None and not self.world_object.dead: # kill joining player if match is already in progress
                    self.world_object.dead = True
                    self.protocol.check_game_status()
            return ret

        def get_respawn_time(self):
            if self.protocol.match_running:
                return -1
            elif self.protocol.game_enabled:
                return 0
            return connection.get_respawn_time(self)

        def respawn(self):              
            if self.protocol.match_running:
                self.cancel_actions()
                return False                    # no respawn which round is in progress
            return connection.respawn(self)

        def on_spawn(self, position):
            ret = connection.on_spawn(self, position) 
            if self.protocol.match_running:
                self.cancel_actions()
                self.kill()                 # kill joining player if match is already in progress
            return ret

        def on_spawn_location(self, pos):
            if self.protocol.game_enabled:
                self.cancel_actions()
                return self.team.spawn     # set team spawn location
            return connection.on_spawn_location(self, pos)

        def on_kill(self, killer, type, grenade):
            if self.protocol.match_running:
                if self.world_object is not None:
                    self.world_object.dead = True
                    self.protocol.check_game_status()  # check game status after each player kill
            if self.protocol.match_running:
                self.cancel_actions()

            return connection.on_kill(self, killer, type, grenade)

        def on_refill(self):
            ret = connection.on_refill(self)
            if self.protocol.match_running:
                return False
            return ret

        def on_flag_take(self):     # event that is triggered when Counter-Terrorist come near the bomb (green intel).
            if not self.protocol.game_enabled or not self.protocol.match_running:
                return connection.on_flag_take(self)

            if self.team == self.protocol.green_team:
                if self.protocol.destruction and self.protocol.destruction.active():
                    return False

                self.send_chat("You picked up the C4 explosive, take it to the bombsite!")
                return connection.on_flag_take(self)

            elif (self.protocol.destruction and self.protocol.destruction.active() and
                  self.team == self.protocol.blue_team):
                if self.protocol.defuse_timer:
                    return False

                if self.tool != BLOCK_TOOL:
                    self.send_chat("You must have your BLOCK TOOL out in order to defuse the bomb!")
                    return False

                ret = connection.on_flag_take(self)
                self.defusing = True
                send_chat(self, "I'm defusing the bomb.", global_message=False)
                self.protocol.defuse_pos = self.get_location()
                self.protocol.defuse_loop = LoopingCall(self.protocol.defuse_action, self)
                self.protocol.defuse_loop.start(0.08)
                return ret

            return False

        def capture_flag(self):   # event that is triggered when terrorist come near their base/tent (bomb site).
            if not self.protocol.game_enabled or not self.protocol.match_running:
                return connection.capture_flag(self)

            if self.protocol.destruction and self.protocol.destruction.active() or self.protocol.plant_timer:
                return False

            if self.team == self.protocol.green_team:
                if self.tool != BLOCK_TOOL:
                    self.send_chat("You must have your BLOCK TOOL out in order to plant the bomb!")
                    return False
                send_chat(self, "I'm planting the bomb.", global_message=False)
                self.protocol.plant_pos = self.get_location()
                self.protocol.plant_loop = LoopingCall(self.protocol.plant_action, self)
                self.protocol.plant_loop.start(0.08)

            return False

    class BombdefusalProtocol(protocol):
        game_mode = CTF_MODE
        
        def on_map_change(self, new_map):
            extensions = self.map_info.extensions  # checking if extensions are set properly
            if not extensions:
                self.game_enabled = False
                self.match_running = False
                return protocol.on_map_change(self, new_map)
            self.building = False
            self.green_team.spawn = extensions.get('t_spawn', out_of_map)
            self.blue_team.spawn = extensions.get('ct_spawn', out_of_map)
            self.bomb_site = extensions.get('bomb_site', out_of_map)
            self.bomb_duration = extensions.get('bomb_duration', BOMB_TIME)
            self.bomb_radius = extensions.get('bomb_radius', 40)
            self.defuse_time = extensions.get('defuse_time', 5)
            self.plant_time = extensions.get('plant_time', 5)
            self.start_round(first=True)
            return protocol.on_map_change(self, new_map)

        def start_round(self, first=False, winner=False):
            self.game_enabled = True  # Whether or not Defusal should run on this map
            self.match_running = False  # Whether or not the round is running

            if not first:
                if self.destruction and self.destruction.active():
                    self.destruction.cancel()
                if self.time_limit_check and self.time_limit_check.active():
                    self.time_limit_check.cancel()
                self.green_team.flag.player.drop_flag() if self.green_team.flag.player else None  # all objects are relocated back to their original place
                self.green_team.flag.set(*out_of_map)
                self.green_team.flag.update()
                self.green_team.base.set(*self.bomb_site)
                self.green_team.base.update()
                self.blue_team.flag.player.drop_flag() if self.blue_team.flag.player else None
                self.blue_team.flag.set(*self.green_team.spawn)
                self.blue_team.flag.update()
                self.blue_team.base.set(*out_of_map)
                self.blue_team.base.update()

            for team in (self.blue_team, self.green_team):  # must have atleast one player in each team to start round
                if team.count() == 0:
                    self.send_chat("Both teams require at least one player to start.")
                    callLater(5, self.start_round, first=True)
                    return

            if winner:
                self.win_action(winner=winner)
            else:
                self.match_running = True
            if winner is None:
                self.send_chat("Time ran out! No team wins.")

            self.reset_everything()
            self.match_running = True
            self.send_chat("Begin round!")
            self.send_chat("Time limit for this round: {} seconds.".format(ROUND_TIME))

            self.time_limit_check = callLater(ROUND_TIME, self.start_round, winner=None)

            self.defuser = None  # Connection object of the bomb defuser
            self.planter = None  # Connection object of the bomb planter
            self.plant_pos = (0, 0, 0)  # Location in which the bomb was planted
            self.plant_timer = 0.0  # Time since bomb plant began. (Reset to 0 after plant is over)
            self.plant_loop = None  # The twisted LoopingCall object that calls the plant_action function
            self.defuse_pos = (0, 0, 0)  # Location in which the bomb was defused
            self.defuse_timer = 0.0  # Time since bomb defuse began. (Reset to 0 after defuse is over)
            self.defuse_loop = None  # The twisted LoopingCall object that calls the defuse_action function
            self.add_delay = False  # Whether to add delay after planting or defusing was cancelled.
            self.destruction = None  # The twisted callLater object that explodes the bomb if not cancelled.

        def on_base_spawn(self, x, y, z, base, entity_id):
            if not self.game_enabled:
                return protocol.on_base_spawn(self, x, y, z, base, entity_id)
            if entity_id == GREEN_BASE:   # Base (tent) of green team (Terrorists) spawns at bomb site
                return self.bomb_site
            return out_of_map

        def on_flag_spawn(self, x, y, z, flag, entity_id):
            if not self.game_enabled:
                return protocol.on_flag_spawn(self, x, y, z, flag, entity_id)
            if entity_id == BLUE_FLAG:    # Flag of blue team (Counter-Terrorists) spawns at Terrorist spawn location
                return self.green_team.spawn 
            return out_of_map

        def plant_action(self, connection): # Logic for planting bomb, this function is called in a loop with set intervals
            if(self.add_delay):
                time.sleep(0.8)
                self.add_delay = False
            self.current_defuser = connection
            remaining = round(self.plant_time - self.plant_timer, 2)
            time.sleep(0.08)
            self.plant_timer += 0.08
            connection.set_location(self.plant_pos)
            try:
                if remaining.is_integer():
                    connection.send_chat("planting bomb: {} seconds remaining. Sneak (Default: V) or change weapons to cancel.".format(remaining))
            except:
                remaining = float(remaining)
                if remaining.is_integer():
                    connection.send_chat("planting bomb: {} seconds remaining. Sneak (Default: V) or change weapons to cancel.".format(remaining))
            if connection and connection.world_object.sneak or connection.tool != BLOCK_TOOL:
                self.stop_planting(canceled=True, connection=connection)
                callLater(2, self.reset_status) 
                return
            if self.plant_timer < self.plant_time:
                return
            self.stop_planting(canceled=False, connection=connection)
            self.plant_success(connection)
        
        def stop_planting(self, canceled, connection): # stop planting loop, either because canceled or successful
            connection.planting = False
            try:
                self.plant_loop.stop()
            except:
                pass
            if canceled:
                self.add_delay = True
            self.plant_pos = (0, 0, 0) if canceled else self.plant_pos  # We use this later for the explosion.
            self.plant_timer = 0 

        def plant_success(self, connection): # bomb is planted, bomb duration call is initiated
            if self.destruction and self.destruction.active():
                return
            if self.time_limit_check and self.time_limit_check.active():
                self.time_limit_check.cancel()
            self.planter = connection
            self.destruction = callLater(self.bomb_duration, self.explode_bomb) # explode function will be called after bomb_duration
            self.send_chat("Bomb has been planted. {} seconds until detonation.".format(self.bomb_duration))
            player_pos = connection.get_location()
            self.green_team.flag.set(player_pos[0], player_pos[1], self.map.get_z(player_pos[0], player_pos[1])) # replace Green Team's tent with Green Team's flag at bomb site to represent bomb.
            self.green_team.flag.update()
            self.green_team.base.set(*out_of_map)
            self.green_team.base.update()
            self.blue_team.flag.set(*out_of_map)
            self.blue_team.flag.update()

        def explode_bomb(self): # bomb effect
            if self.defuse_loop:
                self.stop_defusing(canceled=True)
            for _ in range(12):
                x, y, z = self.bomb_site
                position = Vertex3(x, y, z)
                velocity = Vertex3(0.0, 0.0, 0.0)
                grenade_object = self.world.create_object(Grenade, 0.0, position, None, velocity,
                                                          self.planter.grenade_exploded)
                grenade_object.name = "c4"
                grenade_packet.value = grenade_object.fuse
                grenade_packet.player_id = self.planter.player_id
                grenade_packet.position = position.get()
                grenade_packet.velocity = velocity.get()
                self.send_contained(grenade_packet)
            self.match_running = False
            self.send_chat("Terrorists win.")
            for player in self.players.values(): 
                damage = bomb_calc(self.plant_pos, player.get_location(), self.bomb_radius) * 100
                if damage:
                    player.hit(value=damage, by=self.planter)
            callLater(2, self.start_round, winner=self.green_team)

        def defuse_action(self, connection):  #logic for defusing bomb, this function is called in a loop at set intervals.
            if(self.add_delay):
                time.sleep(0.8)
                self.add_delay = False
            time.sleep(0.08)
            remaining = round(self.defuse_time - self.defuse_timer, 1)
            self.defuse_timer += 0.08
            connection.set_location(self.defuse_pos)
            try:
                if remaining.is_integer():
                    connection.send_chat("Defusing bomb: {} seconds remaining. Sneak (Default: V) or change weapons to cancel.".format(remaining))
            except:
                remaining = float(remaining)
                if remaining.is_integer():
                    connection.send_chat("Defusing bomb: {} seconds remaining. Sneak (Default: V) or change weapons to cancel.".format(remaining))

            if connection and connection.world_object.sneak or connection.tool != BLOCK_TOOL:
                self.stop_defusing(canceled=True, connection=connection)
                callLater(2, self.reset_status)  # if action is cancelled all cooldowns are reset
                return

            if self.defuse_timer < self.defuse_time:
                return
            try:
                self.destruction.cancel()
            except (AlreadyCancelled, AlreadyCalled):
                return
            self.defuser = connection
            self.stop_defusing(canceled=False, connection=connection)
            self.match_running = False
            self.send_chat("Bomb has been defused. Counter-Terrorists win.")
            self.send_chat("{player} defused the bomb.".format(player=self.defuser.name))
            callLater(2, self.start_round, winner=self.blue_team)

        def stop_defusing(self, canceled, connection=None): #stop defusing loop, either because canceled or successful
            if connection:
                connection.defusing = False
            else:
                self.current_defuser.defusing = False
                self.current_defuser = None
            try:
                self.defuse_loop.stop()
            except:
                pass
            if canceled:
                self.add_delay = True
            if canceled and self.green_team.flag.player:  # if cancelled return the bomb (green intel) to its location
                self.green_team.flag.player.drop_flag()
                self.green_team.flag.set(self.plant_pos[0],self.plant_pos[1], self.map.get_z(self.plant_pos[0], self.plant_pos[1]))
                self.green_team.flag.update()
            self.defuse_pos = (0, 0, 0) if canceled else self.defuse_pos
            self.defuse_timer = 0

        def win_action(self, winner):
            player = self.planter if winner == self.green_team else self.defuser
            if not (player and player.team == winner):
                player = random.choice(list(winner.get_players()))
            if player.world_object.dead:
                player.spawn(winner.spawn)
            player.take_flag()
            player.capture_flag()

        def reset_everything(self):  # relocate players to their spawn locations and refill everything. Also respawns all dead players
            # Taken from arena.py
            for player in self.players.values():
                player.defusing = False
                player.planting = False
                if player.team.spectator:
                    continue
                if player.world_object.dead:
                    player.spawn(player.team.spawn)
                else:
                    player.set_location(player.team.spawn)
                    player.refill()

        def check_game_status(self): # check if one of the conditions for ending the round is met
            if not self.match_running: 
                return

            if all_dead(self.green_team):
                if self.destruction:
                    if self.destruction.active():
                        return
                    else:
                        try:
                            self.destruction.cancel()
                        except (AlreadyCancelled, AlreadyCalled):
                            return
                self.match_running = False
                self.send_chat("Conter-Terrorists win")
                callLater(2, self.start_round, winner=self.blue_team)

            elif all_dead(self.blue_team):
                if self.destruction:
                    try:
                        self.destruction.cancel()
                    except (AlreadyCalled, AlreadyCancelled):
                        return
                self.match_running = False
                self.send_chat("Terrorists win.")
                callLater(2, self.start_round, winner=self.green_team)

        def reset_status(self): # reset timers
            self.plant_timer = 0
            self.defuse_timer = 0

    return BombdefusalProtocol, BombdefusalConnection

[code=map extensions]
extensions = {
‘ct_spawn’: (235, 310, 58),
‘t_spawn’: (275, 180, 49),
‘bomb_site’: (205, 307, 52),
}



---

So is this an actual playable new gamemode? :eyes::eyes::eyes:

Not currently on any server but I did some basic testing and it looks like it might be playable in the current form (at least with piqueserver and openspades | haven’t tested it on pysnip or Aos 0.75)

IMO some improvements will make it better.