Skip to content

Instantly share code, notes, and snippets.

@RoyallyFlushed
Created July 1, 2024 13:52
Show Gist options
  • Save RoyallyFlushed/2d9ba68ecfafd21bf14dbedf5619e5cf to your computer and use it in GitHub Desktop.
Save RoyallyFlushed/2d9ba68ecfafd21bf14dbedf5619e5cf to your computer and use it in GitHub Desktop.
Part of the Invasion System for Ghost Simulator
--[[
TITLE: Invasion Event Manager
DESC: This script is designed to implement the server functionality for the Invasion Event. It builds upon the
same system used for the previous 2022 Easter Defence Event.
AUTHOR: RoyallyFlushed
CREATED: 04/12/2022
MODIFIED: 04/07/2023
--]]
--// Constants
local REWARD_KEY_ITEM = 241 -- Easter Key
local REWARD_CURRENCY_ITEM = 240 -- Easter Egg
local POLL_TIME = 5 -- Number of seconds to wait before checking how many participants are still playing
local INVASION_TYPES = {
[0] = {
TYPE = "Small",
NUMBER_OF_WAVES = 3,
EVENT_TIME = 6 * 60,
GHOST_DAMAGE = 3,
GHOSTS_PER_PLAYER = 3,
REWARD_CURRENCY_AMOUNT = 150,
REWARD_KEY_AMOUNT = 1
},
[1] = {
TYPE = "Medium",
NUMBER_OF_WAVES = 5,
EVENT_TIME = 7.5 * 60,
GHOST_DAMAGE = 5,
GHOSTS_PER_PLAYER = 5,
REWARD_CURRENCY_AMOUNT = 250,
REWARD_KEY_AMOUNT = 1
},
[2] = {
TYPE = "Large",
NUMBER_OF_WAVES = 10,
EVENT_TIME = 10 * 60,
GHOST_DAMAGE = 7.5,
GHOSTS_PER_PLAYER = 7,
REWARD_CURRENCY_AMOUNT = 500,
REWARD_KEY_AMOUNT = 2
}
}
local GHOST_DAMAGE_COOLDOWN = 3
local WAVE_DATA = {
[1] = {
GHOST_COUNT = 6
},
[2] = {
GHOST_COUNT = 12
},
[3] = {
GHOST_COUNT = 18,
BOSS = true
},
[4] = {
GHOST_COUNT = 24
},
[5] = {
GHOST_COUNT = 30,
BOSS = true
},
[6] = {
GHOST_COUNT = 36
},
[7] = {
GHOST_COUNT = 42,
BOSS = true
},
[8] = {
GHOST_COUNT = 45
},
[9] = {
GHOST_COUNT = 48,
BOSS = true
},
[10] = {
GHOST_COUNT = 52,
BOSS = true
}
}
local GHOSTS = {
{ -1, 1, 0.75 },
{ -1, 2, 0.25 },
{ -2, 1, 0.75 },
{ -2, 2, 0.25 },
{ -3, 1, 0.75 },
{ -3, 2, 0.25 },
}
--// Services
local TweenService = game:GetService("TweenService")
--// Modules
local FunctionModule = require(game.ReplicatedStorage.FunctionModule)
local Utility = require(game.ReplicatedStorage.Utility)
--// Events
local ClientModuleEvent = game.ReplicatedStorage.Network.ToClient.Effects.ClientModuleEvent
--// Variables
local debounce = Utility.debounce
local random = Random.new(tick())
local folder = script.Parent
local participants = {}
local forceEnd = false
local difficulty = 0
--[[
void startTrace()
DESC: Creates a new entry to store consecutive trace logs.
--]]
local function startTrace(): nil
-- Make an environment if one doesn't already exist
if not shared.Invasion.Log then
shared.Invasion.Log = {}
shared.Invasion.LogKeys = {}
end
shared.Invasion.CurrentKey = DateTime.now():ToIsoDate()
shared.Invasion.Log[shared.Invasion.CurrentKey] = {}
table.insert(shared.Invasion.LogKeys, shared.Invasion.CurrentKey)
end
--[[
void trace(string log)
@param string log : The log message to store
DESC: logs a message which can be used to drump the last event and
trace the major events.
--]]
local function trace(log: string): nil
-- Error if log doesn't exist
if not shared.Invasion.Log or not shared.Invasion.CurrentKey then
error("$ Invasion.Manager -> Trace has not been initialised! Expected call to startTrace() first!")
end
table.insert(shared.Invasion.Log[shared.Invasion.CurrentKey], log)
end
--[[
void showParticipantsEventStatus(string status)
@param string status : The string status to display
DESC: Fires the ClientModuleEvent event to participating players to tell them to set the status
to the given string message.
--]]
local function showParticipantsEventStatus(status)
for _, player in participants do
if player then
ClientModuleEvent:FireClient(player, "DisplayStatus", script.ClientModule, status)
end
end
end
--[[
void pollTouchingPlayers(void)
DESC: Checks the number of participants that are in the match ever POLL_TIME
seconds to identify if everyone has left. If so, forceEnd is set.
--]]
local function pollTouchingPlayers()
while true do
local participants = FunctionModule.GetPlayersTouchingPart(folder.ArenaArea)
-- If number of participants is 0, set force end and return
if #participants == 0 then
forceEnd = true
return
end
task.wait(POLL_TIME)
end
end
--[[
void startTimer(number t)
@param number t : The amount of time in seconds to run the timer for
DESC: Starts a timer for the given amount of time which displays in the event time
of the event tracker at the top of the screen. Also sets forceEnd variable
if time has ran out.
--]]
local function startTimer(t: number)
-- Countdown from t and handle forceEnd
for i = 1, t do
game.ReplicatedStorage.Data.EventTime.Value = t - i
-- Check if forceEnd has been set and return
if forceEnd then
return
end
task.wait(1)
end
-- If timer finished, set forceEnd
forceEnd = true
end
--[[
void wardstoneHealthChanged(void)
DESC: Handles aesthetic updates and wardstone status logic based on health
value changing.
--]]
local function wardstoneHealthChanged(wardstone: Model)
local healthPercentage = wardstone.Health.Value / 100
-- Lerp color of HealthGuage from green to red
wardstone.Statue.HealthGauge.Color = Color3.fromHSV(healthPercentage * 120 / 255, 1, 1)
-- Update numerical percentage display with health percentage
wardstone.HealthDisplay.SurfaceGui.TextLabel.Text = math.floor(healthPercentage * 100) .. "%"
end
--[[
DESC: Handles the entire Invasion event logic from start to finish
synchronously, and then hands back control to the Event Manager
that invoked it when finished.
--]]
folder.StartInvasion.OnInvoke = function()
startTrace()
trace("Starting Event!")
local startTime = time()
local events = {}
-- Translate constants from shared table to constant
INVASION_TYPES = shared.Invasion.INVASION_TYPES
WAVE_DATA = shared.Invasion.WAVE_DATA
GHOST_DAMAGE_COOLDOWN = shared.Invasion.GHOST_DAMAGE_COOLDOWN
-- Get difficulty
local invasionData = INVASION_TYPES[difficulty]
trace(`Difficulty: {invasionData.TYPE}`)
-- Store all participants that we have to begin with
participants = FunctionModule.GetPlayersTouchingPart(folder.ArenaArea)
trace(`Number of players: {#participants}`)
-- Initialise the player's Event Display UI
for _, player in participants do
ClientModuleEvent:FireClient(player, "Init", script.ClientModule, folder, invasionData.EVENT_TIME)
end
-- Reset wardstone health
for _, wardstone in folder.Wardstones:GetChildren() do
wardstone.Health.Value = 100
end
-- Reset counters
folder.GhostsRemaining.Value = 0
folder.CurrentWave.Value = 1
folder.Difficulty.Value = invasionData.TYPE
folder.NumberOfPlayers.Value = #participants
-- Reset variable states
forceEnd = false
-- Start timer
task.spawn(startTimer, invasionData.EVENT_TIME)
-- Poll participants to identify if everyone leaves
task.spawn(pollTouchingPlayers)
for _, wardstone in folder.Wardstones:GetChildren() do
local partsTouchingWardstone = 0
local attacking = false
-- Connect listener to the wardstone's hitbox to check if a ghost is touching it
table.insert(events, wardstone.PrimaryPart.Touched:Connect(function(part)
local ghost = Utility.FindFirstAncestorWithChildWhichIsA(part, "Humanoid")
if ghost and ghost:FindFirstChild("EnemyHumanoid")
and ghost.AI.AIType.Value == "EventCustomAI"
and wardstone.Health.Value > 0
then
partsTouchingWardstone += 1
attacking = true
end
end))
-- Connect listener to the wardstone's hitbox to handle when Touch Ends
table.insert(events, wardstone.PrimaryPart.TouchEnded:Connect(function(part)
local ghost = Utility.FindFirstAncestorWithChildWhichIsA(part, "Humanoid")
if ghost and ghost:FindFirstChild("EnemyHumanoid")
and ghost.AI.AIType.Value == "EventCustomAI"
and wardstone.Health.Value > 0
then
partsTouchingWardstone -= 1
if partsTouchingWardstone <= 0 then
partsTouchingWardstone = 0
attacking = false
end
end
end))
-- Connect coroutine for handling damage of wardstones
task.spawn(function()
while wardstone.Health.Value > 0 and not forceEnd do
while attacking and not forceEnd do
wardstone.Health.Value = math.max(wardstone.Health.Value - invasionData.GHOST_DAMAGE, 0)
-- Handle if wardstone health is less than or equal to 0
if wardstone.Health.Value <= 0 then
-- End of event, loss!
showParticipantsEventStatus(`{wardstone.WardstoneName.Value:upper()} HAS FALLEN!`)
task.delay(2, showParticipantsEventStatus, "")
trace(`{wardstone.WardstoneName.Value} has fallen!`)
local allWardstonesUp = false
-- Loop over all wardstones health and check to see if they are all down
for _, wardstone in folder.Wardstones:GetChildren() do
if wardstone.Health.Value > 0 then
allWardstonesUp = true
end
end
-- If all the wardstones have less than 0 health then forceEnd
if not allWardstonesUp then
forceEnd = true
trace("Event Loss! All wardstones destroyed!")
trace(`Time taken: {Utility.TimeFormatSeconds("mm:ss", math.floor(time() - startTime))}`)
end
break
end
task.wait(GHOST_DAMAGE_COOLDOWN)
end
task.wait()
end
end)
end
local timePerWave = invasionData.EVENT_TIME / invasionData.NUMBER_OF_WAVES
local spawnTimePerWave = timePerWave / 2
local ghosts = {}
-- Add possible ghosts to array
for _, ghostData in GHOSTS do
table.insert(ghosts, {
Entry = game.ReplicatedStorage.GhostEntries:FindFirstChild(ghostData[1]):FindFirstChild(ghostData[2]),
Chance = ghostData[3]
})
end
-- Start wave loop
for currentWave = 1, invasionData.NUMBER_OF_WAVES do
-- Calculate constants based on number of players and difficulty per wave
local waveData = {}
local numPlayers = #FunctionModule.GetPlayersTouchingPart(folder.ArenaArea)
-- Copy contents of WAVE_DATA into another table
for key, value in WAVE_DATA[currentWave] do
waveData[key] = value
end
waveData.GHOST_COUNT += numPlayers * invasionData.GHOSTS_PER_PLAYER
local spawnRounds = math.ceil(waveData.GHOST_COUNT / 3)
local spawnRoundCooldown = spawnTimePerWave / spawnRounds
local spawnedGhosts = {}
folder.GhostsRemaining.Value = waveData.GHOST_COUNT + (waveData.BOSS and 1 or 0)
folder.CurrentWave.Value = currentWave
folder.NumberOfPlayers.Value = numPlayers
-- Tell players new wave starting
showParticipantsEventStatus(`WAVE {currentWave} STARTING!`)
task.delay(2, showParticipantsEventStatus, "")
trace(`Wave {currentWave} starting!`)
-- Spawn ghosts randomly for current wave
for currentRound = 1, spawnRounds do
for i = 1, 3 do
-- If the last round isn't full, break for out of bounds
if ((currentRound - 1) * 3) + i > waveData.GHOST_COUNT then
break
end
local rand = random:NextInteger(0, 75) / 100
local ghostEntry = nil
local spawnCFrame = folder.Portals[i].CFrame
-- Pick a random ghost with weighting applied
repeat
local pick = ghosts[random:NextInteger(1, #ghosts)]
if rand <= pick.Chance then
ghostEntry = pick.Entry
end
until ghostEntry
-- Spawn ghost and set values
local ai = "EventCustomAI"
local ghost = game.ServerScriptService.Functions.SpawnGhost:Invoke(ghostEntry, spawnCFrame, ai)
local died = false
local damageVal = Instance.new("NumberValue")
damageVal.Name = "Damage"
damageVal.Value = invasionData.GHOST_DAMAGE
damageVal.Parent = ghost
local val = Instance.new("ObjectValue")
val.Name = "Arena"
val.Value = folder
val.Parent = ghost
-- Remove ghost if it died
ghost.AI.Died.Event:Connect(function()
if not died then
folder.GhostsRemaining.Value -= 1
died = true
end
end)
-- Remove ghost if parent is nil
ghost.AncestryChanged:Connect(function(i, newParent)
if newParent == nil and not died then
folder.GhostsRemaining.Value -= 1
died = true
end
end)
-- Store currently spawned ghosts
table.insert(spawnedGhosts, ghost)
end
-- Wait spawnRoundCooldown seconds but check for forceEnd
local st = time()
repeat
task.wait()
until time() >= st + spawnRoundCooldown or forceEnd
-- Break if forceEnd
if forceEnd then
break
end
end
-- Spawn boss if boss
if waveData.BOSS then
local ghostEntry = game.ReplicatedStorage.GhostEntries:FindFirstChild("-4"):FindFirstChild("1")
local spawnCFrame = folder.Portals[random:NextInteger(1, 3)].CFrame
-- Spawn ghost and set values
local ai = "EventCustomAI"
local ghost = game.ServerScriptService.Functions.SpawnGhost:Invoke(ghostEntry, spawnCFrame, ai)
local died = false
local damageVal = Instance.new("NumberValue")
damageVal.Name = "Damage"
damageVal.Value = invasionData.GHOST_DAMAGE
damageVal.Parent = ghost
local val = Instance.new("ObjectValue")
val.Name = "Arena"
val.Value = folder
val.Parent = ghost
-- Remove ghost if it died
ghost.AI.Died.Event:Connect(function()
if not died then
folder.GhostsRemaining.Value -= 1
died = true
end
end)
-- Remove ghost if parent is nil
ghost.AncestryChanged:Connect(function(i, newParent)
if newParent == nil and not died then
folder.GhostsRemaining.Value -= 1
died = true
end
end)
-- Store currently spawned ghosts
table.insert(spawnedGhosts, ghost)
end
-- Wait until all the ghosts in the current wave are dead
repeat
task.wait()
until folder.GhostsRemaining.Value <= 0 or forceEnd
-- Remove any ghosts that are left
for _, ghost in spawnedGhosts do
if ghost then
ghost:Destroy()
end
end
-- Break if forceEnd
if forceEnd then
break
end
end
if not forceEnd then
-- End of event, win!
showParticipantsEventStatus("INVASION SUCCESSFULLY DEFEATED!")
task.delay(2, showParticipantsEventStatus, "")
trace("Event Success!")
trace(`Time taken: {Utility.TimeFormatSeconds("mm:ss", math.floor(time() - startTime))}`)
end
-- Clean up time!
task.wait(2)
-- Increment difficulty
difficulty = (difficulty + 1) % 3
local playersToReward = {}
-- Cross reference original participants and those still in the arena for rewarding
for _, player in FunctionModule.GetPlayersTouchingPart(folder.ArenaArea) do
if table.find(participants, player) and not table.find(playersToReward, player) then
table.insert(playersToReward, player)
end
end
-- Teleport anyone touching the arena back to spawn
for _, player in FunctionModule.GetPlayersTouchingPart(folder.ArenaArea) do
game.ReplicatedStorage.Network.ToClient.Effects.FastTeleport:FireClient(player, "Lab")
end
-- Call cleanup for participants of the event
for _, player in participants do
if player then
ClientModuleEvent:FireClient(player, "CleanUp", script.ClientModule)
end
end
local numWardstonesUp = 0
-- Count up number of wardstones left up
for _, wardstone in folder.Wardstones:GetChildren() do
if wardstone.Health.Value > 0 then
numWardstonesUp += 1
end
end
-- Reward players based on participants who are still there at the end to avoid exploiters & afkers
if not forceEnd then
-- Reward player
for _, player in playersToReward do
if player then
local rewardCurrencyAmount = numWardstonesUp * invasionData.REWARD_CURRENCY_AMOUNT
local rewardKeyAmount = player.Stats.BossDropFactor.Value * invasionData.REWARD_KEY_AMOUNT
-- Give extra key if player owns Easter Pass 2023
if player.Records.Miscellaneous:FindFirstChild("OwnsEasterPass2023") then
rewardKeyAmount += 1
end
-- reward player and fire quest events
FunctionModule.ChangePlayerItemCount(player, REWARD_CURRENCY_ITEM, rewardCurrencyAmount)
FunctionModule.ChangePlayerItemCount(player, REWARD_KEY_ITEM, rewardKeyAmount)
game.ServerScriptService.Functions.FireQuestEvent:Invoke(player, "CollectedItems", REWARD_KEY_ITEM, rewardKeyAmount)
game.ServerScriptService.Functions.FireQuestEvent:Invoke(player, "CollectedItems", REWARD_CURRENCY_ITEM, rewardCurrencyAmount)
game.ServerScriptService.Functions.FireQuestEvent:Invoke(player, "FinishedEggDefence")
end
end
else
-- End of event, loss!
showParticipantsEventStatus("INVASION LOST!")
task.delay(2, showParticipantsEventStatus, "")
-- ForceEnd somehow, check if wardstones are up, if so then reward only currency
if numWardstonesUp > 0 then
trace("Event Loss! Time ran out!")
trace(`Time taken: {Utility.TimeFormatSeconds("mm:ss", math.floor(time() - startTime))}`)
for _, player in playersToReward do
if player then
local rewardCurrencyAmount = numWardstonesUp * invasionData.REWARD_CURRENCY_AMOUNT
FunctionModule.ChangePlayerItemCount(player, REWARD_CURRENCY_ITEM, rewardCurrencyAmount)
game.ServerScriptService.Functions.FireQuestEvent:Invoke(player, "CollectedItems", REWARD_CURRENCY_ITEM, rewardCurrencyAmount)
end
end
end
end
-- Set forceEnd to true to kill running coroutines
forceEnd = true
-- Clean up events
for _, event in events do
event:Disconnect()
end
trace("Event End!")
end
--[[
void main(void)
DESC: Main entry point for the script
--]]
local function main()
for _, wardstone in folder.Wardstones:GetChildren() do
-- Connect a listener function to the wardstone's health value to refresh aesthetics related to health
wardstone.Health.Changed:Connect(function()
wardstoneHealthChanged(wardstone)
end)
end
shared.Invasion = {}
shared.Invasion.INVASION_TYPES = INVASION_TYPES
shared.Invasion.GHOST_DAMAGE_COOLDOWN = GHOST_DAMAGE_COOLDOWN
shared.Invasion.WAVE_DATA = WAVE_DATA
shared.Invasion.Dump = function()
print("")
print("===== INVASION CONSTANTS DUMP BEGIN =====")
print(`GHOST_DAMAGE_COOLDOWN : {shared.Invasion.GHOST_DAMAGE_COOLDOWN}`)
print("INVASION_TYPES :")
for key, value in shared.Invasion.INVASION_TYPES do
print(` {key} :`)
for j, v in value do
print(` {j} : {v}`)
end
end
print("WAVE_DATA :")
for key, value in shared.Invasion.WAVE_DATA do
print(` {key} :`)
for j, v in value do
print(` {j} : {v}`)
end
end
print("===== INVASION CONSTANTS DUMP END =====\n")
end
shared.Invasion.TraceDump = function(key: string): nil
if shared.Invasion.Log[key] then
print(`===== INVASION LOG {key} DUMP BEGIN =====`)
for _, log in shared.Invasion.Log[key] do
print(log)
end
print("===== INVASION LOG DUMP END =====")
end
end
shared.Invasion.TraceDumpKeys = function()
print("===== INVASION LOG KEYS DUMP BEGIN =====")
for _, key in shared.Invasion.LogKeys do
print(key)
end
print("===== INVASION LOG KEYS DUMP END =====")
end
end
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment