-
-
Save RoyallyFlushed/2d9ba68ecfafd21bf14dbedf5619e5cf to your computer and use it in GitHub Desktop.
Part of the Invasion System for Ghost Simulator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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