## =========================================== ##
## TNT Run minigame implemented using Denizen  ##
## by @seb303                                  ##
## v1.0.2 2022-05-20                           ##
## Requires Denizen-1.2.4-b6247-DEV or newer   ##  https://ci.citizensnpcs.co/job/Denizen_Developmental/
## Requires TabComplete Engine 1.4             ##  https://forum.denizenscript.com/resources/tabcomplete-engine.13/
## =========================================== ##

## To do:
#  Implement double-jump
#  Add PVP option
#  Add holographic instructions in starting lobby


# Requires the following noted location:
# endgame_lobby     = Location to teleport after game
#
# To note a location, stand in the location and use command:
# /ex note <player.location> as:<noted name>
#
#
## -------- ##
## COMMANDS ##
## -------- ##
#
# To create a new arena:
# /tntrun arena create <my arena>
# Permission: custom.tntrun.arena
#
# To select the cuboid region enclosing the arena:
# Either use the Denizen Area Selector Tool https://forum.denizenscript.com/resources/area-selector-tool.1/
# Or use the WorldEdit wand (requires WorldEdit & Depenizen)
# All blocks within the region are saved for later regeneration.
# The top layer of floor blocks will be used for the lobby and start area.
# A player loses the game when they leave the region, and when spectating they are kept inside the region.
# /tntrun arena setregion <my arena>
# Permission: custom.tntrun.arena
#
# To list all arenas:
# /tntrun arena list
# Permission: custom.tntrun.arena
#
# To remove an arena:
# /tntrun arena remove <my arena>
# Permission: custom.tntrun.arena
#
# To join game in arena with most queued players:
# /tntrun join
# Permission: custom.tntrun.join (permitted by default)
#
# To join a game in a specific arena:
# /tntrun join <my arena>
# Permission: custom.tntrun.join (permitted by default)
#
# To reset all active games:
# /tntrun reset
# Permission: custom.tntrun.reset
#
# To show help info:
# /tntrun
# Permission: custom.tntrun.help (permitted by default)
#
#
# Note: default permissions will not work until after server restart, although usually op'ed users will have all perms


## CONFIG STARTS ##
tntrun_config:
    type: data

    # Countdown time in seconds before game starts
    countdown_time: 5

    # Minimal players for game to start
    min_players: 2

    # Possible materials for floor layers (floors can be up to 2 layers thick)
    floor_blocks:
        - sand
        - red_sand
        - gravel
        - tnt

    # Item to exit game lobby
    exit_item: iron_door

    # Text at bottom of sidebar
    sidebar_footer: "PiglinCraft MiniGames"

## CONFIG ENDS ##



# ----------------------------------------------------------------------------------------
# Server flags:
# tntrun_arenas.<my arena>.status  - 'created', 'ready', 'queued', 'playing', 'resetting'
# tntrun_arenas.<my arena>.timer   - timer/counter
# tntrun_arenas.<my arena>.lobby   - list of locations of lobby/start area
# tntrun_active                    - set while secondly event handler needs to run
#
# Player flags:
# tntrun.<my arena>.queued      - player is in queue waiting to start
# tntrun.<my arena>.playing     - player is playing a game
# tntrun.<my arena>.spectating  - player is spectating a game
# ----------------------------------------------------------------------------------------

tntrun_TabComplete:
    type: data
    tntrun:
        ?custom.tntrun.arena arena:
            create: end
            setregion:
                _*arena: end
            list: end
            remove:
                _*arena: end
        ?custom.tntrun.join join:
            _random: end
            _*arena: end
        ?custom.tntrun.reset reset: end
tntrun_TabComplete_arena:
    type: procedure
    debug: false
    script:
        - determine <server.flag[tntrun_arenas].keys>


tntrun_command_tntrun:
    type: command
    debug: false
    name: tntrun
    description: TNT Run minigame
    usage: /tntrun arena|join|reset
    tab complete:
        - determine <proc[TabComplete].context[<list[tntrun|tntrun_TabComplete].include_single[<context.raw_args>]>]>

    show_docs:
        - if !<player.has_permission[custom.tntrun.help]>:
            - narrate "<&[error]>You need permission custom.tntrun.help to see the help docs"
            - stop
        - narrate <&7><&sp.repeat[72].strikethrough>
        - if <player.has_permission[custom.tntrun.arena]> && ( <[showdoc]> == arena || <[showdoc]> == * ):
            - narrate "<&6>/tntrun arena create <&lt>my arena<&gt> <&f> Create a new arena"
            - narrate "<&6>/tntrun arena setregion <&lt>my arena<&gt> <&f> Set arena location"
            - narrate "<&6>/tntrun arena list <&f> List all arenas"
            - narrate "<&6>/tntrun arena remove <&lt>my arena<&gt> <&f> Remove an arena"
        - if <player.has_permission[custom.tntrun.join]> && ( <[showdoc]> == join || <[showdoc]> == * ):
            - narrate "<&6>/tntrun join <&f> Join the queue to play"
            - narrate "<&6>/tntrun join <&lt>my arena<&gt> <&f> Play in a specific arena"
        - if <player.has_permission[custom.tntrun.reset]> && ( <[showdoc]> == reset || <[showdoc]> == * ):
            - narrate "<&6>/tntrun reset <&f> Reset server flags and all active games"
        - narrate <&7><&sp.repeat[72].strikethrough>
        - stop

    no_permission:
        - narrate "<&[error]>You don't have permission to use this command"
        - stop

    script:
        - if <context.args.size> == 0:
            - define showdoc *
            - inject tntrun_command_tntrun.show_docs
        - define showdoc <context.args.first>
        - choose <context.args.first>:

            # /tntrun arena
            - case arena:
                - if !<player.has_permission[custom.tntrun.arena]>:
                    - inject tntrun_command_tntrun.no_permission
                - if <context.args.size> == 1:
                    - inject tntrun_command_tntrun.show_docs
                - choose <context.args.get[2]>:

                    # /tntrun arena create <my arena>
                    - case create:
                        - if <context.args.size> != 3:
                            - inject tntrun_command_tntrun.show_docs
                        - define arenaName <context.args.get[3]>
                        - if <server.flag[tntrun_arenas].contains[<[arenaName]>]>:
                            - narrate "<&[error]>An arena already exists with this name"
                        - else if !<[arenaName].to_lowercase.matches_character_set[abcdefghijklmnopqrstuvwxyz0123456789_-]>:
                            - narrate "<&[error]>Arena names may only contain alphanumeric characters, underscores or hyphens"
                        - else:
                            - flag server tntrun_arenas.<[arenaName]>.status:created
                            - narrate "<&f>Arena <&[emphasis]><[arenaName]><&f> has been created"

                    # /tntrun arena setregion <my arena>
                    - case setregion:
                        - if <context.args.size> != 3:
                            - inject tntrun_command_tntrun.show_docs
                        - define arenaName <context.args.get[3]>
                        - if !<server.flag[tntrun_arenas].contains[<[arenaName]>]>:
                            - narrate "<&[error]>No arena exists with this name"
                        - else if <server.online_players_flagged[tntrun.<[arenaName]>].size> > 0:
                            - narrate "<&[error]>This arena is currently being played"
                        - else:
                            - if <player.has_flag[seltool_selection]> && <player.flag[seltool_type]> == cuboid:
                                - define region <player.flag[seltool_selection]>
                                - narrate "Got region from Area Selector Tool"
                            - else if <player.we_selection.exists> && <player.we_selection.object_type> == cuboid:
                                - define region <player.we_selection>
                                - narrate "Got region from WorldEdit"
                            - else:
                                - narrate "<&[error]>Please select a cuboid region using the Area Selector Tool or WorldEdit"
                                - stop
                            - narrate "Saving blocks..."

                            # Scan blocks to find top layer of floor blocks
                            - define matcher <script[tntrun_config].data_key[floor_blocks]>
                            - define maxY -9999
                            - foreach <[region].blocks[<[matcher]>]> as:entry:
                                - if <[entry].y> > <[maxY]>:
                                    - define maxY <[entry].y>
                            - if <[maxY]> == -9999:
                                - narrate "<&[error]>Error: No valid floor materials found in region"
                                - stop
                            - define lobby <[region].blocks[<[matcher]>].filter[y.equals[<[maxY]>]].parse[above]>

                            # Note region & lobby area (above top layer of floor blocks)
                            - note <[region]> as:tntrun_arena_<[arenaName]>
                            - flag server tntrun_arenas.<[arenaName]>.lobby:<[lobby]>

                            # Save whole region for later regeneration
                            - ~schematic create name:tntrun_arena_<[arenaName]> area:<[region]> <[region].center> entities
                            - ~schematic save name:tntrun_arena_<[arenaName]>
                            - schematic unload name:tntrun_arena_<[arenaName]>

                            # Indicate arena ready
                            - flag server tntrun_arenas.<[arenaName]>.status:ready
                            - narrate "<&f>Arena <&[emphasis]><[arenaName]><&f> is ready to use"

                    # /tntrun arena list
                    - case list:
                        - if <context.args.size> != 2:
                            - inject tntrun_command_tntrun.show_docs
                        - if <server.flag[tntrun_arenas].size> == 0:
                            - narrate "<&[error]>There are no arenas"
                        - else:
                            - narrate <&[emphasis]><server.flag[tntrun_arenas].keys.alphanumeric.separated_by[<&nl>]>

                    # /tntrun arena remove <my arena>
                    - case remove:
                        - if <context.args.size> != 3:
                            - inject tntrun_command_tntrun.show_docs
                        - define arenaName <context.args.get[3]>
                        - if !<server.flag[tntrun_arenas].contains[<[arenaName]>]>:
                            - narrate "<&[error]>No arena exists with this name"
                        - else if <server.online_players_flagged[tntrun.<[arenaName]>].size> > 0:
                            - narrate "<&[error]>This arena is currently being played"
                        - else:
                            - flag server tntrun_arenas.<[arenaName]>:!
                            - note remove as:tntrun_arena_<[arenaName]>
                            - adjust server delete_file:schematics/tntrun_arena_<[arenaName]>.schem
                            - narrate "<&f>Arena <&[emphasis]><[arenaName]><&f> has been removed"

                    - default:
                        - inject tntrun_command_tntrun.show_docs

            # /tntrun join [<my arena>]
            - case join:
                - if !<player.has_permission[custom.tntrun.join]>:
                    - inject tntrun_command_tntrun.no_permission
                - if <context.args.size> == 1:
                    # Find arena with most queued players
                    - define numbers_queued <map>
                    - foreach <server.flag[tntrun_arenas]>:
                        - if <list[ready|queued].contains[<server.flag[tntrun_arenas.<[key]>.status]>]>:
                            - define numbers_queued.<[key]> <server.online_players_flagged[tntrun.<[key]>.queued].size>
                    - if <[numbers_queued].size> == 0:
                        - narrate "<&[error]>There are no arenas ready to play"
                        - stop
                    - define arenaName <[numbers_queued].sort_by_value.keys.last>
                    # If no arena has queued players, select a random arena
                    - if <[numbers_queued].get[<[arenaName]>]> == 0:
                        - define arenaName <[numbers_queued].keys.random>

                - else if <context.args.size> != 2:
                    - inject tntrun_command_tntrun.show_docs
                - else:
                    - define arenaName <context.args.get[2]>
                    - if !<server.flag[tntrun_arenas].contains[<[arenaName]>]>:
                        - narrate "<&[error]>No arena exists with this name"
                        - stop

                - choose <server.flag[tntrun_arenas.<[arenaName]>.status]>:
                    - case created:
                        - narrate "<&[error]>Arena <[arenaName]> has no region defined"
                        - stop
                    - case ready:
                        - flag server tntrun_active
                        - flag server tntrun_arenas.<[arenaName]>.status:queued
                        - flag server tntrun_arenas.<[arenaName]>.timer:<script[tntrun_config].data_key[countdown_time]>
                        - define playerStatus queued
                    - case queued:
                        - define playerStatus queued
                    - case playing:
                        - define playerStatus spectating
                    - case resetting:
                        - narrate "<&[error]>Arena <[arenaName]> is currently resetting"
                        - stop

                - teleport <server.flag[tntrun_arenas.<[arenaName]>.lobby].random.center>
                - adjust <player.location.world> allow_pvp:false
                - inventory clear
                - adjust <player> health:20
                - feed <player> amount:20
                - run tntrun_setPlayerStatus def:<[arenaName]>|<[playerStatus]>
                - inventory set o:<script[tntrun_config].data_key[exit_item]>[display=<red>Leave] slot:9
                # Need to wait a short while before setting gamemode otherwise Multiverse will override
                # 2t seems to be enough, but wait 5t just in case
                - wait 5t
                - if <[playerStatus]> == spectating:
                    - adjust <player> gamemode:spectator
                - else:
                    - adjust <player> gamemode:adventure

            # /tntrun reset
            - case reset:
                - if !<player.has_permission[custom.tntrun.reset]>:
                    - inject tntrun_command_tntrun.no_permission
                - if <context.args.size> != 1:
                    - inject tntrun_command_tntrun.show_docs

                - foreach <server.flag[tntrun_arenas]> key:arenaName as:arena:
                    - if <[arena].get[status]> == queued || <[arena].get[status]> == playing:
                        - run tntrun_resetArena def:<[arenaName]>

            - default:
                - define showdoc *
                - inject tntrun_command_tntrun.show_docs

