Monsters spawning in waves: script refactor and optimization

Hello, I made this as the first feature of a game. It’s messy and needs a refactor.

What is it?
This script manages a wave system where you enter a part in workspace, wait for the countdown to end, then all players in said part get teleported to a map. Then the script spawns some monsters, when all monsters die you can touch the next wave part to proceed.

I want to improve the next_wave function and make the code more independent. Also there’s lots of for loops that check all players because I can’t put those in a single spot. Would love to see a smart way of optimizing that.

I tried refactoring a portion of the code as it had an awful attribute system before I knew how to set table attributes and also turned an if to a guard clause. As well as some more small improvements.

New features to keep in mind:

  • At the end of every 5th wave the player can choose a buff. The server randomly rolls a table of items, sends it to the client, the client selects one of the items.
  • Calculate total wave difficulty depending on the current wave and change every monster to have its own difficulty multiplier. Currently I do manual changes of monsters, their coin and xp reward attributes, damage, walk speed, monster spawn rate, etc. This lets me replace all attributes with calculation functions for every removed attribute that’d depend on the wave difficulty and monster difficulty.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")

local run_each_x_seconds = ServerScriptService:WaitForChild("Run_Each_X_Seconds")
local wave_parties = script:WaitForChild("Wave_Parties") -- every time a new party is made copy the instance from below inside here
local default_wave_settings = script:WaitForChild("Default_Wave_Settings")

local monsters = ServerStorage:WaitForChild("Monsters") -- the place I copy monsters from
local lobby = workspace:WaitForChild("Lobby")
local wave_party_trigger = lobby:WaitForChild("Wave_Party_Trigger") -- if players are inside this part it'll create a new wave party when the cooldown ends
local party_trigger_cooldown_display = lobby:WaitForChild("Party_Trigger_Cooldown_Display")

local wave_settings = require(script:WaitForChild("Wave_Settings"))
local world_maps_names = require(script:WaitForChild("World_Maps"))
local script_library = require(ServerStorage:WaitForChild("Script_Library"))

local players_to_put_in_next_party = {} -- since multiple worlds can exist change this to be per trigger part

local default_seconds_until_start = 1 -- how long you wait for party to start, short for testing
local next_monster_id = 1 -- idk honestly, trying to serialize monsters for some reason



local function next_wave(current_wave_party, current_world)
  current_wave_party:SetAttribute("Wave_Number", current_wave_party:GetAttribute("Wave_Number") + 1)
  current_wave_party:SetAttribute("Wave_Start_Time", os.clock())
  wave_settings.set_wave_settings(current_wave_party)
  
  current_world:WaitForChild("Door_UI"):WaitForChild("Wave_Number"):WaitForChild("Surface_Gui"):WaitForChild("Text_Label").Text = current_wave_party:GetAttribute("Wave_Number") + 1
  
  local function tp_player_to_map(player_in_party)
    local character = player_in_party.Character
    if character == nil then return end
    local humanoid_root_part = character:WaitForChild("HumanoidRootPart")
    if humanoid_root_part == nil then return end
    humanoid_root_part.CFrame = current_world:WaitForChild("Player_Spawn"):WaitForChild("Spawn_Location").CFrame * CFrame.new(0, 5, 0)
    
    local temporary_y_position = humanoid_root_part.Position.Y
    task.wait(3) -- attempt to fix falling under map since laggy players receive the replicated map later
    if temporary_y_position - humanoid_root_part.Position.Y > 5 then
      humanoid_root_part.CFrame = current_world:WaitForChild("Player_Spawn"):WaitForChild("Spawn_Location").CFrame * CFrame.new(0, 5, 0)
    end
  end
  
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  
  if #players_in_party == 0 then
    current_wave_party:Destroy()
    current_world:Destroy()
    return
  end
  
  for _, player_in_party in players_in_party do
    local tp_player_to_map_coroutine = coroutine.create(tp_player_to_map)
    coroutine.resume(tp_player_to_map_coroutine, player_in_party)
    
    player_in_party.Character:WaitForChild("Humanoid").Health = player_in_party.Character:WaitForChild("Humanoid").MaxHealth -- heal when new wave starts
    script_library.update_wave_ui(player_in_party, true, current_wave_party:GetAttribute("Wave_Number"), 0)
  end
  
  local function spawn_monsters()
    local spawned_monsters = 0
    local killed_monsters = 0
    
    local function spawn_monster()
      local current_monster = monsters:WaitForChild("World_".. table.find(world_maps_names, current_world.Name)):WaitForChild("Rig"):Clone()
      -- change this to be configurable from wave settings
      
      current_monster:PivotTo(current_world:WaitForChild("Monster_Spawn"):WaitForChild("Spawn_Location").CFrame * CFrame.new(0, 5, 0))
      
      local function set_monster_id()
        current_monster:SetAttribute("MonsterId", next_monster_id)
        next_monster_id += 1
        if next_monster_id >= 1000000 then
          next_monster_id = 1
          warn("next_monster_id reached 1000000 and was reset to 1")
        end
      end
      set_monster_id()
      
      current_monster.Parent = workspace
      
      local monster_sounds = {
        npc_dead = monsters:WaitForChild("NPC_Dead"):Clone(), -- shouldn't be like this, got inspired by another system having multiple sounds
      }
      for _, monster_sound in monster_sounds do
        monster_sound.Parent = current_monster
      end
      
      local humanoid_billboard = monsters:WaitForChild("Humanoid_Billboard"):Clone()
      humanoid_billboard.Enabled = true
      humanoid_billboard.Parent = current_monster
      
      local monster_script = monsters:WaitForChild("Monster_Script"):Clone()
      monster_script.Parent = current_monster
      monster_script.Enabled = true
      
      spawned_monsters += 1
      
      -- when the spawned monster gets killed make it count as dead
      local humanoid = current_monster:WaitForChild("Humanoid")
      humanoid.Died:Once(function()
        killed_monsters += 1
        
        -- play sound
        monster_sounds.npc_dead:Stop()
        monster_sounds.npc_dead:Play()
        
        -- update the ui of all players in party
        local players_in_party = script_library.check_for_players_in_party(current_wave_party)
        
        if #players_in_party == 0 then
          current_wave_party:Destroy()
          current_world:Destroy()
        end
        
        for _, player_in_party in players_in_party do
          script_library.update_wave_ui(player_in_party, true, current_wave_party:GetAttribute("Wave_Number"), killed_monsters / current_wave_party:GetAttribute("Amount_Of_Monsters"))
        end
        
        if killed_monsters < current_wave_party:GetAttribute("Amount_Of_Monsters") then return end
        local next_wave_part = current_world:FindFirstChild("Next_Wave")
        if next_wave_part == nil then return end
        
        -- setup next wave if all monsters are killed
        for _, player_in_party in players_in_party do
          ReplicatedStorage:WaitForChild("Remotes"):WaitForChild("Add_Popup"):FireClient(player_in_party, "Completed wave ".. tostring(current_wave_party:GetAttribute("Wave_Number")), "alert")
        end
        
        local next_wave_part_touched = nil
        local triggered_next_wave = false
        next_wave_part_touched = next_wave_part.Touched:Connect(function(touched_part) -- could do :Once and reconnect if touch was invalid
          -- teleport whole party if touched is player from party
          local touched_player = game:GetService("Players"):GetPlayerFromCharacter(touched_part.Parent)
          if touched_player == nil then return end
          
          for _, player_in_party in players_in_party do
            if player_in_party ~= touched_player then continue end
            next_wave_part_touched:Disconnect()
            if triggered_next_wave == true then return end
            -- this runs only once
            triggered_next_wave = true
            
            -- send all clients the perks they rolled and wait until all of them choose a perk
            if current_wave_party:GetAttribute("Wave_Number") % 5 == 0 then
              ReplicatedStorage:WaitForChild("Remotes"):WaitForChild("Add_Popup"):FireClient(player_in_party, "Select a perk", "alert")
              -- connect to the selected perk event and wait until it gets fired
              -- when that happens, add the selected perk to the player folder in the party, update player stats affected by the perk
              -- flaw: this way only one player can choose a perk at a time
            end
            
            local next_wave_coroutine = coroutine.create(next_wave)
            coroutine.resume(next_wave_coroutine, current_wave_party, current_world)
            break
          end
        end)
      end)
    end
    
    -- while spawned monsters are less than total
    while spawned_monsters < current_wave_party:GetAttribute("Amount_Of_Monsters") do
      task.wait(3)
      for i = 1, current_wave_party:GetAttribute("Monsters_Spawned_Per_Loop") do
        if spawned_monsters >= current_wave_party:GetAttribute("Amount_Of_Monsters") then break end
        spawn_monster()
      end
    end
  end
  
  local spawn_monsters_coroutine = coroutine.create(spawn_monsters)
  coroutine.resume(spawn_monsters_coroutine)
