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