tntrun_setPlayerStatus:
    type: task
    debug: false
    definitions: arenaName|playerStatus
    script:
        - flag <player> tntrun:<map[<[arenaName]>=<map[<[playerStatus]>=1]>]>

tntrun_resetArena:
    type: task
    debug: false
    definitions: arenaName
    script:
        - flag server tntrun_arenas.<[arenaName]>.status:resetting
        # Reset players
        - define online_players <server.online_players_flagged[tntrun.<[arenaName]>]>
        - foreach <[online_players]> as:__player:
            - flag <player> tntrun:!
        - sidebar remove players:<[online_players]>
        - teleport <[online_players]> endgame_lobby
        # Regenerate arena
        - ~schematic load name:tntrun_arena_<[arenaName]>
        - ~schematic paste name:tntrun_arena_<[arenaName]> <cuboid[tntrun_arena_<[arenaName]>].center> entities
        - schematic unload name:tntrun_arena_<[arenaName]>
        - flag server tntrun_arenas.<[arenaName]>.status:ready

tntrun_sortBlocks:
  type: procedure
  debug: false
  definitions: blockA|blockB|block_under
  script:
    - define distanceA <[block_under].distance[<[blockA].center>]>
    - define distanceB <[block_under].distance[<[blockB].center>]>
    - if <[distanceA]> == <[distanceB]>:
        - determine 0
    - else if <[distanceA]> > <[distanceB]>:
        - determine 1
    - else:
        - determine -1