end

local function update_party_trigger_cooldown_display()
  party_trigger_cooldown_display.SurfaceGui.TextLabel.Text = "Start in: ".. wave_party_trigger:GetAttribute("Seconds_Until_Start").. " sec"
end

local function remove_player_from_party(current_wave_party, user_id, current_world)
  current_wave_party[tostring(user_id)]:Destroy()
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  
  if #players_in_party == 0 then
    current_wave_party:Destroy()
    current_world:Destroy()
  end
end

local function create_wave_party(players_to_put_in_next_party_copy)
  if players_to_put_in_next_party_copy == {} then return end
  
  local current_wave_party = default_wave_settings:Clone()
  current_wave_party.Name = "Wave_Party_".. tostring(#wave_parties:GetChildren() + 1) -- could repeat names if a party is removed and new one appears
  current_wave_party.Parent = wave_parties
  
  local function create_world()
    local local_current_world = ServerStorage:WaitForChild("World_Maps"):WaitForChild(world_maps_names[current_wave_party:GetAttribute("World_Index")]):Clone()
    
    local_current_world:PivotTo(CFrame.new(Vector3.new(-300 + (200 * tonumber(string.gsub(current_wave_party.Name, "Wave_Party_", ""), 10)), 100, -300)))
    local_current_world.Parent = workspace
    
    while script_library.is_in_instance(local_current_world, workspace) == false do
      task.wait(0.5)
    end
    
    return local_current_world
  end
  
  local current_world = create_world()
  
  for player_index, player in players_to_put_in_next_party_copy do
    local player_folder = Instance.new("Folder") -- this is where I'll be storing perk data
    player_folder.Name = player.UserId
    player_folder.Parent = current_wave_party
    
    ServerScriptService:WaitForChild("Data_Store"):WaitForChild("Data_Update_Binds"):WaitForChild("Update_Weapons"):Fire(player)
    
    player.Character:WaitForChild("Humanoid").Died:Once(function() -- TODO: check if not removing player if they leave makes a problem
      remove_player_from_party(current_wave_party, player.UserId, current_world)
    end)
  end
  
  next_wave(current_wave_party, current_world)
end

local function add_player_to_next_party(player)
  if player == nil then return end
  if table.find(players_to_put_in_next_party, player) ~= nil then return end -- if player has already been detected
  table.insert(players_to_put_in_next_party, player)
end

local function update_wave_party()
  players_to_put_in_next_party = {}
  for _, part_in_wave_party_trigger in workspace:GetPartsInPart(wave_party_trigger, OverlapParams.new()) do
    add_player_to_next_party(game:GetService("Players"):GetPlayerFromCharacter(part_in_wave_party_trigger.Parent))
  end
  
  if #players_to_put_in_next_party > 0 then
    if wave_party_trigger:GetAttribute("Seconds_Until_Start") == -1 then
      wave_party_trigger:SetAttribute("Seconds_Until_Start", default_seconds_until_start)
    elseif wave_party_trigger:GetAttribute("Seconds_Until_Start") > 0 then
      wave_party_trigger:SetAttribute("Seconds_Until_Start", wave_party_trigger:GetAttribute("Seconds_Until_Start") - 1)
    elseif wave_party_trigger:GetAttribute("Seconds_Until_Start") == 0 then
      wave_party_trigger:SetAttribute("Seconds_Until_Start", -1)
      create_wave_party(players_to_put_in_next_party)
      players_to_put_in_next_party = {}
    end
  elseif #players_to_put_in_next_party == 0 then
    wave_party_trigger:SetAttribute("Seconds_Until_Start", -1)
  end
end

local function run_each_1_second()
  update_wave_party()
end

local function update_wave_ui(player)
  script_library.update_wave_ui(player, false, 0, 0)
end



wave_party_trigger:GetAttributeChangedSignal("Seconds_Until_Start"):Connect(update_party_trigger_cooldown_display)

run_each_x_seconds:WaitForChild("1").Event:Connect(run_each_1_second)
ServerScriptService:WaitForChild("Player_Event_Detection"):WaitForChild("Player_Character_Died").Event:Connect(update_wave_ui)

Here’s how the explorer looks during runtime:

1 Like

Here are some ways tol fix your code.

  1. Break the next_wave function into smaller functions:
  • teleport_players_to_map
  • update_wave_ui
  • spawn_monster
  • set_monster_id
  • on_monster_death
  • check_wave_completion
  • handle_wave_completion

This will make the code more organized and easier to understand and debug.

  1. Instead of using a for loop to check all players every time, consider using a function that detects if a player enters or leaves the wave party trigger part. This function can update the list of players in the party and avoid having to check all players every time.
  2. For the new feature that lets players choose a buff at the end of every 5th wave, you can create a table that holds the different buffs and their effects. When it’s time to choose a buff, you can randomly select one from the table and send it to the client. Once the client chooses a buff, you can update the player stats affected by the buff.
  3. For the new feature that calculates the total wave difficulty and changes every monster to have its own difficulty multiplier, you can create a function that calculates the difficulty based on the current wave and returns the multiplier. This function can then be used to set the difficulty of each monster when it’s spawned.
  4. Instead of using global variables, consider using local variables and passing them as arguments to functions. This will make the code more modular and easier to reuse.
  5. Finally, make sure to use meaningful variable and function names, and add comments to explain the purpose of the code blocks. This will make it easier for you and other developers to understand and maintain the code in the future.

A good book that teaches these concept is

2 Likes

You’re giving me an amazing lead, exactly what I wanted! The book also seems very cool and promising, definitely made by people in the know and it gives a very good lead on what I should research.

1 Like

I have multiple articles on system design and clean code practices if you are interested in giving them a read.

1 Like

I absolutely love your posts and the fact that you’re spending time educating us here. Barely even read your first post and it already reminds me of this one.

This is the main reason I have a huge respect for the Factorio dev team. They not only do it right, but they also kinda help you get an idea of how to do it right. I’ve came to the conclusion that it’s not possible to have the same productivity from programmers as from asset creators (anybody who makes standalone assets, even programmers who code said asset) because the programmers who aren’t asset creators need to connect everything together. I like imagining it as every script is a web and every line of code is a string. Goes well with “spiders are the only web developers that enjoy finding bugs” :smiley:

I’ve never watched the linked “Uncle Bob” videos in the blog post I sent, but apparently there’s a great connection between that and the book you sent!

A bit of a useless update, I’ve been refactoring the code and it has a bit of a bug (which I’ll find and remove), but here’s how it is now so it can be compared to the prior version.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")

local remotes = ReplicatedStorage:WaitForChild("Remotes")
local run_each_x_seconds = ServerScriptService:WaitForChild("Run_Each_X_Seconds")
local wave_parties = script:WaitForChild("Wave_Parties")
local default_wave_settings = script:WaitForChild("Default_Wave_Settings")
local to_next_wave = script:WaitForChild("To_Next_Wave")

local monsters = ServerStorage:WaitForChild("Monsters")
local lobby = workspace:WaitForChild("Lobby")
local wave_party_trigger = lobby:WaitForChild("Wave_Party_Trigger")
local party_trigger_cooldown_display = lobby:WaitForChild("Party_Trigger_Cooldown_Display")

local wave_settings = require(script:WaitForChild("Wave_Settings"))
local world_maps_names = require(script:WaitForChild("World_Maps"))
local abstract_library = require(ServerStorage:WaitForChild("Abstract_Library"))
local script_library = require(ServerStorage:WaitForChild("Script_Library"))
local replicated_library = require(ReplicatedStorage:WaitForChild("Replicated_Library"))

local monster_sounds_table = {
  monsters:WaitForChild("NPC_Dead")
}

local players_to_put_in_next_party = {}

local default_seconds_until_start = 1
local next_monster_id = 1



local function tp_player_to_map(player_in_party, current_world)
  local character = player_in_party.Character
  if character == nil then return end
  local humanoid_root_part = character:WaitForChild("HumanoidRootPart")
  if humanoid_root_part == nil then return end
  humanoid_root_part.CFrame = current_world:WaitForChild("Player_Spawn"):WaitForChild("Spawn_Location").CFrame * CFrame.new(0, 5, 0)
  
  local temporary_y_position = humanoid_root_part.Position.Y
  task.wait(3) -- attempt to fix falling under map
  if temporary_y_position - humanoid_root_part.Position.Y > 5 then
    humanoid_root_part.CFrame = current_world:WaitForChild("Player_Spawn"):WaitForChild("Spawn_Location").CFrame * CFrame.new(0, 5, 0)
  end
end

local function set_monster_id(current_monster)
  current_monster:SetAttribute("MonsterId", next_monster_id)
  next_monster_id += 1
  if next_monster_id >= 1000000 then
    next_monster_id = 1
    warn("next_monster_id reached 1000000 and was reset to 1")
  end
end

local function roll_x_perks(amount_to_roll, current_perks) -- not allowed perks: repeats, max level
  local rolled_perks = {}
  local perks_in_table = 0
  
  local current_attempts = 0
  local max_attempts = 10 * amount_to_roll
  
  while perks_in_table < amount_to_roll and current_attempts < max_attempts do
    current_attempts += 1
    
    local rolled_rarity = script_library.choose_random_key(script_library.perk_chances.rarity_chances)
    local perks_of_rarity = script_library.perk_chances.rarities[rolled_rarity]
    local rolled_perk = perks_of_rarity[math.random(1, #perks_of_rarity)]
    
    local function is_perk_allowed()
      return (rolled_perks[rolled_perk] == nil and (current_perks[rolled_perk] or 0) < replicated_library.max_perk_level)
    end
    
    if is_perk_allowed() then -- if allowed add to table
      perks_in_table += 1
      rolled_perks[rolled_perk] = (current_perks[rolled_perk] or 0) + 1
    end
  end
  
  return rolled_perks
end

local function does_wave_give_perk_choice(current_wave_party)
  return current_wave_party:GetAttribute("Wave_Number") % script_library.waves_between_perk_choices == 0
end

local function have_all_players_selected_perks(current_wave_party, players_who_selected_perks)
  local have_all_players_selected = true -- if one hasn't it's easy to set this to false
  local has_one_player_selected = false -- if one has it's easy to set this to true
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  
  for _, player_in_party in players_in_party do
    if players_who_selected_perks[player_in_party.UserId] == true then
      has_one_player_selected = true -- if one player has selected
    elseif players_who_selected_perks[player_in_party.UserId] == false then
      have_all_players_selected = false -- if one player hasn't selected
    end
  end
  
  return have_all_players_selected, has_one_player_selected
end

local function yield_for_players_who_havent_selected_perks(current_wave_party, players_who_selected_perks)
  if does_wave_give_perk_choice(current_wave_party) == true then -- every nth wave
    local failed_attempts = 0
    while failed_attempts < 15 do
      local have_all_players_selected, has_one_player_selected = have_all_players_selected_perks(current_wave_party, players_who_selected_perks)
      if have_all_players_selected == true then break end
      if have_all_players_selected == false and has_one_player_selected == true then
        task.wait(5)
        break
      end
      
      failed_attempts += 1
      task.wait(2)
    end
  end
end

local function destroy_party(current_wave_party, current_world)
  current_wave_party:Destroy()
  current_world:Destroy()
end

local function setup_party_attributes_for_new_wave(current_wave_party)
  current_wave_party:SetAttribute("Wave_Number", current_wave_party:GetAttribute("Wave_Number") + 1)
  current_wave_party:SetAttribute("Wave_Start_Time", os.clock())
  current_wave_party:SetAttribute("Spawned_Monsters_This_Wave", 0)
  current_wave_party:SetAttribute("Killed_Monsters_This_Wave", 0)
  wave_settings.set_wave_settings(current_wave_party)
end

local function update_world_door_ui(current_wave_party, current_world)
  current_world:WaitForChild("Door_UI"):WaitForChild("Wave_Number"):WaitForChild("Surface_Gui"):WaitForChild("Text_Label").Text = current_wave_party:GetAttribute("Wave_Number") + 1
end

local function setup_each_player_for_wave_beginning(current_wave_party, current_world)
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  
  for _, player_in_party in players_in_party do
    local tp_player_to_map_coroutine = coroutine.create(tp_player_to_map)
    coroutine.resume(tp_player_to_map_coroutine, player_in_party, current_world)
    
    player_in_party.Character:WaitForChild("Humanoid").Health = player_in_party.Character:WaitForChild("Humanoid").MaxHealth
    
    local is_wave_ui_visible = true
    local wave_progress = 0
    script_library.update_wave_ui(player_in_party, is_wave_ui_visible, current_wave_party:GetAttribute("Wave_Number"), wave_progress)
  end
end

local function add_sounds_to_monster(current_monster)
  for _, monster_sound in monster_sounds_table do
    local monster_sound_clone = monster_sound:Clone()
    monster_sound_clone.Parent = current_monster
  end
end

local function add_humanoid_billboard_to_monster(current_monster)
  local humanoid_billboard = monsters:WaitForChild("Humanoid_Billboard"):Clone()
  humanoid_billboard.Enabled = true
  humanoid_billboard.Parent = current_monster
end

local function add_monster_script_to_monster(current_monster)
  local monster_script = monsters:WaitForChild("Monster_Script"):Clone()
  monster_script.Parent = current_monster
  monster_script.Enabled = true
end

local function play_monster_dead_sound(current_monster)
  current_monster.NPC_Dead:Stop()
  current_monster.NPC_Dead:Play()
end

local function update_player_wave_ui(player, current_wave_party)
  local is_wave_ui_visible = true
  local wave_progress = current_wave_party:GetAttribute("Killed_Monsters_This_Wave") / current_wave_party:GetAttribute("Amount_Of_Monsters")
  script_library.update_wave_ui(player, is_wave_ui_visible, current_wave_party:GetAttribute("Wave_Number"), wave_progress)
end

local function update_all_players_wave_ui(current_wave_party)
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  for _, player_in_party in players_in_party do
    update_player_wave_ui(player_in_party, current_wave_party)
  end
end

local function are_all_monsters_killed(current_wave_party)
  return current_wave_party:GetAttribute("Killed_Monsters_This_Wave") >= current_wave_party:GetAttribute("Amount_Of_Monsters")
end

local function connect_when_monster_dies(current_monster, current_wave_party, current_world)
  local humanoid = current_monster:WaitForChild("Humanoid")
  humanoid.Died:Once(function()
    abstract_library.add_to_attribute(current_wave_party, "Killed_Monsters_This_Wave", 1)
    play_monster_dead_sound(current_monster)
    update_all_players_wave_ui(current_wave_party)
    
    if not are_all_monsters_killed(current_wave_party) then return end
    local next_wave_part = current_world:FindFirstChild("Next_Wave")
    if next_wave_part == nil then return end
    
    -- setup next wave if all monsters are killed
    local players_who_selected_perks = {} -- key: player_id, val: bool
    local players_in_party = script_library.check_for_players_in_party(current_wave_party)
    for _, player_in_party in players_in_party do
      remotes:WaitForChild("Add_Popup"):FireClient(player_in_party, "Completed wave ".. tostring(current_wave_party:GetAttribute("Wave_Number")), "alert")
      
      if does_wave_give_perk_choice(current_wave_party) == true then -- every nth wave
        players_who_selected_perks[player_in_party.UserId] = false
        
        local current_perks = abstract_library.get_table_attribute(current_wave_party[player_in_party.UserId], "Perks")
        local rolled_perks = roll_x_perks(3, current_perks)
        
        local select_perk_event = nil
        local function select_perk(player_who_requested, selected_perk)
          if player_who_requested ~= player_in_party then
            select_perk_event = remotes:WaitForChild("Request_Perk_Select").OnServerEvent:Once(select_perk)
            return -- if request isn't from the same player
          end
          
          if rolled_perks[selected_perk] == nil then
            script_library.send_player_message_to_discord(player_who_requested, "tried to select a perk that wasn't rolled", 2)
            return
          end
          
          -- if anticheat passed, give player the perk
          players_who_selected_perks[player_in_party.UserId] = true
          
          current_perks = abstract_library.get_table_attribute(current_wave_party[player_who_requested.UserId], "Perks")
          --print(current_perks)
          current_perks[selected_perk] = (current_perks[selected_perk] or 0) + 1
          --print(current_perks)
          abstract_library.set_table_attribute(current_wave_party[player_who_requested.UserId], "Perks", current_perks)
          
          ServerScriptService:WaitForChild("Data_Store"):WaitForChild("Data_Update_Binds"):WaitForChild("Update_Perk_Effects"):Fire(player_who_requested)
        end
        
        select_perk_event = remotes:WaitForChild("Request_Perk_Select").OnServerEvent:Once(select_perk) -- client fires this with the selected perk
        
        local rolled_perks_with_reward = {}
        for perk_name, perk_level in rolled_perks do
          local reward_for_perk = script_library.calculate_perk_reward[perk_name]({perk_name = perk_level})
          rolled_perks_with_reward[perk_name] = {["perk_level"] = perk_level, ["perk_reward"] = reward_for_perk}
        end
        print(rolled_perks)
        print(rolled_perks_with_reward)
        remotes:WaitForChild("Select_Perk"):FireClient(player_in_party, rolled_perks_with_reward) -- client receives perks to select, TODO: make client accept this
        -- connect to the selected perk event and wait until it gets fired
        -- when that happens, add the selected perk to the player folder in the party, update player stats affected by the perk
        -- flaw: this way only one player can choose a perk at a time
      end
    end
    
    yield_for_players_who_havent_selected_perks(current_wave_party, players_who_selected_perks)
    
    local is_next_wave_already_triggered = false
    
    local function touched_next_wave_part(touched_part) -- when something touches the next wave part
      local touched_player = abstract_library.get_player_from_touched_part(touched_part)
      if touched_player == nil then
        next_wave_part.Touched:Once(touched_next_wave_part)
        return
      end
      
      players_in_party = script_library.check_for_players_in_party(current_wave_party)
      for _, player_in_party in players_in_party do
        if is_next_wave_already_triggered == true then return end
        if player_in_party ~= touched_player then continue end
        is_next_wave_already_triggered = true
        
        to_next_wave:Fire(current_wave_party, current_world)
        return
      end
    end
    
    next_wave_part.Touched:Once(touched_next_wave_part)
  end)
end

local function spawn_monster(current_wave_party, current_world)
  abstract_library.add_to_attribute(current_wave_party, "Spawned_Monsters_This_Wave", 1)
  
  local current_monster = monsters:WaitForChild("World_".. table.find(world_maps_names, current_world.Name)):WaitForChild("Rig"):Clone()
  -- change this to be configurable from wave settings
  current_monster:PivotTo(current_world:WaitForChild("Monster_Spawn"):WaitForChild("Spawn_Location").CFrame * CFrame.new(0, 5, 0))
  current_monster.Parent = workspace
  set_monster_id(current_monster)
  add_sounds_to_monster(current_monster)
  add_humanoid_billboard_to_monster(current_monster)
  add_monster_script_to_monster(current_monster)
  
  connect_when_monster_dies(current_monster, current_wave_party, current_world)
end

local function did_spawn_all_monsters(current_wave_party)
  return current_wave_party:GetAttribute("Spawned_Monsters_This_Wave") >= current_wave_party:GetAttribute("Amount_Of_Monsters")
end

local function spawn_monsters(current_wave_party, current_world)
  while not did_spawn_all_monsters(current_wave_party) do
    for i = 1, current_wave_party:GetAttribute("Monsters_Spawned_Per_Loop") do
      spawn_monster(current_wave_party, current_world)
    end
    task.wait(2)
  end
end

local function next_wave(current_wave_party: Folder, current_world: Model)
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  if #players_in_party == 0 then
    destroy_party(current_wave_party, current_world)
    return
  end
  
  setup_party_attributes_for_new_wave(current_wave_party)
  update_world_door_ui(current_wave_party, current_world)
  setup_each_player_for_wave_beginning(current_wave_party, current_world)
  
  local spawn_monsters_coroutine = coroutine.create(spawn_monsters)
  coroutine.resume(spawn_monsters_coroutine, current_wave_party, current_world)
end

local function update_party_trigger_cooldown_display()
  party_trigger_cooldown_display.SurfaceGui.TextLabel.Text = "Start in: ".. wave_party_trigger:GetAttribute("Seconds_Until_Start").. " sec"
end

local function remove_player_from_party(current_wave_party: Folder, user_id: number, current_world: Model)
  current_wave_party[tostring(user_id)]:Destroy()
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  
  if #players_in_party == 0 then
    destroy_party(current_wave_party, current_world)
  end
end

local function get_party_number(current_wave_party)
  local party_number = string.gsub(current_wave_party.Name, "Wave_Party_", "")
  return tonumber(party_number, 10)
end

local function has_world_loaded(current_world)
  return script_library.is_in_instance(current_world, workspace)
end

local function get_world_to_clone(current_wave_party)
  return ServerStorage:WaitForChild("World_Maps"):WaitForChild(world_maps_names[current_wave_party:GetAttribute("World_Index")])
end

local function set_world_position(current_wave_party, current_world)
  local world_x_offset = 200 * get_party_number(current_wave_party)
  local world_position = CFrame.new(Vector3.new(-300 + world_x_offset, 100, -300))
  current_world:PivotTo(world_position)
  current_world.Parent = workspace
end

local function create_world(current_wave_party)
  local current_world = get_world_to_clone(current_wave_party):Clone()
  set_world_position(current_wave_party, current_world)
  
  local while_attempts = 0
  local max_attempts = 10
  while has_world_loaded(current_world) == false and while_attempts < max_attempts do
    while_attempts += 1
    task.wait(0.5)
  end
  
  return current_world
end

local function create_player_party_folder(player, current_wave_party)
  local player_party_folder = Instance.new("Folder") -- this is where I'll be storing perk data
  player_party_folder.Name = player.UserId
  player_party_folder.Parent = current_wave_party
  return player_party_folder
end

local function setup_events_to_remove_player_from_party(player: Player, current_wave_party, current_world)
  player.Character:WaitForChild("Humanoid").Died:Once(function() -- TODO: check if not removing player if they leave makes a problem
    remove_player_from_party(current_wave_party, player.UserId, current_world)
  end)
end

local function create_wave_party_folder()
  local current_wave_party = default_wave_settings:Clone()
  current_wave_party.Name = "Wave_Party_".. tostring(#wave_parties:GetChildren() + 1) -- could repeat names if a party is removed and new one appears
  current_wave_party.Parent = wave_parties
  return current_wave_party
end

local function create_wave_party(players_to_put_in_next_party_copy)
  if players_to_put_in_next_party_copy == {} then return end
  
  local current_wave_party = create_wave_party_folder()
  local current_world = create_world(current_wave_party)
  
  for _, player in players_to_put_in_next_party_copy do
    local player_party_folder = create_player_party_folder(player, current_wave_party)
    abstract_library.set_table_attribute(player_party_folder, "Perks", {})
    ServerScriptService:WaitForChild("Data_Store"):WaitForChild("Data_Update_Binds"):WaitForChild("Update_Weapons"):Fire(player) -- TODO: the same for perk stat updates
    
    setup_events_to_remove_player_from_party(player, current_wave_party, current_world)
  end
  
  next_wave(current_wave_party, current_world)
end

local function add_player_to_next_party(player)
  if player == nil then return end
  if table.find(players_to_put_in_next_party, player) ~= nil then return end -- if player has already been detected
  table.insert(players_to_put_in_next_party, player)
end

local function update_wave_party()
  players_to_put_in_next_party = {}
  for _, part_in_wave_party_trigger in workspace:GetPartsInPart(wave_party_trigger, OverlapParams.new()) do
    add_player_to_next_party(game:GetService("Players"):GetPlayerFromCharacter(part_in_wave_party_trigger.Parent))
  end
  
  if #players_to_put_in_next_party > 0 then
    if wave_party_trigger:GetAttribute("Seconds_Until_Start") == -1 then
      wave_party_trigger:SetAttribute("Seconds_Until_Start", default_seconds_until_start)
    elseif wave_party_trigger:GetAttribute("Seconds_Until_Start") > 0 then
      wave_party_trigger:SetAttribute("Seconds_Until_Start", wave_party_trigger:GetAttribute("Seconds_Until_Start") - 1)
    elseif wave_party_trigger:GetAttribute("Seconds_Until_Start") == 0 then
      wave_party_trigger:SetAttribute("Seconds_Until_Start", -1)
      create_wave_party(players_to_put_in_next_party)
      players_to_put_in_next_party = {}
    end
  elseif #players_to_put_in_next_party == 0 then
    wave_party_trigger:SetAttribute("Seconds_Until_Start", -1)
  end
end

local function run_each_1_second()
  update_wave_party()
end

local function update_wave_ui(player)
  script_library.update_wave_ui(player, false, 0, 0)
end



wave_party_trigger:GetAttributeChangedSignal("Seconds_Until_Start"):Connect(update_party_trigger_cooldown_display)

run_each_x_seconds:WaitForChild("1").Event:Connect(run_each_1_second)
ServerScriptService:WaitForChild("Player_Event_Detection"):WaitForChild("Player_Character_Died").Event:Connect(update_wave_ui)

to_next_wave.Event:Connect(next_wave)
1 Like

Again useless update, but I reduced the big function to small functions and I think even though the script grew a lot it’s so much cleaner. I showing the difference allows the reader to better understand how powerful this cleanup is.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")

local remotes = ReplicatedStorage:WaitForChild("Remotes")
local run_each_x_seconds = ServerScriptService:WaitForChild("Run_Each_X_Seconds")
local wave_parties = script:WaitForChild("Wave_Parties")
local default_wave_settings = script:WaitForChild("Default_Wave_Settings")
local wave_events = script:WaitForChild("Wave_Events")

local monsters = ServerStorage:WaitForChild("Monsters")
local lobby = workspace:WaitForChild("Lobby")
local wave_party_trigger = lobby:WaitForChild("Wave_Party_Trigger")
local party_trigger_cooldown_display = lobby:WaitForChild("Party_Trigger_Cooldown_Display")

local wave_settings = require(script:WaitForChild("Wave_Settings"))
local world_maps_names = require(script:WaitForChild("World_Maps"))
local abstract_library = require(ServerStorage:WaitForChild("Abstract_Library"))
local script_library = require(ServerStorage:WaitForChild("Script_Library"))
local replicated_library = require(ReplicatedStorage:WaitForChild("Replicated_Library"))

local monster_sounds_table = {
  monsters:WaitForChild("NPC_Dead")
}

local players_to_put_in_next_party = {}

local default_seconds_until_start = 1
local next_monster_id = 1



local function tp_player_to_map(player_in_party, current_world)
  local character = player_in_party.Character
  if character == nil then return end
  local humanoid_root_part = character:WaitForChild("HumanoidRootPart")
  if humanoid_root_part == nil then return end
  humanoid_root_part.CFrame = current_world:WaitForChild("Player_Spawn"):WaitForChild("Spawn_Location").CFrame * CFrame.new(0, 5, 0)
  
  local temporary_y_position = humanoid_root_part.Position.Y
  task.wait(3) -- attempt to fix falling under map
  if temporary_y_position - humanoid_root_part.Position.Y > 5 then
    humanoid_root_part.CFrame = current_world:WaitForChild("Player_Spawn"):WaitForChild("Spawn_Location").CFrame * CFrame.new(0, 5, 0)
  end
end

local function set_monster_id(current_monster)
  current_monster:SetAttribute("Monster_Id", next_monster_id)
  next_monster_id += 1
  if next_monster_id >= 1000000 then
    next_monster_id = 1
    warn("next_monster_id reached 1000000 and was reset to 1")
  end
end

local function roll_x_perks(amount_to_roll, current_perks) -- not allowed perks: repeats, max level
  local rolled_perks = {}
  local perks_in_table = 0
  
  local current_attempts = 0
  local max_attempts = 10 * amount_to_roll
  
  while perks_in_table < amount_to_roll and current_attempts < max_attempts do
    current_attempts += 1
    
    local rolled_rarity = script_library.choose_random_key(script_library.perk_chances.rarity_chances)
    local perks_of_rarity = script_library.perk_chances.rarities[rolled_rarity]
    local rolled_perk = perks_of_rarity[math.random(1, #perks_of_rarity)]
    
    local function is_perk_allowed()
      return (rolled_perks[rolled_perk] == nil and (current_perks[rolled_perk] or 0) < replicated_library.max_perk_level)
    end
    
    if is_perk_allowed() then -- if allowed add to table
      perks_in_table += 1
      rolled_perks[rolled_perk] = (current_perks[rolled_perk] or 0) + 1
    end
  end
  
  return rolled_perks
end

local function does_wave_give_perk_choice(current_wave_party)
  return current_wave_party:GetAttribute("Wave_Number") % script_library.waves_between_perk_choices == 0
end

local function have_all_players_selected_perks(current_wave_party)
  local have_all_players_selected = true -- if one hasn't it's easy to set this to false
  local has_one_player_selected = false -- if one has it's easy to set this to true
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  local players_who_selected_perks = abstract_library.get_table_attribute(current_wave_party, "players_who_selected_perks")
  
  for _, player_in_party in players_in_party do
    if players_who_selected_perks[player_in_party.UserId] == true then
      has_one_player_selected = true -- if one player has selected
    elseif players_who_selected_perks[player_in_party.UserId] == false then
      have_all_players_selected = false -- if one player hasn't selected
    end
  end
  
  return have_all_players_selected, has_one_player_selected
end

local function yield_for_players_who_havent_selected_perks(current_wave_party)
  local select_timeout_for_all_players = 120
  local select_timeout_after_one_player = 15
  local when_yield_started = os.time()
  local when_first_player_selected_perk = math.huge
  while os.time() - when_yield_started < select_timeout_for_all_players and
    os.time() - when_first_player_selected_perk < select_timeout_after_one_player do
    -- could send wave update remotes
    local have_all_players_selected, has_one_player_selected = have_all_players_selected_perks(current_wave_party)
    if have_all_players_selected == true then break end
    if has_one_player_selected == true and when_first_player_selected_perk > os.time() then
      when_first_player_selected_perk = os.time()
    end
    task.wait(1)
  end
end

local function destroy_party(current_wave_party, current_world)
  current_wave_party:Destroy()
  current_world:Destroy()
end

local function setup_party_attributes_for_new_wave(current_wave_party)
  current_wave_party:SetAttribute("Wave_Number", current_wave_party:GetAttribute("Wave_Number") + 1)
  current_wave_party:SetAttribute("Wave_Start_Time", os.clock())
  current_wave_party:SetAttribute("Spawned_Monsters_This_Wave", 0)
  current_wave_party:SetAttribute("Killed_Monsters_This_Wave", 0)
  wave_settings.set_wave_settings(current_wave_party)
end

local function update_world_door_ui(current_wave_party, current_world)
  current_world:WaitForChild("Door_UI"):WaitForChild("Wave_Number"):WaitForChild("Surface_Gui"):WaitForChild("Text_Label").Text = current_wave_party:GetAttribute("Wave_Number") + 1
end

local function setup_each_player_for_wave_beginning(current_wave_party, current_world)
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  
  for _, player_in_party in players_in_party do
    local tp_player_to_map_coroutine = coroutine.create(tp_player_to_map)
    coroutine.resume(tp_player_to_map_coroutine, player_in_party, current_world)
    
    player_in_party.Character:WaitForChild("Humanoid").Health = player_in_party.Character:WaitForChild("Humanoid").MaxHealth
    
    local is_wave_ui_visible = true
    local wave_progress = 0
    script_library.update_wave_ui(player_in_party, is_wave_ui_visible, current_wave_party:GetAttribute("Wave_Number"), wave_progress)
  end
end

local function add_sounds_to_monster(current_monster)
  for _, monster_sound in monster_sounds_table do
    local monster_sound_clone = monster_sound:Clone()
    monster_sound_clone.Parent = current_monster
  end
end

local function add_humanoid_billboard_to_monster(current_monster)
  local humanoid_billboard = monsters:WaitForChild("Humanoid_Billboard"):Clone()
  humanoid_billboard.Enabled = true
  humanoid_billboard.Parent = current_monster
end

local function add_monster_script_to_monster(current_monster)
  local monster_script = monsters:WaitForChild("Monster_Script"):Clone()
  monster_script.Parent = current_monster
  monster_script.Enabled = true
end

local function play_monster_dead_sound(current_monster)
  current_monster.NPC_Dead:Stop()
  current_monster.NPC_Dead:Play()
end

local function update_player_wave_ui(player, current_wave_party)
  local is_wave_ui_visible = true
  local wave_progress = current_wave_party:GetAttribute("Killed_Monsters_This_Wave") / current_wave_party:GetAttribute("Amount_Of_Monsters")
  script_library.update_wave_ui(player, is_wave_ui_visible, current_wave_party:GetAttribute("Wave_Number"), wave_progress)
end

local function update_all_players_wave_ui(current_wave_party)
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  for _, player_in_party in players_in_party do
    update_player_wave_ui(player_in_party, current_wave_party)
  end
end

local function are_all_monsters_killed(current_wave_party)
  return current_wave_party:GetAttribute("Killed_Monsters_This_Wave") >= current_wave_party:GetAttribute("Amount_Of_Monsters")
end

local function did_next_wave_trigger_fail(touched_part, touched_player, next_wave_part, current_wave_party, current_world)
  if touched_player == nil then
    wave_events.Connect_Next_Wave_Trigger:Fire(next_wave_part, current_wave_party, current_world)
    return
  end
end

local function touched_next_wave_part(touched_part, next_wave_part, current_wave_party, current_world) -- when something touches the next wave part
  local touched_player = abstract_library.get_player_from_touched_part(touched_part)
  if did_next_wave_trigger_fail(touched_part, touched_player, next_wave_part, current_wave_party, current_world) == true then return end
  
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  for _, player_in_party in players_in_party do
    if player_in_party ~= touched_player then continue end
    wave_events.To_Next_Wave:Fire(current_wave_party, current_world)
    return
  end
end

local function connect_next_wave_trigger(next_wave_part, current_wave_party, current_world)
  next_wave_part.Touched:Once(function(touched_part)
    touched_next_wave_part(touched_part, next_wave_part, current_wave_party, current_world)
  end)
end

local function timeout_perk_selection(current_wave_party)
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  for _, player_in_party in players_in_party do
    remotes:WaitForChild("Select_Perk_Timeout"):FireClient(player_in_party)
  end
end

local function create_rolled_perks_with_reward(rolled_perks)
  local rolled_perks_with_reward = {}
  for perk_name, perk_level in rolled_perks do
    local reward_for_perk = script_library.calculate_perk_reward[perk_name](rolled_perks)
    rolled_perks_with_reward[perk_name] = {["perk_level"] = perk_level, ["perk_reward"] = reward_for_perk}
  end
  
  return rolled_perks_with_reward
end

local function reconnect_select_perk_request(player_in_party, rolled_perks, current_perks, current_wave_party)
  remotes:WaitForChild("Request_Perk_Select").OnServerEvent:Once(function(player, selected_perk) -- client fires this with the selected perk
    --select_perk(player, selected_perk)
    wave_events:WaitForChild("Reconnect_Select_Perk_Request"):Fire(player, selected_perk, player_in_party, rolled_perks, current_perks, current_wave_party)
  end)
end

local function select_perk(player_who_requested, selected_perk, player_in_party, rolled_perks, current_perks, current_wave_party)
  if player_who_requested ~= player_in_party then
    reconnect_select_perk_request(player_in_party, rolled_perks, current_perks, current_wave_party)
    return -- if request isn't from the same player
  end

  if rolled_perks[selected_perk] == nil then
    script_library.send_player_message_to_discord(player_who_requested, "tried to select a perk that wasn't rolled", 0)
    return
  end
  -- if anticheat passed, give player the perk
  local players_who_selected_perks = abstract_library.get_table_attribute(current_wave_party, "players_who_selected_perks")
  players_who_selected_perks[player_in_party.UserId] = true
  abstract_library.set_table_attribute(current_wave_party, "players_who_selected_perks", players_who_selected_perks)

  current_perks = abstract_library.get_table_attribute(current_wave_party[player_who_requested.UserId], "Perks")
  current_perks[selected_perk] = (current_perks[selected_perk] or 0) + 1
  abstract_library.set_table_attribute(current_wave_party[player_who_requested.UserId], "Perks", current_perks)

  ServerScriptService:WaitForChild("Data_Store"):WaitForChild("Data_Update_Binds"):WaitForChild("Update_Perk_Effects"):Fire(player_who_requested)
  remotes:WaitForChild("Update_Current_Perks"):FireClient(player_who_requested, current_perks)
end

local function let_player_select_perks(player_in_party, current_wave_party)
  local players_who_selected_perks = abstract_library.get_table_attribute(current_wave_party, "players_who_selected_perks")
  players_who_selected_perks[player_in_party.UserId] = false
  abstract_library.set_table_attribute(current_wave_party, "players_who_selected_perks", players_who_selected_perks)
  
  local current_perks = abstract_library.get_table_attribute(current_wave_party[player_in_party.UserId], "Perks")
  local rolled_perks = roll_x_perks(3, current_perks)
  
  reconnect_select_perk_request(player_in_party, rolled_perks, current_perks, current_wave_party)
  
  local rolled_perks_with_reward = create_rolled_perks_with_reward(rolled_perks)
  remotes:WaitForChild("Select_Perk"):FireClient(player_in_party, rolled_perks_with_reward)
end

local function setup_next_wave(next_wave_part, current_wave_party, current_world)
  abstract_library.set_table_attribute(current_wave_party, "players_who_selected_perks", {}) -- key: player_id, val: bool
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  for _, player_in_party in players_in_party do
    remotes:WaitForChild("Add_Popup"):FireClient(player_in_party, "Completed wave ".. tostring(current_wave_party:GetAttribute("Wave_Number")), "alert")
    
    if does_wave_give_perk_choice(current_wave_party) == true then
      let_player_select_perks(player_in_party, current_wave_party)
    end
  end
  
  if does_wave_give_perk_choice(current_wave_party) == true then
    yield_for_players_who_havent_selected_perks(current_wave_party)
    timeout_perk_selection(current_wave_party)
  end
  
  wave_events.Connect_Next_Wave_Trigger:Fire(next_wave_part, current_wave_party, current_world)
end

local function connect_when_monster_dies(current_monster, current_wave_party, current_world)
  local humanoid = current_monster:WaitForChild("Humanoid")
  humanoid.Died:Once(function()
    abstract_library.add_to_attribute(current_wave_party, "Killed_Monsters_This_Wave", 1)
    play_monster_dead_sound(current_monster)
    update_all_players_wave_ui(current_wave_party)
    
    if not are_all_monsters_killed(current_wave_party) then return end
    local next_wave_part = current_world:FindFirstChild("Next_Wave")
    if next_wave_part == nil then return end
    setup_next_wave(next_wave_part, current_wave_party, current_world)
  end)
end

local function spawn_monster(current_wave_party, current_world)
  abstract_library.add_to_attribute(current_wave_party, "Spawned_Monsters_This_Wave", 1)
  
  local current_monster = monsters:WaitForChild("World_".. table.find(world_maps_names, current_world.Name)):WaitForChild("Rig"):Clone()
  -- change this to be configurable from wave settings
  current_monster:PivotTo(current_world:WaitForChild("Monster_Spawn"):WaitForChild("Spawn_Location").CFrame * CFrame.new(0, 5, 0))
  current_monster.Parent = workspace
  set_monster_id(current_monster)
  add_sounds_to_monster(current_monster)
  add_humanoid_billboard_to_monster(current_monster)
  add_monster_script_to_monster(current_monster)
  
  connect_when_monster_dies(current_monster, current_wave_party, current_world)
end

local function did_spawn_all_monsters(current_wave_party)
  return current_wave_party:GetAttribute("Spawned_Monsters_This_Wave") >= current_wave_party:GetAttribute("Amount_Of_Monsters")
end

local function spawn_monsters(current_wave_party, current_world)
  while not did_spawn_all_monsters(current_wave_party) do
    for i = 1, current_wave_party:GetAttribute("Monsters_Spawned_Per_Loop") do
      spawn_monster(current_wave_party, current_world)
      if did_spawn_all_monsters(current_wave_party) then return end -- fixes spawning extra monsters
    end
    task.wait(2)
  end
end

local function next_wave(current_wave_party: Folder, current_world: Model)
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  if #players_in_party == 0 then
    destroy_party(current_wave_party, current_world)
    return
  end
  
  setup_party_attributes_for_new_wave(current_wave_party)
  update_world_door_ui(current_wave_party, current_world)
  setup_each_player_for_wave_beginning(current_wave_party, current_world)
  
  local spawn_monsters_coroutine = coroutine.create(spawn_monsters)
  coroutine.resume(spawn_monsters_coroutine, current_wave_party, current_world)
end

local function update_party_trigger_cooldown_display()
  party_trigger_cooldown_display.SurfaceGui.TextLabel.Text = "Start in: ".. wave_party_trigger:GetAttribute("Seconds_Until_Start").. " sec"
end

local function remove_player_from_party(current_wave_party: Folder, user_id: number, current_world: Model)
  current_wave_party[tostring(user_id)]:Destroy()
  local players_in_party = script_library.check_for_players_in_party(current_wave_party)
  
  if #players_in_party == 0 then
    destroy_party(current_wave_party, current_world)
  end
end

local function get_party_number(current_wave_party)
  local party_number = string.gsub(current_wave_party.Name, "Wave_Party_", "")
  return tonumber(party_number, 10)
end

local function has_world_loaded(current_world)
  return script_library.is_in_instance(current_world, workspace)
end

local function get_world_to_clone(current_wave_party)
  return ServerStorage:WaitForChild("World_Maps"):WaitForChild(world_maps_names[current_wave_party:GetAttribute("World_Index")])
end

local function set_world_position(current_wave_party, current_world)
  local world_x_offset = 200 * get_party_number(current_wave_party)
  local world_position = CFrame.new(Vector3.new(-300 + world_x_offset, 100, -300))
  current_world:PivotTo(world_position)
  current_world.Parent = workspace
end

local function create_world(current_wave_party)
  local current_world = get_world_to_clone(current_wave_party):Clone()
  set_world_position(current_wave_party, current_world)
  
  local while_attempts = 0
  local max_attempts = 10
  while has_world_loaded(current_world) == false and while_attempts < max_attempts do
    while_attempts += 1
    task.wait(0.5)
  end
  
  return current_world
end

local function create_player_party_folder(player, current_wave_party)
  local player_party_folder = Instance.new("Folder") -- this is where I'll be storing perk data
  player_party_folder.Name = player.UserId
  player_party_folder.Parent = current_wave_party
  return player_party_folder
end

local function setup_events_to_remove_player_from_party(player: Player, current_wave_party, current_world)
  player.Character:WaitForChild("Humanoid").Died:Once(function() -- TODO: check if not removing player if they leave makes a problem
    remove_player_from_party(current_wave_party, player.UserId, current_world)
  end)
end

local function create_wave_party_folder()
  local current_wave_party = default_wave_settings:Clone()
  current_wave_party.Name = "Wave_Party_".. tostring(#wave_parties:GetChildren() + 1) -- could repeat names if a party is removed and new one appears
  current_wave_party.Parent = wave_parties
  return current_wave_party
end

local function create_wave_party(players_to_put_in_next_party_copy)
  if players_to_put_in_next_party_copy == {} then return end
  
  local current_wave_party = create_wave_party_folder()
  local current_world = create_world(current_wave_party)
  
  for _, player in players_to_put_in_next_party_copy do
    local player_party_folder = create_player_party_folder(player, current_wave_party)
    abstract_library.set_table_attribute(player_party_folder, "Perks", {})
    ServerScriptService:WaitForChild("Data_Store"):WaitForChild("Data_Update_Binds"):WaitForChild("Update_Weapons"):Fire(player) -- TODO: the same for perk stat updates
    
    setup_events_to_remove_player_from_party(player, current_wave_party, current_world)
  end
  
  next_wave(current_wave_party, current_world)
end

local function add_player_to_next_party(player)
  if player == nil then return end
  if table.find(players_to_put_in_next_party, player) ~= nil then return end -- if player has already been detected
  table.insert(players_to_put_in_next_party, player)
end

local function update_wave_party_triggers()
  players_to_put_in_next_party = {}
  for _, part_in_wave_party_trigger in workspace:GetPartsInPart(wave_party_trigger) do
    add_player_to_next_party(game:GetService("Players"):GetPlayerFromCharacter(part_in_wave_party_trigger.Parent))
  end
  
  if #players_to_put_in_next_party > 0 then
    if wave_party_trigger:GetAttribute("Seconds_Until_Start") == -1 then
      wave_party_trigger:SetAttribute("Seconds_Until_Start", default_seconds_until_start)
    elseif wave_party_trigger:GetAttribute("Seconds_Until_Start") > 0 then
      wave_party_trigger:SetAttribute("Seconds_Until_Start", wave_party_trigger:GetAttribute("Seconds_Until_Start") - 1)
    elseif wave_party_trigger:GetAttribute("Seconds_Until_Start") == 0 then
      wave_party_trigger:SetAttribute("Seconds_Until_Start", -1)
      create_wave_party(players_to_put_in_next_party)
      players_to_put_in_next_party = {}
    end
  elseif #players_to_put_in_next_party == 0 then
    wave_party_trigger:SetAttribute("Seconds_Until_Start", -1)
  end
end

local function run_each_1_second()
  update_wave_party_triggers()
end

local function update_wave_ui(player)
  script_library.update_wave_ui(player, false, 0, 0)
end



wave_party_trigger:GetAttributeChangedSignal("Seconds_Until_Start"):Connect(update_party_trigger_cooldown_display)

run_each_x_seconds:WaitForChild("1").Event:Connect(run_each_1_second)
ServerScriptService:WaitForChild("Player_Event_Detection"):WaitForChild("Player_Character_Died").Event:Connect(update_wave_ui)

wave_events:WaitForChild("To_Next_Wave").Event:Connect(next_wave)
wave_events:WaitForChild("Connect_Next_Wave_Trigger").Event:Connect(connect_next_wave_trigger)
wave_events:WaitForChild("Reconnect_Select_Perk_Request").Event:Connect(select_perk)

485 lines of code
I should definitely break that down in modules, but for now it will suffice.
Remember the boy scout rule: “Always leave it a little better than you found it.”

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.