· 6 years ago · Jul 29, 2019, 11:12 AM
1///////////////////////////////////////////////////////////////////////////
2// ScriptedMode Enabler & Script loader
3// by kapkan
4// version 29-Jul-2019
5//
6// add your script to IncludeScipts() function to make it work
7//
8// new function ScriptedMode_Hook() added, usage:
9// ScriptedMode_Hook("AllowTakeDamage", function(dmgTable))
10// ScriptedMode_Hook("AllowBash", function(basher, bashee))
11// ScriptedMode_Hook("BotQuery", function(flag, bot, val))
12// ScriptedMode_Hook("CanPickupObject", function(object))
13// ScriptedMode_Hook("InterceptChat", function(msg, speaker))
14// ScriptedMode_Hook("UserConsoleCommand", function(player, args))
15//
16// details: https://developer.valvesoftware.com/wiki/List_of_L4D2_Script_Functions#Hooks_4
17//
18///////////////////////////////////////////////////////////////////////////
19
20local function IncludeScipts() {
21
22}
23
24printl("Well, okay")
25
26if (!("IncludeScriptDefault" in getroottable())) {
27 ::IncludeScriptDefault <- IncludeScript
28 ::IncludeScript <- function(name, scope = null) {
29 if (getstackinfos(2).src.find("vscripts/scriptedmode")) {
30 //calling IncludeScript from scriptedmode.nut, should always return true
31 try {
32 if (!IncludeScriptDefault(name, scope))
33 error("WARNING! Cannot include \"" + name + "\", file is missing or empty\n")
34 } catch (exception) {}
35 return true
36 } else {
37 return IncludeScriptDefault(name, scope)
38 }
39 }
40}
41
42local hooks = {
43 AllowTakeDamage = 1,
44 AllowBash = 2,
45 BotQuery = 3,
46 CanPickupObject = 1,
47 InterceptChat = 2,
48 UserConsoleCommand = 2
49}
50
51::ScriptedMode_Hook <- function(name, func) {
52 if (typeof(func) != "function" && typeof(func) != "native function" && !func.rawin("_call")) {
53 throw "Cannot hook event \"" + name + "\": second argument is not a valid function"
54 }
55 local infos = func.getinfos()
56 local argnum = ("typecheck" in infos) ? (infos.typecheck.len() - 1) : (infos.parameters.len() - 1)
57 if (name in hooks) {
58 if (argnum != hooks[name]) {
59 throw "Cannot hook event \"" + name + "\": function should accept " + hooks[name] + " argument(s)"
60 }
61 getroottable()[name + "_hooks"].push(func)
62 } else {
63 throw "Cannot hook unknown event \"" + name + "\""
64 }
65}
66
67local err_logger = function(exception, caller_name) {
68 error("[ScriptedMode hooks] AN ERROR HAS OCCURED [" + exception + "]\n\nCALLSTACK\n")
69 for(local i = 2;;i++) {
70 local stack = getstackinfos(i)
71 if (!stack) break
72 error("*FUNCTION [" + stack.func + "] " + stack.src + " line [" + stack.line + "]\n")
73 }
74}
75
76foreach(_name, _ in hooks) {
77 local name = _name //variable should be local
78 getroottable()[name + "_hooks"] <- []
79 g_MapScript[name] <- function(...) {
80 if(developer()) printl("ScriptedMode event fired: " + name)
81 local last_result = null
82 foreach(hook in getroottable()[name + "_hooks"]) {
83 try {
84 local args = [this]
85 args.extend(vargv)
86 last_result = hook.acall(args)
87 if (!last_result && name != "AllowBash") break
88 } catch (exception) {
89 err_logger(exception)
90 }
91 }
92 return last_result
93 }
94 printl("ScriptedMode Enabler: registered hook for " + name)
95}
96
97///////////////////////////////////////////////////////////////////////////
98// End of modified code
99// vanilla sm_utilities further
100///////////////////////////////////////////////////////////////////////////
101
102
103///////////////////////////////////////////////////////////////////////////////
104//
105// A collection of utilities and helpers for new script mutations
106//
107///////////////////////////////////////////////////////////////////////////////
108
109//printl("Loading sm_utilities.nut")
110
111//=========================================================
112// Teleport players to spawn points
113//=========================================================
114scripthelp_TeleportPlayersToStartPoints <- "Teleport players to start entities named by the argument - must have 4 spawnpoints"
115function TeleportPlayersToStartPoints( spawnpointName )
116{
117 // Teleport players to spawn points
118 local FindPlayerEntity = null
119 local FindSpawnPointEntity = null
120 local playercount = 0
121 local spawnpointcount = 0
122
123 while ( FindPlayerEntity = Entities.FindByClassname( FindPlayerEntity, "player" ) )
124 {
125 if ( !FindPlayerEntity.IsSurvivor() )
126 continue
127
128 ++playercount
129 if( FindSpawnPointEntity = Entities.FindByName( FindSpawnPointEntity, spawnpointName ) )
130 {
131 spawnpointcount++
132 FindPlayerEntity.SetOrigin( FindSpawnPointEntity.GetOrigin() )
133 }
134 }
135
136 if( spawnpointcount != 4 )
137 return false
138
139 return true
140}
141
142//---------------------------------------------------------
143// Entity group spawn flags used by MapSpawn table
144//---------------------------------------------------------
145::SPAWN_FLAGS <-
146{
147 SPAWN = (1<<0), // default. spawn on map spawn.
148 NOSPAWN = (1<<1), // include the entity group but do not spawn the entity
149 TARGETSPAWN = (1<<2), // spawn to a set number of targets
150}
151
152///////////////////////////////////////////////////////////////////////////////
153// StartBox tables and functions
154//
155// The startbox is a procedurally generated box on the ground around a point
156// which makes it easy to create a new start area for a map, and know when players first leave it
157// if you dont want to have your mutation start at usual safe house but also dont want to have to write code
158//
159// The code allows some amount of general control of a startbox, what model, what size, etc
160// And stores a few globals so it can easily remove itself on demand, knows its center object, etc
161///////////////////////////////////////////////////////////////////////////////
162
163g_RoundState.smStartboxList <- [] // all the startbox spawned barricade objects
164g_RoundState.smStartboxCenter <- null // the center object
165g_RoundState.smStartboxRemoveAll <- true // are we supposed to delete everything after (false for ground)
166
167//=========================================================
168// callbacks for managing the post-spawn startbox data - for later cleanup
169//=========================================================
170function StoreStartboxCB( startboxObj, rarity )
171{
172 g_RoundState.smStartboxList.append( startboxObj )
173}
174
175function StoreStartboxCenterCB( startboxcenterObj, rarity )
176{
177 g_RoundState.smStartboxCenter = startboxcenterObj
178}
179
180//-------------------------------------
181// These are the spawn tables for the center point and the two types of outer shell model used in startboxes
182//-------------------------------------
183StartboxSpeedbump_Info <-
184{
185 classname = "prop_dynamic"
186 model = "models/props_placeable/speedBump.mdl"
187 angles = Vector(0,0,0)
188 origin = Vector(0,0,0)
189}
190
191StartboxFloating_Info <-
192{
193 classname = "prop_dynamic"
194 model = "models/props_placeable/striped_barricade.mdl"
195 angles = Vector(0,0,0)
196 solid = "0"
197 origin = Vector(0,0,0)
198}
199
200StartboxCenter_Info <-
201{
202 classname = "info_item_position"
203 angles = Vector(0,0,0)
204 vscripts = "startbox"
205 thinkfunction = "Think"
206 movetype = "0"
207 spawnflags = "8"
208}
209
210function Startbox_Precache( )
211{
212 PrecacheEntityFromTable( StartboxFloating_Info )
213 PrecacheEntityFromTable( StartboxSpeedbump_Info )
214}
215
216//=========================================================
217// this spawns a start box around the named object, of width and height, using barriermodel
218//=========================================================
219scripthelp_SpawnStartBox <- "Auto-create a startbox that will give you a callback when the first player exits it"
220function SpawnStartBox( centerobjname, useFloating = true, width = 384, depth = 384, barriermodel = null, min_gap = 12 )
221{
222 local center_ent = Entities.FindByName( null, centerobjname )
223 if (center_ent == null || width <= 0 || depth <= 0)
224 return false
225
226 local visualToSpawn = useFloating ? StartboxFloating_Info : StartboxSpeedbump_Info
227 g_RoundState.smStartboxRemoveAll = useFloating
228
229 local box_origin = center_ent.GetOrigin()
230 local sbCenter = CreateSingleSimpleEntityFromTable( StartboxCenter_Info, center_ent )
231 g_RoundState.smStartboxCenter = sbCenter
232 local centerScr = g_RoundState.smStartboxCenter.GetScriptScope()
233 centerScr.SetSBSize( width, depth )
234
235 // compute the bounding area
236 local forw = center_ent.GetForwardVector()
237 local targ_angles = center_ent.GetAngles()
238 visualToSpawn.angles <- Vector( targ_angles.x, targ_angles.y, targ_angles.z )
239
240 // need to do something smarter with Z!!!
241
242// printl( "Start box forward looks like " + forw + " and " + Items_StartboxVisuals.SpawnInfo.angles )
243 local barr_width = 60 // can we get this from the model somehow
244
245 local num_wide = width / barr_width
246 if (num_wide < 1)
247 num_wide = 1 // deal with width smaller than barrier
248 local gap_wide = (width - (num_wide * barr_width)) / num_wide
249 while ( num_wide > 1 && gap_wide < min_gap )
250 {
251 num_wide--
252 gap_wide = (width - (num_wide * barr_width)) / num_wide
253 }
254
255 local num_deep = depth / barr_width
256 if (num_deep < 1)
257 num_deep = 1 // deal with width smaller than barrier
258 local gap_deep = (depth - (num_deep * barr_width)) / num_deep
259 while ( num_deep > 1 && gap_deep < min_gap )
260 {
261 num_deep--
262 gap_deep = (depth - (num_deep * barr_width)) / num_deep
263 }
264
265 // need to do Z smarter!!!
266 local front_mid = forw.Scale(depth/2.0)
267 local bk = forw.Scale(-depth)
268 local side_step = Vector ( -forw.y, forw.x, 0)
269 local per_barr = barr_width + gap_wide
270 local st = front_mid - (side_step * (per_barr * (num_wide - 1) / 2.0) )
271 st.z = front_mid.z
272
273 visualToSpawn.angles.y += 90;
274 for ( local i_w = 0; i_w < num_wide; i_w++ )
275 {
276 visualToSpawn.origin = box_origin + st + side_step * (per_barr * i_w)
277 g_RoundState.smStartboxList.append( CreateSingleSimpleEntityFromTable( visualToSpawn ) )
278 visualToSpawn.origin = box_origin + st + bk + side_step * (per_barr * i_w)
279 g_RoundState.smStartboxList.append( CreateSingleSimpleEntityFromTable( visualToSpawn ) )
280 }
281
282 // for the sides, just do the x,y -> -y,x trick, since we know forw is a unit vector
283 local side_mid = Vector( width/2.0 * forw.y, -width/2.0 * forw.x, width/2.0 * forw.z )
284 bk = Vector ( -width * forw.y, width * forw.x, -width * forw.z )
285 side_step = Vector ( forw.x, forw.y, 0)
286 per_barr = barr_width + gap_deep
287 st = side_mid - ( side_step * (per_barr * (num_deep - 1) / 2.0 ) )
288 st.z = side_mid.z
289
290 // then the left and right sides, i.e. the depth ones
291 visualToSpawn.angles.y += 90;
292 for ( local i_d = 0; i_d < num_deep; i_d++ )
293 {
294 visualToSpawn.origin = box_origin + st + side_step * (per_barr * i_d)
295 g_RoundState.smStartboxList.append( CreateSingleSimpleEntityFromTable( visualToSpawn ) )
296 visualToSpawn.origin = box_origin + st + bk + side_step * (per_barr * i_d)
297 g_RoundState.smStartboxList.append( CreateSingleSimpleEntityFromTable( visualToSpawn ) )
298 }
299
300// printl("Built startbox size " + width + "x" + depth + "\n chose " + num_wide + " across and " + num_deep + " deep\n which gave gaps of " + gap_wide + " and " + gap_deep)
301
302 return true
303}
304
305function ClearStartBox( )
306{
307 if ( g_RoundState.smStartboxRemoveAll )
308 {
309 foreach ( ent in g_RoundState.smStartboxList )
310 {
311 EntFire( ent.GetName(), "Kill" )
312 }
313 g_RoundState.smStartboxList <- null
314 }
315 if ( g_RoundState.smStartboxCenter != null )
316 {
317 EntFire( g_RoundState.smStartboxCenter.GetName(), "Kill" )
318 g_RoundState.smStartboxCenter = null
319 }
320 g_RoundState.smStartboxRemoveAll = false
321}
322
323
324///////////////////////////////////////////////////////////////////////////////
325//
326// Radio helper stuff
327//
328///////////////////////////////////////////////////////////////////////////////
329function RadioSpawnCB( entity, rarity)
330{
331 entity.ValidateScriptScope()
332 //get the radio's script scope
333 local radioScope = entity.GetScriptScope()
334
335 radioScope.Used <- function()
336 {
337 // Notify the map script that the starting line has been touched
338 if ("StartRadioUsed" in g_MapScript)
339 g_MapScript.StartRadioUsed() // if you need to do additional work
340 else
341 Director.ForceNextStage() // default is just "go to next stage"
342
343 EntFire( entity, "Lock" )
344 EmitSoundOn( "npc.soldier_RadioButton210", entity )
345 }
346
347// entity.ConnectOutput( "OnOpen", "Used" ) // lets put the function in before connecting it, for safety
348}
349
350
351///////////////////////////////////////////////////////////////////////////////
352// Debug helpers
353//
354// Things that are really for during development - you really should never call any of this
355// in final/real/workshop submitted code
356///////////////////////////////////////////////////////////////////////////////
357
358// if you want a table printed to console formatted like a table (dont we already have this somewhere?)
359scripthelp_DeepPrintTable <- "Print out a table (and subtables) to the console"
360function DeepPrintTable( debugTable, prefix = "" )
361{
362 if (prefix == "")
363 {
364 printl("{")
365 prefix = " "
366 }
367 foreach (idx, val in debugTable)
368 {
369 if ( typeof(val) == "table" )
370 {
371 printl( prefix + idx + " = \n" + prefix + "{")
372 DeepPrintTable( val, prefix + " " )
373 printl(prefix + "}")
374 }
375 else if ( typeof(val) == "string" )
376 printl(prefix + idx + "\t= \"" + val + "\"")
377 else
378 printl(prefix + idx + "\t= " + val)
379 }
380 if (prefix == " ")
381 printl("}")
382}
383
384
385
386///////////////////////////////////////////////////////////////////////////////
387// Misc helpers
388// Table management, run functions on lists, merge and add defaults to tables, etc
389///////////////////////////////////////////////////////////////////////////////
390
391// @TODO: is there not a squirrel default way to do this?
392scripthelp_DuplicateTable <- "This returns a deep copy of the passed in table"
393function DuplicateTable( srcTable )
394{
395 local result = clone srcTable;
396 foreach( key, val in srcTable )
397 {
398 if ( typeof( val ) == "table" )
399 {
400 result[ key ] = DuplicateTable( val )
401 }
402 }
403 return result;
404}
405
406// List helpers, table checks/defaults - run a member or function on entities in a list
407function RunMemberOnList( member_name, list_name )
408{
409 foreach (idx, val in list_name)
410 if ( member_name in val )
411 val[member_name]()
412}
413
414function RunFunctionOnList( func_name, list_name)
415{
416 foreach (idx, val in list_name)
417 func_name( val )
418}
419
420// if you want to place a callback into the Map level script scope if it isnt there
421// i.e. you can do "CheckOrSetMapCallback("CheckForFish", @(who) "HasFish" in who)"
422// and then it will check if there is a CheckForFish callback at map scope
423// if so, this call does nothing, and leaves it alone, if there isnt one, this is "injected" to mapscope
424// so that in later code, you know it is there and can just call it, rather than checking every time
425scripthelp_CheckOrSetMapCallback <- "Pass a callback and default @ for it, if it doesnt exist in current table, place it there"
426function CheckOrSetMapCallback( cb_name, cb_default )
427{
428 if ( !(cb_name in g_MapScript) )
429 {
430 g_MapScript[cb_name] <- cb_default
431 }
432}
433
434//=========================================================
435// Copy source table into the dest table.
436// Duplicate keys will be overwritten by the source table's value.
437// If the destination table doesn't exist, it will be created.
438// This first one is for Tables, the next one is for arrays
439//=========================================================
440function AddDefaultsToTable( srcname, srcscope, destname, destscope )
441{
442 if ( (destname in destscope) && typeof(destscope[destname]) != "table" )
443 {
444 printl( "Warning: Hey... you can't AddDefaultToTable of a non-table " + destname )
445 return
446 }
447
448 if ( (srcname in srcscope) && typeof(srcscope[srcname]) != "table" )
449 {
450 printl( "Warning: Hey... you can't AddDefaultToTable of a non-table " + srcname )
451 return
452 }
453
454 if ( !(destname in destscope) )
455 {
456 destscope[destname] <- {}
457 }
458
459 if ( srcname in srcscope )
460 {
461 local srctable = srcscope[srcname]
462 local desttable = destscope[destname]
463
464 smDbgLoud( " Table Copying " + srctable + " into " + desttable )
465 foreach ( key, val in srctable )
466 {
467 smDbgLoud( " Copying " + val + " into " + key )
468 desttable[key] <- val
469 }
470 }
471}
472
473function AddDefaultsToArray( srcname, srcscope, destname, destscope )
474{
475 if ( (destname in destscope) && typeof(destscope[destname]) != "array" )
476 {
477 printl( "Warning: Hey... you can't AddDefaultToArray of a non-array " + destname )
478 return
479 }
480
481 if ( (srcname in srcscope) && typeof(srcscope[srcname]) != "array" )
482 {
483 printl( "Warning: Hey... you can't AddDefaultToArray of a non-array " + srcname )
484 return
485 }
486
487 if ( !(destname in destscope) )
488 {
489 destscope[destname] <- []
490 }
491
492 if ( srcname in srcscope )
493 {
494 local srcarray = srcscope[srcname]
495 local destarray = destscope[destname]
496
497 smDbgLoud( " Array Copying " + srcarray + " into " + destarray )
498 foreach ( idx, val in srcarray )
499 {
500 smDbgLoud( " Checking on " + val + " [e0 is " + val[0] + "]" )
501 if ( !destarray.find( val ) )
502 {
503 destarray.append( val )
504 smDbgLoud( " decided to Copy " + val )
505 }
506 }
507 }
508}
509
510///////////////////////////////////////////////////////////////////////////////
511//
512// Scoring system - High-score/table type stuff
513// The dream here is that these map/mode specific scoreboards could persist in the cloud or something
514// But we dont really have any good way to do that without it exploding into infinite waste right now
515// so for now, they persist on the server machine hard drive - so yea, not so very cloud
516//
517// Still, keeping it as an example, basically
518///////////////////////////////////////////////////////////////////////////////
519
520g_MutationScores <- []
521g_DefaultScores <- []
522
523// if no scores, add some defaults
524function Scoring_SetDefaultScores( score_table )
525{
526 if (g_MutationScores == null || g_MutationScores.len() == 0)
527 g_MutationScores = score_table
528 g_DefaultScores = score_table
529}
530
531function Scoring_MakeFileName( mapname, modename )
532{
533 return "Scores_" + mapname + "_" + modename + ".txt"
534}
535
536function Scoring_LoadTable( mapname, modename )
537{
538 local saved_data = FileToString( Scoring_MakeFileName( mapname, modename ) )
539 if (saved_data != null)
540 {
541// printl("Think we loaded the file - and got ")
542// printl(saved_data)
543 local score_closure = compilestring( "return " + saved_data )
544 g_MutationScores = score_closure()
545 if (g_MutationScores.len() > 0 && ("time" in g_MutationScores[0]))
546 {
547 printl("Old score file! ignoring!")
548 g_MutationScores = g_DefaultScores
549 }
550 }
551 else
552 g_MutationScores = g_DefaultScores
553}
554
555function Scoring_SaveTable( mapname, modename )
556{
557 local save_string = "[ "
558 if (g_MutationScores && g_MutationScores.len() > 0)
559 {
560 foreach (idx, val in g_MutationScores)
561 {
562 save_string = save_string + "\n { name = \"" + val.name + "\" , score = " + val.score + " }, "
563 }
564 save_string = save_string + "\n]"
565 StringToFile( Scoring_MakeFileName( mapname, modename ), save_string )
566 }
567 else
568 printl("No ScoreTable to Save")
569}
570
571function Scoring_AddScore( new_name, new_score, lower_is_better = true )
572{
573 local score_pos = -1
574 local do_insert = false
575
576 foreach (idx, val in g_MutationScores)
577 {
578 if ( ( lower_is_better && new_score <= val.score) || ( (!lower_is_better) && new_score > val.score ))
579 {
580 do_insert = true
581 score_pos = idx
582 break
583 }
584 }
585 if (do_insert)
586 {
587 g_MutationScores.insert(score_pos, {name=new_name, score=new_score})
588 while (g_MutationScores.len() > 10)
589 {
590 g_MutationScores.remove(10)
591 }
592 }
593 else if ( g_MutationScores == null || g_MutationScores.len() < 10 ) // if we have <10 scores, add to end, else we already have 10
594 {
595 g_MutationScores.append({name=new_name, score=new_score})
596 score_pos = g_MutationScores.len() - 1
597 }
598
599 return score_pos
600}
601
602function Scoring_MakeName( )
603{
604 local score_names = ""
605 local player_count = 0
606 local FindPlayerEntity = null
607
608 while ( FindPlayerEntity = Entities.FindByClassname( FindPlayerEntity, "player" ) )
609 {
610 if (FindPlayerEntity.IsSurvivor())
611 {
612 if (player_count > 0)
613 score_names = score_names + ", "
614 score_names = score_names + FindPlayerEntity.GetPlayerName()
615 player_count += 1
616 }
617 }
618 return score_names
619}
620
621pos_strs <- [ "1st", "2nd", "3rd" ]
622function GetPosStr( pos )
623{
624 if (pos < 0)
625 return "out of the scoring"
626 else if (pos < 3)
627 return pos_strs[pos]
628 else
629 return (pos+1) + "th"
630}
631
632// need a score_time to display function
633scripthelp_TimeToDisplayString <- "Convert a # of seconds to a displayable time string form m:ss"
634function TimeToDisplayString( disp_time )
635{
636 local minutes = ( disp_time / 60 ).tointeger()
637 local seconds_10s = ( ( disp_time % 60) / 10 ).tointeger()
638 local seconds_1s = ( disp_time % 10 ).tointeger()
639 return minutes + ":" + seconds_10s + seconds_1s
640}
641
642// should this always just use Ticker?
643function Scoring_AddScoreAndBuildStrings( name, score, show_as_time = true, lower_is_better = true )
644{
645 local pos = Scoring_AddScore( name, score, lower_is_better )
646 local score_strings = {}
647 if (show_as_time)
648 score_strings.yourtime <- "Your Time was " + TimeToDisplayString(score) + "\n"
649 else
650 score_strings.yourscore <- "Your Score was " + score + "\n"
651 if (pos != -1)
652 score_strings.finish <- "and you finished " + GetPosStr(pos) + "\n"
653 // now the top 4
654 local scores_to_show = g_MutationScores.len() < 4 ? g_MutationScores.len() : 4
655 score_strings.topscores <- []
656 for (local a = 0; a < scores_to_show; a += 1 )
657 {
658 local score_display = show_as_time ? TimeToDisplayString(g_MutationScores[a].score) : g_MutationScores[a].score
659 score_strings.topscores.append( score_display + " " + g_MutationScores[a].name + "\n" )
660 }
661 return score_strings
662}
663
664///////////////////////////////////////////////////////////////////////////////
665//
666// Rescue Timer
667// @TODO: should be a class. Or maybe a larger table with these as subtables? probably
668//
669///////////////////////////////////////////////////////////////////////////////
670
671//---------------------------------------------------------
672// Rescue timer helpers so UI can interact, etc - by
673// convention Rescue Timer uses Timer3 (@todo re# from 0)
674//---------------------------------------------------------
675RescueTimerTimerID <- 3
676RescueTimerBlinkIfOff <- true
677RescueTimerFinished <- false
678RescueTimerActive <- false
679RescueHUDSlot <- null
680RescueHUDTitle <- null
681
682function RescueTimer_Init( hudTable, titleslot, mainslot, blinkifoff = true )
683{
684 hudTable.Fields.rescuetitle <- { slot = titleslot, name = "rescuetitle", dataval = "Find Fuel!", flags = 0 } // since we are going to tweak flags
685 hudTable.Fields.rescuetimer <- { slot = mainslot, name = "rescuetime", special = HUD_SPECIAL_TIMER3, flags = HUD_FLAG_BLINK | HUD_FLAG_NOBG }
686
687 RescueHUDTitle = hudTable.Fields.rescuetitle
688 RescueHUDSlot = hudTable.Fields.rescuetimer
689 RescueTimerBlinkIfOff = blinkifoff
690}
691
692//=========================================================
693//=========================================================
694function RescueTimer_Set( initial_time )
695{
696 HUDManageTimers( RescueTimerTimerID, TIMER_STOP, 0 )
697 HUDManageTimers( RescueTimerTimerID, TIMER_SET, initial_time ) // hmmm, we need "set countdown and stop all at once"
698 if ( RescueTimerBlinkIfOff )
699 RescueHUDSlot.flags = RescueHUDSlot.flags | HUD_FLAG_BLINK;
700 RescueTimerFinished = false
701 RescueTimerActive = false
702}
703
704//=========================================================
705// if 0, it just uses whatever you last set it to
706//=========================================================
707function RescueTimer_Start( initial_time = 0 )
708{
709 if (RescueTimerFinished)
710 return
711 HUDManageTimers( RescueTimerTimerID, TIMER_COUNTDOWN, initial_time )
712 if ( RescueTimerBlinkIfOff )
713 RescueHUDSlot.flags = RescueHUDSlot.flags & ~HUD_FLAG_BLINK;
714 RescueTimerActive = true
715
716 RescueHUDTitle.flags = RescueHUDTitle.flags & ~HUD_FLAG_BLINK
717 RescueHUDTitle.dataval = "Rescue in:"
718}
719
720//=========================================================
721//=========================================================
722function RescueTimer_Stop( )
723{
724 if (RescueTimerFinished)
725 return
726 HUDManageTimers( RescueTimerTimerID, TIMER_STOP, 0 )
727 if ( RescueTimerBlinkIfOff )
728 RescueHUDSlot.flags = RescueHUDSlot.flags | HUD_FLAG_BLINK
729 if ( HUDReadTimer( RescueTimerTimerID ) <= 0)
730 {
731 RescueTimerFinished = true
732 RescueHUDTitle.dataval = "Summoned"
733 }
734 else
735 {
736 RescueHUDTitle.flags = RescueHUDTitle.flags | HUD_FLAG_BLINK
737 RescueHUDTitle.dataval = "Find Fuel!"
738 }
739 RescueTimerActive = false
740}
741
742//=========================================================
743//=========================================================
744function RescueTimer_Get()
745{
746 if (RescueTimerFinished)
747 return 0
748 return HUDReadTimer( RescueTimerTimerID )
749}
750
751//=========================================================
752// should redo this to know whether it is actually moving
753// or not at the moment - hmmmm
754//=========================================================
755function RescueTimer_Tick()
756{
757 if ( !RescueTimerFinished && RescueTimerActive)
758 if ( HUDReadTimer( RescueTimerTimerID ) <= 0)
759 RescueTimer_Stop()
760}
761
762///////////////////////////////////////////////////////////////////////////////
763// Message Ticker helpers
764//
765// Creates a simple set of calls to manage a Ticker text line for convenience
766// Provides set string, set timeout, hide, blink, etc
767// And hooks a slowpoll so it can manage all the timing related stuff
768// NOTE: you certainly can ignore all this and just do your own whatever w/slot HUD_TICKER
769//
770// @todo: same as timer - should be a master table
771//
772///////////////////////////////////////////////////////////////////////////////
773
774// Ticker config variables and a few easy set/modify calls
775TickerShowing <- false
776TickerLastTime <- 0
777TickerTimeout <- 10
778TickerHUDSlot <- null
779TickerBlinkTime <- 5
780
781// this really wants to self-register an "Update" iff it has a timeout... hmmm
782function Ticker_SetTimeout( val )
783{
784 TickerTimeout = val
785}
786
787function Ticker_SetBlinkTime( val )
788{
789 TickerBlinkTime = val
790}
791
792function Ticker_SetBlink( BlinkOn )
793{
794 if (TickerHUDSlot == null)
795 {
796 printl("You need a HUD and to Ticker_AddToHud a Ticker before you use it!")
797 return false
798 }
799 if( BlinkOn )
800 {
801 TickerHUDSlot.flags = TickerHUDSlot.flags | HUD_FLAG_BLINK
802 }
803 else
804 {
805 TickerHUDSlot.flags = TickerHUDSlot.flags & ~HUD_FLAG_BLINK
806 }
807}
808
809function Ticker_Hide( )
810{
811 if (TickerShowing)
812 {
813 TickerShowing = false
814 TickerLastTime = 0
815 TickerHUDSlot.flags = TickerHUDSlot.flags | HUD_FLAG_NOTVISIBLE
816 }
817}
818
819//=========================================================
820// actually put our ticker config into a hudTable, and hook slow poll/init vars
821//=========================================================
822scripthelp_Ticker_AddToHud <- "adds ticker data to a passed in HUDTable"
823function Ticker_AddToHud( hudTable, strInit, blink = false )
824{
825 if ( ! ("Fields" in hudTable ) ) // if you are setting up a Ticker Only HUD
826 hudTable.Fields <- {}
827 hudTable.Fields.ticker <- { slot = HUD_TICKER, name = "ticker", dataval = strInit, flags = 0 }
828 TickerHUDSlot = hudTable.Fields.ticker // reference the real table
829 TickerShowing = true
830 TickerLastTime = Time()
831 if (blink)
832 TickerHUDSlot.flags = TickerHUDSlot.flags | HUD_FLAG_BLINK
833 ScriptedMode_AddSlowPoll( Ticker_SlowPoll )
834}
835
836// little helper if you want to just have a Ticker as a HUD, but really, just do this locally
837function CreateTickerOnlyHUD( startStr = "" )
838{
839 TickerHUD <- {}
840 Ticker_AddToHud( TickerHUD, startStr )
841 HUDSetLayout( TickerHUD )
842 HUDPlace( HUD_TICKER, 0.25, 0.04, 0.5, 0.08 )
843}
844
845//=========================================================
846// this is slowpoll function the ticker uses to manage the timers
847//=========================================================
848function Ticker_SlowPoll( )
849{
850 if (TickerShowing && TickerTimeout > 0) // i.e. on screen and has a fadeout time
851 if (Time() - TickerLastTime > TickerTimeout)
852 Ticker_Hide()
853 if ( TickerShowing && (TickerHUDSlot.flags & HUD_FLAG_BLINK) && ( Time() - TickerLastTime > TickerBlinkTime ) )
854 TickerHUDSlot.flags = TickerHUDSlot.flags & ~HUD_FLAG_BLINK
855}
856
857//=========================================================
858// This puts a new string into the Ticker, and can optionally also set a new timeout value
859//=========================================================
860scripthelp_Ticker_NewStr <- "sets the current Ticker string, w/an optional timeout value"
861function Ticker_NewStr( newStr, newTimeout = -1 )
862{
863 if (TickerHUDSlot == null)
864 {
865 printl("You need a HUD and to Ticker_AddToHud a Ticker before you use it!")
866 return false
867 }
868 if (newTimeout != -1)
869 Ticker_SetTimeout( newTimeout )
870 TickerHUDSlot.dataval = newStr
871 if (!TickerShowing)
872 {
873 TickerShowing = true
874 TickerHUDSlot.flags = TickerHUDSlot.flags & ~HUD_FLAG_NOTVISIBLE
875 }
876 TickerLastTime = Time()
877 return true
878}
879
880///////////////////////////////////////////////////////////////////////////////
881//
882// "clearout" manager - for waiting for a wave to end
883//
884// There is of course STAGE_CLEAROUT, if you want to just let the C++ side manage waiting for the mobs to be cleared
885// But if you want a more specific set of clearout behaviors (different timings, rules on mob types, whatever)
886// You can use this script based Clearout "wave" manager (or extend it, or write your own)
887//
888// It takes a "ClearoutTable" which parameterizes the clearout you want
889// and then is called once at the start of the panic wave (to track mob counts) [i.e. ClearoutNotifyPanicStart]
890// and then when the panic wave itself ends (i.e. the director thinks it is done spawning stuff)
891// rather than going into a STAGE_CLEAROUT, you go into an infinite STAGE_DELAY [and call ClearoutStart]
892// except you also a running a SlowPoll of Clearout_Tick (ClearoutStart automatically sets this up and makes it happen)
893// and that _Tick checks the counts/monitors mobs checking against the Table's goals
894// when the requirements are met, the slowpoll is removed and ForceNextStage is called to move on
895//
896///////////////////////////////////////////////////////////////////////////////
897
898//---------------------------------------------------------
899// a default clearout config - if you dont supply one this is used
900defaultClearout <-
901{
902 commons = 2
903 specials = 1
904 tanks = 0
905 witches = 0
906 plateautime = 5
907 plateaucommons = 6
908 plateauspecials = 1
909}
910
911//---------------------------------------------------------
912// globals to store the current clearout config table
913// the flag is for whether you want verbose console feedback during clearout
914::g_ClearoutLoud <- false
915::g_ClearoutTable <- null
916
917//=========================================================
918// we use the clearout table to see whether the wave is really done
919// (i.e. have we spawned everything and they are dead)
920//=========================================================
921function Clearout_Tick( )
922{
923 if (g_ClearoutTable == null)
924 {
925 printl("Hey - how did we get Ticked with no clearout table?!?")
926 ScriptedMode_RemoveSlowPoll(Clearout_Tick)
927 // remove the slowpoll
928 return
929 }
930
931 local bDoMoveOn = false
932 local bTimerDone = false
933 local infStats = {}
934 GetInfectedStats( infStats )
935
936 if (g_ClearoutLoud)
937 {
938 local pt = "plateautarget" in g_ClearoutTable ? g_ClearoutTable.plateautarget : -1
939 local wt_str = " also witches " + infStats.Witches + "/" + g_ClearoutTable.witches + " tanks " + infStats.Tanks + "/" + g_ClearoutTable.tanks
940 printl("CTick: @ " + Time() + " goal " + pt + " c " + infStats.Commons + "/" + g_ClearoutTable.lastcommons + " s " + infStats.Specials + "/" + g_ClearoutTable.lastspecials + wt_str )
941 }
942
943 local bStaticCounts = (infStats.Commons == g_ClearoutTable.lastcommons && infStats.Specials == g_ClearoutTable.lastspecials)
944
945 if ("plateautarget" in g_ClearoutTable)
946 {
947 if (!bStaticCounts)
948 g_ClearoutTable.plateautarget = Time() + g_ClearoutTable.plateautime
949 else if (Time() > g_ClearoutTable.plateautarget)
950 bTimerDone = true
951 g_ClearoutTable.lastspecials = infStats.Specials
952 g_ClearoutTable.lastcommons = infStats.Commons
953 }
954
955 if (bStaticCounts)
956 {
957 if ( Time() > g_ClearoutTable.ubertimeout)
958 {
959 bDoMoveOn = true
960 printl("Warning: UberTimeout Giving up!")
961 }
962 else
963 g_ClearoutTable.ubertimeout = Time() + 60
964 }
965
966 // if it is way way long w/no changes - just go to next phase no matter what is happening
967 if (!bDoMoveOn && ("specialtarget" in g_ClearoutTable && infStats.SpecialsSpawned < g_ClearoutTable.specialtarget) )
968 return // this is the "havent spawned enough yet" case
969 // do this after the plateau testing - really need to combine so stuck specials dont end the wave - hmmmm
970
971 if (bTimerDone)
972 {
973 if ( infStats.Commons <= g_ClearoutTable.plateaucommons && infStats.Specials <= g_ClearoutTable.plateauspecials )
974 {
975 if (infStats.Tanks <= g_ClearoutTable.tanks && infStats.Witches <= g_ClearoutTable.witches)
976 bDoMoveOn = true
977 if (g_ClearoutLoud)
978 printl("TimerDone - MoveOn now " + bDoMoveOn )
979 }
980 if (!bDoMoveOn && ("stopspecials" in g_ClearoutTable && SessionOptions.MaxSpecials != 0) ) // as soon as we plateau, go to maxspecials zero
981 {
982 SessionOptions.MaxSpecials = 0 // note: if we have a specialtarget, this wont happen till we hit it
983 if (g_ClearoutLoud)
984 printl("Setting MaxSpecials and Assault")
985 }
986 }
987
988 if (!bDoMoveOn)
989 {
990 if (infStats.Specials <= g_ClearoutTable.specials && infStats.Commons <= g_ClearoutTable.commons &&
991 infStats.Tanks <= g_ClearoutTable.tanks && infStats.Witches <= g_ClearoutTable.witches)
992 bDoMoveOn = true
993 if (g_ClearoutLoud)
994 printl("Moveon " + bDoMoveOn + " from infStats w/t of " + infStats.Witches + "/" + infStats.Tanks + " and c/s " + infStats.Commons + "/" + infStats.Specials )
995 }
996
997 if (bDoMoveOn)
998 { // force next stage
999 g_ClearoutTable.rawdelete("specialtarget")
1000 g_ClearoutTable = null
1001 ScriptedMode_RemoveSlowPoll( Clearout_Tick )
1002 if (g_ClearoutLoud)
1003 printl("Clearout Moving on (ps. trying to RemoveSlowPoll...)") // make it a return false instead?
1004 Director.ForceNextStage() // NOTE: this is a synchronous call - be warned!
1005 }
1006}
1007
1008//=========================================================
1009// This is to store off the current info about counts, so that clearout can analyze that
1010//=========================================================
1011function ClearoutNotifyPanicStart( clearoutTable )
1012{
1013 local infStats = {}
1014 GetInfectedStats( infStats )
1015 if ("specialcount" in g_ClearoutTable) // gonna need to figure out when we've spawned enough
1016 g_ClearoutTable.specialtarget <- infStats.SpecialsSpawned + g_ClearoutTable.specialcount
1017}
1018
1019//=========================================================
1020// this is how you start the script based clearout
1021//=========================================================
1022function ClearoutStart( clearoutTable )
1023{
1024 local bNeedPoll = (g_ClearoutTable == null)
1025 if (clearoutTable)
1026 g_ClearoutTable = clearoutTable
1027 else
1028 g_ClearoutTable = defaultClearout
1029 local infStats = {}
1030 GetInfectedStats( infStats )
1031 if ( ("specialcount" in g_ClearoutTable) && (!("specialtarget" in g_ClearoutTable)) ) // gonna need to figure out when we've spawned enough
1032 g_ClearoutTable.specialtarget <- infStats.SpecialsSpawned + g_ClearoutTable.specialcount
1033 if ("plateautime" in g_ClearoutTable)
1034 g_ClearoutTable.plateautarget <- Time() + g_ClearoutTable.plateautime
1035 g_ClearoutTable.ubertimeout <- Time() + 60
1036 g_ClearoutTable.lastcommons <- infStats.Commons
1037 g_ClearoutTable.lastspecials <- infStats.Specials
1038 if (!("plateaucommons" in g_ClearoutTable))
1039 g_ClearoutTable.plateaucommons <- 10
1040 if (!("plateauspecials" in g_ClearoutTable))
1041 g_ClearoutTable.plateauspecials <- 1
1042 if (bNeedPoll) // now make sure to add the Tick
1043 ScriptedMode_AddSlowPoll( Clearout_Tick )
1044 SessionOptions.SpecialInfectedAssault = 1 // new specials will attack - though shouldnt be any
1045 StartAssault() // tell existing ones to attack, too
1046 if ( (!("specialcount" in g_ClearoutTable)) && (!("specialcontinue" in g_ClearoutTable)) )
1047 SessionOptions.MaxSpecials = 0 // if you dont have a "# of specials" wave, stop new specials too
1048 printl("Starting script based Clearout with C:" + infStats.Commons + " and s:" + infStats.Specials )
1049}
1050
1051//=========================================================
1052// Go through and remove map entities based on the sanitize table
1053//=========================================================
1054function SanitizeMap( sanitizeTable )
1055{
1056 foreach( key, value in sanitizeTable )
1057 {
1058 local delay = 0
1059 local param = 0
1060 local targetname = ""
1061 local searchname = ""
1062 local searchfunc = null
1063 local input = value.input
1064
1065 // set delay and param if they are used
1066 if ( "delay" in value ) delay = value.delay
1067 if ( "param" in value ) param = value.param
1068
1069 // special handling for the kill command - call directly to avoid EntFire delay
1070 local kill = ( input.find( "kill" ) != null )
1071
1072 if ( "model" in value )
1073 {
1074 // if the table has a model key then operate on all entities in the map that use the model
1075 targetname = "!activator"
1076 searchname = value.model
1077 searchfunc = Entities.FindByModel
1078 }
1079 else if ( "targetname" in value )
1080 {
1081 targetname = value.targetname
1082 searchname = targetname
1083 searchfunc = Entities.FindByName
1084 }
1085 else if ( "classname" in value )
1086 {
1087 if ( "position" in value )
1088 {
1089 // Special case to find a single entity
1090 local entity = FindClassnameByPosition( value.classname, value.position )
1091
1092 SanitizeEntity( entity, "!activator", value.input, param, delay, kill )
1093 }
1094 else
1095 {
1096 targetname = value.classname
1097 searchname = targetname
1098 searchfunc = Entities.FindByClassname
1099 }
1100 }
1101
1102 if ( searchfunc )
1103 {
1104 local entity = null
1105 entity = searchfunc.call( Entities, entity, searchname )
1106
1107 while( entity )
1108 {
1109 // if we're sanitizing by region only sanitize if we're in the region
1110 if( "region" in value )
1111 {
1112 if( EntityInsideRegion( entity, value.region ) )
1113 {
1114 SanitizeEntity( entity, targetname, input, param, delay, kill )
1115 }
1116 }
1117 else
1118 {
1119 SanitizeEntity( entity, targetname, input, param, delay, kill )
1120 }
1121
1122 entity = searchfunc.call( Entities, entity, searchname )
1123 }
1124 }
1125 }
1126}
1127
1128//=========================================================
1129// Checks to see if an entity is inside a region
1130//
1131// A region is defined by two entities that share the same name.
1132// They mark the corners of the region.
1133//
1134// entity: The handle to the entity to find
1135// regionName: The name of the entities that define the region
1136//=========================================================
1137function EntityInsideRegion( entity, regionName )
1138{
1139 if( !entity )
1140 {
1141 return false
1142 }
1143
1144 // find regionName pairs
1145 local count = 0
1146 local regionCorners = []
1147
1148 local foundEnt = Entities.FindByName( null, regionName )
1149
1150 // collect the corner entities of the region
1151 while( foundEnt )
1152 {
1153 count++
1154 regionCorners.append( foundEnt )
1155
1156 foundEnt = Entities.FindByName( foundEnt, regionName )
1157 }
1158
1159 if( count != 2 )
1160 {
1161 printl(" ** EntityInsideRegion() error: Found " + count + " entitie(s) with the region name: " + regionName + "! Expected to find only upper and lower corner ents - Aborting.")
1162 return false
1163 }
1164
1165 local vec0 = regionCorners[0].GetOrigin()
1166 local vec1 = regionCorners[1].GetOrigin()
1167
1168 local entityOrigin = entity.GetOrigin()
1169
1170 if( NumBetween( vec0.x, vec1.x, entityOrigin.x ) && NumBetween( vec0.y, vec1.y, entityOrigin.y ) && NumBetween( vec0.z, vec1.z, entityOrigin.z ) )
1171 {
1172 return true
1173 }
1174 else
1175 {
1176 return false
1177 }
1178}
1179
1180//=========================================================
1181// is num between a,b where a and b do not have to be ordered
1182//=========================================================
1183function NumBetween( a, b, num )
1184{
1185 return ( b > a ? num > a && num < b : num > b && num < a )
1186}
1187
1188//=========================================================
1189// actually do the sanitize based on the parsed table data
1190//=========================================================
1191function SanitizeEntity( entity, name, input, param, delay, kill )
1192{
1193 if ( !entity )
1194 {
1195 return
1196 }
1197
1198 if( entity.GetClassname() == "info_item_position" )
1199 {
1200 // do not sanitize item position entities!
1201 return
1202 }
1203
1204 if ( kill )
1205 {
1206 entity.Kill()
1207 }
1208 else
1209 {
1210 EntFire( name, input, param, delay, entity )
1211 }
1212}
1213
1214//=========================================================
1215// Find an entity near a position and of a particular class and fire an input into it.
1216// classname is the name of the class to find. e.g., "info_target"
1217// position is a comma delimited string for the entity position. e.g., "100, 200, 300"
1218//=========================================================
1219function FindClassnameByPosition( classname, position )
1220{
1221 local SEARCH_DIST = 2 // radius to use for search
1222
1223 local cur_ent = Entities.FindByClassnameNearest( classname, position, SEARCH_DIST )
1224 if ( !cur_ent )
1225 {
1226 printl(" *** Sanitize Error: Couldn't find a " + classname + " at " + position )
1227 }
1228
1229 return cur_ent
1230}
1231
1232//=========================================================
1233// Take a comma delimited string in the format "x, y, z" and return a vector
1234//=========================================================
1235function StringToVector( str, delimiter )
1236{
1237 local vec = Vector( 0, 0, 0 )
1238
1239 local result = split( str, delimiter )
1240
1241 vec.x = result[0].tointeger()
1242 vec.y = result[1].tointeger()
1243 vec.z = result[2].tointeger()
1244
1245 return vec
1246}
1247
1248///////////////////////////////////////////////////////////////////////////////
1249// dynamic spawn helpers
1250// for Melee Weapons, Particle Systems, and then some more complex stuff for hints
1251///////////////////////////////////////////////////////////////////////////////
1252
1253function SpawnMeleeWeapon( weaponname, pos, ang )
1254{
1255 local weaponMeleeSpawnInfo =
1256 {
1257 classname = "weapon_melee_spawn"
1258 origin = pos
1259 angles = ang
1260 spawnflags = "2"
1261 count = "1"
1262 melee_weapon = weaponname
1263 solid = "6"
1264 }
1265 return CreateSingleSimpleEntityFromTable( weaponMeleeSpawnInfo )
1266}
1267
1268function CreateParticleSystemAt( srcEnt, vOffset, particleName, startActive = true, overrideTable = null)
1269{
1270 local particleSpawnInfo =
1271 {
1272 classname = "info_particle_system"
1273 effect_name = particleName
1274 targetname = "our_particles"
1275 render_in_front = "0"
1276 start_active = startActive ? "1" : "0"
1277 origin = vOffset
1278 }
1279 if (overrideTable)
1280 InjectTable( overrideTable, particleSpawnInfo )
1281 return CreateSingleSimpleEntityFromTable( particleSpawnInfo, srcEnt )
1282}
1283
1284//=========================================================
1285// create hint at versus create hint on? - on hintEnt,
1286// with Str, with overrides from Table
1287//=========================================================
1288function CreateHintOn( hintTargetName, hintPos, hintStr, hintTable, callback = null )
1289{
1290 local hintShell =
1291 {
1292 function GetSpawnList() { return [ EntityGroup.SpawnTables.hint ] }
1293 function GetEntityGroup() { return EntityGroup }
1294 EntityGroup =
1295 {
1296 SpawnTables =
1297 {
1298 hint =
1299 {
1300 initialSpawn = true
1301 SpawnInfo =
1302 {
1303 classname = "env_instructor_hint"
1304 hint_allow_nodraw_target = "1"
1305 hint_alphaoption = "0"
1306 hint_auto_start = "1"
1307 hint_binding = "+use"
1308 hint_caption = "Default Dynamic Hint!"
1309 hint_color = "255 255 255"
1310 hint_forcecaption = "0" // diff
1311 hint_icon_offscreen = "icon_tip"
1312 hint_icon_offset = "0"
1313 hint_icon_onscreen = "use_binding"
1314 hint_instance_type = "2" // show multiple
1315 hint_nooffscreen = "0" // 0/1
1316 hint_pulseoption = "0"
1317 hint_range = "130" // 130/300
1318 hint_shakeoption = "0"
1319 hint_static = "0" // 0/1 - 1 is show on hud, show in world
1320 hint_target = "prop_resource"
1321 hint_timeout = "0"
1322 targetname = "hint"
1323 origin = Vector( 0, 0, 28.2918 )
1324 connections =
1325 {
1326 OnUser4 =
1327 {
1328 cmd1 = "!selfEndHint0-1"
1329 cmd2 = "!selfKill0.01-1"
1330 }
1331 }
1332 }
1333 }
1334 }
1335 }
1336 }
1337
1338 if (hintTable)
1339 foreach (idx, val in hintTable) // merge
1340 hintShell.EntityGroup.SpawnTables.hint.SpawnInfo[idx] <- val
1341 if (hintStr)
1342 hintShell.EntityGroup.SpawnTables.hint.SpawnInfo.hint_caption = hintStr
1343 if (hintTargetName)
1344 {
1345 hintShell.EntityGroup.SpawnTables.hint.SpawnInfo.hint_target = hintTargetName
1346 }
1347
1348 if (hintPos == null && hintTargetName != null)
1349 {
1350 local hintEnt = Entities.FindByName(null, hintTargetName)
1351 if (hintEnt)
1352 hintPos = hintEnt.GetOrigin()
1353 else
1354 printl("Cant find an entity for spawning hint")
1355 }
1356
1357 local hintGroup = hintShell.GetEntityGroup()
1358 if( callback )
1359 {
1360 if( "PostPlaceListCB" in this )
1361 {
1362 printl(" CreateHintOn: Error! Trying to create a PostPlaceListCB when one already exists! Stomping existing CB.")
1363 // @todo: go ahead and chain this? ugh...
1364 }
1365 hintGroup.PostPlaceListCB <- callback
1366 }
1367 SpawnSingleAt( hintGroup, hintPos, QAngle(0,0,0) )
1368}
1369
1370//=========================================================
1371// inject the override data into the baseTable - i.e. add any new ones, overwrite dupes
1372//=========================================================
1373function InjectTable( overrideTable, baseTable )
1374{
1375 foreach (idx, val in overrideTable)
1376 {
1377 if ( typeof(val) == "table" )
1378 {
1379 if (! (idx in baseTable) )
1380 {
1381 baseTable[idx] <- {} // make sure there is a table here to inject into in the base
1382 }
1383 InjectTable( val, baseTable[idx] )
1384 }
1385 else
1386 {
1387 if (val == null)
1388 baseTable.rawdelete(idx) // specify null to remove a key!
1389 else
1390 baseTable[idx] <- overrideTable[idx]
1391 }
1392 }
1393}
1394
1395// all the training hint entity names generated from the InstructorHintTable
1396g_RoundState.TrainingHintList <- []
1397
1398//=========================================================
1399// Callback that runs after CreateHintOn creates a hint entity.
1400// The hint is then shown and stored in a list for subsequent
1401// use (such as ending, killing hints when they're not needed)
1402//=========================================================
1403function TrainingHintCB( entNameList )
1404{
1405 foreach (idx, val in entNameList)
1406 {
1407 if ( val != null )
1408 {
1409 EntFire( val, "ShowHint", 0, 1 )
1410
1411 g_RoundState.TrainingHintList.append( val )
1412 }
1413 }
1414}
1415
1416//=========================================================
1417// Callback that runs when the target entity for a hint gets created.
1418//=========================================================
1419function TrainingHintTargetCB( entNameList )
1420{
1421 if( entNameList.len() != 1 )
1422 {
1423 printl(" TrainingHintTargetCB: Error! List contains more than one object, expected one.")
1424 }
1425
1426 SessionState.TrainingHintTargetNextName <- entNameList[0]
1427}
1428
1429//=========================================================
1430// This helper kills off all the hints in the training hint list
1431// so you can give a pile of hints to spawn, then when you want (on time, or on some event) go clear them out
1432//=========================================================
1433function EndTrainingHints( delay )
1434{
1435 foreach( idx, value in g_RoundState.TrainingHintList )
1436 {
1437 EntFire( value, "kill", 0, delay )
1438 }
1439}
1440
1441//=========================================================
1442// Will find all entities in the supplied table and create instructor hints at their respective positions.
1443//
1444// If you want finer control over the hint parameters, consider using CreateInstructorHint() to meticulously
1445// define each individual hint
1446//
1447// Expects a table formatted like this:
1448//
1449// { hintEntityName = "<name for this hint entity>", targetEntityName = "<name of target entity>", hintText = "<Actual hint display text>", hintOnScreenIcon = "<name of hint icon>" }
1450//
1451// NOTE: This function will spawn a hint entity for each entry in the table that you pass in.
1452// Each such entity will receive the hintEntityName that you supply in the passed table.
1453// You may not care what name this is, but take care that it doesn't collide with something else important.
1454//
1455// NOTE: This function will attach a hint to ALL entities found matching targetEntityName.
1456// So use unique names for each target entity unless you want this behavior.
1457//=========================================================
1458function CreateSimpleInstructorHints( hintTable )
1459{
1460 local HintDefaultSpawnInfo =
1461 {
1462 hint_auto_start = "1"
1463 hint_range = "0"
1464 hint_suppress_rest = "1"
1465 hint_nooffscreen = "1"
1466 hint_forcecaption = "1"
1467 hint_icon_onscreen = "icon_shield"
1468 }
1469
1470 foreach( key, value in hintTable )
1471 {
1472 local ent = null
1473 HintDefaultSpawnInfo.targetname <- value.hintEntityName
1474 do
1475 {
1476 ent = Entities.FindByName( ent, value.targetEntityName )
1477
1478 if( ent )
1479 {
1480 local hintTargetName = value.hintEntityName + "_target"
1481 CreateHintTarget( hintTargetName, ent.GetOrigin(), null, TrainingHintTargetCB )
1482 CreateHintOn( SessionState.TrainingHintTargetNextName, ent.GetOrigin(), value.hintText, HintDefaultSpawnInfo, TrainingHintCB )
1483
1484 // delete the training hint next name key
1485 SessionState.rawdelete( "TrainingHintTargetNextName" )
1486 }
1487 } while( ent )
1488 }
1489}
1490
1491//=========================================================
1492// Creates a hint on a target entity that uses a model.
1493//
1494// Expects a table with this format:
1495//
1496// { targetname = "<name to give created hint>", mdl = "<name of model used by hint target>", targetEntName = "<name (or substring) of entity that uses the mdl>", hintText = "<text to display>" }
1497//
1498// NOTE: The MDL and targetEntName are both necessary in order to narrow down the entity for hint attachment. You do not need to provide the exact name of the target entity, just a substring contained
1499// by the name.
1500//=========================================================
1501function CreateTrainingHints( hintTable )
1502{
1503 local HintSpawnInfo =
1504 {
1505 hint_auto_start = "1"
1506 hint_range = "0"
1507 hint_suppress_rest = "1"
1508 hint_nooffscreen = "1"
1509 hint_forcecaption = "1"
1510 hint_icon_onscreen = "icon_shield"
1511 }
1512
1513 foreach( key, value in hintTable )
1514 {
1515 local ent = null
1516 HintSpawnInfo.targetname <- value.targetname
1517 do
1518 {
1519 ent = Entities.FindByModel( ent, value.mdl )
1520
1521 if( ent )
1522 {
1523 if( ent.GetName().find( value.targetEntName ) != null && ent.GetClassname() != "info_item_position" ) // ignore item position entities
1524 {
1525 local hintTargetName = value.targetname + "_target"
1526 CreateHintTarget( hintTargetName, ent.GetOrigin(), null, TrainingHintTargetCB )
1527 CreateHintOn( SessionState.TrainingHintTargetNextName, ent.GetOrigin(), value.hintText, HintSpawnInfo, TrainingHintCB )
1528
1529 // delete the training hint next name key
1530 SessionState.rawdelete( "TrainingHintTargetNextName" )
1531 }
1532 }
1533 } while( ent )
1534 }
1535}
1536
1537//=========================================================
1538// Creates a info_target_instructor_hint entity
1539//=========================================================
1540function CreateHintTarget( hintTargetName, hintPos, hintTargetTable, callback )
1541{
1542 local hintTargetShell =
1543 {
1544 function GetSpawnList() { return [ EntityGroup.SpawnTables.hintTarget ] }
1545 function GetEntityGroup() { return EntityGroup }
1546 EntityGroup =
1547 {
1548 SpawnTables =
1549 {
1550 hintTarget =
1551 {
1552 initialSpawn = true
1553 SpawnInfo =
1554 {
1555 classname = "info_target_instructor_hint"
1556 targetname = "_target"
1557 origin = Vector( 0,0,0 )
1558 }
1559 }
1560 }
1561 }
1562 }
1563
1564 if (hintTargetTable)
1565 {
1566 foreach (idx, val in hintTargetTable)
1567 {
1568 hintTargetShell.EntityGroup.SpawnTables.hint.SpawnInfo[idx] <- val
1569 }
1570 }
1571
1572 if (hintTargetName)
1573 hintTargetShell.EntityGroup.SpawnTables.hintTarget.SpawnInfo.targetname = hintTargetName
1574
1575 local hintGroup = hintTargetShell.GetEntityGroup()
1576 hintGroup.PostPlaceListCB <- callback
1577 SpawnSingleAt( hintGroup, hintPos, QAngle(0,0,0) )
1578}
1579
1580//=========================================================
1581// Creates a "simple" singleton entity from KeyValues - by wrapping an EntityGroup shell around it
1582//=========================================================
1583// this is the callback that simply stores off the entity for returning to the caller
1584::_SingleSimpleEntityTempStore <- null
1585function _SingleSimpleEntityCB( entity, rarity )
1586{
1587 ::_SingleSimpleEntityTempStore = entity
1588}
1589
1590// builds an EntityGroup shell around the spawnTable data
1591function BuildShell( spawnTable )
1592{
1593 local simpleShell =
1594 {
1595 groupname = "sm_utilities::SimpleShell"
1596 function GetSpawnList() { return [ EntityGroup.SpawnTables.simple ] }
1597 function GetEntityGroup() { return EntityGroup }
1598 EntityGroup =
1599 {
1600 SpawnTables =
1601 {
1602 simple =
1603 {
1604 initialSpawn = true
1605 SpawnInfo =
1606 {
1607 origin = Vector(0,0,0)
1608 }
1609 }
1610 }
1611 }
1612 }
1613 if (!("targetName" in spawnTable))
1614 spawnTable.targetname = "simple"
1615 local overrides = { simple = { SpawnInfo = spawnTable } }
1616 InjectTable( overrides, simpleShell.GetEntityGroup().SpawnTables )
1617 return simpleShell
1618}
1619
1620// wrap the table in a shell, then create an entity for the table, and then return the entity
1621function CreateSingleSimpleEntityFromTable( spawnTable, entityAt = null )
1622{
1623 // how do we precache these!!!
1624 local singleSimpleShell =
1625 {
1626 function GetSpawnList() { return [ EntityGroup.SpawnTables.singlesimple ] }
1627 function GetEntityGroup() { return EntityGroup }
1628 EntityGroup =
1629 {
1630 SpawnTables =
1631 {
1632 singlesimple =
1633 {
1634 PostPlaceCB = _SingleSimpleEntityCB
1635 initialSpawn = true
1636 SpawnInfo =
1637 {
1638 origin = Vector(0,0,0) // do we need/want this key? if you remove it, change the = to <- below
1639 angles = QAngle(0,0,0)
1640 }
1641 }
1642 }
1643 }
1644 }
1645
1646 if (!("targetname" in spawnTable))
1647 spawnTable.targetname <- "singlesimple"
1648 local overrides = { singlesimple = { SpawnInfo = spawnTable } }
1649
1650 InjectTable( overrides, singleSimpleShell.GetEntityGroup().SpawnTables )
1651 if (entityAt != null)
1652 {
1653 singleSimpleShell.EntityGroup.SpawnTables.singlesimple.SpawnInfo.origin = entityAt.GetOrigin()
1654 singleSimpleShell.EntityGroup.SpawnTables.singlesimple.SpawnInfo.angles = entityAt.GetAngles()
1655 }
1656 SpawnSingle( singleSimpleShell.GetEntityGroup() )
1657
1658 local tmp = ::_SingleSimpleEntityTempStore
1659 ::_SingleSimpleEntityTempStore <- null
1660 return tmp
1661}