tntrun_events_timed:
    type: world
    debug: false
    events:
        on delta time secondly server_flagged:tntrun_active:
            - define stillActive false
            - define sidebar_footer <yellow><script[tntrun_config].data_key[sidebar_footer]>
            - foreach <server.flag[tntrun_arenas]> key:arenaName as:arena:
                - choose <[arena].get[status]>:
                    - case queued:
                        - define stillActive true
                        - define playersList <server.online_players_flagged[tntrun.<[arenaName]>.queued]>
                        - define playerCount <[playersList].size>
                        - if <[arena].get[timer]> <= 0 && <[playerCount]> >= <script[tntrun_config].data_key[min_players]>:
                            # Countdown ended & enough players, so start game
                            - foreach <server.online_players_flagged[tntrun.<[arenaName]>.queued]> as:__player:
                                - run tntrun_setPlayerStatus def:<[arenaName]>|playing
                                #- teleport <[arena].get[lobby].random.center>
                                - inventory clear
                            - flag server tntrun_arenas.<[arenaName]>.status:playing
                            - flag server tntrun_arenas.<[arenaName]>.timer:0

                            # Main game loop here
                            - define matcher <script[tntrun_config].data_key[floor_blocks]>
                            - define playing <server.online_players_flagged[tntrun.<[arenaName]>.playing]>
                            - define remove_block_queue <list>
                            - while <[playing].size> > 1:
                                - modifyblock <[remove_block_queue]> air
                                - define remove_block_queue <list>
                                - foreach <[playing]> as:__player:
                                    # Block directly under player location
                                    - define block_under <player.location.sub[0,0.5,0]>
                                    - if <[block_under].center.find_blocks[<[matcher]>].within[0.5].any>:
                                        - define remove_block_queue:->:<[block_under]>
                                        # Another layer?
                                        - define remove_block_queue:|:<[block_under].block.add[0.5,-0.5,0.5].find_blocks[<[matcher]>].within[0.5]>
                                    - else:
                                        # Integer coords of block under
                                        - define block_under_int <[block_under].block>
                                        # Build list of other blocks which are supporting the player
                                        - define other_supporting_blocks <list>
                                        - define pos_offset <[block_under].sub[<[block_under_int]>]>
                                        - if <[pos_offset].x> < 0.3:
                                            - define other_supporting_blocks:->:<[block_under_int].add[-1,0,0]>
                                            - if <[pos_offset].z> < 0.3:
                                                - define other_supporting_blocks:->:<[block_under_int].add[-1,0,-1]>
                                            - else if <[pos_offset].z> > 0.7:
                                                - define other_supporting_blocks:->:<[block_under_int].add[-1,0,1]>
                                        - else if <[pos_offset].x> > 0.7:
                                            - define other_supporting_blocks:->:<[block_under_int].add[1,0,0]>
                                            - if <[pos_offset].z> < 0.3:
                                                - define other_supporting_blocks:->:<[block_under_int].add[1,0,-1]>
                                            - else if <[pos_offset].z> > 0.7:
                                                - define other_supporting_blocks:->:<[block_under_int].add[1,0,1]>
                                        - if <[pos_offset].z> < 0.3:
                                            - define other_supporting_blocks:->:<[block_under_int].add[0,0,-1]>
                                        - else if <[pos_offset].z> > 0.7:
                                            - define other_supporting_blocks:->:<[block_under_int].add[0,0,1]>
                                        # Only interested in blocks which match specific materials
                                        - define other_supporting_blocks <[other_supporting_blocks].filter[center.find_blocks[<[matcher]>].within[0.5].any]>
                                        # Sort other block list in order of proximity
                                        - define other_supporting_blocks <[other_supporting_blocks].sort[tntrun_sortBlocks].context[<[block_under]>]>
                                        # Remove nearest block
                                        - if <[other_supporting_blocks].any>:
                                            - define remove_block_queue:->:<[other_supporting_blocks].first>
                                            # Another layer?
                                            - define remove_block_queue:|:<[other_supporting_blocks].first.add[0.5,-0.5,0.5].find_blocks[<[matcher]>].within[0.5]>

                                - define playing <server.online_players_flagged[tntrun.<[arenaName]>.playing]>
                                - wait 3t

                            # We have a winner
                            - flag server tntrun_arenas.<[arenaName]>.status:resetting
                            - if <server.online_players_flagged[tntrun.<[arenaName]>.playing].size> == 1:
                                - define winner <server.online_players_flagged[tntrun.<[arenaName]>.playing].get[1]>
                                - title title:<green>WINNER! fade_in:0 stay:2 fade_out:1 targets:<[winner]>
                                - sidebar set "title:<bold>TNT Run" "values:|<green><[winner].name><white> is the winner!||<[sidebar_footer]>" players:<server.online_players_flagged[tntrun.<[arenaName]>]>
                                - repeat 5:
                                    - firework <[winner].location> star primary:yellow fade:white
                                    - firework <[winner].location> star primary:red fade:white
                                    - firework <[winner].location> star primary:green fade:blue
                                    - wait 1s
                                - wait 2s
                            - run tntrun_resetArena def:<[arenaName]>

                        - else:
                            # In countdown / waiting for players phase
                            - if <[playerCount]> < <script[tntrun_config].data_key[min_players]>:
                                - define need_players <script[tntrun_config].data_key[min_players].sub[<[playerCount]>]>
                                - if <[need_players]> == 1:
                                    - define players player
                                - else:
                                    - define players players
                                - define status "Need <[need_players]> more <[players]>..."
                                - flag server tntrun_arenas.<[arenaName]>.timer:<script[tntrun_config].data_key[countdown_time]>
                            - else:
                                - define status "Starting in: <green><[arena].get[timer]>"
                                - if <[arena].get[timer]> <= 5:
                                    - title title:<[status]> fade_in:0 stay:1 fade_out:1 targets:<[playersList]>
                                - flag server tntrun_arenas.<[arenaName]>.timer:--
                            - sidebar set "title:<bold>TNT Run" "values:|Players waiting: <green><[playerCount]>||<[status]>||<[sidebar_footer]>" players:<[playersList]>

                    - case playing:
                        - define stillActive true
                        # Play mode
                        - define playersList <server.online_players_flagged[tntrun.<[arenaName]>]>
                        - define playerCount <server.online_players_flagged[tntrun.<[arenaName]>.playing].size>
                        - flag server tntrun_arenas.<[arenaName]>.timer:++
                        - define playtime <server.flag[tntrun_arenas.<[arenaName]>.timer]>
                        - define playtime <[playtime].div[60].round_down>:<[playtime].mod[60].pad_left[2].with[0]>
                        - sidebar set "title:<bold>TNT Run" "values:|Play time: <gold><[playtime]>|Players remaining: <green><[playerCount]>||<[sidebar_footer]>" players:<[playersList]>

            # Do we still need to run the secondly event?
            - if !<[stillActive]>:
                - flag server tntrun_active:!


tntrun_events:
    type: world
    debug: false

    events:
        on server prestart:
            # Set default permissions
            - adjust server register_permission:<map[name=custom.tntrun.join;default=true]>
            - adjust server register_permission:<map[name=custom.tntrun.help;default=true]>
        after server start:
            - if !<server.has_flag[tntrun_arenas]>:
                - flag server tntrun_arenas:<map>
            - foreach <server.players_flagged[tntrun]> as:__player:
                - flag <player> tntrun:!
            # Check if any arenas need resetting (if active when server shut down)
            - foreach <server.flag[tntrun_arenas]> key:arenaName as:arena:
                - if <[arena].get[status]> == queued || <[arena].get[status]> == playing:
                    - run tntrun_resetArena def:<[arenaName]>
            - flag server tntrun_active:!
        after reload scripts:
            # First time initialisation after install
            - if !<server.has_flag[tntrun_arenas]>:
                - flag server tntrun_arenas:<map>

        after player changes world flagged:tntrun:
            - flag <player> tntrun:!
            - sidebar remove
        on player quits flagged:tntrun:
            - flag <player> tntrun:!
            - sidebar remove

        on player exits tntrun_arena_* flagged:tntrun:
            # Ignore the event if the player is changing worlds
            - if <context.cause> == WORLD_CHANGE:
                - stop
            - define arenaName <context.area.note_name.substring[14]>
            - if <player.has_flag[tntrun.<[arenaName]>.queued]>:
                # Put queued players back in lobby
                - teleport <server.flag[tntrun_arenas.<[arenaName]>.lobby].random.center>
            - else if <player.has_flag[tntrun.<[arenaName]>.playing]>:
                # Player has fallen out of bottom
                - adjust <player> gamemode:spectator
                - run tntrun_setPlayerStatus def:<[arenaName]>|spectating
                - teleport <server.flag[tntrun_arenas.<[arenaName]>.lobby].random.center>
                - inventory set o:<script[tntrun_config].data_key[exit_item]>[display=<red>Leave] slot:9
            - else:
                # Teleport spectating players back into area
                - wait 1t
                - teleport <context.from>

        on player drops item flagged:tntrun:
            - determine cancelled
        on player clicks in inventory flagged:tntrun:
            - determine cancelled
        on player swaps items flagged:tntrun:
            - determine cancelled
        on player damaged flagged:tntrun:
            - determine cancelled

        on player right clicks block flagged:tntrun:
            - if <context.item.material.name> == <script[tntrun_config].data_key[exit_item]>:
                - foreach <server.flag[tntrun_arenas]> key:arenaName as:arena:
                    - if <player.has_flag[tntrun.<[arenaName]>.queued]> || <player.has_flag[tntrun.<[arenaName]>.spectating]>:
                        - flag <player> tntrun:!
                        - sidebar remove
                        - teleport endgame_lobby
                        - foreach stop

        # Players shouldn't die, but just in case
        after player respawns flagged:tntrun:
            - flag <player> tntrun:!
            - sidebar remove
            - teleport endgame_lobby
