-- implement seed functionality for debugging -- when moving to testing, test with intermediate on a specific seed and fine tune the balance there -- before moving on -- menu to pick the mission difficulty at map start -- Intermediate -- Advanced -- Expert -- Program chooses from predefined list of wavespawns in popfile -- Data about each wavespawn in the popfile is stored in the program -- Each wavespawn in the popfile is directly connected to one spawnpoint, -- therefore we can enable / disable the spawnpoint to "control" the wavespawn -- Teleport spawnpoints to each gate / bot spawn (e.g. left, right) as appropriate -- Reserve 1 mainwave bot for win condition -- Popfile wavespawn keyvalues: -- Where _spawnbot_wavespawn_1 -- Support 1 -- Spawncount 1 -- WaitBetweenSpawns 0.01 -- TFBot { ... Tag _spawnbot_wavespawn_1 } -- Program controls total currency for mission and waves -- to implement this, when bots die they spawn an item_currencypack_custom (store the amt through a custom member) -- when picked up, it distributes it's currency to red (tf_gamerules["$AddCurrencyGlobal"] or player["$AddCurrency"]) -- Popfile is 1 wave, program simulates multiple waves and wave start / finish (pause bot spawning, players vote to ready up, control map logic) -- waves "end" in the program when all non support wavespawns in the wave are dead -- Because of this implementation, wave failure will have to be handled manually as well (disable the hatch deploy relay) -- (pause bot spawning, reset map, scare players before respawning them, reset internal state of wave to default, vote to ready up) -- To track bots and simulate totalcount, maxactive, spawncount, etc: -- function OnWaveSpawnBot(bot, wave, tags) -- we can associate a bot with a wavespawn through it's tag (remember tag holds spawnpoint, spawnpoint -> WaveSpawn in wavespawn_data) -- function OnWaveSpawnTank(tank, wave) -- To track tanks, they will have their targetname set to their spawnpoint name (e.g _tank_spawnbot_wavespawn_1) -- there is a limit to simulation: customwaitbetween / (customspawncount * 0.01) if the result is less than 1, we cannot spawn fast enough with params -- (this is still plenty enough room for 99% of usage though, why would we try to spawn 100 bots in 1 second?) -- tanks wait for a wavespawn that has 1 bot that will die immediately, limited number of tanks as a result (this should be fine) -- set tank health and speed in here -- have a global table where we store changes we want for a specific tank, when the tank spawns, those changes are implemented and its removed from the table -- generation: -- difficulty -- mission name -- adjective(s) + noun or [adjective +] noun + preposition + noun -- mission type -- wave count -- totalcurrency -- startingcurrency -- currency per wave adding up to totalcurrency - startingcurrency -- wave: -- subwave count -- create support wavespawns to span multiple subwaves when appropriate -- wavespawns based on difficulty and current currency available to red (push force and support force) -- distribute currency across wavespawns -- connect subwaves table.DeepCopyCustom = function(t) local newtable = {}; for k, v in pairs(t) do if (string.find(k, "__") ~= 1 and type(v) ~= "function") then if (type(v) == "table") then newtable[k] = table.DeepCopyCustom(v); else newtable[k] = v; end end end return newtable; end table.JoinTableCustom = function(t1, t2) local t = table.DeepCopyCustom(t1); if (not t2) then return t; end for k, v in pairs(t2) do t[k] = v; end return t; end BOT_SKILL_EASY = 0; BOT_SKILL_NORMAL = 1; BOT_SKILL_HARD = 2; BOT_SKILL_EXPERT = 3; Bot = { class = TF_CLASS_UNDEFINED, classicon = "", }; Bot.__index = Bot; Tank = { health = 0, speed = 0, }; Tank.__index = Tank; Squad = { formationsize = -1.0, preservesquad = false, spawners = {}, }; Squad.__index = Squad function Bot:new(bot) bot = table.JoinTableCustom(table.DeepCopyCustom(self), bot) or {}; setmetatable(bot, self); return bot; end function Tank:new(tank) tank = table.JoinTableCustom(table.DeepCopyCustom(self), tank) or {}; setmetatable(tank, self); return tank; end function Squad:new(squad) squad = table.JoinTableCustom(table.DeepCopyCustom(self), squad) or {}; setmetatable(squad, self); return squad; end -- spawners: tfbot tank, squad, (no randomchoice, cannot predict what game will choose (and its a bit redundant since the mission is random)) wavespawn_data = { _spawnbot_wavespawn_1 = { spawnpoint = "_spawnbot_wavespawn_1", spawner = Bot:new({class=TF_CLASS_SCOUT, classicon="scout"}), threatlevel = 0.25, }, _spawnbot_wavespawn_2 = { spawnpoint = "_spawnbot_wavespawn_2", spawner = Bot:new({class=TF_CLASS_SOLDIER, classicon="soldier"}), threatlevel = 0.25, }, _spawnbot_wavespawn_3 = { spawnpoint = "_spawnbot_wavespawn_3", spawner = Bot:new({class=TF_CLASS_PYRO, classicon="pyro"}), threatlevel = 0.25, }, }; wavespawn_data_threat_low = {}; wavespawn_data_threat_medium = {}; wavespawn_data_threat_high = {}; for spawnpoint, data in pairs(wavespawn_data) do if (data and data.threatlevel) then if (data.threatlevel < 0.33) then wavespawn_data_threat_low[spawnpoint] = data; elseif (data.threatlevel < 0.66) then wavespawn_data_threat_medium[spawnpoint] = data; elseif (data.threatlevel >= 0.66) then wavespawn_data_threat_high[spawnpoint] = data; end end end -- We handle health and speed over on the lua side for more flexibility tank_spawns_area1 = { "_spawnbot_tank_1", "_spawnbot_tank_2", "_spawnbot_tank_3", }; Mission = { name = "mission", difficulty = "adv", mission_type = MISSION_TYPE_NONE, totalcurrency = 0, startingcurrency = 0, waves = {}, current_wave = 0, }; Mission.__index = Mission; Wave = { totalcurrency = 0, subwave_count = 0, wavespawns = {}, spawnpoint_map = {}, }; Wave.__index = Wave; WaveSpawn = { spawnpoint = nil, subwave = nil, is_support = false, totalspawned = 0, totalactive = 0, totalcurrency = 0, totalcount = 0, maxactive = 999, spawncount = 1, waitforalldead = nil, waitforallspawned = nil, waitbeforespawning = 0, waitbetweenspawns = 0, }; WaveSpawn.__index = WaveSpawn; function Mission:new(mission) mission = table.JoinTableCustom(table.DeepCopyCustom(self), mission) or {}; setmetatable(mission, self); return mission; end function Mission:AddWave(wave) wave = wave or Wave:new(); table.insert(self.waves, wave); return #self.waves; end function Mission:GetWave(wave_number) return self.waves[wave_number]; end function Mission:InitRandomWaves() if (math.random(1,3) == 1) then self.mission_type = MISSION_TYPE_ENDURANCE; for i = 1, math.random(1,4) do random_mission:AddWave(); end else self.mission_type = MISSION_TYPE_INVASION; for i = 1, math.random(5,8) do random_mission:AddWave(); end end self.current_wave = 1; end function Mission:InitRandomCurrency() local diff = self.difficulty; if (diff == "int") then self.totalcurrency = math.rounddiv(math.random(5000, 6000), 100); elseif (diff == "adv") then self.totalcurrency = math.rounddiv(math.random(4000, 5000), 100); elseif (diff == "exp") then self.totalcurrency = math.rounddiv(math.random(3000, 4000), 100); end self.startingcurrency = math.rounddiv(math.random(400, 1200), 50); end function Mission:PrintInfo() local t = table.DeepCopyCustom(self); if (t.mission_type == MISSION_TYPE_INVASION) then t.mission_type = "Invasion"; elseif (t.mission_type == MISSION_TYPE_ENDURANCE) then t.mission_type = "Endurance"; else t.mission_type = "None"; end if (t.waves) then t.waves = #t.waves; end PrintTable(t); print(""); end function Wave:new(wave) wave = table.JoinTableCustom(table.DeepCopyCustom(self), wave) or {}; setmetatable(wave, self); return wave; end function Wave:AddWaveSpawn(wavespawn) wavespawn = wavespawn or WaveSpawn:new(); local spawnpoint = wavespawn.spawnpoint; if (spawnpoint) then self.spawnpoint_map[spawnpoint] = wavespawn; else return; end table.insert(self.wavespawns, wavespawn); return wavespawn; end function WaveSpawn:new(wavespawn) wavespawn = table.JoinTableCustom(table.DeepCopyCustom(self), wavespawn) or {}; setmetatable(wavespawn, self); return wavespawn; end use_population_manager = true; local nouns = { "ability","access","accident","action","activity","administration","affair","addition","army","area","application","ash", "blood","breath","cell","chemistry","choice","city","coast","combination","competition","computer","concept","confusion", "connection","consequence","construction","contact","context","contract","contribution","control","courage","cycle", "data","database","death","dealer","debt","decision","definition","delivery","demand","departure","depression","depth", "description","design","development","device","difference","difficulty","direction","director","dirt","disaster","discipline", "disease","disk","distribution","driver","editor","effect","efficiency","effort","emotion","end","energy","engine", "entry","environment","equipment","error","event","exam","examination","example","exchange","excercise","experience", "expression","failure","fact","feedback","field","figure","fire","finding","flight","food","force","form","fortune", "foundation","frame","freedom","friendship","funeral","future","game","garbage","gate","goal","guide","heat","hope", "ice","idea","image","impact","impression","independence","industry","information","initiative","injury","inspection","inside", "instance","intention","interaction","issue","judgement","key","knowledge","lab","law","leadership","level","life","light", "line","link","location","love","loss","machine","maintenance","management","manufacturer","material","matter","measurement", "medium","menu","message","metal","midnight","mind","mixture","model","morning","mud","nature","negotiation","network","night", "note","number","object","obligation","oil","operation","opportunity","order","organization","outcome","payment","penalty", "perception","performance","period","perspective","physics","platform","point","population","possession","possibility", "potato","power","presence","pressure","price","priority","problem","procedure","process","product","program","property", "proposal","protection","purpose","quality","quantity","question","radio","rate","ratio","range","reaction","reality", "reason","reception","recipe","recognition","recommendation","record","recording","reflection","region","replacement", "requirement","research","resolution","radiation","resource","response","result","review","revolution","risk","river", "road","rule","salt","sample","scale","scene","science","screen","section","sector","security","selection","sense","series", "service","session","setting","shape","share","significance","site","situation","society","software","soil","solution", "sound","source","speaker","standard","state","storage","strategy","stress","structure","success","suggestion","surgery", "sympathy","system","task","technology","temperature","tension","term","theory","thing","thought","tool","topic","training", "understanding","variation","variety","version","virus","voice","volume","war","warning","water","way","weakness", "web","wind","work","metal","plastic","steel","iron","copper","wiring","circuits","motherboard","transistor","power", "automation","intelligence","furnace", "bolts", "nails","freeze","frost","emerald","diamond","ruby","amethyst","topaz","rust", "hydro","sapphire","tin_can","tin","coal","waters","wrath","takeover" }; local adjectives = { "abandoned","abashed","abhorrent","abject","ablaze","abnormal","aboriginal","abrasive","abrupt","absent", "absorbed","abstracted","abstract","absurd","abundant","abusive","accidental","acidic","adamant","adorable", "adventurous","acceptable","aggressive","agitated","alert","aloof","amiable","amused","annoyed","antsy", "anxious","appalling","appetizing","apprehensive","arrogant","ashamed","astonishing","attactive","average", "bad","barbarous","bashful","bawdy","beautiful","beefy","belligerent","beneficial","berserk","bent","better", "bewildered","big","bitesized","biting","bitter","bizarre","black","bland","bloody","blue","boiling","bored", "boring","bouncy","boundless","brainy","brash","brave","brawny","breakable","breezy","broad","broken","brown", "bulky","bumpy","burly","busy","calculating","callous","calm","capable","capricious","careful","careless","caring", "cautious","ceaseless","charming","cheap","cheerful","chemical","chilly","chivalrous","chunky","clammy","clever", "closed","cloudy","clueless","clumsy","cluttered","coherent","cold","colorful","colossal","combative","comfortable", "common","complex","condemned","condescending","confused","conscious","contemplative","convoluted","cool","cooperative", "coordinated","costly","courageous","cowardly","craven","crazy","creepy","crooked","cruel","cuddly","cultured","cumbersome", "curious","curved","damaged","damaging","damp","dangerous","dapper","dark","dashing","dazzling","dead","deafining","dear", "debonair","decayed","deceitful","decisive","decorous","deep","defeated","defective","defiant","delicated","delicious", "delightful","delirious","demonic","dependent","depraved","depressed","deranged","deserted","despicable","detailed","determined", "devillish","dilapidated","diminutive","disgusting","distinctful","distraught","distressed","disturbed","dizzy","drab","dull", "eager","easy","eatable","economic","educated","efficient","elastic","electric","electrical","elegant","elite","emaciated", "embarrassed","eminent","empty","enchanted","enchanting","encouraging","endurable","endless","energetic","enourmous","entertaining", "enthusiastic","envious","erect","erratic","ethereal","exasperated","excited","exhilarated","exuberant","extensive","fabulous", "faded","faint","fair","fallacious","false","familiar","famous","fanatical","fancy","fantastic","far","fascinating","fast", "fat","faulty","fearful","fearless","feeble","feigned","festive","few","fierce","filthy","flat","floppy","fluttering","foolish", "frantic","fresh","friendly","frightened","frothing","gaping","garrulous","gaudy","gentle","ghastly","giant","giddy", "gifted","gigantic","ginormous","gleaming","glistening","glorious","godly","good","goofy","graceful","gray","greasy","greased", "grotesque","gritty","grubby","grumpy","hallowed","handsome","handy","hanging","hard","harsh","hateful","healthy","heavy", "hellish","helpless","hesitant","hideous","high","hilarious","hissing","hollow","homeless","honorable","horrible","horrific", "hot","huge","hulking","humorous","hungry","hurried","hurt","hushed","husky","hypnotic","hysterical","icky","idiotic", "ignorant","ill","illegal","imaginary","immense","imperfect","impolite","important","imported","incandescent","incompetent", "inconclusive","incredible","industrious","inexpensive","infamous","innocent","insidious","intelligent","invincible","irate", "irritable","irritating","itchy","jaded","jagged","jealous","jittery","jobless","jolly","joyous","judicious","juicy", "jumbled","jumpy","juvenile","keen","kind","kindly","knotted","labored","lacking","lame","lamentable","languid","large","last", "late","laughable","lavish","lazy","lean","legel","lethal","lewd","light","likeable","limping","little","lively","livid","lonely", "long","loose","lopsided","loud","lovely","low","lowly","lucky","ludicrous","lumpy","lush","macho","maddening","mad","magical", "makeshift","malicious","mammoth","maniacal","many","marked","marvelous","massive","materialistic","mature","mean","measly", "meaty","meek","melancholy","mellow","melted","merciful","messy","mighty","military","milky","mindless","miniature","minor", "minute","misty","mixed","moaning","mordern","moldy","momentous","moody","motionless","mountainous","muddled","mundane","murky", "mushy","mute","mysterious","naive","narrow","nasty","natural","naughty","nauseating","neat","nebulous","necessary","needless", "needy","neighborly","nervous","new","next","nice","nimble","noiseless","noisy","nonchalant","nondescript","nonsensical", "nonstop","normal","noxious","numberless","numerous","nutritious","nutty","oafish","obedient","obese","obnoxious","obscene", "observant","obsolete","odd","offbeat","old","omniscient","open","optimal","ordinary","organic","painful","paltry","panicky", "parched","pathetic","peaceful","perfect","petite","precious","proud","pungent","puny","quaint","quarrelsome","questionable", "quick","quiet","quirky","rabid","ragged","rambunctious","rampant","rapid","rare","raspy","ready","real","rebel","receptive", "red","redundant","reflective","regular","repulsive","responsive","robust","rotten","rough","round","sad","salty","sarcastic", "sassy","savory","scandalous","scant","scary","scattered","scientific","scrawny","screeching","selfish","shaggy","shaky", "shallow","sharp","shiny","short","silly","skinny","slimy","slippery","small","smoggy","smooth","smug","soggy","solid","sore", "sour","sparkling","spicy","splendid","spotless","square","stale","steady","steep","sticky","stormy","stout","straight","strange", "strong","stunning","substantial","successful","succulent","superficial","superior","sweaty","sweet","taboo","tacky","talented", "tall","tame","tangible","tart","tasteless","tasty","tedious","teeny","tender","tense","terrible","thick","thoughtful", "thoughtless","tight","timely","tricky","trite","troubled","ugly","ultra","unarmed","unbiased","uncovered","undesirable", "unequal","unequaled","uneven","unhealthy","unique","unkempt","unknown","unnatural","unruly","unsightly","untidy","unused", "unusual","unwieldy","upset","used","useful","useless","vagabond","valuable","various","vast","vengeful","venemous","verdant", "vexed","vigorous","violent","virtuous","vivid","voiceless","volatile","voracious","vulgar","wacky","wandering","warm","wary", "weak","wealthy","weary","well_groomed","well_made","wet","whimsical","whispering","white","whole","wicked","wide","wiggly", "wild","willing","wiry","wise","witty","wobbly","womanly","worthless","wretched","wry","yellow","young","yummy","zealous","zany", "operation", "rusted","pointed","medieval","primitive", }; local prepositions = { "about","aboard","above","across","after","against","amid","among","along","around","as","at","before", "behind","below","beside","between","by","down","during","for","from","in","inside","into","near","of", "off","on","out_of","over","through","to","toward","under","up","with","and" }; function GenerateRandomMissionName() if (math.random(1, 4) == 1) then local name = ""; if (math.random(1, 4) == 1) then name = table.Random(adjectives).."_"; end return name..table.Random(nouns).."_"..table.Random(prepositions).."_"..table.Random(nouns); else local name = table.Random(adjectives); local count = 1; local val = math.random(1,3); while (val == 1) do if (count > 2) then break; end count = count + 1; val = math.random(1,3); name = name.."_"..table.Random(adjectives); end return name.."_"..table.Random(nouns); end end votemenu = { timeout = 0, title = "Vote: Mission Difficulty", flags = MENUFLAG_NO_SOUND, [1] = "Intermediate", [2] = "Advanced", [3] = "Expert", [4] = "Random", onSelect = function(player, selectedIndex, value) local playerdata = player_list[player:GetUserId()]; if (playerdata.votemenu_choice ~= selectedIndex) then playerdata.votemenu_choice = selectedIndex; player:PlaySoundToSelf("ui/hint.wav"); else playerdata.votemenu_choice = nil; end end, }; resultmenu = { timeout = 3, title = "", flags = MENUFLAG_NO_SOUND, [1] = { text = "", disabled = true }, }; function DifficultySelected() random_mission = Mission:new(); votemenu_timer_active = false; votemenu_finish_timer_active = false; votemenu_active = false; local maxvotes_index = 1; local votes_to_pass = math.floor(#player_list / 2); for index, count in pairs(votes) do if (count > votes[maxvotes_index]) then maxvotes_index = index; end end local success = true; if (votes[maxvotes_index] > votes_to_pass) then votemenu_choice = votemenu_choices[maxvotes_index]; resultmenu.title = "āœ“ Vote Passed"; if (votemenu_choice ~= "Random") then resultmenu[1].text = "RED team chose "..votemenu_choice; end else success = false; votemenu_choice = "Advanced"; resultmenu.title = "āœ— Vote Failed"; resultmenu[1].text = "Not enough players voted, Advanced difficulty selected"; end local difficulty_map = { Intermediate="int", Advanced="adv", Expert="exp"}; if (votemenu_choice == "Random") then votemenu_choice = votemenu_choices[math.random(1,3)]; resultmenu[1].text = "Random difficulty: "..votemenu_choice; end random_mission.difficulty = difficulty_map[votemenu_choice]; for index, playerdata in pairs(player_list) do local player = ents.GetPlayerByUserId(index); player:DisplayMenu(resultmenu); if (success) then player:PlaySoundToSelf('ui/vote_success.wav') else player:PlaySoundToSelf('ui/vote_failure.wav') end end if (tf_objective_resource and tf_objective_resource:IsValid()) then local prefix = "scripts/population/mvm_winterbridge_rc6_"; random_mission.name = GenerateRandomMissionName(); tf_objective_resource:SetFakeSendProp("m_iszMvMPopfileName", prefix..random_mission.difficulty.."_"..random_mission.name..".pop"); end random_mission:InitRandomWaves(); random_mission:InitRandomCurrency(); random_mission:PrintInfo(); local spawn = "_spawnbot_wavespawn_1"; local wavespawn = random_mission.waves[1]:AddWaveSpawn({spawnpoint=spawn, totalcount=4, waitbetweenspawns=2}); end function UpdateVoteMenu() if (midwave or (tf_gamerules and tf_gamerules.m_flRestartRoundTime > -1 and tf_gamerules.m_flRestartRoundTime - CurTime() <= 10)) then DifficultySelected() return; end voters = {"", "", "", ""}; votes = {0, 0, 0, 0}; playercount = #player_list; votemenu[1] = "Intermediate"; votemenu[2] = "Advanced"; votemenu[3] = "Expert"; votemenu[4] = "Random"; for index, playerdata in pairs(player_list) do local player = ents.GetPlayerByUserId(index); local choice = playerdata.votemenu_choice; if (choice) then voters[choice] = voters[choice].."\nāœ“ "..player.m_szNetname; votemenu[choice] = votemenu_choices[choice]..voters[choice]; votes[choice] = votes[choice] + 1; end end if (votes[1] == 0 and votes[2] == 0 and votes[3] == 0 and votes[4] == 0) then votemenu_timer_active = false; votemenu_finish_timer_active = false; votemenu_duration_value = 0; votemenu_finish_value = 0; elseif (votes[1] == playercount or votes[2] == playercount or votes[3] == playercount or votes[4] == playercount) then votemenu_finish_timer_active = true; votemenu_timer_active = true; else votemenu_timer_active = true; votemenu_finish_value = votemenu_finish_duration; end if (votemenu_finish_timer_active) then votemenu_finish_value = votemenu_finish_value + 0.015; if (votemenu_finish_value >= votemenu_finish_duration) then DifficultySelected(); return; end end if (votemenu_timer_active) then votemenu_duration_value = votemenu_duration_value + 0.015; if (votemenu_duration_value >= votemenu_duration and not votemenu_finish_timer_active) then DifficultySelected(); return; end end for index, playerdata in pairs(player_list) do local player = ents.GetPlayerByUserId(index); player:DisplayMenu(votemenu); end end function ResetDifficultyVote() votemenu_active = true; votemenu_choice = nil; votemenu_timer_active = false; votemenu_finish_timer_active = false; votemenu_duration_value = 0; votemenu_finish_value = 0; for index, playerdata in pairs(player_list) do playerdata.votemenu_choice = nil; end end function SpawnBots(spawnpoint, totalcount, _spawncount) if (not spawnpoint or not totalcount) then return false; end if (not _spawncount) then _spawncount = totalcount; end local ent; if (type(spawnpoint) == "string") then ent = ents.FindByName(spawnpoint); if (not ent or not ent:IsValid()) then return false; end else ent = spawnpoint; end if (_spawncount > 0) then ent:Enable(); timer.Create(0.015, function() ent:Disable(); timer.Create(0.3, function() SpawnBots(ent, totalcount, _spawncount - 1); end, 1); end, 1); end return true; end