aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts/vscripts
diff options
context:
space:
mode:
authorBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-08-31 23:14:58 +0100
committerBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-08-31 23:14:58 +0100
commit9a96d0bff56f1969c68bb52a2f33296095bdc67d (patch)
tree4175928e488632705692e3cccafa1a38dd854615 /Northstar.CustomServers/mod/scripts/vscripts
parent27bd240871b7c0f2f49fef137718b2e3c208e3b4 (diff)
downloadNorthstarMods-9a96d0bff56f1969c68bb52a2f33296095bdc67d.tar.gz
NorthstarMods-9a96d0bff56f1969c68bb52a2f33296095bdc67d.zip
move to new mod format
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_anim.gnut1395
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_auto_precache.gnut771
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_bubble_shield.gnut524
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_common.gnut853
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_player_input.gnut552
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_control_panel.gnut727
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_dogfighter.gnut343
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_entitystructs.gnut692
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_global_entities.gnut343
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_harvester.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_health_regen.gnut172
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_init.gnut40
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut261
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_mapspawn.gnut217
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_menu_callbacks.gnut17
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_misc.gnut40
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_networkvars.gnut169
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_objective.gnut108
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_on_spawned.gnut508
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_pain_death_sounds.gnut455
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_passives.gnut1657
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_ping.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_powerup.gnut93
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_remote_functions_mp.gnut943
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_script_movers.gnut1783
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_script_movers_light.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_script_triggers.gnut318
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_side_notifications.gnut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_store.gnut38
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_trigger_functions.gnut585
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_utility.gnut4394
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_utility_shared.nut4069
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_viewcone.gnut520
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_vscript.gnut84
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/_xp.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_boss_titan.gnut794
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_chatter.gnut129
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_cloak_drone.gnut678
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut1388
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_emp_titans.gnut181
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_gunship.gnut97
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_lethality.gnut97
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_faces.gnut226
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_jobs.gnut600
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvins.gnut141
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_spectres.gnut7
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_titans.gnut395
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_nuke_titans.gnut129
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_personal_shield.gnut371
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_pilots.gnut808
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_sniper_titans.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut787
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_mp.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_sp.gnut17
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn.gnut696
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn_content.gnut879
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spectre.gnut131
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut606
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stationary_firing_positions.gnut261
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_suicide_spectres.gnut576
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret.gnut24
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret_sentry.gnut72
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_utility.gnut558
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod.gnut187
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod_fireteam.gnut246
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_grunt_chatter.gnut1786
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_squad_spawn.gnut167
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_titan_npc_behavior.gnut404
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/burnmeter/_burnmeter.gnut42
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/class/CHardPointEntity.nut16
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/class/cai_basenpc.nut272
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/class/cbasecombatcharacter.nut28
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/class/cbaseentity.nut229
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/class/cplayer.nut355
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/class/ctitansoul.nut50
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/conversation/_battle_chatter.gnut25
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/conversation/_conversation_schedule.gnut629
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/conversation/_faction_dialogue.gnut46
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/conversation/_grunt_chatter_mp.gnut18
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/conversation/_spectre_chatter_mp.gnut18
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter.gnut508
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut136
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut315
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/faction_xp.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_frontline.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_capture_point.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_featured_mode_settings.gnut125
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_frontline.gnut159
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut12
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut18
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_coliseum.nut98
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut282
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut518
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fd.nut12
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut17
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut27
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut103
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut232
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut12
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut127
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut19
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut73
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_hardpoints.gnut35
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_riff_floor_is_lava.nut102
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_spawnpoints.gnut0
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/sh_gamemodes.gnut819
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/sh_gamemodes_custom.gnut20
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/item_inventory/sv_item_inventory.gnut60
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut37
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/lobby/_private_lobby.gnut194
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_lobby.gnut495
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_private_lobby_modes_init.gnut55
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/melee/_melee.gnut89
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_rewards.gnut74
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_synced_human.gnut588
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_synced_titan.gnut1543
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_mp.gnut41
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_mp.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_superspectre.nut736
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut2179
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut613
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_battery_port.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_bleedout.gnut403
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_challenges.gnut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_changemap.nut24
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp.nut67
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut239
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_no_intro.gnut80
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut999
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_dropship_spawn_common.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate.nut144
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut734
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_goblin_dropship.nut784
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_lasermesh.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_loadout_crate.nut183
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_mp_mapspawn.gnut65
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_music.gnut107
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_pickups.gnut1195
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_pickups_glow.gnut53
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_playlist.gnut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_revive.gnut352
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut224
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_serverflags.nut35
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_sniper_spectres.nut485
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_spawn_functions.nut60
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_spectre_rack.nut395
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut78
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_npc.nut818
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_tether.gnut307
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_transfer.nut641
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_tonecontroller.nut189
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_utility_mp.gnut18
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_vr.nut66
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/_lf_maps_shared.gnut8
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_angel_city.nut27
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_angel_city_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_black_water_canal.nut19
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_black_water_canal_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_coliseum.nut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_coliseum_column.nut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_complex3.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_crashsite3.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_drydock.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_drydock_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_eden.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_forwardbase_kodai.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_forwardbase_kodai_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_glitch.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_glitch_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_grave.nut19
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_grave_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_homestead.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_homestead_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_meadow.nut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_stacks.nut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_township.nut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_traffic.nut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_uma.nut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_relic02.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_relic02_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_rise.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_rise_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_thaw.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_thaw_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut7
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames_fd.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/pintelemetry.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/player_cloak.nut184
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut439
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_debug.gnut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_on_friendly.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_wave.gnut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_wave_dropship.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/_leeching.gnut493
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/_pilot_leeching.gnut610
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/_slamzoom.nut85
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/_zipline.gnut838
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/class_wallrun.gnut224
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/rodeo/_rodeo.gnut545
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut2456
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/sh_calling_cards.gnut424
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/sh_loadouts_mp.nut19
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/sh_northstar_utils.gnut42
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/sh_remote_functions_mp_custom.gnut20
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/sh_stats.gnut526
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/superbar/orbitalstrike.nut167
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/superbar/smokescreen.nut417
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/sv_globals.gnut0
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan/_battery_generator.gnut128
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan/_replacement_titans.gnut1183
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan/_replacement_titans_drop.gnut443
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_commands.gnut49
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_health.gnut1072
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_hints.gnut267
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_hotdrop.gnut778
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_triple_health.gnut524
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan/class_titan.gnut77
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/titan_xp.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/vehicle/_vehicle_behavior.gnut6
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/vehicle/_vehicle_dropship_new.nut528
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapon_xp.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_arc_cannon.nut1032
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_at_turrets.gnut284
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_ball_lightning.gnut363
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_cloaker.gnut121
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_grenade.nut604
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_particle_wall.gnut460
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_team_emp.gnut38
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_vortex.nut1983
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_weapon_dialogue.nut44
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/weapons/_weapon_utility.nut3966
235 files changed, 78699 insertions, 0 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_anim.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_anim.gnut
new file mode 100644
index 00000000..2ead1d30
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_anim.gnut
@@ -0,0 +1,1395 @@
+untyped
+
+global function Anim_Init
+
+global function FirstPersonSequence
+global function GetAnim
+global function HasAnim
+global function SetAnim
+global function PlayAnimTeleport
+global function GetAnimStartInfo
+
+global function PlayFPSAnim
+global function PlayFPSAnimShowProxy
+global function PlayFPSAnimTeleport
+global function PlayFPSAnimTeleportShowProxy
+
+global function PlayAnim
+global function PlayAnimGravity
+global function PlayAnimGravityClientSyncing
+global function PlayAnimRunGravity
+global function PlayAnimRun
+
+global function RunToAnimStart_Deprecated
+global function RunToAnimStartForced_Deprecated
+
+global function RunToAnimStartPos
+global function RunToAndPlayAnim
+global function RunToAndPlayAnimAndWait
+global function RunToAndPlayAnimGravity
+global function RunToAndPlayAnimGravityForced
+
+function Anim_Init()
+{
+ RegisterSignal( "NewViewAnim" )
+ RegisterSignal( "NewFirstPersonSequence" )
+ RegisterSignal( "ScriptAnimStop" )
+ RegisterSignal( "AnimEventKill" )
+
+ AddGlobalAnimEvent( "enable_weapon", GlobalAnimEvent_EnableWeapon )
+ AddGlobalAnimEvent( "disable_weapon", GlobalAnimEvent_DisableWeapon )
+ AddGlobalAnimEvent( "clear_parent", GlobalAnimEvent_ClearParent )
+ AddGlobalAnimEvent( "hide", GlobalAnimEvent_Hide )
+ AddGlobalAnimEvent( "show", GlobalAnimEvent_Show )
+ AddGlobalAnimEvent( "RecordOrigin", GlobalAnimEvent_RecordOrigin )
+ AddGlobalAnimEvent( "ShowFPSProxy", GlobalAnimEvent_ShowFPSProxy )
+ AddGlobalAnimEvent( "clear_anim_view_ent",GlobalAnimEvent_ClearAnimViewEntity )
+ AddGlobalAnimEvent( "scripted_death_to_ragdoll", GlobalAnimEvent_ScriptedDeathToRagdoll )
+ AddGlobalAnimEvent( "SetVelocity", GlobalAnimEvent_SetVelocity )
+ AddGlobalAnimEvent( "stance_kneel", GlobalAnimEvent_StanceKneel )
+ AddGlobalAnimEvent( "stance_kneeling", GlobalAnimEvent_StanceKneeling )
+ AddGlobalAnimEvent( "stance_stand", GlobalAnimEvent_StanceStand )
+ AddGlobalAnimEvent( "stance_standing", GlobalAnimEvent_StanceStanding )
+ AddGlobalAnimEvent( "enable_planting", GlobalAnimEvent_EnablePlanting )
+ AddGlobalAnimEvent( "kill", GlobalAnimEvent_Kill )
+ AddGlobalAnimEvent( "gib", GlobalAnimEvent_Gib )
+ AddGlobalAnimEvent( "titan_gib", GlobalAnimEvent_TitanGib )
+ AddGlobalAnimEvent( "EnableAimAssist", GlobalAnimEvent_EnableAimAssist )
+ AddGlobalAnimEvent( "DisableAimAssist", GlobalAnimEvent_DisableAimAssist )
+ AddGlobalAnimEvent( "give_ammo", GlobalAnimEvent_GiveAmmo )
+
+ #if SP
+ PrecacheWeapon( "mp_titanweapon_salvo_rockets" ) // used by bt_pod_fire_left/bt_pod_fire_right anim events. Only BT has these anim events.
+ AddGlobalAnimEvent( "bt_pod_fire_left", GlobalAnimEvent_BT_Pod_Left )
+ AddGlobalAnimEvent( "bt_pod_fire_right", GlobalAnimEvent_BT_Pod_Right )
+ #endif
+}
+
+void function GlobalAnimEvent_BT_Pod_Left( entity guy )
+{
+ BT_Pod( guy, "POD_L" )
+}
+
+void function GlobalAnimEvent_BT_Pod_Right( entity guy )
+{
+ BT_Pod( guy, "POD_R" )
+}
+
+void function BT_Pod( entity guy, string tag )
+{
+ entity oldOffhandWeapon = guy.GetOffhandWeapon( 0 )
+ guy.TakeOffhandWeapon( 0 )
+ guy.GiveOffhandWeapon( "mp_titanweapon_salvo_rockets", 0, [ "scripted_no_damage" ] )
+
+ //printt( tag )
+ entity newOffhandWeapon = guy.GetOffhandWeapon( 0 )
+ int attachID = guy.LookupAttachment( tag )
+ vector angles = guy.GetAttachmentAngles( attachID )
+ WeaponPrimaryAttackParams params
+ params.pos = guy.GetAttachmentOrigin( attachID )
+ params.dir = AnglesToForward( angles )
+ StartParticleEffectOnEntity( guy, GetParticleSystemIndex( $"P_muzzleflash_predator" ), FX_PATTACH_POINT_FOLLOW, attachID )
+
+ thread OnWeaponPrimaryAttack_titanweapon_salvo_rockets( newOffhandWeapon, params )
+
+ guy.TakeOffhandWeapon( 0 )
+
+ if ( oldOffhandWeapon )
+ guy.GiveOffhandWeapon( oldOffhandWeapon.GetWeaponClassName(), 0, oldOffhandWeapon.GetMods() )
+}
+
+void function GlobalAnimEvent_EnableWeapon( entity guy )
+{
+ if ( guy.IsPlayer() )
+ {
+ guy.EnableWeapon()
+ guy.EnableWeaponViewModel()
+ }
+ else
+ printt( "Warning: Tried to enable weapon on non player: " + guy )
+}
+
+void function GlobalAnimEvent_DisableWeapon( entity guy )
+{
+ if ( guy.IsPlayer() )
+ {
+ guy.DisableWeapon()
+ }
+ else
+ printt( "Warning: Tried to disable weapon on non player: " + guy )
+}
+
+void function GlobalAnimEvent_ClearParent( entity guy )
+{
+ guy.ClearParent()
+}
+
+void function GlobalAnimEvent_Hide( entity guy )
+{
+ guy.Hide()
+}
+
+void function GlobalAnimEvent_Show( entity guy )
+{
+ guy.Show()
+}
+
+void function GlobalAnimEvent_RecordOrigin( entity actor )
+{
+ if ( !actor.IsPlayer() )
+ return
+ if ( !( "recordedOrigin" in actor.s ) )
+ actor.s.recordedOrigin <- []
+
+ table record = {}
+ record.origin <- actor.GetOrigin()
+ record.time <- Time()
+
+ actor.s.recordedOrigin.append( record )
+}
+
+void function GlobalAnimEvent_ShowFPSProxy( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ local viewmodel = player.GetFirstPersonProxy()
+ viewmodel.ShowFirstPersonProxy()
+}
+
+void function GlobalAnimEvent_ClearAnimViewEntity( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ClearPlayerAnimViewEntity( player, 1.0 )
+}
+
+void function GlobalAnimEvent_ScriptedDeathToRagdoll( entity ent )
+{
+ ent.Die()
+ ent.SetContinueAnimatingAfterRagdoll( true )
+ ent.BecomeRagdoll( Vector(0,0,0), false )
+}
+
+void function GlobalAnimEvent_SetVelocity( entity actor )
+{
+ if ( !actor.IsPlayer() )
+ return
+ local record = null
+
+ if ( ( "recordedOrigin" in actor.s ) && actor.s.recordedOrigin.len() )
+ record = actor.s.recordedOrigin[ actor.s.recordedOrigin.len() - 1 ]
+
+ Assert( record, "anim had AE_SV_VSCRIPT_CALLBACK: SetVelocity, but no AE_SV_VSCRIPT_CALLBACK:RecordOrigin" )
+
+ local dir = Normalize( actor.GetOrigin() - record.origin )
+ local distance = Distance( actor.GetOrigin(), record.origin )
+ local time = Time() - record.time
+ if ( time <= 0 )
+ time = 0.001 // timescale bug?
+ local speed = distance / time
+
+ actor.SetVelocity( dir * speed )
+}
+
+void function GlobalAnimEvent_StanceKneel( entity guy )
+{
+ Assert( guy.IsTitan() )
+ Assert( guy.IsNPC() )
+ SetStanceKneel( guy.GetTitanSoul() )
+}
+
+void function GlobalAnimEvent_StanceKneeling( entity guy )
+{
+ Assert( guy.IsTitan() )
+ Assert( guy.IsNPC() )
+ SetStanceKneeling( guy.GetTitanSoul() )
+}
+
+void function GlobalAnimEvent_StanceStand( entity guy )
+{
+ Assert( guy.IsTitan() )
+ Assert( guy.IsNPC() )
+ SetStanceStand( guy.GetTitanSoul() )
+}
+
+void function GlobalAnimEvent_StanceStanding( entity guy )
+{
+ Assert( guy.IsTitan() )
+ Assert( guy.IsNPC() )
+ SetStanceStanding( guy.GetTitanSoul() )
+}
+
+void function GlobalAnimEvent_EnablePlanting( entity guy )
+{
+ if ( guy.IsNPC() || guy.IsPlayer() )
+ guy.Anim_EnablePlanting()
+ else
+ printt( "Warning: Tried to enable planting on " + guy )
+}
+
+void function GlobalAnimEvent_Kill( entity guy )
+{
+ if ( IsAlive( guy ) )
+ {
+ Signal( guy, "AnimEventKill" )
+ guy.TakeDamage( guy.GetMaxHealth() + 1, null, null, { damageSourceId=damagedef_suicide } )
+ guy.BecomeRagdoll( Vector( 0, 0, 0 ), false )
+ }
+}
+
+void function GlobalAnimEvent_Gib( entity guy )
+{
+ if ( IsAlive( guy ) )
+ {
+ Signal( guy, "AnimEventKill" )
+ guy.Gib( <0,0,100> )
+ }
+}
+
+void function GlobalAnimEvent_TitanGib( entity guy )
+{
+ if ( IsAlive( guy ) )
+ {
+ PlayTitanDeathFxUp( guy )
+
+ local entKVs = guy.CreateTableFromModelKeyValues()
+ local hitData = entKVs["hit_data"]
+
+ foreach ( bodyGroupName, bodyGroupData in hitData )
+ {
+ if ( !("blank" in bodyGroupData) )
+ continue
+
+ local bodyGroupIndex = guy.FindBodyGroup( bodyGroupName )
+ local stateCount = guy.GetBodyGroupModelCount( bodyGroupIndex )
+ guy.SetBodygroup( bodyGroupIndex, stateCount - 1 )
+ }
+ }
+}
+
+
+void function GlobalAnimEvent_EnableAimAssist( entity guy )
+{
+ if ( IsAlive( guy ) )
+ {
+ guy.SetAimAssistAllowed( true )
+ }
+}
+
+void function GlobalAnimEvent_DisableAimAssist( entity guy )
+{
+ if ( IsAlive( guy ) )
+ {
+ guy.SetAimAssistAllowed( false )
+ }
+}
+
+void function GlobalAnimEvent_GiveAmmo( entity guy )
+{
+ if ( IsAlive( guy ) )
+ {
+ array<entity> weapons = guy.GetMainWeapons()
+ if ( weapons.len() > 0 )
+ {
+ entity weapon = weapons[0]
+ if ( IsValid( weapon ) )
+ {
+ weapon.SetWeaponPrimaryClipCount( weapon.GetWeaponPrimaryClipCountMax() )
+ }
+ }
+ }
+}
+
+
+function GetAnim( guy, animation )
+{
+ if ( !( "anims" in guy.s ) )
+ return animation
+
+ if ( !( animation in guy.s.anims ) )
+ return animation
+
+ return guy.s.anims[ animation ]
+}
+
+function HasAnim( guy, animation )
+{
+ if ( !( "anims" in guy.s ) )
+ return false
+
+ return animation in guy.s.anims
+}
+
+function SetAnim( guy, name, animation )
+{
+ if ( !( "anims" in guy.s ) )
+ guy.s.anims <- {}
+
+ Assert( !( name in guy.s.anims ), guy + " already has set anim " + name )
+
+ guy.s.anims[ name ] <- animation
+}
+
+AnimRefPoint function GetAnimStartInfo( entity ent, string animAlias, animref )
+{
+ string animData = expect string( GetAnim( ent, animAlias ) )
+ AnimRefPoint animStartInfo = ent.Anim_GetStartForRefPoint( animData, animref.GetOrigin(), animref.GetAngles() )
+
+ return animStartInfo
+}
+
+
+function GetRefPosition( reference )
+{
+ Assert( reference.HasKey( "model" ) && reference.GetValueForModelKey() != $"", "Tried to play an anim relative to " + reference + " but it has no model/ref attachment." )
+
+ local position = {}
+ local attach_id
+ attach_id = reference.LookupAttachment( "REF" )
+
+ if ( attach_id )
+ {
+ position.origin <- reference.GetAttachmentOrigin( attach_id )
+ position.angles <- reference.GetAttachmentAngles( attach_id )
+ }
+
+ return position
+}
+
+// play the anim
+function __PlayAnim( guy, animation_name, reference = null, optionalTag = null, blendTime = DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME )
+{
+ expect entity( guy )
+
+ Assert( IsValid_ThisFrame( guy ), "Invalid ent sent to PlayAnim " + animation_name )
+ local animation = GetAnim( guy, animation_name )
+
+ guy.SetNextThinkNow()
+
+ #if !DEV
+ if ( guy.IsNPC() && !guy.IsInterruptable() )
+ {
+ // better than nothing failsafe
+ guy.Signal( "OnAnimationInterrupted" )
+ guy.Signal( "OnAnimationDone" )
+ return
+ }
+ #endif
+
+ if ( guy.IsNPC() )
+ {
+ guy.EndSignal( "OnDeath" )
+ Assert( IsAlive( guy ), "Guy " + guy + " tried to play an anim, but it is not alive." )
+ }
+
+ if ( reference )
+ {
+ if ( reference == guy )
+ {
+ local position = GetRefPosition( reference )
+ local origin = position.origin
+ local angles = position.angles
+
+ if ( guy.IsNPC() )
+ guy.Anim_ScriptedPlayWithRefPoint( animation, origin, angles, blendTime )
+ else
+ guy.Anim_PlayWithRefPoint( animation, origin, angles, blendTime )
+
+ return
+ }
+
+ if ( optionalTag )
+ {
+ if ( typeof( reference ) == "vector" )
+ {
+ Assert( typeof( optionalTag ) == "vector", "Expected angles but got " + optionalTag )
+ if ( guy.IsNPC() )
+ guy.Anim_ScriptedPlayWithRefPoint( animation, reference, optionalTag, blendTime )
+ else
+ guy.Anim_PlayWithRefPoint( animation, reference, optionalTag, blendTime )
+ return
+ }
+
+ Assert( typeof( optionalTag ) == "string", "Passed invalid optional tag " + optionalTag )
+
+ if ( guy.GetParent() == reference )
+ {
+ if ( guy.IsNPC() )
+ guy.Anim_ScriptedPlay( animation )
+ else
+ guy.Anim_Play( animation )
+ }
+ else
+ {
+ local attachIndex = reference.LookupAttachment( optionalTag )
+ local origin = reference.GetAttachmentOrigin( attachIndex )
+ local angles = reference.GetAttachmentAngles( attachIndex )
+ if ( guy.IsNPC() )
+ {
+ //local origin = reference.GetOrigin()
+ //local angles = reference.GetAngles()
+ guy.Anim_ScriptedPlayWithRefPoint( animation, origin, angles, blendTime )
+ }
+ else
+ {
+ //local animStartPos = guy.Anim_GetStartForRefEntity_Old( animation, reference, optionalTag )
+ //local origin = animStartPos.origin
+ //local angles = animStartPos.angles
+ guy.Anim_PlayWithRefPoint( animation, origin, angles, blendTime )
+ }
+ }
+ return
+ }
+ }
+ else
+ {
+ Assert( optionalTag == null, "Reference was null, but optionalTag was not. Did you mean to set the tag?" )
+ }
+
+ if ( reference != null && guy.GetParent() == reference )
+ {
+ if ( guy.IsNPC() )
+ guy.Anim_ScriptedPlay( animation )
+ else
+ guy.Anim_Play( animation )
+
+ return
+ }
+
+ if ( !reference )
+ reference = guy
+
+ local origin = reference.GetOrigin()
+ local angles = reference.GetAngles()
+
+ if ( guy.IsNPC() )
+ guy.Anim_ScriptedPlayWithRefPoint( animation, origin, angles, blendTime )
+ else
+ guy.Anim_PlayWithRefPoint( animation, origin, angles, blendTime )
+
+}
+
+function TeleportToAnimStart( _guy, animation_name, reference, optionalTag = null, smooth = false )
+{
+ entity guy = expect entity( _guy )
+
+ Assert( reference, "NO reference" )
+ string animation = expect string( GetAnim( guy, animation_name ) )
+ AnimRefPoint animStartPos
+
+ if ( optionalTag )
+ {
+ if ( typeof( reference ) == "vector" )
+ {
+ Assert( typeof( optionalTag ) == "vector", "Expected angles but got " + optionalTag )
+ animStartPos = guy.Anim_GetStartForRefPoint( animation, reference, optionalTag )
+ }
+ else
+ {
+ animStartPos = guy.Anim_GetStartForRefEntity( animation, reference, optionalTag )
+ }
+ }
+ else
+ {
+ //printt( "Reference is " + reference )
+ //printt( "guy is " + guy )
+ //printt( "animation is " + animation )
+ local origin = reference.GetOrigin()
+ local angles = reference.GetAngles()
+ animStartPos = guy.Anim_GetStartForRefPoint( animation, origin, angles )
+ }
+ //Assert( animStartPos, "No animStartPos for " + animation + " on " + guy )
+
+ // hack! shouldn't need to do this
+ animStartPos.origin = ClampToWorldspace( animStartPos.origin )
+
+ if ( guy.GetParent() )
+ {
+ if ( smooth )
+ {
+ guy.SetAbsOriginSmooth( animStartPos.origin )
+ guy.SetAbsAnglesSmooth( animStartPos.angles )
+ }
+ else
+ {
+ guy.SetAbsOrigin( animStartPos.origin )
+ guy.SetAbsAngles( animStartPos.angles )
+ }
+ }
+ else
+ {
+ guy.SetOrigin( animStartPos.origin )
+ guy.SetAngles( animStartPos.angles )
+ }
+}
+
+// wait till arrive at goal and animation is done
+function RunToAndPlayAnimAndWait( entity guy, string animation_name, reference, bool doArrival = false, optionalTag = null )
+{
+ bool savedEnableFriendlyFollower = guy.ai.enableFriendlyFollower
+ guy.ai.enableFriendlyFollower = false
+
+ local allowFlee = guy.GetNPCFlag( NPC_ALLOW_FLEE )
+ local allowHandSignal = guy.GetNPCFlag( NPC_ALLOW_HAND_SIGNALS )
+ guy.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS )
+
+ __RunToAndPlayAnim( guy, animation_name, reference, doArrival, optionalTag )
+
+ guy.WaitSignal( "OnFinishedAssault" )
+ WaittillAnimDone( guy )
+
+ guy.SetNPCFlag( NPC_ALLOW_FLEE, allowFlee )
+ guy.ai.enableFriendlyFollower = savedEnableFriendlyFollower
+}
+
+// wait till arrive at goal and start animation but don't wait until animation is done
+function RunToAndPlayAnim( entity guy, string animation_name, reference, bool doArrival = false, optionalTag = null )
+{
+ bool savedEnableFriendlyFollower = guy.ai.enableFriendlyFollower
+ guy.ai.enableFriendlyFollower = false
+
+ local allowFlee = guy.GetNPCFlag( NPC_ALLOW_FLEE )
+ local allowHandSignal = guy.GetNPCFlag( NPC_ALLOW_HAND_SIGNALS )
+ guy.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS )
+
+ __RunToAndPlayAnim( guy, animation_name, reference, doArrival, optionalTag )
+
+ guy.WaitSignal( "OnFinishedAssault" )
+
+ guy.SetNPCFlag( NPC_ALLOW_FLEE, allowFlee )
+ guy.ai.enableFriendlyFollower = savedEnableFriendlyFollower
+}
+
+function RunToAndPlayAnimGravity( entity guy, string animation_name, reference, bool doArrival = false, optionalTag = null )
+{
+ local allowFlee = guy.GetNPCFlag( NPC_ALLOW_FLEE )
+ local allowHandSignal = guy.GetNPCFlag( NPC_ALLOW_HAND_SIGNALS )
+ guy.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS )
+
+ float arrivalTolerance = guy.AssaultGetArrivalTolerance()
+ __RunToAndPlayAnim( guy, animation_name, reference, doArrival, optionalTag )
+
+ guy.WaitSignal( "OnFinishedAssault" )
+ guy.Anim_EnablePlanting()
+ WaittillAnimDone( guy )
+
+ guy.AssaultSetArrivalTolerance( arrivalTolerance )
+
+ guy.SetNPCFlag( NPC_ALLOW_HAND_SIGNALS, allowHandSignal )
+}
+
+
+function RunToAndPlayAnimGravityForced( entity guy, string animation_name, reference, bool doArrival = false, optionalTag = null )
+{
+ bool savedEnableFriendlyFollower = guy.ai.enableFriendlyFollower
+ guy.ai.enableFriendlyFollower = false
+
+ local allowFlee = guy.GetNPCFlag( NPC_ALLOW_FLEE )
+ local allowHandSignal = guy.GetNPCFlag( NPC_ALLOW_HAND_SIGNALS )
+ guy.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS )
+
+ float arrivalTolerance = guy.AssaultGetArrivalTolerance()
+ __RunToAndPlayAnim( guy, animation_name, reference, doArrival, optionalTag )
+
+ guy.WaitSignal( "OnFinishedAssault" )
+ guy.Anim_EnablePlanting()
+ WaittillAnimDone( guy )
+
+ guy.AssaultSetArrivalTolerance( arrivalTolerance )
+
+ guy.SetNPCFlag( NPC_ALLOW_HAND_SIGNALS, allowHandSignal )
+ guy.ai.enableFriendlyFollower = savedEnableFriendlyFollower
+}
+
+function __RunToAndPlayAnim( entity guy, string animation_name, reference, bool doArrival, optionalTag )
+{
+ Assert( IsAlive( guy ) )
+ guy.Anim_Stop() // in case we were doing an anim already
+ guy.EndSignal( "OnDeath" )
+
+ string animation = expect string( GetAnim( guy, animation_name ) )
+ local origin, angles
+
+ if ( optionalTag )
+ {
+ if ( typeof( reference ) == "vector" )
+ {
+ Assert( typeof( optionalTag ) == "vector", "Expected angles but got " + optionalTag )
+ origin = reference
+ angles = optionalTag
+ }
+ else
+ {
+ local attach_id = reference.LookupAttachment( optionalTag )
+ origin = reference.GetAttachmentOrigin( attach_id )
+ angles = reference.GetAttachmentAngles( attach_id )
+ }
+ }
+ else
+ {
+ Assert( typeof( reference ) != "vector", "Expected an entity, but got an origin with no angles" )
+ origin = reference.GetOrigin()
+ angles = reference.GetAngles()
+ }
+
+ guy.AssaultPointToAnim( origin, angles, animation, doArrival, 4.0 )
+}
+
+// run to the place to start the anim, then play it
+function RunToAnimStart_Deprecated( guy, animation_name, reference = null, optionalTag = null )
+{
+ expect entity( guy )
+
+ Assert( IsAlive( guy ) )
+ guy.Anim_Stop() // in case we were doing an anim already
+ guy.EndSignal( "OnDeath" )
+
+ local allowFlee = guy.GetNPCFlag( NPC_ALLOW_FLEE )
+ local allowHandSignal = guy.GetNPCFlag( NPC_ALLOW_HAND_SIGNALS )
+
+ guy.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS )
+
+ local animation = GetAnim( guy, animation_name )
+ local animStartPos
+
+ if ( optionalTag )
+ {
+ if ( typeof( reference ) == "vector" )
+ {
+ Assert( typeof( optionalTag ) == "vector", "Expected angles but got " + optionalTag )
+ animStartPos = guy.Anim_GetStartForRefPoint_Old( animation, reference, optionalTag )
+ }
+ else
+ {
+ animStartPos = guy.Anim_GetStartForRefEntity_Old( animation, reference, optionalTag )
+ }
+ }
+ else
+ {
+ local origin = reference.GetOrigin()
+ local angles = reference.GetAngles()
+ animStartPos = guy.Anim_GetStartForRefPoint_Old( animation, origin, angles )
+ }
+
+ guy.AssaultPoint( animStartPos.origin )
+ guy.WaitSignal( "OnFinishedAssault" )
+
+ guy.SetNPCFlag( NPC_ALLOW_FLEE, allowFlee )
+ guy.SetNPCFlag( NPC_ALLOW_HAND_SIGNALS, allowHandSignal )
+
+ local dist = Distance( animStartPos.origin, guy.GetOrigin() )
+ if ( dist > 8 )
+ {
+ //DebugDrawLine( animStartPos.origin, guy.GetOrigin(), 255, 150, 0, true, 60 )
+ printt( guy, " was ", dist, " units away from where he wanted to end his scripted sequence" )
+ }
+// printt( guy + " finished assault at dist ", Distance( animStartPos.origin, guy.GetOrigin() ) )
+// Assert( Distance( animStartPos.origin, guy.GetOrigin() ) < 32, guy + " finished assault but was " + ( Distance( animStartPos.origin, guy.GetOrigin() ) ) + " away from where he should have ended up." )
+}
+
+// only use this if you are OK with a frame pause before the start of the animation
+void function RunToAnimStartPos( entity guy, string animation_name, reference = null, bool doArrival = false, optionalTag = null )
+{
+ Assert( IsAlive( guy ) )
+ guy.Anim_Stop() // in case we were doing an anim already
+ guy.EndSignal( "OnDeath" )
+
+ local allowFlee = guy.GetNPCFlag( NPC_ALLOW_FLEE )
+ local allowHandSignal = guy.GetNPCFlag( NPC_ALLOW_HAND_SIGNALS )
+ local allowArrivals = guy.GetNPCMoveFlag( NPCMF_DISABLE_ARRIVALS )
+
+ if ( !doArrival )
+ {
+ // guy.DisableArrivalOnce( true )
+ guy.EnableNPCMoveFlag( NPCMF_DISABLE_ARRIVALS )
+ }
+
+ guy.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS )
+
+ local animation = GetAnim( guy, animation_name )
+ local animStartPos
+
+ if ( optionalTag )
+ {
+ if ( typeof( reference ) == "vector" )
+ {
+ Assert( typeof( optionalTag ) == "vector", "Expected angles but got " + optionalTag )
+ animStartPos = guy.Anim_GetStartForRefPoint_Old( animation, reference, optionalTag )
+ }
+ else
+ {
+ animStartPos = guy.Anim_GetStartForRefEntity_Old( animation, reference, optionalTag )
+ vector ornull clampedPos = NavMesh_ClampPointForAI( animStartPos.origin, guy )
+ if ( clampedPos != null )
+ animStartPos.origin = clampedPos
+ }
+ }
+ else
+ {
+ local origin = reference.GetOrigin()
+ local angles = reference.GetAngles()
+ animStartPos = guy.Anim_GetStartForRefPoint_Old( animation, origin, angles )
+ }
+
+ var fightRadius = guy.AssaultGetFightRadius()
+ var arrivalTolerance = guy.AssaultGetArrivalTolerance()
+ float runtoRadius = 61.16
+ guy.AssaultSetFightRadius( runtoRadius )
+ guy.AssaultSetArrivalTolerance( runtoRadius )
+
+ bool savedEnableFriendlyFollower = guy.ai.enableFriendlyFollower
+ guy.ai.enableFriendlyFollower = false
+
+ guy.AssaultPoint( animStartPos.origin )
+
+ //DebugDrawLine( guy.GetOrigin(), animStartPos.origin, 255, 0, 0, true, 20.0 )
+ //DebugDrawAngles( animStartPos.origin, animStartPos.angles )
+ //thread DebugAssaultEnt( guy, assaultEnt )
+ WaitSignal( guy, "OnFinishedAssault" )
+
+ //in case the scripter reset during run, we want to honor the intended change
+ if ( guy.AssaultGetFightRadius() == runtoRadius )
+ guy.AssaultSetFightRadius( fightRadius )
+
+ if ( guy.AssaultGetArrivalTolerance() == runtoRadius )
+ guy.AssaultSetArrivalTolerance( arrivalTolerance )
+
+ guy.SetNPCFlag( NPC_ALLOW_FLEE, allowFlee )
+ guy.SetNPCFlag( NPC_ALLOW_HAND_SIGNALS, allowHandSignal )
+ guy.SetNPCMoveFlag( NPCMF_DISABLE_ARRIVALS, allowArrivals )
+
+ guy.ai.enableFriendlyFollower = savedEnableFriendlyFollower
+}
+
+///////////////////////////////////////////////////////////////////
+// Deprecated, use RunToAndPlayAnim, otherwise there will be a gap between arriving at position and playing the animation
+function RunToAnimStartForced_Deprecated( entity guy, string animation_name, reference = null, optionalTag = null, bool disableArrival = true, disableAssaultAngles = false )
+{
+ Assert( IsAlive( guy ) )
+ guy.Anim_Stop() // in case we were doing an anim already
+ guy.EndSignal( "OnDeath" )
+
+ local allowFlee = guy.GetNPCFlag( NPC_ALLOW_FLEE )
+ local allowHandSignal = guy.GetNPCFlag( NPC_ALLOW_HAND_SIGNALS )
+ local allowArrivals = guy.GetNPCMoveFlag( NPCMF_DISABLE_ARRIVALS )
+
+ if ( disableArrival )
+ {
+ // guy.DisableArrivalOnce( true )
+ guy.EnableNPCMoveFlag( NPCMF_DISABLE_ARRIVALS )
+ }
+
+ guy.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS )
+
+ local animation = GetAnim( guy, animation_name )
+ local animStartPos
+
+ if ( optionalTag )
+ {
+ if ( typeof( reference ) == "vector" )
+ {
+ Assert( typeof( optionalTag ) == "vector", "Expected angles but got " + optionalTag )
+ animStartPos = guy.Anim_GetStartForRefPoint_Old( animation, reference, optionalTag )
+ }
+ else
+ {
+ animStartPos = guy.Anim_GetStartForRefEntity_Old( animation, reference, optionalTag )
+ vector ornull clampedPos = NavMesh_ClampPointForAI( animStartPos.origin, guy )
+ if ( clampedPos != null )
+ animStartPos.origin = clampedPos
+ }
+ }
+ else
+ {
+ local origin = reference.GetOrigin()
+ local angles = reference.GetAngles()
+ animStartPos = guy.Anim_GetStartForRefPoint_Old( animation, origin, angles )
+ }
+
+ var fightRadius = guy.AssaultGetFightRadius()
+ var arrivalTolerance = guy.AssaultGetArrivalTolerance()
+ float runtoRadius = 61.16
+ guy.AssaultSetFightRadius( runtoRadius )
+ guy.AssaultSetArrivalTolerance( runtoRadius )
+
+ bool savedEnableFriendlyFollower = guy.ai.enableFriendlyFollower
+ guy.ai.enableFriendlyFollower = false
+
+ guy.AssaultPoint( animStartPos.origin )
+ if ( !disableAssaultAngles )
+ guy.AssaultSetAngles( animStartPos.angles, true )
+
+ //DebugDrawLine( guy.GetOrigin(), animStartPos.origin, 255, 0, 0, true, 20.0 )
+ //DebugDrawAngles( animStartPos.origin, animStartPos.angles )
+ //thread DebugAssaultEnt( guy, assaultEnt )
+ WaitSignal( guy, "OnFinishedAssault" )
+
+/*
+ if ( !disableAssaultAngles )
+ guy.AssaultSetAngles( animStartPos.angles, true )
+
+ guy.AssaultPointToAnim( animStartPos.origin, animation, 4.0 )
+ WaittillAnimDone( guy )
+// guy.WaitSignal( "OnFinishedAssault" )
+
+*/
+ //in case the scripter reset during run, we want to honor the intended change
+ if ( guy.AssaultGetFightRadius() == runtoRadius )
+ guy.AssaultSetFightRadius( fightRadius )
+
+ if ( guy.AssaultGetArrivalTolerance() == runtoRadius )
+ guy.AssaultSetArrivalTolerance( arrivalTolerance )
+
+ guy.SetNPCFlag( NPC_ALLOW_FLEE, allowFlee )
+ guy.SetNPCFlag( NPC_ALLOW_HAND_SIGNALS, allowHandSignal )
+ guy.SetNPCMoveFlag( NPCMF_DISABLE_ARRIVALS, allowArrivals )
+
+ guy.ai.enableFriendlyFollower = savedEnableFriendlyFollower
+}
+
+void function ShowEnt( entity viewmodel )
+{
+ if ( IsValid_ThisFrame( viewmodel ) )
+ viewmodel.ShowFirstPersonProxy()
+}
+
+// anim teleport
+function PlayAnimTeleport( guy, animation_name, reference = null, optionalTag = null, initialTime = -1.0, smooth = false )
+{
+ if ( type( guy ) == "array" || type( guy ) == "table" )
+ {
+ Assert( reference, "NO reference" )
+ local firstEnt = null
+ foreach ( ent in guy )
+ {
+ if ( !firstEnt )
+ firstEnt = ent
+
+ TeleportToAnimStart( ent, animation_name, reference, optionalTag, smooth )
+ __PlayAnim( ent, animation_name, reference, optionalTag, 0 )
+ if ( initialTime > 0.0 )
+ guy.Anim_SetInitialTime( initialTime )
+ }
+
+ WaittillAnimDone( expect entity( firstEnt ) )
+ }
+ else
+ {
+ if ( !reference )
+ reference = guy
+
+ TeleportToAnimStart( guy, animation_name, reference, optionalTag, smooth )
+ __PlayAnim( guy, animation_name, reference, optionalTag, 0 )
+ if ( initialTime > 0.0 )
+ guy.Anim_SetInitialTime( initialTime )
+ WaittillAnimDone( expect entity( guy ) )
+ }
+}
+
+// play the anim
+function PlayAnim( guy, animation_name, reference = null, optionalTag = null, blendTime = DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME, initialTime = -1.0 )
+{
+ if ( type( guy ) == "array" )
+ {
+ foreach ( ent in guy )
+ {
+ __PlayAnim( ent, animation_name, reference, optionalTag, blendTime )
+ if ( initialTime > 0.0 )
+ guy.Anim_SetInitialTime( initialTime )
+ }
+
+ WaittillAnimDone( expect entity( guy[0] ) )
+ }
+ else
+ {
+ __PlayAnim( guy, animation_name, reference, optionalTag, blendTime )
+ if ( initialTime > 0.0 )
+ guy.Anim_SetInitialTime( initialTime )
+ WaittillAnimDone( expect entity( guy ) )
+ }
+}
+
+// play the anim
+function PlayAnimRun( entity guy, string animation_name, reference, bool doArrival, optionalTag = null, blendTime = DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME )
+{
+ RunToAndPlayAnim( guy, animation_name, reference, doArrival, optionalTag )
+ WaitSignal( guy, "OnFinishedAssault" )
+ WaittillAnimDone( guy )
+}
+
+function PlayAnimRunGravity( entity guy, string animation_name, reference, bool doArrival, optionalTag = null, blendTime = DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME )
+{
+ RunToAndPlayAnim( guy, animation_name, reference, doArrival, optionalTag )
+ WaitSignal( guy, "OnFinishedAssault" )
+ guy.Anim_EnablePlanting()
+ WaittillAnimDone( guy )
+}
+
+function PlayAnimGravityClientSyncing( guy, animation_name, reference = null, optionalTag = null, blendTime = DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME )
+{
+ __PlayAnim( guy, animation_name, reference, optionalTag, blendTime )
+ guy.Anim_EnablePlanting()
+ WaittillAnimDone( expect entity( guy ) )
+}
+
+function PlayAnimGravity( guy, animation_name, reference = null, optionalTag = null, blendTime = DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME )
+{
+ __PlayAnim( guy, animation_name, reference, optionalTag, blendTime )
+ guy.Anim_EnablePlanting()
+ WaittillAnimDone( expect entity( guy ) )
+}
+
+
+function CalcSequenceBlendTime( FirstPersonSequenceStruct sequence, entity player, entity ent = null )
+{
+ if ( sequence.blendTime != CALCULATE_SEQUENCE_BLEND_TIME )
+ return
+
+ sequence.blendTime = 0
+ if ( ent && sequence.thirdPersonAnim != "" )
+ {
+ local start
+ if ( sequence.attachment != "" )
+ {
+ start = player.Anim_GetStartForRefEntity_Old( sequence.thirdPersonAnim, ent, sequence.attachment )
+ }
+ else
+ {
+ start = {}
+ start.origin <- ent.GetOrigin()
+ start.angles <- ent.GetAngles()
+ }
+
+ if ( sequence.teleport )
+ {
+ player.SetAbsOrigin( start.origin )
+ player.SetAbsAngles( start.angles )
+ }
+ else
+ {
+ local dist = Distance( player.GetOrigin(), start.origin )
+ sequence.blendTime = GraphCapped( dist, 0, 350, 0.25, 0.9 )
+ }
+ }
+}
+
+void function PlayFPSAnim( entity player, string anim3rd, string anim1st = "", entity ref = null, string optionalTag = "", void functionref(entity) animView = null, float blendTime = DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME, float initialTime = 0.0 )
+{
+ bool teleport = false
+ bool hideProxy = true
+ __PlayFPSAnimInternal( player, anim3rd, anim1st, ref, optionalTag, animView, blendTime, initialTime, teleport, hideProxy )
+}
+
+void function PlayFPSAnimShowProxy( entity player, string anim3rd, string anim1st = "", entity ref = null, string optionalTag = "", void functionref(entity) animView = null, float blendTime = DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME, float initialTime = 0.0 )
+{
+ bool teleport = false
+ bool hideProxy = false
+ __PlayFPSAnimInternal( player, anim3rd, anim1st, ref, optionalTag, animView, blendTime, initialTime, teleport, hideProxy )
+}
+
+void function PlayFPSAnimTeleport( entity player, string anim3rd, string anim1st = "", entity ref = null, string optionalTag = "", void functionref(entity) animView = null, float initialTime = 0.0 )
+{
+ bool teleport = true
+ bool hideProxy = true
+ float blendTime = 0.0
+ __PlayFPSAnimInternal( player, anim3rd, anim1st, ref, optionalTag, animView, blendTime, initialTime, teleport, hideProxy )
+}
+
+void function PlayFPSAnimTeleportShowProxy( entity player, string anim3rd, string anim1st = "", entity ref = null, string optionalTag = "", void functionref(entity) animView = null, float initialTime = 0.0 )
+{
+ bool teleport = true
+ bool hideProxy = false
+ float blendTime = 0.0
+ __PlayFPSAnimInternal( player, anim3rd, anim1st, ref, optionalTag, animView, blendTime, initialTime, teleport, hideProxy )
+}
+
+void function __PlayFPSAnimInternal( entity player, string anim3rd, string anim1st, entity ref, string optionalTag, void functionref(entity) animView, float blendTime, float initialTime, bool teleport, bool hideProxy )
+{
+ if ( animView == null )
+ animView = ViewConeRampFree
+
+ FirstPersonSequenceStruct sequence
+
+ sequence.firstPersonAnim = anim1st
+ sequence.thirdPersonAnim = anim3rd
+ sequence.attachment = optionalTag
+ sequence.viewConeFunction = animView
+ sequence.setInitialTime = initialTime
+ sequence.blendTime = blendTime
+ sequence.teleport = teleport
+ sequence.hideProxy = hideProxy
+
+ //hard coded in this function
+ sequence.noParent = true
+
+ FirstPersonSequence( sequence, player, ref )
+}
+
+void function FirstPersonSequence( FirstPersonSequenceStruct sequence, entity player, entity ent = null )
+{
+ player.Signal( "NewFirstPersonSequence" )
+ player.EndSignal( "NewFirstPersonSequence" )
+ player.EndSignal( "ScriptAnimStop" )
+
+ player.SetVelocity( <0,0,0> ) // fix this
+ if ( player.IsPlayer() && sequence.snapPlayerFeetToEyes )
+ {
+ player.SnapFeetToEyes()
+ }
+
+ //figure out if we have/should do a first person sequence and handle Spawn slots.
+ bool doFirstPersonAnim = sequence.firstPersonAnim != ""
+
+ entity firstPersonProxy
+ if ( doFirstPersonAnim )
+ {
+ Assert( player.IsPlayer(), player + " is not a player" )
+ firstPersonProxy = player.GetFirstPersonProxy()
+ if ( !IsValid( firstPersonProxy ) || !EntHasModelSet( firstPersonProxy ) )
+ {
+ doFirstPersonAnim = false;
+ }
+ }
+
+ if ( doFirstPersonAnim )
+ {
+ firstPersonProxy.ShowFirstPersonProxy()
+
+ if ( sequence.renderWithViewModels )
+ firstPersonProxy.RenderWithViewModels( true )
+ else
+ firstPersonProxy.RenderWithViewModels( false )
+
+ firstPersonProxy.ClearParent()
+ firstPersonProxy.SetAbsOrigin( player.GetOrigin() )
+ firstPersonProxy.SetAbsAngles( player.GetAngles() )
+
+ // Set anim view entity *after* setting the proxy's origin so that we calculate our initial view offset correctly (for lerping)
+ SetPlayerAnimViewEntity( player, firstPersonProxy )
+ firstPersonProxy.SetNextThinkNow()
+ SetForceDrawWhileParented( firstPersonProxy, true )
+ }
+ else if ( sequence.thirdPersonCameraAttachments.len() > 0 )
+ {
+ if ( player.IsPlayer() )
+ {
+ entity fpProxy = player.GetFirstPersonProxy() //Shouldn't ever need to show the first person proxy when doing thirdPersonCameraAttachments; hide it explicitly here to stop it from showing up when chaining animations
+ if ( IsValid( fpProxy ) )
+ fpProxy.HideFirstPersonProxy()
+
+ if ( sequence.thirdPersonCameraEntity )
+ {
+ SetPlayerAnimViewEntity( player, sequence.thirdPersonCameraEntity )
+ }
+ else
+ {
+ SetPlayerAnimViewEntity( player, player )
+ }
+
+ player.AnimViewEntity_SetThirdPersonCameraAttachments( sequence.thirdPersonCameraAttachments )
+ if ( sequence.thirdPersonCameraVisibilityChecks )
+ {
+ player.AnimViewEntity_EnableThirdPersonCameraVisibilityChecks()
+ }
+ }
+ }
+ else
+ {
+ if ( player.IsPlayer() )
+ {
+ ClearPlayerAnimViewEntity( player )
+ }
+ }
+
+ entity soul
+
+ if ( ent )
+ {
+ // the entity we are animating relative to may change during the animation
+ if ( IsSoul( ent ) )
+ {
+ soul = ent
+ ent = soul.GetTitan()
+ if ( !IsValid( ent ) )
+ return
+ }
+ else if ( HasSoul( ent ) )
+ {
+ soul = ent.GetTitanSoul()
+ }
+ }
+
+ CalcSequenceBlendTime( sequence, player, ent )
+
+ if ( player.IsPlayer() )
+ {
+ if ( sequence.teleport )
+ {
+ player.AnimViewEntity_SetLerpInTime( 0.0 )
+ player.PlayerCone_SetLerpTime( 0.0 )
+ player.SnapToAbsOrigin( player.GetOrigin() )
+ }
+ else
+ {
+ if ( sequence.noViewLerp || (sequence.blendTime <= 0.0) )
+ {
+ player.AnimViewEntity_SetLerpInTime( 0.0 )
+ player.PlayerCone_SetLerpTime( 0.0 )
+ }
+ else
+ {
+ player.AnimViewEntity_SetLerpInTime( sequence.blendTime )
+ player.PlayerCone_SetLerpTime( sequence.blendTime )
+ }
+ }
+
+ if ( sequence.thirdPersonCameraAttachments.len() == 0 )
+ {
+ if ( sequence.firstPersonBlendOutTime >= 0.0 )
+ {
+ player.AnimViewEntity_SetLerpOutTime( sequence.firstPersonBlendOutTime )
+ }
+ else
+ {
+ player.AnimViewEntity_SetLerpOutTime( 0.4 )
+ }
+ }
+
+ if ( !doFirstPersonAnim || !sequence.playerPushable )
+ {
+ player.SnapFeetToEyes()
+ }
+ }
+
+ if ( ent && !sequence.noParent )
+ {
+ local optionalTag
+ if ( sequence.attachment != "" )
+ {
+ optionalTag = sequence.attachment
+ }
+ else
+ {
+ optionalTag = ""
+ }
+
+ if ( player.GetParent() != ent )
+ {
+ // you could be parenting from one tag to another but we don't do
+ // that anywhere currently, and if we want to do it we can do some
+ // special stuff
+ player.SetParent( ent, optionalTag, false, sequence.blendTime )
+ }
+ }
+
+ if ( doFirstPersonAnim )
+ {
+ if ( sequence.teleport )
+ {
+ firstPersonProxy.SnapToAbsOrigin( player.GetOrigin() )
+ }
+
+ if ( sequence.playerPushable )
+ {
+ firstPersonProxy.SetParent( player, "", false )
+ }
+ else
+ {
+ firstPersonProxy.SetToSameParentAs( player )
+ }
+ }
+
+ if ( sequence.relativeAnim != "" )
+ {
+ if ( sequence.teleport )
+ {
+ thread PlayAnimGravityClientSyncing( ent, sequence.relativeAnim, null, null, 0.0 )
+ }
+ else
+ {
+ thread PlayAnimGravityClientSyncing( ent, sequence.relativeAnim )
+ }
+
+ if ( sequence.setInitialTime != 0.0 )
+ ent.Anim_SetInitialTime( sequence.setInitialTime )
+ }
+
+ if ( doFirstPersonAnim )
+ {
+ if ( ent )
+ {
+ thread PlayAnim( firstPersonProxy, sequence.firstPersonAnim, ent, sequence.attachment, sequence.blendTime )
+ }
+ else if ( sequence.playerPushable )
+ {
+ firstPersonProxy.Anim_Play( sequence.firstPersonAnim )
+ firstPersonProxy.Anim_DisableUpdatePosition()
+ }
+ else if ( sequence.gravity )
+ {
+ thread PlayAnimGravityClientSyncing( firstPersonProxy, sequence.firstPersonAnim, sequence.origin, sequence.angles, sequence.blendTime )
+ }
+ else
+ {
+ thread PlayAnim( firstPersonProxy, sequence.firstPersonAnim, sequence.origin, sequence.angles, sequence.blendTime )
+ }
+
+ // BROKEN - Anim_EnablePlanting() only works on players and NPCs
+ // if ( sequence.enablePlanting )
+ // {
+ // viewmodel.Anim_EnablePlanting()
+ // }
+
+ if ( sequence.setInitialTime != 0.0 )
+ {
+ firstPersonProxy.Anim_SetInitialTime( sequence.setInitialTime )
+ }
+
+ if ( sequence.useAnimatedRefAttachment )
+ {
+ firstPersonProxy.Anim_EnableUseAnimatedRefAttachmentInsteadOfRootMotion()
+ }
+
+ if ( sequence.hideProxy )
+ {
+ firstPersonProxy.HideFirstPersonProxy()
+ }
+ }
+
+ if ( sequence.thirdPersonAnim != "" )
+ {
+ if ( ent )
+ {
+ thread PlayAnim( player, sequence.thirdPersonAnim, ent, sequence.attachment, sequence.blendTime )
+ }
+ else if ( player.IsPlayer() && sequence.playerPushable )
+ {
+ player.Anim_Play( sequence.thirdPersonAnim )
+ player.Anim_DisableUpdatePosition()
+ }
+ else if ( sequence.gravity )
+ {
+ thread PlayAnimGravityClientSyncing( player, sequence.thirdPersonAnim, sequence.origin, sequence.angles, sequence.blendTime )
+ }
+ else
+ {
+ thread PlayAnim( player, sequence.thirdPersonAnim, sequence.origin, sequence.angles, sequence.blendTime )
+ }
+
+ if ( sequence.enablePlanting )
+ player.Anim_EnablePlanting()
+
+ if ( sequence.viewConeFunction != null )
+ {
+ if ( sequence.thirdPersonCameraAttachments.len() == 0 )
+ {
+ sequence.viewConeFunction( player )
+ }
+ }
+
+ if ( sequence.setInitialTime != 0.0 )
+ player.Anim_SetInitialTime( sequence.setInitialTime )
+
+ if ( sequence.useAnimatedRefAttachment )
+ player.Anim_EnableUseAnimatedRefAttachmentInsteadOfRootMotion()
+
+ WaittillAnimDone( player )
+ }
+
+ if ( doFirstPersonAnim && IsValid( firstPersonProxy ) && firstPersonProxy.Anim_IsActive() && !firstPersonProxy.IsSequenceFinished() )
+ {
+ WaittillAnimDone( firstPersonProxy )
+ }
+
+ if ( !IsValid( player ) )
+ return
+
+ if ( player.IsPlayer() )
+ {
+ if ( !IsAlive( player ) )
+ return
+
+ if ( IsDisconnected( player ) )
+ return
+ }
+ else
+ if ( player.IsNPC() )
+ {
+ if ( !IsAlive( player ) )
+ return
+ }
+
+ // time passed
+ if ( soul )
+ {
+ if ( !IsValid( soul ) )
+ return
+
+ ent = soul.GetTitan()
+ if ( !IsAlive( ent ) )
+ return
+ }
+
+ if ( sequence.thirdPersonAnimIdle != "" )
+ {
+ //thread PlayAnim( player, sequence.thirdPersonAnimIdle, ent, sequence.attachment, 0 )
+ if ( ent )
+ {
+ thread PlayAnim( player, sequence.thirdPersonAnimIdle, ent, sequence.attachment, sequence.blendTime )
+ }
+ else if ( player.IsPlayer() && sequence.playerPushable )
+ {
+ player.Anim_Play( sequence.thirdPersonAnimIdle )
+ player.Anim_DisableUpdatePosition()
+ }
+ else
+ {
+ thread PlayAnim( player, sequence.thirdPersonAnimIdle, sequence.origin, sequence.angles, sequence.blendTime )
+ }
+ }
+
+ if ( sequence.firstPersonAnimIdle != "" )
+ {
+ firstPersonProxy = player.GetFirstPersonProxy()
+ firstPersonProxy.ShowFirstPersonProxy()
+
+ if ( IsValid( firstPersonProxy ) && EntHasModelSet( firstPersonProxy ) ) //JFS: Defensive fix for player not having view models sometimes
+ {
+ if ( sequence.renderWithViewModels )
+ firstPersonProxy.RenderWithViewModels( true )
+ else
+ firstPersonProxy.RenderWithViewModels( false )
+
+ SetPlayerAnimViewEntity( player, firstPersonProxy )
+ firstPersonProxy.SetNextThinkNow()
+
+ firstPersonProxy.SetAbsOrigin( player.GetOrigin() )
+ firstPersonProxy.SetAbsAngles( player.GetAngles() )
+
+ firstPersonProxy.Anim_Play( sequence.firstPersonAnimIdle )
+
+ if ( sequence.playerPushable )
+ {
+ firstPersonProxy.SetParent( player, "", false )
+ firstPersonProxy.Anim_DisableUpdatePosition()
+ }
+ else
+ {
+ firstPersonProxy.SetToSameParentAs( player )
+ }
+ }
+ }
+
+ if ( sequence.thirdPersonAnimIdle != "" && sequence.firstPersonAnimIdle != "" )
+ {
+ if ( sequence.viewConeFunction != null )
+ sequence.viewConeFunction( player )
+ }
+}
+
+
+function ClampPlayerViewCone( player )
+{
+ player.EndSignal( "OnDeath" )
+ player.PlayerCone_SetLerpTime( 0.0 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_auto_precache.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_auto_precache.gnut
new file mode 100644
index 00000000..75c7873e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_auto_precache.gnut
@@ -0,0 +1,771 @@
+#if DEV
+global function AutoPrecache_Init
+global function SetAutoPrecacheVersion
+global function MarkNPCForAutoPrecache
+global function AP_NPCSpawnerFound
+global function AP_PrecacheWeapon
+global function AP_PrecacheModel
+
+const int AUTO_PRECACHE_VERSION = 5
+
+global struct AutoPrecacheList
+{
+ array<string> weapons
+ table<string,int> weaponCount
+ table<string,array<entity> > npcSpawners
+ table<asset,string> autoPrecacheScript
+
+ array<asset> models
+}
+
+struct
+{
+ int autoPrecacheVersion
+ array<string> forceAutoPrecacheAiSettings
+
+ table<string,int> autoPrecacheFound_weapons
+ table<asset,bool> autoPrecacheFound_models
+ table<string,int> autoPrecacheFound_npcs
+
+} file
+
+void function AutoPrecache_Init()
+{
+ thread VerifyAutoPrecaches()
+
+ AddCallback_OnClientConnecting( AutoPrecache_OnPlayerConnect )
+}
+
+void function AutoPrecache_OnPlayerConnect( entity player )
+{
+ if ( Dev_CommandLineHasParm( "-autoprecache_all" ) )
+ {
+ switch ( GetMapName() )
+ {
+ case "sp_training":
+ ClientCommand( player, "map sp_crashsite" )
+ return
+
+ case "sp_crashsite":
+ ClientCommand( player, "map sp_sewers1" )
+ return
+
+ case "sp_sewers1":
+ ClientCommand( player, "map sp_boomtown" )
+ return
+
+ case "sp_boomtown":
+ ClientCommand( player, "map sp_boomtown_end" )
+ return
+
+ case "sp_boomtown_end":
+ ClientCommand( player, "map sp_boomtown_start" )
+ return
+
+ case "sp_boomtown_start":
+ ClientCommand( player, "map sp_hub_timeshift" )
+ return
+
+ case "sp_hub_timeshift":
+ ClientCommand( player, "map sp_timeshift_spoke02" )
+ return
+
+ case "sp_timeshift_spoke02":
+ ClientCommand( player, "map sp_beacon" )
+ return
+
+ case "sp_beacon":
+ ClientCommand( player, "map sp_beacon_spoke0" )
+ return
+
+ case "sp_beacon_spoke0":
+ ClientCommand( player, "map sp_tday" )
+ return
+
+ case "sp_tday":
+ ClientCommand( player, "map sp_s2s" )
+ return
+
+ case "sp_s2s":
+ ClientCommand( player, "map sp_skyway_v1" )
+ return
+
+ case "sp_skyway_v1":
+ ClientCommand( player, "map mp_grave" )
+ return
+
+ case "mp_grave":
+ ClientCommand( player, "quit" )
+ return
+
+ default:
+ ClientCommand( player, "map sp_training" )
+ return
+ }
+
+ }
+}
+
+void function VerifyAutoPrecaches()
+{
+ WaitEndFrame()
+ if ( !IsTestMap() )
+ Autoprecache_Verify()
+}
+
+void function Autoprecache_Verify()
+{
+ AutoPrecacheList autoPrecacheList = GenerateAutoPrecacheListForLevel()
+ if ( AutoPrecacheUpToDate( autoPrecacheList ) )
+ return
+
+ if ( !Dev_CommandLineHasParm( "-autoprecache_all" ) && !Dev_CommandLineHasParm( "-autoprecache" ) )
+ {
+ // dont really want mp generating auto precache if one map has an npc randomly placed in it, or an mp dedi going rogue
+ CodeWarning( "Entities have changed. Re-export auto precache script or run game with -autoprecache." )
+ return
+ }
+
+ ExportAutoPrecacheList( autoPrecacheList )
+
+ if ( Dev_CommandLineHasParm( "-autoprecache" ) )
+ {
+ Dev_CommandLineRemoveParm( "-autoprecache" )
+ ServerCommand( "reload" )
+ return
+ }
+
+ LevelTransitionStruct ornull trans = GetLevelTransitionStruct()
+ if ( trans != null )
+ {
+ expect LevelTransitionStruct( trans )
+ ChangeLevel( GetMapName(), trans )
+ return
+ }
+
+ LevelTransitionStruct trans2
+ ChangeLevel( GetMapName(), trans2 )
+
+}
+
+bool function IsTitanAISettings( string aiSettings )
+{
+ return Dev_GetAISettingByKeyField_Global( aiSettings, "aiclass" ) == "titan"
+}
+
+void function AddAutoPrecacheWeapon( AutoPrecacheList autoPrecacheList, string weapon )
+{
+ if ( weapon == "" )
+ return
+ autoPrecacheList.weapons.append( weapon )
+}
+
+void function FillAISettingsPrecaches( string aiSettings, AutoPrecacheList autoPrecacheList )
+{
+ if ( Dev_GetAISettingByKeyField_Global( aiSettings, "ForceAutoPrecacheDefaultWeapon" ) == 1 )
+ {
+ string weapon = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "DefaultWeapon" ) )
+ Assert( weapon != "", "Expected a weapon because ForceAutoPrecacheDefaultWeapon 1" )
+ AddAutoPrecacheWeapon( autoPrecacheList, weapon )
+ }
+
+ var grenadeWeapon = Dev_GetAISettingByKeyField_Global( aiSettings, "GrenadeWeaponName" )
+
+ if ( grenadeWeapon != "" )
+ {
+ expect string( grenadeWeapon )
+ AddAutoPrecacheWeapon( autoPrecacheList, grenadeWeapon )
+ }
+
+ var AdditionalScriptWeapon = Dev_GetAISettingByKeyField_Global( aiSettings, "AdditionalScriptWeapon" )
+ if ( AdditionalScriptWeapon != null )
+ {
+ expect string( AdditionalScriptWeapon )
+ AddAutoPrecacheWeapon( autoPrecacheList, AdditionalScriptWeapon )
+ }
+
+ var AdditionalAISettings = Dev_GetAISettingByKeyField_Global( aiSettings, "AdditionalAISettings" )
+ if ( AdditionalAISettings != null )
+ {
+ expect string( AdditionalAISettings )
+ FillAISettingsPrecaches( AdditionalAISettings, autoPrecacheList )
+ }
+
+ for ( int i = 0;; i++ )
+ {
+ asset gibModel = Dev_GetAISettingAssetByKeyField_Global( aiSettings, "GibModel" + i )
+ if ( gibModel == $"" )
+ break
+ autoPrecacheList.models.append( gibModel )
+ }
+
+ if ( IsTitanAISettings( aiSettings ) )
+ {
+ var titanSettings = Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" )
+ // is it a titan?
+ Assert( titanSettings != null, "No npc_titan_player_settings field in titan settings " + titanSettings )
+
+ // titans get their model from player model
+ expect string( titanSettings )
+ TitanLoadoutDef ornull titanLoadout = GetTitanLoadoutForColumn( "setFile", titanSettings )
+ if ( titanLoadout == null )
+ return
+
+ expect TitanLoadoutDef( titanLoadout )
+ AddTitanLoadoutToAutoPrecache( titanLoadout, autoPrecacheList )
+ }
+ else
+ {
+ // non-titan npcs get their model from their set file
+ string baseClass = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "BaseClass" ) )
+ array<string> keys = [ "DefaultModelName", "DefaultModelName_IMC", "DefaultModelName_MIL" ]
+
+ foreach ( key in keys )
+ {
+ var model = Dev_GetAISettingAssetByKeyField_Global( aiSettings, key )
+ if ( model == null )
+ continue
+
+ if ( model == $"" )
+ continue
+
+ expect asset( model )
+ autoPrecacheList.models.append( model )
+ }
+ }
+}
+
+void function AddTitanLoadoutToAutoPrecache( TitanLoadoutDef titanLoadout, AutoPrecacheList autoPrecacheList )
+{
+ array<string> weapons = GetWeaponsFromTitanLoadout( titanLoadout )
+ foreach ( weapon in weapons )
+ {
+ AddAutoPrecacheWeapon( autoPrecacheList, weapon )
+ }
+
+ #if MP
+ //Precache both the prime and non-prime versions
+ string primeSetFile
+ string nonPrimeSetFile
+ string titanClass = titanLoadout.titanClass
+ Assert( titanClass != "" )
+ nonPrimeSetFile = GetSetFileForTitanClassAndPrimeStatus( titanClass, false )
+ AddTitanSetFileToAutoPrecache( nonPrimeSetFile, autoPrecacheList )
+
+ if( TitanClassHasPrimeTitan( titanClass ) )
+ {
+ primeSetFile = GetSetFileForTitanClassAndPrimeStatus( titanClass, true )
+ AddTitanSetFileToAutoPrecache( primeSetFile, autoPrecacheList )
+ }
+ #elseif SP
+ string nonPrimeSetFile = titanLoadout.setFile
+ //printt( "nonPrimeSetFile: " + nonPrimeSetFile )
+ AddTitanSetFileToAutoPrecache( nonPrimeSetFile, autoPrecacheList )
+ #endif
+}
+
+void function AddTitanSetFileToAutoPrecache( string setFile, AutoPrecacheList autoPrecacheList )
+{
+ asset model = GetPlayerSettingsAssetForClassName( setFile, "bodymodel" )
+ autoPrecacheList.models.append( model )
+
+ autoPrecacheList.models.extend( GetModelsFromSetFile_3rdPerson( setFile ) )
+
+ asset hatchmodel = Dev_GetPlayerSettingAssetByKeyField_Global( setFile, "hatchmodel" )
+ if ( hatchmodel != $"" )
+ {
+ autoPrecacheList.models.append( hatchmodel )
+ }
+
+ AddAutoPrecacheScript( autoPrecacheList, setFile )
+
+ #if MP
+ autoPrecacheList.models.extend( GetModelsFromSetFile( setFile ) )
+ #endif
+}
+
+void function MarkNPCForAutoPrecache( string aiSettings )
+{
+ Assert( !file.forceAutoPrecacheAiSettings.contains( aiSettings ), "Already marked " + aiSettings + " for auto precache" )
+ file.forceAutoPrecacheAiSettings.append( aiSettings )
+}
+
+bool function AutoPrecacheUpToDate( AutoPrecacheList autoPrecacheList )
+{
+ foreach ( weapon in autoPrecacheList.weapons )
+ {
+ if ( !( weapon in file.autoPrecacheFound_weapons ) )
+ {
+ CodeWarning( "Auto Precache Failed: Weapon " + weapon + " not found." )
+ return false
+ }
+
+ if ( file.autoPrecacheFound_weapons[ weapon ] != autoPrecacheList.weaponCount[ weapon ] )
+ {
+ CodeWarning( "Auto Precache Failed: Weapon " + weapon + " count changed from " + file.autoPrecacheFound_weapons[ weapon ] + " to " + autoPrecacheList.weaponCount[ weapon ] )
+ return false
+ }
+
+ if ( !WeaponIsPrecached( weapon ) )
+ {
+ CodeWarning( "Auto Precache Failed: Weapon " + weapon + " is not precached." )
+ return false
+ }
+ }
+
+ foreach ( model in autoPrecacheList.models )
+ {
+ if ( !( model in file.autoPrecacheFound_models ) )
+ {
+ CodeWarning( "Auto Precache Failed: Model " + model + " not found." )
+ return false
+ }
+
+ if ( !ModelIsPrecached( model ) )
+ {
+ CodeWarning( "Auto Precache Failed: Model " + model + " is not precached." )
+ return false
+ }
+
+ //TODO: I think this is correct but it would make SP's autoprecache stuff need to get updated. Not worth the risk for R2.
+ /*if ( file.autoPrecacheFound_models.len() != autoPrecacheList.models.len() )
+ {
+ CodeWarning( "Auto Precache Failed: autoPrecacheFound_models.len() is not the same as autoPrecacheList.models.len()" )
+ return false
+ }*/
+ }
+
+ foreach ( settings, spawners in autoPrecacheList.npcSpawners )
+ {
+ if ( !( settings in file.autoPrecacheFound_npcs ) )
+ {
+ CodeWarning( "Auto Precache Failed: NPC " + settings + " not found." )
+ return false
+ }
+
+ if ( file.autoPrecacheFound_npcs[ settings ] != spawners.len() )
+ {
+ CodeWarning( "Auto Precache Failed: NPC spawner " + settings + " count changed from " + file.autoPrecacheFound_npcs[ settings ] + " to " + spawners.len() )
+ return false
+ }
+ }
+
+ // verify up to date autoprecache
+ return file.autoPrecacheVersion == AUTO_PRECACHE_VERSION
+}
+
+
+void function SetAutoPrecacheVersion( int ver )
+{
+ file.autoPrecacheVersion = ver
+}
+
+
+void function FillFromNPCSettings( array<string> npcAiSettings, AutoPrecacheList autoPrecacheList )
+{
+ table<string,bool> filledAiSettings
+
+ foreach ( aiSettings in file.forceAutoPrecacheAiSettings )
+ {
+ FillAISettingsPrecaches( aiSettings, autoPrecacheList )
+ filledAiSettings[ aiSettings ] <- true
+ }
+
+ // precache weapons from the AI
+ foreach ( aiSettings in npcAiSettings )
+ {
+ // any of these spawned in the level?
+ string baseClass = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "BaseClass" ) )
+ array<entity> spawners = GetSpawnerArrayByClassName( baseClass )
+
+ bool titanSettings = IsTitanAISettings( aiSettings )
+
+ foreach ( spawner in spawners )
+ {
+ // this may be set on the entity in leveled
+ table kvs = spawner.GetSpawnEntityKeyValues()
+
+ string leveledAISettings
+ if ( "leveled_aisettings" in kvs )
+ {
+ leveledAISettings = expect string( kvs.leveled_aisettings )
+ }
+
+ // this finds all spawners with the same baseclass, so only check the spawners that match ai settings.
+ if ( leveledAISettings == "" )
+ {
+ if ( baseClass != aiSettings )
+ continue
+ }
+ else
+ {
+ if ( leveledAISettings != aiSettings )
+ continue
+ }
+
+ if ( !( aiSettings in filledAiSettings ) )
+ {
+ // found a spawner with these leveled AI settings
+ FillAISettingsPrecaches( aiSettings, autoPrecacheList )
+ filledAiSettings[ aiSettings ] <- true
+ }
+
+ if ( !( aiSettings in autoPrecacheList.npcSpawners ) )
+ autoPrecacheList.npcSpawners[ aiSettings ] <- []
+ autoPrecacheList.npcSpawners[ aiSettings ].append( spawner )
+
+ if ( "script_drone_type" in kvs )
+ {
+ string script_drone_type = expect string( kvs.script_drone_type )
+ if ( !( script_drone_type in filledAiSettings ) )
+ {
+ filledAiSettings[ script_drone_type ] <- true
+ FillAISettingsPrecaches( script_drone_type, autoPrecacheList )
+ }
+ }
+
+ if ( "additionalequipment" in kvs )
+ {
+ string additionalequipment = expect string( kvs.additionalequipment )
+ if ( LegalWeaponString( additionalequipment ) && additionalequipment.find( "auto_" ) != 0 )
+ {
+ AddAutoPrecacheWeapon( autoPrecacheList, additionalequipment )
+ }
+ }
+
+ if ( "grenadeWeaponName" in kvs )
+ {
+ string grenadeWeaponName = expect string( kvs.grenadeWeaponName )
+ if ( LegalWeaponString( grenadeWeaponName ) )
+ {
+ AddAutoPrecacheWeapon( autoPrecacheList, grenadeWeaponName )
+ }
+ }
+
+ if ( titanSettings )
+ {
+ int titanType = int( expect string( kvs.TitanType ) )
+ string leveledTitanLoadout = expect string( kvs.leveled_titan_loadout )
+
+ TitanLoadoutDef loadout = GetTitanLoadoutFromPlayerSetFile( leveledTitanLoadout )
+
+ array<string> weapons = GetWeaponsFromTitanLoadout( loadout )
+ foreach ( weapon in weapons )
+ {
+ AddAutoPrecacheWeapon( autoPrecacheList, weapon )
+ }
+
+ #if SP
+ if ( titanType == TITAN_MERC )
+ {
+ // we have a boss!
+ string titanSettings = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" ) )
+ string bossName = GetMercCharacterForSetFile( titanSettings )
+ BossTitanData bossTitanData = GetBossTitanData( bossName )
+ autoPrecacheList.models.append( bossTitanData.characterModel )
+ }
+ #endif
+ }
+ }
+ }
+}
+
+AutoPrecacheList function GenerateAutoPrecacheListForLevel()
+{
+ AutoPrecacheList autoPrecacheList
+
+ FillFromNPCSettings( GetAllNPCSettings(), autoPrecacheList )
+ array<string> deprecatedNPCs = GetAllDeprecatedNPCSettings()
+ FillFromNPCSettings( deprecatedNPCs, autoPrecacheList )
+
+ foreach ( aiSettings in deprecatedNPCs )
+ {
+ if ( !( aiSettings in autoPrecacheList.npcSpawners ) )
+ continue
+ foreach ( spawner in autoPrecacheList.npcSpawners[ aiSettings ] )
+ {
+ CodeWarning( "Found deprecated NPC " + aiSettings + " at " + spawner.GetSpawnEntityKeyValues().origin )
+ }
+ }
+
+ foreach ( npc in GetNPCArray() )
+ {
+ if ( !IsValid( npc ) )
+ continue
+
+ string weapon = expect string( npc.kv.additionalequipment )
+ if ( LegalWeaponString( weapon ) )
+ AddAutoPrecacheWeapon( autoPrecacheList, weapon )
+// string weapon = npc.AISetting_GetDefaultWeapon()
+// if ( LegalWeaponString( weapon ) )
+// weapons.append( weapon )
+
+ if ( npc.HasKey( "grenadeWeaponName" ) )
+ {
+ string grenadeWeaponName = expect string( npc.kv.grenadeWeaponName )
+ if ( LegalWeaponString( grenadeWeaponName ) )
+ {
+ AddAutoPrecacheWeapon( autoPrecacheList, grenadeWeaponName )
+ }
+ }
+
+ string grenadeWeapon = npc.AISetting_GetGrenadeWeapon()
+ if ( grenadeWeapon != "" )
+ AddAutoPrecacheWeapon( autoPrecacheList, grenadeWeapon )
+
+ var AdditionalScriptWeapon = npc.Dev_GetAISettingByKeyField( "AdditionalScriptWeapon" )
+ if ( AdditionalScriptWeapon != null )
+ {
+ expect string( AdditionalScriptWeapon )
+ AddAutoPrecacheWeapon( autoPrecacheList, AdditionalScriptWeapon )
+ }
+ }
+
+ #if SP
+ LeveledScriptedWeapons leveledScriptedWeapons = GetAllLeveledScriptWeapons()
+ foreach ( weaponClass, _ in leveledScriptedWeapons.foundScriptWeapons )
+ {
+ AddAutoPrecacheWeapon( autoPrecacheList, weaponClass )
+ }
+
+ array<string> weapons
+
+ weapons = GetNPCDefaultWeapons()
+ foreach ( weapon in weapons )
+ {
+ AddAutoPrecacheWeapon( autoPrecacheList, weapon )
+ }
+
+ PilotLoadoutDef loadout = GetPilotLoadoutForCurrentMapSP()
+ weapons = GetWeaponsFromPilotLoadout( loadout )
+ foreach ( weapon in weapons )
+ {
+ AddAutoPrecacheWeapon( autoPrecacheList, weapon )
+ }
+
+ autoPrecacheList.models.extend( GetModelsFromSetFile( loadout.setFile ) )
+ AddAutoPrecacheScript( autoPrecacheList, loadout.setFile )
+
+ TitanLoadoutDef titanLoadout = GetTitanLoadoutForCurrentMap()
+ autoPrecacheList.models.extend( GetModelsFromSetFile( titanLoadout.setFile ) )
+ AddAutoPrecacheScript( autoPrecacheList, titanLoadout.setFile )
+ #endif
+
+
+ #if MP
+ array<string> pilotTypes = GetAllItemRefsOfType( eItemTypes.PILOT_SUIT )
+
+ foreach ( suit in pilotTypes )
+ {
+ string suitMale = GetSuitAndGenderBasedSetFile( suit, "race_human_male" )
+ autoPrecacheList.models.extend( GetModelsFromSetFile( suitMale ) )
+ AddAutoPrecacheScript( autoPrecacheList, suitMale )
+
+ string suitFemale = GetSuitAndGenderBasedSetFile( suit, "race_human_female" )
+ autoPrecacheList.models.extend( GetModelsFromSetFile( suitFemale ) )
+ AddAutoPrecacheScript( autoPrecacheList, suitFemale )
+ }
+ #endif
+
+ array<TitanLoadoutDef> titanLoadouts = GetAllowedTitanLoadouts()
+
+ foreach ( loadout in titanLoadouts )
+ {
+ #if MP
+ // in sp we dont want all the extra cockpit models and whatnot
+ AddTitanLoadoutToAutoPrecache( loadout, autoPrecacheList )
+ #endif
+
+ #if SP
+ // in sp it would be good to get away from giving all weapons on all levels
+ weapons = GetWeaponsFromTitanLoadout( loadout )
+ foreach ( weapon in weapons )
+ {
+ AddAutoPrecacheWeapon( autoPrecacheList, weapon )
+ }
+ #endif
+ }
+
+ AutoPrecache_InitFlightpathShared( autoPrecacheList )
+
+ autoPrecacheList.weapons.sort( SortStringAlphabetize )
+
+ table<string,int> weaponCount
+ foreach ( weapon in autoPrecacheList.weapons )
+ {
+ if ( !( weapon in weaponCount ) )
+ weaponCount[ weapon ] <- 0
+ weaponCount[ weapon ]++
+ }
+
+ autoPrecacheList.weaponCount = weaponCount
+
+ RemoveDupesFromSorted_String( autoPrecacheList.weapons )
+
+ autoPrecacheList.models.sort( SortAssetAlphabetize )
+ RemoveDupesFromSorted_Asset( autoPrecacheList.models )
+
+ return autoPrecacheList
+}
+
+void function AddAutoPrecacheScript( AutoPrecacheList autoPrecacheList, string settings )
+{
+ var autoprecache = Dev_GetPlayerSettingByKeyField_Global( settings, "autoprecache_script" )
+ if ( autoprecache == null )
+ return
+
+ expect string( autoprecache )
+ Assert( autoprecache != "" )
+
+ asset bodyModel = GetPlayerSettingsAssetForClassName( settings, "bodymodel" )
+ autoPrecacheList.autoPrecacheScript[ bodyModel ] <- autoprecache
+}
+
+void function AP_NPCSpawnerFound( string settings, int count )
+{
+ file.autoPrecacheFound_npcs[ settings ] <- count
+}
+
+void function AP_PrecacheWeapon( string weapon, int count )
+{
+ file.autoPrecacheFound_weapons[ weapon ] <- count
+
+ PrecacheWeapon( weapon )
+}
+
+void function AP_PrecacheModel( asset model )
+{
+ file.autoPrecacheFound_models[ model ] <- true
+
+ PrecacheModel( model )
+}
+
+void function ExportAutoPrecacheList( AutoPrecacheList autoPrecacheList )
+{
+ string mapName
+ #if SP
+ mapName = GetMapName().toupper()
+ #endif
+
+ #if MP
+ mapName = "MP"
+ #endif
+
+ // Write function open
+ DevTextBufferClear()
+ // Write verification call
+
+ DevTextBufferWrite( "global function " + mapName + "_AutoPrecache\n\n" )
+ DevTextBufferWrite( "void function " + mapName + "_AutoPrecache()\n" )
+ DevTextBufferWrite( "{\n" )
+
+ DevTextBufferWrite( "#if DEV\n" )
+
+ DevTextBufferWrite( " #if SERVER\n" )
+ DevTextBufferWrite( " SetAutoPrecacheVersion( " + AUTO_PRECACHE_VERSION + " )\n" )
+
+ DevTextBufferWrite( " // NPC spawners found:\n" )
+ array<string> spawnerNames
+ foreach ( aiSettings, spawnerArray in autoPrecacheList.npcSpawners )
+ {
+ spawnerNames.append( aiSettings )
+ }
+ spawnerNames.sort( SortStringAlphabetize )
+
+ foreach ( aiSettings in spawnerNames )
+ {
+ array<entity> spawnerArray = autoPrecacheList.npcSpawners[ aiSettings ]
+ DevTextBufferWrite( " AP_NPCSpawnerFound( \"" + aiSettings + "\", " + spawnerArray.len() + " )\n" )
+ }
+ DevTextBufferWrite( " #endif\n" )
+ DevTextBufferWrite( "\n" )
+
+ foreach ( weapon in autoPrecacheList.weapons )
+ {
+ int count = autoPrecacheList.weaponCount[ weapon ]
+ DevTextBufferWrite( " AP_PrecacheWeapon( \"" + weapon + "\", " + count + " )\n" )
+ }
+
+ foreach ( model in autoPrecacheList.models )
+ {
+ DevTextBufferWrite( " AP_PrecacheModel( " + model + " )\n" )
+ }
+
+ DevTextBufferWrite( "#endif\n\n" )
+
+ DevTextBufferWrite( "#if !DEV\n" )
+
+ DevTextBufferWrite( "\n" )
+
+ foreach ( weapon in autoPrecacheList.weapons )
+ {
+ int count = autoPrecacheList.weaponCount[ weapon ]
+ DevTextBufferWrite( " PrecacheWeapon( \"" + weapon + "\" )\n" )
+ }
+
+ foreach ( model in autoPrecacheList.models )
+ {
+ DevTextBufferWrite( " PrecacheModel( " + model + " )\n" )
+ }
+
+ DevTextBufferWrite( "#endif\n\n" )
+
+ DevTextBufferWrite( "#if CLIENT\n" )
+
+ array<string>[4] titanModelAssets
+ foreach ( model, script in autoPrecacheList.autoPrecacheScript )
+ {
+ switch ( script )
+ {
+ case "atlas":
+ titanModelAssets[ 0 ].append( " ClTitanAtlas_Init( " + model + " )\n" )
+ break
+
+ case "ogre":
+
+ titanModelAssets[ 1 ].append( " ClTitanOgre_Init( " + model + " )\n" )
+ break
+
+ case "stryder":
+ titanModelAssets[ 2 ].append( " ClTitanStryder_Init( " + model + " )\n" )
+ break
+
+ case "buddy":
+ titanModelAssets[ 2 ].append( " ClTitanBuddy_Init( " + model + " )\n" )
+ break
+
+ default:
+ Assert( 0, "Unknown autoprecache_script key " + script )
+ break
+ }
+ }
+
+ foreach( arrayOfAsset in titanModelAssets ) //Sort output so exported precache file can be diffed easily
+ {
+ arrayOfAsset.sort( SortStringAlphabetize )
+ foreach( assetElement in arrayOfAsset )
+ {
+ DevTextBufferWrite( assetElement )
+ }
+ }
+
+ DevTextBufferWrite( "#endif\n" )
+
+ // Write function close
+ DevTextBufferWrite( "}\n\n" )
+
+
+ #if SP
+ string filename = "scripts/vscripts/sp/autoprecache/" + mapName + "_autoprecache.nut"
+ #endif
+
+ #if MP
+ string filename = "scripts/vscripts/mp/" + mapName + "_autoprecache.nut"
+ #endif
+ DevP4Checkout( filename )
+ DevTextBufferDumpToFile( filename )
+ DevP4Add( filename )
+ printt( "Wrote " + filename )
+}
+
+#endif \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_bubble_shield.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_bubble_shield.gnut
new file mode 100644
index 00000000..30758bec
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_bubble_shield.gnut
@@ -0,0 +1,524 @@
+global function BubbleShield_Init
+
+global function CreateBubbleShield
+global function IsTitanWithinBubbleShield
+global function TitanHasBubbleShieldWeapon
+global function LetTitanPlayerShootThroughBubbleShield
+global function CreateGenericBubbleShield
+global function CreateParentedBubbleShield
+
+global function WaitUntilTitanStandsOrDies
+global function DestroyBubbleShield
+global function CreateBubbleShieldWithSettings
+
+const float SHIELD_TITAN_DAMAGE_FLOOR = 250.0
+const float SHIELD_TITAN_DAMAGE_CEILING = 16000 //Some arbitrarily large number really
+const float SHIELD_PILOT_DAMAGE_FLOOR = 30.0
+const float SHIELD_PILOT_DAMAGE_CEILING = 60.0
+const float SHIELD_NPC_DAMAGE_FLOOR = 30.0
+
+const float SHIELD_FADE_ARBITRARY_DELAY = 3.0
+const float SHIELD_FADE_ENDCAP_DELAY = 1.0
+
+const float SHIELD_DISTANCE_TO_DESTROY = 40
+
+struct BubbleShieldDamageStruct
+{
+ float damageFloor
+ float damageCeiling
+ array<float> quadraticPolynomialCoefficients //Should actually be float[3], but because float[ 3 ] and array<float> are different types and this needs to be fed into EvaluatePolynomial make it an array<float> instead
+}
+
+struct
+{
+ BubbleShieldDamageStruct titanDamageStruct
+ BubbleShieldDamageStruct pilotDamageStruct
+ BubbleShieldDamageStruct aiDamageStruct
+
+}file
+
+
+void function BubbleShield_Init()
+{
+ RegisterSignal( "TitanBrokeBubbleShield" )
+ RegisterSignal( "NewBubbleShield" )
+ RegisterSignal( "StopBubbleShieldDamage" )
+
+ InitBubbleShieldDamageStructValues( file.titanDamageStruct, SHIELD_TITAN_DAMAGE_FLOOR, SHIELD_TITAN_DAMAGE_CEILING, [ 12.0, 5.0, 2.0 ] )
+ InitBubbleShieldDamageStructValues( file.pilotDamageStruct, SHIELD_PILOT_DAMAGE_FLOOR, SHIELD_PILOT_DAMAGE_CEILING, [ 2.0, 1.0, 1.0 ] )
+ InitBubbleShieldDamageStructValues( file.aiDamageStruct, SHIELD_PILOT_DAMAGE_FLOOR, SHIELD_PILOT_DAMAGE_CEILING, [ 2.0, 1.0, 1.0 ] )
+}
+
+void function InitBubbleShieldDamageStructValues( BubbleShieldDamageStruct damageStruct, float damageFloor, float damageCeiling, array<float> quadPolynomialCoeffs )
+{
+ damageStruct.damageFloor = damageFloor
+ damageStruct.damageCeiling = damageCeiling
+ damageStruct.quadraticPolynomialCoefficients = quadPolynomialCoeffs
+}
+
+void function CreateBubbleShield( entity titan, vector origin, vector angles )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ titan.Signal( "ClearDisableTitanfall" )
+
+ entity soul = titan.GetTitanSoul()
+ entity player = soul.GetBossPlayer()
+
+ if ( !IsValid( player ) )
+ return
+
+ if ( !svGlobal.bubbleShieldEnabled )
+ return
+
+ player.EndSignal( "OnDestroy" )
+
+ float embarkTime = GetBubbleShieldDuration( player )
+ float bubTime = embarkTime + SHIELD_FADE_ARBITRARY_DELAY + SHIELD_FADE_ENDCAP_DELAY
+
+ soul.Signal( "NewBubbleShield" )
+ entity bubbleShield = CreateBubbleShieldWithSettings( titan.GetTeam(), origin, angles, player, bubTime )
+ bubbleShield.SetBossPlayer( player ) // so code knows AI should try to shoot at titan inside shield
+ soul.soul.bubbleShield = bubbleShield
+
+ player.SetTitanBubbleShieldTime( Time() + GetBubbleShieldDuration( player ) ) //This sets the time to display "Titan Shielded" on the HUD
+
+ AI_CreateDangerousArea_Static( bubbleShield, null, TITAN_BUBBLE_SHIELD_INVULNERABILITY_RANGE, titan.GetTeam(), true, true, origin )
+
+ //titan.SetNPCPriorityOverride( 1 )
+
+ OnThreadEnd(
+ function () : ( titan, soul, player, bubbleShield )
+ {
+ if ( IsValid( player ) )
+ player.SetTitanBubbleShieldTime( 0 ) //This sets the time to display "Titan Shielded" on the HUD
+
+ CleanupTitanBubbleShieldVars( titan, soul, bubbleShield )
+
+ }
+ )
+
+ waitthread WaitUntilShieldFades( player, titan, bubbleShield, bubTime + 4.0 )
+}
+
+void function MonitorTitanMovement( entity soul, entity bubbleShield )
+{
+ entity titan = soul.GetTitan()
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "OnTitanDeath" )
+ bubbleShield.EndSignal( "OnDestroy" )
+ titan.EndSignal( "OnDestroy" )
+
+ vector startPos = titan.GetOrigin()
+ float endTime = Time() + SHIELD_FADE_ARBITRARY_DELAY
+ while( endTime >= Time() )
+ {
+ if ( Distance( titan.GetOrigin(), startPos ) > SHIELD_DISTANCE_TO_DESTROY )
+ break
+
+ wait 0.1
+ }
+
+ soul.Signal( "TitanBrokeBubbleShield" )
+}
+
+void function CreateGenericBubbleShield( entity titan, vector origin, vector angles, float duration = 9999.0 )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ entity soul = titan.GetTitanSoul()
+ soul.Signal( "NewBubbleShield" )
+ entity bubbleShield = CreateBubbleShieldWithSettings( titan.GetTeam(), origin, angles, titan, 9999 )
+ soul.soul.bubbleShield = bubbleShield
+
+ titan.SetNPCPriorityOverride( 10 )
+
+ OnThreadEnd(
+ function () : ( titan, soul, bubbleShield )
+ {
+ CleanupTitanBubbleShieldVars( titan, soul, bubbleShield )
+ }
+ )
+
+ waitthread WaitUntilShieldFades( null, titan, bubbleShield, duration )
+}
+
+void function CreateParentedBubbleShield( entity titan, vector origin, vector angles, float duration = 9999.0 )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ entity soul = titan.GetTitanSoul()
+ soul.Signal( "NewBubbleShield" )
+ entity bubbleShield = CreateBubbleShieldWithSettings( titan.GetTeam(), origin, angles, titan, 9999 )
+ soul.soul.bubbleShield = bubbleShield
+
+ titan.SetNPCPriorityOverride( 10 )
+
+ OnThreadEnd(
+ function () : ( titan, soul, bubbleShield )
+ {
+ CleanupTitanBubbleShieldVars( titan, soul, bubbleShield )
+ }
+ )
+
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "OnDestroy" )
+
+ soul.soul.bubbleShield.SetParent( titan, "ORIGIN" )
+ table bubleshieldDotS = expect table( soul.soul.bubbleShield.s )
+ entity friendlyColoredFX = expect entity (bubleshieldDotS.friendlyColoredFX )
+ entity enemyColoredFX = expect entity (bubleshieldDotS.enemyColoredFX )
+ friendlyColoredFX.SetParent( soul.soul.bubbleShield )
+ enemyColoredFX.SetParent( soul.soul.bubbleShield )
+
+ wait duration
+}
+
+void function CleanupTitanBubbleShieldVars( entity titan, entity soul, entity bubbleShield )
+{
+ DestroyBubbleShield( bubbleShield )
+
+ if ( IsValid( soul ) ){
+ soul.soul.bubbleShield = null
+ }
+
+ if ( IsAlive( titan ) )
+ titan.ClearNPCPriorityOverride()
+}
+
+void function DestroyBubbleShield( entity bubbleShield )
+{
+ if ( IsValid( bubbleShield ) )
+ {
+ ClearChildren( bubbleShield )
+ bubbleShield.Destroy()
+ }
+}
+
+entity function CreateBubbleShieldWithSettings( int team, vector origin, vector angles, entity owner = null, float duration = 9999 )
+{
+ entity bubbleShield = CreateEntity( "prop_dynamic" )
+ bubbleShield.SetValueForModelKey( $"models/fx/xo_shield.mdl" )
+ bubbleShield.kv.solid = SOLID_VPHYSICS
+ bubbleShield.kv.rendercolor = "81 130 151"
+ bubbleShield.kv.contents = (int(bubbleShield.kv.contents) | CONTENTS_NOGRAPPLE)
+ bubbleShield.SetOrigin( origin )
+ bubbleShield.SetAngles( angles )
+ // Blocks bullets, projectiles but not players and not AI
+ bubbleShield.kv.CollisionGroup = TRACE_COLLISION_GROUP_BLOCK_WEAPONS
+ bubbleShield.SetBlocksRadiusDamage( true )
+ DispatchSpawn( bubbleShield )
+ bubbleShield.Hide()
+
+ SetTeam( bubbleShield, team )
+ array<entity> bubbleShieldFXs
+
+ vector coloredFXOrigin = origin + Vector( 0, 0, 25 )
+ table bubbleShieldDotS = expect table( bubbleShield.s )
+ if ( team == TEAM_UNASSIGNED )
+ {
+ entity neutralColoredFX = StartParticleEffectInWorld_ReturnEntity( BUBBLE_SHIELD_FX_PARTICLE_SYSTEM_INDEX, coloredFXOrigin, <0, 0, 0> )
+ SetTeam( neutralColoredFX, team )
+ bubbleShieldDotS.neutralColoredFX <- neutralColoredFX
+ bubbleShieldFXs.append( neutralColoredFX )
+ }
+ else
+ {
+ //Create friendly and enemy colored particle systems
+ entity friendlyColoredFX = StartParticleEffectInWorld_ReturnEntity( BUBBLE_SHIELD_FX_PARTICLE_SYSTEM_INDEX, coloredFXOrigin, <0, 0, 0> )
+ SetTeam( friendlyColoredFX, team )
+ friendlyColoredFX.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY
+ EffectSetControlPointVector( friendlyColoredFX, 1, FRIENDLY_COLOR_FX )
+
+ entity enemyColoredFX = StartParticleEffectInWorld_ReturnEntity( BUBBLE_SHIELD_FX_PARTICLE_SYSTEM_INDEX, coloredFXOrigin, <0, 0, 0> )
+ SetTeam( enemyColoredFX, team )
+ enemyColoredFX.kv.VisibilityFlags = ENTITY_VISIBLE_TO_ENEMY
+ EffectSetControlPointVector( enemyColoredFX, 1, ENEMY_COLOR_FX )
+
+ bubbleShieldDotS.friendlyColoredFX <- friendlyColoredFX
+ bubbleShieldDotS.enemyColoredFX <- enemyColoredFX
+ bubbleShieldFXs.append( friendlyColoredFX )
+ bubbleShieldFXs.append( enemyColoredFX )
+ }
+
+ #if MP
+ DisableTitanfallForLifetimeOfEntityNearOrigin( bubbleShield, origin, TITANHOTDROP_DISABLE_ENEMY_TITANFALL_RADIUS )
+ #endif
+
+ EmitSoundOnEntity( bubbleShield, "BubbleShield_Sustain_Loop" )
+
+ thread CleanupBubbleShield( bubbleShield, bubbleShieldFXs, duration )
+ thread BubbleShieldDamageEnemies( bubbleShield, owner )
+
+ return bubbleShield
+}
+
+void function CleanupBubbleShield( entity bubbleShield, array<entity> bubbleShieldFXs, float fadeTime )
+{
+ bubbleShield.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function () : ( bubbleShield, bubbleShieldFXs )
+ {
+ if ( IsValid_ThisFrame( bubbleShield ) )
+ {
+ StopSoundOnEntity( bubbleShield, "BubbleShield_Sustain_Loop" )
+ EmitSoundOnEntity( bubbleShield, "BubbleShield_End" )
+ DestroyBubbleShield( bubbleShield )
+ }
+
+ foreach ( fx in bubbleShieldFXs )
+ {
+ if ( IsValid_ThisFrame( fx ) )
+ {
+ EffectStop( fx )
+ }
+ }
+ }
+ )
+
+ wait fadeTime
+}
+
+void function WaitUntilShieldFades( entity player, entity titan, entity bubbleShield, float failTime )
+{
+ bubbleShield.EndSignal( "OnDestroy" )
+ entity soul = titan.GetTitanSoul()
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "NewBubbleShield" )
+
+ soul.EndSignal( "TitanBrokeBubbleShield" )
+
+ if ( player != null )
+ waitthread WaitUntilPlayerTitanStandsOrDies( player, titan, failTime )
+ else
+ waitthread WaitUntilTitanStandsOrDies( titan, failTime )
+
+ // have to add this since OnTitanDeath is somewhat unreliable, especially in the middle of titan transfer
+ if ( !IsAlive( soul.GetTitan() ) )
+ return
+
+ thread MonitorTitanMovement( soul, bubbleShield )
+ wait SHIELD_FADE_ARBITRARY_DELAY
+}
+
+void function WaitUntilPlayerTitanStandsOrDies( entity player, entity titan, float failTime )
+{
+ waitthread WaitUntilTitanStandsOrDies( titan, failTime )
+
+ if ( !IsAlive( player ) )
+ return
+
+ if ( IsPlayerEmbarking( player ) && player.Anim_IsActive() )
+ WaittillAnimDone( player )
+}
+
+void function WaitUntilTitanStandsOrDies( entity titan, float timeout = -1.0 )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "ChangedTitanMode" )
+ float endTime = Time() + timeout
+
+ for ( ;; )
+ {
+ if ( titan.GetTitanSoul().GetStance() == STANCE_STAND )
+ return
+
+ if ( Time() > endTime && timeout != -1 )
+ break
+
+ wait 0.2
+ }
+}
+
+void function BubbleShieldDamageEnemies( entity bubbleShield, entity bubbleShieldPlayer )
+{
+ bubbleShield.EndSignal( "OnDestroy" )
+ if ( IsValid( bubbleShieldPlayer ) )
+ bubbleShieldPlayer.EndSignal( "OnDestroy" )
+
+ bubbleShield.EndSignal( "StopBubbleShieldDamage" )
+
+ entity trigger = CreateEntity( "trigger_cylinder" )
+ trigger.SetRadius( TITAN_BUBBLE_SHIELD_INVULNERABILITY_RANGE )
+ trigger.SetAboveHeight( TITAN_BUBBLE_SHIELD_CYLINDER_TRIGGER_HEIGHT ) //Still not quite a sphere, will see if close enough
+ trigger.SetBelowHeight( 0 )
+ trigger.SetOrigin( bubbleShield.GetOrigin() )
+ trigger.SetParent( bubbleShield )
+ DispatchSpawn( trigger )
+
+ trigger.SearchForNewTouchingEntity() //JFS: trigger.GetTouchingEntities() will not return entities already in the trigger unless this is called. See bug 202843
+
+ /*DebugDrawCylinder( trigger.GetOrigin(), <270,0,0>, TITAN_BUBBLE_SHIELD_INVULNERABILITY_RANGE, TITAN_BUBBLE_SHIELD_CYLINDER_TRIGGER_HEIGHT, 255, 255, 255, true, 20.0 )
+ DebugDrawSphere( bubbleShield.GetOrigin(), TITAN_BUBBLE_SHIELD_INVULNERABILITY_RANGE, 255, 0, 0, true, 20 )*/
+ OnThreadEnd(
+ function() : ( trigger )
+ {
+ trigger.Destroy()
+ }
+ )
+
+ float refreshLowerBound = 0.5
+ float refreshUpperBound = 0.8
+
+ table<entity, int> soulTable = {}
+ table<entity, int> npcTable = {}
+ table<entity, int> pilotTable = {}
+
+ table<entity, int> countTable
+
+ while ( true )
+ {
+ array<entity> touchingEnts = trigger.GetTouchingEntities()
+
+ foreach( touchingEnt in touchingEnts )
+ {
+ if ( touchingEnt.IsTitan() )
+ countTable = soulTable
+ else if( touchingEnt.IsPlayer() )
+ countTable = pilotTable
+ else
+ countTable = npcTable
+
+ DamageEntWithinBubbleShield( bubbleShield, bubbleShieldPlayer, touchingEnt, countTable )
+ }
+
+ wait RandomFloatRange( refreshLowerBound, refreshUpperBound )
+ }
+}
+
+void function LetTitanPlayerShootThroughBubbleShield( entity titanPlayer )
+{
+ Assert( titanPlayer.IsTitan() )
+
+ entity soul = titanPlayer.GetTitanSoul()
+ entity bubbleShield = soul.soul.bubbleShield
+
+ if ( !IsValid( bubbleShield ) )
+ return
+
+ bubbleShield.SetOwner( titanPlayer ) //After this, player is able to fire out from shield. WATCH OUT FOR POTENTIAL COLLISION BUGS!
+
+ thread MonitorLastFireTime( titanPlayer )
+ thread StopPlayerShootThroughBubbleShield( titanPlayer, bubbleShield )
+}
+
+void function StopPlayerShootThroughBubbleShield( entity player, entity bubbleShield )
+{
+ player.EndSignal( "OnDeath" )
+ player.WaitSignal( "OnChangedPlayerClass" ) //Kill this thread once player gets out of the Titan
+
+ if ( !IsValid( bubbleShield ) )
+ return
+
+ bubbleShield.SetOwner( null )
+}
+
+void function MonitorLastFireTime( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "OnChangedPlayerClass" ) //Kill this thread once player gets out of the Titan
+
+ player.WaitSignal( "OnPrimaryAttack" ) //Sent when player fires his weapon
+ //printt( "Player fired weapon! in MonitorLastFireTime" )
+
+ entity soul = player.GetTitanSoul()
+
+ if ( !IsValid( soul ) )
+ return
+
+ soul.Signal( "TitanBrokeBubbleShield" ) //WaitUntilShieldFades will end when this signal is sent
+}
+
+void function DamageEntWithinBubbleShield( entity bubbleShield, entity bubbleShieldPlayer, entity touchingEnt, table<entity, int> countTable, )
+{
+ int ownerTeam = IsValid( bubbleShieldPlayer ) ? bubbleShieldPlayer.GetTeam() : bubbleShield.GetTeam()
+ if ( !BubbleShieldShouldDamage( bubbleShield, ownerTeam, touchingEnt ) )
+ return
+
+ entity entInCountTable = null
+
+ if ( touchingEnt.IsTitan() )
+ {
+ entity soul = touchingEnt.GetTitanSoul()
+ if ( !IsValid( soul ) )
+ return
+
+ entInCountTable = soul
+ }
+ else
+ {
+ entInCountTable = touchingEnt
+ }
+
+ if ( IsValid( entInCountTable ) && !( entInCountTable in countTable ) )
+ countTable[ entInCountTable ] <- 0
+
+ int timesTouched = ++countTable[ entInCountTable ]
+
+ BubbleShieldDamageStruct damageStruct
+
+ if ( touchingEnt.IsTitan() )
+ damageStruct = file.titanDamageStruct
+ else if ( touchingEnt.IsPlayer() )
+ damageStruct = file.pilotDamageStruct
+ else
+ damageStruct = file.aiDamageStruct
+
+ float damageAmount = damageStruct.damageFloor + EvaluatePolynomial( float ( countTable[ entInCountTable ] ), damageStruct.quadraticPolynomialCoefficients )
+
+ //printt( "Damage amount: " + damageAmount + ", touchingEnt: " + touchingEnt )
+
+ touchingEnt.TakeDamage( damageAmount, bubbleShieldPlayer, bubbleShield, { origin = bubbleShield.GetOrigin(), damageSourceId=eDamageSourceId.bubble_shield } )
+ StatusEffect_AddTimed( touchingEnt, eStatusEffect.emp, 0.1, 1.0, 0.2 )
+
+ EmitSoundOnEntity( bubbleShield, "titan_energyshield_damage" )
+}
+
+bool function BubbleShieldShouldDamage( entity bubbleShield, int ownerTeam, entity ent )
+{
+ if ( !IsAlive( ent ) )
+ return false
+
+ if ( ownerTeam == ent.GetTeam() )
+ return false
+
+ /*if ( ent.IsTitan() && IsTitanWithinBubbleShield( ent ) )
+ return false*/
+
+ if ( ! ( ent instanceof CBaseCombatCharacter ) ) //Projectiles etc won't get damaged
+ return false
+
+ float distSqr = DistanceSqr( bubbleShield.GetOrigin(), ent.GetOrigin() )
+
+ return distSqr <= TITAN_BUBBLE_SHIELD_INVULNERABILITY_RANGE_SQUARED
+}
+
+bool function IsTitanWithinBubbleShield( entity titan )
+{
+ if ( !IsAlive( titan ) )
+ return false
+
+ entity soul = titan.GetTitanSoul()
+
+ if ( !IsValid( soul ) ) //Bug 152438. Defensive coding, but there's a small window after embarking where the npc Titan doesn't have a soul anymore but can be damaged
+ return false
+
+ if ( !IsValid( soul.soul.bubbleShield ) )
+ return false
+
+ return DistanceSqr( soul.soul.bubbleShield.GetOrigin(), titan.GetOrigin() ) < TITAN_BUBBLE_SHIELD_INVULNERABILITY_RANGE * TITAN_BUBBLE_SHIELD_INVULNERABILITY_RANGE
+}
+
+bool function TitanHasBubbleShieldWeapon( entity titan )
+{
+ entity weapon = titan.GetActiveWeapon()
+ if ( IsValid( weapon ) && IsValid( weapon.w.bubbleShield ) )
+ return true
+
+ return false
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_common.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_common.gnut
new file mode 100644
index 00000000..b08fdcf1
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_common.gnut
@@ -0,0 +1,853 @@
+
+global function CodeCallback_DamageEntity
+global function HandleFootstepDamage
+global function CodeCallback_OnEntityKilled
+
+global function AddDamageCallback
+global function RemoveDamageCallback
+global function RunClassDamageCallbacks
+global function AddDamageFinalCallback
+global function RunClassDamageFinalCallbacks
+global function AddPostDamageCallback
+global function RunClassPostDamageCallbacks
+
+global function AddDamageByCallback
+global function AddDamageCallbackSourceID
+global function AddDeathCallback
+global function RemoveDeathCallback
+global function AddSoulDeathCallback
+global function AddCallback_OnPlayerRespawned
+global function AddCallback_OnPlayerKilled
+global function AddCallback_OnNPCKilled
+global function AddCallback_OnTitanDoomed
+global function AddCallback_OnTitanHealthSegmentLost
+global function AddCallback_OnClientConnecting
+global function AddCallback_OnClientConnected
+global function AddCallback_OnClientDisconnected
+global function AddCallback_OnPilotBecomesTitan
+global function AddCallback_OnTitanBecomesPilot
+global function AddCallback_EntityChangedTeam
+global function AddCallback_OnTouchHealthKit
+global function AddCallback_OnPlayerAssist
+global function AddCallback_OnPlayerGetsNewPilotLoadout
+global function AddCallback_OnTitanGetsNewTitanLoadout
+global function AddCallback_OnUpdateDerivedPilotLoadout
+global function AddCallback_OnUpdateDerivedTitanLoadout
+global function AddCallback_OnUpdateDerivedPlayerTitanLoadout
+global function AddClientCommandCallback
+global function AddPlayerDropScriptedItemsCallback
+global function AddCallback_OnPlayerInventoryChanged
+
+// Register functions are called when an entity spawns.
+global function RegisterForDamageDeathCallbacks
+global function CodeCallback_OnInventoryChanged
+global function CodeCallback_OnEntityChangedTeam
+
+global function AddEntityCallback_OnDamaged
+global function RemoveEntityCallback_OnDamaged
+global function AddEntityCallback_OnPostDamaged
+global function RemoveEntityCallback_OnPostDamaged
+global function AddEntityCallback_OnKilled
+global function RemoveEntityCallback_OnKilled
+global function AddEntityCallback_OnPostShieldDamage
+global function RemoveEntityCallback_OnPostShieldDamage
+
+global function AddTitanCallback_OnHealthSegmentLost
+global function RemoveTitanCallback_OnHealthSegmentLost
+
+// Player movement callbacks
+global function AddPlayerMovementEventCallback
+global function RemovePlayerMovementEventCallback
+global function CodeCallback_OnPlayerJump
+global function CodeCallback_OnPlayerDoubleJump
+global function CodeCallback_OnPlayerDodge
+global function CodeCallback_OnPlayerLeaveGround
+global function CodeCallback_OnPlayerTouchGround
+global function CodeCallback_OnPlayerMantle
+global function CodeCallback_OnPlayerBeginWallrun
+global function CodeCallback_OnPlayerEndWallrun
+global function CodeCallback_OnPlayerBeginWallhang
+global function CodeCallback_OnPlayerEndWallhang
+
+struct
+{
+ table<string, array< void functionref( entity, var ) > > classDamageCallbacks
+ table<string, array< void functionref( entity, var ) > > classDamageFinalCallbacks
+ table<string, array< void functionref( entity, var ) > > classPostDamageCallbacks
+ array< void functionref( entity ) > playerInventoryChangedCallbacks
+} file
+
+void function CodeCallback_DamageEntity( entity ent, var damageInfo )
+{
+ // gametype script decides if ent should take damage
+ if ( !ScriptCallback_ShouldEntTakeDamage( ent, damageInfo ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( "CodeCallback_DamageEntity() top:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == damagedef_titan_step )
+ HandleFootstepDamage( ent, damageInfo )
+
+ RunClassDamageCallbacks( ent, damageInfo )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after class damage callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ // Added via AddEntityCallback_OnDamaged
+ foreach ( callbackFunc in ent.e.entDamageCallbacks )
+ callbackFunc( ent, damageInfo )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after AddEntityCallback_OnDamaged() callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( damageSourceId in shGlobal.damageSourceIdCallbacks )
+ {
+ foreach ( callbackFunc in shGlobal.damageSourceIdCallbacks[ damageSourceId ] )
+ callbackFunc( ent, damageInfo )
+ }
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after damageSourceId callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ RunClassDamageFinalCallbacks( ent, damageInfo )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after class damage final callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ // make destructible vehicles take more damage from DF_EXPLOSION damage type
+ if ( "isDestructibleVehicle" in ent.s && DamageInfo_GetCustomDamageType( damageInfo ) & DF_EXPLOSION )
+ {
+ DamageInfo_ScaleDamage( damageInfo, 2.0 )
+ }
+
+ if ( ent.GetShieldHealth() > 0 )
+ {
+ DamageInfo_AddCustomDamageType( damageInfo, DF_SHIELD_DAMAGE )
+ ShieldModifyDamage( ent, damageInfo )
+ }
+
+ // Added via AddEntityCallback_OnPostDamaged
+ foreach ( callbackFunc in ent.e.entPostDamageCallbacks )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( "CodeCallback_DamageEntity() bottom:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+
+ UpdateLastDamageTime( ent )
+
+ AddFlinch( ent, damageInfo )
+
+ UpdateAttackerInfo( ent, DamageInfo_GetAttacker( damageInfo ), DamageInfo_GetDamage( damageInfo ) )
+}
+
+bool function TrySpectreVirus( entity victim, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsSpectre( victim ) )
+ return false
+
+ if ( !IsAlive( attacker ) )
+ return false
+
+ if ( !attacker.IsTitan() )
+ return false
+
+ if ( !attacker.IsPlayer() )
+ return false
+
+ if ( !PlayerHasPassive( attacker, ePassives.PAS_WIFI_SPECTRE ) )
+ return false
+
+ thread LeechPropagate( victim, attacker )
+ return true
+}
+
+
+void function HandleFootstepDamage( entity victim, var damageInfo )
+{
+ if ( TrySpectreVirus( victim, damageInfo ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+}
+
+void function CodeCallback_OnEntityKilled( entity ent, var damageInfo )
+{
+ // npcs and player do death package in their own killed callbacks which are always called (even if deathNotifications is false)
+ if ( !ent.IsNPC() && !ent.IsPlayer() )
+ HandleDeathPackage( ent, damageInfo )
+
+ string className = ent.GetClassName()
+ if ( className in shGlobal.deathCallbacks )
+ {
+ foreach ( callbackFunc in shGlobal.deathCallbacks[className] )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+ }
+
+
+ // Added via AddEntityCallback_OnKilled
+ foreach ( callbackFunc in ent.e.entKilledCallbacks )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+
+ SendEntityKilledEvent( ent, damageInfo )
+}
+
+
+void function SendEntityKilledEvent( entity ent, var damageInfo )
+{
+ array<entity> players = GetPlayerArray()
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ // trigger_hurt is no longer networked, so the "attacker" fails to display obituaries
+ if ( attacker )
+ {
+ string attackerClassname = attacker.GetClassName()
+
+ if ( attackerClassname == "trigger_hurt" || attackerClassname == "trigger_multiple" )
+ attacker = GetEntByIndex( 0 ) // worldspawn
+ }
+
+ int attackerEHandle = attacker ? attacker.GetEncodedEHandle() : -1
+
+ int victimEHandle = ent.GetEncodedEHandle()
+ int scriptDamageType = DamageInfo_GetCustomDamageType( damageInfo )
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ if ( scriptDamageType & DF_VORTEX_REFIRE )
+ damageSourceId = eDamageSourceId.mp_titanweapon_vortex_shield
+
+ if ( IsValidHeadShot( damageInfo, ent ) )
+ scriptDamageType = scriptDamageType | DF_HEADSHOT
+ else
+ scriptDamageType = scriptDamageType & (~DF_HEADSHOT)
+
+ foreach ( player in players )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_OnEntityKilled", attackerEHandle, victimEHandle, scriptDamageType, damageSourceId )
+ }
+}
+
+//=====================================================================================
+// Utility functions
+//=====================================================================================
+
+void function AddDamageCallback( string className, void functionref( entity, var ) callbackFunc )
+{
+ if ( !( className in file.classDamageCallbacks ) )
+ file.classDamageCallbacks[className] <- []
+
+ file.classDamageCallbacks[className].append( callbackFunc )
+}
+
+void function RemoveDamageCallback( string className, void functionref( entity, var ) callbackFunc )
+{
+ Assert( className in file.classDamageCallbacks, "Tried to remove damage callback that isn't added" )
+ Assert( file.classDamageCallbacks[className].contains( callbackFunc ), "Tried to remove damage callback that isn't added" )
+ file.classDamageCallbacks[className].fastremovebyvalue( callbackFunc )
+}
+
+void function RunClassDamageCallbacks( entity ent, var damageInfo )
+{
+ string className = ent.GetClassName()
+ if ( !( className in file.classDamageCallbacks ) )
+ return
+
+ foreach ( callbackFunc in file.classDamageCallbacks[className] )
+ {
+ callbackFunc( ent, damageInfo )
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+ }
+}
+
+void function AddDamageFinalCallback( string className, void functionref( entity, var ) callbackFunc )
+{
+ if ( !( className in file.classDamageFinalCallbacks ) )
+ file.classDamageFinalCallbacks[className] <- []
+
+ file.classDamageFinalCallbacks[className].append( callbackFunc )
+}
+void function RunClassDamageFinalCallbacks( entity ent, var damageInfo )
+{
+ string className = ent.GetClassName()
+ if ( !( className in file.classDamageFinalCallbacks ) )
+ return
+
+ foreach ( callbackFunc in file.classDamageFinalCallbacks[className] )
+ {
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+ callbackFunc( ent, damageInfo )
+ }
+}
+
+
+void function AddPostDamageCallback( string className, void functionref( entity, var ) callbackFunc )
+{
+ if ( !( className in file.classPostDamageCallbacks ) )
+ file.classPostDamageCallbacks[className] <- []
+
+ file.classPostDamageCallbacks[className].append( callbackFunc )
+}
+
+void function RunClassPostDamageCallbacks( entity ent, var damageInfo )
+{
+ string className = ent.GetClassName()
+ if ( !( className in file.classPostDamageCallbacks ) )
+ return
+
+ foreach ( callbackFunc in file.classPostDamageCallbacks[className] )
+ {
+ #if DEV
+ float damage = DamageInfo_GetDamage( damageInfo )
+ #endif
+ callbackFunc( ent, damageInfo )
+
+ #if DEV
+ Assert( damage == DamageInfo_GetDamage( damageInfo ), "Damage changed in a post damage callback" )
+ #endif
+ }
+}
+
+void function AddDamageByCallback( string className, void functionref( entity, var ) callbackFunc )
+{
+ if ( !( className in svGlobal.damageByCallbacks ) )
+ svGlobal.damageByCallbacks[className] <- []
+
+ svGlobal.damageByCallbacks[className].append( callbackFunc )
+}
+
+void function AddDamageCallbackSourceID( int id, void functionref(entity, var) callbackFunc )
+{
+ if ( !( id in shGlobal.damageSourceIdCallbacks ) )
+ shGlobal.damageSourceIdCallbacks[id] <- []
+
+ shGlobal.damageSourceIdCallbacks[id].append( callbackFunc )
+}
+
+void function AddDeathCallback( string className, void functionref( entity, var ) callbackFunc )
+{
+ if ( !( className in shGlobal.deathCallbacks ) )
+ shGlobal.deathCallbacks[className] <- []
+
+ shGlobal.deathCallbacks[className].append( callbackFunc )
+}
+
+void function RemoveDeathCallback( string className, void functionref( entity, var ) callbackFunc )
+{
+ Assert( className in shGlobal.deathCallbacks, "Tried to remove death callback that isn't added" )
+ Assert( shGlobal.deathCallbacks[className].contains( callbackFunc ), "Tried to remove death callback that isn't added" )
+ shGlobal.deathCallbacks[className].fastremovebyvalue( callbackFunc )
+}
+
+void function AddSoulDeathCallback( void functionref( entity, var ) callbackFunc )
+{
+ #if DEV
+ foreach ( func in svGlobal.soulDeathFuncs )
+ {
+ Assert( func != callbackFunc , "Already added " + string( callbackFunc ) + " with AddSoulDeathCallback" )
+ }
+ #endif
+
+ svGlobal.soulDeathFuncs.append( callbackFunc )
+}
+
+void function AddCallback_OnTouchHealthKit( string className, bool functionref( entity player, entity healthpack ) callbackFunc )
+{
+ if ( ! (className in svGlobal.onTouchHealthKitCallbacks ) )
+ {
+ svGlobal.onTouchHealthKitCallbacks[ className ] <- [ callbackFunc ]
+ return
+ }
+ else
+ {
+ Assert( !svGlobal.onTouchHealthKitCallbacks[className].contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnTouchHealthKit to class " + className )
+ svGlobal.onTouchHealthKitCallbacks[className].append( callbackFunc )
+ }
+
+}
+
+void function AddCallback_OnPlayerRespawned( void functionref( entity ) callbackFunc )
+{
+ Assert( !svGlobal.onPlayerRespawnedCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnPlayerRespawned" )
+ svGlobal.onPlayerRespawnedCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnPlayerKilled( void functionref( entity victim, entity attacker, var damageInfo ) callbackFunc )
+{
+ Assert( !svGlobal.onPlayerKilledCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnPlayerKilled" )
+ svGlobal.onPlayerKilledCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnNPCKilled( void functionref( entity victim, entity attacker, var damageInfo ) callbackFunc )
+{
+ Assert( !svGlobal.onNPCKilledCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnPlayerKilled" )
+ svGlobal.onNPCKilledCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnTitanDoomed( void functionref( entity victim, var damageInfo ) callbackFunc )
+{
+ Assert( !svGlobal.onTitanDoomedCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnTitanDoomed" )
+ svGlobal.onTitanDoomedCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnTitanHealthSegmentLost( void functionref( entity victim, entity attacker ) callbackFunc )
+{
+ Assert( !svGlobal.onTitanHealthSegmentLostCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnTitanHealthSegmentLost" )
+ svGlobal.onTitanHealthSegmentLostCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnClientConnecting( void functionref( entity player ) callbackFunc )
+{
+ Assert( !svGlobal.onClientConnectingCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnClientConnecting" )
+ svGlobal.onClientConnectingCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnClientConnected( void functionref( entity player ) callbackFunc )
+{
+ Assert( !svGlobal.onClientConnectedCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnClientConnected" )
+ svGlobal.onClientConnectedCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnClientDisconnected( void functionref( entity player ) callbackFunc )
+{
+ Assert( !svGlobal.onClientDisconnectedCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnClientDisconnected" )
+ svGlobal.onClientDisconnectedCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnPilotBecomesTitan( void functionref( entity pilot, entity npc_titan ) callbackFunc )
+{
+ Assert( !svGlobal.onPilotBecomesTitanCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnPilotBecomesTitan" )
+ svGlobal.onPilotBecomesTitanCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnTitanBecomesPilot( void functionref( entity pilot, entity npc_titan ) callbackFunc )
+{
+ Assert( !svGlobal.onTitanBecomesPilotCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnTitanBecomesPilot" )
+ svGlobal.onTitanBecomesPilotCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnPlayerAssist( void functionref( entity attacker, entity victim ) callbackFunc )
+{
+ Assert( !svGlobal.onPlayerAssistCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnPlayerAssist" )
+ svGlobal.onPlayerAssistCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_EntityChangedTeam( string className, void functionref( entity ent ) callbackFunc )
+{
+ if ( !( className in svGlobal.onEntityChangedTeamCallbacks ) )
+ {
+ svGlobal.onEntityChangedTeamCallbacks[ className ] <- [ callbackFunc ]
+ return
+ }
+ else
+ {
+ Assert( !svGlobal.onEntityChangedTeamCallbacks[ className ].contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_EntityChangedTeam" )
+ svGlobal.onEntityChangedTeamCallbacks[ className ].append( callbackFunc )
+ }
+}
+
+void function AddCallback_OnTitanGetsNewTitanLoadout( void functionref( entity titan, TitanLoadoutDef newTitanLoadout ) callbackFunc )
+{
+ Assert( !svGlobal.onTitanGetsNewLoadoutCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnTitanGetsNewTitanLoadout" )
+ svGlobal.onTitanGetsNewLoadoutCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnPlayerGetsNewPilotLoadout( void functionref( entity player, PilotLoadoutDef newTitanLoadout ) callbackFunc )
+{
+ Assert( !svGlobal.onPlayerGetsNewPilotLoadoutCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnPlayerGetsNewPilotLoadout" )
+ svGlobal.onPlayerGetsNewPilotLoadoutCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnUpdateDerivedTitanLoadout( void functionref( TitanLoadoutDef newTitanLoadout ) callbackFunc )
+{
+ Assert( !svGlobal.onUpdateDerivedTitanLoadoutCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnUpdateDerivedTitanLoadout" )
+ svGlobal.onUpdateDerivedTitanLoadoutCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnUpdateDerivedPlayerTitanLoadout( void functionref( entity player, TitanLoadoutDef newTitanLoadout ) callbackFunc )
+{
+ Assert( !svGlobal.onUpdateDerivedPlayerTitanLoadoutCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnUpdateDerivedTitanLoadout" )
+ svGlobal.onUpdateDerivedPlayerTitanLoadoutCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_OnUpdateDerivedPilotLoadout( void functionref( PilotLoadoutDef newPilotLoadout ) callbackFunc )
+{
+ Assert( !svGlobal.onUpdateDerivedPilotLoadoutCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnUpdateDerivedPilotLoadout" )
+ svGlobal.onUpdateDerivedPilotLoadoutCallbacks.append( callbackFunc )
+}
+
+void function AddClientCommandCallback( string commandString, bool functionref( entity player, array<string> args ) callbackFunc )
+{
+ Assert( !( commandString in svGlobal.clientCommandCallbacks ), "Already added " + commandString + " with AddClientCommandCallback" )
+ svGlobal.clientCommandCallbacks[ commandString ] <- callbackFunc
+}
+
+void function AddPlayerDropScriptedItemsCallback( void functionref(entity player) callbackFunc )
+{
+ Assert( !( svGlobal.onPlayerDropsScriptedItemsCallbacks.contains( callbackFunc ) ), "Already added " + string( callbackFunc ) + " with AddPlayerDropScriptedItemsCallback" )
+ svGlobal.onPlayerDropsScriptedItemsCallbacks.append( callbackFunc )
+}
+
+//=====================================================================================
+// Register functions are called when an entity spawns.
+//=====================================================================================
+
+void function RegisterForDamageDeathCallbacks( entity ent )
+{
+ string className = ent.GetClassName()
+
+ if ( (className in file.classDamageCallbacks) || (className in file.classDamageFinalCallbacks) )
+ ent.SetDamageNotifications( true )
+
+ if ( className in shGlobal.deathCallbacks )
+ ent.SetDeathNotifications( true )
+}
+
+void function AddTitanCallback_OnHealthSegmentLost( entity ent, void functionref( entity titan, entity victim ) callbackFunc )
+{
+ Assert( !ent.e.entSegmentLostCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " to entity" )
+
+ ent.e.entSegmentLostCallbacks.append( callbackFunc )
+}
+
+void function RemoveTitanCallback_OnHealthSegmentLost( entity ent, void functionref( entity titan, entity victim ) callbackFunc )
+{
+ int index = ent.e.entSegmentLostCallbacks.find( callbackFunc )
+
+ Assert( index != -1, "Requested DamageCallback " + string( callbackFunc ) + " to be removed not found! " )
+ ent.e.entSegmentLostCallbacks.fastremove( index )
+}
+
+void function AddEntityCallback_OnDamaged( entity ent, void functionref( entity ent, var damageInfo ) callbackFunc )
+{
+ Assert( !ent.e.entDamageCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " to entity" )
+
+ ent.SetDamageNotifications( true )
+ ent.e.entDamageCallbacks.append( callbackFunc )
+}
+
+void function RemoveEntityCallback_OnDamaged( entity ent, void functionref( entity ent, var damageInfo ) callbackFunc )
+{
+ int index = ent.e.entDamageCallbacks.find( callbackFunc )
+
+ Assert( index != -1, "Requested DamageCallback " + string( callbackFunc ) + " to be removed not found! " )
+ ent.e.entDamageCallbacks.fastremove( index )
+
+ if ( ent.e.entDamageCallbacks.len() == 0 && ent.e.entPostDamageCallbacks.len() == 0 )
+ ent.SetDamageNotifications( false )
+}
+
+void function AddEntityCallback_OnPostDamaged( entity ent, void functionref( entity ent, var damageInfo ) callbackFunc )
+{
+ Assert( !ent.e.entPostDamageCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " to entity" )
+
+ ent.SetDamageNotifications( true )
+ ent.e.entPostDamageCallbacks.append( callbackFunc )
+}
+
+void function RemoveEntityCallback_OnPostDamaged( entity ent, void functionref( entity ent, var damageInfo ) callbackFunc )
+{
+ int index = ent.e.entPostDamageCallbacks.find( callbackFunc )
+
+ Assert( index != -1, "Requested PostDamageCallback " + string( callbackFunc ) + " to be removed not found! " )
+ ent.e.entPostDamageCallbacks.fastremove( index )
+
+ if ( ent.e.entPostDamageCallbacks.len() == 0 && ent.e.entDamageCallbacks.len() == 0 )
+ ent.SetDamageNotifications( false )
+}
+
+void function AddEntityCallback_OnKilled( entity ent, void functionref( entity, var ) callbackFunc )
+{
+ #if DEV
+ foreach ( func in ent.e.entKilledCallbacks )
+ {
+ Assert( func != callbackFunc , "Already added " + string( callbackFunc ) + " to entity" )
+ }
+ #endif
+
+ ent.SetDeathNotifications( true )
+ ent.e.entKilledCallbacks.append( callbackFunc )
+}
+
+void function RemoveEntityCallback_OnKilled( entity ent, void functionref( entity, var ) callbackFunc )
+{
+ int index = ent.e.entKilledCallbacks.find( callbackFunc )
+
+ Assert( index != -1, "Requested KilledCallback " + string( callbackFunc ) + " to be removed not found! " )
+ ent.e.entKilledCallbacks.fastremove( index )
+
+ if ( ent.e.entKilledCallbacks.len() == 0 )
+ ent.SetDeathNotifications( false )
+}
+
+void function AddEntityCallback_OnPostShieldDamage( entity ent, void functionref( entity, var, float ) callbackFunc )
+{
+ #if DEV
+ foreach ( func in ent.e.entPostShieldDamageCallbacks )
+ {
+ Assert( func != callbackFunc , "Already added " + string( callbackFunc ) + " to entity" )
+ }
+ #endif
+
+ ent.e.entPostShieldDamageCallbacks.append( callbackFunc )
+}
+
+void function RemoveEntityCallback_OnPostShieldDamage( entity ent, void functionref( entity, var, float ) callbackFunc )
+{
+ int index = ent.e.entPostShieldDamageCallbacks.find( callbackFunc )
+
+ Assert( index != -1, "Requested OnPostShieldDamage " + string( callbackFunc ) + " to be removed not found! " )
+ ent.e.entPostShieldDamageCallbacks.fastremove( index )
+}
+
+void function CodeCallback_OnInventoryChanged( entity player )
+{
+ player.Signal( "InventoryChanged" )
+
+ if ( !IsAlive( player ) )
+ return
+
+#if HAS_TITAN_WEAPON_SWAPPING
+ if ( player.IsTitan() )
+ {
+ array<entity> weapons = GetPrimaryWeapons( player )
+ bool weaponSwap = true
+ foreach ( weapon in weapons )
+ {
+ if ( weapon == player.p.lastPrimaryWeaponEnt )
+ weaponSwap = false
+ player.p.lastPrimaryWeaponEnt = weapon
+ }
+
+ if ( weaponSwap )
+ {
+ table<int,float> cooldowns = GetWeaponCooldownsForTitanLoadoutSwitch( player )
+
+ ResetTitanLoadoutFromPrimary( player )
+
+ bool foundNewWeapon
+
+ foreach ( weapon in weapons )
+ {
+ int loadoutIndex = GetSPTitanLoadoutIndexForWeapon( weapon.GetWeaponClassName() )
+ if ( loadoutIndex >= 0 )
+ {
+ if ( GetSPTitanLoadoutHasEverBeenSelected( loadoutIndex ) )
+ continue
+
+ foundNewWeapon = true
+ SetSPTitanLoadoutHasEverBeenSelected( loadoutIndex )
+ }
+ }
+
+ if ( !JustLoadedFromCheckpoint() && !foundNewWeapon )
+ SetWeaponCooldownsForTitanLoadoutSwitch( player, cooldowns )
+ }
+
+ Assert( player.GetOffhandWeapon( OFFHAND_SPECIAL ) == null || !player.GetOffhandWeapon( OFFHAND_SPECIAL ).HasMod( "npc_normal_difficulty" ), "Player should never have mod npc_normal_difficulty" )
+ }
+#endif // #if HAS_TITAN_WEAPON_SWAPPING
+
+ foreach ( callbackFunc in file.playerInventoryChangedCallbacks )
+ {
+ callbackFunc( player )
+ }
+}
+
+void function CodeCallback_OnEntityChangedTeam( entity ent )
+{
+ string className = ent.GetClassName()
+ if ( !( className in svGlobal.onEntityChangedTeamCallbacks ) )
+ return
+
+ // Added via AddCallback_EntityChangedTeam
+ foreach ( callbackFunc in svGlobal.onEntityChangedTeamCallbacks[ className ] )
+ {
+ callbackFunc( ent )
+ }
+}
+
+//=============================
+// Player movement callbacks
+//=============================
+
+void function AddPlayerMovementEventCallback( entity player, int playerMovementEvent, void functionref( entity player ) callbackFunc )
+{
+ if ( !player.GetSendMovementCallbacks() )
+ player.SetSendMovementCallbacks( true )
+
+ table<int, array<void functionref( entity )> > callbackTable = player.p.playerMovementEventCallbacks
+
+ if ( ! ( playerMovementEvent in callbackTable ) )
+ callbackTable[ playerMovementEvent ] <- []
+
+ Assert( !callbackTable[ playerMovementEvent ].contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddPlayerMovementEventCallback for player " + player.GetPlayerName() )
+ callbackTable[ playerMovementEvent ].append( callbackFunc )
+}
+
+void function RemovePlayerMovementEventCallback( entity player, int playerMovementEvent, void functionref( entity player ) callbackFunc )
+{
+ table<int, array<void functionref( entity )> > callbackTable = player.p.playerMovementEventCallbacks
+
+ Assert( playerMovementEvent in callbackTable )
+
+ callbackTable[ playerMovementEvent ].fastremovebyvalue( callbackFunc )
+
+ if ( callbackTable[ playerMovementEvent ].len() == 0 )
+ {
+ //printt( "No more callbacks for playerMovementEvent: " + playerMovementEvent + ", removing array of functions" )
+ delete callbackTable[ playerMovementEvent ]
+ }
+
+ if ( callbackTable.len() == 0 )
+ {
+ //printt( "No more playerMovementEventCallbacks for player : " + player + ", make player not get movementcallbacks anymore." )
+ player.SetSendMovementCallbacks( false )
+ }
+}
+
+void function CodeCallback_OnPlayerJump( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.JUMP in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Jump")
+
+ //Run actual functions
+ foreach( callbackFunc in player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.JUMP ] )
+ callbackFunc( player )
+}
+
+void function CodeCallback_OnPlayerDoubleJump( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.DOUBLE_JUMP in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Double Jump")
+
+ //Run actual functions
+ foreach( callbackFunc in player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.DOUBLE_JUMP ] )
+ callbackFunc( player )
+}
+
+void function CodeCallback_OnPlayerDodge( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.DODGE in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Dodge" )
+
+ //Run actual functions
+ foreach( callbackFunc in player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.DODGE ] )
+ callbackFunc( player )
+}
+
+void function CodeCallback_OnPlayerLeaveGround( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.LEAVE_GROUND in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Left Ground")
+
+ //Run actual functions
+ foreach( callbackFunc in player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.LEAVE_GROUND ] )
+ callbackFunc( player )
+}
+
+void function CodeCallback_OnPlayerTouchGround( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.TOUCH_GROUND in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Touch Ground")
+
+ array<void functionref(entity)> callbacks = clone player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.TOUCH_GROUND ]
+
+ //Run actual functions
+ foreach( callbackFunc in callbacks )
+ callbackFunc( player )
+}
+
+void function CodeCallback_OnPlayerMantle( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.MANTLE in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Mantle")
+
+ //Run actual functions
+ foreach( callbackFunc in player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.MANTLE ] )
+ callbackFunc( player )
+}
+
+void function CodeCallback_OnPlayerBeginWallrun( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.BEGIN_WALLRUN in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Wallrun Begin")
+
+ //Run actual functions
+ foreach( callbackFunc in player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.BEGIN_WALLRUN ] )
+ callbackFunc( player )
+}
+
+void function CodeCallback_OnPlayerEndWallrun( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.END_WALLRUN in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Wallrun End")
+
+ //Run actual functions
+ foreach( callbackFunc in player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.END_WALLRUN ] )
+ callbackFunc( player )
+}
+
+void function CodeCallback_OnPlayerBeginWallhang( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.BEGIN_WALLHANG in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Wallhang Begin")
+
+ //Run actual functions
+ foreach( callbackFunc in player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.BEGIN_WALLHANG ] )
+ callbackFunc( player )
+}
+
+void function CodeCallback_OnPlayerEndWallhang( entity player )
+{
+ if ( ! ( ePlayerMovementEvents.END_WALLHANG in player.p.playerMovementEventCallbacks ) )
+ return
+
+ //printt( "Player Wallhang End")
+
+ //Run actual functions
+ foreach( callbackFunc in player.p.playerMovementEventCallbacks[ ePlayerMovementEvents.END_WALLHANG ] )
+ callbackFunc( player )
+}
+
+
+void function AddCallback_OnPlayerInventoryChanged( void functionref( entity ) callbackFunc )
+{
+ file.playerInventoryChangedCallbacks.append( callbackFunc )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_player_input.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_player_input.gnut
new file mode 100644
index 00000000..12062056
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_player_input.gnut
@@ -0,0 +1,552 @@
+//TODO: Add "retrigger if held" functionality to stick
+
+
+// Player input callbacks
+global function AddButtonPressedPlayerInputCallback
+global function RemoveButtonPressedPlayerInputCallback
+global function AddButtonReleasedPlayerInputCallback
+global function RemoveButtonReleasedPlayerInputCallback
+global function AddPlayerHeldButtonEventCallback
+global function RemovePlayerHeldButtonEventCallback
+
+global function AddPlayerPressedForwardCallback
+global function RemovePlayerPressedForwardCallback
+global function AddPlayerPressedBackCallback
+global function RemovePlayerPressedBackCallback
+global function AddPlayerPressedLeftCallback
+global function RemovePlayerPressedLeftCallback
+global function AddPlayerPressedRightCallback
+global function RemovePlayerPressedRightCallback
+
+global function DEBUG_AddSprintJumpHeldMeleePressed //Only for reference on how to do more complicated input events
+
+global function CodeCallback_OnPlayerInputCommandChanged
+global function CodeCallback_OnPlayerInputAxisChanged
+
+void function CodeCallback_OnPlayerInputCommandChanged( entity player, int cmdsHeld, int cmdsPressed, int cmdsReleased )
+{
+ foreach( callbackStruct in player.p.playerInputEventCallbacks )
+ {
+ if ( PlayerInputsMatchCallbackInputs( cmdsHeld, cmdsPressed, cmdsReleased, callbackStruct ) )
+ callbackStruct.callbackFunc( player )
+ }
+
+ foreach( callbackStruct in player.p.playerHeldButtonEventCallbacks )
+ {
+ if ( cmdsPressed & callbackStruct.buttonHeld )
+ thread RunHeldCallbackAfterTimePasses( player, callbackStruct )
+ }
+
+ foreach( callbackStruct in player.p.playerHeldButtonEventCallbacks )
+ {
+ if ( cmdsReleased & callbackStruct.buttonHeld )
+ {
+ string endSignalName = GetEndSignalNameForHeldButtonCallback( callbackStruct )
+ player.Signal( endSignalName ) //Send signal to kill corresponding RunHeldCallbackAfterTimePasses
+ }
+ }
+}
+
+void function CodeCallback_OnPlayerInputAxisChanged( entity player, float horizAxis, float vertAxis )
+{
+ //printt( "Axis Changed: X: " + horizAxis + " Y: " + vertAxis )
+
+ foreach( callbackStruct in player.p.playerInputAxisEventCallbacks )
+ {
+ if ( ShouldRunPlayerInputAxisCallbackFunc( horizAxis, vertAxis, callbackStruct ) )
+ RunPlayerInputAxisCallbackFunc( player, callbackStruct )
+ }
+}
+
+void function AddPlayerInputEventCallback_Internal( entity player, PlayerInputEventCallbackStruct inputCallbackStruct ) //Not really meant to be used directly unless you know what you're doing! Use utility functions like AddButtonPressedPlayerInputCallback instead
+{
+ if ( !player.GetSendInputCallbacks() )
+ player.SetSendInputCallbacks( true )
+
+ Assert( !InputEventCallbackAlreadyExists( player, inputCallbackStruct ), " Adding the same inputEventCallback " + string ( inputCallbackStruct.callbackFunc ) + " with the same inputs!" )
+
+ player.p.playerInputEventCallbacks.append( inputCallbackStruct )
+}
+
+void function RemovePlayerInputEventCallback_Internal( entity player, PlayerInputEventCallbackStruct inputCallbackStruct ) //Not really meant to be used directly unless you know what you're doing! Use utility functions like RemoveButtonPressedPlayerInputCallback instead
+{
+ for( int i = player.p.playerInputEventCallbacks.len() - 1; i >= 0; --i ) //Removing from the end of an array, so it's fine to remove as we go along
+ {
+ if ( InputCallbackStructsAreTheSame( player.p.playerInputEventCallbacks[i], inputCallbackStruct ) )
+ {
+ player.p.playerInputEventCallbacks.remove( i )
+ break
+ }
+
+ }
+
+ TurnOffInputCallbacksIfNecessary( player )
+}
+
+bool function InputEventCallbackAlreadyExists( entity player, PlayerInputEventCallbackStruct inputCallbackStruct )
+{
+ foreach( existingCallbackStruct in player.p.playerInputEventCallbacks )
+ {
+ if ( InputCallbackStructsAreTheSame( existingCallbackStruct, inputCallbackStruct ) )
+ return true
+ }
+
+ return false
+}
+
+void function AddPlayerHeldButtonEventCallback( entity player, int buttonEnum, void functionref( entity player ) callbackFunc, float buttonHeldTime = 1.0 )
+{
+ if ( !player.GetSendInputCallbacks() )
+ player.SetSendInputCallbacks( true )
+
+ PlayerHeldButtonEventCallbackStruct callbackStruct
+ callbackStruct.buttonHeld = buttonEnum
+ callbackStruct.callbackFunc = callbackFunc
+ callbackStruct.timeHeld = buttonHeldTime
+
+ Assert( !HeldEventCallbackAlreadyExists( player, callbackStruct ), " Adding the same heldEventCallback " + string ( callbackStruct.callbackFunc ) + " with the same parameters!" )
+
+ string endSignalName = GetEndSignalNameForHeldButtonCallback( callbackStruct )
+ RegisterSignal( endSignalName )//Signal meant to kill the waiting thread if button is released. Note that registering the same signal multiple times seems to be ok.
+
+ player.p.playerHeldButtonEventCallbacks.append( callbackStruct )
+}
+
+
+void function RemovePlayerHeldButtonEventCallback( entity player, int buttonEnum, void functionref( entity player ) callbackFunc, float buttonHeldTime = 1.0 )
+{
+ PlayerHeldButtonEventCallbackStruct callbackStruct
+ callbackStruct.buttonHeld = buttonEnum
+ callbackStruct.callbackFunc = callbackFunc
+ callbackStruct.timeHeld = buttonHeldTime
+
+ for( int i = player.p.playerHeldButtonEventCallbacks.len() - 1; i >= 0; --i ) //Removing from the end of an array, so it's fine to remove as we go along
+ {
+ if ( HeldButtonCallbackStructsAreTheSame( player.p.playerHeldButtonEventCallbacks[i], callbackStruct ) )
+ player.p.playerHeldButtonEventCallbacks.remove( i )
+ }
+
+ TurnOffInputCallbacksIfNecessary( player )
+}
+
+bool function HeldEventCallbackAlreadyExists( entity player, PlayerHeldButtonEventCallbackStruct callbackStruct )
+{
+ foreach( existingCallbackStruct in player.p.playerHeldButtonEventCallbacks )
+ {
+ if ( HeldButtonCallbackStructsAreTheSame( existingCallbackStruct, callbackStruct ) )
+ return true
+ }
+
+ return false
+}
+
+void function DEBUG_PlayerHeldSprintJumpAndPressedMelee( entity player ) //Debug function, just an example on how to hook up more complicated InputEvents
+{
+ PrintFunc()
+}
+
+void function DEBUG_AddSprintJumpHeldMeleePressed() //Debug function, just an example on how to hook up more complicated InputEvents
+{
+ PlayerInputEventCallbackStruct callbackStruct
+ callbackStruct.cmdsHeldBitMask = IN_SPEED | IN_JUMP
+ callbackStruct.cmdsPressedBitMask = IN_MELEE
+ callbackStruct.callbackFunc = DEBUG_PlayerHeldSprintJumpAndPressedMelee
+
+ AddPlayerInputEventCallback_Internal( GetPlayerArray()[0], callbackStruct )
+}
+
+//List of valid inputs are in sh_constants for reference
+void function AddButtonPressedPlayerInputCallback( entity player, int buttonEnum, void functionref( entity player ) callbackFunc )
+{
+ PlayerInputEventCallbackStruct callbackStruct
+ callbackStruct.cmdsPressedBitMask = buttonEnum
+ callbackStruct.callbackFunc = callbackFunc
+
+ AddPlayerInputEventCallback_Internal( player, callbackStruct )
+}
+
+void function RemoveButtonPressedPlayerInputCallback( entity player, int buttonEnum, void functionref( entity player ) callbackFunc )
+{
+ PlayerInputEventCallbackStruct callbackStruct
+ callbackStruct.cmdsPressedBitMask = buttonEnum
+ callbackStruct.callbackFunc = callbackFunc
+
+ RemovePlayerInputEventCallback_Internal( player, callbackStruct )
+}
+
+void function AddButtonReleasedPlayerInputCallback( entity player, int buttonEnum, void functionref( entity player ) callbackFunc )
+{
+ PlayerInputEventCallbackStruct callbackStruct
+ callbackStruct.cmdsReleasedBitMask = buttonEnum
+ callbackStruct.callbackFunc = callbackFunc
+
+ AddPlayerInputEventCallback_Internal( player, callbackStruct )
+}
+
+void function RemoveButtonReleasedPlayerInputCallback( entity player, int buttonEnum, void functionref( entity player ) callbackFunc )
+{
+ PlayerInputEventCallbackStruct callbackStruct
+ callbackStruct.cmdsReleasedBitMask = buttonEnum
+ callbackStruct.callbackFunc = callbackFunc
+
+ RemovePlayerInputEventCallback_Internal( player, callbackStruct )
+}
+
+
+void function RunHeldCallbackAfterTimePasses( entity player, PlayerHeldButtonEventCallbackStruct callbackStruct )
+{
+ string endSignalName = GetEndSignalNameForHeldButtonCallback( callbackStruct )
+ player.EndSignal( endSignalName )
+ player.EndSignal( "OnDeath" )
+
+ /*OnThreadEnd(
+ function() : ( )
+ {
+ printt( "function ended at: " + Time() )
+
+ }
+ )
+
+ printt( "Pre wait time: " + Time() )*/
+ wait callbackStruct.timeHeld
+
+ //printt( "Post wait time: " + Time() )
+
+ if ( !IsValid( player ) )
+ return
+
+ callbackStruct.callbackFunc( player )
+}
+
+string function GetEndSignalNameForHeldButtonCallback( PlayerHeldButtonEventCallbackStruct callbackStruct )
+{
+ return ( "Button" + callbackStruct.buttonHeld + "Released_EndSignal" )
+}
+
+bool function InputCallbackStructsAreTheSame( PlayerInputEventCallbackStruct callbackStruct1, PlayerInputEventCallbackStruct callbackStruct2 ) //Really just a comparison function because == does a compare by reference, not a compare by value
+{
+ if ( callbackStruct1.cmdsPressedBitMask != callbackStruct2.cmdsPressedBitMask )
+ return false
+
+ if ( callbackStruct1.cmdsHeldBitMask != callbackStruct2.cmdsHeldBitMask )
+ return false
+
+ if ( callbackStruct1.cmdsReleasedBitMask != callbackStruct2.cmdsReleasedBitMask )
+ return false
+
+ if ( callbackStruct1.callbackFunc != callbackStruct2.callbackFunc )
+ return false
+
+ return true
+}
+
+bool function PlayerInputsMatchCallbackInputs( int cmdsHeld, int cmdsPressed, int cmdsReleased, PlayerInputEventCallbackStruct callbackStruct )
+{
+ if ( !HasBitMask( cmdsHeld, callbackStruct.cmdsHeldBitMask ) )
+ return false
+
+ if ( !HasBitMask( cmdsPressed, callbackStruct.cmdsPressedBitMask ) )
+ return false
+
+ if ( !HasBitMask( cmdsReleased, callbackStruct.cmdsReleasedBitMask ) )
+ return false
+
+ return true
+}
+
+bool function HeldButtonCallbackStructsAreTheSame( PlayerHeldButtonEventCallbackStruct struct1, PlayerHeldButtonEventCallbackStruct struct2 ) //Really just a comparison function because == does a compare by reference, not a compare by value
+{
+ if ( struct1.buttonHeld != struct2.buttonHeld )
+ return false
+
+ if ( struct1.callbackFunc != struct2.callbackFunc )
+ return false
+
+ if ( struct1.timeHeld != struct2.timeHeld )
+ return false
+
+ return true
+
+}
+
+void function TurnOffInputCallbacksIfNecessary( entity player )
+{
+ if ( player.p.playerInputEventCallbacks.len() > 0 )
+ return
+
+ if ( player.p.playerHeldButtonEventCallbacks.len() > 0 )
+ return
+
+ if ( player.p.playerInputAxisEventCallbacks.len() > 0 )
+ return
+
+ //printt( "No more input callbacks, SetInputCallbacks to false" )
+ player.SetSendInputCallbacks( false )
+}
+
+PlayerInputAxisEventCallbackStruct function MakePressedForwardCallbackStruct()
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct
+ callbackStruct.horizAxisMinThreshold = -1.0
+ callbackStruct.horizAxisMaxThreshold = 1.0
+ callbackStruct.vertAxisMinThreshold = 0.4
+ callbackStruct.vertAxisMaxThreshold = 1.0
+
+ return callbackStruct
+}
+
+void function AddPlayerPressedForwardCallback( entity player, bool functionref( entity player ) callbackFunc, float debounceTime = 2.0 )
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct = MakePressedForwardCallbackStruct()
+ callbackStruct.debounceTime = debounceTime
+ callbackStruct.callbackFunc = callbackFunc
+
+ AddPlayerInputAxisEventCallback_Internal( player, callbackStruct )
+}
+
+void function RemovePlayerPressedForwardCallback( entity player, bool functionref( entity player ) callbackFunc, float debounceTime = 2.0 )
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct = MakePressedForwardCallbackStruct()
+ callbackStruct.debounceTime = debounceTime
+ callbackStruct.callbackFunc = callbackFunc
+
+ RemovePlayerInputAxisEventCallback_Internal( player, callbackStruct )
+}
+
+PlayerInputAxisEventCallbackStruct function MakePressedBackCallbackStruct()
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct
+ callbackStruct.horizAxisMinThreshold = -1.0
+ callbackStruct.horizAxisMaxThreshold = 1.0
+ callbackStruct.vertAxisMinThreshold = -1.0
+ callbackStruct.vertAxisMaxThreshold = -0.4
+
+ return callbackStruct
+}
+
+void function AddPlayerPressedBackCallback( entity player, bool functionref( entity player ) callbackFunc, float debounceTime = 2.0 )
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct = MakePressedBackCallbackStruct()
+ callbackStruct.debounceTime = debounceTime
+ callbackStruct.callbackFunc = callbackFunc
+
+ AddPlayerInputAxisEventCallback_Internal( player, callbackStruct )
+
+}
+
+void function RemovePlayerPressedBackCallback( entity player, bool functionref( entity player ) callbackFunc, float debounceTime = 2.0 )
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct = MakePressedBackCallbackStruct()
+ callbackStruct.debounceTime = debounceTime
+ callbackStruct.callbackFunc = callbackFunc
+
+ RemovePlayerInputAxisEventCallback_Internal( player, callbackStruct )
+
+}
+
+PlayerInputAxisEventCallbackStruct function MakePressedLeftCallbackStruct()
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct
+ callbackStruct.horizAxisMinThreshold = -1.0
+ callbackStruct.horizAxisMaxThreshold = -0.4
+ callbackStruct.vertAxisMinThreshold = -1.0
+ callbackStruct.vertAxisMaxThreshold = 1.0
+
+ return callbackStruct
+}
+
+void function AddPlayerPressedLeftCallback( entity player, bool functionref( entity player ) callbackFunc, float debounceTime = 2.0 )
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct = MakePressedLeftCallbackStruct()
+ callbackStruct.debounceTime = debounceTime
+ callbackStruct.callbackFunc = callbackFunc
+
+ AddPlayerInputAxisEventCallback_Internal( player, callbackStruct )
+}
+
+void function RemovePlayerPressedLeftCallback( entity player, bool functionref( entity player ) callbackFunc, float debounceTime = 2.0 )
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct = MakePressedLeftCallbackStruct()
+ callbackStruct.debounceTime = debounceTime
+ callbackStruct.callbackFunc = callbackFunc
+
+ RemovePlayerInputAxisEventCallback_Internal( player, callbackStruct )
+}
+
+PlayerInputAxisEventCallbackStruct function MakePressedRightCallbackStruct()
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct
+ callbackStruct.horizAxisMinThreshold = 0.4
+ callbackStruct.horizAxisMaxThreshold = 1.0
+ callbackStruct.vertAxisMinThreshold = -1.0
+ callbackStruct.vertAxisMaxThreshold = 1.0
+
+ return callbackStruct
+}
+
+void function AddPlayerPressedRightCallback( entity player, bool functionref( entity player ) callbackFunc, float debounceTime = 2.0 )
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct = MakePressedRightCallbackStruct()
+ callbackStruct.debounceTime = debounceTime
+ callbackStruct.callbackFunc = callbackFunc
+
+ AddPlayerInputAxisEventCallback_Internal( player, callbackStruct )
+}
+
+void function RemovePlayerPressedRightCallback( entity player, bool functionref( entity player ) callbackFunc, float debounceTime = 2.0 )
+{
+ PlayerInputAxisEventCallbackStruct callbackStruct = MakePressedRightCallbackStruct()
+ callbackStruct.debounceTime = debounceTime
+ callbackStruct.callbackFunc = callbackFunc
+
+ RemovePlayerInputAxisEventCallback_Internal( player, callbackStruct )
+}
+
+
+void function AddPlayerInputAxisEventCallback_Internal( entity player, PlayerInputAxisEventCallbackStruct callbackStruct )
+{
+ if ( !player.GetSendInputCallbacks() )
+ player.SetSendInputCallbacks( true )
+
+ Assert( IsValidPlayerInputAxisEventCallbackStruct( callbackStruct ) )
+
+ Assert( !InputAxisEventCallbackAlreadyExists( player, callbackStruct ), " Adding the same inputEventCallback " + string ( callbackStruct.callbackFunc ) + " with the same inputs!" )
+
+ player.p.playerInputAxisEventCallbacks.append( callbackStruct )
+}
+
+void function RemovePlayerInputAxisEventCallback_Internal( entity player, PlayerInputAxisEventCallbackStruct callbackStruct )
+{
+ for( int i = player.p.playerInputAxisEventCallbacks.len() - 1; i >= 0; --i ) //Removing from the end of an array, so it's fine to remove as we go along
+ {
+ if ( InputAxisCallbackStructsAreTheSame( player.p.playerInputAxisEventCallbacks[i], callbackStruct ) )
+ {
+ player.p.playerInputAxisEventCallbacks.remove( i )
+ break //Can break since we shouldn't have more than one callbackstruct that's exactly the same
+ }
+ }
+
+ TurnOffInputCallbacksIfNecessary( player )
+}
+
+bool function InputAxisEventCallbackAlreadyExists( entity player, PlayerInputAxisEventCallbackStruct callbackStruct )
+{
+ foreach( existingStruct in player.p.playerInputAxisEventCallbacks )
+ {
+ if ( InputAxisCallbackStructsAreTheSame( existingStruct, callbackStruct ) )
+ return true
+ }
+
+ return false
+}
+
+bool function InputAxisCallbackStructsAreTheSame( PlayerInputAxisEventCallbackStruct callbackStruct1, PlayerInputAxisEventCallbackStruct callbackStruct2 ) //Really just a comparison function because == does a compare by reference, not a compare by value
+{
+ if ( callbackStruct1.horizAxisMinThreshold != callbackStruct2.horizAxisMinThreshold )
+ return false
+
+ if ( callbackStruct1.horizAxisMaxThreshold != callbackStruct2.horizAxisMaxThreshold )
+ return false
+
+ if ( callbackStruct1.vertAxisMinThreshold != callbackStruct2.vertAxisMinThreshold )
+ return false
+
+ if ( callbackStruct1.vertAxisMaxThreshold != callbackStruct2.vertAxisMaxThreshold )
+ return false
+
+ if ( callbackStruct1.debounceTime != callbackStruct2.debounceTime )
+ return false
+
+ if ( callbackStruct1.callbackFunc != callbackStruct2.callbackFunc )
+ return false
+
+ return true
+}
+
+bool function ShouldRunPlayerInputAxisCallbackFunc( float horizAxis, float vertAxis, PlayerInputAxisEventCallbackStruct callbackStruct )
+{
+ if ( horizAxis < callbackStruct.horizAxisMinThreshold )
+ return false
+
+ if ( horizAxis > callbackStruct.horizAxisMaxThreshold )
+ return false
+
+ if ( vertAxis < callbackStruct.vertAxisMinThreshold )
+ return false
+
+ if ( vertAxis > callbackStruct.vertAxisMaxThreshold )
+ return false
+
+ if ( Time() < callbackStruct.lastTriggeredTime + callbackStruct.debounceTime )
+ return false
+
+ return true
+
+}
+
+bool function IsValidPlayerInputAxisEventCallbackStruct( PlayerInputAxisEventCallbackStruct callbackStruct )
+{
+ //Make sure thresholds are within valid ranges
+ if ( callbackStruct.horizAxisMinThreshold < -1.0 )
+ return false
+
+ if ( callbackStruct.horizAxisMinThreshold > 1.0 )
+ return false
+
+ if ( callbackStruct.horizAxisMaxThreshold < -1.0 )
+ return false
+
+ if ( callbackStruct.horizAxisMaxThreshold > 1.0 )
+ return false
+
+ if ( callbackStruct.vertAxisMinThreshold < -1.0 )
+ return false
+
+ if ( callbackStruct.vertAxisMinThreshold > 1.0 )
+ return false
+
+ if ( callbackStruct.vertAxisMaxThreshold < 1.0 )
+ return false
+
+ if ( callbackStruct.vertAxisMaxThreshold > 1.0 )
+ return false
+
+ //Make sure min and maxes are correct relative to each other
+ if ( callbackStruct.horizAxisMinThreshold > callbackStruct.horizAxisMaxThreshold )
+ return false
+
+ if ( callbackStruct.vertAxisMinThreshold > callbackStruct.vertAxisMaxThreshold )
+ return false
+
+ return true
+}
+
+void function RunPlayerInputAxisCallbackFunc( entity player, PlayerInputAxisEventCallbackStruct callbackStruct )
+{
+ bool callbackResult = callbackStruct.callbackFunc( player )
+ if ( callbackResult )
+ {
+ callbackStruct.lastTriggeredTime = Time()
+
+ if ( callbackStruct.debounceTime > 0 )
+ thread RunInputAxisCallbackAfterTimePasses( player, callbackStruct ) //Note that this has the potential to call RunPlayerInputAxisCallbackFunc again
+ }
+}
+
+void function RunInputAxisCallbackAfterTimePasses( entity player, PlayerInputAxisEventCallbackStruct callbackStruct )
+{
+ player.EndSignal( "OnDeath" )
+
+ wait callbackStruct.debounceTime
+ WaitFrame() //Time to wait isn't exact due to floating point precision, so wait an extra frame.
+
+
+ if ( !IsValid( player ) )
+ return
+
+ float horizAxis = player.GetInputAxisRight()
+ float vertAxis = player.GetInputAxisForward()
+
+ if ( ShouldRunPlayerInputAxisCallbackFunc( horizAxis, vertAxis, callbackStruct ) )
+ RunPlayerInputAxisCallbackFunc( player, callbackStruct ) //Note that this has the potential to call RunInputAxisCallbackAfterTimePasses again
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_control_panel.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_control_panel.gnut
new file mode 100644
index 00000000..f9d7a4ff
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_control_panel.gnut
@@ -0,0 +1,727 @@
+untyped
+
+global function ControlPanel_Init
+
+global function InitControlPanelUseFuncTable
+global function AddControlPanelUseFuncTable
+global function SetControlPanelPrompts
+global function SetPanelUsableToEnemies
+global function PanelFlipsToPlayerTeamAndUsableByEnemies
+global function GetAllControlPanels
+global function CaptureAllAvailableControlPanels
+global function GetPanelUseEnts
+global function PlayIncomingFX
+global function SetControlPanelUseFunc
+global function ClearControlPanelUseFuncs
+
+const INCOMING_SPAWN_FX = $"P_ar_titan_droppoint"
+
+struct
+{
+ array<entity> controlPanels
+} file
+
+//=========================================================
+// Control Panels
+//
+//=========================================================
+
+//////////////////////////////////////////////////////////////////////
+function ControlPanel_Init()
+{
+ PrecacheModel( $"models/communication/terminal_usable_imc_01.mdl" )
+ PrecacheParticleSystem( INCOMING_SPAWN_FX )
+
+ //PrecacheMaterial( $"vgui/hud/control_panel/console_disabled/console_disabled" )
+ //PrecacheMaterial( $"vgui/hud/control_panel/console_f_deploy/console_f_deploy" )
+ //PrecacheMaterial( $"vgui/hud/control_panel/console_f_search/console_f_search" )
+ //PrecacheMaterial( $"vgui/hud/control_panel/console_f_active/console_f_active" )
+ //PrecacheMaterial( $"vgui/hud/control_panel/console_f_repair/console_f_repair" )
+ //PrecacheMaterial( $"vgui/hud/control_panel/console_e_deploy/console_e_deploy" )
+ //PrecacheMaterial( $"vgui/hud/control_panel/console_e_search/console_e_search" )
+ //PrecacheMaterial( $"vgui/hud/control_panel/console_e_active/console_e_active" )
+ //PrecacheMaterial( $"vgui/hud/control_panel/console_e_repair/console_e_repair" )
+
+ AddSpawnCallback( "prop_control_panel", OnPanelSpawn )
+
+
+ RegisterSignal( "PanelReprogrammed" )
+ RegisterSignal( "PanelReprogram_Success" )
+ RegisterSignal( "OnContinousUseStopped" )
+}
+
+//////////////////////////////////////////////////////////
+function GameModeRemovePanel( ent )
+{
+ local keepUndefined
+ string gameMode = GameRules_GetGameMode()
+
+ switch ( gameMode )
+ {
+ // if we are in this game mode, then don't keep undefined panels
+ default:
+ keepUndefined = true
+ gameMode = TEAM_DEATHMATCH
+ break
+ }
+
+ local gamemodeKey = "gamemode_" + gameMode
+
+ if ( ent.HasKey( gamemodeKey ) && ent.kv[gamemodeKey] == "1" )
+ {
+ // the key exists and it's true so keep it
+ return
+ }
+
+ if ( !ent.HasKey( gamemodeKey ) && keepUndefined )
+ {
+ // the key doesn't exist but keepUndefined is true so still keep it
+ return
+ }
+
+ ent.Destroy()
+}
+
+
+//////////////////////////////////////////////////////////////////////
+void function OnPanelSpawn( entity panel )
+{
+ Assert( panel.GetModelName() == $"models/communication/terminal_usable_imc_01.mdl" )
+
+ thread OnPanelSpawn_Internal( panel )
+}
+
+//////////////////////////////////////////////////////////////////////
+void function OnPanelSpawn_Internal( entity panel )
+{
+ panel.EndSignal( "OnDestroy" )
+ GameModeRemovePanel( panel )
+
+ panel.s.useFuncArray <- []
+
+ Assert( IsValid( panel ), "Invalid panel " + panel )
+ panel.EndSignal( "OnDestroy" )
+
+ file.controlPanels.append( panel )
+
+ thread PanelUpdateUsability( panel )
+
+ panel.useFunction = ControlPanel_CanUseFunction
+
+ panel.s.leechTimeNormal <- 3.0
+ panel.s.leechTimeFast <- 1.1
+
+ panel.kv.forceVisibleInPhaseShift = true
+
+ panel.s.onPlayerFinishesUsing_func <- null
+ panel.s.hackedOnce <- false
+ //Used in Frontier Mode for knowing if NPCs are hacking the panel.
+ panel.s.hackingEntity <- null
+
+ panel.s.remoteTurret <- null
+ panel.s.remoteTurretStartFunc <- null
+
+ #if HAS_PANEL_HIGHLIGHT
+ int contextId = 0
+ panel.Highlight_SetFunctions( contextId, 0, true, HIGHLIGHT_OUTLINE_INTERACT_BUTTON, 1, 0, false )
+ panel.Highlight_SetParam( contextId, 0, HIGHLIGHT_COLOR_INTERACT )
+ panel.Highlight_SetCurrentContext( contextId )
+ panel.Highlight_ShowInside( 0.0 )
+ panel.Highlight_ShowOutline( 0.0 )
+ #endif
+
+ string flag
+ if ( panel.HasKey( "scr_flag_set" ) )
+ {
+ string editorVal = expect string( panel.kv.scr_flag_set )
+ if ( editorVal != "" )
+ {
+ flag = editorVal
+ FlagInit( flag )
+ }
+ }
+
+ string hackFlag
+ if ( panel.HasKey( "scr_flag_hack_started" ) )
+ {
+ string editorVal = expect string( panel.kv.scr_flag_hack_started )
+ if ( editorVal != "" )
+ {
+ hackFlag = editorVal
+ FlagInit( hackFlag )
+ }
+ }
+
+ bool toggleFlag = false
+ if ( panel.HasKey( "toggleFlagWhenHacked" ) )
+ toggleFlag = panel.kv.toggleFlagWhenHacked == "1"
+
+ bool singleUse = false
+ if ( panel.HasKey( "singleUse" ) )
+ singleUse = panel.kv.singleUse.tointeger() > 0
+
+ string requiredFlag = ""
+ if ( panel.HasKey( "scr_flagRequired" ) && panel.GetValueForKey( "scr_flagRequired" ) != "" )
+ requiredFlag = panel.GetValueForKey( "scr_flagRequired" )
+
+ for ( ;; )
+ {
+ var player = panel.WaitSignal( "OnPlayerUse" ).player
+ Assert( player.IsPlayer() )
+ expect entity( player )
+
+ if ( !IsAlive( player ) || player.IsTitan() )
+ continue
+
+ // Panel might be disabled with a flag, so don't allow a hack. We don't disable usability though, because we want use prompts still, with custom hint text
+ if ( (requiredFlag != "") && !Flag( requiredFlag ) )
+ continue
+
+ // already a user?
+ if ( IsAlive( panel.GetBossPlayer() ) )
+ continue
+
+ if ( !panel.useFunction( player, panel ) )
+ {
+ //play buzzer sound
+ //EmitSoundOnEntity( panel, "Operator.Ability_offline" )
+ wait 1
+ continue
+ }
+
+ waitthread PlayerUsesControlPanel( player, panel, flag, toggleFlag, hackFlag )
+
+ if ( singleUse && (panel.s.hackedOnce == true) )
+ break
+ }
+
+ // control panel no longer usable
+ panel.UnsetUsable()
+ panel.SetUsePrompts( "", "" )
+ #if HAS_PANEL_HIGHLIGHT
+ panel.Highlight_HideInside( 1.0 )
+ panel.Highlight_HideOutline( 1.0 )
+ #endif
+}
+
+void function PanelUpdateUsability( entity panel )
+{
+ panel.EndSignal( "OnDestroy" )
+
+ //Default, set it usable by everyone
+ panel.SetUsableByGroup( "pilot" )
+ panel.SetUsePrompts( "#DEFAULT_HACK_HOLD_PROMPT", "#DEFAULT_HACK_PRESS_PROMPT" )
+
+ if ( !panel.HasKey( "scr_flagRequired" ) )
+ return
+
+ string flag = panel.GetValueForKey( "scr_flagRequired" )
+
+ if ( flag == "" )
+ return
+
+ FlagInit( flag )
+
+ string disabledUsePrompt = ""
+ if ( panel.HasKey( "disabledHintString" ) )
+ disabledUsePrompt = panel.GetValueForKey( "disabledHintString" )
+
+ while(true)
+ {
+ panel.SetUsePrompts( disabledUsePrompt, disabledUsePrompt )
+ FlagWait( flag )
+ panel.SetUsePrompts( "#DEFAULT_HACK_HOLD_PROMPT", "#DEFAULT_HACK_PRESS_PROMPT" )
+ FlagWaitClear( flag )
+ }
+}
+
+void function PlayIncomingFX( vector origin, int teamNum )
+{
+ wait 1.50
+ EmitSoundAtPosition( teamNum, origin, "Titan_1P_Warpfall_Start" )
+
+ local colorVec = Vector( 0, 255, 0 )
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "pickup_controlpoint" ) )
+ DispatchSpawn( cpoint )
+ cpoint.SetOrigin( colorVec )
+ entity glowFX = PlayFXWithControlPoint( INCOMING_SPAWN_FX, origin, cpoint, -1, null, null, C_PLAYFX_LOOP )
+
+ OnThreadEnd(
+ function() : ( glowFX, cpoint )
+ {
+ if ( IsValid( glowFX ) )
+ glowFX.Destroy()
+ if ( IsValid( cpoint ) )
+ cpoint.Destroy()
+ }
+ )
+
+ wait 1.25
+}
+
+void function PlayerUsesControlPanel( entity player, entity panel, string flag, bool toggleFlag, string hackFlag )
+{
+ thread PlayerProgramsControlPanel( panel, player, hackFlag )
+
+ local result = panel.WaitSignal( "PanelReprogrammed" )
+
+ if ( result.success )
+ {
+ local panelEHandle = IsValid( panel ) ? panel.GetEncodedEHandle() : null
+ array<entity> players = GetPlayerArray()
+ foreach( player in players )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_ControlPanelRefresh", panelEHandle )
+ }
+
+ RunPanelUseFunctions( panel, player )
+ panel.Signal( "PanelReprogram_Success" )
+ if ( flag != "" )
+ {
+ if ( toggleFlag && Flag( flag ) )
+ FlagClear( flag )
+ else
+ {
+ FlagSet( flag )
+ }
+ }
+
+ panel.s.hackedOnce = true
+ }
+ else
+ {
+ //play buzzer sound
+ //EmitSoundOnEntity( panel, "Operator.Ability_offline" )
+ WaitFrame() // arbitrary delay so that you can't restart the leech instantly after failing
+ if ( hackFlag != "" )
+ FlagClear( hackFlag )
+ }
+}
+
+function RunPanelUseFunctions( panel, player )
+{
+ if ( panel.s.useFuncArray.len() <= 0 )
+ return
+
+ foreach ( useFuncTable in clone panel.s.useFuncArray )
+ {
+ if ( useFuncTable.useEnt == null )
+ useFuncTable.useFunc( panel, player )
+ else
+ useFuncTable.useFunc( panel, player, useFuncTable.useEnt )
+ }
+}
+
+function SetControlPanelUseFunc( panel, func, ent = null )
+{
+ local Table = InitControlPanelUseFuncTable()
+ Table.useFunc <- func
+ Table.useEnt <- ent
+ AddControlPanelUseFuncTable( panel, Table )
+}
+
+function ClearControlPanelUseFuncs( panel )
+{
+ panel.s.useFuncArray.clear()
+}
+
+//////////////////////////////////////////////////////////////////////
+void function PlayerProgramsControlPanel( entity panel, entity player, string hackFlag )
+{
+ Assert( IsAlive( player ) )
+
+ // need to wait here so that the panel script can start waiting for the PanelReprogrammed signal.
+ WaitFrame()
+
+ local action =
+ {
+ playerAnimation1pStart = "ptpov_data_knife_console_leech_start"
+ playerAnimation1pIdle = "ptpov_data_knife_console_leech_idle"
+ playerAnimation1pEnd = "ptpov_data_knife_console_leech_end"
+
+ playerAnimation3pStart = "pt_data_knife_console_leech_start"
+ playerAnimation3pIdle = "pt_data_knife_console_leech_idle"
+ playerAnimation3pEnd = "pt_data_knife_console_leech_end"
+
+ panelAnimation3pStart = "tm_data_knife_console_leech_start"
+ panelAnimation3pIdle = "tm_data_knife_console_leech_idle"
+ panelAnimation3pEnd = "tm_data_knife_console_leech_end"
+
+ direction = Vector( -1, 0, 0 )
+ }
+
+ #if HAS_PANEL_HIGHLIGHT
+ panel.Highlight_HideInside( 1.0 )
+ panel.Highlight_HideOutline( 1.0 )
+ #endif
+
+ local e = {}
+ e.success <- false
+ e.knives <- []
+
+ e.panelUsableValueToRestore <- panel.GetUsableValue()
+ e.startOrigin <- player.GetOrigin()
+ panel.SetBossPlayer( player )
+ panel.SetUsableValue( USABLE_BY_OWNER )
+
+ e.setIntruder <- false
+
+ e.finishedPanelOpen <- false
+ e.animViewLerpoutTime <- 0.3
+ e.doRequireUseButtonHeld <- true
+
+ player.ForceStand()
+ HolsterAndDisableWeapons( player ) //Do here instead of after doRequireUseButtonHeld check since DisableOffhandWeapons() is counter based, i.e. a call to DisableOffhandWeapons() must be matched with a call to EnableOffhandWeapons()
+
+ //
+ if ( panel.s.remoteTurret )
+ {
+ action.playerAnimation1pStart = "ptpov_data_knife_console_leech_remoteturret_start"
+ action.playerAnimation3pStart = "pt_data_knife_console_leech_remoteturret_start"
+ action.panelAnimation3pStart = "tm_data_knife_console_leech_remoteturret_start"
+
+ e.animViewLerpoutTime = 0.0
+ e.doRequireUseButtonHeld = false
+
+ panel.SetUsePrompts( "", "" )
+ }
+
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "ScriptAnimStop" )
+
+ OnThreadEnd
+ (
+ function() : ( e, player, panel )
+ {
+ if ( e.setIntruder )
+ level.nv.panelIntruder = null
+
+ if ( IsValid( player ) )
+ {
+ player.ClearAnimNearZ()
+ player.ClearParent()
+
+ // stop any running first person sequences
+ player.Anim_Stop()
+
+ if ( IsAlive( player ) )
+ PutEntityInSafeSpot( player, panel, null, expect vector( e.startOrigin ), player.GetOrigin() )
+
+ // done with first person anims
+ ClearPlayerAnimViewEntity( player, expect float( e.animViewLerpoutTime ) )
+ DeployAndEnableWeapons( player )
+ player.UnforceStand()
+
+ if ( player.ContextAction_IsLeeching() )
+ player.Event_LeechEnd()
+ }
+
+ if ( IsValid( panel ) )
+ {
+ // stop any running first person sequences
+ panel.Anim_Stop()
+ panel.Anim_Play( "ref" ) // close the hatch
+
+ // reset default usability
+ if ( !panel.s.remoteTurret || !e.finishedPanelOpen )
+ {
+ panel.ClearBossPlayer()
+ panel.SetUsableValue( e.panelUsableValueToRestore )
+ }
+
+ if ( !e.success )
+ {
+ #if HAS_PANEL_HIGHLIGHT
+ panel.Highlight_ShowInside( 1.0 )
+ panel.Highlight_ShowOutline( 1.0 )
+ #endif
+
+ panel.Signal( "PanelReprogrammed", { success = e.success } )
+ #if MP
+ local turret = GetMegaTurretLinkedToPanel( panel ) //CHIN: Control panels shouldn't need to know about turrets
+ if ( IsValid( turret ) && IsTurret( turret ) )
+ {
+ local usableValue = MegaTurretUsabilityFunc( turret, panel )
+ panel.SetUsableByGroup( usableValue )
+ SetUsePromptForPanel( panel, turret )
+ }
+ else
+ {
+ // Turret got destoyed while hacking.
+ // Usability state has been set by ReleaseTurret( ... ) in ai_turret.nut
+ // Changing it to the previous usable value would put us in a bad state.
+
+
+ // we should change how this works for R2
+ //
+ // HACK remove s.scriptedPanel when these are refactored
+ if ( "scriptedPanel" in panel.s )
+ panel.SetUsableValue( e.panelUsableValueToRestore )
+ }
+ #endif
+
+ #if SP
+ if ( "scriptedPanel" in panel.s )
+ panel.SetUsableValue( e.panelUsableValueToRestore )
+ #endif
+ }
+
+ if ( panel.s.remoteTurret && e.finishedPanelOpen )
+ thread panel.s.remoteTurretStartFunc( panel, player, e.panelUsableValueToRestore )
+
+ if ( panel.s.onPlayerFinishesUsing_func )
+ thread panel.s.onPlayerFinishesUsing_func( panel, player, e.success )
+ }
+
+ foreach ( knife in e.knives )
+ {
+ if ( IsValid( knife ) )
+ knife.Destroy()
+ }
+ }
+ )
+
+ if ( e.doRequireUseButtonHeld && !player.UseButtonPressed() )
+ return // it's possible to get here and no longer be holding the use button. If that is the case lets not continue.
+
+ if ( player.ContextAction_IsActive() )
+ return
+
+ if ( player.IsPhaseShifted() )
+ return
+
+ player.SetAnimNearZ( 1 )
+
+ player.Event_LeechStart()
+
+ local leechTime = panel.s.leechTimeNormal
+
+ if ( PlayerHasPassive( player, ePassives.PAS_FAST_HACK ) )
+ leechTime = panel.s.leechTimeFast
+
+ local totalTime = leechTime + player.GetSequenceDuration( action.playerAnimation3pStart )
+
+ thread TrackContinuousUse( player, totalTime, e.doRequireUseButtonHeld )
+
+ waitthread ControlPanelFlipAnimation( panel, player, action, e )
+
+ if ( e.doRequireUseButtonHeld && !player.UseButtonPressed() )
+ return // we might have returned from the flip anim because we released the use button.
+
+ if ( hackFlag != "" )
+ FlagSet( hackFlag )
+
+ e.finishedPanelOpen = true
+ if ( panel.s.remoteTurret )
+ {
+ // Called on thread end above.
+ return
+ }
+
+ Remote_CallFunction_Replay( player, "ServerCallback_DataKnifeStartLeech", leechTime )
+
+ waitthread WaitForEndLeechOrStoppedUse( player, leechTime, e, panel )
+
+ if ( e.success )
+ {
+ thread DataKnifeSuccessSounds( player )
+ }
+ else
+ {
+ DataKnifeCanceledSounds( player )
+ Remote_CallFunction_Replay( player, "ServerCallback_DataKnifeCancelLeech" )
+ }
+
+ waitthread ControlPanelFlipExitAnimation( player, panel, action, e )
+}
+
+function WaitForEndLeechOrStoppedUse( player, leechTime, e, panel )
+{
+ player.EndSignal( "OnContinousUseStopped" )
+ wait leechTime
+ e.success = true
+ panel.Signal( "PanelReprogrammed", { success = e.success } )
+}
+
+
+//////////////////////////////////////////////////////////////////////
+function ControlPanelFlipAnimation( entity panel, entity player, action, e )
+{
+// OnThreadEnd
+// (
+// function() : ( panel )
+// {
+// if ( IsValid( panel ) )
+// DeleteAnimEvent( panel, "knife_popout" )
+// }
+// )
+ player.EndSignal( "OnContinousUseStopped" )
+
+ FirstPersonSequenceStruct playerSequence
+ playerSequence.attachment = "ref"
+ playerSequence.thirdPersonAnim = expect string ( action.playerAnimation3pStart )
+ playerSequence.thirdPersonAnimIdle = expect string ( action.playerAnimation3pIdle )
+ playerSequence.firstPersonAnim = expect string ( action.playerAnimation1pStart )
+ playerSequence.firstPersonAnimIdle = expect string ( action.playerAnimation1pIdle )
+ if ( IntroPreviewOn() )
+ playerSequence.viewConeFunction = ControlPanelFlipViewCone
+
+ FirstPersonSequenceStruct panelSequence
+ panelSequence.thirdPersonAnim = expect string ( action.panelAnimation3pStart )
+ panelSequence.thirdPersonAnimIdle = expect string ( action.panelAnimation3pIdle )
+
+
+ asset model = DATA_KNIFE_MODEL
+
+ entity knife = CreatePropDynamic( model )
+ SetTargetName( knife, "dataKnife" )
+ knife.SetParent( player, "PROPGUN", false, 0.0 )
+ e.knives.append( knife )
+
+ thread PanelFirstPersonSequence( panelSequence, panel, player )
+ waitthread FirstPersonSequence( playerSequence, player, panel )
+}
+
+
+void function ControlPanelFlipViewCone( entity player )
+{
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -80 )
+ player.PlayerCone_SetMaxYaw( 80 )
+ player.PlayerCone_SetMinPitch( -80 )
+ player.PlayerCone_SetMaxPitch( 10 )
+}
+
+
+//////////////////////////////////////////////////////////////////////
+function PanelFirstPersonSequence( FirstPersonSequenceStruct panelSequence, entity panel, entity player )
+{
+ player.EndSignal( "OnDeath" )
+ panel.EndSignal( "OnDestroy" )
+
+ waitthread FirstPersonSequence( panelSequence, panel )
+}
+
+
+//////////////////////////////////////////////////////////////////////
+function ControlPanelFlipExitAnimation( entity player, entity panel, action, e )
+{
+ FirstPersonSequenceStruct playerSequence
+ playerSequence.blendTime = 0.0
+ playerSequence.attachment = "ref"
+ playerSequence.teleport = true
+
+ FirstPersonSequenceStruct panelSequence
+ panelSequence.blendTime = 0.0
+
+ playerSequence.thirdPersonAnim = expect string ( action.playerAnimation3pEnd )
+ playerSequence.firstPersonAnim = expect string ( action.playerAnimation1pEnd )
+ panelSequence.thirdPersonAnim = expect string ( action.panelAnimation3pEnd )
+
+ thread FirstPersonSequence( panelSequence, panel )
+ waitthread FirstPersonSequence( playerSequence, player, panel )
+}
+
+
+//////////////////////////////////////////////////////////////////////
+function TrackContinuousUse( player, leechTime, doRequireUseButtonHeld )
+{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "ScriptAnimStop" )
+
+ local result = {}
+ result.success <- false
+
+ OnThreadEnd
+ (
+ function() : ( player, result )
+ {
+ if ( !result.success )
+ {
+ player.Signal( "OnContinousUseStopped" )
+ }
+ }
+ )
+
+ float startTime = Time()
+ while ( Time() < startTime + leechTime && (!doRequireUseButtonHeld || player.UseButtonPressed()) && !player.IsPhaseShifted() )
+ WaitFrame()
+
+ if ( !doRequireUseButtonHeld || player.UseButtonPressed() )
+ result.success = true
+}
+
+function InitControlPanelUseFuncTable()
+{
+ local Table = {}
+ Table.useEnt <- null
+ Table.useFunc <- null
+ return Table
+}
+
+function AddControlPanelUseFuncTable( panel, Table )
+{
+ // a table that contains
+ //1. a function to be called when the control panel is used
+ //2. an entity that the function refers to, e.g. the turret to be created
+ panel.s.useFuncArray.append( Table )
+}
+
+function SetControlPanelPrompts( ent, func )
+{
+ ent.s.prompts <- func( ent )
+}
+
+function SetPanelUsableToEnemies( panel )
+{
+ if ( panel.GetTeam() == TEAM_IMC || panel.GetTeam() == TEAM_MILITIA )
+ {
+ panel.SetUsableByGroup( "enemies pilot" )
+ return
+ }
+
+ //Not on either player team, just set usable to everyone
+ panel.SetUsableByGroup( "pilot" )
+}
+
+function PanelFlipsToPlayerTeamAndUsableByEnemies( panel, entity player )
+{
+ expect entity( panel )
+
+ SetTeam( panel, player.GetTeam() )
+ SetPanelUsableToEnemies( panel )
+}
+
+function GetPanelUseEnts( panel )
+{
+ local useEntsArray = []
+ foreach( useFuncTable in panel.s.useFuncArray )
+ {
+ if ( useFuncTable.useEnt )
+ useEntsArray.append( useFuncTable.useEnt )
+ }
+
+ return useEntsArray
+
+}
+
+array<entity> function GetAllControlPanels()
+{
+ //Defensively remove control panels that are invalid.
+ //This is because we can have control panels in levels for some game modes
+ //but not in others, e.g. refuel mode vs tdm
+
+ ArrayRemoveInvalid( file.controlPanels )
+ return file.controlPanels
+}
+
+function CaptureAllAvailableControlPanels( player )
+{
+ array<entity> panels = GetAllControlPanels()
+ foreach ( panel in panels )
+ {
+ printt( "panel team " + panel.GetTeam() )
+ RunPanelUseFunctions( panel, player )
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_dogfighter.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_dogfighter.gnut
new file mode 100644
index 00000000..db616173
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_dogfighter.gnut
@@ -0,0 +1,343 @@
+untyped
+
+global function Dogfighter_Init
+
+global function CreateDogFighterAttack
+global function LaunchRandomDogFighterAttacks
+global function CreateDogFighter
+global function CreateDogFighterAssist
+global function GetGunshipModel
+const TURRET_WEAPON_BULLETS = "mp_weapon_yh803_bullet"
+
+function Dogfighter_Init()
+{
+
+ AddDeathCallback( "npc_dropship", OnDogFighterDeath )
+
+ RegisterSignal( "new_attack_thread" )
+ RegisterSignal( "GunshipForceLeave" )
+}
+
+function LaunchRandomDogFighterAttacks( int team )
+{
+ svGlobal.levelEnt.Signal( "new_attack_thread" )
+ svGlobal.levelEnt.EndSignal( "new_attack_thread" )
+
+ for ( ;; )
+ {
+ thread CreateDogFighterAttack( team )
+ wait RandomFloatRange( 3, 9 )
+ }
+}
+
+function CreateDogFighterAttack( int team )
+{
+ FlightPath flightPath = GetAnalysisForModel( GetFlightPathModel( "fp_straton_model" ), STRATON_ATTACK_FULL )
+
+ CallinData drop
+ InitCallinData( drop )
+ SetCallinStyle( drop, eDropStyle.RANDOM_FROM_YAW )
+ SetCallinYaw( drop, RandomFloat( 360 ) )
+
+
+ SpawnPointFP spawnPoint = GetSpawnPointForStyle( flightPath, drop )
+ if ( !spawnPoint.valid )
+ return
+
+ entity ship = CreateDogFighter( Vector(0,0,0), Vector(0,0,0), team )
+
+ int hornet_health = 2000
+ ship.SetHealth( hornet_health )
+ ship.SetMaxHealth( hornet_health )
+
+ ship.EndSignal( "OnDeath" )
+
+ //AddBulletTurrets( ship, team )
+
+ waitthread PlayAnimTeleport( ship, "st_AngelCity_IMC_Win_ComeIn", spawnPoint.origin, spawnPoint.angles )
+
+ thread PlayAnim( ship, "st_AngelCity_IMC_Win_Idle", spawnPoint.origin, spawnPoint.angles )
+ waitthread DogFighterWaitsUntilLeave( ship )
+
+ waitthread PlayAnim( ship, "st_AngelCity_IMC_Win_Leave", spawnPoint.origin, spawnPoint.angles, 0.5 )
+
+ ship.Kill_Deprecated_UseDestroyInstead()
+}
+
+
+function DogFighterWaitsUntilLeave( ship, idleMin = 10, idleMax = 15 )
+{
+ local duration = ship.GetSequenceDuration( "st_AngelCity_IMC_Win_Idle" )
+
+ // make it play full increments of the idle anim
+ local maxHealth = ship.GetMaxHealth().tofloat()
+ local idleTime = RandomFloatRange( idleMin, idleMax )
+ local reps = ( duration / idleTime ).tointeger()
+ local totalTime = reps * duration
+ local endTime = Time() + totalTime
+
+ for ( ;; )
+ {
+ if ( ship.GetHealth().tofloat() / maxHealth < 0.2 )
+ return
+ if ( Time() >= endTime )
+ return
+ wait 0.1
+ }
+}
+
+entity function CreateDogFighter( vector origin, vector angles, int team )
+{
+ entity hornet
+ if ( GetBugReproNum() == 81765 )
+ {
+ hornet = CreateEntity( "npc_dropship" )
+ }
+ else
+ {
+ // HACK: using a prop script for now since NPC dropships are still buggy
+ // Jiesang told me to do it!
+ hornet = CreatePropScript( GetGunshipModel( team ), origin, angles, 8 )
+ hornet.EnableAttackableByAI( 50, 0, AI_AP_FLAG_NONE )
+ SetTeam( hornet, team )
+ hornet.SetOrigin( origin )
+ hornet.SetAngles( angles )
+ }
+
+ hornet.s.dogfighter <- true
+ hornet.kv.teamnumber = team
+
+ local title
+ switch ( team )
+ {
+ case TEAM_MILITIA:
+ hornet.SetModel( GetFlightPathModel( "fp_hornet_model" ) )
+ title = "Militia Hornet"
+ break
+
+ case TEAM_IMC:
+ hornet.SetModel( GetFlightPathModel( "fp_straton_model" ) )
+ title = "IMC Phantom"
+ break
+ }
+
+ hornet.SetTitle( title )
+
+ hornet.SetOrigin( origin )
+ hornet.SetAngles( angles )
+ // DispatchSpawn( hornet )
+
+ //hornet.EnableRenderAlways()
+ //hornet.SetAimAssistAllowed( false )
+
+ return hornet
+}
+
+void function OnDogFighterDeath( entity ent, var damageInfo )
+{
+ if ( !IsValid( ent ) )
+ return
+
+ if ( !( "dogfighter" in ent.s ) )
+ return
+
+ if ( ent.GetHealth() <= 0 )
+ FighterExplodes( ent )
+}
+
+
+
+
+function AddRocketTurrets( entity ship, int team, int prof = eWeaponProficiency.VERYGOOD )
+{
+ entity turret = AddTurret( ship, team, "mp_weapon_yh803", "l_exhaust_front_1" )
+ turret.kv.WeaponProficiency = prof
+ turret.NotSolid()
+ turret.Show()
+ entity weapon = turret.GetActiveWeapon()
+ weapon.Show()
+
+ turret = AddTurret( ship, team, "mp_weapon_yh803", "r_exhaust_front_1" )
+ turret.kv.WeaponProficiency = prof
+ turret.NotSolid()
+ turret.Show()
+ weapon = turret.GetActiveWeapon()
+ weapon.Show()
+}
+
+void function AddBulletTurrets( entity ship, int team, int prof = eWeaponProficiency.VERYGOOD )
+{
+ entity turret = AddTurret( ship, team, TURRET_WEAPON_BULLETS, "l_exhaust_front_1" )
+ turret.kv.WeaponProficiency = prof
+ turret.NotSolid()
+ turret = AddTurret( ship, team, TURRET_WEAPON_BULLETS, "r_exhaust_front_1" )
+ turret.kv.WeaponProficiency = prof
+ turret.NotSolid()
+}
+
+asset function GetGunshipModel( int team )
+{
+ switch ( team )
+ {
+ case TEAM_MILITIA:
+ return GetFlightPathModel( "fp_hornet_model" )
+
+ case TEAM_IMC:
+ return GetFlightPathModel( "fp_straton_model" )
+ }
+
+ unreachable
+}
+
+void function CreateDogFighterAssist( int team, vector origin, vector angles, float duration = 10.0, entity ship = null, float dropHeight = 1500 )
+{
+ angles += <0,90,0>
+
+ angles = < 0, angles.y%360, 0 >
+
+ // DebugDrawSphere( origin, 256, 255, 0, 0, true, 10.0 )
+
+ // warp in effect before
+ Point start = GetWarpinPosition( GetFighterModelForTeam( team ), "st_AngelCity_IMC_Win_ComeIn_fast", origin, angles )
+
+ if ( !IsValid( ship ) )
+ {
+ ship = CreateDogFighter( start.origin, start.angles, team )
+ }
+ else
+ {
+ ship.SetOrigin( start.origin )
+ ship.SetAngles( start.angles )
+ }
+
+ waitthread __WarpInEffectShared( start.origin, start.angles, "", 0.0 )
+
+
+ ship.SetHealth( 10000 )
+ ship.SetMaxHealth( 10000 )
+ ship.EndSignal( "OnDeath" )
+
+ #if R1_VGUI_MINIMAP
+ ship.Minimap_SetDefaultMaterial( GetMinimapMaterial( "VIP_friendly" ) )
+ ship.Minimap_SetFriendlyMaterial( GetMinimapMaterial( "VIP_friendly" ) )
+ ship.Minimap_SetEnemyMaterial( GetMinimapMaterial( "VIP_enemy" ) )
+ ship.Minimap_SetBossPlayerMaterial( GetMinimapMaterial( "VIP_friendly" ) )
+ #endif
+ ship.Minimap_SetObjectScale( 0.11 )
+ ship.Minimap_SetZOrder( MINIMAP_Z_NPC )
+
+ entity mover = CreateScriptMover( origin, angles )
+
+ OnThreadEnd(
+ function () : ( ship, mover )
+ {
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ if ( IsValid( ship ) )
+ ship.Destroy()
+ }
+ )
+
+ float SHIP_SIZE = 530
+ vector SHIP_MIN = Vector( -SHIP_SIZE/2, -SHIP_SIZE/2, -250 )
+ vector SHIP_MAX = Vector( SHIP_SIZE/2, SHIP_SIZE/2, 100 )
+
+ entity turret
+
+ turret = CreateEntity( "npc_turret_sentry" )
+ SetSpawnOption_AISettings( turret, "npc_turret_sentry_rockets_dropship" )
+ SetSpawnOption_Weapon( turret, "mp_turretweapon_blaster" )
+ SetSpawnOption_Alert( turret )
+ SetTeam( turret, team )
+ DispatchSpawn( turret )
+ turret.SetInvulnerable()
+ turret.SetParent( ship, "r_turret_attach", false, 0.0 )
+ turret.SetTitle( ship.GetTitle() )
+ NPC_NoTarget( turret )
+
+ turret = CreateEntity( "npc_turret_sentry" )
+ SetSpawnOption_AISettings( turret, "npc_turret_sentry_rockets_dropship" )
+ SetSpawnOption_Weapon( turret, "mp_turretweapon_blaster" )
+ SetSpawnOption_Alert( turret )
+ SetTeam( turret, team )
+ DispatchSpawn( turret )
+ turret.SetInvulnerable()
+ turret.SetParent( ship, "l_turret_attach", false, 0.0 )
+ turret.SetTitle( ship.GetTitle() )
+ NPC_NoTarget( turret )
+
+ // DrawArrow( origin, angles, 10, 50 )
+
+ waitthread PlayAnimTeleport( ship, "st_AngelCity_IMC_Win_ComeIn_fast", origin, angles )
+
+ // -------------------------------------------------
+ // now we want to drop the ship close to the ground
+ // -------------------------------------------------
+
+ vector shipOrigin = ship.GetOrigin()
+ vector newOrigin = shipOrigin
+
+ if ( dropHeight > 0 )
+ {
+ // TRACE to find the floor
+ float traceFrac = TraceLineSimple( shipOrigin, shipOrigin - Vector(0,0,dropHeight), ship )
+ vector floorPos = shipOrigin - Vector(0,0,dropHeight * traceFrac)
+ floorPos += Vector( 0,0,400 ) //we don't want the ship to land!
+
+ // TRACE to see if anything is in the way
+ float result = TraceHullSimple( shipOrigin, floorPos, SHIP_MIN, SHIP_MAX, ship )
+ vector offset = ( shipOrigin - floorPos ) * result
+
+ // This is where we will move the spawnpoint
+ newOrigin = origin - offset
+ }
+
+ // float duration = ship.GetSequenceDuration( "st_AngelCity_IMC_Win_Idle" )
+
+ ship.SetParent( mover, "REF" )
+ ship.Anim_EnableUseAnimatedRefAttachmentInsteadOfRootMotion()
+
+ // Ship comes in...
+ thread PlayAnim( ship, "st_AngelCity_IMC_Win_Idle", mover, "REF" )
+
+ // Ship goes down...
+ float dropDuration = 5.0
+ if ( dropHeight > 0 )
+ {
+ mover.NonPhysicsMoveTo( newOrigin, dropDuration, dropDuration*0.4, dropDuration*0.4 )
+ wait dropDuration
+ }
+
+ // Ship hangs out for a while...
+ waitthread GunshipWaitLeave( ship, duration )
+
+ // Ship raises before it leaves...
+ if ( dropHeight > 0 )
+ {
+ mover.NonPhysicsMoveTo( origin, dropDuration, dropDuration*0.4, dropDuration*0.4 )
+ wait dropDuration
+ }
+ ship.ClearParent()
+
+ // Ship leaves...
+ waitthread PlayAnim( ship, "st_AngelCity_IMC_Win_Leave", origin, angles, 0.5 )
+}
+
+void function GunshipWaitLeave( entity ship, float duration )
+{
+ ship.EndSignal( "GunshipForceLeave" )
+ wait duration
+}
+
+asset function GetFighterModelForTeam( int team )
+{
+ switch ( team )
+ {
+ case TEAM_MILITIA:
+ return GetFlightPathModel( "fp_hornet_model" )
+
+ case TEAM_IMC:
+ return GetFlightPathModel( "fp_straton_model" )
+ }
+ unreachable
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_entitystructs.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_entitystructs.gnut
new file mode 100644
index 00000000..378ceae3
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_entitystructs.gnut
@@ -0,0 +1,692 @@
+global struct BeamEffect
+{
+ entity effect
+ entity cpoint
+}
+
+global struct SpawnPointData
+{
+ table lastRatingData = {}
+}
+
+global struct BallLightningData
+{
+ asset zapFx = BALL_LIGHTNING_ZAP_FX
+ float zapLifetime = BALL_LIGHTNING_ZAP_LIFETIME
+ string zapSound = BALL_LIGHTNING_ZAP_SOUND
+ string zapImpactTable = BALL_LIGHTNING_FX_TABLE
+ float radius = BALL_LIGHTNING_ZAP_RADIUS
+ float humanRadius = BALL_LIGHTNING_ZAP_HUMANSIZE_RADIUS
+ float height = BALL_LIGHTNING_ZAP_HEIGHT
+ float minDot = -1.0
+ float damageToPilots = BALL_LIGHTNING_DAMAGE_TO_PILOTS
+ float damage = BALL_LIGHTNING_DAMAGE
+ bool zapPylons = false
+ int deathPackage = ( DF_DISSOLVE | DF_GIB | DF_ELECTRICAL | DF_STOPS_TITAN_REGEN )
+ bool fatalToDoomedTitans = false
+}
+
+global struct PhaseRewindData
+{
+ vector origin
+ vector angles
+ vector velocity
+ bool wasInContextAction
+ bool wasCrouched
+}
+
+global struct PlayerInputEventCallbackStruct
+{
+ int cmdsPressedBitMask = 0
+ int cmdsHeldBitMask = 0
+ int cmdsReleasedBitMask = 0
+ void functionref( entity player ) callbackFunc
+}
+
+global struct PlayerHeldButtonEventCallbackStruct
+{
+ int buttonHeld = 0
+ void functionref( entity player ) callbackFunc
+ float timeHeld = 1.0
+}
+
+global struct PlayerInputAxisEventCallbackStruct
+{
+ float horizAxisMinThreshold = -1.0
+ float horizAxisMaxThreshold = 1.0
+ float vertAxisMinThreshold= -1.0
+ float vertAxisMaxThreshold= 1.0
+
+ bool functionref( entity player ) callbackFunc
+ float debounceTime = 0.0
+ float lastTriggeredTime = 0.0
+}
+
+global enum eStatUpdateTime
+{
+ DISTANCE,
+ TIME_PLAYED,
+ WEAPON_USAGE,
+ COUNT
+}
+
+global enum eStoredWeaponType
+{
+ main,
+ offhand
+}
+
+global struct TitanDamage
+{
+ int shieldDamage
+
+ bool doomedNow
+ int doomedDamage
+}
+
+global struct RecentUnlock
+{
+ int refGuid = 0
+ int parentRefGuid = 0
+ int count = 0
+}
+
+global struct StoredWeapon
+{
+ string name
+ int weaponType = eStoredWeaponType.main
+ bool activeWeapon = false
+ int inventoryIndex
+ array<string> mods
+ int modBitfield
+ int ammoCount
+ int clipCount
+ float nextAttackTime
+ int skinIndex
+ int camoIndex
+ bool isProScreenOwner
+ #if MP
+ string burnReward
+ #endif
+ int scriptFlags0
+ int scriptTime0
+}
+
+global struct ScriptTriggerData
+{
+ bool enabled
+ float radius
+ table<entity> entities
+ array<void functionref(entity, entity)> enterCallbacks
+ array<void functionref(entity, entity)> leaveCallbacks
+ float top
+ float bottom
+ int flags
+ int managedEntArrayHandle
+}
+
+global struct BurnCardPhaseRewindStruct
+{
+ array<PhaseRewindData> phaseRetreatSavedPositions
+ bool phaseRetreatShouldSave = true
+}
+
+// This struct is hooked up to entity.e in code
+global struct ServerEntityStruct
+{
+ entity repairSoul // repair drones
+ array<void functionref( entity, var )> entKilledCallbacks
+ array<entity> fxArray
+ entity cpoint1
+ bool moverPathPrecached
+ bool blockActive = false
+ int embarkCount = 0 // For the Titan soul to know how many times a player has embarked.
+
+ entity syncedMeleeAttacker
+ entity lastSyncedMeleeAttacker
+ bool markedForExecutionDeath
+
+ bool proto_weakToPilotWeapons
+
+ bool isHotDropping
+
+ bool spawnPointInUse
+ entity lastAttacker
+
+ // sticky props
+ float spawnTime = 0.0
+ bool isStickyCrit = false
+ int stickyRoundsArrayId = -1 //script managed ent array index
+
+ table<string,AnimEventData> animEventDataForEntity
+
+ array<DamageHistoryStruct> recentDamageHistory
+
+ float lastTakeDamageTime_thermite // meteor thermite does an extra burst of damage on first contact
+ float lastTakeDamageTime_laser_cannon
+
+ // tracker rounds
+ int myTrackerRoundsIdx = -1 //script managed ent array index
+ int myReservedTrackerRoundsIdx = -1 //script managed ent array index
+ int trackerRoundsOnMeIdx = -1 //script managed ent array index
+ bool allowLifetimeDeath = true
+
+ // entities
+ float nextAllowStickyExplodeTime = 0.0
+ float stickyClearTime = 0.0
+
+ entity shieldWallFX
+ entity cpoint
+
+ // vortex rules
+ var functionref( entity, var ) BulletHitRules
+ bool functionref( entity, entity, bool ) ProjectileHitRules
+
+ // soul shield
+ float nextShieldDecayTime = 0.0
+ float forcedRegenTime = 0.0
+
+ // ball lightning
+ BallLightningData ballLightningData
+ int ballLightningTargetsIdx = -1
+ int arcPylonArrayIdx = -1 //script managed ent array index
+ float lastArcTime = 0.0
+
+ // laser tripwire
+ int laserPylonArrayIdx = -1 //script managed ent array index
+
+ //Survivor
+ int crateType
+
+ //Bomb
+ // TODO: remove hasBomb and replace with deterministic checks
+ bool hasBomb = false
+ bool destroyOutOfBounds = false
+ bool destroyTriggerHurt = false
+
+ //Rodeo
+ entity lastRodeoAttacker
+
+ array<int> smokeScreenSlowdownIdx
+
+ // number of shield beacons affecting me
+ int shieldBeaconCount
+ array<BeamEffect> shieldBeaconFXArray
+
+ //Ping
+ entity lastPlayerToSpot
+ float lastSpotTime = -9999.0
+
+ bool forceRagdollDeath = false
+
+ int projectileID
+
+ bool windPushEnabled = true
+ bool inWindTunnel
+ float windTunnelStartTime
+ float windTunnelStrength
+ vector windTunnelDirection
+
+ bool forceGibDeath = false
+
+ SpawnPointData spawnPointData
+
+ array<void functionref( entity ent, var damageInfo )> entDamageCallbacks
+ array<void functionref( entity ent, var damageInfo )> entPostDamageCallbacks
+ array<void functionref( entity titan, entity attacker )> entSegmentLostCallbacks
+ array<void functionref( entity ent, var damageInfo, float actualShieldDamage )> entPostShieldDamageCallbacks
+
+ // Used for weapons and abilities that have multiple ticks, but we only want a single tick to hit each player
+ array<entity> damagedEntities
+ bool onlyDamageEntitiesOnce = false
+ bool onlyDamageEntitiesOncePerTick = false
+ float lastDamageTickTime
+
+ array<entity> attachedEnts
+
+ //Scorch Variables
+ table< int, array<entity> > waveLinkFXTable //Wave FX Link - Used for the wide projectile attacks so we can link FX across the rows.
+ table< int, vector > fireTrapEndPositions //Used to spawn 1 particle per arm instead of many.
+ table< int, entity > fireTrapMovingGeo //Used track which piece of moving geo the fire trap arms are on
+
+ //Legion Variables
+ bool ammoSwapPlaying = false
+ bool gunShieldActive = false
+
+ int fxType
+ array<entity> fxControlPoints
+
+ int totalEntsStoredID = 0
+ int AT_BossID
+
+ ScriptTriggerData scriptTriggerData
+
+ bool noOwnerFriendlyFire = false
+
+ bool hasDefaultEnemyHighlight
+
+ bool isDisabled = false
+
+ int gameModeId
+
+ // PVE //
+ int roamerSpawnType = -1
+ int pveSpawnType = -1
+ int pveSpawnFlags = 0
+ bool roamerIsAggro = false
+ //
+ int objectiveGoalVersion = 0
+ int objectiveGoalFlags = 0 // eObjectiveTracking.*
+ /////////
+
+ array<entity> sonarTriggers
+
+ string enemyHighlight = ""
+ string burnReward = ""
+ int fd_roundDeployed = -1
+}
+
+global struct MeritData
+{
+ int scoreMeritState
+ int completionMeritState
+ int winMeritState
+ int evacMeritState
+
+ int weaponMerits
+ int titanMerits
+
+ int happyHourMeritState
+}
+
+// This struct is hooked up to entity.p in code
+global struct ServerPlayerStruct
+{
+ float connectTime
+ bool clientScriptInitialized = false
+
+ bool hasMatchLossProtection = false
+
+ bool usingLoadoutCrate
+ int activePilotLoadoutIndex = -1
+ int activeTitanLoadoutIndex = -1
+ int npcFollowersArrayID
+ entity lastFriendlyTriggerTouched
+ float titanDamageDealt // Does not include shield damage
+ float titanDamageDealt_Stat // Does not include shield damage
+ string spectreSquad
+ bool fastballActivatePressed
+ float nextATShieldRegenTime
+ bool demigod
+ bool partyMember
+
+ bool[OFFHAND_COUNT] offhandSlotLocked = [ false, false, false, false, false, false ]
+ float[OFFHAND_COUNT] lastPilotOffhandUseTime = [ -99.0, -99.0, -99.0, -99.0, -99.0, -99.0 ]
+ float[OFFHAND_COUNT] lastPilotOffhandChargeFrac = [ -1.0, -1.0, -1.0, -1.0, -1.0, -1.0 ]
+ float[OFFHAND_COUNT] lastPilotClipFrac = [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ]
+ float[OFFHAND_COUNT] lastTitanOffhandUseTime = [ -99.0, -99.0, -99.0, -99.0, -99.0, -99.0 ]
+ float[OFFHAND_COUNT] lastTitanOffhandChargeFrac = [ -1.0, -1.0, -1.0, -1.0, -1.0, -1.0 ]
+ float[OFFHAND_COUNT] lastTitanClipFrac = [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ]
+ float lastSuitPower = -1.0
+
+ entity currentTargetPlayerOrSoul_Ent
+ float currentTargetPlayerOrSoul_LastHitTime
+
+ entity lastPrimaryWeaponEnt // track when your primary changes
+
+ void functionref( entity, entity ) followPlayerOverride
+
+ float postDeathThreadStartTime
+ float lastSelectSPTitanLoadoutTime
+
+ float lastNpcSyncedMeleeVsPlayerTime = -99
+
+ bool watchingPetTitanKillReplay
+
+ bool hasSniperWeapon = false
+
+ int controllableProjectiles_scriptManagedID = -1
+
+ float lastDroneShieldStunPushTime
+
+ float lastRespawnTime
+ float lastDamageTime
+ float lastDeathTime
+ vector deathOrigin
+ vector deathAngles
+ vector rematchOrigin
+
+ entity lastKiller
+ array<float> recentPlayerKilledTimes
+ array<float> recentAllKilledTimes
+ bool seekingRevenge
+ table<entity, int> playerKillStreaks
+ int playerOrTitanKillsSinceLastDeath //This used to only be player Kills, changed primarily for challenge unlock. See 207007
+ float lastOnslaughtTime = -ONSLAUGHT_REQUIREMENT_TIME
+ float lastMayhemTime = -MAYHEM_REQUIREMENT_TIME
+
+ entity lastSpawnPoint
+
+ string lastExecutionUsed
+
+ MeritData meritData
+
+ void functionref( entity ) currViewConeFunction = null
+ RodeoPackageStruct& rodeoPackage //To assign into a nested struct, reference is needed. Less efficient than directly changing values of struct
+ bool rodeoReadyForAction = true
+ float lastClamberFailedTime = 0.0
+
+ table<int, array<void functionref( entity )> > playerMovementEventCallbacks
+ array<PlayerInputEventCallbackStruct> playerInputEventCallbacks
+ array<PlayerHeldButtonEventCallbackStruct> playerHeldButtonEventCallbacks
+ array<PlayerInputAxisEventCallbackStruct> playerInputAxisEventCallbacks
+
+ // for titan zipline
+ entity activeZiplineBolt
+ table<entity> activeZiplineEnts
+ int activeZiplineTargetID
+ string ogTitanOffhandWeaponName
+
+ array<PlayerSlowDownEffect> slowEffects
+
+ //AT Turrets
+ float PROTO_UseDebounceEndTime = 0.0 //Working around hacky implementation.
+
+ entity deployableAmmoBeacon
+
+ bool pilotLoadoutChanged
+ bool titanLoadoutChanged
+ int pilotModelNeedsUpdate = -1
+ int titanModelNeedsUpdate = -1
+
+ int lastActivatedSpreeRewardsWeaponReward
+
+ float empEndTime
+
+ int disableOffhandWeaponsStackCount
+
+ float timeTitanUpgradesStartCountingDown = -1.0
+ float timeTitanUpgradesAccumulatedPauseTime = -1.0
+
+ bool isEmbarking = false
+ bool isDisembarking = false
+ bool isCustomDisembark = false
+
+ vector ornull quickDeathOrigin = null
+ vector ornull quickDeathAngles = null
+ bool doingQuickDeath = false
+ bool quickDeathRealDeathFadesToBlack = false
+
+ int numberOfDeaths = 0
+ int numberOfDeathsSinceLastKill = 0
+
+ float lastGrappledTime
+
+ bool isReviving = false
+
+ float lastEjectTime = 0
+
+ bool showingMobilityGhost
+ float timeNearMobilityGhostHint
+
+ array<StoredWeapon> storedWeapons
+
+ bool rodeoShouldAdjustJumpOffVelocity = false
+ float rodeoRequestBatteryHintLastShownTime = 0.0
+ float batteryLastTouchedNotificationTime = 0.0
+
+ entity leechTarget = null
+ float lastLeechTypeSoundTime = -1
+ table<entity, entity> leechedEnts = {}
+
+ float lastFullHealthTime
+
+ bool isDisconnected = false
+ array<int> empStatusEffectsToClearForPhaseShift //Not great, done to avoid needing code work to get a separate empSlow/empSTurnEffects
+
+ float stats_wallrunTime = 0
+ float stats_wallhangTime = 0
+ float stats_airTime = 0
+ float[ eStatUpdateTime.COUNT ] statUpdateTimes
+ table<string, float> lastPlayerDidDamageTimes
+ bool rewardedMatchCredit = false
+
+ bool lastPosForDistanceStatValid = false
+ vector lastPosForDistanceStat
+
+ bool pilotEjecting = false
+ float pilotEjectStartTime
+ float pilotEjectEndTime
+
+ array<int> deathHintViews
+
+ float earnMeterOwnedFrac
+ float earnMeterOverdriveFrac
+ float earnMeterRewardFrac
+
+ BurnCardPhaseRewindStruct burnCardPhaseRewindStruct
+ array<entity> rodeoAnimTempProps
+
+ bool controllingTurret = false
+
+ int pveTacticalType = -1
+ int pveTacticalLevel = -1
+
+ array<RecentUnlock> challengeUnlocks
+
+ float lastDpadSayTime = -999
+ int consecutiveDpadMessages = 0
+ float replacementTitanETATimer = 0
+ float replacementTitanReady_lastNagTime = 0
+
+ int turretArrayId = -1
+
+ float hotStreakTime = 0.0
+}
+
+global struct TitanSettings
+{
+ string titanSetFile = ""
+ array<string> titanSetFileMods
+}
+
+global struct NPCDefaultWeapon
+{
+ string wep
+ array<string> mods
+}
+
+// This struct is hooked up to entity.ai in code
+global struct ServerAIStruct
+{
+ TitanSettings titanSettings
+
+ TitanLoadoutDef& titanSpawnLoadout
+
+ vector spawnOrigin
+
+ NPCDefaultWeapon ornull mySpawnOptions_weapon
+
+ string droneSpawnAISettings
+
+ float startCrawlingTime
+ bool crawling = false
+ bool transitioningToCrawl = false
+ bool preventOwnerDamage
+ bool invulnerableToNPC = false
+ bool buddhaMode
+ bool killShotSound = true
+
+ table<int,int> stalkerHitgroupDamageAccumulated // used to decide when to blow off limbs
+ table<int,float> stalkerHitgroupLastHitTime // used to decide when to blow off limbs
+
+ bool fragDroneArmed = true
+ entity suicideSpectreExplodingAttacker
+ float suicideSpectreDefaultExplosionDelay
+ float suicideSpectreExplosionDelay
+ float suicideSpectreExplosionDistance
+ float suicideSpectreExplosionTraceTime
+
+ bool superSpectreEnableFragDrones = true
+ int fragDroneMin = 0
+ int fragDroneMax = 0
+ int fragDroneBatch = 0
+ int activeMinionEntArrayID = -1
+
+ bool readyToFire = true
+
+ float nextRegenTime
+ float nextAllowAnnounceTime
+
+ bool enableFriendlyFollower = true
+ entity lastFriendlyTrigger
+
+
+ bool leechInProgress = false
+ float leechStartTime = -1
+
+ //Marvins
+ entity carryBarrel
+ entity mortarTarget
+
+ int bossTitanType
+ bool bossTitanVDUEnabled = true
+ bool bossTitanPlayIntro = true
+ int mercCharacterID
+ string bossCharacterName
+
+ int killCount
+ int scoreCount
+
+ bool shouldDropBattery = true
+ int nukeCore = 0
+
+ int playerDoomedProficiency
+ int defaultProficiency
+
+ string dropshipSpawnStyle = ""
+ float spawnTime
+}
+
+
+// hooked up to entity.w in code
+global struct ServerWeaponStruct
+{
+ float startChargeTime = 0.0
+ bool wasCharged = false
+ bool initialized = false
+ entity lastProjectileFired
+ array vortexImpactData
+
+ array<PhaseRewindData> phaseRetreatSavedPositions
+ bool phaseRetreatShouldSave = true
+
+ entity laserWorldModel
+ entity guidedMissileTarget = null
+ array<entity> salvoMissileArray
+
+ entity weaponOwner
+ entity bubbleShield
+ array<int> statusEffects
+ array<entity> fxHandles
+ float lastFireTime
+
+ table< entity, int> targetLockEntityStatusEffectID
+ void functionref(entity, entity) missileFiredCallback
+ int savedKillCount
+}
+
+
+global struct RemoteTurretSettings
+{
+ vector turretOrigin
+ vector turretAngles
+ vector panelOrigin
+ vector panelAngles
+
+ asset turretModel
+ asset panelModel
+
+ string turretSettingsName
+ string weaponName
+
+ bool viewClampEnabled
+ float viewClampRangeYaw
+ float viewClampRangePitch
+ float viewStartPitch
+}
+
+// hooked up to entity.remoteturret in code
+global struct ServerRemoteTurretStruct
+{
+ RemoteTurretSettings ornull settings
+ entity controlPanel
+ int statusEffectID
+}
+
+
+// hooked up to entity.proj in code
+global struct ServerProjectileStruct
+{
+ bool isChargedShot = false
+ float damageScale = 1.0
+ bool onlyAllowSmartPistolDamage = false
+ bool selfPropelled = true
+ bool startPlanting = false
+ entity trackedEnt
+ int projectileBounceCount = 0
+ array<entity> projectileGroup
+ int projectileID
+ bool tetherAttached
+ vector savedOrigin
+ vector savedRelativeDelta
+ entity savedMovingGeo
+ vector savedAngles
+ entity inflictorOverride
+ bool hasBouncedOffVortex = false
+ bool isPlanted = false
+}
+
+// hooked up to entity.soul in code
+global struct ServerTitanSoulStruct
+{
+ bool rebooting = false
+ float lastSegmentLossTime = 0.0
+ float batteryTime
+ entity bubbleShield
+ NPCPilotStruct seatedNpcPilot
+ bool skipDoomState
+ bool regensHealth = true
+ bool diesOnEject = true
+ float doomedStartTime = 0.0
+ entity batteryContainer = null
+ entity armBadge = null
+ bool batteryContainerBeingUsed = false
+ bool batteryContainerPastPointOfNoReturn = false
+ entity lastOwner
+ int upgradeCount = 0
+ TitanLoadoutDef& titanLoadout
+ entity nukeAttacker = null
+ bool batteryMovedDown = false
+}
+
+// hooked up to entity.decoy in code
+global struct ServerPlayerDecoyStruct
+{
+ array< entity > fxHandles
+ array< string > loopingSounds
+}
+
+// hooked up to entity.sp in code
+global struct ServerSpawnpointStruct
+{
+ bool enabled
+ float lastUsedTime
+ array< int > zones
+ array< entity > visibleToTurret
+}
+
+global struct ServerFirstPersonProxyStruct
+{
+ entity battery
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_global_entities.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_global_entities.gnut
new file mode 100644
index 00000000..767436d9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_global_entities.gnut
@@ -0,0 +1,343 @@
+untyped
+
+globalize_all_functions
+
+//=========================================================
+// _global_entities
+// Create/initialize various global entities
+//=========================================================
+
+
+// if you set this to zero, various things that spam developer 2 won't run
+const SPAMS_DEVELOPER2 = 1
+
+void function MP_PlayerPostInit( entity self )
+{
+ entity player = self
+ Assert( !player.hasSpawned )
+
+ player.InitMPClasses()
+
+ player.hasSpawned = true
+}
+
+array _PlayerDidSpawnCallbacks = []
+
+function __PlayerDidSpawn( entity player )
+{
+ if ( player.GetPlayerName() == "Replay" )
+ return
+
+ foreach( callback in _PlayerDidSpawnCallbacks )
+ {
+ thread callback()
+ }
+
+ svGlobal.levelEnt.Signal( "PlayerDidSpawn", { player=player } )
+ FlagSet( "PlayerDidSpawn" )
+}
+
+function AddCallback_PlayerDidSpawn( callback )
+{
+ _PlayerDidSpawnCallbacks.append( callback )
+}
+
+function ClientCommand( client, command, delay = 0 )
+{
+ EntFireByHandle( _cc, "Command", command, delay, client, null )
+}
+
+function ServerCommand( command, delay = 0 )
+{
+ EntFireByHandle( _sc, "Command", command, delay, null, null )
+}
+
+table __trackedRefs = {}
+
+function AddTrackRef( ref )
+{
+ //printl( "Adding ref for: " + string( ref ) )
+ __trackedRefs[string( ref )] <- ref.weakref()
+}
+
+function RefTrackerThink()
+{
+ foreach ( refName, entity refObj in __trackedRefs )
+ {
+ if ( !refObj || !refObj.ref() )
+ {
+ delete __trackedRefs[refName]
+ if ( SPAMS_DEVELOPER2 )
+ svGlobal.levelEnt.Fire( "CallScriptFunction", "RefTrackerThink", 0.033 )
+ return
+ }
+
+ if ( IsValid_ThisFrame( refObj ) )
+ continue
+
+ printl( "UNFREED REFERENCE (use weakref for entities): " + refName )
+ __trackedRefs[ refName ] = null
+ }
+
+ if ( SPAMS_DEVELOPER2 )
+ svGlobal.levelEnt.Fire( "CallScriptFunction", "RefTrackerThink", 2.0 )
+}
+
+function DumpTrackRefs()
+{
+ foreach ( refName, refObj in __trackedRefs )
+ {
+ if ( !refObj || !refObj.ref() )
+ continue
+
+ printl( "TRACKREF: " + refName + " " + refObj.ref() )
+
+ }
+}
+
+function MapRequiresFullFlightpathSupport()
+{
+ return false //GetMapName().find( "mp_" ) == 0
+}
+
+function AINFileIsUpToDate_Wrapper()
+{
+ if ( GetAINScriptVersion() != AIN_REV )
+ return false
+
+ return AINFileIsUpToDate()
+}
+
+
+function Hud_Hide( __t__, __tt__ )
+{
+}
+
+function Hud_Show( __t__, __tt__ )
+{
+}
+
+function PathsOutOfDate( player )
+{
+ SendHudMessage( player, "Paths Out of Date. Type buildainfile at console.", -1, 0.4, 255, 255, 0, 255, 0.0, 0.5, 0.0 )
+ // , x_pos, y_pos, R, G, B, A, fade_in_time, hold_time, fade_out_time )
+}
+
+function NavmeshOutOfDate( player )
+{
+ SendHudMessage( player, "Navmesh Out of Date. Build in LevelEd", -1, 0.6, 192, 255, 0, 255, 0.0, 0.5, 0.0 )
+ // , x_pos, y_pos, R, G, B, A, fade_in_time, hold_time, fade_out_time )
+}
+
+
+function NavmeshUpToDateCheck()
+{
+ FlagWait( "PlayerDidSpawn" )
+
+ if ( NavMesh_IsUpToDate() )
+ return
+
+ for ( int i = 0; i < 5; i++ )
+ {
+ wait 1
+
+ array<entity> players = GetPlayerArray()
+ // let's not spam the whole server
+ if ( players.len() )
+ NavmeshOutOfDate( players[0] )
+ }
+}
+
+
+// paths out of date
+function AINFileIsUpToDateCheck()
+{
+ FlagWait( "PlayerDidSpawn" )
+
+ if ( AINFileIsUpToDate_Wrapper() )
+ return
+
+ if ( !AINExists() )
+ return
+
+ for ( int i = 0; i < 5; i++ )
+ {
+ wait 1
+
+ array<entity> players = GetPlayerArray()
+ // let's not spam the whole server
+ if ( players.len() )
+ PathsOutOfDate( players[0] )
+ }
+}
+
+function PlayerSeesGraphWarning( player )
+{
+ player.EndSignal( "OnDestroy" )
+ local i
+ float minWait = 0.03
+ float maxWait = 0.7
+ local result
+ int max = 15
+ for ( i = 0; i < max; i++ )
+ {
+ result = Graph( i, 0, max, maxWait, minWait )
+
+ wait result
+ if ( !IsValid( player ) )
+ return
+
+ if ( GetNPCArray().len() == 0 )
+ continue
+
+ wait result * 0.5
+ if ( !IsValid( player ) )
+ return
+
+ PathsOutOfDate( player )
+ }
+
+ for ( ;; )
+ {
+ wait 0.5
+ if ( !IsValid( player ) )
+ return
+
+ if ( GetNPCArray().len() == 0 )
+ continue
+
+ wait 0.3
+ if ( !IsValid( player ) )
+ return
+
+ PathsOutOfDate( player )
+ }
+}
+
+
+
+// Look up and set damageSourceIds for environmental damage triggers
+// This works this way so maps don't have to be recompiled if any damageSourceIds change
+void function InitDamageTriggers( entity self )
+{
+ if ( !self.HasKey( "damageSourceName" ) )
+ return
+
+ switch ( self.GetValueForKey( "damageSourceName" ) )
+ {
+ case "fall":
+ self.kv.damageSourceId = eDamageSourceId.fall
+ break
+
+ case "splat":
+ self.kv.damageSourceId = eDamageSourceId.splat
+ break
+
+ case "burn":
+ self.kv.damageSourceId = eDamageSourceId.burn
+ break
+
+ case "submerged":
+ self.kv.damageSourceId = eDamageSourceId.submerged
+ break
+
+ case "electric_conduit":
+ self.kv.damageSourceId = eDamageSourceId.electric_conduit
+ break
+
+ case "turbine":
+ self.kv.damageSourceId = eDamageSourceId.turbine
+ break
+
+ case "lasergrid":
+ self.kv.damageSourceId = eDamageSourceId.lasergrid
+ break
+
+ case "crush":
+ self.kv.damageSourceId = eDamageSourceId.damagedef_crush
+ break
+
+ case "toxic_sludge":
+ self.kv.damageSourceId = eDamageSourceId.toxic_sludge
+ break
+
+ default:
+ Assert( false, "Unsupported damage source name on trigger_hurt: " + self.GetValueForKey( "damageSourceName" ) )
+ }
+}
+
+bool function IsDamageFromDamageTrigger( damageInfo )
+{
+ switch ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) )
+ {
+ case eDamageSourceId.fall:
+ case eDamageSourceId.splat:
+ case eDamageSourceId.burn:
+ case eDamageSourceId.submerged:
+ return true
+ }
+
+ return false
+}
+
+function ScriptLeakDetector()
+{
+ svGlobal.levelEnt.Signal( "GameEnd" )
+ OnThreadEnd(
+ function() : ()
+ {
+ TotalEnts()
+ }
+ )
+
+ WaitFrame()
+ TotalEnts()
+
+ for ( ;; )
+ {
+ wait 60
+ TotalEnts()
+ }
+}
+
+void function NavmeshSeparatorThink( entity separator )
+{
+ bool connected = true
+ if ( separator.HasKey( "startDisconnected" ) && separator.kv.startDisconnected == "1" )
+ connected = false
+
+ ToggleNPCPathsForEntity( separator, connected )
+ if ( connected )
+ separator.NotSolid()
+ else
+ separator.Solid()
+
+ if ( separator.HasKey( "script_flag" ) && separator.kv.script_flag != "" )
+ {
+ string flag = string( separator.kv.script_flag )
+ FlagInit( flag, connected )
+
+ while( IsValid( separator ) )
+ {
+ if ( connected )
+ FlagWaitClear( flag )
+ else
+ FlagWait( flag )
+
+ connected = !connected
+ ToggleNPCPathsForEntity( separator, connected )
+ if ( connected )
+ separator.NotSolid()
+ else
+ separator.Solid()
+ }
+ }
+}
+
+void function DevDebugText( entity node )
+{
+ Assert( node.HasKey( "text" ) && node.kv.text != "", "info_debug_text at " + node.GetOrigin() + " doesn't have text set on it." )
+ // Debug text doesn't work right away becuase of code, this delay makes them show up
+ wait 3.0
+ DebugDrawText( node.GetOrigin(), string( node.kv.text ), true, 999999.9 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_harvester.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_harvester.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_harvester.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_health_regen.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_health_regen.gnut
new file mode 100644
index 00000000..ded25dc3
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_health_regen.gnut
@@ -0,0 +1,172 @@
+
+global function HealthRegen_Init
+
+global function PilotHealthRegenThinkSP
+global function PilotShieldHealthUpdate
+
+struct
+{
+ float healthRegenRate
+} file
+
+void function HealthRegen_Init()
+{
+ if ( IsSingleplayer() )
+ {
+ file.healthRegenRate = 1.0
+ }
+ else
+ {
+ file.healthRegenRate = 6.0
+ AddCallback_PlayerClassChanged( HealthRegen_OnPlayerClassChangedMP )
+ RegisterSignal( "PilotHealthRegenThink" )
+ }
+}
+
+void function PilotHealthRegenThinkSP( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ while ( IsValid( player ) )
+ {
+ wait( HEALTH_REGEN_TICK_TIME )
+
+ if ( !IsAlive( player ) )
+ continue
+
+ if ( !IsPilot( player ) )
+ continue
+
+ if ( shGlobal.proto_pilotHealthRegenDisabled )
+ continue
+
+ //Assert( IsTestMap() || player.GetPlayerSettings() == DEFAULT_PILOT_SETTINGS, "for now, we should all be pilot_solo at all times, or in a test map." )
+
+ if ( player.GetHealth() == player.GetMaxHealth() )
+ continue
+
+ float healthRegenRate = 4.0
+ float healthRegenStartDelay = GraphCapped( player.GetHealth(), 0, player.GetMaxHealth(), 3.0, 0.8 )
+
+ //printt( "recentDamage " + recentDamage + " delay " + healthRegenStartDelay + " rate " + healthRegenRate )
+
+ if ( Time() - player.p.lastDamageTime < healthRegenStartDelay )
+ {
+ continue
+ }
+
+ player.SetHealth( min( player.GetMaxHealth(), player.GetHealth() + healthRegenRate ) )
+ }
+}
+
+bool function IsHealActive( entity player )
+{
+ return StatusEffect_Get( player, eStatusEffect.stim_visual_effect ) > 0.0
+}
+
+void function PilotHealthRegenThinkMP( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+ player.Signal( "PilotHealthRegenThink" )
+ player.EndSignal( "PilotHealthRegenThink" )
+
+ //float healthRegenStartDelay = player.GetPlayerSettingsField( "powerRegenRateOp" ) // seconds after we take damager to start regen
+ float healthRegenStartDelay = 5.0 //Needs to use GetPlayerSettingsField() instead of hard coding, waiting on Bug 129567
+ if ( PlayerHasPassive( player, ePassives.PAS_FAST_HEALTH_REGEN ) )
+ healthRegenStartDelay = 2.5
+
+ while ( IsValid( player ) )
+ {
+ wait( HEALTH_REGEN_TICK_TIME )
+
+ if ( !IsAlive( player ) )
+ continue
+
+ if ( !IsPilot( player ) )
+ continue
+
+ if ( shGlobal.proto_pilotHealthRegenDisabled )
+ continue
+
+ float healthRegenRate = file.healthRegenRate // health regen per tick
+
+ if ( player.GetHealth() == player.GetMaxHealth() )
+ continue
+
+ // No regen during phase shift
+ if ( player.IsPhaseShifted() )
+ continue
+
+ if ( IsHealActive( player ) )
+ {
+ if ( Time() - player.p.lastDamageTime < min( ABILITY_STIM_REGEN_DELAY, healthRegenStartDelay ) )
+ continue
+ else
+ healthRegenRate = healthRegenRate * ABILITY_STIM_REGEN_MOD
+ }
+ else if ( Time() - player.p.lastDamageTime < healthRegenStartDelay )
+ {
+ continue
+ }
+
+ player.SetHealth( min( player.GetMaxHealth(), player.GetHealth() + healthRegenRate ) )
+ if ( player.GetHealth() == player.GetMaxHealth() )
+ {
+ ClearRecentDamageHistory( player )
+ ClearLastAttacker( player )
+ }
+ }
+}
+
+void function HealthRegen_OnPlayerClassChangedMP( entity player )
+{
+ thread PilotHealthRegenThinkMP( player )
+}
+
+float function PilotShieldHealthUpdate( entity player, var damageInfo )
+{
+ if ( DamageInfo_GetForceKill( damageInfo ) )
+ {
+ player.SetShieldHealth( 0 )
+ return 0.0
+ }
+
+ int shieldHealth = player.GetShieldHealth()
+
+ float shieldDamage = 0
+
+ if ( shieldHealth )
+ {
+ DamageInfo_AddCustomDamageType( damageInfo, DF_SHIELD_DAMAGE )
+
+ shieldDamage = PilotShieldModifyDamage( player, damageInfo )
+
+ if ( shieldDamage )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ }
+
+ return shieldDamage
+}
+
+float function PilotShieldModifyDamage( entity player, var damageInfo )
+{
+ float shieldHealth = float( player.GetShieldHealth() )
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ float newShieldHealth = shieldHealth - damage
+ float permanentDamage = 0.0
+
+ if ( newShieldHealth < 0 )
+ permanentDamage = fabs( newShieldHealth )
+
+ player.SetShieldHealth( maxint( 0, int( newShieldHealth ) ) )
+
+ if ( shieldHealth && newShieldHealth <= 0 )
+ {
+ EmitSoundOnEntity( player, "titan_energyshield_down" )
+ }
+
+ DamageInfo_SetDamage( damageInfo, permanentDamage )
+
+ return min( shieldHealth, damage )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_init.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_init.gnut
new file mode 100644
index 00000000..fc9fe2b9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_init.gnut
@@ -0,0 +1,40 @@
+#if DEV
+untyped
+#endif
+
+//=========================================================
+// _init
+// Called on newgame or transitions, AFTER entities have been created and initialized
+//=========================================================
+
+global function CodeCallback_PostEntityInit
+
+bool _initialized = false
+
+void function CodeCallback_PostEntityInit()
+{
+ printl( "Code Script: _init" )
+
+ // prevent save/load code from running global scripts again
+ Assert( !_initialized )
+ _initialized = true
+
+ RunCallbacks_EntitiesDidLoad()
+
+ FlagInit( "EntitiesDidLoad" )
+ FlagSet( "EntitiesDidLoad" )
+
+ array<entity> exfilPanels = GetEntArrayByClass_Expensive( "prop_exfil_panel" )
+ foreach ( panel in exfilPanels )
+ panel.Destroy()
+
+ // regexp unit tests
+ Assert( regexp( "^foo.*bar$" ).match( "foobar" ) )
+ Assert( !regexp( "^foo.+bar$" ).match( "foobar" ) )
+ Assert( regexp( "^foo.*bar$" ).match( "fooxbar" ) )
+ Assert( regexp( "^foo.+bar$" ).match( "fooxbar" ) )
+ Assert( regexp( "^foo.*$" ).match( "foo" ) )
+ Assert( !regexp( "^foo.+$" ).match( "foo" ) )
+ Assert( regexp( "^foo.*$" ).match( "foon" ) )
+ Assert( regexp( "^foo.+$" ).match( "foon" ) )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut
new file mode 100644
index 00000000..2b7b90b3
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut
@@ -0,0 +1,261 @@
+untyped
+global function SvLoadoutsMP_Init
+
+global function SetLoadoutGracePeriodEnabled
+global function SetWeaponDropsEnabled
+global function GetTitanLoadoutForPlayer
+
+struct {
+ bool loadoutGracePeriodEnabled = true
+ bool weaponDropsEnabled = true
+ array<entity> dirtyLoadouts
+} file
+
+void function SvLoadoutsMP_Init()
+{
+ InitDefaultLoadouts() // titan loadout code relies on this, not called on server by default
+
+ // most of these are fairly insecure right now, could break pdata if called maliciously, need fixing eventually
+ RegisterSignal( "EndUpdateCachedLoadouts" )
+ RegisterSignal( "GracePeriodDone" ) // temp to get weapons\_weapon_utility.nut:2271 to behave
+
+ AddCallback_OnClientConnected( UpdateCallsignOnConnect )
+
+ AddClientCommandCallback( "RequestPilotLoadout", ClientCommandCallback_RequestPilotLoadout )
+ AddClientCommandCallback( "RequestTitanLoadout", ClientCommandCallback_RequestTitanLoadout )
+ AddClientCommandCallback( "SetPersistentLoadoutValue", ClientCommandCallback_SetPersistentLoadoutValue )
+ AddClientCommandCallback( "SwapSecondaryAndWeapon3PersistentLoadoutData", ClientCommandCallback_SwapSecondaryAndWeapon3PersistentLoadoutData )
+ AddClientCommandCallback( "SetBurnCardPersistenceSlot", ClientCommandCallback_SetBurnCardPersistenceSlot )
+
+ if ( IsLobby() ) // can't usually set these in real games
+ {
+ AddClientCommandCallback( "SetCallsignIcon", ClientCommandCallback_SetCallsignIcon )
+ AddClientCommandCallback( "SetCallsignCard", ClientCommandCallback_SetCallsignCard )
+ AddClientCommandCallback( "SetFactionChoicePersistenceSlot", ClientCommandCallback_SetFactionChoicePersistenceSlot )
+ }
+ else
+ {
+ AddClientCommandCallback( "InGameMPMenuClosed", ClientCommandCallback_InGameMPMenuClosed )
+ AddClientCommandCallback( "LoadoutMenuClosed", ClientCommandCallback_LoadoutMenuClosed )
+ }
+
+ AddCallback_OnPlayerKilled( DestroyDroppedWeapon )
+}
+
+void function SetLoadoutGracePeriodEnabled( bool enabled )
+{
+ file.loadoutGracePeriodEnabled = enabled
+}
+
+void function SetWeaponDropsEnabled( bool enabled )
+{
+ file.weaponDropsEnabled = enabled
+}
+
+void function DestroyDroppedWeapon( entity victim, entity attacker, var damageInfo )
+{
+ if ( !file.weaponDropsEnabled && IsValid( victim.GetActiveWeapon() ) )
+ victim.GetActiveWeapon().Destroy()
+}
+
+TitanLoadoutDef function GetTitanLoadoutForPlayer( entity player )
+{
+ SetActiveTitanLoadout( player ) // set right loadout
+
+ // fix bug with titan weapons having null mods
+ // null mods aren't valid and crash if we try to give them to npc
+ TitanLoadoutDef def = GetActiveTitanLoadout( player )
+ def.primaryMods.removebyvalue( "null" )
+
+ return def
+}
+
+void function UpdateCallsignOnConnect( entity player )
+{
+ // these netints are required for callsigns and such to display correctly on other clients
+ player.SetPlayerNetInt( "activeCallingCardIndex", player.GetPersistentVarAsInt( "activeCallingCardIndex" ) )
+ player.SetPlayerNetInt( "activeCallsignIconIndex", player.GetPersistentVarAsInt( "activeCallsignIconIndex" ) )
+}
+
+// loadout clientcommands
+bool function ClientCommandCallback_RequestPilotLoadout( entity player, array<string> args )
+{
+ if ( args.len() != 1 )
+ return true
+
+ print( player + " RequestPilotLoadout " + args[0] )
+
+ // insecure, could be used to set invalid spawnloadout index potentially
+ SetPersistentSpawnLoadoutIndex( player, "pilot", args[0].tointeger() )
+
+ SetPlayerLoadoutDirty( player )
+
+ return true
+}
+
+bool function ClientCommandCallback_RequestTitanLoadout( entity player, array<string> args )
+{
+ if ( args.len() != 1 )
+ return true
+
+ print( player + " RequestTitanLoadoutLoadout " + args[0] )
+
+ // insecure, could be used to set invalid spawnloadout index potentially
+ SetPersistentSpawnLoadoutIndex( player, "titan", args[0].tointeger() )
+
+ if ( !IsLobby() )
+ EarnMeterMP_SetTitanLoadout( player )
+
+ return true
+}
+
+bool function ClientCommandCallback_SetPersistentLoadoutValue( entity player, array<string> args )
+{
+ //if ( args.len() != 4 )
+ // return true
+
+ if ( args.len() < 4 )
+ return true
+
+ string val = args[ 3 ]
+ if ( args.len() > 4 ) // concat args after 3 into last arg so we can do strings with spaces and such
+ for ( int i = 4; i < args.len(); i++ )
+ val += " " + args[ i ]
+
+ val = strip( val ) // remove any tailing whitespace
+
+ print( player + " SetPersistentLoadoutValue " + args[0] + " " + args[1] + " " + args[2] + " " + val )
+
+ // VERY temp and insecure
+ SetPersistentLoadoutValue( player, args[0], args[1].tointeger(), args[2], val )
+
+ if ( args[0] == "pilot" )
+ SetPlayerLoadoutDirty( player )
+
+ return true
+}
+
+bool function ClientCommandCallback_SwapSecondaryAndWeapon3PersistentLoadoutData( entity player, array<string> args )
+{
+ if ( args.len() != 1 )
+ return true
+
+ print( "SwapSecondaryAndWeapon3PersistentLoadoutData " + args[0] )
+
+ // get loadout
+ int index = args[0].tointeger()
+ PilotLoadoutDef loadout = GetPilotLoadoutFromPersistentData( player, index )
+
+ // swap loadouts
+ // is this a good way of doing it? idk i think this is the best way of doing it
+ // can't use validation because when you swap, you'll have a secondary/weapon3 in 2 slots at once at one point, which fails validation
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "secondary", loadout.weapon3 )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "secondaryMod1", loadout.weapon3Mod1 )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "secondaryMod2", loadout.weapon3Mod2 )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "secondaryMod3", loadout.weapon3Mod3 )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "secondarySkinIndex", loadout.weapon3SkinIndex.tostring() )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "secondaryCamoIndex", loadout.weapon3CamoIndex.tostring() )
+
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "weapon3", loadout.secondary )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "weapon3Mod1", loadout.secondaryMod1 )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "weapon3Mod2", loadout.secondaryMod2 )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "weapon3Mod3", loadout.secondaryMod3 )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "weapon3SkinIndex", loadout.secondarySkinIndex.tostring() )
+ SetPlayerPersistentVarWithoutValidation( player, "pilot", index, "weapon3CamoIndex", loadout.secondaryCamoIndex.tostring() )
+
+ SetPlayerLoadoutDirty( player )
+
+ return true
+}
+
+bool function ClientCommandCallback_SetBurnCardPersistenceSlot( entity player, array<string> args )
+{
+ if ( args.len() != 1 || GetGameState() >= eGameState.Playing )
+ return true
+
+ print( player + " SetBurnCardPersistenceSlot " + args[0] )
+
+ // insecure, could be used to set invalid burnmeterslot potentially
+ if ( IsRefValidAndOfType( args[0], eItemTypes.BURN_METER_REWARD ) )
+ player.SetPersistentVar( "burnmeterSlot", BurnReward_GetByRef( args[0] ).id )
+ else
+ print( player + " invalid ref " + args[0] )
+
+ return true
+}
+
+// lobby clientcommands
+bool function ClientCommandCallback_SetCallsignIcon( entity player, array<string> args )
+{
+ print( player + " SetCallsignIcon " + args[0] )
+
+ if ( IsRefValidAndOfType( args[0], eItemTypes.CALLSIGN_ICON ) )
+ PlayerCallsignIcon_SetActiveByRef( player, args[0] )
+ else
+ print( player + " invalid ref " + args[0] )
+
+ return true
+}
+
+bool function ClientCommandCallback_SetCallsignCard( entity player, array<string> args )
+{
+ print( player + " SetCallsignIcon " + args[0] )
+
+ if ( IsRefValidAndOfType( args[0], eItemTypes.CALLING_CARD ) )
+ PlayerCallingCard_SetActiveByRef( player, args[0] )
+ else
+ print( player + " invalid ref " + args[0] )
+
+ return true
+}
+
+bool function ClientCommandCallback_SetFactionChoicePersistenceSlot( entity player, array<string> args )
+{
+ print( player + " SetFactionChoicePersistenceSlot " + args[0] )
+
+ if ( IsRefValidAndOfType( args[0], eItemTypes.FACTION ) )
+ player.SetPersistentVar( "factionChoice", args[0] ) // no function for this so gotta set directly lol
+
+ return true
+}
+
+bool function ClientCommandCallback_LoadoutMenuClosed( entity player, array<string> args )
+{
+ SavePdataForEntityIndex( player.GetPlayerIndex() )
+ TryGivePilotLoadoutForGracePeriod( player )
+ return true
+}
+
+bool function ClientCommandCallback_InGameMPMenuClosed( entity player, array<string> args )
+{
+ SavePdataForEntityIndex( player.GetPlayerIndex() )
+ //TryGivePilotLoadoutForGracePeriod( player )
+ return true
+}
+
+bool function IsRefValidAndOfType( string ref, int itemType )
+{
+ return IsRefValid( ref ) && GetItemType( ref ) == itemType
+}
+
+void function SetPlayerLoadoutDirty( entity player )
+{
+ if ( file.loadoutGracePeriodEnabled || player.p.usingLoadoutCrate )
+ file.dirtyLoadouts.append( player )
+}
+
+void function TryGivePilotLoadoutForGracePeriod( entity player )
+{
+ if ( !IsLobby() && file.dirtyLoadouts.contains( player ) )
+ {
+ file.dirtyLoadouts.remove( file.dirtyLoadouts.find( player ) )
+
+ if ( Time() - player.s.respawnTime <= CLASS_CHANGE_GRACE_PERIOD || player.p.usingLoadoutCrate )
+ {
+ Loadouts_TryGivePilotLoadout( player )
+ player.p.usingLoadoutCrate = false
+ }
+ else
+ SendHudMessage( player, "#LOADOUT_CHANGE_NEXT_BOTH", -1, 0.4, 255, 255, 255, 255, 0.15, 3.0, 0.5 ) // like 90% sure this is innacurate lol
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_mapspawn.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_mapspawn.gnut
new file mode 100644
index 00000000..3efee093
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_mapspawn.gnut
@@ -0,0 +1,217 @@
+//=========================================================
+// _mapspawn.nut
+// Called on newgame or transitions, BEFORE entities have been created and initialized
+//=========================================================
+
+global function CodeCallback_MapSpawn
+global function CodeCallback_ClientCommand
+global table _ClientCommandCallbacks = {}
+global entity _cc = null
+global entity _sc = null
+
+global struct spawnCallbackFuncArray
+{
+ array<void functionref( entity )> callbackArray
+ string entityClassname
+}
+
+global struct spawnCallbackFuncArray_scriptNoteworthy
+{
+ array<void functionref( entity )> callbackArray
+ string scriptNoteworthy
+}
+
+global struct spawnCallbackEditorClassFuncArray
+{
+ array<void functionref( entity )> callbackArray
+ string entityClassname
+ string entityEditorClassname
+}
+
+global typedef pilotEliminationDialogueCallbackType void functionref( int, array<entity>, int, array<entity> )
+
+global struct SvGlobals
+{
+ entity worldspawn
+
+ array<spawnCallbackFuncArray> spawnCallbackFuncs
+ array<spawnCallbackEditorClassFuncArray> spawnCallbackEditorClassFuncs
+ array<spawnCallbackFuncArray_scriptNoteworthy> spawnCallbackFuncs_scriptNoteworthy
+
+ table<string, array<void functionref( entity )> > spawnCallbacks_scriptName
+
+ array<pilotEliminationDialogueCallbackType> pilotEliminationDialogueCallbacks
+ table<string, array<bool functionref( entity player, entity healthpack)> > onTouchHealthKitCallbacks
+ array<void functionref( entity )> onClientConnectedCallbacks
+ array<void functionref(entity)> onPlayerRespawnedCallbacks
+ array<void functionref( entity player, entity npc_titan )> onPilotBecomesTitanCallbacks
+ array<void functionref( entity player, entity npc_titan )> onTitanBecomesPilotCallbacks
+ array<void functionref( entity, entity, entity) > soulTransferFuncs
+ array<void functionref( entity titanSoul )> soulSettingsChangeFuncs
+ array<void functionref( entity titanSoul )> soulInitFuncs
+ table<string, array<void functionref( entity, var )> > damageByCallbacks
+
+ bool functionref( entity ) gameModeAbandonPenaltyApplies
+
+ bool functionref() timelimitCompleteFunc
+ bool functionref( entity ) titanAvailabilityCheck
+ bool cloakBreaksOnMelee = true //Reexamine if still needed if we have same behavior for cloak in MP/SP.
+ float defaultPilotLeechTime = 2.8
+ int winReason
+ string winReasonText
+ string lossReasonText
+ string gameWonAnnouncement
+ string gameLostAnnouncement
+
+ table< int, int > npcsSpawnedThisFrame_scriptManagedArray
+
+ float pilotRespawnDelay = 0.0
+
+ array<void functionref( entity, var )> soulDeathFuncs
+
+ table<string, void functionref(entity)> globalAnimEventCallbacks
+
+ array<void functionref( entity titan, TitanLoadoutDef newTitanLoadout )> onTitanGetsNewLoadoutCallbacks
+ array<void functionref( entity player, PilotLoadoutDef newTitanLoadout )> onPlayerGetsNewPilotLoadoutCallbacks
+ array<void functionref( TitanLoadoutDef newTitanLoadout )> onUpdateDerivedTitanLoadoutCallbacks
+ array<void functionref( entity player, TitanLoadoutDef newTitanLoadout )> onUpdateDerivedPlayerTitanLoadoutCallbacks
+ array<void functionref( PilotLoadoutDef newPilotLoadout )> onUpdateDerivedPilotLoadoutCallbacks
+
+ array<void functionref( entity victim, entity attacker, var damageInfo )> onPlayerKilledCallbacks
+ array<void functionref( entity victim, entity attacker, var damageInfo )> onNPCKilledCallbacks
+
+ array<void functionref( entity victim, var damageInfo )> onTitanDoomedCallbacks
+ array<void functionref( entity victim, entity attacker )> onTitanHealthSegmentLostCallbacks
+ array<void functionref( entity player )> onClientConnectingCallbacks
+ array<void functionref( entity player )> onClientDisconnectedCallbacks
+ array<void functionref( entity attacker, entity victim )> onPlayerAssistCallbacks
+
+ array<void functionref( entity player )> onPlayerDropsScriptedItemsCallbacks
+ array<void functionref( entity player )> onPlayerClassChangedCallbacks
+
+ array<void functionref( entity ship, string anim )> onWaveSpawnDropshipSpawned
+
+ table<string, array<void functionref( entity ent )> >onEntityChangedTeamCallbacks
+
+ table<string, bool functionref( entity player, array<string>args )> clientCommandCallbacks
+ array<void functionref()>[ eGameState._count_ ] gameStateEnterCallbacks
+
+ bool allowPointsOverLimit = false
+
+ bool bubbleShieldEnabled = true
+
+ entity levelEnt
+
+ //TODO: Get rid of these and use the new StartParticleEffectInWorld_ReturnEntity etc functions
+ entity fx_CP_color_enemy //Used for setting control points on FX
+ entity fx_CP_color_friendly //Used for setting control points on FX
+ entity fx_CP_color_neutral //Used for setting control points on FX
+
+ array<entity>[ TEAM_COUNT ] classicMPDropships
+ bool evacEnabled = false
+
+ void functionref( entity player ) observerFunc
+ array<void functionref()> playingThinkFuncTable
+ array<void functionref()> thirtySecondsLeftFuncTable
+ void functionref( int progress ) matchProgressAnnounceFunc
+
+ void functionref( entity player ) cp_VO_NagFunc
+ void functionref( entity player, entity hardpoint, float distance ) cp_VO_ApproachFunc
+ void functionref( entity touchEnt, entity hardpoint ) cp_VO_LeftTriggerWithoutCappingFunc
+
+ table<int, string> hardpointStringIDs
+
+ entity[ TEAM_COUNT ] flagSpawnPoints
+
+ vector distCheckTestPoint
+
+ void functionref() scoreEventOverrideFunc
+
+ array<void functionref( entity, entity )> onLeechedCustomCallbackFunc
+
+ bool forceSpawnAsTitan = false
+ bool forceSpawnIntoTitan = false
+ bool forceDisableTitanfalls = false
+ bool titanfallEnabled = true
+
+ //RoundWinningKillReplay related
+ entity roundWinningKillReplayViewEnt = null
+ entity roundWinningKillReplayVictim = null
+ int roundWinningKillReplayInflictorEHandle = -1
+ bool watchingRoundWinningKillReplay = false
+
+ bool forceNoFinalRoundDraws = false //Setting this to true will force a round based mode to keep playing rounds until a winner is determined. Game will not end on draw.
+
+ bool roundBasedTeamScore_RoundReset = true //if true, reset team scores at the start of each round.
+ bool isInPilotGracePeriod = false // if true, all players will be allowed to switch loadouts
+}
+
+global SvGlobals svGlobal
+
+void function CodeCallback_MapSpawn() // original script entry point
+{
+ ScriptCompilerTest()
+ LoadDiamond()
+
+ _cc = CreateEntity( "point_clientcommand" )
+ _sc = CreateEntity( "point_servercommand" )
+ PrecacheEntity( "env_entity_dissolver" )
+
+ LevelVarInit()
+
+ svGlobal.worldspawn = GetEnt( "worldspawn" )
+ svGlobal.worldspawn.kv.startdark = true
+
+ PrecacheModel( $"models/dev/editor_ref.mdl" )
+ PrecacheModel( $"models/dev/empty_model.mdl" )
+ PrecacheModel( $"models/test/brad/store_card.mdl" )
+ PrecacheModel( $"models/test/brad/store_card_angel_city.mdl" )
+ PrecacheModel( $"models/test/brad/store_card_colony.mdl" )
+ PrecacheModel( $"models/test/brad/store_card_relic.mdl" )
+ PrecacheModel( $"models/test/brad/store_card_prime_bundle.mdl" )
+ PrecacheModel( $"models/test/brad/store_titan_warpaint_bundle.mdl" )
+ PrecacheModel( $"models/test/brad/store_weapon_warpaint_bundle.mdl" )
+ PrecacheModel( $"models/test/brad/jump_start.mdl" )
+ PrecacheModel( $"models/weapons/shoulder_rocket_SRAM/ptpov_law_menu.mdl" ) // HACK
+ PrecacheModel( $"models/weapons/lstar/ptpov_lstar_menu.mdl" ) // HACK
+ PrecacheModel( $"models/weapons/softball_at/ptpov_softball_at_menu.mdl" ) // HACK
+ PrecacheModel( $"models/weapons/mastiff_stgn/ptpov_mastiff_menu.mdl" ) // HACK
+ PrecacheModel( $"models/error.mdl" ) // model used when no model is provided
+ if ( DREW_MODE == 2 ) // TEMPHACK
+ PrecacheModel( GREEN_SCREEN_MODEL )
+
+ printl( "Code Script: _mapspawn" )
+
+ // This will end up in either SP or MP
+ SPMP_MapSpawn_Init()
+}
+
+
+var function CodeCallback_ClientCommand( entity player, array<string> args )
+{
+ /*printl( "############################" )
+ printl( "CodeCallback_ClientCommand() before" )
+ printl( "player = " + player )
+ printl( "args:" )
+ foreach( key, value in args )
+ printl( key + " : " + value )
+ printl( "############################" )*/
+
+ string commandString = args.remove( 0 )
+
+ //TODO: Track down Why VModEnable is being called from code?
+
+ //Assert( commandString in svGlobal.clientCommandCallbacks )
+ if ( commandString in svGlobal.clientCommandCallbacks )
+ {
+ return svGlobal.clientCommandCallbacks[ commandString ]( player, args )
+ }
+ else
+ {
+ printl( "############################" )
+ printl( "CommandString: " + commandString + " was not added via AddClientCommandCallback but is being called in CodeCallback_ClientCommand" )
+ printl( "############################" )
+ return false
+ }
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_menu_callbacks.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_menu_callbacks.gnut
new file mode 100644
index 00000000..c116ac33
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_menu_callbacks.gnut
@@ -0,0 +1,17 @@
+global function MenuCallbacks_Init
+
+void function MenuCallbacks_Init()
+{
+ AddClientCommandCallback( "LeaveMatch", ClientCommandCallback_LeaveMatch )
+}
+
+bool function ClientCommandCallback_LeaveMatch( entity player, array<string> args )
+{
+ // todo: ideally, it'd be nice to get clients to return to lobby here, rather than just dcing them
+ // kind of a pain tho, since we'd have to get it to call script code without a remote func, since that'd break compatibility
+
+ ClientCommand( player, "disconnect" )
+ //ClientCommand( player, "setplaylist tdm; map mp_lobby" )
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_misc.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_misc.gnut
new file mode 100644
index 00000000..20b53c50
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_misc.gnut
@@ -0,0 +1,40 @@
+//todo figure out where these stub functions should be and move them to those places
+global function Spotting_Init
+global function FW_Border_GlobalInit
+global function IsVDUTitan
+global function PIN_PlayerRodeoedEnemyTitanToCompletion
+global function PlayerProgressionAllowed
+
+void function Spotting_Init()
+{
+
+}
+
+void function FW_Border_GlobalInit()
+{
+ AddSpawnCallbackEditorClass( "func_brush", "func_brush_fw_territory_border", RemoveFWBorder )
+}
+
+void function RemoveFWBorder( entity border )
+{
+ if ( GameModeRemove( border ) )
+ return
+
+ if ( !border.HasKey( "gamemode_" + GAMETYPE ) )
+ border.Destroy()
+}
+
+bool function IsVDUTitan(entity titan)
+{
+ return false
+}
+
+void function PIN_PlayerRodeoedEnemyTitanToCompletion( entity player, entity titan, bool playerHadBattery )
+{
+
+}
+
+bool function PlayerProgressionAllowed( entity player )
+{
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_networkvars.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_networkvars.gnut
new file mode 100644
index 00000000..14990a15
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_networkvars.gnut
@@ -0,0 +1,169 @@
+untyped
+
+
+global function SetEntityVar
+global function SetServerVar
+global function SetNetworkVar
+global function SyncServerVars
+global function SyncEntityVars
+
+
+function SetEntityVar( entity ent, varName, value )
+{
+ Assert( IsServer() )
+ Assert( varName in _entityClassVars[ent.GetClassName()], "Entity " + ent + " does not have remote var " + varName )
+ Assert( varName in _entityClassVarsIsEnts[ent.GetClassName()] )
+ Assert( varName in _entityClassVarsSyncToAllClients[ent.GetClassName()] )
+ Assert( typeof value != "string" )
+
+ Assert( "_entityVars" in ent )
+
+ if ( ent._entityVars[varName] == value )
+ return
+
+ ent._entityVars[varName] = value
+
+ if ( _entityClassVarsIsEnts[ent.GetClassName()][varName] && value != null )
+ {
+ //printl( "SET NETWORK ENTITY VAR TO AN ENTITY. GETTING EHANDLE" )
+ value = value.GetEncodedEHandle()
+ }
+
+ local syncToAllPlayers = _entityClassVarsSyncToAllClients[ent.GetClassName()][varName]
+
+ // only sync "player" variables to that player
+ if ( ent.IsPlayer() && !ent.IsBot() && !syncToAllPlayers )
+ {
+ if ( !ent.p.clientScriptInitialized )
+ return
+
+ Remote_CallFunction_NonReplay( ent, "ServerCallback_SetEntityVar", ent.GetEncodedEHandle(), _entityClassVarHandles[varName], value )
+ }
+ else
+ {
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ if ( player.IsBot() )
+ continue
+
+ if ( !player.p.clientScriptInitialized )
+ continue
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_SetEntityVar", ent.GetEncodedEHandle(), _entityClassVarHandles[varName], value )
+ }
+ }
+}
+
+function SetServerVar( varName, value )
+{
+ Assert( IsServer() )
+ Assert( varName in _serverVars )
+ Assert( typeof value != "string" )
+ expect string( varName )
+
+ if ( _serverVars[varName] == value )
+ return
+
+ _serverVars[varName] = value
+
+ if ( varName in _serverEntityVars && value != null )
+ {
+ if ( IsValid( value ) )
+ value = value.GetEncodedEHandle()
+ else
+ value = null
+ }
+
+ // Run server script change callback if one exists
+ thread ServerVarChangedCallbacks( varName )
+
+ // Update the var on all clients
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ if ( !player.p.clientScriptInitialized )
+ continue
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_SetServerVar", _serverVarHandles[varName], value )
+ }
+}
+
+function SetNetworkVar( obj, varName, value )
+{
+ if ( obj == level )
+ {
+ return SetServerVar( varName, value )
+ }
+ else
+ {
+ expect entity( obj )
+ return SetEntityVar( obj, varName, value )
+ }
+}
+
+function SyncServerVars( entity player )
+{
+ Assert( IsServer() )
+
+ foreach ( varName, value in _serverVars )
+ {
+ if ( varName in _serverEntityVars && value != null )
+ {
+ if ( IsValid( value ) )
+ value = value.GetEncodedEHandle()
+ else
+ value = null
+ }
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_SetServerVar", _serverVarHandles[varName], value )
+ }
+}
+
+function SyncEntityVars( entity player )
+{
+ Assert( IsServer() )
+
+ foreach ( className, _ in _entityClassVars )
+ {
+ array<entity> entities
+ if ( className == "player" )
+ entities = GetPlayerArray()
+ else
+ entities = GetNPCArrayByClass( className )
+
+ foreach ( ent in entities )
+ {
+ if ( !IsValid( ent ) )
+ continue
+
+ foreach( varName, value in _entityClassVars[className] )
+ {
+ local entValue = ent._entityVars[varName]
+ if ( entValue == value )
+ continue
+
+ if ( !_entityClassVarsSyncToAllClients[className][varName] && ent != player )
+ {
+ Assert( className == "player" )
+ continue
+ }
+ //if ( className == "player" && !_entityClassVarsSyncToAllClients[className][varName] )
+ // continue
+ //
+ if ( _entityClassVarsIsEnts[className][varName] )
+ {
+ if ( !IsValid( entValue ) )
+ continue
+ // if this is an entity var, change over to e-handle
+ entValue = entValue.GetEncodedEHandle()
+ }
+
+ Assert( player.p.clientScriptInitialized )
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_SetEntityVar", ent.GetEncodedEHandle(), _entityClassVarHandles[varName], entValue )
+ }
+ }
+ }
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_objective.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_objective.gnut
new file mode 100644
index 00000000..893861bf
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_objective.gnut
@@ -0,0 +1,108 @@
+untyped
+
+global function Objective_Init
+
+global function RegisterObjective
+global function SetCurrentTeamObjectiveForPlayer
+global function SetTeamActiveObjective
+global function ClearTeamActiveObjective
+
+global function SetPlayerActiveObjective
+global function ClearPlayerActiveObjective
+
+int convIndex = 0 //Note that objectiveIndex 0 is reserved by code to mean no objective active!
+
+//Split this out into _objective_shared, _objective and cl_objective once QA gets a chance to hammer at it.
+function Objective_Init()
+{
+ level.objToIndex <- {}
+ level.teamActiveObjective <- { [TEAM_IMC] = null, [TEAM_MILITIA] = null }
+
+}
+
+function RegisterObjective( objectiveName )
+{
+ convIndex++
+ level.objToIndex[ objectiveName ] <- convIndex
+}
+
+function CreateTeamActiveObjectiveTable( objectiveName, objectiveTimer = 0, objectiveEntity = null )
+{
+ local Table = {}
+ Table.objectiveName <- objectiveName
+ Table.objectiveTimer <- objectiveTimer
+ Table.objectiveEntity <- objectiveEntity
+
+ return Table
+}
+
+function SetCurrentTeamObjectiveForPlayer( entity player )
+{
+ int team = player.GetTeam()
+ local objectiveTable = GetTeamActiveObjective( team )
+
+ if ( objectiveTable )
+ {
+ local objectiveName = objectiveTable.objectiveName
+ local objectiveTimer = objectiveTable.objectiveTimer
+ local objectiveEntity = objectiveTable.objectiveEntity
+ SetPlayerActiveObjective( player, objectiveName, objectiveTimer, objectiveEntity )
+ }
+}
+
+function GetTeamActiveObjective( team )
+{
+ if ( (team != TEAM_IMC) && (team != TEAM_MILITIA) )
+ return null
+ return level.teamActiveObjective[team]
+}
+
+function SetTeamActiveObjective( team, objectiveName, objectiveTimer = 0, objectiveEntity = null )
+{
+ Assert( team == TEAM_IMC || team == TEAM_MILITIA )
+ array<entity> players = GetPlayerArrayOfTeam( team )
+
+ local objectiveIndex = level.objToIndex[ objectiveName ]
+
+ foreach ( player in players )
+ {
+ SetPlayerActiveObjective_Internal( player, objectiveIndex, objectiveTimer, objectiveEntity )
+ }
+
+ level.teamActiveObjective[ team ] = CreateTeamActiveObjectiveTable( objectiveName, objectiveTimer, objectiveEntity )
+}
+
+function ClearTeamActiveObjective( team )
+{
+ Assert( team == TEAM_IMC || team == TEAM_MILITIA )
+ array<entity> players = GetPlayerArrayOfTeam( team )
+ foreach ( player in players )
+ {
+ ClearPlayerActiveObjective( player )
+ }
+
+ level.teamActiveObjective[ team ] = null
+
+}
+
+function SetPlayerActiveObjective( player, objectiveName, objectiveTimer = 0, objectiveEntity = null )
+{
+ local objectiveIndex = level.objToIndex[ objectiveName ]
+
+ SetPlayerActiveObjective_Internal( player, objectiveIndex, objectiveTimer, objectiveEntity )
+}
+
+function SetPlayerActiveObjective_Internal( player, objectiveIndex, objectiveTimer, objectiveEntity )
+{
+ player.SetObjectiveEndTime( objectiveTimer )
+ player.SetObjectiveEntity( objectiveEntity )
+ player.SetObjectiveIndex( objectiveIndex )
+}
+
+function ClearPlayerActiveObjective( player )
+{
+ player.SetObjectiveEndTime( 0 )
+ player.SetObjectiveEntity( null )
+ player.SetObjectiveIndex( 0 )
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_on_spawned.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_on_spawned.gnut
new file mode 100644
index 00000000..d1935d62
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_on_spawned.gnut
@@ -0,0 +1,508 @@
+untyped
+
+global function CodeCallback_PreSpawn
+global function CodeCallback_OnSpawned
+global function RunMySpawnFunctions
+global function AddScriptNoteworthySpawnCallback
+global function SpawnFromSpawnerArray
+global function AddSpawnCallback
+global function AddSpawnCallbackEditorClass
+global function AddSpawnCallback_ScriptName
+global function GetLeveledAISettings
+global function GetSpawnAISettings
+global function GetDefaultAISetting
+
+void function CodeCallback_PreSpawn( entity npc )
+{
+ /*
+ SCRIPTERS READ THIS
+
+ The purpose of this function is to fixup npc fields coming from all the many places they can come from, so that code doesn't break them during DispatchSpawn.
+ If you want to fix an AI field on spawned, you should do it in ai_spawn_content, unless the field change is related to code functionality and needs
+ to change before code spawns the AI. Then, it should be here.
+
+ Thanks
+ -Mackey
+ */
+ Assert( npc.IsNPC() )
+
+ if ( !npc.HasAISettings() )
+ {
+ // this ai has no ai settings file
+ string aisettings = GetDefaultAISetting( npc )
+ Assert( aisettings != "" )
+ SetAISettingsWrapper( npc, aisettings )
+ }
+
+ if ( npc.IsTitan() )
+ {
+ if ( npc.ai.titanSettings.titanSetFile == "" )
+ {
+ if ( npc.HasKey( "leveled_titan_settings" ) )
+ {
+ SetTitanSettings( npc.ai.titanSettings, expect string( npc.kv.leveled_titan_settings ) )
+ }
+ }
+
+ var builtInLoadout
+
+ if ( npc.HasKey( "leveled_titan_loadout" ) )
+ {
+ builtInLoadout = npc.GetValueForKey( "leveled_titan_loadout" )
+ }
+ else
+ {
+ if ( npc.Dev_GetAISettingByKeyField( "WeaponCapacity" ) == "FromLoadout" )
+ builtInLoadout = npc.Dev_GetAISettingByKeyField( "npc_titan_player_settings" )
+ }
+
+ if ( builtInLoadout != null )
+ {
+ // derive loadout from built in loadout in this case
+ expect string( builtInLoadout )
+ if ( npc.ai.titanSettings.titanSetFile != builtInLoadout )
+ SetTitanSettings( npc.ai.titanSettings, builtInLoadout )
+
+ npc.ai.titanSpawnLoadout.setFile = builtInLoadout
+ // OverwriteLoadoutWithDefaultsForSetFile_ExceptSpecialAndAntiRodeo( npc.ai.titanSpawnLoadout )
+ OverwriteLoadoutWithDefaultsForSetFile( npc.ai.titanSpawnLoadout ) // get the entire loadout, including defensive and tactical
+
+ //Set camo, decal, and skin indices from npc settings.
+ //Using Dev_GetAISettingsByKeyField_Global because titan has not spawned yet, so the non-global version of this function does not work.
+ var camoIndex = Dev_GetAISettingByKeyField_Global( npc.GetAISettingsName(), "titanCamoIndex" )
+ var decalIndex = Dev_GetAISettingByKeyField_Global( npc.GetAISettingsName(), "titanDecalIndex" )
+ var skinIndex = Dev_GetAISettingByKeyField_Global( npc.GetAISettingsName(), "titanSkinIndex" )
+ if ( camoIndex != null )
+ npc.ai.titanSpawnLoadout.camoIndex = expect int ( camoIndex )
+ if ( decalIndex != null )
+ npc.ai.titanSpawnLoadout.decalIndex = expect int ( decalIndex )
+ if ( skinIndex != null )
+ npc.ai.titanSpawnLoadout.skinIndex = expect int ( skinIndex )
+ }
+
+ AssignSpawnOptionsFromLeveled( npc, SetSpawnOption_Weapon, "additionalequipment", "primaryWeapon_mods" )
+ npc.kv.additionalequipment = ""
+ AssignSpawnOptionsFromLeveled( npc, SetSpawnOption_Ordnance, "titanOrdnance", "titanOrdnance_mods" )
+ AssignSpawnOptionsFromLeveled( npc, SetSpawnOption_Special, "titanSpecial", "titanSpecial_mods" )
+ AssignSpawnOptionsFromLeveled( npc, SetSpawnOption_Antirodeo, "titanAntiRodeo", "titanAntiRodeo_mods" )
+
+ // temp fix for npc_create npc_titan, probably should refactor away npc.ai.titanSettings
+ if ( npc.ai.titanSettings.titanSetFile == "" )
+ SetTitanSettings( npc.ai.titanSettings, expect string( npc.Dev_GetAISettingByKeyField( "npc_titan_player_settings" ) ) )
+
+ CreateTitanModelAndSkinSetup( npc )
+
+ if ( npc.GetAIClass() == AIC_TITAN_BUDDY )
+ npc.kv.squadname = "bt"
+ }
+ else
+ {
+ npc.SetValueForModelKey( npc.GetSettingModelName() )
+ }
+
+ if ( !IsTurret( npc ) && IsSingleplayer() && npc.kv.squadname == "" && npc.GetTeam() >= FIRST_GAME_TEAM )
+ npc.SetAutoSquad()
+ //AutoSquadnameAssignment( npc )
+
+ if ( !npc.IsTitan() )
+ {
+ AssignGrenadeWeaponFromAISettings( npc )
+
+ AssignDroneSpawnAISettings( npc )
+
+ if ( npc.ai.mySpawnOptions_weapon == null )
+ {
+ if ( !IsTurret( npc ) )
+ {
+ NPCDefaultWeapon ornull defaultWeapon = GetNPCDefaultWeaponForLevel( npc )
+ if ( defaultWeapon != null )
+ {
+ expect NPCDefaultWeapon( defaultWeapon )
+ SetSpawnOption_Weapon( npc, defaultWeapon.wep, defaultWeapon.mods )
+ npc.kv.additionalequipment = ""
+ return
+ }
+ }
+
+ switch ( npc.kv.additionalequipment )
+ {
+ case "":
+ case "auto_weapon":
+ case "auto_weapon_antititan":
+ case "auto_weapon_sidearm":
+ case "auto_weapon_rifle":
+ case "auto_weapon_lmg":
+ case "auto_weapon_shield_captain":
+ case "auto_weapon_shotgun":
+ case "auto_weapon_smg":
+ case "auto_weapon_sniper":
+ case "auto_weapon_specialist":
+
+ // fill weapon in from ai settings file
+ npc.kv.additionalequipment = ""
+ string aiSettingsWeapon = npc.AISetting_GetDefaultWeapon()
+ if ( aiSettingsWeapon != "" )
+ SetSpawnOption_Weapon( npc, aiSettingsWeapon )
+ break
+
+ case "none":
+ npc.kv.additionalequipment = ""
+ break
+ }
+ }
+ }
+}
+
+void function AssignDroneSpawnAISettings( entity npc )
+{
+ if ( npc.HasKey( "script_drone_type" ) )
+ {
+ var droneType = npc.kv.script_drone_type
+ if ( droneType != null )
+ {
+ expect string( droneType )
+ if ( droneType.tolower() == "none" )
+ droneType = ""
+ npc.ai.droneSpawnAISettings = droneType
+ }
+ return
+ }
+
+ npc.ai.droneSpawnAISettings = npc.AISetting_SummonDrone()
+}
+
+
+void function AssignGrenadeWeaponFromAISettings( entity npc )
+{
+ if( npc.kv.grenadeWeaponName == "none" )
+ {
+ npc.kv.grenadeWeaponName = ""
+ return
+ }
+
+ if ( npc.kv.GrenadeWeaponName != "" )
+ return
+
+ string grenadeWeaponName = npc.AISetting_GetGrenadeWeapon()
+ if ( grenadeWeaponName == "" )
+ return
+
+ npc.kv.grenadeWeaponName = grenadeWeaponName
+}
+
+void function AssignSpawnOptionsFromLeveled( entity npc, void functionref( entity, string, array<string> = 0 ) spawnSettingsFunc, string kvWeapon, string kvWeaponMods )
+{
+ if ( !npc.HasKey( kvWeapon ) )
+ return
+ string weapon = npc.GetValueForKey( kvWeapon )
+ if ( weapon == "" )
+ return
+
+ array<string> mods
+ if ( npc.HasKey( kvWeaponMods ) )
+ {
+ mods = split( npc.GetValueForKey( kvWeaponMods ), " " )
+ }
+
+ spawnSettingsFunc( npc, weapon, mods )
+}
+
+string function GetDefaultAISetting( entity npc )
+{
+// change this to map directly by file name from subClass, and error if its not there.
+// This insures consistent settings file naming and makes settings files less of a mix-and-match concept.
+// subclasses should also sub-name off their class (except for craaaaaazy soldier/grunt guy)
+
+ if ( npc.mySpawnOptions_aiSettings != null )
+ {
+ // you have to include base if you use SpawnOption_AISettings
+ return string( npc.mySpawnOptions_aiSettings )
+ }
+
+ if ( npc.HasKey( "leveled_aisettings" ) )
+ {
+ return GetLeveledAISettings( npc )
+ }
+
+ if ( npc.IsTitan() && npc.ai.titanSettings.titanSetFile != "" )
+ {
+ // from titan player set file
+ string settingsKey = GetAISettingsStringForMode()
+
+ var aiSettings = Dev_GetPlayerSettingByKeyField_Global( npc.ai.titanSettings.titanSetFile, settingsKey )
+ if ( aiSettings != null )
+ return expect string( aiSettings )
+ }
+
+ return npc.GetClassName()
+}
+
+string function GetLeveledAISettings( entity npc )
+{
+ Assert( npc.IsNPC() )
+ Assert( npc.HasKey( "leveled_aisettings" ) )
+ string settings = expect string( npc.kv.leveled_aisettings )
+ switch ( settings )
+ {
+ // remap deprecated substrings for awhile
+ case "npc_soldier_drone_summoner_shield":
+ return "npc_soldier_drone_summoner"
+ }
+ return settings
+}
+
+string function GetSpawnAISettings( entity npc )
+{
+ if ( npc.mySpawnOptions_aiSettings != null)
+ return expect string( npc.mySpawnOptions_aiSettings )
+ else if ( npc.HasKey( "leveled_aisettings" ) )
+ return expect string( npc.kv.leveled_aisettings )
+
+ return ""
+}
+
+void function CodeCallback_OnSpawned( entity ent )
+{
+ if ( IsSpawner( ent ) )
+ {
+ var spawnerKVs = ent.GetSpawnEntityKeyValues()
+ if ( "script_flag_killed" in spawnerKVs )
+ thread SetupFlagKilledForNPC( ent )
+ return
+ }
+
+ string classname = ent.GetClassName()
+
+ if ( classname in _entityClassVars )
+ {
+ if ( !ent._entityVars )
+ InitEntityVars( ent )
+
+ //ent.ConnectOutput( "OnDestroy", "_RemoveFromEntityList" )
+ }
+
+ int teamNum = int( expect string( ent.kv.teamnumber ) )
+ if ( teamNum != 0 )
+ SetTeam( ent, teamNum )
+
+ SetModelSkinFromLeveled( ent )
+
+ if ( IsLobby() )
+ {
+ RunMySpawnFunctions( ent )
+ return
+ }
+
+ if ( ent.IsNPC() )
+ {
+ CommonNPCOnSpawned( ent )
+ }
+
+ if ( ent instanceof CBaseCombatCharacter && ent.GetModelName() != $"" )
+ InitDamageStates( ent )
+
+ if ( ent instanceof CProjectile || ent instanceof CBaseGrenade )
+ thread PROTO_InitTrackedProjectile( ent )
+
+ /*
+ if ( !( "totalSpawned" in level ) )
+ {
+ level.totalSpawned <- {}
+ level.totalSpawned.total <- 0
+ }
+
+ if ( !( "classname" in level.totalSpawned ) )
+ {
+ level.totalSpawned[ classname ] <- {}
+ }
+
+ level.totalSpawned[ classname ][ ent ] <- ent
+ level.totalSpawned.total++
+ */
+
+ RegisterForDamageDeathCallbacks( ent )
+
+ RunMySpawnFunctions( ent )
+}
+
+function RunMySpawnFunctions( entity self )
+{
+ if ( !IsValid( self ) )
+ {
+ // entity was deleted already
+ return
+ }
+
+ RunSpawnCallbacks( self )
+ RunEditorClassCallbacks( self )
+ RunScriptNoteworthyCallbacks( self )
+ RunScriptNameCallbacks( self )
+}
+
+void function AddSpawnCallback( string classname, void functionref( entity ) func )
+{
+ foreach ( spawnCallbackFuncArray funcArray in svGlobal.spawnCallbackFuncs )
+ {
+ if ( funcArray.entityClassname == classname )
+ {
+ funcArray.callbackArray.append( func )
+ return
+ }
+ }
+
+ spawnCallbackFuncArray funcArray
+ funcArray.entityClassname = classname
+ funcArray.callbackArray.append( func )
+ svGlobal.spawnCallbackFuncs.append( funcArray )
+}
+
+void function AddSpawnCallbackEditorClass( string classname, string editorClassname, void functionref( entity ) func )
+{
+ foreach ( spawnCallbackEditorClassFuncArray funcArray in svGlobal.spawnCallbackEditorClassFuncs )
+ {
+ if ( funcArray.entityClassname == classname && funcArray.entityEditorClassname == editorClassname )
+ {
+ funcArray.callbackArray.append( func )
+ return
+ }
+ }
+
+ spawnCallbackEditorClassFuncArray funcArray
+ funcArray.entityClassname = classname
+ funcArray.entityEditorClassname = editorClassname
+ funcArray.callbackArray.append( func )
+ svGlobal.spawnCallbackEditorClassFuncs.append( funcArray )
+}
+
+function RunSpawnCallbacks( entity self )
+{
+ string classname = self.GetClassName()
+
+ foreach ( spawnCallbackFuncArray funcArray in svGlobal.spawnCallbackFuncs )
+ {
+ if ( funcArray.entityClassname == classname )
+ {
+ foreach ( func in funcArray.callbackArray )
+ {
+ func( self )
+ }
+ }
+ }
+}
+
+function RunEditorClassCallbacks( entity self )
+{
+ string editorClassname = GetEditorClass( self )
+ if ( editorClassname == "" )
+ return
+
+ string classname = self.GetClassName()
+
+ foreach ( spawnCallbackEditorClassFuncArray funcArray in svGlobal.spawnCallbackEditorClassFuncs )
+ {
+ if ( funcArray.entityEditorClassname == editorClassname )
+ {
+ //Assert( funcArray.entityClassname == classname, "Editor classname callback was set on entity with wrong base classname type" )
+ if ( funcArray.entityClassname != classname )
+ CodeWarning( "Entity " + editorClassname + " is expecting alias of " + funcArray.entityClassname + " but found a " + classname + ". You may just need to reexport from LevelEd and recompile the map to fix this." )
+
+ foreach ( func in funcArray.callbackArray )
+ {
+ thread func( self )
+ }
+ }
+ }
+}
+
+array<entity> function SpawnFromSpawnerArray( array<entity> spawners, void functionref( entity ) ornull spawnSettingsFunc = null )
+{
+ array<entity> spawned
+ if ( spawnSettingsFunc == null )
+ {
+ foreach ( entity spawner in spawners )
+ {
+ entity ent = spawner.SpawnEntity()
+ DispatchSpawn( ent )
+ spawned.append( ent )
+ }
+ }
+ else
+ {
+ expect void functionref( entity )( spawnSettingsFunc )
+ foreach ( entity spawner in spawners )
+ {
+ entity ent = spawner.SpawnEntity()
+ spawnSettingsFunc( ent )
+ DispatchSpawn( ent )
+ spawned.append( ent )
+ }
+ }
+
+ return spawned
+}
+
+void function RunScriptNameCallbacks( entity ent )
+{
+ string name = ent.GetScriptName()
+ if ( !( name in svGlobal.spawnCallbacks_scriptName ) )
+ return
+
+ foreach ( callback in svGlobal.spawnCallbacks_scriptName[ name ] )
+ {
+ thread callback( ent )
+ }
+}
+
+void function AddSpawnCallback_ScriptName( string scriptName, void functionref( entity ) func )
+{
+ if ( !( scriptName in svGlobal.spawnCallbacks_scriptName ) )
+ svGlobal.spawnCallbacks_scriptName[ scriptName ] <- []
+ svGlobal.spawnCallbacks_scriptName[ scriptName ].append( func )
+}
+
+void function RunScriptNoteworthyCallbacks( entity ent )
+{
+ if ( !( ent.HasKey( "script_noteworthy" ) ) )
+ return
+
+ foreach ( noteworthyCallback in svGlobal.spawnCallbackFuncs_scriptNoteworthy )
+ {
+ if ( ent.kv.script_noteworthy != noteworthyCallback.scriptNoteworthy )
+ continue
+
+ foreach ( func in noteworthyCallback.callbackArray )
+ {
+ func( ent )
+ }
+
+ break // ??? break?
+ }
+}
+
+void function AddScriptNoteworthySpawnCallback( string script_noteworthy, void functionref( entity ) func )
+{
+ foreach ( noteworthyCallback in svGlobal.spawnCallbackFuncs_scriptNoteworthy )
+ {
+ if ( script_noteworthy != noteworthyCallback.scriptNoteworthy )
+ continue
+
+ noteworthyCallback.callbackArray.append( func )
+ return
+ }
+
+ spawnCallbackFuncArray_scriptNoteworthy newNoteworthyCallback
+ newNoteworthyCallback.scriptNoteworthy = script_noteworthy
+ newNoteworthyCallback.callbackArray.append( func )
+ svGlobal.spawnCallbackFuncs_scriptNoteworthy.append( newNoteworthyCallback )
+}
+
+void function SetModelSkinFromLeveled( entity ent )
+{
+ // Hack that we have to wait a frame for it to work. Code should just do this for us anyways.
+ if ( !ent.HasKey( "modelskin" ) )
+ return
+
+ int skin = expect int( ent.kv.modelskin.tointeger() )
+ if ( skin > 0 )
+ ent.SetSkin( skin )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_pain_death_sounds.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_pain_death_sounds.gnut
new file mode 100644
index 00000000..10d2b616
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_pain_death_sounds.gnut
@@ -0,0 +1,455 @@
+global function PainDeathSounds_Init
+global function PlayDeathSounds
+global function PlayPainSounds
+global function TogglePainDeathDebug
+
+struct PainOrDeathSound
+{
+ bool functionref( entity, entity, bool, int, int ) isSoundTypeFunc
+ string alias_1p_victim_only
+ string alias_3p_except_victim
+ string alias_3p_attacker_only
+ string alias_3p_except_attacker
+ bool blocksPriority
+ int priority
+}
+
+struct
+{
+ array< array<PainOrDeathSound> > painSounds
+ array< array<PainOrDeathSound> > deathSounds
+
+ bool painDeathDebug
+} file
+
+
+enum eBodyTypes
+{
+ NPC_ANDROID
+ NPC_GRUNT
+ NPC_MARVIN
+ NPC_PROWLER
+ NPC_SPECIALIST
+ NPC_SPECTRE
+ NPC_STALKER
+ NPC_SUPER_SPECTRE
+ PLAYER_ANDROID_FEMALE
+ PLAYER_ANDROID_MALE
+ PLAYER_HUMAN_FEMALE
+ PLAYER_HUMAN_MALE
+ TITAN
+ total
+}
+
+int function GetBodyTypeIndexFromVictim( entity victim )
+{
+ // can add hologram support if needed
+ if ( victim.IsHologram() )
+ return -1
+
+ if ( victim.IsTitan() )
+ return eBodyTypes.TITAN
+
+ if ( victim.IsPlayer() )
+ {
+ if ( victim.IsMechanical() )
+ {
+ if ( IsPlayerFemale( victim ) )
+ return eBodyTypes.PLAYER_ANDROID_FEMALE
+
+ return eBodyTypes.PLAYER_ANDROID_MALE
+ }
+ else
+ {
+ if ( IsPlayerFemale( victim ) )
+ return eBodyTypes.PLAYER_HUMAN_FEMALE
+
+ return eBodyTypes.PLAYER_HUMAN_MALE
+ }
+ }
+
+ if ( IsSpecialist( victim ) )
+ return eBodyTypes.NPC_SPECIALIST
+
+ if ( IsGrunt( victim ) )
+ return eBodyTypes.NPC_GRUNT
+
+ if ( IsProwler( victim ) )
+ return eBodyTypes.NPC_PROWLER
+
+ if ( IsSuperSpectre( victim ) )
+ return eBodyTypes.NPC_SUPER_SPECTRE
+
+ if ( IsSpectre( victim ) )
+ return eBodyTypes.NPC_SPECTRE
+
+ if ( IsStalker( victim ) )
+ return eBodyTypes.NPC_STALKER
+
+ if ( IsMarvin( victim ) )
+ return eBodyTypes.NPC_MARVIN
+
+ return -1
+}
+
+void function PainDeathSounds_Init()
+{
+ file.painSounds.resize( eBodyTypes.total )
+ file.deathSounds.resize( eBodyTypes.total )
+
+ var dataTable = GetDataTable( $"datatable/pain_death_sounds.rpak" )
+ int numRows = GetDatatableRowCount( dataTable )
+
+ int eventColumn = GetDataTableColumnByName( dataTable, "event" )
+ int blocksPriorityColumn = GetDataTableColumnByName( dataTable, "blocksNextPriority" )
+ int methodColumn = GetDataTableColumnByName( dataTable, "method" )
+ int priorityColumn = GetDataTableColumnByName( dataTable, "priority" )
+ int bodyTypeColumn = GetDataTableColumnByName( dataTable, "bodyType" )
+ int alias_1p_victim_only_column = GetDataTableColumnByName( dataTable, "alias_1p_victim_only" )
+ int alias_3p_except_victim_column = GetDataTableColumnByName( dataTable, "alias_3p_except_victim" )
+ int alias_3p_attacker_only_column = GetDataTableColumnByName( dataTable, "alias_3p_attacker_only" )
+ int alias_3p_except_attacker_column = GetDataTableColumnByName( dataTable, "alias_3p_except_attacker" )
+ int visibleColumn = GetDataTableColumnByName( dataTable, "spmp" )
+
+ table<string,bool> visibleMask
+ visibleMask[ "spmp" ] <- true
+ if ( IsMultiplayer() )
+ visibleMask[ "mp" ] <- true
+ else if ( IsSingleplayer() )
+ visibleMask[ "sp" ] <- true
+
+ for ( int i = 0; i < numRows; i++ )
+ {
+ string visible = GetDataTableString( dataTable, i, visibleColumn )
+ if ( !( visible in visibleMask ) )
+ continue
+
+ int priority = GetDataTableInt( dataTable, i, priorityColumn )
+ bool blocksPriority = GetDataTableBool( dataTable, i, blocksPriorityColumn )
+ string event = GetDataTableString( dataTable, i, eventColumn )
+ string method = GetDataTableString( dataTable, i, methodColumn )
+ string bodyTypeName = GetDataTableString( dataTable, i, bodyTypeColumn )
+ string alias_1p_victim_only = GetDataTableString( dataTable, i, alias_1p_victim_only_column )
+ string alias_3p_except_victim = GetDataTableString( dataTable, i, alias_3p_except_victim_column )
+ string alias_3p_attacker_only = GetDataTableString( dataTable, i, alias_3p_attacker_only_column )
+ string alias_3p_except_attacker = GetDataTableString( dataTable, i, alias_3p_except_attacker_column )
+ int bodyType = eBodyTypes[ bodyTypeName ]
+
+ PainOrDeathSound painOrDeathSound
+ painOrDeathSound.isSoundTypeFunc = GetSoundTypeFuncFromName( method )
+ painOrDeathSound.alias_1p_victim_only = alias_1p_victim_only
+ painOrDeathSound.alias_3p_except_victim = alias_3p_except_victim
+ painOrDeathSound.alias_3p_attacker_only = alias_3p_attacker_only
+ painOrDeathSound.alias_3p_except_attacker = alias_3p_except_attacker
+ painOrDeathSound.blocksPriority = blocksPriority
+ painOrDeathSound.priority = priority
+
+ #if DEV
+ if ( priority < 100 || priority > 500 )
+ CodeWarning( "PainDeathSound event priority must be between 100 and 500. See " + event + " " + method )
+ #endif
+
+ switch ( event )
+ {
+ case "pain":
+ file.painSounds[ bodyType ].append( painOrDeathSound )
+ break
+
+ case "death":
+ file.deathSounds[ bodyType ].append( painOrDeathSound )
+ break
+
+ default:
+ CodeWarning( "Couldn't find pain/death event type " + event )
+ break
+ }
+ }
+
+ for ( int i = 0; i < eBodyTypes.total; i++ )
+ {
+ file.painSounds[ i ].sort( PainOrDeathSort )
+ file.deathSounds[ i ].sort( PainOrDeathSort )
+ }
+}
+
+int function PainOrDeathSort( PainOrDeathSound a, PainOrDeathSound b )
+{
+ if ( a.priority < b.priority )
+ return -1
+ if ( b.priority < a.priority )
+ return 1
+ return 0
+}
+
+
+bool functionref( entity, entity, bool, int, int ) function GetSoundTypeFuncFromName( string method )
+{
+ switch ( method )
+ {
+ case "SE_ANY":
+ return SE_ANY
+
+ case "SE_GIB":
+ return SE_GIB
+
+ case "SE_BULLET":
+ return SE_BULLET
+
+ case "SE_DISSOLVE":
+ return SE_DISSOLVE
+
+ case "SE_ELECTRICAL":
+ return SE_ELECTRICAL
+
+ case "SE_EXPLOSION":
+ return SE_EXPLOSION
+
+ case "SE_FALL":
+ return SE_FALL
+
+ case "SE_HEADSHOT_BULLET":
+ return SE_HEADSHOT_BULLET
+
+ case "SE_HEADSHOT_SHOTGUN":
+ return SE_HEADSHOT_SHOTGUN
+
+ case "SE_HEADSHOT_TITAN":
+ return SE_HEADSHOT_TITAN
+
+ case "SE_NECK_SNAP":
+ return SE_NECK_SNAP
+
+ case "SE_THERMITE_GRENADE":
+ return SE_THERMITE_GRENADE
+
+ case "SE_PROWLER":
+ return SE_PROWLER
+
+ case "SE_SMOKE":
+ return SE_SMOKE
+
+ case "SE_TITAN_STEP":
+ return SE_TITAN_STEP
+ }
+}
+
+bool function SE_ANY( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return true
+}
+
+bool function SE_GIB( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return bool( damageTypes & DF_GIB )
+}
+
+bool function SE_BULLET( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return bool( damageTypes & DF_BULLET )
+}
+
+bool function SE_DISSOLVE( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return bool( damageTypes & DF_DISSOLVE )
+}
+
+bool function SE_ELECTRICAL( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return bool( damageTypes & DF_ELECTRICAL )
+}
+
+bool function SE_EXPLOSION( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return bool( damageTypes & DF_EXPLOSION )
+}
+
+bool function SE_FALL( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return damageSourceID == eDamageSourceId.fall
+}
+
+bool function SE_HEADSHOT_BULLET( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ if ( !isValidHeadshot )
+ return false
+
+ return bool( damageTypes & DF_BULLET )
+}
+
+bool function SE_HEADSHOT_SHOTGUN( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ if ( !isValidHeadshot )
+ return false
+
+ return bool( damageTypes & DF_SHOTGUN )
+}
+
+bool function SE_HEADSHOT_TITAN( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ if ( !attacker.IsTitan() )
+ return false
+
+ return isValidHeadshot
+}
+
+bool function SE_NECK_SNAP( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return damageSourceID == eDamageSourceId.human_execution
+}
+
+bool function SE_THERMITE_GRENADE( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return damageSourceID == eDamageSourceId.mp_weapon_thermite_grenade
+}
+
+bool function SE_PROWLER( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ if ( !IsValid( attacker ) )
+ return false
+
+ return IsProwler( attacker )
+}
+
+bool function SE_SMOKE( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return damageSourceID == eDamageSourceId.mp_weapon_grenade_electric_smoke
+}
+
+bool function SE_TITAN_STEP( entity victim, entity attacker, bool isValidHeadshot, int damageTypes, int damageSourceID )
+{
+ return bool( damageTypes & DF_TITAN_STEP )
+}
+
+void function PlayPainSounds( entity victim, var damageInfo )
+{
+ int bodyType = GetBodyTypeIndexFromVictim( victim )
+ if ( bodyType >= 0 )
+ PlayPainOrDeathSounds( file.painSounds[ bodyType ], victim, damageInfo )
+}
+
+void function PlayDeathSounds( entity victim, var damageInfo )
+{
+ int bodyType = GetBodyTypeIndexFromVictim( victim )
+ if ( bodyType >= 0 )
+ PlayPainOrDeathSounds( file.deathSounds[ bodyType ], victim, damageInfo )
+}
+
+void function PlayPainOrDeathSounds( array<PainOrDeathSound> soundEvents, entity victim, var damageInfo )
+{
+ array<string> alias_1p_victim_only
+ array<string> alias_3p_except_victim
+ array<string> alias_3p_attacker_only
+ array<string> alias_3p_except_attacker
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ bool isValidHeadshot = IsValidHeadShot( damageInfo, victim )
+ int damageTypes = DamageInfo_GetCustomDamageType( damageInfo )
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ int lastPriority = 0
+ bool blockingPriority
+
+ foreach ( painOrDeathSound in soundEvents )
+ {
+ Assert( painOrDeathSound.priority >= lastPriority )
+
+ if ( blockingPriority )
+ {
+ if ( painOrDeathSound.priority > lastPriority )
+ break
+ }
+
+ if ( painOrDeathSound.isSoundTypeFunc( victim, attacker, isValidHeadshot, damageTypes, damageSourceID ) )
+ {
+ if ( painOrDeathSound.alias_1p_victim_only != "" )
+ alias_1p_victim_only.append( painOrDeathSound.alias_1p_victim_only )
+ if ( painOrDeathSound.alias_3p_except_victim != "" )
+ alias_3p_except_victim.append( painOrDeathSound.alias_3p_except_victim )
+ if ( painOrDeathSound.alias_3p_attacker_only != "" )
+ alias_3p_attacker_only.append( painOrDeathSound.alias_3p_attacker_only )
+ if ( painOrDeathSound.alias_3p_except_attacker != "" )
+ alias_3p_except_attacker.append( painOrDeathSound.alias_3p_except_attacker )
+
+ blockingPriority = painOrDeathSound.blocksPriority || blockingPriority
+ }
+
+ lastPriority = painOrDeathSound.priority
+ }
+
+ foreach ( sound in alias_3p_except_victim )
+ {
+ EmitSoundOnEntity( victim, sound )
+ }
+
+ if ( victim.IsPlayer() )
+ {
+ foreach ( sound in alias_1p_victim_only )
+ {
+ EmitSoundOnEntityOnlyToPlayer( victim, victim, sound )
+ }
+ }
+
+ if ( attacker.IsPlayer() )
+ {
+ foreach ( sound in alias_3p_except_attacker )
+ {
+ EmitSoundOnEntityExceptToPlayer( victim, attacker, sound )
+ }
+
+ foreach ( sound in alias_3p_attacker_only )
+ {
+ EmitSoundOnEntityOnlyToPlayer( victim, attacker, sound )
+ }
+ }
+ else
+ {
+ foreach ( sound in alias_3p_except_attacker )
+ {
+ EmitSoundOnEntity( victim, sound )
+ }
+ }
+
+ #if DEV
+ if ( !file.painDeathDebug )
+ return
+
+ foreach ( sound in alias_3p_except_victim )
+ {
+ printt( "PAIN_DEATH_DEBUG: EmitSoundOnEntity - " + sound )
+ }
+
+ if ( victim.IsPlayer() )
+ {
+ foreach ( sound in alias_1p_victim_only )
+ {
+ printt( "PAIN_DEATH_DEBUG: EmitSoundOnEntityOnlyToPlayer - " + sound )
+ }
+ }
+
+ if ( attacker.IsPlayer() )
+ {
+ foreach ( sound in alias_3p_except_attacker )
+ {
+ printt( "PAIN_DEATH_DEBUG: EmitSoundOnEntityExceptToPlayer - " + sound )
+ }
+
+ foreach ( sound in alias_3p_attacker_only )
+ {
+ printt( "PAIN_DEATH_DEBUG: EmitSoundOnEntityOnlyToPlayer - " + sound )
+ }
+ }
+ else
+ {
+ foreach ( sound in alias_3p_except_attacker )
+ {
+ printt( "PAIN_DEATH_DEBUG: EmitSoundOnEntity - " + sound )
+ }
+ }
+ #endif
+}
+
+void function TogglePainDeathDebug()
+{
+ file.painDeathDebug = !file.painDeathDebug
+ printt( "PainDeathDebug is " + file.painDeathDebug )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_passives.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_passives.gnut
new file mode 100644
index 00000000..1264686e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_passives.gnut
@@ -0,0 +1,1657 @@
+untyped
+
+global function Passives_Init
+
+global function InitPassives
+global function GivePassive
+global function GivePassiveLifeLong
+global function GiveTitanPassiveLifeLong
+global function TakePassive
+global function TakeAllPassives
+global function ScanMinimap
+global function MinimapPlayerConnected
+global function SoulHasPassive
+global function ScanMinimapUntilDeath
+global function GivePlayerPassivesFromSoul
+global function PrintAllPassives
+
+global function IsConscript
+
+global function UpdateMinimapStatusToOtherPlayers
+global function UpdateTitanMinimapStatusToOtherPlayers
+global function UpdateAIMinimapStatusToOtherPlayers
+global function UpdateMinimapStatus // moves to minimap script eventually?
+global function ApplyTitanWeaponPassives
+global function UpdateScorchHotStreakCoreMeter
+#if MP
+global function ApplyFDUpgradeWeaponPassives
+global function ApplyFDDerviedUpgrades
+#endif
+
+const FD_HOT_STREAK_DAMAGE_MAX = 10000
+const FD_HOT_STREAK_DECAY_TIME = 30.0 //1/2 this value until it the hotstreak falls off. 1/2 the value until it goes from full to empty.
+//const FD_HOT_STREAK_CORE_MULTIPLIER_MAX = 0.5
+
+function Passives_Init()
+{
+ RegisterSignal( "EndCloakedWallHangs" )
+ RegisterSignal( "EndCloakedWallruns" )
+
+ AddSpawnCallback( "npc_spectre", MinimapNPCSpawned )
+ AddSpawnCallback( "npc_soldier", MinimapNPCSpawned )
+ AddDeathCallback( "player", PassiveDeathCallback )
+
+ AddCallback_OnTitanGetsNewTitanLoadout( ApplyTitanWeaponPassives )
+
+ #if MP
+ if ( GetCurrentPlaylistVarInt( "aegis_upgrades", 0 ) == 1 )
+ AddCallback_OnUpdateDerivedPlayerTitanLoadout( ApplyFDDerviedUpgrades ) //Half of the meta functions, the other half lives in passives.gnut This is used for class mods.
+ #endif
+
+ level.wifiLeachInterval <- 2.5
+}
+
+function InitPassives( entity player )
+{
+ player.s.removePassiveOnDeath <- {}
+}
+
+#if MP
+void function ApplyFDDerviedUpgrades( entity player, TitanLoadoutDef loadout )
+{
+ array<ItemData> fdUpgrades = GetAllItemsOfType( eItemTypes.TITAN_FD_UPGRADE )
+ array<string> upgradeRefs
+ foreach ( ItemData upgrade in fdUpgrades )
+ {
+ if ( loadout.titanClass == upgrade.parentRef && !IsSubItemLocked( player, upgrade.ref, upgrade.parentRef ) )
+ {
+ upgradeRefs.append( upgrade.ref )
+ }
+ }
+ if ( loadout.titanClass == "ronin" )
+ ApplyDerivedRoninFDUpgrades( upgradeRefs, loadout )
+ else if ( loadout.titanClass == "northstar" )
+ ApplyDerivedNorthstarFDUpgrades( upgradeRefs, loadout )
+ else if ( loadout.titanClass == "vanguard" )
+ ApplyDerivedVanguardFDUpgrades( upgradeRefs, loadout )
+ else if ( loadout.titanClass == "ion" )
+ ApplyDerivedIonFDUpgrades( upgradeRefs, loadout )
+ else if ( loadout.titanClass == "tone" )
+ ApplyDerivedToneFDUpgrades( upgradeRefs, loadout )
+ else if ( loadout.titanClass == "scorch" )
+ ApplyDerivedScorchFDUpgrades( upgradeRefs, loadout )
+ else if ( loadout.titanClass == "legion" )
+ ApplyDerivedLegionFDUpgrades( upgradeRefs, loadout )
+}
+
+void function ApplyFDUpgradeWeaponPassives( entity titan, TitanLoadoutDef loadout )
+{
+ entity player
+ if ( titan.IsPlayer() )
+ player = titan
+ else if ( IsValid( titan.mySpawnOptions_ownerPlayer ) )
+ player = expect entity( titan.mySpawnOptions_ownerPlayer )
+ else
+ player = titan.GetBossPlayer()
+
+ if ( !IsValid( player ) )
+ return
+
+ array<ItemData> fdUpgrades = GetAllItemsOfType( eItemTypes.TITAN_FD_UPGRADE )
+ array<string> upgradeRefs
+ foreach ( ItemData upgrade in fdUpgrades )
+ {
+ if ( loadout.titanClass == upgrade.parentRef && !IsSubItemLocked( player, upgrade.ref, upgrade.parentRef ) )
+ upgradeRefs.append( upgrade.ref )
+ }
+
+ if ( loadout.titanClass == "ronin" )
+ ApplyRoninFDUpgrades( upgradeRefs, titan, loadout )
+ else if ( loadout.titanClass == "northstar" )
+ ApplyNorthstarFDUpgrades( upgradeRefs, titan, loadout )
+ else if ( loadout.titanClass == "vanguard" )
+ ApplyVanguardFDUpgrades( upgradeRefs, titan, loadout )
+ else if ( loadout.titanClass == "ion" )
+ ApplyIonFDUpgrades( upgradeRefs, titan, loadout )
+ else if ( loadout.titanClass == "tone" )
+ ApplyToneFDUpgrades( upgradeRefs, titan, loadout )
+ else if ( loadout.titanClass == "scorch" )
+ ApplyScorchFDUpgrades( upgradeRefs, titan, loadout )
+ else if ( loadout.titanClass == "legion" )
+ ApplyLegionFDUpgrades( upgradeRefs, titan, loadout )
+}
+
+void function ApplyDerivedRoninFDUpgrades( array<string> upgradeRefs, TitanLoadoutDef loadout )
+{
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_ronin_defense_tier_1":
+ loadout.setFileMods.append( "fd_health_upgrade" )
+ break
+ }
+ }
+}
+
+void function ApplyRoninFDUpgrades( array<string> upgradeRefs, entity titan, TitanLoadoutDef loadout )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Ejecting
+ return
+
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_ronin_utility_tier_1":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_phase_charges" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_ronin_utility_tier_2":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_phase_distance" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_ronin_defense_tier_2":
+ float titanShieldHealth = GetTitanSoulShieldHealth( soul )
+ soul.SetShieldHealthMax( int( titanShieldHealth * 1.5 ) )
+ break
+ case "fd_upgrade_ronin_weapon_tier_1":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_MELEE )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_sword_upgrade" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_ronin_weapon_tier_2":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_LEFT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_sword_block" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_ronin_ultimate":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_duration" )
+ weapon.SetMods( mods )
+ break
+ }
+ }
+}
+
+void function ApplyDerivedNorthstarFDUpgrades( array<string> upgradeRefs, TitanLoadoutDef loadout )
+{
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_northstar_defense_tier_1":
+ loadout.setFileMods.append( "fd_health_upgrade" )
+ break
+ }
+ }
+}
+
+void function ApplyNorthstarFDUpgrades( array<string> upgradeRefs, entity titan, TitanLoadoutDef loadout )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Ejecting
+ return
+
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_northstar_utility_tier_1":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_LEFT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_explosive_trap" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_northstar_utility_tier_2":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_LEFT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_trap_charges" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_northstar_defense_tier_2":
+ float titanShieldHealth = GetTitanSoulShieldHealth( soul )
+ soul.SetShieldHealthMax( int( titanShieldHealth * 1.5 ) )
+ break
+ case "fd_upgrade_northstar_weapon_tier_1":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_upgrade_charge" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_northstar_weapon_tier_2":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_upgrade_crit" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_northstar_ultimate":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_twin_cluster" )
+ weapon.SetMods( mods )
+ break
+ }
+ }
+}
+
+void function ApplyDerivedVanguardFDUpgrades( array<string> upgradeRefs, TitanLoadoutDef loadout )
+{
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_vanguard_defense_tier_1":
+ loadout.setFileMods.append( "fd_health_upgrade" )
+ break
+ }
+ }
+}
+
+void function ApplyVanguardFDUpgrades( array<string> upgradeRefs, entity titan, TitanLoadoutDef loadout )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Ejecting
+ return
+
+ entity primaryWeapon = titan.GetMainWeapons()[0]
+ array<string> primaryMods = primaryWeapon.GetMods()
+ primaryMods.append( "fd_balance" )
+ primaryWeapon.SetMods( primaryMods )
+
+ entity ordanance = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ array<string> ordananceMods = ordanance.GetMods()
+ ordananceMods.append( "fd_balance" )
+ ordanance.SetMods( ordananceMods )
+
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_vanguard_utility_tier_1":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_vanguard_utility_1" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_vanguard_utility_tier_2":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_vanguard_utility_2" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_vanguard_defense_tier_2":
+ float titanShieldHealth = GetTitanSoulShieldHealth( soul )
+ soul.SetShieldHealthMax( int( titanShieldHealth * 1.5 ) )
+ break
+ case "fd_upgrade_vanguard_weapon_tier_1":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_vanguard_weapon_1" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_vanguard_weapon_tier_2":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_vanguard_weapon_2" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_vanguard_ultimate":
+ if ( SoulHasPassive( soul, ePassives.PAS_VANGUARD_CORE1 ) ) //Has Arc Rounds, Choose Energy Transfer or Missile Racks
+ {
+ if ( RandomIntRange( 1, 100 ) <= 50 )
+ {
+ entity offhandWeapon = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ if ( IsValid( offhandWeapon ) )
+ {
+ array<string> mods = offhandWeapon.GetMods()
+ mods.append( "missile_racks" )
+ offhandWeapon.SetMods( mods )
+ }
+ }
+ else
+ {
+ entity offhandWeapon = titan.GetOffhandWeapon( OFFHAND_LEFT )
+ if ( IsValid( offhandWeapon ) )
+ {
+ array<string> mods = offhandWeapon.GetMods()
+ mods.append( "energy_transfer" )
+ offhandWeapon.SetMods( mods )
+ }
+ }
+ }
+ else if ( SoulHasPassive( soul, ePassives.PAS_VANGUARD_CORE2 ) ) //Has Missile Racks, Choose Energy Transfer or Arc Rounds
+ {
+ if ( RandomIntRange( 1, 100 ) <= 50 )
+ {
+ array<entity> weapons = GetPrimaryWeapons( titan )
+ if ( weapons.len() > 0 )
+ {
+ entity primaryWeapon = weapons[0]
+ if ( IsValid( primaryWeapon ) )
+ {
+ array<string> mods = primaryWeapon.GetMods()
+ mods.append( "arc_rounds" )
+ primaryWeapon.SetMods( mods )
+ primaryWeapon.SetWeaponPrimaryClipCount( primaryWeapon.GetWeaponPrimaryClipCountMax() )
+ }
+ }
+ }
+ else
+ {
+ entity offhandWeapon = titan.GetOffhandWeapon( OFFHAND_LEFT )
+ if ( IsValid( offhandWeapon ) )
+ {
+ array<string> mods = offhandWeapon.GetMods()
+ mods.append( "energy_transfer" )
+ offhandWeapon.SetMods( mods )
+ }
+ }
+ }
+ else if ( SoulHasPassive( soul, ePassives.PAS_VANGUARD_CORE3 ) ) //Has Energy Transfer, Choose Arc Rounds or Missile Racks
+ {
+ if ( RandomIntRange( 1, 100 ) <= 50 )
+ {
+ array<entity> weapons = GetPrimaryWeapons( titan )
+ if ( weapons.len() > 0 )
+ {
+ entity primaryWeapon = weapons[0]
+ if ( IsValid( primaryWeapon ) )
+ {
+ array<string> mods = primaryWeapon.GetMods()
+ mods.append( "arc_rounds" )
+ primaryWeapon.SetMods( mods )
+ primaryWeapon.SetWeaponPrimaryClipCount( primaryWeapon.GetWeaponPrimaryClipCountMax() )
+ }
+ }
+ }
+ else
+ {
+ entity offhandWeapon = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ if ( IsValid( offhandWeapon ) )
+ {
+ array<string> mods = offhandWeapon.GetMods()
+ mods.append( "missile_racks" )
+ offhandWeapon.SetMods( mods )
+ }
+ }
+ }
+ break
+ }
+ }
+}
+
+void function ApplyDerivedIonFDUpgrades( array<string> upgradeRefs, TitanLoadoutDef loadout )
+{
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_ion_utility_tier_1":
+ loadout.setFileMods.append( "fd_energy_regen" )
+ break
+ case "fd_upgrade_ion_utility_tier_2":
+ loadout.setFileMods.append( "fd_energy_max" )
+ break
+ case "fd_upgrade_ion_defense_tier_1":
+ loadout.setFileMods.append( "fd_health_upgrade" )
+ break
+ }
+ }
+}
+
+void function ApplyIonFDUpgrades( array<string> upgradeRefs, entity titan, TitanLoadoutDef loadout )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Ejecting
+ return
+
+ entity primaryWeapon = titan.GetMainWeapons()[0]
+ array<string> primaryMods = primaryWeapon.GetMods()
+ primaryMods.append( "fd_balance" )
+ primaryWeapon.SetMods( primaryMods )
+
+ entity ordanance = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ array<string> ordananceMods = ordanance.GetMods()
+ ordananceMods.append( "fd_balance" )
+ ordanance.SetMods( ordananceMods )
+
+ //entity utilityWeapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ //array<string> utilityMods = utilityWeapon.GetMods()
+ //utilityMods.append( "fd_balance" )
+ //utilityWeapon.SetMods( utilityMods )
+
+ entity coreWeapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+ array<string> coreMods = coreWeapon.GetMods()
+ coreMods.append( "fd_balance" )
+ coreWeapon.SetMods( coreMods )
+
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_ion_defense_tier_2":
+ float titanShieldHealth = GetTitanSoulShieldHealth( soul )
+ soul.SetShieldHealthMax( int( titanShieldHealth * 1.5 ) )
+ break
+ case "fd_upgrade_ion_weapon_tier_1":
+ entity ordanance = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ array<string> ordananceMods = ordanance.GetMods()
+ ordananceMods.append( "fd_laser_upgrade" )
+ ordanance.SetMods( ordananceMods )
+ break
+ case "fd_upgrade_ion_weapon_tier_2":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_split_shot_cost" )
+ weapon.SetMods( mods )
+ break
+
+ case "fd_upgrade_ion_ultimate":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_laser_cannon" )
+ weapon.SetMods( mods )
+ break
+ }
+ }
+}
+
+void function ApplyDerivedToneFDUpgrades( array<string> upgradeRefs, TitanLoadoutDef loadout )
+{
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_tone_defense_tier_1":
+ loadout.setFileMods.append( "fd_health_upgrade" )
+ break
+ }
+ }
+}
+
+void function ApplyToneFDUpgrades( array<string> upgradeRefs, entity titan, TitanLoadoutDef loadout )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Ejecting
+ return
+
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_tone_utility_tier_1":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_sonar_duration" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_tone_utility_tier_2":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_sonar_damage_amp" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_tone_defense_tier_2":
+ float titanShieldHealth = GetTitanSoulShieldHealth( soul )
+ soul.SetShieldHealthMax( int( titanShieldHealth * 1.5 ) )
+ break
+ case "fd_upgrade_tone_weapon_tier_1":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_splasher_rounds" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_tone_weapon_tier_2":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_tone_weapon_2" )
+ weapon.SetMods( mods )
+ weapon.SetWeaponPrimaryClipCount( weapon.GetWeaponPrimaryClipCountMax() )
+ break
+ case "fd_upgrade_tone_ultimate":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_salvo_core" )
+ weapon.SetMods( mods )
+ break
+ }
+ }
+}
+
+void function ApplyDerivedScorchFDUpgrades( array<string> upgradeRefs, TitanLoadoutDef loadout )
+{
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_scorch_defense_tier_1":
+ loadout.setFileMods.append( "fd_health_upgrade" )
+ break
+ }
+ }
+}
+
+void function ApplyScorchFDUpgrades( array<string> upgradeRefs, entity titan, TitanLoadoutDef loadout )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Ejecting
+ return
+
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_scorch_utility_tier_1":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_hot_streak" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_scorch_utility_tier_2":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_fire_damage_upgrade" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_scorch_defense_tier_2":
+ float titanShieldHealth = GetTitanSoulShieldHealth( soul )
+ soul.SetShieldHealthMax( int( titanShieldHealth * 1.5 ) )
+ break
+ case "fd_upgrade_scorch_weapon_tier_1":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ if ( !mods.contains( "fd_wpn_upgrade_2" ) )
+ {
+ mods.append( "fd_wpn_upgrade_1" )
+ weapon.SetMods( mods )
+ weapon.SetWeaponPrimaryClipCount( weapon.GetWeaponPrimaryClipCountMax() )
+ }
+ break
+ case "fd_upgrade_scorch_weapon_tier_2":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.fastremovebyvalue( "fd_wpn_upgrade_1" )
+ mods.append( "fd_wpn_upgrade_2" )
+ weapon.SetMods( mods )
+ weapon.SetWeaponPrimaryClipCount( weapon.GetWeaponPrimaryClipCountMax() )
+ break
+ case "fd_upgrade_scorch_ultimate":
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_explosive_barrel" )
+ weapon.SetMods( mods )
+ break
+ }
+ }
+}
+
+void function ApplyDerivedLegionFDUpgrades( array<string> upgradeRefs, TitanLoadoutDef loadout )
+{
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_legion_defense_tier_1":
+ loadout.setFileMods.append( "fd_health_upgrade" )
+ break
+ }
+ }
+}
+
+void function ApplyLegionFDUpgrades( array<string> upgradeRefs, entity titan, TitanLoadoutDef loadout )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Ejecting
+ return
+
+ foreach ( upgrade in upgradeRefs )
+ {
+ switch ( upgrade )
+ {
+ case "fd_upgrade_legion_utility_tier_1":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_closerange_helper" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_legion_utility_tier_2":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_longrange_helper" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_legion_defense_tier_2":
+ float titanShieldHealth = GetTitanSoulShieldHealth( soul )
+ soul.SetShieldHealthMax( int( titanShieldHealth * 1.5 ) )
+ break
+ case "fd_upgrade_legion_weapon_tier_1":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_piercing_shots" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_legion_weapon_tier_2":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "fd_gun_shield_redirect" )
+ weapon.SetMods( mods )
+ break
+ case "fd_upgrade_legion_ultimate":
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ if ( !mods.contains( "pas_legion_weapon" ) )
+ {
+ mods.append( "pas_legion_weapon" )
+ weapon.SetMods( mods )
+ weapon.SetWeaponPrimaryClipCount( weapon.GetWeaponPrimaryClipCountMax() )
+ }
+ if ( !mods.contains( "pas_legion_spinup" ) )
+ {
+ mods.append( "pas_legion_spinup" )
+ weapon.SetMods( mods )
+ }
+ weapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+ mods = weapon.GetMods()
+ if ( !mods.contains( "pas_legion_smartcore" ) )
+ {
+ mods.append( "pas_legion_smartcore" )
+ }
+ weapon.SetMods( mods )
+ weapon = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ mods = weapon.GetMods()
+ if ( !mods.contains( "pas_legion_chargeshot" ) )
+ {
+ mods.append( "pas_legion_chargeshot" )
+ }
+ weapon.SetMods( mods )
+ weapon = titan.GetOffhandWeapon( OFFHAND_LEFT )
+ mods = weapon.GetMods()
+ mods.append( "fd_gun_shield" )
+ weapon.SetMods( mods )
+ break
+ }
+ }
+}
+#endif
+
+void function ApplyTitanWeaponPassives( entity titan, TitanLoadoutDef loadout )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Ejecting
+ return
+
+ if ( loadout.titanClass == "ronin" && loadout.isPrime == "titan_is_prime" )
+ {
+ array<int> offhandSlots = [ OFFHAND_MELEE, OFFHAND_LEFT, OFFHAND_RIGHT ]
+
+ foreach ( slot in offhandSlots )
+ {
+ entity weapon = titan.GetOffhandWeapon( slot )
+ array<string> mods = weapon.GetMods()
+ mods.append( "modelset_prime" )
+ weapon.SetMods( mods )
+ }
+ }
+
+ foreach ( passive, value in soul.passives )
+ {
+ if ( !value )
+ continue
+
+ switch ( passive )
+ {
+ case ePassives.PAS_ION_TRIPWIRE:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_ion_tripwire" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_ION_VORTEX:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_LEFT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_ion_vortex" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_ION_LASERCANNON:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_ion_lasercannon" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_ION_WEAPON_ADS:
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_ion_weapon_ads" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_NORTHSTAR_WEAPON:
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_northstar_weapon" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_NORTHSTAR_TRAP:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_LEFT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_northstar_trap" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_NORTHSTAR_CLUSTER:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_northstar_cluster" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_NORTHSTAR_OPTICS:
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_northstar_optics" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_SCORCH_SELFDMG:
+ soul.SetPreventCrits( true )
+ break
+
+ case ePassives.PAS_SCORCH_WEAPON:
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_scorch_weapon" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_SCORCH_SHIELD:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_LEFT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_scorch_shield" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_SCORCH_FIREWALL:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_scorch_firewall" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_SCORCH_FLAMECORE:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_scorch_flamecore" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_LEGION_WEAPON:
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_legion_weapon" )
+ weapon.SetMods( mods )
+ int max = weapon.GetWeaponPrimaryClipCountMax()
+ weapon.SetWeaponPrimaryClipCount( max )
+ break
+
+ case ePassives.PAS_LEGION_SPINUP:
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_legion_spinup" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_LEGION_SMARTCORE:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_legion_smartcore" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_LEGION_CHARGESHOT:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ORDNANCE )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_legion_chargeshot" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_TONE_WEAPON:
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_tone_weapon" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_TONE_BURST:
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_tone_burst" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_TONE_ROCKETS:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_tone_rockets" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_TONE_SONAR:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_tone_sonar" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_RONIN_WEAPON:
+ entity weapon = titan.GetMainWeapons()[0]
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_ronin_weapon" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_RONIN_ARCWAVE:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_RIGHT )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_ronin_arcwave" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_RONIN_PHASE:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_ronin_phase" )
+ weapon.SetMods( mods )
+ break
+
+ case ePassives.PAS_VANGUARD_REARM:
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ array<string> mods = weapon.GetMods()
+ mods.append( "pas_vanguard_rearm" )
+ weapon.SetMods( mods )
+ break
+ }
+ }
+
+ #if MP
+ if ( GetCurrentPlaylistVarInt( "aegis_upgrades", 0 ) == 1 ) //Necessary to occur after normal weapon mods are assigned from passives.
+ ApplyFDUpgradeWeaponPassives( titan, loadout )
+ #endif
+}
+
+function GivePassive( entity player, int passive )
+{
+ if ( IsSoul( player ) )
+ {
+ entity soul = player
+ Assert( passive in level.titanPassives, "This is not a titan passive" )
+ soul.passives[ passive ] = true
+
+ entity titan = soul.GetTitan()
+ if ( IsValid( titan ) && titan.IsPlayer() )
+ GiveTitanPassiveLifeLong( titan, passive ) //This actually loops back around to GivePassive
+ return
+ }
+
+ // printt( "give passive " + GetPassiveName( passive ), passive )
+ player.GivePassive( passive )
+
+ // enter/exit functions for specific passives
+ switch ( passive )
+ {
+ case ePassives.PAS_MINIMAP_AI:
+ case ePassives.PAS_MINIMAP_ALL:
+ case ePassives.PAS_MINIMAP_PLAYERS:
+ UpdateMinimapStatus( player )
+ break
+
+ case ePassives.PAS_CONSCRIPT:
+ thread PlayerConscription( player )
+ break
+
+ case ePassives.PAS_WIFI_SPECTRE:
+ thread PlayerSpectreWifi( player )
+ break
+
+ case ePassives.PAS_FAST_SWAP:
+ player.GiveExtraWeaponMod( "pas_fast_swap" )
+ break
+
+ case ePassives.PAS_POWER_CELL:
+ player.GiveExtraWeaponMod( "pas_power_cell" )
+ break
+
+ case ePassives.PAS_WALLHANG:
+ player.GiveExtraWeaponMod( "pas_wallhang" )
+ break
+
+ case ePassives.PAS_FAST_HEALTH_REGEN:
+ player.GiveExtraWeaponMod( "pas_fast_health_regen" )
+ break
+
+ case ePassives.PAS_DEFENSIVE_CORE:
+ player.GiveExtraWeaponMod( "pas_defensive_core" )
+ break
+
+ case ePassives.PAS_RUN_AND_GUN:
+ player.GiveExtraWeaponMod( "pas_run_and_gun" )
+ break
+
+ case ePassives.PAS_ORDNANCE_PACK:
+ player.GiveExtraWeaponMod( "pas_ordnance_pack" )
+ break
+
+ case ePassives.PAS_FAST_RELOAD:
+ player.GiveExtraWeaponMod( "pas_fast_reload" )
+ break
+
+ case ePassives.PAS_ASSAULT_REACTOR:
+ player.GiveExtraWeaponMod( "mod_ordnance_core" )
+ break
+
+ case ePassives.PAS_MARATHON_CORE:
+ player.GiveExtraWeaponMod( "mod_marathon_core" )
+ break
+
+ case ePassives.PAS_CLOAKED_WALLHANG:
+ thread CloakedWallHangs( player )
+ break
+
+ case ePassives.PAS_CLOAKED_WALLRUN:
+ thread CloakedWallruns( player )
+ break
+
+ case ePassives.PAS_SMOKE_SIGHT:
+ Remote_CallFunction_Replay( player, "ServerCallback_BeginSmokeSight" )
+ break
+ }
+}
+
+string function GetPassiveName( int passive )
+{
+ Assert( passive in level.passiveEnumFromPassive, "Passive bitfield: " + passive + " does not exist" )
+
+ return expect string( level.passiveEnumFromPassive[ passive ] )
+}
+
+function TakePassive( entity player, int passive )
+{
+ if ( IsSoul( player ) )
+ {
+ entity soul = player
+ Assert( passive in level.titanPassives, "This is not a titan passive" )
+ soul.passives[ passive ] = false
+ entity titan = soul.GetTitan()
+ if ( IsValid( titan ) && titan.IsPlayer() )
+ TakePassive( titan, passive )
+ return
+ }
+
+ //printt( "take passive " + PassiveEnumFromBitfield( passive ) )
+ player.RemovePassive( passive )
+
+ // enter/exit functions for specific passives
+ switch ( passive )
+ {
+ case ePassives.PAS_MINIMAP_AI:
+ case ePassives.PAS_MINIMAP_ALL:
+ case ePassives.PAS_MINIMAP_PLAYERS:
+ UpdateMinimapStatus( player )
+ break
+
+ case ePassives.PAS_FAST_SWAP:
+ player.TakeExtraWeaponMod( "pas_fast_swap" )
+ break
+
+ case ePassives.PAS_POWER_CELL:
+ player.TakeExtraWeaponMod( "pas_power_cell" )
+ break
+
+ case ePassives.PAS_WALLHANG:
+ player.TakeExtraWeaponMod( "pas_wallhang" )
+ break
+
+ case ePassives.PAS_FAST_HEALTH_REGEN:
+ player.TakeExtraWeaponMod( "pas_fast_health_regen" )
+ break
+
+ case ePassives.PAS_DEFENSIVE_CORE:
+ player.TakeExtraWeaponMod( "pas_defensive_core" )
+ break
+
+ case ePassives.PAS_RUN_AND_GUN:
+ player.TakeExtraWeaponMod( "pas_run_and_gun" )
+ break
+
+ case ePassives.PAS_ORDNANCE_PACK:
+ player.TakeExtraWeaponMod( "pas_ordnance_pack" )
+ break
+
+ case ePassives.PAS_FAST_RELOAD:
+ player.TakeExtraWeaponMod( "pas_fast_reload" )
+ break
+
+ case ePassives.PAS_ASSAULT_REACTOR:
+ player.TakeExtraWeaponMod( "mod_ordnance_core" )
+ break
+
+ case ePassives.PAS_MARATHON_CORE:
+ player.TakeExtraWeaponMod( "mod_marathon_core" )
+ break
+
+ case ePassives.PAS_CLOAKED_WALLHANG:
+ player.Signal( "EndCloakedWallHangs" )
+ break
+
+ case ePassives.PAS_CLOAKED_WALLRUN:
+ player.Signal( "EndCloakedWallruns" )
+ break
+
+ case ePassives.PAS_SMOKE_SIGHT:
+ Remote_CallFunction_Replay( player, "ServerCallback_EndSmokeSight" )
+ break
+ }
+}
+
+void function PassiveDeathCallback( entity player, var damageInfo )
+{
+ foreach ( int passive in player.s.removePassiveOnDeath )
+ {
+ TakePassive( player, passive )
+ }
+
+ player.s.removePassiveOnDeath = {}
+}
+
+function GivePassiveLifeLong( entity player, int passive )
+{
+ //Note: Badness happens if a burn card with passive tries to give a server flag!
+ Assert( !( passive in level.titanPassives ), "This is a titan passive" )
+
+ // give the passive for one life
+ player.s.removePassiveOnDeath[ passive ] <- passive
+ GivePassive( player, passive )
+}
+
+function GiveTitanPassiveLifeLong( entity player, int passive )
+{
+ Assert( passive in level.titanPassives, "This is a titan passive" )
+
+ // give the passive for one life
+ player.s.removePassiveOnDeath[ passive ] <- passive
+ GivePassive( player, passive )
+}
+
+function TakeAllPassives( entity player )
+{
+ foreach( passiveName, passive in _PassiveFromEnum )
+ {
+ if ( player.HasPassive( passive ) )
+ TakePassive( player, passive )
+ }
+
+ player.ClearExtraWeaponMods()
+ player.s.removePassiveOnDeath = {}
+}
+
+
+function GivePlayerPassivesFromSoul( entity player, entity soul )
+{
+ Assert( player == soul.GetTitan() )
+
+ foreach( passiveName, passive in _PassiveFromEnum ) //Since this is just a bitmask, we could just add all the soul's passives directly instead of trying to break it down to its components first like we do here. However, while it is less efficent, it's also easier to debug.
+ {
+ if ( soul.passives[ passive ] )
+ GiveTitanPassiveLifeLong( player, passive )
+ }
+}
+
+table function GetRevealParms( entity player )
+{
+ table Table = {}
+
+ if ( player.HasPassive( ePassives.PAS_MINIMAP_ALL ) )
+ {
+ Table.ai <- true
+ Table.players <- true
+ Table.titans <- true
+ }
+ else
+ {
+ Table.titans <- false
+
+ if ( player.HasPassive( ePassives.PAS_MINIMAP_PLAYERS ) )
+ Table.players <- true
+ else
+ Table.players <- false
+
+ if ( player.HasPassive( ePassives.PAS_MINIMAP_AI ) )
+ Table.ai <- true
+ else
+ Table.ai <- false
+ }
+
+ return Table
+}
+
+function UpdateMinimapStatusToOtherPlayers( entity player )
+{
+ int team = player.GetTeam()
+ array players = GetPlayerArray()
+ foreach ( otherPlayer in players )
+ {
+ // teammates are on minimap by default
+ if ( team == otherPlayer.GetTeam() )
+ continue
+
+ table reveal = GetRevealParms( expect entity( otherPlayer ) )
+ if ( reveal.players )
+ {
+ player.Minimap_AlwaysShow( 0, otherPlayer )
+ }
+ }
+}
+
+function UpdateTitanMinimapStatusToOtherPlayers( entity titan )
+{
+ int team = titan.GetTeam()
+ array players = GetPlayerArray()
+ foreach ( otherPlayer in players )
+ {
+ // teammates are on minimap by default
+ if ( team == otherPlayer.GetTeam() )
+ continue
+
+ table reveal = GetRevealParms( expect entity( otherPlayer ) )
+ if ( reveal.titans )
+ {
+ titan.Minimap_AlwaysShow( 0, otherPlayer )
+ }
+ }
+}
+
+function UpdateAIMinimapStatusToOtherPlayers( entity guy )
+{
+ int team = guy.GetTeam()
+ array players = GetPlayerArray()
+ foreach ( otherPlayer in players )
+ {
+ // teammates are on minimap by default
+ if ( team == otherPlayer.GetTeam() )
+ continue
+
+ table reveal = GetRevealParms( expect entity( otherPlayer ) )
+ if ( reveal.ai )
+ {
+ guy.Minimap_AlwaysShow( 0, otherPlayer )
+ }
+ }
+}
+
+function UpdateMinimapStatus( entity player )
+{
+ int team = player.GetTeam()
+ table reveal = GetRevealParms( player )
+
+ array players = GetPlayerArray()
+ if ( reveal.players )
+ {
+ foreach ( target in players )
+ {
+ if ( team != target.GetTeam() )
+ target.Minimap_AlwaysShow( 0, player )
+ }
+ }
+ else
+ {
+ foreach ( target in players )
+ {
+ if ( team != target.GetTeam() )
+ target.Minimap_DisplayDefault( 0, player )
+ }
+ }
+
+ array<entity> titans = GetNPCArrayByClass( "npc_titan" )
+ if ( reveal.titans )
+ {
+ foreach ( target in titans )
+ {
+ if ( team != target.GetTeam() )
+ target.Minimap_AlwaysShow( 0, player )
+ }
+ }
+ else
+ {
+ foreach ( target in titans )
+ {
+ if ( team != target.GetTeam() )
+ target.Minimap_DisplayDefault( 0, player )
+ }
+ }
+
+ array<entity> ai = GetNPCArrayByClass( "npc_soldier" )
+ ai.extend( GetNPCArrayByClass( "npc_spectre" ) )
+
+ if ( reveal.ai )
+ {
+ foreach ( target in ai )
+ {
+ if ( team != target.GetTeam() )
+ target.Minimap_AlwaysShow( 0, player )
+ }
+ }
+ else
+ {
+ foreach ( target in ai )
+ {
+ if ( team != target.GetTeam() )
+ target.Minimap_DisplayDefault( 0, player )
+ }
+ }
+
+// foreach ( target in ai )
+// {
+// if ( target.GetBossPlayer() == player )
+// target.Minimap_AlwaysShow( 0, player )
+// }
+}
+
+function ScanMinimapUntilDeath( entity player )
+{
+ player.EndSignal( "OnDeath" )
+ for ( ;; )
+ {
+ thread ScanMinimap( player, true )
+ wait 10.0
+ }
+}
+
+function ScanMinimap( entity player, bool playSound, float displayTime = 3.0 )
+{
+ // already has the passive?
+ if ( PlayerHasPassive( player, ePassives.PAS_MINIMAP_ALL ) )
+ return
+
+ player.EndSignal( "OnDeath" )
+
+ int handle = player.GetEncodedEHandle()
+ Remote_CallFunction_Replay( player, "ServerCallback_MinimapPulse", handle )
+
+ OnThreadEnd(
+ function () : ( player )
+ {
+ if ( IsValid( player ) )
+ TakePassive( player, ePassives.PAS_MINIMAP_ALL )
+ }
+ )
+
+ GivePassive( player, ePassives.PAS_MINIMAP_ALL )
+ if ( playSound )
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Burn_Card_Map_Hack_Radar_Pulse_V1_1P" )
+ wait displayTime
+}
+
+void function MinimapNPCSpawned( entity guy )
+{
+ // show up on minimap for player that has the passive
+
+ int team = guy.GetTeam()
+ if ( IsIMCOrMilitiaTeam( team ) == false )
+ return
+ array<entity> players = GetPlayerArrayOfEnemies( team )
+ foreach ( player in players )
+ {
+ if ( !PlayerRevealsNPCs( player ) )
+ continue
+
+ guy.Minimap_AlwaysShow( 0, player )
+ }
+}
+
+function PlayerRevealsPlayers( entity player )
+{
+ return player.HasPassive( ePassives.PAS_MINIMAP_PLAYERS ) || player.HasPassive( ePassives.PAS_MINIMAP_ALL )
+}
+
+function MinimapPlayerConnected( entity guy )
+{
+ int team = guy.GetTeam()
+ array<entity> players = GetPlayerArrayOfEnemies( team )
+ foreach ( player in players )
+ {
+ if ( !PlayerRevealsPlayers( player ) )
+ continue
+
+ guy.Minimap_AlwaysShow( 0, player )
+ }
+}
+
+function PlayerSpectreWifi( entity player )
+{
+ player.EndSignal( "OnDeath" )
+
+ for ( ;; )
+ {
+ if ( !PlayerHasPassive( player, ePassives.PAS_WIFI_SPECTRE ) )
+ return
+
+ LeechSurroundingSpectres( player.GetOrigin(), player )
+
+ wait level.wifiLeachInterval
+ }
+}
+
+const CLEAR_CONSCRIPTED_GRUNTS_ON_DEATH = false
+
+function PlayerConscription( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ if ( CLEAR_CONSCRIPTED_GRUNTS_ON_DEATH )
+ {
+ if ( "conscriptedGrunts" in player.s )
+ return
+
+ table conscriptedGrunts
+ if ( "conscriptedGrunts" in player.s )
+ {
+ conscriptedGrunts = expect table( player.s.conscriptedGrunts )
+ }
+ else
+ {
+ conscriptedGrunts = {}
+ player.s.conscriptedGrunts <- conscriptedGrunts
+ }
+
+ OnThreadEnd(
+ function () : ( player, conscriptedGrunts )
+ {
+ ClearConscriptedGrunts( player, conscriptedGrunts )
+ }
+ )
+ }
+
+ for ( ;; )
+ {
+ if ( !PlayerHasPassive( player, ePassives.PAS_CONSCRIPT ) )
+ return
+
+ if ( IsAlive( player ) )
+ {
+ ConscriptNearbyGrunts( player )
+ }
+ wait 1.5
+ }
+}
+
+function ConscriptNearbyGrunts( entity player )
+{
+ int team = player.GetTeam()
+ array<entity> grunts = GetNPCArrayEx( "npc_soldier", team, TEAM_ANY, player.GetOrigin(), 220 )
+
+ foreach ( grunt in grunts )
+ {
+ if ( !IsValid( grunt ) )
+ continue
+
+ if ( IsAlive( grunt.GetBossPlayer() ) )
+ continue
+
+ ConscriptGruntSquad( grunt, player, team )
+ WaitFrame()
+ }
+}
+
+function ConscriptGrunt( entity grunt, entity player, int team )
+{
+ EmitSoundOnEntity( grunt, "BurnCard_Conscription_TurnSoldier" )
+ grunt.Signal( "StopHardpointBehavior" )
+ SetTeam( grunt, team )
+ grunt.SetBossPlayer( player )
+ grunt.SetTitle( "#NPC_CONSCRIPT" )
+ grunt.ai.preventOwnerDamage = true
+
+ #if HAS_STATS
+ UpdatePlayerStat( player, "misc_stats", "gruntsConscripted", 1 )
+ #endif
+
+ if ( CLEAR_CONSCRIPTED_GRUNTS_ON_DEATH )
+ {
+ player.s.conscriptedGrunts[ grunt ] <- grunt
+ // printt( player + " Conscripted " + grunt + " to squadname " + grunt.kv.squadname )
+ }
+}
+
+function MakePlayerSquad( entity player )
+{
+ string squad = "player" + player.entindex() + "gruntSquad"
+ int index = 0
+ string squadName = squad + index
+ for ( ;; )
+ {
+ if ( GetNPCSquadSize( squadName ) == 0 )
+ return squadName
+
+ index++
+ squadName = squad + index
+ }
+}
+
+function ConscriptGruntSquad( entity grunt, entity player, int team )
+{
+ array<entity> grunts
+ string gruntSquad = expect string( grunt.Get( "squadname" ) )
+
+ if ( gruntSquad == "" )
+ grunts.append( grunt )
+ else
+ grunts = GetNPCArrayBySquad( gruntSquad )
+
+ foreach ( guy in grunts )
+ {
+ if ( IsAlive( guy.GetBossPlayer() ) )
+ continue
+ if ( guy.GetTeam() != team )
+ continue
+
+ ConscriptGrunt( guy, player, team )
+ }
+}
+
+function ClearConscriptedGrunts( entity player, table conscriptedGrunts )
+{
+ foreach ( grunt in conscriptedGrunts )
+ {
+ expect entity( grunt )
+ if ( !IsAlive( grunt ) )
+ continue
+
+ entity owner = grunt.GetBossPlayer()
+ if ( IsValid( owner ) && owner != player )
+ continue
+
+ grunt.ClearBossPlayer()
+ asset model = grunt.GetModelName()
+ int team
+ if ( model.find( "imc" ) != null )
+ {
+ team = TEAM_IMC
+ }
+ else
+ {
+ team = TEAM_MILITIA
+ }
+
+ var title = grunt.GetSettingTitle()
+ if ( title != null && title != "" )
+ {
+ grunt.SetTitle( title )
+ FixupTitle( grunt )
+ }
+
+ grunt.Signal( "StopHardpointBehavior" )
+ SetTeam( grunt, team )
+ }
+
+ delete player.s.conscriptedGrunts
+}
+
+bool function IsConscript( entity guy )
+{
+ return IsAlive( guy.GetBossPlayer() )
+}
+
+bool function SoulHasPassive( entity soul, int passive )
+{
+ return expect bool( soul.passives[ passive ] )
+}
+
+function PrintAllPassives( entity player )
+{
+ foreach( passiveName, passive in _PassiveFromEnum )
+ {
+ if ( player.HasPassive( passive ) )
+ printt( "Player " + player + " has passive: " + passiveName )
+ }
+}
+
+function CloakedWallHangs( entity player )
+{
+ player.Signal( "EndCloakedWallHangs" )
+ player.EndSignal( "EndCloakedWallHangs" )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnDestroy" )
+
+ float cloakStartTime
+ float rechargeStartTime
+
+ if ( !( "wallHangCloakDuration" in player.s ) )
+ player.s.wallHangCloakDuration <- WALLHANG_CLOAK_DURATION
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( !IsValid( player ) )
+ return
+
+ if ( IsCloaked( player ) )
+ DisableCloak( player, 0 )
+ }
+ )
+
+ while ( true )
+ {
+ while ( !player.IsWallHanging() )
+ {
+ rechargeStartTime = Time()
+ WaitFrame()
+ player.s.wallHangCloakDuration += Time() - rechargeStartTime
+
+ if ( player.s.wallHangCloakDuration > WALLHANG_CLOAK_DURATION )
+ player.s.wallHangCloakDuration = WALLHANG_CLOAK_DURATION
+ }
+
+ if ( !IsCloaked( player ) && player.s.wallHangCloakDuration )
+ EnableCloak( player, expect float( player.s.wallHangCloakDuration ), WALLHANG_CLOAK_TRANSITION_TIME )
+
+ while ( player.s.wallHangCloakDuration && player.IsWallHanging() )
+ {
+ cloakStartTime = Time()
+ WaitFrame()
+ player.s.wallHangCloakDuration -= Time() - cloakStartTime
+
+ if ( player.s.wallHangCloakDuration < 0 )
+ player.s.wallHangCloakDuration = 0
+ }
+
+ if ( IsCloaked( player ) )
+ DisableCloak( player, WALLHANG_CLOAK_TRANSITION_TIME )
+
+ if ( player.IsWallHanging() )
+ WaitFrame()
+ }
+}
+
+function CloakedWallruns( entity player )
+{
+ player.Signal( "EndCloakedWallruns" )
+ player.EndSignal( "EndCloakedWallruns" )
+ player.EndSignal( "OnDeath" )
+
+ float cloakStartTime
+ float rechargeStartTime
+
+ if ( !( "wallRunCloakDuration" in player.s ) )
+ player.s.wallRunCloakDuration <- WALLRUN_CLOAK_DURATION
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( !IsValid( player ) )
+ return
+
+ if ( IsCloaked( player ) )
+ DisableCloak( player, 0 )
+ }
+ )
+
+ while ( true )
+ {
+ while ( !player.IsWallRunning() )
+ {
+ rechargeStartTime = Time()
+ WaitFrame()
+ player.s.wallRunCloakDuration += Time() - rechargeStartTime
+
+ if ( player.s.wallRunCloakDuration > WALLRUN_CLOAK_DURATION )
+ player.s.wallRunCloakDuration = WALLRUN_CLOAK_DURATION
+ }
+
+ if ( !IsCloaked( player ) && player.s.wallRunCloakDuration )
+ EnableCloak( player, expect float( player.s.wallRunCloakDuration ), WALLRUN_CLOAK_TRANSITION_TIME )
+
+ while ( player.s.wallRunCloakDuration && player.IsWallRunning() )
+ {
+ cloakStartTime = Time()
+ WaitFrame()
+ player.s.wallRunCloakDuration -= Time() - cloakStartTime
+
+ if ( player.s.wallRunCloakDuration < 0 )
+ player.s.wallRunCloakDuration = 0
+ }
+
+ if ( IsCloaked( player ) )
+ DisableCloak( player, WALLRUN_CLOAK_TRANSITION_TIME )
+
+ if ( player.IsWallRunning() )
+ WaitFrame()
+ }
+}
+
+//To avoid threads and callbacks, this is using any netFloat > 0.5 to mean full CoreMeter scaling.
+void function UpdateScorchHotStreakCoreMeter( entity attacker, float damage )
+{
+ if ( !attacker.IsPlayer() )
+ return
+
+ float baseValue = attacker.GetPlayerNetFloat( "coreMeterModifier" )
+ float newValue = damage / FD_HOT_STREAK_DAMAGE_MAX * 0.5
+ float combinedValue = baseValue + newValue
+ if ( baseValue + newValue >= 0.5 )
+ combinedValue = 1.0
+
+ attacker.SetPlayerNetFloat( "coreMeterModifier", combinedValue )
+ attacker.SetPlayerNetFloatOverTime( "coreMeterModifier", 0.0, FD_HOT_STREAK_DECAY_TIME )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_ping.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_ping.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_ping.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_powerup.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_powerup.gnut
new file mode 100644
index 00000000..03b9fcfc
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_powerup.gnut
@@ -0,0 +1,93 @@
+untyped
+global function PowerUps_Init
+
+struct {
+ array<entity> powerupSpawns
+} file
+
+void function PowerUps_Init()
+{
+ SH_PowerUp_Init()
+
+ AddSpawnCallbackEditorClass( "script_ref", "script_power_up_other", AddPowerupSpawn )
+ AddCallback_OnTouchHealthKit( "item_powerup", OnPowerupCollected )
+ AddCallback_GameStateEnter( eGameState.Prematch, RespawnPowerups )
+}
+
+void function AddPowerupSpawn( entity spawnpoint )
+{
+ file.powerupSpawns.append( spawnpoint )
+}
+
+void function RespawnPowerups()
+{
+ foreach ( entity spawnpoint in file.powerupSpawns )
+ {
+ PowerUp powerupDef = GetPowerUpFromItemRef( expect string( spawnpoint.kv.powerUpType ) )
+ thread PowerupSpawnerThink( spawnpoint, powerupDef )
+ }
+}
+
+void function PowerupSpawnerThink( entity spawnpoint, PowerUp powerupDef )
+{
+ svGlobal.levelEnt.EndSignal( "CleanUpEntitiesForRoundEnd" )
+
+ entity base = CreatePropDynamic( powerupDef.baseModel, spawnpoint.GetOrigin(), spawnpoint.GetAngles(), 2 )
+ OnThreadEnd( function() : ( base )
+ {
+ base.Destroy()
+ })
+
+ while ( true )
+ {
+ if ( !powerupDef.spawnFunc() )
+ return
+
+ entity powerup = CreateEntity( "item_powerup" )
+
+ powerup.SetOrigin( base.GetOrigin() + powerupDef.modelOffset )
+ powerup.SetAngles( base.GetAngles() + powerupDef.modelAngles )
+ powerup.SetValueForModelKey( powerupDef.model )
+
+ DispatchSpawn( powerup )
+
+ // unless i'm doing something really dumb, this all has to be done after dispatchspawn to get the powerup to not have gravity
+ powerup.StopPhysics()
+ powerup.SetOrigin( base.GetOrigin() + powerupDef.modelOffset )
+ powerup.SetAngles( base.GetAngles() + powerupDef.modelAngles )
+
+ powerup.SetModel( powerupDef.model )
+ powerup.s.powerupRef <- powerupDef.itemRef
+
+ PickupGlow glow = CreatePickupGlow( powerup, powerupDef.glowColor.x.tointeger(), powerupDef.glowColor.y.tointeger(), powerupDef.glowColor.z.tointeger() )
+ glow.glowFX.SetOrigin( spawnpoint.GetOrigin() ) // want the glow to be parented to the powerup, but have the position of the spawnpoint
+
+ OnThreadEnd( function() : ( powerup )
+ {
+ if ( IsValid( powerup ) )
+ powerup.Destroy()
+ })
+
+ powerup.WaitSignal( "OnDestroy" )
+ wait powerupDef.respawnDelay
+ }
+}
+
+bool function OnPowerupCollected( entity player, entity healthpack )
+{
+ PowerUp powerup = GetPowerUpFromItemRef( expect string( healthpack.s.powerupRef ) )
+
+ if ( player.IsTitan() == powerup.titanPickup )
+ {
+ // hack because i couldn't figure out any other way to do this without modifying sh_powerup
+ // ensure we don't kill the powerup if it's a battery the player can't pickup
+ if ( ( powerup.index == ePowerUps.titanTimeReduction || powerup.index == ePowerUps.LTS_TitanTimeReduction ) && ( player.IsTitan() || PlayerHasMaxBatteryCount( player ) ) )
+ return false
+
+ // idk why the powerup.destroyFunc doesn't just return a bool? would mean they could just handle stuff like this in powerup code
+ powerup.destroyFunc( player )
+ return true // destroys the powerup
+ }
+
+ return false // keeps powerup alive
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_remote_functions_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_remote_functions_mp.gnut
new file mode 100644
index 00000000..567954b1
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_remote_functions_mp.gnut
@@ -0,0 +1,943 @@
+untyped
+
+global function RemoteFunctions_Init
+
+function RemoteFunctions_Init()
+{
+ Remote_BeginRegisteringFunctions()
+ _RegisteringFunctions = true
+
+ switch ( GetMapName() )
+ {
+ case "mp_colony":
+ RegisterServerVar( "ClientTiming", 0 )
+ Remote_RegisterFunction( "ServerCallback_CreateSpectrePaletteLighting" )
+ break
+
+ case "mp_wargames":
+ Remote_RegisterFunction( "ServerCallback_StopWargamesPodAmbienceSound" )
+ Remote_RegisterFunction( "ServerCallback_SpawnIMCFactionLeaderForIntro" )
+ Remote_RegisterFunction( "ServerCallback_SpawnMilitiaFactionLeaderForIntro" )
+ Remote_RegisterFunction( "ServerCallback_ClearFactionLeaderIntro" )
+ Remote_RegisterFunction( "ServerCallback_PlayPodTransitionScreenFX" )
+ break
+ }
+
+ Remote_RegisterFunction( "ServerCallback_DpadCommSay" )
+
+ Remote_RegisterFunction( "ServerCallback_CaptialShips" )
+
+ Remote_RegisterFunction( "ServerCallback_RewardReadyMessage" )
+ Remote_RegisterFunction( "ServerCallback_TitanReadyMessage" )
+
+ Remote_RegisterFunction( "ServerCallback_FPS_Test" )// This is for local FPS tests using myscripts for standardized optimization
+ Remote_RegisterFunction( "ServerCallback_FPS_Avg" )// general callback for more people to use - soupy
+ Remote_RegisterFunction( "DebugSetFrontline" )
+ Remote_RegisterFunction( "ServerCallback_StartCinematicNodeEditor" )
+ Remote_RegisterFunction( "ServerCallback_AISkitDebugMessage" ) //chad - temp to do debug lines on my client only during real MP matches
+ Remote_RegisterFunction( "ServerCallback_UpdateClientChallengeProgress" )
+ Remote_RegisterFunction( "ServerCallback_EventNotification" )
+
+ Remote_RegisterFunction( "SCB_RefreshBurnCardSelector" )
+ Remote_RegisterFunction( "ServerCallback_EjectConfirmed" )
+
+ Remote_RegisterFunction( "SCB_AddGrenadeIndicatorForEntity" )
+
+ Remote_RegisterFunction( "SCB_SetUserPerformance" )
+ Remote_RegisterFunction( "SCB_UpdateSponsorables" )
+ Remote_RegisterFunction( "SCB_ClientDebug" )
+
+ Remote_RegisterFunction( "ScriptCallback_UnlockAchievement" )
+ Remote_RegisterFunction( "ServerCallback_UpdateHeroStats" )
+
+ RegisterNetworkedVariable( "sentryTurretCount", SNDC_PLAYER_EXCLUSIVE, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "itemInventoryCount", SNDC_PLAYER_EXCLUSIVE, SNVT_INT, 0 )
+ Remote_RegisterFunction( "ServerCallback_GiveSentryTurret" )
+ Remote_RegisterFunction( "ServerCallback_TurretReport" )
+ Remote_RegisterFunction( "ServerCallback_TurretWorldIconShow" )
+ Remote_RegisterFunction( "ServerCallback_TurretWorldIconHide" )
+
+ // SIDE NOTIFICATION
+ Remote_RegisterFunction( "ServerCallback_LoadoutNotification" )
+ Remote_RegisterFunction( "ServerCallback_ItemNotification" )
+
+ Remote_RegisterFunction( "ServerCallback_AnnouncePathLevelUp" )
+
+ Remote_RegisterFunction( "ServerCallback_SonarPulseFromPosition" )
+
+ // Survival Start
+ Remote_RegisterFunction( "ServerCallback_OpenShopMenu" )
+ Remote_RegisterFunction( "ServerCallback_CloseShopMenu" )
+ RegisterServerVar( "survivorEventActive", false )
+ RegisterServerVar( "survivorEventEndTime", 0.0 )
+ RegisterServerVar( "survivorEventMilitiaScrap", 0 )
+ RegisterServerVar( "survivorEventIMCScrap", 0 )
+ // Survival End
+
+ // Shield core
+ Remote_RegisterFunction( "ServerCallback_StartShieldPlayer" )
+ Remote_RegisterFunction( "ServerCallback_StopShieldPlayer" )
+ Remote_RegisterFunction( "ServerCallback_AddShieldedPlayer" )
+ Remote_RegisterFunction( "ServerCallback_RemoveShieldedPlayer" )
+
+ //HACK: these nv's should eventually be code driven concepts
+ RegisterEntityVar_AllSynced( "player", "empEndTime", 0 )
+ RegisterEntityVar_AllSynced( "titan_soul", "PROTO_stickyExplosiveCount", 0 )
+ RegisterEntityVar_AllSynced( "titan_soul", "PROTO_trackerCount", 0 )
+
+ RegisterNetworkedVariable( "playerAllowedToMelee", SNDC_PLAYER_EXCLUSIVE, SNVT_BOOL, true )
+ RegisterNetworkedVariable( "playerAllowedToLeech", SNDC_PLAYER_EXCLUSIVE, SNVT_BOOL, true )
+ RegisterNetworkedVariable( "playerAllowedToSyncedMelee", SNDC_PLAYER_EXCLUSIVE, SNVT_BOOL, true )
+ RegisterNetworkedVariable( "rodeoBatteryCount", SNDC_TITAN_SOUL, SNVT_INT, 3 )
+
+ RegisterNetworkedVariable( "coreMeterModifier", SNDC_PLAYER_GLOBAL, SNVT_FLOAT_RANGE_OVER_TIME, 0.0, 0.0, 1.0 )
+
+ Remote_RegisterFunction( "SCB_SmartAmmoForceLockedOntoHudDraw" )
+
+ // we want to keep these as nv's because we want them to ignore kill replay
+ // -------------
+ RegisterEntityVar( "player", "nextRespawnTime", 0 )
+ // -------------
+ // end
+
+ RegisterEntityVar( "player", "titanQueueNum", NOT_IN_TITAN_QUEUE )
+
+ RegisterEntityVar_AllSynced( "player", "titanRequestNum", null )
+ RegisterEntityVar_AllSynced( "player", "titanRequestSkipped", 0 )
+ RegisterServerVar( "titanNextRequestEventTime", 0 )
+ RegisterServerVar( "titanNextRequestEventType", TITAN_REQUEST_WAITING_FOR_WAVE )
+
+ Remote_RegisterFunction( "ServerCallback_UpdateMarker" )
+ Remote_RegisterFunction( "DisablePrecacheErrors" )
+ Remote_RegisterFunction( "RestorePrecacheErrors" )
+
+
+ RegisterEntityVar_AllSynced( "player", "inSmoke", false )
+
+ Remote_RegisterFunction( "SCB_PlayTitanCockpitSounds" )
+ Remote_RegisterFunction( "SCB_StopTitanCockpitSounds" )
+
+ Remote_RegisterFunction( "ServerCallback_RewardUsed" )
+ Remote_RegisterFunction( "ServerCallback_VanguardUpgradeMessage" )
+
+ // SHOULD PROBABLY BE CODE
+ RegisterServerVar( "gameStateChangeTime", null )
+ RegisterServerVar( "gameState", -1 )
+ RegisterServerVar( "gameStartTime", null )
+ RegisterServerVar( "coopStartTime", null )
+ RegisterServerVar( "gameEndTime", 0.0 )
+ RegisterServerVar( "switchedSides", null )
+ RegisterServerVar( "replayDisabled", false )
+
+ //Round Winning Kill replay related
+ RegisterServerVar( "roundWinningKillReplayEnabled", false )
+ RegisterServerVar( "roundWinningKillReplayPlaying", false )
+ RegisterServerVar( "roundScoreLimitComplete", false )
+ RegisterServerVar( "roundWinningKillReplayEntHealthFrac", 0.0 ) //Using .nv because we need the non-rolled back value during round winning kill replay
+
+ RegisterServerVar( "badRepPresent", false )
+
+ RegisterServerVar( "nonStandardScoring", false )
+
+ RegisterServerVar( "roundBased", false )
+ RegisterServerVar( "roundStartTime", null )
+ RegisterServerVar( "roundEndTime", 0.0 )
+ RegisterServerVar( "roundsPlayed", 0 )
+
+ RegisterServerVar( "minPickLoadOutTime", null )
+ RegisterServerVar( "connectionTimeout", 0 )
+ RegisterServerVar( "winningTeam", null )
+ RegisterServerVar( "titanDropEnabledForTeam", TEAM_BOTH )
+ RegisterServerVar( "matchProgress", 0 )
+
+ // Linked Hardpoints
+ Remote_RegisterFunction( "ServerCallback_HardpointChanged" )
+
+ Remote_RegisterFunction( "ServerCallback_DisableHudForEvac" )
+
+ // Seconds
+ RegisterServerVar( "secondsTitanCheckTime", null )
+
+ // Attack/Defend based game modes
+ RegisterServerVar( "attackingTeam", null )
+
+ // Riffs
+ RegisterServerVar( "spawnAsTitan", null )
+ RegisterServerVar( "titanAvailability", null )
+ RegisterServerVar( "titanExitEnabled", null )
+ RegisterServerVar( "allowNPCs", null )
+ RegisterServerVar( "aiLethality", null )
+ RegisterServerVar( "minimapState", null )
+ RegisterServerVar( "ospState", null )
+ RegisterServerVar( "ammoLimit", null )
+ RegisterServerVar( "eliminationMode", null )
+ RegisterServerVar( "floorIsLava", null )
+ RegisterServerVar( "playerBleedout", null )
+ RegisterServerVar( "titanQueueLimit", 0 )
+ RegisterServerVar( "boostAvailability", 0 )
+ RegisterServerVar( "teamShareCoreMeter", 0 )
+ RegisterNetworkedVariable( "titanEjectEnabled", SNDC_GLOBAL, SNVT_BOOL, true )
+
+ // MFD
+ RegisterServerVar( "mfdOverheadPingDelay", 0 )
+
+ RegisterNetworkedVariable( "gameInfoStatusText", SNDC_PLAYER_EXCLUSIVE, SNVT_INT, -1 )
+ RegisterNetworkedVariable( "indicatorId", SNDC_PLAYER_EXCLUSIVE, SNVT_INT, 0 )
+
+ switch ( GameRules_GetGameMode() )
+ {
+ case ATTRITION:
+ RegisterNetworkedVariable( "AT_currentWave", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "AT_bankStartTime", SNDC_GLOBAL, SNVT_TIME, 0.0 )
+ RegisterNetworkedVariable( "AT_bankEndTime", SNDC_GLOBAL, SNVT_TIME, 0.0 )
+ RegisterNetworkedVariable( "AT_supplyDropExpireTime", SNDC_GLOBAL, SNVT_TIME, 0.0 )
+ RegisterNetworkedVariable( "shouldDisplayBountyPortraits", SNDC_GLOBAL, SNVT_BOOL )
+
+ RegisterNetworkedVariable( "camp1Ent", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "camp2Ent", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "camp3Ent", SNDC_GLOBAL, SNVT_ENTITY )
+
+ RegisterNetworkedVariable( "AcampProgress", SNDC_GLOBAL, SNVT_FLOAT_RANGE, 0.0, 0.0, 1.0 )
+ RegisterNetworkedVariable( "BcampProgress", SNDC_GLOBAL, SNVT_FLOAT_RANGE, 0.0, 0.0, 1.0 )
+
+ RegisterNetworkedVariable( "1AcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+ RegisterNetworkedVariable( "2AcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+ RegisterNetworkedVariable( "3AcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+ RegisterNetworkedVariable( "4AcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+ RegisterNetworkedVariable( "5AcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+
+ RegisterNetworkedVariable( "1BcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+ RegisterNetworkedVariable( "2BcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+ RegisterNetworkedVariable( "3BcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+ RegisterNetworkedVariable( "4BcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+ RegisterNetworkedVariable( "5BcampCount", SNDC_GLOBAL, SNVT_INT, -1 )
+
+ RegisterNetworkedVariable( "banksOpen", SNDC_GLOBAL, SNVT_BOOL, false )
+ RegisterNetworkedVariable( "preBankPhase", SNDC_GLOBAL, SNVT_BOOL, false )
+
+ Remote_RegisterFunction( "ServerCallback_AT_AnnouncePreParty" )
+ Remote_RegisterFunction( "ServerCallback_AT_AnnounceBoss" )
+ Remote_RegisterFunction( "ServerCallback_AT_AnnounceWaveOver" )
+ Remote_RegisterFunction( "ServerCallback_AT_YouKilledBoss" )
+ Remote_RegisterFunction( "ServerCallback_AT_YouCollectedBox" )
+ Remote_RegisterFunction( "ServerCallback_AT_WarnPlayerBounty" )
+ Remote_RegisterFunction( "ServerCallback_AT_YouSurvivedBounty" )
+ Remote_RegisterFunction( "ServerCallback_AT_TeammateSurvivedBounty" )
+ Remote_RegisterFunction( "ServerCallback_AT_PromptBossRodeo" )
+ Remote_RegisterFunction( "ServerCallback_AT_PromptBossExecute" )
+ Remote_RegisterFunction( "ServerCallback_AT_BossDoomed" )
+ Remote_RegisterFunction( "ServerCallback_AT_OnPlayerConnected" )
+ Remote_RegisterFunction( "ServerCallback_AT_UpdateMostWanted" )
+ Remote_RegisterFunction( "ServerCallback_AT_ScoreSplashStartMultTimer" )
+ Remote_RegisterFunction( "ServerCallback_AT_ShowRespawnBonusLoss" )
+ Remote_RegisterFunction( "ServerCallback_AT_BankOpen" )
+ Remote_RegisterFunction( "ServerCallback_AT_BankClose" )
+ Remote_RegisterFunction( "ServerCallback_AT_FinishDeposit" )
+ Remote_RegisterFunction( "ServerCallback_AT_ShowATScorePopup" )
+ Remote_RegisterFunction( "ServerCallback_AT_BossDamageScorePopup" )
+ Remote_RegisterFunction( "ServerCallback_AT_PlayerKillScorePopup" )
+ Remote_RegisterFunction( "ServerCallback_AT_ShowStolenBonus" )
+ Remote_RegisterFunction( "ServerCallback_AT_ClearCampAndBossPortraits" )
+ Remote_RegisterFunction( "ServerCallback_AT_PulseBankAntena" )
+ RegisterNetworkedVariable( "AT_bonusPoints", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_bonusPoints256", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_bonusPointMult", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_bonusMultTimer", SNDC_PLAYER_GLOBAL, SNVT_TIME, 0.0 )
+ RegisterNetworkedVariable( "AT_earnedPoints", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_earnedPoints256", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_totalPoints", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_totalPoints256", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_playerUploading", SNDC_PLAYER_GLOBAL, SNVT_BOOL, false )
+
+ /*
+ RegisterNetworkedVariable( "milGoldPlayer", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "milSilverPlayer", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "milBronzePlayer", SNDC_GLOBAL, SNVT_ENTITY )
+
+ RegisterNetworkedVariable( "milGoldPlayerBonus", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "milSilverPlayerBonus", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "milBronzePlayerBonus", SNDC_GLOBAL, SNVT_INT, 0 )
+
+ RegisterNetworkedVariable( "imcGoldPlayer", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "imcSilverPlayer", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "imcBronzePlayer", SNDC_GLOBAL, SNVT_ENTITY )
+
+ RegisterNetworkedVariable( "imcGoldPlayerBonus", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "imcSilverPlayerBonus", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "imcBronzePlayerBonus", SNDC_GLOBAL, SNVT_INT, 0 )
+ */
+
+#if CLIENT
+ CLAttrition_RegisterNetworkFunctions()
+#endif
+ break
+
+ case AI_TDM:
+ RegisterNetworkedVariable( "AT_bonusPoints", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_bonusPoints256", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_earnedPoints", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "AT_earnedPoints256", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+
+ RegisterNetworkedVariable( "IMCdefcon", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "MILdefcon", SNDC_GLOBAL, SNVT_INT, 0 )
+ Remote_RegisterFunction( "ServerCallback_AITDM_OnPlayerConnected" )
+#if CLIENT
+ CLAITDM_RegisterNetworkFunctions()
+#endif
+ break
+
+ case CAPTURE_POINT:
+ printt( "registering gamemode network variables for CAPTURE_POINT" )
+ RegisterNetworkedVariable( "objectiveAEnt", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "objectiveBEnt", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "objectiveCEnt", SNDC_GLOBAL, SNVT_ENTITY )
+
+ RegisterNetworkedVariable( "objectiveAState", SNDC_GLOBAL, SNVT_INT )
+ RegisterNetworkedVariable( "objectiveBState", SNDC_GLOBAL, SNVT_INT )
+ RegisterNetworkedVariable( "objectiveCState", SNDC_GLOBAL, SNVT_INT )
+
+ RegisterNetworkedVariable( "objectiveACappingTeam", SNDC_GLOBAL, SNVT_INT )
+ RegisterNetworkedVariable( "objectiveBCappingTeam", SNDC_GLOBAL, SNVT_INT )
+ RegisterNetworkedVariable( "objectiveCCappingTeam", SNDC_GLOBAL, SNVT_INT )
+
+ RegisterNetworkedVariable( "objectiveAProgress", SNDC_GLOBAL, SNVT_FLOAT_RANGE_OVER_TIME, 0.0, 0.0, 2.0 )
+ RegisterNetworkedVariable( "objectiveBProgress", SNDC_GLOBAL, SNVT_FLOAT_RANGE_OVER_TIME, 0.0, 0.0, 2.0 )
+ RegisterNetworkedVariable( "objectiveCProgress", SNDC_GLOBAL, SNVT_FLOAT_RANGE_OVER_TIME, 0.0, 0.0, 2.0 )
+
+ RegisterNetworkedVariable( "imcChevronState", SNDC_GLOBAL, SNVT_INT )
+ RegisterNetworkedVariable( "milChevronState", SNDC_GLOBAL, SNVT_INT )
+
+ Remote_RegisterFunction( "ServerCallback_CP_PlayMatchEndingMusic" )
+
+ /*
+ #if DEV
+ Remote_RegisterFunction( "ServerCallback_CP_PrintHardpointOccupants" )
+ #endif
+ */
+
+#if CLIENT
+ CLCapturePoint_RegisterNetworkFunctions()
+#endif
+ break
+
+ case CAPTURE_THE_FLAG:
+ RegisterNetworkedVariable( "imcFlag", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "milFlag", SNDC_GLOBAL, SNVT_ENTITY )
+
+ RegisterNetworkedVariable( "imcFlagHome", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "milFlagHome", SNDC_GLOBAL, SNVT_ENTITY )
+
+ RegisterNetworkedVariable( "imcFlagState", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "milFlagState", SNDC_GLOBAL, SNVT_INT, 0 )
+
+ RegisterNetworkedVariable( "flagReturnProgress", SNDC_GLOBAL, SNVT_FLOAT_RANGE_OVER_TIME, 0.0, 0.0, 1.0 )
+ RegisterNetworkedVariable( "returningFlag", SNDC_PLAYER_EXCLUSIVE, SNVT_BOOL, false )
+
+ Remote_RegisterFunction( "ServerCallback_CTF_PlayMatchNearEndMusic" )
+ Remote_RegisterFunction( "ServerCallback_CTF_StartReturnFlagProgressBar" )
+ Remote_RegisterFunction( "ServerCallback_CTF_StopReturnFlagProgressBar" )
+
+#if CLIENT
+ CLCaptureTheFlag_RegisterNetworkFunctions()
+#endif
+ break
+
+ case FORT_WAR:
+ {
+ Remote_RegisterFunction( "ServerCallback_FW_FriendlyBaseAttacked" )
+ Remote_RegisterFunction( "ServerCallback_FW_NotifyTitanRequired" )
+ Remote_RegisterFunction( "ServerCallback_FW_NotifyEnterFriendlyArea" )
+ Remote_RegisterFunction( "ServerCallback_FW_NotifyExitFriendlyArea" )
+ Remote_RegisterFunction( "ServerCallback_FW_NotifyEnterEnemyArea" )
+ Remote_RegisterFunction( "ServerCallback_FW_NotifyExitEnemyArea" )
+ Remote_RegisterFunction( "ServerCallback_FW_SetObjective" )
+ }
+ break
+
+ case MARKED_FOR_DEATH:
+ Remote_RegisterFunction( "ServerCallback_MFD_StartNewMarkCountdown" )
+ break
+
+ case LAST_TITAN_STANDING:
+ {
+ Remote_RegisterFunction( "ServerCallback_LTSThirtySecondWarning" )
+ }
+ break
+
+ case COLISEUM:
+ Remote_RegisterFunction( "ServerCallback_ColiseumDisplayTickets" )
+ Remote_RegisterFunction( "ServerCallback_ColiseumIntro" )
+ break
+
+ case SPEEDBALL:
+ RegisterNetworkedVariable( "flagCarrier", SNDC_GLOBAL, SNVT_ENTITY )
+ Remote_RegisterFunction( "ServerCallback_SPEEDBALL_LastPlayer" )
+ Remote_RegisterFunction( "ServerCallback_SPEEDBALL_LastFlagOwner" )
+#if CLIENT
+ CLSPEEDBALL_RegisterNetworkFunctions()
+#endif
+ break
+ case FD:
+ RegisterNetworkedVariable( "FD_waveState", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_waveActive", SNDC_GLOBAL, SNVT_BOOL, false )
+ RegisterNetworkedVariable( "FD_totalWaves", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_currentWave", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_activeHarvester", SNDC_GLOBAL, SNVT_ENTITY )
+ RegisterNetworkedVariable( "FD_restartsRemaining", SNDC_GLOBAL, SNVT_INT )
+
+ //AI Type counts
+ RegisterNetworkedVariable( "FD_AICount_Titan", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Titan_Nuke", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Titan_Mortar", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Titan_Arc", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Grunt", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Spectre", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Spectre_Mortar", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Stalker", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Reaper", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Ticks", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Drone", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Drone_Cloak", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Current", SNDC_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "FD_AICount_Total", SNDC_GLOBAL, SNVT_INT, 0 )
+
+ RegisterNetworkedVariable( "FD_wavePoints", SNDC_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "FD_wavePoints256", SNDC_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "FD_harvesterInvulTime", SNDC_GLOBAL, SNVT_TIME, 0 )
+ RegisterNetworkedVariable( "FD_nextWaveStartTime", SNDC_GLOBAL, SNVT_TIME, 0 )
+
+ RegisterNetworkedVariable( "FD_readyForNextWave", SNDC_PLAYER_GLOBAL, SNVT_BOOL, false )
+
+ Remote_RegisterFunction( "ServerCallback_FD_AnnouncePreParty" )
+ Remote_RegisterFunction( "ServerCallback_FD_ClearPreParty" )
+ Remote_RegisterFunction( "ServerCallback_FD_PingMinimap" )
+ Remote_RegisterFunction( "ServerCallback_FD_MoneyFly" )
+ Remote_RegisterFunction( "ServerCallback_FD_SayThanks" )
+ Remote_RegisterFunction( "ServerCallback_FD_DisplayHarvesterKiller" )
+ Remote_RegisterFunction( "ServerCallback_FD_NotifyStoreOpen" )
+
+ Remote_RegisterFunction( "ServerCallback_ShowCycleHint" )
+ Remote_RegisterFunction( "ServerCallback_OpenBoostStore" )
+ Remote_RegisterFunction( "ServerCallback_UpdateMoney" )
+ Remote_RegisterFunction( "ServerCallback_UpdateTeamReserve" )
+ Remote_RegisterFunction( "ServerCallback_EnableDropshipBoostStore" )
+ Remote_RegisterFunction( "ServerCallback_DisableDropshipBoostStore" )
+ Remote_RegisterFunction( "ServerCallback_UpdateTurretCount" )
+ Remote_RegisterFunction( "ServerCallback_UpdatePlayerHasBattery" )
+ Remote_RegisterFunction( "ServerCallback_UpdateAmpedWeaponState" )
+ Remote_RegisterFunction( "ServerCallback_BoostStoreTitanHint" )
+ Remote_RegisterFunction( "ServerCallback_UpdateGameStats" )
+ Remote_RegisterFunction( "ServerCallback_ShowGameStats" )
+ Remote_RegisterFunction( "ServerCallback_FD_UpdateWaveInfo" )
+ Remote_RegisterFunction( "ServerCallback_FD_NotifyMVP" )
+
+ RegisterNetworkedVariable( "boostStoreOpen", SNDC_GLOBAL, SNVT_BOOL, false )
+ RegisterNetworkedVariable( "playerHasBatteryBoost", SNDC_PLAYER_EXCLUSIVE, SNVT_BOOL, false )
+ RegisterNetworkedVariable( "FD_money", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "FD_money256", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+
+ RegisterNetworkedVariable( "numSuperRodeoGrenades", SNDC_PLAYER_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "numHarvesterShieldBoost", SNDC_PLAYER_GLOBAL, SNVT_INT, 0 )
+
+ RegisterNetworkedVariable( "showOverheadIcon", SNDC_TITAN_SOUL, SNVT_BOOL, false )
+
+#if CLIENT
+ CLFD_RegisterNetworkFunctions()
+#endif
+ break
+ }
+
+ #if DEVSCRIPTS
+ Dev_RemoteFunctions_Init()
+ #endif
+ Remote_RegisterFunction( "ServerCallback_NukeGrenadeWindowOpen" )
+ Remote_RegisterFunction( "ServerCallback_NukeGrenadeWindowClosed" )
+
+ Remote_RegisterFunction( "ServerCallback_RegisterTeamTitanMenuButtons" )
+ Remote_RegisterFunction( "ServerCallback_OpenTeamTitanMenu" )
+ Remote_RegisterFunction( "ServerCallback_CloseTeamTitanMenu" )
+ Remote_RegisterFunction( "ServerCallback_UpdateTeamTitanMenuTime" )
+ Remote_RegisterFunction( "ServerCallback_UpdateTeamTitanSelectionMenu" )
+
+ RegisterNetworkedVariable( "playerHardpointID", SNDC_PLAYER_EXCLUSIVE, SNVT_UNSIGNED_INT, 255 )
+
+
+ //Bleedout mechanic
+ //Remote_RegisterFunction( "ServerCallback_BLEEDOUT_StartFirstAidProgressBar" )
+ //Remote_RegisterFunction( "ServerCallback_BLEEDOUT_StopFirstAidProgressBar" )
+ //Remote_RegisterFunction( "ServerCallback_BLEEDOUT_ShowWoundedMarker" )
+ //Remote_RegisterFunction( "ServerCallback_BLEEDOUT_HideWoundedMarker" )
+
+ // NEW INTRO SYSTEM ( _cl_spawnslot_system.nut )
+ Remote_RegisterFunction( "ServerCallback_ResetEntSkyScale" )
+ Remote_RegisterFunction( "ServerCallback_SetEntSkyScale" )
+ Remote_RegisterFunction( "ServerCallback_ResetMapSettings" )
+ Remote_RegisterFunction( "ServerCallback_SetMapSettings" )
+ Remote_RegisterFunction( "ServerCallback_ToneMapping" )
+ Remote_RegisterFunction( "ServerCallback_LaptopFX" )
+
+ Remote_RegisterFunction( "ServerCallback_YouDied" )
+ Remote_RegisterFunction( "ServerCallback_YouRespawned" )
+
+ Remote_RegisterFunction( "ServerCallback_ShowDeathHint" )
+
+ Remote_RegisterFunction( "ServerCallback_ShowNextSpawnMessage" )
+ Remote_RegisterFunction( "ServerCallback_HideNextSpawnMessage" )
+
+ Remote_RegisterFunction( "ServerCallback_AnnounceWinner" )
+ Remote_RegisterFunction( "ServerCallback_AnnounceRoundWinner" )
+
+ //Remote_RegisterFunction( "ServerCallback_ToggleRankedInGame" )
+ Remote_RegisterFunction( "ServerCallback_GuidedMissileDestroyed" )
+ Remote_RegisterFunction( "ServerCallback_DoClientSideCinematicMPMoment" ) // hard to say if this is safe as fire and forget
+ Remote_RegisterFunction( "ServerCallback_SetAssistInformation" )
+ Remote_RegisterFunction( "ServerCallback_TitanEMP" )
+ Remote_RegisterFunction( "ServerCallback_AirburstIconUpdate" )
+ Remote_RegisterFunction( "ServerCallback_TitanCockpitBoot" ) // all this does is reset the tone mapping
+ Remote_RegisterFunction( "ServerCallback_DataKnifeStartLeech" )
+ Remote_RegisterFunction( "ServerCallback_DataKnifeCancelLeech" )
+ Remote_RegisterFunction( "ServerCallback_ControlPanelRefresh" )
+ Remote_RegisterFunction( "ServerCallback_TurretRefresh" )
+ Remote_RegisterFunction( "ServerCallback_CreateEvacShipIcon" )
+ Remote_RegisterFunction( "ServerCallback_DestroyEvacShipIcon" )
+ Remote_RegisterFunction( "ServerCallback_AddCapturePoint" )
+ Remote_RegisterFunction( "ServerCallback_TitanDisembark" ) // plays a line of dialog and calls "cockpit.StartDisembark()", and does tonemapping update, hides crosshair and names
+ Remote_RegisterFunction( "ServerCallback_OnEntityKilled" ) // handles obit and death recap
+ Remote_RegisterFunction( "ServerCallback_OnTitanKilled" ) // handles obit for titans
+ Remote_RegisterFunction( "ServerCallback_PlayerConnectedOrDisconnected" )
+ Remote_RegisterFunction( "SCBUI_PlayerConnectedOrDisconnected" )
+ Remote_RegisterFunction( "ServerCallback_PlayerChangedTeams" )
+ Remote_RegisterFunction( "ServerCallback_AnnounceTitanReservation" )
+
+ // IMPORTANT BUT MAYBE FINE AS A REMOTE CALL
+ Remote_RegisterFunction( "ServerCallback_ReplacementTitanSpawnpoint" )
+ Remote_RegisterFunction( "ServerCallback_TitanTookDamage" ) // should be converted into a code callback... similar to NotifyDidDamage
+ Remote_RegisterFunction( "ServerCallback_PilotTookDamage" ) // should be converted into a code callback... similar to NotifyDidDamage
+ Remote_RegisterFunction( "ServerCallback_PlayerUsesBurnCard" ) // tell a player that somebody used a burn card he should know about
+ Remote_RegisterFunction( "ServerCallback_ScreenShake" )
+ Remote_RegisterFunction( "ServerCallback_MinimapPulse" ) // if burn card moves to weapon then we dont need this
+ Remote_RegisterFunction( "ServerCallback_UpdateOverheadIconForNPC" )
+ Remote_RegisterFunction( "ServerCallback_SetFlagHomeOrigin" )
+ //Remote_RegisterFunction( "ServerCallback_OpenBurnCardMenu" )
+ //Remote_RegisterFunction( "ServerCallback_OpenDifficultyMenu" )
+ //Remote_RegisterFunction( "ServerCallback_ExitBurnCardMenu" )
+
+ // TITAN SHIELD BATTERY
+ Remote_RegisterFunction( "ServerCallback_StartBatteryTimer" )
+ Remote_RegisterFunction( "ServerCallback_TitanBatteryDown" )
+
+ // Ping
+ Remote_RegisterFunction( "ServerCallback_SpottingHighlight" )
+ Remote_RegisterFunction( "ServerCallback_SpottingDeny" )
+
+ // XP
+ Remote_RegisterFunction( "ServerCallback_PlayerLeveledUp" )
+ Remote_RegisterFunction( "ServerCallback_TitanLeveledUp" )
+ Remote_RegisterFunction( "ServerCallback_TitanXPAdded" )
+ Remote_RegisterFunction( "ServerCallback_WeaponLeveledUp" )
+ Remote_RegisterFunction( "ServerCallback_WeaponXPAdded" )
+ Remote_RegisterFunction( "ServerCallback_WeaponChallengeCompleted" )
+ Remote_RegisterFunction( "ServerCallback_TitanChallengeCompleted" )
+ Remote_RegisterFunction( "ServerCallback_PlayerChallengeCompleted" )
+
+ // Rodeo Battery
+ RegisterNetworkedVariable( "batteryOnBack", SNDC_PLAYER_EXCLUSIVE, SNVT_ENTITY )
+ RegisterNetworkedVariable( "offerRodeoBatteryLastUsedTime", SNDC_PLAYER_EXCLUSIVE, SNVT_TIME )
+ RegisterNetworkedVariable( "requestRodeoBatteryLastUsedTime", SNDC_PLAYER_EXCLUSIVE, SNVT_TIME )
+
+ Remote_RegisterFunction( "ServerCallback_UpdateRodeoRiderHud" )
+
+ RegisterEntityVar( "player", "permanentEventNotification", -1 )
+
+ //Titan Selection Screen - Clients don't have access to other player's persistent vars.
+ Remote_RegisterFunction( "ServerCallback_UpdateTeamTitanSelection" )
+
+ //FFA
+ Remote_RegisterFunction( "ServerCallback_FFASuddenDeathAnnouncement" )
+
+ // Bomb Mode
+ //Remote_RegisterFunction( "ServerCallback_AnnounceBombPickup" )
+ //Remote_RegisterFunction( "ServerCallback_AnnounceBombDropped" )
+ //Remote_RegisterFunction( "ServerCallback_AnnounceBombArmed" )
+ //Remote_RegisterFunction( "ServerCallback_AnnounceBombDisarmed" )
+ //Remote_RegisterFunction( "ServerCallback_AnnounceBombRespawned" )
+ //Remote_RegisterFunction( "ServerCallback_AnnounceBombExploded" )
+ //Remote_RegisterFunction( "ServerCallback_IncomingBombSpawnpoint" )
+
+ //Air Drops
+ Remote_RegisterFunction( "ServerCallback_IncomingAirdrop" )
+
+ // DEV ONLY
+ Remote_RegisterFunction( "ServerCallback_TitanLostHealthSegment" )
+
+ // LESS ESSENTIAL, CAN SHIP AS REMOTE FUNCTIONS
+ Remote_RegisterFunction( "ServerCallback_PlayScreenFXWarpJump" )
+ Remote_RegisterFunction( "ServerCallback_Phantom_Scan" )
+ Remote_RegisterFunction( "ServerCallback_RodeoScreenShake" )
+ Remote_RegisterFunction( "ServerCallback_RodeoerEjectWarning" ) // play pre-eject fx on titan
+ Remote_RegisterFunction( "ServerCallback_TitanEmbark" ) // used purely to play a single line of dialog
+ Remote_RegisterFunction( "ServerCallback_DogFight" )
+ Remote_RegisterFunction( "ServerCallback_Announcement" )
+ Remote_RegisterFunction( "ServerCallback_GameModeAnnouncement" )
+
+ Remote_RegisterFunction( "ServerCallback_ScoreEvent" )
+ Remote_RegisterFunction( "ServerCallback_CallingCardEvent" )
+
+ Remote_RegisterFunction( "ServerCallback_PlayConversation" )
+ Remote_RegisterFunction( "ServerCallback_PlayTitanConversation" )
+ Remote_RegisterFunction( "ServerCallback_PlaySquadConversation" )
+ Remote_RegisterFunction( "ServerCallback_CreateDropShipIntLighting" )
+ Remote_RegisterFunction( "ServerCallback_EvacObit" )
+ Remote_RegisterFunction( "ServerCallback_ShowTurretHint" )
+ Remote_RegisterFunction( "ServerCallback_HideTurretHint" )
+ Remote_RegisterFunction( "ServerCallback_ShowTurretInUseHint" )
+ Remote_RegisterFunction( "ServerCallback_UpdateBurnCardTitle" )
+ Remote_RegisterFunction( "ServerCallback_UpdateTitanModeHUD" )
+ Remote_RegisterFunction( "ServerCallback_GiveMatchLossProtection" )
+ Remote_RegisterFunction( "ServerCallback_SquadLeaderBonus" )
+ Remote_RegisterFunction( "ServerCallback_SquadLeaderDoubleXP" )
+
+ Remote_RegisterFunction( "ServerCallback_TitanFallWarning" )
+ Remote_RegisterFunction( "SCB_TitanDialogue" )
+
+ Remote_RegisterFunction( "ServerCallback_PlayLobbyScene" )
+
+ Remote_RegisterFunction( "ServerCallback_PilotCreatedGunShield" )
+
+ Remote_RegisterFunction( "ServerCallback_BeginSmokeSight" )
+ Remote_RegisterFunction( "ServerCallback_EndSmokeSight" )
+
+ Remote_RegisterFunction( "UpdateCachedPilotLoadout" )
+ Remote_RegisterFunction( "UpdateCachedTitanLoadout" )
+ Remote_RegisterFunction( "UpdateAllCachedPilotLoadouts" )
+ Remote_RegisterFunction( "UpdateAllCachedTitanLoadouts" )
+ Remote_RegisterFunction( "ServerCallback_UpdatePilotModel" )
+ Remote_RegisterFunction( "ServerCallback_UpdateTitanModel" )
+
+ // DEV ONLY
+ Remote_RegisterFunction( "ServerCallback_MVUpdateModelBounds" )
+ Remote_RegisterFunction( "ServerCallback_MVEnable" )
+ Remote_RegisterFunction( "ServerCallback_MVDisable" )
+ Remote_RegisterFunction( "ServerCallback_ModelViewerDisableConflicts" )
+
+ Remote_RegisterFunction( "ServerCallback_Test" )
+
+ // SHOULD BE REMOVED
+ Remote_RegisterFunction( "ServerCallback_SetClassicSkyScale" )
+ Remote_RegisterFunction( "ServerCallback_ResetClassicSkyScale" )
+
+ RegisterEntityVar( "player", "drawFastballHud", false )
+ RegisterEntityVar( "player", "reviveBleedingOut", 0.0, true )
+ RegisterEntityVar( "player", "reviveHealedTime", 0.0, true )
+
+ // SHOULD PROBABLY BE CODE
+ Remote_RegisterFunction( "ServerCallback_ClientInitComplete" )
+ RegisterServerVar( "forcedDialogueOnly", false )
+ //RegisterNetworkedVariable( "squadConversationEnabled", SNDC_GLOBAL, SNVT_BOOL, true ) //TEMP, remove when we do Miles meta data conversation controls
+ //RegisterNetworkedVariable( "titanOSDialogueEnabled", SNDC_GLOBAL, SNVT_BOOL, true ) //TEMP, remove when we do Miles meta data conversation controls
+ Remote_RegisterFunction( "SCB_LockCapturePointForTeam" )
+ Remote_RegisterFunction( "SCB_UnlockCapturePointForTeam" )
+
+ // SHOULD GO AWAY
+ Remote_RegisterFunction( "ServerCallback_SetEntityVar" )
+ Remote_RegisterFunction( "ServerCallback_SetServerVar" )
+
+
+ // POSSIBLY CAN STAY AS REMOTE FUNCTIONS
+ Remote_RegisterFunction( "ServerCallback_PlayTeamMusicEvent" )
+ Remote_RegisterFunction( "ServerCallback_PlayMusicToCompletion" )
+ Remote_RegisterFunction( "ServerCallback_PlayMusic" )
+ Remote_RegisterFunction( "ServerCallback_TitanCockpitEMP" )
+ Remote_RegisterFunction( "ServerCallback_PlayerEarnedBurnCard" )
+ Remote_RegisterFunction( "ServerCallback_PlayerStoppedBurnCard" )
+
+ // UI FUNCTIONS
+ Remote_RegisterFunction( "ServerCallback_SetUIVar" )
+ Remote_RegisterFunction( "ServerCallback_ShopPurchaseStatus" )
+ Remote_RegisterFunction( "ServerCallback_OpenPilotLoadoutMenu" )
+ Remote_RegisterFunction( "ServerCallback_GenericDialog" )
+
+ // Ghost Recorder
+ RegisterEntityVar( "player", "mobilityGhostAnalyzed", false )
+ RegisterEntityVar( "player", "displayMobilityGhostHint", 0.0 )
+ RegisterEntityVar( "player", "displayMobilityGhostAnim", false )
+
+ // Dev Only
+ Remote_RegisterFunction( "Dev_PrintClientMessage" )
+ Remote_RegisterFunction( "Dev_BuildClientMessage" )
+
+ // Class Functions
+ Remote_RegisterFunction( "ServerCallback_DeploymentDeath" )
+ Remote_RegisterFunction( "ServerCallback_AddArcConnectorToy" )
+ Remote_RegisterFunction( "ServerCallback_PlayDialogueOnEntity" )
+ Remote_RegisterFunction( "ServerCallback_PlayDialogueAtPosition" )
+ Remote_RegisterFunction( "ServerCallback_PlayerConversation" )
+
+ //Weapon Flyout
+ RegisterNetworkedVariable( "shouldShowWeaponFlyout", SNDC_PLAYER_EXCLUSIVE, SNVT_BOOL, true )
+
+ Remote_RegisterFunction( "SCB_SetDoubleXPStatus" )
+
+ Remote_RegisterFunction( "SCB_SetScoreMeritState" )
+ Remote_RegisterFunction( "SCB_SetCompleteMeritState" )
+ Remote_RegisterFunction( "SCB_SetWinMeritState" )
+ Remote_RegisterFunction( "SCB_SetEvacMeritState" )
+ Remote_RegisterFunction( "SCB_SetMeritCount" )
+ Remote_RegisterFunction( "SCB_SetWeaponMeritCount" )
+ Remote_RegisterFunction( "SCB_SetTitanMeritCount" )
+ Remote_RegisterFunction( "SCB_UpdateTitanLoadouts" )
+
+ Remote_RegisterFunction( "SCB_SetHighlightFlagDisableDeathFade" ) //Hack, just for PulseBladeExecution
+
+ if ( IsLobby() )
+ {
+ Remote_RegisterFunction( "SCB_UpdateRankedPlayMenu" )
+ Remote_RegisterFunction( "SCB_UpdateBC" )
+ Remote_RegisterFunction( "SCB_RefreshBlackMarket" )
+ Remote_RegisterFunction( "ServerCallback_ShopOpenBurnCardPack" )
+ Remote_RegisterFunction( "ServerCallback_ShopOpenGenericItem" )
+ Remote_RegisterFunction( "SCB_RefreshCards" )
+ Remote_RegisterFunction( "SCB_UpdateEmptySlots" )
+ Remote_RegisterFunction( "SCB_UpdateBCFooter" )
+ }
+
+ if ( !IsModelViewer() )
+ {
+ switch ( GameRules_GetGameMode() )
+ {
+ case MARKED_FOR_DEATH:
+ case MARKED_FOR_DEATH_PRO:
+ Remote_RegisterFunction( "SCB_MarkedChanged" )
+ break
+ }
+ }
+
+ RegisterString( "#GAMEMODE_NO_TITANS_REMAINING" )
+ RegisterString( "#GAMEMODE_ENEMY_TITANS_DESTROYED" )
+ RegisterString( "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" )
+ RegisterString( "#GAMEMODE_ENEMY_PILOTS_ELIMINATED" )
+ RegisterString( "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" )
+ RegisterString( "#GAMEMODE_ENEMY_PILOT_ELIMINATED" )
+ RegisterString( "#GAMEMODE_FRIENDLY_PILOT_ELIMINATED" )
+ RegisterString( "#GAMEMODE_WAVE_LIMIT_REACHED" )
+ RegisterString( "#GAMEMODE_TIME_LIMIT_REACHED" )
+ RegisterString( "#GAMEMODE_SCORE_LIMIT_REACHED" )
+ RegisterString( "#GAMEMODE_ROUND_LIMIT_REACHED" )
+ RegisterString( "#GAMEMODE_ROUND_LIMIT_REACHED_WON_MORE_ROUNDS" )
+ RegisterString( "#GAMEMODE_ROUND_LIMIT_REACHED_LOSS_MORE_ROUNDS" )
+ RegisterString( "#GAMEMODE_ROUND_LIMIT_REACHED_ROUND_SCORE_DRAW" )
+ RegisterString( "#GAMEMODE_PREPARE_FOR_EVAC" )
+ RegisterString( "#GAMEMODE_AWAIT_INSTRUCTIONS" )
+ RegisterString( "#GAMEMODE_TITAN_TIME_ADVANTAGE" )
+ RegisterString( "#GAMEMODE_TITAN_TIME_DISADVANTAGE" )
+ RegisterString( "#GAMEMODE_TITAN_DAMAGE_ADVANTAGE" )
+ RegisterString( "#GAMEMODE_TITAN_DAMAGE_DISADVANTAGE" )
+ RegisterString( "#GAMEMODE_TITAN_TITAN_ADVANTAGE" )
+ RegisterString( "#GAMEMODE_TITAN_TITAN_DISADVANTAGE" )
+ RegisterString( "#GAMEMODE_DEFENDERS_WIN" )
+ RegisterString( "#GAMEMODE_ATTACKERS_WIN" )
+ RegisterString( "#GAMEMODE_LTS_TIME_LIMIT_REACHED_WIN" )
+ RegisterString( "#GAMEMODE_LTS_TIME_LIMIT_REACHED_LOSS" )
+ RegisterString( "#GAMEMODE_LTS_BOMB_DEFUSED_WIN" )
+ RegisterString( "#GAMEMODE_LTS_BOMB_DEFUSED_LOSS" )
+ RegisterString( "#GAMEMODE_LTS_BOMB_DETONATED_WIN" )
+ RegisterString( "#GAMEMODE_LTS_BOMB_DETONATED_LOSS" )
+ RegisterString( "#GAMEMODE_MARKED_FOR_DEATH_PRO_WIN_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_MARKED_FOR_DEATH_PRO_LOSS_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_MARKED_FOR_DEATH_PRO_DISCONNECT_WIN_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_MARKED_FOR_DEATH_PRO_DISCONNECT_LOSS_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_COLISEUM_DISCONNECT_WIN_ANNOUNCEMENT" )
+
+ RegisterString( "#GAMEMODE_LH_WIN_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_LH_LOSS_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_LH_TIME_OVER_WIN_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_LH_TIME_OVER_LOSS_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_LH_TIME_OVER_DRAW_ANNOUNCEMENT" )
+
+ RegisterString( "#GAMEMODE_HUNTED_WIN_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_HUNTED_LOSS_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_HUNTED_WIN_TIME_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_HUNTED_LOSS_TIME_ANNOUNCEMENT" )
+
+ RegisterString( "#GAMEMODE_SPEEDBALL_WIN_TIME_FLAG_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_SPEEDBALL_LOSS_TIME_FLAG_ANNOUNCEMENT" )
+
+ RegisterString( "#GAMEMODE_SPEEDBALL_WIN_TIME_FLAG_LAST" )
+ RegisterString( "#GAMEMODE_SPEEDBALL_LOSS_TIME_FLAG_LAST" )
+
+ RegisterString( "#GAMEMODE_SPEEDBALL_WIN_MORE_PILOTS_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_SPEEDBALL_LOSS_MORE_PILOTS_ANNOUNCEMENT" )
+
+ RegisterString( "#GAMEMODE_DON_WIN_MORE_KILLS_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_DON_LOSS_MORE_KILLS_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_VICTORY" )
+ RegisterString( "#GAMEMODE_DEFEATED" )
+
+ RegisterString( "#DEV_COMMAND_FORCED_WIN_ANNOUNCEMENT" )
+ RegisterString( "#DEV_COMMAND_FORCED_LOSS_ANNOUNCEMENT" )
+ RegisterString( "#COOP_TOTAL_VICTORY_HINT" )
+ RegisterString( "#COOP_TOTAL_DEFEAT_HINT" )
+ RegisterString( "#GAMEMODE_SUR_WIN_ANNOUNCEMENT" )
+ RegisterString( "#GAMEMODE_SUR_LOSS_ANNOUNCEMENT" )
+ RegisterString( "#ENEMY_TEAM_DISCONNECTED_WIN_ANNOUNCEMENT" )
+ RegisterString( "#ENEMY_TEAM_DISCONNECTED_LOSS_ANNOUNCEMENT" )
+ RegisterString( "#SUDDEN_DEATH_WIN_ANNOUNCEMENT" )
+ RegisterString( "#SUDDEN_DEATH_LOSS_ANNOUNCEMENT" )
+ RegisterString( "#SUDDEN_DEATH_KILLED_NEXT_PLAYER_WIN_ANNOUNCEMENT" )
+ RegisterString( "#SUDDEN_DEATH_KILLED_NEXT_PLAYER_LOSS_ANNOUNCEMENT" )
+
+ RegisterString( "#CAPTURE_THE_FLAG_FLAG_ESCAPED" )
+ RegisterString( "#CAPTURE_THE_FLAG_FLAG_CAPTURE_STOPPED" )
+
+ RegisterString( "#GAMESTATE_SWITCHING_SIDES" )
+ RegisterString( "#GAMEMODE_HOST_ENDED_MATCH" )
+
+ RegisterString( "#GENERIC_DRAW_ANNOUNCEMENT" )
+
+ RegisterString( "#RODEO_MULTI_SPOT_MOVE_HINT" )
+ RegisterString( "#RODEO_RIP_BATTERY_HINT" )
+ RegisterString( "#RODEO_APPLY_BATTERY_HINT" )
+ RegisterString( "#RODEO_REQUEST_BATTERY_HINT" )
+ RegisterString( "#RODEO_ANTI_RODEO_SMOKE_HINT" )
+ RegisterString( "#RODEO_ANTI_RODEO_SMOKE_NO_CHARGES_HINT" )
+
+ RegisterString( "#GAMEMODE_FRONTIER_WIN_ALL_CAPTURED" )
+ RegisterString( "#GAMEMODE_FRONTIER_LOSS_ALL_CAPTURED" )
+
+ RegisterString( "#FW_TEAM_TOWER_UNDER_ATTACK" )
+ RegisterString( "#FW_TEAM_TOWER_UNDER_ATTACK_SUB" )
+ RegisterString( "#FW_SHIELD_UNDER_ATTACK")
+ RegisterString( "#FW_SHIELD_DOWN" )
+ RegisterString( "#FW_USE_GENERATOR_NO_BATTERY" )
+ RegisterString( "#FW_USE_TURRET_GENERATOR" )
+ RegisterString( "#FW_USE_TURRET_GENERATOR_PC" )
+ RegisterString( "#FW_TURRET_OWNER" )
+ RegisterString( "#FW_TURRET_DESTROYED" )
+ RegisterString( "#FW_TITAN_REQUIRED" )
+ RegisterString( "#FW_TITAN_REQUIRED_SUB" )
+ RegisterString( "#FW_FRIENDLY_TOWER" )
+ RegisterString( "#FW_ENEMY_TOWER" )
+ RegisterString( "#FW_FRIENDLY_AREA_ENTER" )
+ RegisterString( "#FW_FRIENDLY_AREA_EXIT" )
+ RegisterString( "#FW_ENEMY_AREA_ENTER" )
+ RegisterString( "#FW_ENEMY_AREA_EXIT" )
+ RegisterString( "#FW_USE_BATTERY" )
+
+ RegisterString( "#CP_CAPTURE_POINTS" )
+ RegisterString( "#CP_AMP_POINTS" )
+ RegisterString( "#CP_DEFEND_POINTS" )
+
+ RegisterString( "#FW_OBJECTIVE_EARN" )
+ RegisterString( "#FW_OBJECTIVE_TITANFALL" )
+ RegisterString( "#FW_OBJECTIVE_EMBARK" )
+ RegisterString( "#FW_OBJECTIVE_ATTACK" )
+
+ RegisterString( "#AT_OBJECTIVE_KILL_DZ" )
+ RegisterString( "#AT_OBJECTIVE_KILL_DZ_MULTI" )
+ RegisterString( "#AT_OBJECTIVE_KILL_BOSS" )
+ RegisterString( "#AT_OBJECTIVE_KILL_BOSS_MULTI" )
+ RegisterString( "#AT_BANK_OPEN")
+ RegisterString( "#AT_BANK_OPEN_OBJECTIVE" )
+
+ RegisterString( "#SPEEDBALL_OBJECTIVE_KILL_CAP" )
+ RegisterString( "#SPEEDBALL_OBJECTIVE_ENEMY_FLAG" )
+ RegisterString( "#SPEEDBALL_OBJECTIVE_FRIENDLY_FLAG" )
+ RegisterString( "#SPEEDBALL_OBJECTIVE_PLAYER_FLAG" )
+
+ RegisterString( "#FD_TOTAL_VICTORY_HINT" )
+ RegisterString( "#FD_TOTAL_DEFEAT_HINT" )
+
+#if DEVSCRIPTS
+ Dev_RemoteStrings_Init()
+#endif // DEVSCRIPTS
+
+ //Note: The following are all test variables, feel free to comment them out as we hit the limit
+ //Begin test variables
+ //RegisterNetworkedVariable( "b", SNDC_PLAYER_EXCLUSIVE, SNVT_BOOL )
+ //RegisterNetworkedVariable( "i", SNDC_GLOBAL, SNVT_INT )
+ //RegisterNetworkedVariable( "u", SNDC_PLAYER_EXCLUSIVE, SNVT_UNSIGNED_INT )
+ //RegisterNetworkedVariable( "r", SNDC_TITAN_SOUL, SNVT_FLOAT_RANGE, .2, -1, 1 )
+ //RegisterNetworkedVariable( "rot", SNDC_PLAYER_GLOBAL, SNVT_FLOAT_RANGE_OVER_TIME, .5, -1, 1 )
+ //RegisterNetworkedVariable( "t", SNDC_PLAYER_GLOBAL, SNVT_TIME, 500 )
+ //RegisterNetworkedVariable( "e", SNDC_TITAN_SOUL, SNVT_ENTITY )
+ //end test variables
+
+ RegisterServerVar( "titanAvailableBits", 0 ) // HACK; we need this information to be 100% accurate, even during kill replay
+ RegisterServerVar( "respawnAvailableBits", 0 ) // HACK; we need this information to be 100% accurate, even during kill replay
+
+ RegisterNetworkedVariable( "batteryCount", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "activeCallingCardIndex", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+ RegisterNetworkedVariable( "activeCallsignIconIndex", SNDC_PLAYER_GLOBAL, SNVT_UNSIGNED_INT, 0 )
+
+ RegisterNetworkedVariable( "rewardState", SNDC_PLAYER_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "goalState", SNDC_PLAYER_GLOBAL, SNVT_INT, 0 )
+ RegisterNetworkedVariable( EARNMETER_OWNEDFRAC, SNDC_PLAYER_EXCLUSIVE, SNVT_FLOAT_RANGE, 0.0, 0.0, 1.0 )
+ RegisterNetworkedVariable( EARNMETER_EARNEDFRAC, SNDC_PLAYER_EXCLUSIVE, SNVT_FLOAT_RANGE, 0.0, 0.0, 1.0 )
+ RegisterNetworkedVariable( EARNMETER_REWARDFRAC, SNDC_PLAYER_EXCLUSIVE, SNVT_FLOAT_RANGE, 0.0, 0.0, 1.0 )
+
+ RegisterNetworkedVariable( EARNMETER_GOALID, SNDC_PLAYER_EXCLUSIVE, SNVT_UNSIGNED_INT )
+ RegisterNetworkedVariable( EARNMETER_REWARDID, SNDC_PLAYER_EXCLUSIVE, SNVT_UNSIGNED_INT )
+ RegisterNetworkedVariable( EARNMETER_MODE, SNDC_PLAYER_EXCLUSIVE, SNVT_INT )
+
+ RegisterNetworkedVariable( TOP_INVENTORY_ITEM_BURN_CARD_ID, SNDC_PLAYER_EXCLUSIVE, SNVT_INT, -1 )
+
+ RegisterNetworkedVariable( "activePilotLoadoutIndex", SNDC_PLAYER_EXCLUSIVE, SNVT_INT, 0 )
+ RegisterNetworkedVariable( "activeTitanLoadoutIndex", SNDC_PLAYER_EXCLUSIVE, SNVT_INT, 0 )
+
+ RegisterNetworkedVariable( "coreAvailableFrac", SNDC_TITAN_SOUL, SNVT_FLOAT_RANGE, 0.0, 0.0, 1.0 )
+ RegisterNetworkedVariable( "coreExpireFrac", SNDC_TITAN_SOUL, SNVT_FLOAT_RANGE_OVER_TIME, 0.0, 0.0, 1.0 )
+ RegisterNetworkedVariable( "upgradeCount", SNDC_TITAN_SOUL, SNVT_INT, 0 )
+
+ RegisterNetworkedVariable( "xpMultiplier", SNDC_PLAYER_EXCLUSIVE, SNVT_INT, 0 )
+
+ //Battle Chatter
+ Remote_RegisterFunction( "ServerCallback_PlayBattleChatter" )
+ RegisterNetworkedVariable( "battleChatterVoiceIndex", SNDC_PLAYER_GLOBAL, SNVT_INT, 0 )
+
+ //Faction Dialogue
+ Remote_RegisterFunction( "ServerCallback_PlayFactionDialogue" )
+ Remote_RegisterFunction( "ServerCallback_ForcePlayFactionDialogue" )
+ Remote_RegisterFunction( "ServerCallback_SpawnFactionCommanderInDropship" )
+
+ Remote_RegisterFunction( "ServerCallback_PlaySpectreChatterMP" )
+ Remote_RegisterFunction( "ServerCallback_PlayGruntChatterMP" )
+
+ Remote_RegisterFunction( "ServerCallback_EarnMeterAwarded" )
+
+ Remote_RegisterFunction( "ServerCallback_GetObjectiveReminderOnLoad" )
+ Remote_RegisterFunction( "ServerCallback_ClearObjectiveReminderOnLoad" )
+
+ Remote_RegisterFunction( "ServerCallback_PingMinimap" )
+
+ //Boosts
+ RegisterNetworkedVariable( "boostTimedEffectLastsTill", SNDC_PLAYER_EXCLUSIVE, SNVT_TIME )
+ RegisterNetworkedVariable( "burn_numTurrets", SNDC_PLAYER_GLOBAL, SNVT_INT )
+ RegisterNetworkedVariable( "burn_turretLimit", SNDC_GLOBAL, SNVT_INT, 5 )
+
+ #if CLIENT
+ //RegisterNetworkedVariableChangeCallback_time( "t", Changed )
+ RegisterNetworkedVariableChangeCallback_int( "upgradeCount", NetworkedVarChangedCallback_UpdateVanguardRUICoreStatus )
+ if ( !IsLobby() )
+ {
+ ClGameState_RegisterNetworkFunctions()
+
+ Cl_EarnMeter_RegisterNetworkFunctions()
+ ClRodeoTitan_RegisterNetworkFunctions()
+ ClSentryTurret_RegisterNetworkFunctions()
+
+ ClBurnMeter_RegisterNetworkFunctions()
+ }
+ #endif
+
+ InitCustomNetworkVars()
+
+ Remote_EndRegisteringFunctions()
+ _RegisteringFunctions = false
+}
+
+void function Changed( entity ent, float old, float new, bool actuallyChanged )
+{
+ printt( "Changed (" + ent + "): " + old + " -> " + new )
+}
+
+// script GetPlayerArray()[0].SetPlayerNetInt( "i", 0 )
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_script_movers.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_script_movers.gnut
new file mode 100644
index 00000000..ca7b839b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_script_movers.gnut
@@ -0,0 +1,1783 @@
+untyped
+
+global function ScriptMovers_Init
+global function ScriptedSwitchDeactivate
+global function ScriptToyChangeStatusLights
+global function SetSwitchUseFunc
+global function ScriptedRotatorRotate
+global function CodeCallback_PreBuildAINFile
+
+const FX_BARREL_EXPLOSION = $"P_spectre_suicide"
+const FX_GENERATOR_EXPLOSION = $"xo_exp_death"
+const FX_CONSOLE_EXPLOSION = $"xo_exp_death"
+const FX_PANEL_EXPLOSION = $"P_drone_exp_md"
+const FX_BARREL_FIRE_SMOKE = $"P_fire_small_FULL"
+const FX_GENERATOR_FIRE_SMOKE = $"P_fire_med_FULL"
+const FX_CONSOLE_FIRE_SMOKE = $"P_fire_med_FULL"
+const FX_PANEL_FIRE_SMOKE = $"P_fire_small_FULL"
+
+const SOUND_BARREL_EXPLODE = "corporate_spectre_death_explode"
+const SOUND_GENERATOR_EXPLODE = "Goblin_Dropship_Explode"
+const SOUND_PANEL_EXPLODE = "AngelCity_Scr_DroneExplodes"
+const SOUND_CONSOLE_EXPLODE = "corporate_spectre_death_explode"
+
+const FAN_PUSH_RAMP_TIME = 0.8
+const FAN_DEFAULT_PUSH_ACCEL = 25000 // units/sec^2
+const FAN_PUSH_ANTI_GRAVITY = 1000 // units/sec^2
+const FAN_PUSH_DECAY_SIDE_VELOCITY = false
+const FAN_DEBUG = false
+
+struct
+{
+ table switchCallbacks
+ //array<entity> entsInWindTunnel
+} file
+
+void function ScriptMovers_Init()
+{
+ AddSpawnCallbackEditorClass( "prop_dynamic", "script_door", ScriptedDoorInit )
+ AddSpawnCallbackEditorClass( "prop_dynamic", "script_switch", ScriptedSwitchInit )
+ AddSpawnCallbackEditorClass( "script_mover_lightweight", "script_rotator", ScriptedRotatorThink )
+ AddSpawnCallbackEditorClass( "script_mover_lightweight", "script_seesaw", SeeSawThink )
+ AddSpawnCallbackEditorClass( "prop_dynamic", "shootable_clasp", ClaspInit )
+
+ AddSpawnCallback_ScriptName( "FanPusher", FanPusherThink )
+
+ PrecacheParticleSystem( FX_BARREL_EXPLOSION )
+ PrecacheParticleSystem( FX_GENERATOR_EXPLOSION )
+ PrecacheParticleSystem( FX_PANEL_EXPLOSION )
+ PrecacheParticleSystem( FX_BARREL_FIRE_SMOKE )
+ PrecacheParticleSystem( FX_GENERATOR_FIRE_SMOKE )
+ PrecacheParticleSystem( FX_PANEL_FIRE_SMOKE )
+ PrecacheParticleSystem( FX_CONSOLE_EXPLOSION )
+ PrecacheParticleSystem( FX_CONSOLE_FIRE_SMOKE )
+
+ AddSpawnCallback( "script_mover", MoverInit )
+ AddSpawnCallback( "script_mover_lightweight", MoverInit )
+
+ RegisterSignal( "OpenDoor" )
+ RegisterSignal( "CloseDoor" )
+ RegisterSignal( "OnDeactivate")
+ RegisterSignal( "OnActivate")
+ RegisterSignal( "StopRotating" )
+}
+
+void function FlagControlsDoor( entity door, string flag )
+{
+ EndSignal( door, "OnDestroy" )
+ while ( true )
+ {
+ WaitSignal( level, flag )
+ if ( Flag( flag ) )
+ Signal( door, "OpenDoor" )
+ else
+ Signal( door, "CloseDoor" )
+ }
+}
+
+void function TriggerControlsDoor( entity door, entity trigger )
+{
+ EndSignal( door, "OpenDoor" )
+ EndSignal( door, "OnDestroy" )
+ EndSignal( trigger, "OnDestroy" )
+
+ WaitSignal( trigger, "OnTrigger" )
+
+ Signal( door, "OpenDoor" )
+}
+
+void function MotionActivatedDoor( entity door )
+{
+ bool doorOpen = false
+ if ( door.HasKey( "startOpen" ) )
+ doorOpen = (door.kv.startOpen == "1")
+
+ EndSignal( door, "OnDestroy" )
+
+ while ( true )
+ {
+ if ( doorOpen )
+ {
+ while ( ArrayEntityWithinDistance( GetPlayerArray(), door.GetOrigin(), 300 ) )
+ wait 0.2
+ Signal( door, "CloseDoor" )
+ }
+ else
+ {
+ while ( !ArrayEntityWithinDistance( GetPlayerArray(), door.GetOrigin(), 200 ) )
+ wait 0.2
+ Signal( door, "OpenDoor" )
+ }
+ doorOpen = !doorOpen
+ wait 1
+ }
+}
+
+void function CodeCallback_PreBuildAINFile()
+{
+ array<entity> doors = GetEntArrayByClass_Expensive( "prop_dynamic" )
+
+ foreach ( entity door in doors )
+ {
+ if ( GetEditorClass( door ) != "script_door" )
+ continue
+
+ door.SetBoneFollowersSolid( false )
+ array<entity> linkedEnts = door.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ if ( ent.GetClassName() == "func_brush" )
+ {
+ ent.NotSolid()
+ break
+ }
+ }
+ }
+}
+
+
+void function ScriptedDoorInit( entity door )
+{
+ #if DEV
+ array validModels = [
+ $"models/door/door_imc_interior_03_128_animated.mdl",
+ $"models/door/pod_door_Hangar_IMC_01_animated.mdl",
+ $"models/door/door_512x512x16_elevatorstyle01_animated.mdl",
+ $"models/door/door_256x256x8_elevatorstyle01_animated.mdl",
+ $"models/door/door_128x104x8_rolldownstyle01_animated.mdl",
+ $"models/door/door_256x256x8_rolldownstyle01_animated.mdl",
+ $"models/door/door_256_02_beacon_metal_door_animated.mdl",
+ $"models/door/door_beacon_core_animated.mdl",
+ $"models/door/door_128x104x8_elevatorstyle01_animated.mdl"
+ $"models/door/door_marvin_animated.mdl"
+ ]
+ Assert( validModels.contains( door.GetModelName() ), "Door model at " + door.GetOrigin() + " is invalid: " + door.GetModelName() )
+ #endif
+
+ EndSignal( door, "OnDestroy" )
+ door.SetBlocksLOS( true )
+
+ bool doorOpen = false
+ if ( door.HasKey( "startOpen" ) )
+ doorOpen = (door.kv.startOpen == "1")
+ bool initializing = true
+
+ if ( door.HasKey( "script_flag" ) )
+ {
+ string flag = expect string( door.kv.script_flag )
+ FlagInit( flag )
+ if ( doorOpen )
+ FlagSet( flag )
+ thread FlagControlsDoor( door, flag )
+ }
+
+ string flagToggle
+ if ( door.HasKey( "scr_flagToggle" ) )
+ {
+ flagToggle = expect string( door.kv.scr_flagToggle )
+ FlagInit( flagToggle )
+ }
+
+ if ( door.HasKey( "motionActivated" ) && door.kv.motionActivated == "1" )
+ thread MotionActivatedDoor( door )
+
+ // The door can link to a func_brush that is the collision of the door. The collision will be enabled/disabled based on the door state
+ entity clipBrush
+ array<entity> linkedEnts = door.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ if ( ent.GetClassName() == "func_brush" )
+ {
+ clipBrush = ent
+ break
+ }
+ }
+ if ( IsValid( clipBrush ) )
+ {
+ clipBrush.Hide()
+ clipBrush.NotSolid()
+ WaitFrame()
+ }
+
+ // A trigger_multiple can link to the door, causing the door to open. It will only open the door once.
+ entity trigger
+ array<entity> linkParents = door.GetLinkParentArray()
+ foreach ( entity ent in linkParents )
+ {
+ if ( ent.GetClassName() == "trigger_multiple" )
+ {
+ trigger = ent
+ break
+ }
+ }
+ if ( IsValid( trigger ) )
+ thread TriggerControlsDoor( door, trigger )
+
+ while ( IsValid( door ) )
+ {
+ // UPDATE THE DOOR STATE
+ if ( doorOpen )
+ door.Anim_Play("open")
+ else if ( !initializing )
+ door.Anim_Play("close")
+
+ if ( IsValid( clipBrush ) )
+ {
+ ToggleNPCPathsForEntity( clipBrush, doorOpen )
+ if ( doorOpen )
+ {
+ clipBrush.Hide()
+ clipBrush.NotSolid()
+ }
+ else
+ {
+ clipBrush.Show()
+ clipBrush.Solid()
+ }
+ }
+ else
+ {
+ // door must be setup with expensive bone_follower collision for this to work
+ ToggleNPCPathsForEntity( door, doorOpen )
+ }
+
+ if ( flagToggle != "" )
+ {
+ if ( doorOpen )
+ FlagSet( flagToggle )
+ else
+ FlagClear( flagToggle )
+ }
+ initializing = false
+
+ // WAIT FOR STATE CHANGE
+ if ( doorOpen )
+ WaitSignal( door, "CloseDoor" )
+ else
+ WaitSignal( door, "OpenDoor" )
+
+ CreateShakeRumbleOnly( door.GetOrigin(), 15, 150, 1 )
+
+ doorOpen = !doorOpen
+ }
+}
+
+
+void function ScriptedSwitchInit( entity button )
+{
+ #if DEV
+ array< asset > validModels = [
+ $"models/domestic/light_switch_touchscreen.mdl",
+ $"models/props/pressure_plates/pressure_plate_titan_industrial_01.mdl",
+ $"models/domestic/elevator_switch_01.mdl",
+ $"models/beacon/crane_room_monitor_console.mdl",
+ $"models/props/global_access_panel_button/global_access_panel_button_wall.mdl",
+ $"models/props/global_access_panel_button/global_access_panel_button_console.mdl"
+ ]
+ Assert( validModels.contains( button.GetModelName() ) )
+ #endif
+
+ bool usesSkins
+ int activeSkinID
+ int inactiveSkinID
+
+ switch( button.GetModelName() )
+ {
+ case $"models/props/global_access_panel_button/global_access_panel_button_wall.mdl":
+ case $"models/props/global_access_panel_button/global_access_panel_button_console.mdl":
+ usesSkins = true
+ activeSkinID = 0
+ inactiveSkinID = 1
+ break
+ case $"models/beacon/crane_room_monitor_console.mdl":
+ usesSkins = true
+ activeSkinID = 1
+ inactiveSkinID = 2
+ break
+ default:
+ usesSkins = false
+ break
+ }
+
+ int contextId = 0
+ button.Highlight_SetFunctions( contextId, 0, true, HIGHLIGHT_OUTLINE_INTERACT_BUTTON, 1, 0, false )
+ button.Highlight_SetParam( contextId, 0, HIGHLIGHT_COLOR_INTERACT )
+ button.Highlight_SetCurrentContext( contextId )
+
+ EndSignal( button, "OnDestroy" )
+ EndSignal( button, "OnDeactivate" )
+
+ OnThreadEnd(
+ function() : ( button )
+ {
+ // If we haven't destroyed the button, it must be inactive
+ if ( IsValid( button ) )
+ {
+ //ScriptToyChangeStatusLights( button, $"runway_light_red" )
+ Entity_StopFXArray( button )
+ button.UnsetUsable()
+ button.Highlight_HideInside( 1.0 )
+ button.Highlight_HideOutline( 1.0 )
+ }
+ }
+ )
+
+ bool buttonActivated = false
+ bool buttonIsSingleUse = false
+ bool initialized = false
+ bool buttonIsUsable = false
+ float multiUseDelay = 0.2
+
+ bool isPressurePlate = button.GetModelName() == $"models/props/pressure_plates/pressure_plate_titan_industrial_01.mdl"
+
+ if ( isPressurePlate )
+ button.kv.solid = 0 //hack until we can figure out why collision on this model kills titans when embarked
+
+ if ( button.HasKey( "singleUse" ) )
+ buttonIsSingleUse = (button.kv.singleUse == "1" )
+
+ if ( button.HasKey( "usable" ) )
+ buttonIsUsable = (button.kv.usable == "1" )
+
+ if ( button.HasKey( "multiUseDelay" ) )
+ {
+ multiUseDelay = float(button.kv.multiUseDelay)
+ if ( multiUseDelay > 0.0 )
+ Assert( !buttonIsSingleUse, "script_switch at " + button.GetOrigin() + "has multiUseDelay set and is single use" )
+ }
+
+ string flagToggle
+ if ( button.HasKey( "scr_flagToggle" ) )
+ {
+ flagToggle = expect string( button.kv.scr_flagToggle )
+ FlagInit( flagToggle )
+ }
+
+ string flagRequired
+ if ( button.HasKey( "scr_flagRequired" ) )
+ {
+ flagRequired = expect string( button.kv.scr_flagRequired )
+ FlagInit( flagRequired )
+ }
+
+ string hintString_hold = "#HOLD_TO_USE_GENERIC"
+ if ( button.HasKey( "hintString_hold" ) && button.kv.hintString_hold != "" )
+ hintString_hold = string( button.kv.hintString_hold )
+ string hintString_press = "#PRESS_TO_USE_GENERIC"
+ if ( button.HasKey( "hintString_press" ) && button.kv.hintString_press != "" )
+ hintString_press = string( button.kv.hintString_press )
+
+ entity trigger = GetLinkedTrigger( button )
+
+ //need a trigger for pressure plate unless it's just for show
+ if ( ( isPressurePlate ) && ( buttonIsUsable ) )
+ Assert( IsValid( trigger ), "script_switch pressure plate at " + button.GetOrigin() + " needs to link to a trigger_multiple" )
+
+ if ( isPressurePlate && buttonIsUsable )
+ {
+ Assert( IsValid( trigger ), "pressure plate switch at " + button.GetOrigin() + " requires a triggerTarget to activate" )
+ Assert( trigger.GetClassName() == "trigger_multiple", "pressure plate switch at " + button.GetOrigin() + " requires a trigger_multiple to activate" )
+ Assert( trigger.kv.spawnflags == "3", "Trigger for pressure plate at " + button.GetOrigin() + " needs spawnflags set to 3" )
+ }
+
+ if ( !isPressurePlate )
+ {
+ button.SetUsable()
+ button.SetUsableByGroup( "pilot" )
+ button.SetUsePrompts( hintString_hold, hintString_press )
+ button.Highlight_ShowInside( 1.0 )
+ button.Highlight_ShowOutline( 1.0 )
+ }
+
+ var player //hack: have to use "var" when waiting on a usable signal or trigger
+ bool buttonUsedOnce = false
+
+ while ( IsValid( button ) )
+ {
+ //-------------------------
+ // UPDATE EFFECTS
+ //-------------------------
+
+ if ( buttonActivated )
+ {
+ //ScriptToyChangeStatusLights( button, $"runway_light_red" )
+ Entity_StopFXArray( button )
+ if ( usesSkins )
+ button.SetSkin( inactiveSkinID )
+ }
+ else
+ {
+ ScriptToyChangeStatusLights( button, $"runway_light_green" )
+ if ( usesSkins )
+ button.SetSkin( activeSkinID )
+ }
+
+ if ( !buttonIsUsable )
+ break //exit loop if we just want the pretty lights, but no player usability
+
+ if ( buttonIsSingleUse && buttonUsedOnce )
+ break //exit loop if this is a single use button
+
+ if ( isPressurePlate )
+ {
+ //-------------------------
+ // WAIT FOR STATE CHANGE (PRESSURE PLATE)
+ //-------------------------
+
+ if ( buttonActivated )
+ waitthread PressurePlateWaitSignal( trigger, "OnEndTouchAll" )
+ else
+ waitthread PressurePlateWaitSignal( trigger, "OnTrigger" )
+ }
+ else
+ {
+ if ( flagRequired != "" && !Flag( flagRequired ) )
+ {
+ if ( !isPressurePlate )
+ {
+ if ( button.HasKey( "disabledHintString" ) )
+ button.SetUsePrompts( button.kv.disabledHintString, button.kv.disabledHintString )
+ else
+ button.UnsetUsable()
+ }
+
+ //ScriptToyChangeStatusLights( button, $"runway_light_red" )
+ Entity_StopFXArray( button )
+ if ( usesSkins )
+ button.SetSkin( inactiveSkinID )
+ FlagWait( flagRequired )
+ ScriptToyChangeStatusLights( button, $"runway_light_green" )
+ if ( usesSkins )
+ button.SetSkin( activeSkinID )
+ button.SetUsePrompts( hintString_hold , hintString_press )
+ button.SetUsable()
+ }
+
+ //-------------------------
+ // WAIT FOR STATE CHANGE (SIMPLE PUSH BUTTON)
+ //-------------------------
+
+ if ( buttonActivated )
+ {
+ wait multiUseDelay
+ }
+ else
+ {
+ player = button.WaitSignal( "OnPlayerUse" ).player
+ if ( !IsValid( player ) )
+ continue
+ if ( !player.IsPlayer() )
+ continue
+ }
+ }
+
+ //--------------------------------------
+ // Player activated, switch button state
+ //--------------------------------------
+
+ buttonUsedOnce = true
+ buttonActivated = !buttonActivated
+
+ EmitSoundOnEntity( button, "Switch_Activate" )
+
+ button.Signal( "OnActivate" )
+
+ if ( !isPressurePlate && buttonActivated )
+ {
+ button.UnsetUsable() //make the button unusable right after clicking so player doesn't double hit it
+ button.Highlight_HideInside( 1.0 )
+ button.Highlight_HideOutline( 1.0 )
+
+ // Run callbacks
+ if ( button in file.switchCallbacks )
+ {
+ foreach( table callbackTable in file.switchCallbacks[ button ] )
+ {
+ if ( callbackTable.useEnt == null )
+ callbackTable.useFunc( button, player )
+ else
+ callbackTable.useFunc( button, player, callbackTable.useEnt )
+ }
+ }
+ }
+
+ //------------
+ // SET FLAGS
+ //------------
+
+ // Button activated (green)
+ if ( flagToggle != "" && buttonActivated )
+ {
+ FlagSet( flagToggle )
+ }
+
+ if ( buttonActivated )
+ SpawnSpawnersLinkedToButton( button, expect entity( player ) )
+
+ //else if ( buttonActivated && isPressurePlate )
+ // wait 1.5 //wait a bit before re-enabling the usability
+
+ // Button deactivated (red)
+ if ( flagToggle != "" && !buttonActivated )
+ FlagClear( flagToggle )
+
+ if ( !buttonActivated )
+ {
+ button.SetUsable()
+ button.Highlight_ShowInside( 1.0 )
+ button.Highlight_ShowOutline( 1.0 )
+ }
+ }
+
+ if ( ( isPressurePlate ) && ( IsValid( trigger ) ) )
+ trigger.Destroy()
+ else
+ {
+ button.UnsetUsable()
+ button.Highlight_HideInside( 1.0 )
+ button.Highlight_HideOutline( 1.0 )
+ }
+}
+
+void function SpawnSpawnersLinkedToButton( entity button, entity activator )
+{
+ foreach ( entity linkedEnt in button.GetLinkEntArray() )
+ {
+ if ( IsStalkerRack( linkedEnt ) )
+ {
+ thread SpawnFromStalkerRack( linkedEnt, activator )
+ }
+ else if ( IsSpawner( linkedEnt ) )
+ {
+ entity spawned = linkedEnt.SpawnEntity()
+ DispatchSpawn( spawned )
+ }
+ else
+ {
+ Signal( linkedEnt, "OpenDoor" )
+ }
+ }
+}
+
+void function ScriptedSwitchDeactivate( entity button )
+{
+ Assert( IsValid( button ) )
+ button.Signal( "OnDeactivate" )
+}
+
+void function ScriptToyChangeStatusLights( entity button, asset fxName )
+{
+ //--------------------------
+ // Kill any previous effects
+ //--------------------------
+ Entity_StopFXArray( button )
+
+ //--------------------------
+ // Start new effects at tags
+ //--------------------------
+ array<entity> newFxLights
+ array<string> fxTags
+ entity newFx
+ int index = 0
+ string tagName
+
+ while (true)
+ {
+ tagName = "light" + index
+ local id = button.LookupAttachment( tagName )
+ if ( id == 0 )
+ break
+
+ newFx = PlayLoopFXOnEntity( fxName, button, tagName )
+ newFxLights.append( newFx )
+
+ index++
+ }
+
+ button.e.fxArray = newFxLights
+}
+
+void function PressurePlateWaitSignal( entity trigger, string waitSignal )
+{
+ //waitSignal is either "OnTrigger" or "OnEndTouchAll"
+
+ trigger.EndSignal( "OnDestroy" )
+ var result //hack. Result info from triggers
+
+ while ( IsValid( trigger ) )
+ {
+ result = trigger.WaitSignal( waitSignal )
+
+ if ( !IsValid( result.activator ) )
+ continue
+ if ( !result.activator.IsTitan() )
+ continue
+ if ( ( result.activator.IsPlayer() ) || ( IsPetTitan( result.activator ) ) )
+ break
+ }
+}
+
+void function ScriptedRotatorThink( entity rotator )
+{
+ rotator.Hide()
+
+ if ( rotator.HasKey( "use_local_rotation" ) && rotator.kv.use_local_rotation == "1" )
+ rotator.NonPhysicsSetRotateModeLocal( true )
+
+ EndSignal( rotator, "OnDestroy" )
+
+ vector baseAngles = rotator.GetAngles()
+
+ // Linked entities get parented
+ array<entity> linkedEnts = rotator.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ ent.SetParent( rotator, "", true )
+ }
+
+ if ( rotator.HasKey( "player_collides" ) && rotator.kv.player_collides == "1" )
+ rotator.SetPusher( true )
+
+ if ( rotator.HasKey( "change_navmesh" ) && rotator.kv.change_navmesh == "1" )
+ rotator.ChangeNPCPathsOnMove( true )
+
+ // script will custom rotate this one
+ if ( rotator.HasKey( "scripted_rotator" ) && expect string( rotator.kv.scripted_rotator ) == "true" )
+ return
+
+ if ( rotator.HasKey( "script_flag" ) )
+ {
+ string flag = expect string( rotator.kv.script_flag )
+ FlagInit( flag )
+ while ( true )
+ {
+ bool returnToBaseAngle = false
+ if ( rotator.HasKey( "flag_clear_resets" ) )
+ returnToBaseAngle = rotator.kv.flag_clear_resets == "1"
+
+ FlagWait( flag )
+ thread ScriptedRotatorRotate( baseAngles, rotator )
+ FlagWaitClear( flag )
+ Signal( rotator, "StopRotating" )
+
+ // Return when the flag is cleared
+ if ( returnToBaseAngle )
+ {
+ if ( !IsValid( rotator ) )
+ return
+
+ float rotateTime = 1.0
+ if ( rotator.HasKey( "rotate_to_time" ) )
+ rotateTime = float( rotator.kv.rotate_to_time )
+ float easeTime = 0.0
+ if ( rotator.HasKey( "rotate_to_ease" ) && rotator.kv.rotate_to_ease == "1" )
+ easeTime = rotateTime * 0.33
+
+ rotator.NonPhysicsRotateTo( baseAngles, rotateTime, easeTime, easeTime )
+ }
+ }
+ }
+ else
+ {
+ thread ScriptedRotatorRotate( baseAngles, rotator )
+ }
+}
+
+vector function GetRotationVector( entity rotator )
+{
+ string axis
+ if ( rotator.HasKey( "rotation_axis" ) )
+ axis = expect string( rotator.kv.rotation_axis )
+
+ vector angles = rotator.GetAngles()
+ switch ( axis )
+ {
+ case "pitch":
+ return AnglesToRight( angles )
+
+ case "yaw":
+ return AnglesToUp( angles )
+
+ case "roll":
+ default:
+ return AnglesToForward( angles )
+
+ }
+
+ unreachable
+}
+
+void function ScriptedRotatorRotate( vector baseAngles, entity rotator )
+{
+ Signal( rotator, "StopRotating" )
+ EndSignal( rotator, "OnDestroy" )
+ EndSignal( rotator, "StopRotating" )
+
+ OnThreadEnd(
+ function() : ( rotator )
+ {
+ if ( IsValid( rotator ) )
+ rotator.NonPhysicsRotate( Vector( 0, 0, 0), 0 )
+ }
+ )
+
+ if ( rotator.HasKey( "start_delay" ) )
+ {
+ float delay = float( rotator.kv.start_delay )
+ if ( delay > 0 )
+ wait delay
+ }
+
+ if ( rotator.kv.rotate_forever_speed != "0" )
+ {
+ // Rotate forever
+ float speed = float( rotator.kv.rotate_forever_speed )
+ Assert( speed != 0.0 )
+
+ vector rotateVec = GetRotationVector( rotator )
+ rotator.NonPhysicsRotate( rotateVec, speed )
+ WaitForever()
+ }
+ else
+ {
+ // Rotate specified amount
+ Assert( rotator.HasKey( "rotate_to_degrees" ) )
+ Assert( rotator.HasKey( "rotate_to_time" ) )
+
+ string soundEffect = ""
+ if ( rotator.HasKey( "script_sound" ) )
+ soundEffect = string( rotator.kv.script_sound )
+
+ float rotateTime = float( rotator.kv.rotate_to_time )
+ if ( rotateTime > 0.0 )
+ {
+ vector rotateAngles = AnglesCompose( baseAngles, Vector( 0.0, 0.0, float( rotator.kv.rotate_to_degrees ) ) )
+
+ float easeTime = 0.0
+ if ( rotator.HasKey( "rotate_to_ease" ) && rotator.kv.rotate_to_ease == "1" )
+ easeTime = rotateTime * 0.33
+
+ while ( true )
+ {
+ // Rotate to the goal angle
+ rotator.NonPhysicsRotateTo( rotateAngles, rotateTime, easeTime, easeTime )
+ if ( soundEffect != "" )
+ EmitSoundOnEntity( rotator, soundEffect )
+ wait rotateTime
+
+ // Rotate back to base angle if specified
+ if ( !rotator.HasKey( "rotate_to_return_delay" ) || rotator.kv.rotate_to_return_delay == "-1" )
+ return
+ Assert( float( rotator.kv.rotate_to_return_delay ) >= 0.0 )
+ wait float( rotator.kv.rotate_to_return_delay )
+ rotator.NonPhysicsRotateTo( baseAngles, rotateTime, easeTime, easeTime )
+ if ( soundEffect != "" )
+ EmitSoundOnEntity( rotator, soundEffect )
+ wait rotateTime
+
+ // Wait a delay and repeat the rotation if specified
+ if ( !rotator.HasKey( "rotate_to_loop_time" ) || rotator.kv.rotate_to_loop_time == "-1" )
+ return
+ Assert( float( rotator.kv.rotate_to_loop_time ) >= 0.0 )
+ wait float( rotator.kv.rotate_to_loop_time )
+ }
+ }
+ }
+}
+
+void function MoverInit( entity mover )
+{
+ if ( !mover.HasKey( "leveledplaced" ) || mover.kv.leveledplaced != "1" )
+ return
+
+ // Linked entities get parented
+ if ( mover.HasKey( "parent_linked_ents" ) && mover.kv.parent_linked_ents == "1" )
+ {
+ array<entity> linkedEnts = mover.GetLinkEntArray()
+ foreach( entity ent in linkedEnts )
+ {
+ if ( GetEditorClass( ent ) != "script_mover_path" )
+ ent.SetParent( mover, "", true )
+ }
+ }
+
+ if ( mover.HasKey( "player_collides" ) && mover.kv.player_collides == "1" )
+ mover.SetPusher( true )
+
+ if ( mover.HasKey( "change_navmesh" ) && mover.kv.change_navmesh == "1" )
+ mover.ChangeNPCPathsOnMove( true )
+
+ thread MoverThink( mover )
+}
+
+void function MoverThink( entity mover )
+{
+ EndSignal( mover, "OnDestroy" )
+
+ if ( mover.GetModelName() == $"models/dev/editor_ref.mdl" )
+ mover.Hide()
+
+ array<entity> pathNodes = GetNextMoverPathNodes( mover )
+
+ // Go down the path gathering the nodes and init any flags
+ foreach( entity node in pathNodes )
+ InitMoverNodeFlagsAndErrorCheck( node )
+
+ Assert( mover.HasKey( "path_speed") && float( mover.kv.path_speed ) >= 0.0, "script_mover doesnt have a valid path speed" )
+ float pathSpeed = float( mover.kv.path_speed )
+ bool easeIn
+ bool easeOut
+
+ string startFlag = ""
+ if ( mover.HasKey( "script_flag" ) && mover.kv.script_flag != "" )
+ {
+ startFlag = mover.GetValueForKey( "script_flag" )
+ FlagInit( startFlag )
+ }
+
+ if ( mover.HasKey( "dangerous_area_radius" ) )
+ mover.AllowNPCGroundEnt( false )
+
+ if ( pathNodes.len() == 0 )
+ return
+
+ if ( startFlag != "" )
+ FlagWait( startFlag )
+
+ if ( mover.HasKey( "start_delay" ) && float( mover.kv.start_delay ) > 0.0 )
+ wait float( mover.kv.start_delay )
+
+ entity pathNode = pathNodes.getrandom()
+ entity lastNode
+
+ easeOut = pathNode.HasKey( "ease_from_node" ) && pathNode.GetValueForKey( "ease_from_node" ) == "1"
+
+ bool isMoving = false
+
+ while( IsValid( pathNode ) )
+ {
+ bool teleport = false
+ if ( pathNode.HasKey( "teleport_to_node" ) )
+ teleport = pathNode.GetValueForKey( "teleport_to_node" ) == "1"
+
+ bool perfectRotation = false
+ if ( IsValid( lastNode ) && lastNode.HasKey( "perfect_circular_rotation" ) )
+ perfectRotation = lastNode.GetValueForKey( "perfect_circular_rotation" ) == "1"
+
+ float rotationTime = 0.0
+ if ( IsValid( lastNode ) && lastNode.HasKey( "circular_rotation_time" ) )
+ rotationTime = float( lastNode.GetValueForKey( "circular_rotation_time" ) )
+
+ float dist = Distance( pathNode.GetOrigin(), mover.GetOrigin() )
+
+ if ( !isMoving )
+ MoverPath_StartSound( mover, pathNode )
+
+ if ( dist > 0.0 && !teleport )
+ {
+ easeIn = pathNode.HasKey( "ease_to_node" ) && pathNode.GetValueForKey( "ease_to_node" ) == "1"
+ float moveTime = dist / pathSpeed
+ float easeLeaving = easeOut ? moveTime * 0.5 : 0.0
+ float easeArriving = easeIn ? moveTime * 0.5 : 0.0
+ float angleChange = IsValid( lastNode ) ? MoverPath_GetAngleChange( lastNode, pathNode ) : 0.0
+
+ if ( perfectRotation && angleChange != 0 )
+ {
+ string rotationSoundEvent = ""
+ if ( mover.HasKey( "sound_circular_rotation" ) )
+ rotationSoundEvent = mover.GetValueForKey( "sound_circular_rotation" )
+ if ( rotationSoundEvent != "" )
+ EmitSoundOnEntity( mover, rotationSoundEvent )
+
+ vector turnAnchorPos = MoverPath_GetAngleAnchor( lastNode, pathNode )
+
+ // Create a new mover because as far as I know I can't get all the children of the mover and clearparent and reparent.
+ entity curveMover = CreateScriptMover( turnAnchorPos, lastNode.GetAngles() )
+ curveMover.SetPusher( mover.GetPusher() )
+ mover.SetParent( curveMover, "", true )
+
+ // Find the circumference of the turn so we can calculate the rotation time based on the distance traveled around the bend
+ float c = 2 * PI * Length(turnAnchorPos - lastNode.GetOrigin())
+ float frac = fabs(angleChange) / 360.0
+ moveTime = (c * frac) / pathSpeed
+
+ isMoving = true
+ curveMover.NonPhysicsRotateTo( pathNode.GetAngles(), moveTime, 0.0, 0.0 )
+
+ wait moveTime - 0.01
+
+ mover.ClearParent()
+ curveMover.Destroy()
+
+ if ( rotationSoundEvent != "" )
+ StopSoundOnEntity( mover, rotationSoundEvent )
+ }
+ else
+ {
+ // Linear move/rotate
+ isMoving = true
+ if ( mover.HasKey( "dangerous_area_radius" ) )
+ thread CreateMoverDangrousAreas( mover, mover.GetOrigin(), pathNode.GetOrigin(), float( mover.GetValueForKey( "dangerous_area_radius" ) ), moveTime )
+ mover.NonPhysicsMoveTo( pathNode.GetOrigin(), moveTime, easeLeaving, easeArriving )
+ mover.NonPhysicsRotateTo( pathNode.GetAngles(), moveTime, easeLeaving, easeArriving )
+ wait moveTime - 0.01
+ }
+ }
+ else if ( dist == 0.0 && !teleport && rotationTime > 0.0 )
+ {
+ // Rotation in place
+ string rotationSoundEvent = ""
+ if ( mover.HasKey( "sound_rotation" ) )
+ rotationSoundEvent = mover.GetValueForKey( "sound_rotation" )
+ if ( rotationSoundEvent != "" )
+ EmitSoundOnEntity( mover, rotationSoundEvent )
+
+ isMoving = false
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ float easeIn = easeOut ? rotationTime * 0.5 : 0.0
+ float easeOut = easeIn ? rotationTime * 0.5 : 0.0
+ mover.NonPhysicsRotateTo( pathNode.GetAngles(), rotationTime, easeIn, easeOut )
+ wait rotationTime - 0.01
+
+ if ( rotationSoundEvent != "" )
+ StopSoundOnEntity( mover, rotationSoundEvent )
+ }
+ else
+ {
+ mover.SetOrigin( pathNode.GetOrigin() )
+ mover.SetAngles( pathNode.GetAngles() )
+ }
+
+ easeOut = pathNode.HasKey( "ease_from_node" ) && pathNode.GetValueForKey( "ease_from_node" ) == "1"
+
+ if ( pathNode.HasKey( "scr_flag_set" ) )
+ FlagSet( pathNode.GetValueForKey( "scr_flag_set" ) )
+
+ if ( pathNode.HasKey( "scr_flag_clear" ) )
+ FlagClear( pathNode.GetValueForKey( "scr_flag_clear" ) )
+
+ if ( pathNode.HasKey( "scr_flag_wait" ) )
+ {
+ string flag = pathNode.GetValueForKey( "scr_flag_wait" )
+ if ( !Flag( flag ) )
+ {
+ isMoving = false
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ FlagWait( flag )
+ }
+ }
+
+ if ( pathNode.HasKey( "scr_flag_wait_clear" ) )
+ {
+ string flag = pathNode.GetValueForKey( "scr_flag_wait_clear" )
+ if ( Flag( flag ) )
+ {
+ isMoving = false
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ FlagWaitClear( flag )
+ }
+ }
+
+ if ( pathNode.HasKey( "path_wait" ) )
+ {
+ float time = float( pathNode.GetValueForKey( "path_wait" ) )
+ if ( time > 0.0 )
+ {
+ isMoving = false
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ wait time
+ }
+ }
+
+ pathNodes = GetNextMoverPathNodes( pathNode )
+ if ( pathNodes.len() == 0 )
+ {
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ break
+ }
+
+ // Update speed based on the node
+ if ( pathNode.HasKey( "path_speed" ) )
+ pathSpeed = float( pathNode.GetValueForKey( "path_speed" ) )
+
+ lastNode = pathNode
+ pathNode = pathNodes.getrandom()
+ }
+}
+
+void function CreateMoverDangrousAreas( entity mover, vector start, vector end, float radius, float duration )
+{
+ float d = Distance( start, end )
+ float spacing = radius * 1.5
+ int numDangerousSpots = int( ceil( d / spacing ) )
+ vector direction = Normalize( end - start )
+ vector pos
+
+ for ( int i = 0 ; i < numDangerousSpots ; i++ )
+ {
+ pos = start + ( direction * spacing * i )
+ thread CreateMoverDangrousAreaUntilMoverPasses( mover, start, end, pos, radius, duration )
+ }
+}
+
+void function CreateMoverDangrousAreaUntilMoverPasses( entity mover, vector start, vector end, vector pos, float radius, float maxDuration )
+{
+ // Create entity to link it to (lifetime)
+ entity lifetimeEnt = CreateScriptRef( pos )
+
+ // Create the dangerous area
+ AI_CreateDangerousArea_Static( lifetimeEnt, null, radius, TEAM_INVALID, true, true, pos )
+
+ // Wait for mover to go past the dangerous area, or timeout
+ float endTime = Time() + maxDuration
+ while( Time() <= endTime )
+ {
+ if ( DotProduct( end - start, pos - mover.GetOrigin() ) < 0 )
+ break
+ WaitFrame()
+ }
+
+ lifetimeEnt.Destroy()
+}
+
+void function MoverPath_StopMoveSound( entity mover )
+{
+ // Stops any move sounds playing on the mover
+
+ // Stop playing a sound on the mover if one is specified & it is set to do so
+ if ( mover.HasKey( "sound_move" ) && mover.kv.sound_move != "" )
+ {
+ // "sound_move" sound continues to play after moving unless this is checked"
+ if ( mover.HasKey( "stop_sound_move_on_stop" ) && mover.GetValueForKey( "stop_sound_move_on_stop" ) == "1" )
+ StopSoundOnEntity( mover, string( mover.kv.sound_move ) )
+ }
+}
+
+void function MoverPath_StopSound( entity mover, entity node )
+{
+ // Play sound on the node if one is specified
+ if ( node.HasKey( "sound_stop_move" ) && node.kv.sound_stop_move != "" )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, node.GetOrigin(), string( node.kv.sound_stop_move ) )
+
+ // Play sound on the mover if one is specified
+ if ( mover.HasKey( "sound_stop_move" ) && mover.kv.sound_stop_move != "" )
+ EmitSoundOnEntity( mover, string( mover.kv.sound_stop_move ) )
+}
+
+void function MoverPath_StartSound( entity mover, entity node )
+{
+ // Play sound on the node if one is specified
+ if ( node.HasKey( "sound_start_move" ) && node.kv.sound_start_move != "" )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, node.GetOrigin(), string( node.kv.sound_start_move ) )
+
+ // Play sound on mover if one is specified
+ if ( mover.HasKey( "sound_move" ) && mover.kv.sound_move != "" )
+ EmitSoundOnEntity( mover, string( mover.kv.sound_move ) )
+}
+
+float function MoverPath_GetAngleChange( entity node1, entity node2 )
+{
+ vector vec1 = node1.GetForwardVector()
+ vector vec2 = node2.GetForwardVector()
+ float angle = acos( DotProduct( vec1, vec2 ) ) * 180 / PI
+ return angle
+}
+
+vector function MoverPath_GetAngleAnchor( entity node1, entity node2 )
+{
+ vector node1Origin = node1.GetOrigin()
+ vector node2Origin = node2.GetOrigin()
+ vector node1Angles = node1.GetAngles()
+ vector node2Angles = node2.GetAngles()
+ vector node1SideVec
+ vector node2SideVec
+
+ if ( node1Origin.z != node2Origin.z )
+ {
+ // vertical turn
+ node1SideVec = AnglesToUp( node1Angles )
+ node2SideVec = AnglesToUp( node2Angles )
+ }
+ else
+ {
+ // horizontal turn
+ node1SideVec = AnglesToRight( node1Angles )
+ node2SideVec = AnglesToRight( node2Angles )
+ }
+
+ float angleChange = MoverPath_GetAngleChange( node1, node2 )
+ Assert( angleChange != 0 )
+ if ( angleChange > 0 )
+ {
+ node1SideVec *= -1
+ node2SideVec *= -1
+ }
+ float d = Distance( node1Origin, node2Origin )
+ vector intersect = GetClosestPointToLineSegments( node1Origin, node1Origin + node1SideVec * d, node2Origin, node2Origin + node2SideVec * d )
+
+ //DebugDrawLine( intersect, node1Origin, 255, 255, 0, true, 5.0 )
+ //DebugDrawLine( intersect, node2Origin, 0, 255, 255, true, 5.0 )
+ //DebugDrawLine( node1Origin, node1Origin + node1SideVec * 250, 100, 100, 100, true, 5.0 )
+ //DebugDrawLine( node2Origin, node2Origin + node2SideVec * 250, 100, 100, 100, true, 5.0 )
+ //DebugDrawLine( intersect, intersect - <0,0,128>, 100, 0, 0, true, 5.0 )
+ //DebugDrawText( intersect, angleChange.tostring(), true, 5.0 )
+
+ return intersect
+}
+
+array<entity> function GetNextMoverPathNodes( entity node, bool errorChecking = false )
+{
+ array<entity> nodes
+ array<entity> linkedEnts = node.GetLinkEntArray()
+ foreach( entity ent in linkedEnts )
+ {
+ if ( GetEditorClass( ent ) == "script_mover_path" )
+ {
+ if ( !errorChecking && ent.HasKey( "switchtrack_flag" ) && !Flag( ent.GetValueForKey( "switchtrack_flag" ) ) )
+ continue
+ nodes.append( ent )
+ }
+ }
+ return nodes
+}
+
+void function InitMoverNodeFlagsAndErrorCheck( entity node )
+{
+ if ( node.e.moverPathPrecached )
+ return
+
+ if ( node.HasKey( "path_speed" ) )
+ Assert( float( node.kv.path_speed ) > 0.0, "Node path_speed at " + node.GetOrigin() + " must be greater than 0." )
+
+ if ( node.HasKey( "path_wait" ) )
+ Assert( float( node.kv.path_wait ) >= 0.0, "Node path_wait at " + node.GetOrigin() + " must be greater than 0." )
+
+ if ( node.HasKey( "teleport_to_node" ) && node.kv.teleport_to_node == "1" )
+ {
+ if ( node.HasKey( "ease_to_node" ) )
+ Assert( node.kv.ease_to_node == "0", "Node at " + node.GetOrigin() + " cant have both teleport_to_node and ease_to_node checked." )
+ }
+
+ if ( node.HasKey( "scr_flag_set" ) )
+ FlagInit( node.GetValueForKey( "scr_flag_set" ) )
+ if ( node.HasKey( "scr_flag_clear" ) )
+ FlagInit( node.GetValueForKey( "scr_flag_clear" ) )
+ if ( node.HasKey( "scr_flag_wait" ) )
+ FlagInit( node.GetValueForKey( "scr_flag_wait" ) )
+ if ( node.HasKey( "switchtrack_flag" ) )
+ FlagInit( node.GetValueForKey( "switchtrack_flag" ), true )
+
+ node.e.moverPathPrecached = true
+
+ array<entity> pathNodes = GetNextMoverPathNodes( node, true )
+ foreach( entity node in pathNodes )
+ InitMoverNodeFlagsAndErrorCheck( node )
+}
+
+entity function GetLinkedTrigger( entity ent )
+{
+ array<entity> linkedEnts = ent.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ if ( ent.GetClassName() == "trigger_multiple" )
+ return ent
+ }
+ return null
+}
+
+struct SeeSawThinkStruct // struct that is internal to seeSaw think logic
+{
+ float speed
+ bool touching
+ bool wasTouched
+ vector startAngles
+ float oldSpeed
+ bool playerIgnore
+ float maxSpeed = 10
+ float acceleration = 0.425
+}
+
+void function SeeSawThink( entity seeSaw )
+{
+ seeSaw.Hide()
+
+ seeSaw.EndSignal( "OnDestroy" )
+// seeSaw.NonPhysicsSetRotateModeLocal( true )
+ seeSaw.SetPusher( true )
+
+ array<entity> parents = seeSaw.GetLinkParentArray()
+ array<entity> brushes
+ foreach ( ent in parents )
+ {
+ if ( ent.GetClassName() == "func_brush" )
+ brushes.append( ent )
+ }
+
+ float minz = 0
+ float maxz = 0
+ foreach ( brush in brushes )
+ {
+ vector mins = brush.GetBoundingMins()
+ vector maxs = brush.GetBoundingMaxs()
+
+ if ( mins.z < minz )
+ minz = mins.z
+
+ if ( maxs.z > maxz )
+ maxz = maxs.z
+ }
+
+ float height = fabs( minz ) + maxz
+
+ entity trigger = seeSaw.GetLinkEnt()
+ trigger.EndSignal( "OnDestroy" )
+
+ SeeSawThinkStruct e
+ e.startAngles = seeSaw.GetAngles()
+
+ if ( seeSaw.HasKey( "script_start_moving" ) && int( seeSaw.kv.script_start_moving ) > 0 )
+ {
+ e.wasTouched = true
+ e.speed = 8
+ }
+
+ thread SeeSawSpeedThink( seeSaw, e )
+
+ if ( seeSaw.HasKey( "script_player_ignore" ) && int( seeSaw.kv.script_player_ignore ) > 0 )
+ {
+ e.playerIgnore = true
+ }
+ else
+ {
+ thread SeeSawTriggerThink( seeSaw, trigger, height, e )
+ }
+
+
+}
+
+void function SeeSawTriggerThink( entity seeSaw, entity trigger, float height, SeeSawThinkStruct e )
+{
+ for ( ;; )
+ {
+ e.touching = false
+ table results = trigger.WaitSignal( "OnTrigger" )
+ entity player = expect entity( results.activator )
+ if ( !IsAlive( player ) )
+ continue
+
+ while ( trigger.IsTouching( player ) )
+ {
+ PlayerNearSeeSaw( player, seeSaw, height, e )
+ WaitFrame()
+ }
+ }
+}
+
+float ornull function SeeSawPitchLimitOverride()
+{
+// return 60.0
+ return null
+}
+
+void function SeeSawSpeedThink( entity seeSaw, SeeSawThinkStruct e )
+{
+ seeSaw.EndSignal( "OnDestroy" )
+ float pitchLimit = 50 // 70.75
+
+ if ( seeSaw.HasKey( "script_pitch_limit" ) )
+ pitchLimit = float( seeSaw.kv.script_pitch_limit )
+
+ vector angles = seeSaw.GetAngles()
+ vector forward = AnglesToRight( angles ) * -1
+ vector rotateDir = forward // < -1,0,0 >
+
+ for ( ;; )
+ {
+ WaitFrame()
+ SeeSawSpeedThink_internal( seeSaw, e, rotateDir, pitchLimit )
+ }
+}
+
+void function SeeSawSpeedThink_internal( entity seeSaw, SeeSawThinkStruct e, vector rotateDir, float pitchLimit )
+{
+ float ornull pitchLimitOverride = SeeSawPitchLimitOverride()
+ if ( pitchLimitOverride != null )
+ {
+ pitchLimit = expect float( pitchLimitOverride )
+ }
+ vector localAngles = seeSaw.GetLocalAngles()
+
+ if ( ( !e.touching && e.wasTouched ) || e.playerIgnore )
+ {
+ vector startForward = AnglesToForward( e.startAngles )
+ vector startUp = AnglesToUp( e.startAngles )
+ vector seeSawForward = AnglesToForward( seeSaw.GetAngles() )
+
+// if ( ge(106)==seeSaw)
+// {
+// printt( "dot is " + DotProduct( startForward, seeSawForward ) + " speed " + e.speed )
+// }
+ //printt( "Dot " + DotProduct( startForward, seeSawForward ) )
+ //printt( "Dot " +
+ //printt( DotProduct( startUp, seeSawForward ) )
+
+
+ // return to normal
+ //printt( "start angles " + e.startAngles + " current angles " + seeSaw.GetAngles() )
+
+ if ( fabs( DotProduct( startForward, seeSawForward ) ) < 0.75 )
+ {
+ if ( DotProduct( startUp, seeSawForward ) > 0 )
+ {
+ if ( e.speed < e.maxSpeed )
+ e.speed += e.acceleration
+ }
+ else
+ {
+ if ( e.speed > -e.maxSpeed )
+ e.speed -= e.acceleration
+ }
+ }
+
+ //DebugDrawText( seeSaw.GetOrigin(), "" + e.speed, true, 1 )
+ //DebugDrawLine( seeSaw.GetOrigin(), GetPlayerArray()[0].GetOrigin(), 255, 0, 0, true, 0.2 )
+ }
+
+// if ( ge(117) == seeSaw )
+// return
+ if ( e.speed < 0 )
+ {
+ if ( localAngles.x < -pitchLimit )
+ {
+ seeSaw.NonPhysicsRotate( rotateDir, 0 )
+ e.speed = 0
+ return
+ }
+ }
+ else
+ {
+ if ( localAngles.x > pitchLimit )
+ {
+ seeSaw.NonPhysicsRotate( rotateDir, 0 )
+ e.speed = 0
+ return
+ }
+ }
+
+ if ( e.oldSpeed != e.speed || pitchLimitOverride != null )
+ {
+ seeSaw.NonPhysicsRotate( rotateDir, e.speed )
+ e.oldSpeed = e.speed
+ }
+}
+
+void function PlayerNearSeeSaw( entity player, entity seeSaw, float height, SeeSawThinkStruct e )
+{
+ vector playerOrigin = player.GetOrigin()
+ vector seeSawOrigin = seeSaw.GetOrigin()
+ //DebugDrawLine( playerOrigin, seeSawOrigin, 255, 0, 0, true, 0.2 )
+ vector originDif = playerOrigin - seeSawOrigin
+ vector difNormal = Normalize( originDif )
+ vector seeSawAngles = seeSaw.GetAngles()
+ vector seeSawUp = AnglesToUp( seeSawAngles )
+ bool onTop = DotProduct( difNormal, seeSawUp ) > 0
+
+ // may need to do something special for hands holding on
+ if ( !onTop )
+ {
+ e.touching = false
+ return
+ }
+
+ float amountAbove = DotProduct( originDif, seeSawUp )
+ amountAbove -= height
+ amountAbove += 2.5 // player is in the ground?
+
+// if ( ge(117) == seeSaw )
+// {
+// printt( "amountAbove " + amountAbove )
+// }
+
+ if ( amountAbove < 0 || amountAbove > 15 )
+ {
+ e.touching = false
+ return
+ }
+
+
+
+ vector seeSawForward = AnglesToForward( seeSawAngles )
+ float amountForward = DotProduct( originDif, seeSawForward )
+ e.speed += Graph( amountForward, 0, 1000, 0, 2 )
+ float maxSpeed = 10
+ e.speed = min( maxSpeed, e.speed )
+ e.speed = max( -maxSpeed, e.speed )
+ e.touching = true
+ e.wasTouched = true
+}
+
+void function ClaspInit(entity clasp)
+{
+ //printt( "" )
+ //printt( "INIT SHOOTABLE CLASP" )
+ //printt( "" )
+
+ if ( clasp.HasKey( "scr_flag_set" ) )
+ FlagInit( clasp.GetValueForKey( "scr_flag_set" ) )
+
+ AddEntityCallback_OnDamaged( clasp, OnClaspDamaged )
+
+}
+
+void function OnClaspDamaged( entity clasp, var damageInfo)
+{
+ //printt( "CLASP HAS TAKEN DAMAGE" )
+
+ //if the attacker is not valid
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsValid( attacker ) || !attacker.IsPlayer() )
+ return
+
+ if ( clasp.HasKey( "scr_flag_set") )
+ {
+ //printt( clasp )
+ //printt( clasp.GetEncodedEHandle() )
+ //printt( clasp.GetTargetName() )
+ //printt( clasp.GetValueForKey( "scr_flag_set" ) )
+ FlagSet( clasp.GetValueForKey( "scr_flag_set" ) )
+ }
+
+ //printt( "" )
+ //printt( "DESTROYING CLASP" )
+ //printt( "" )
+
+ //Destroy the clasp
+ clasp.Destroy()
+}
+
+function SetSwitchUseFunc( button, func, ent = null )
+{
+ local Table = InitControlPanelUseFuncTable()
+ Table.useFunc <- func
+ Table.useEnt <- ent
+
+ if ( !( button in file.switchCallbacks ) )
+ file.switchCallbacks[ button ] <- []
+ file.switchCallbacks[ button ].append( Table )
+}
+
+
+void function FanPusherThink( entity fanPusher )
+{
+ float fanPushDist = 3000
+
+ if ( fanPusher.HasKey( "height" ) )
+ fanPushDist = float( fanPusher.kv.height )
+
+ if ( fanPusher.HasKey( "script_gravityscale" ) && fanPusher.kv.script_gravityscale != "" )
+ {
+ fanPushDist *= string( fanPusher.kv.script_gravityscale ).tofloat()
+ }
+ else
+ {
+ float gravityScale = expect float( GetPlayerSettingsFieldForClassName( DEFAULT_PILOT_SETTINGS, "gravityscale" ) )
+ fanPushDist *= gravityScale // adjusted for new gravity scale
+ }
+
+ float radius = float( fanPusher.kv.script_radius )
+ vector forward = AnglesToForward( fanPusher.GetAngles() )
+ vector cylinderBottom = fanPusher.GetOrigin()
+ vector cylinderTop = cylinderBottom + ( forward * fanPushDist )
+
+ if ( FAN_DEBUG )
+ DebugDrawCylinder( fanPusher.GetOrigin(), fanPusher.GetAngles(), radius, fanPushDist, 100, 0, 0, true, 120.0 )
+
+ string flag = ""
+ if ( fanPusher.HasKey( "script_flag" ) )
+ {
+ flag = string( fanPusher.kv.script_flag )
+ FlagInit( flag )
+ }
+
+ bool lifterFan = DotProduct( forward, <0,0,1> ) >= 0.98
+ float pushAccel = FAN_DEFAULT_PUSH_ACCEL
+ if ( fanPusher.HasKey( "strength" ) )
+ pushAccel = float( fanPusher.kv.strength )
+
+ array<entity> fanPushables
+ table<entity,float> startTimes
+ table<entity,float> startHeights
+
+ // Play fan sound on entity instead of at position because some occluder bug with miles. Easiest fix for late in game. Next project fan pusher shoudln't be info_target that you can't play sounds on
+ entity fanSoundEntity = CreateScriptMover( fanPusher.GetOrigin() )
+ fanSoundEntity.DisableHibernation()
+
+ // Delay to fix code bug where audio wont play at map load
+ wait 0.2
+
+ FanOnSoundEffects( fanPusher, radius, fanSoundEntity )
+
+ while( true )
+ {
+ if ( flag != "" && !Flag( flag ) )
+ {
+ foreach( entity ent, float time in startTimes )
+ ent.e.inWindTunnel = false
+
+ startTimes.clear()
+ startHeights.clear()
+
+ FanOffSoundEffects( fanPusher, radius, fanSoundEntity )
+ FlagWait( flag )
+ FanOnSoundEffects( fanPusher, radius, fanSoundEntity )
+ }
+
+ fanPushables.clear()
+ fanPushables.extend( GetPlayerArray() )
+ fanPushables.extend( GetNPCArray() )
+ fanPushables.extend( GetProjectileArrayEx( "any", TEAM_ANY, TEAM_ANY, cylinderBottom, fanPushDist ) )
+ fanPushables.extend( GetEntArrayByClass_Expensive( "prop_physics" ) )
+
+ foreach( entity ent in fanPushables )
+ {
+ array<vector> testPosArray = [ ent.GetWorldSpaceCenter(), ent.EyePosition() + <0,0,16>, ent.GetOrigin() - <0,0,16> ]
+ bool isInFanCylinder = false
+ vector testPos = ent.GetWorldSpaceCenter()
+ if ( ent.e.windPushEnabled )
+ {
+ foreach( vector pos in testPosArray )
+ {
+ if ( !PointInCylinder( cylinderBottom, cylinderTop, radius, pos ) )
+ continue
+ isInFanCylinder = true
+ break
+ }
+ }
+
+ if ( isInFanCylinder )
+ {
+ if ( ent.IsPlayer() && ent.IsNoclipping() )
+ continue
+
+ if ( !( ent in startTimes ) )
+ {
+ ent.e.inWindTunnel = true
+ ent.e.windTunnelDirection = forward
+ if ( ent.IsPlayer() )
+ thread PlayerInWindTunnel( ent )
+ else
+ ent.SetOrigin( ent.GetOrigin() + <0,0,48> )
+ startTimes[ ent ] <- Time()
+ }
+ float startTime = startTimes[ ent ]
+ ent.e.windTunnelStartTime = startTime
+
+ float startHeight
+ if ( lifterFan )
+ {
+ if ( !( ent in startHeights ) )
+ startHeights[ ent ] <- ent.GetOrigin().z
+ startHeight = startHeights[ ent ]
+ }
+
+ // Figure out what force should be based on proximity to fan
+ vector pointAlongTunnel = GetClosestPointOnLineSegment( cylinderBottom, cylinderTop, testPos )
+ float distanceFromFanAlongTunnel = Distance( pointAlongTunnel, cylinderBottom )
+
+ float fanStrength = GetFanStrengthWithGeoBlockage( ent, testPos, cylinderBottom, cylinderTop, forward )
+ if ( ent.IsPlayer() )
+ ent.SetPlayerNetFloatOverTime( "FanRumbleStrength", fanStrength, 0.0 )
+
+ if ( lifterFan )
+ fanStrength = GraphCapped( distanceFromFanAlongTunnel, 0.0, fanPushDist, 1.0, 0.0 )
+
+ if ( ent.IsProjectile() )
+ fanStrength *= 2.0
+
+ if ( ent.GetModelName() == $"models/containers/barrel.mdl" )
+ fanStrength = 0.0
+
+ // Ramp up the push over time when first entering
+ float ramp = GraphCapped( Time(), startTime, startTime + FAN_PUSH_RAMP_TIME, 0.0, 1.0 )
+
+ float dt = 0.01666667 // old behavior
+ vector velocity = ent.GetVelocity()
+
+ // Apply push to the velocity
+ velocity += forward * ( dt * pushAccel * fanStrength )
+
+ // Decay other directional movement on the vector
+ if ( FAN_PUSH_DECAY_SIDE_VELOCITY )
+ {
+ vector velInOtherDirs = velocity - forward * DotProduct( velocity, forward )
+ float decayFrac = pow( ramp * fanStrength, dt )
+ vector loseVelInOtherDirs = velInOtherDirs * (1 - decayFrac)
+ velocity -= loseVelInOtherDirs
+ }
+
+ // Add some anti-gravity
+ velocity.z += dt * FAN_PUSH_ANTI_GRAVITY * fanStrength
+ ent.e.windTunnelStrength = fanStrength
+
+ // Apply new force to ent
+ ent.SetVelocity( velocity )
+
+ // Hack for drones. You can't set velocity on them yet so I'm doing this for now to test the gameplay
+ if ( ent.GetClassName() == "npc_drone" )
+ {
+ float zChange = dt * pushAccel * fanStrength * 1
+ ent.SetOrigin( ent.GetOrigin() + < 0, 0, zChange > )
+ }
+ }
+ else
+ {
+ if ( ent in startTimes )
+ {
+ ent.e.inWindTunnel = false
+ delete startTimes[ ent ]
+ }
+
+ if ( lifterFan && ent in startHeights )
+ {
+ delete startHeights[ ent ]
+ }
+
+ if ( ent.IsPlayer() )
+ ent.SetPlayerNetFloatOverTime( "FanRumbleStrength", 0.0, 1.0 )
+ }
+ }
+ WaitFrame()
+ }
+}
+
+void function PlayerInWindTunnel( entity player )
+{
+ //int poseIndex = player.LookupPoseParameterIndex( "windfrac" )
+
+ EndSignal( player, "OnDestroy" )
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( FAN_DEBUG )
+ printt( "OUT!" )
+ if ( IsValid( player ) )
+ {
+ player.SetOneHandedWeaponUsageOff()
+ FadeOutSoundOnEntity( player, "Beacon_WindBuffet_Player", 1.0 )
+
+ player.kv.airSpeed = player.GetPlayerSettingsField( "airSpeed" )
+ player.kv.airAcceleration = player.GetPlayerSettingsField( "airAcceleration" )
+ }
+ }
+ )
+
+ if ( FAN_DEBUG )
+ printt( "IN!" )
+
+ bool playingWindBuffet = false
+
+ player.kv.airSpeed = 150
+ player.kv.airAcceleration = 650
+
+ while( player.e.inWindTunnel )
+ {
+ //player.GetViewModelEntity().SetPoseParameter( poseIndex, player.e.windTunnelStrength )
+
+ if ( player.e.windTunnelStrength > 0 )
+ {
+ player.SetOneHandedWeaponUsageOn()
+ if ( !playingWindBuffet )
+ {
+ EmitSoundOnEntityOnlyToPlayerWithFadeIn( player, player, "Beacon_WindBuffet_Player", 1.0 )
+ playingWindBuffet = true
+ }
+ }
+ else
+ {
+ player.SetOneHandedWeaponUsageOff()
+ if ( playingWindBuffet )
+ {
+ FadeOutSoundOnEntity( player, "Beacon_WindBuffet_Player", 1.0 )
+ playingWindBuffet = false
+ }
+ }
+ WaitFrame()
+ }
+}
+
+float function GetFanStrengthWithGeoBlockage( entity ent, vector testPos, vector cylinderBottom, vector cylinderTop, vector fanDirection )
+{
+ vector pointAlongFan = GetClosestPointOnLineSegment( cylinderBottom, cylinderTop, testPos )
+ vector vecFromFanCenter = testPos - pointAlongFan
+ vector traceEnd = cylinderBottom + vecFromFanCenter
+
+ // Trace from the entity towards the fan along the fan axis to see if we are getting blocked
+ TraceResults result = TraceLine( testPos, traceEnd, ent, TRACE_MASK_NPCSOLID, TRACE_COLLISION_GROUP_NONE )
+ if ( FAN_DEBUG )
+ {
+ DebugDrawLine( testPos, result.endPos, 255, 255, 0, true, 0.1 )
+ DebugDrawLine( result.endPos, traceEnd, 255, 255, 255, true, 0.1 )
+ //DebugDrawLine( <0,0,0>, result.endPos, 255, 0, 0, true, 0.1 )
+ }
+
+ float distFromCover = Distance( testPos, result.endPos )
+ float strength = GraphCapped( distFromCover, 256, 1024, 0.0, 1.0 )
+ if ( result.fraction == 1.0 )
+ strength = 1.0
+
+ //if ( FAN_DEBUG )
+ //{
+ // printt( "strength:", strength )
+ // printt( "fraction:", result.fraction )
+ // printt( "dist from fan:", Distance( testPos, cylinderBottom ) )
+ // printt( "distFromCover:", distFromCover )
+ //}
+
+ Assert( strength >= 0.0 && strength <= 1.0 )
+ return strength
+}
+
+void function FanOnSoundEffects( entity fanPusher, float radius, entity fanSoundEntity )
+{
+ // Turn on sound
+ if ( radius >= 350 )
+ {
+ EmitSoundOnEntity( fanSoundEntity, "Beacon_VerticalFanControl_On" )
+ if ( fanPusher.HasKey( "fan_loop_sound" ) )
+ EmitSoundOnEntity( fanSoundEntity, string( fanPusher.kv.fan_loop_sound ) )
+
+ }
+ else
+ {
+ EmitSoundOnEntity( fanSoundEntity, "Beacon_MediumBlueFan_On" )
+ string loopAlias = fanPusher.HasKey( "fan_loop_sound" ) ? string( fanPusher.kv.fan_loop_sound ) : "Beacon_MediumBlueFan_Loop_01"
+ EmitSoundOnEntity( fanSoundEntity, loopAlias )
+ //DebugDrawText( fanPusher.GetOrigin(), loopAlias, true, 90.0 )
+ }
+}
+
+void function FanOffSoundEffects( entity fanPusher, float radius, entity fanSoundEntity )
+{
+ // Turn off sound
+ if ( radius >= 350 )
+ {
+ string alias = "Beacon_VerticalFanControl_Off"
+ if ( fanPusher.HasKey( "shutoff_sound" ) )
+ alias = string( fanPusher.kv.shutoff_sound )
+ EmitSoundOnEntity( fanSoundEntity, alias )
+
+ if ( fanPusher.HasKey( "fan_loop_sound" ) )
+ StopSoundOnEntity( fanSoundEntity, string( fanPusher.kv.fan_loop_sound ) )
+ }
+ else
+ {
+ EmitSoundOnEntity( fanSoundEntity, "Beacon_MediumBlueFan_Off" )
+ string loopAlias = fanPusher.HasKey( "fan_loop_sound" ) ? string( fanPusher.kv.fan_loop_sound ) : "Beacon_MediumBlueFan_Loop_01"
+ StopSoundOnEntity( fanSoundEntity, loopAlias )
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_script_movers_light.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_script_movers_light.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_script_movers_light.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_script_triggers.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_script_triggers.gnut
new file mode 100644
index 00000000..c5e026b3
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_script_triggers.gnut
@@ -0,0 +1,318 @@
+
+untyped
+
+globalize_all_functions
+
+// flags defaults to player only
+entity function CreateTriggerRadiusMultiple( vector origin, float radius, array<entity> ents = [], int flags = TRIG_FLAG_PLAYERONLY, float top = 16384.0, float bottom = -16384.0 )
+{
+ return _CreateScriptCylinderTriggerInternal( origin, radius, flags, ents, top, bottom )
+}
+
+// flags defaults to player only
+entity function CreateTriggerRadiusOnce( vector origin, float radius, array<entity> ents = [], int flags = TRIG_FLAG_PLAYERONLY )
+{
+ return _CreateScriptCylinderTriggerInternal( origin, radius, flags | TRIG_FLAG_ONCE, ents, null, null )
+}
+
+entity function CreateScriptCylinderTrigger( vector origin, float radius, float ornull top = null, float ornull bottom = null )
+{
+ return _CreateScriptCylinderTriggerInternal( origin, radius, TRIG_FLAG_PLAYERONLY | TRIG_FLAG_START_DISABLED, [], top, bottom )
+}
+
+entity function _CreateScriptCylinderTriggerInternal( vector origin, float radius, int flags, array<entity> ents, float ornull top, float ornull bottom )
+{
+ entity trigger = CreateScriptRef( origin, < 0.0, 0.0, 0.0 > )
+
+
+ if ( flags & TRIG_FLAG_START_DISABLED )
+ trigger.e.scriptTriggerData.enabled = false
+ else
+ trigger.e.scriptTriggerData.enabled = true
+ trigger.e.scriptTriggerData.radius = radius
+
+ trigger.e.scriptTriggerData.top = 16384.0
+
+ if ( top != null )
+ trigger.e.scriptTriggerData.top = origin.z + expect float( top )
+
+ trigger.e.scriptTriggerData.bottom = -16384.0
+ if ( bottom != null )
+ trigger.e.scriptTriggerData.bottom = origin.z + expect float( bottom )
+
+ trigger.e.scriptTriggerData.flags = flags
+ trigger.e.scriptTriggerData.managedEntArrayHandle = -1
+
+ if ( ents.len() )
+ {
+ trigger.e.scriptTriggerData.flags = (flags | TRIG_FLAG_EXCLUSIVE)
+ trigger.e.scriptTriggerData.managedEntArrayHandle = CreateScriptManagedEntArray()
+
+ foreach ( ent in ents )
+ AddToScriptManagedEntArray( trigger.e.scriptTriggerData.managedEntArrayHandle, ent )
+ }
+
+ if ( flags & TRIG_FLAG_DEVDRAW )
+ DebugDrawTrigger( origin, radius, RandomInt( 255 ), RandomInt( 255 ), RandomInt( 255 ) )
+
+ thread CylinderTriggerThink( trigger )
+
+ return trigger
+}
+
+void function ScriptTriggerSetEnabled( entity trigger, bool state )
+{
+ trigger.e.scriptTriggerData.enabled = state
+}
+
+void function CylinderTriggerThink( entity triggerEnt )
+{
+ //Ensures that any callbacks the user sets are in place when the user spawns.
+ WaitFrame()
+
+ bool wasEnabled = triggerEnt.e.scriptTriggerData.enabled
+ int flags = triggerEnt.e.scriptTriggerData.flags
+
+ while ( IsValid( triggerEnt ) )
+ {
+ if ( !triggerEnt.e.scriptTriggerData.enabled )
+ {
+ if ( wasEnabled )
+ {
+ array<entity> entitiesToRemove // build an array since looping through a table and removing elements is undefined
+ foreach( ent in triggerEnt.e.scriptTriggerData.entities )
+ {
+ entitiesToRemove.append( ent )
+ }
+
+ foreach ( ent in entitiesToRemove )
+ {
+ ScriptTriggerRemoveEntity( triggerEnt, ent )
+ }
+
+ Assert( !triggerEnt.e.scriptTriggerData.entities.len() )
+ }
+ }
+ else
+ {
+ array<entity> entities
+ if ( flags & TRIG_FLAG_EXCLUSIVE )
+ {
+ entities = GetScriptManagedEntArray( triggerEnt.e.scriptTriggerData.managedEntArrayHandle )
+ // all of the entites from this array are gone, this trigger is of no use
+ if ( !entities.len() )
+ {
+ triggerEnt.Kill_Deprecated_UseDestroyInstead()
+ return
+ }
+ }
+ else if ( flags & TRIG_FLAG_PLAYERONLY )
+ {
+ entities = GetPlayerArray()
+ }
+ else if ( flags & TRIG_FLAG_NPCONLY )
+ {
+ entities = GetNPCArray()
+ }
+ else
+ {
+ entities = GetPlayerArray()
+ entities.extend( GetNPCArray() )
+ entities.extend( GetPlayerDecoyArray() )
+ }
+
+ foreach ( ent in entities )
+ {
+ if ( !IsAlive( ent ) )
+ {
+ if ( ent in triggerEnt.e.scriptTriggerData.entities )
+ ScriptTriggerRemoveEntity( triggerEnt, ent )
+ continue
+ }
+
+ if ( ent.IsPlayer() && ent.IsPhaseShifted() && (flags & TRIG_FLAG_NO_PHASE_SHIFT) )
+ {
+ if ( ent in triggerEnt.e.scriptTriggerData.entities )
+ ScriptTriggerRemoveEntity( triggerEnt, ent )
+ continue
+ }
+
+ vector entityOrg = ent.GetOrigin()
+
+ if ( Distance2D( entityOrg, triggerEnt.GetOrigin() ) < triggerEnt.e.scriptTriggerData.radius )
+ {
+ if ( entityOrg.z > triggerEnt.e.scriptTriggerData.top )
+ continue
+
+ if ( entityOrg.z + 72.0 < triggerEnt.e.scriptTriggerData.bottom ) //72 is magic number for height of players. Should account for height of NPCs
+ continue
+
+ if ( (flags & TRIG_FLAG_NOCONTEXTBUSY) && !ent.IsPlayerDecoy() && ent.ContextAction_IsBusy() ) //This should probably be ContextAction_IsActive()
+ continue
+
+ if ( !(ent in triggerEnt.e.scriptTriggerData.entities) )
+ {
+ ScriptTriggerAddEntity( triggerEnt, ent )
+ if ( flags & TRIG_FLAG_ONCE )
+ {
+ WaitEndFrame()
+ triggerEnt.Kill_Deprecated_UseDestroyInstead()
+ return
+ }
+ }
+ }
+ else if ( ent in triggerEnt.e.scriptTriggerData.entities )
+ {
+ ScriptTriggerRemoveEntity( triggerEnt, ent )
+ }
+ }
+ }
+
+ wasEnabled = triggerEnt.e.scriptTriggerData.enabled
+ WaitFrame()
+ }
+}
+
+void function ScriptTriggerRemoveEntity( entity triggerEnt, entity ent )
+{
+ Assert( ent in triggerEnt.e.scriptTriggerData.entities )
+
+ foreach ( callbackFunc in triggerEnt.e.scriptTriggerData.leaveCallbacks )
+ {
+ callbackFunc( triggerEnt, ent )
+ }
+
+ delete triggerEnt.e.scriptTriggerData.entities[ent]
+}
+
+void function ScriptTriggerAddEntity( entity triggerEnt, entity ent )
+{
+ Assert( !(ent in triggerEnt.e.scriptTriggerData.entities) )
+
+ triggerEnt.e.scriptTriggerData.entities[ent] <- ent
+
+ foreach ( callbackFunc in triggerEnt.e.scriptTriggerData.enterCallbacks )
+ {
+ callbackFunc( triggerEnt, ent )
+ }
+
+ triggerEnt.Signal( TRIGGER_INTERNAL_SIGNAL )
+
+ thread ScriptTriggerPlayerDisconnectThink( triggerEnt, ent )
+}
+
+void function ScriptTriggerPlayerDisconnectThink( entity triggerEnt, entity ent )
+{
+ triggerEnt.EndSignal( "OnDestroy" )
+ ent.EndSignal( "OnDeath" )
+
+ OnThreadEnd(
+ function() : ( triggerEnt, ent )
+ {
+ if ( !IsValid( ent ) )
+ return
+
+ if ( ent in triggerEnt.e.scriptTriggerData.entities )
+ ScriptTriggerRemoveEntity( triggerEnt, ent )
+ }
+ )
+
+ ent.WaitSignal( "OnDestroy" )
+}
+
+array<entity> function GetAllEntitiesInTrigger( entity trigger )
+{
+ array<entity> ents
+ foreach ( ent in trigger.e.scriptTriggerData.entities )
+ {
+ ents.append( ent )
+ }
+
+ return ents
+}
+
+void function AddCallback_ScriptTriggerEnter( entity trigger, void functionref( entity, entity ) callbackFunc )
+{
+ trigger.e.scriptTriggerData.enterCallbacks.append( callbackFunc )
+}
+
+void function AddCallback_ScriptTriggerLeave( entity trigger, void functionref( entity, entity ) callbackFunc )
+{
+ trigger.e.scriptTriggerData.leaveCallbacks.append( callbackFunc )
+}
+
+/*
+void function thing()
+{
+ RegisterSignal( "TriggerGetOutThink" )
+
+ array<entity> targets = GetEntArrayByScriptName( "fling_target" )
+
+ foreach ( target in targets )
+ {
+ entity trigger = CreateEntity( "trigger_cylinder" )
+ trigger.SetRadius( 128 )
+ trigger.SetAboveHeight( 64 ) //Still not quite a sphere, will see if close enough
+ trigger.SetBelowHeight( 32 )
+ trigger.SetOrigin( target.GetOrigin() )
+ trigger.ConnectOutput( "OnStartTouch", TriggerGetOutStartTouch )
+ DispatchSpawn( trigger )
+ }
+}
+*/
+void function TriggerGetOutStartTouch( entity trigger, entity ent, entity caller, var value )
+{
+ if ( !ent.IsPlayer() )
+ return
+
+ if ( ent.IsTitan() )
+ return
+
+ if ( !ent.IsAlive() )
+ return
+
+ thread TriggerGetOutThink( trigger, ent )
+}
+
+void function TriggerGetOutThink( entity trigger, entity player )
+{
+ const float FALL_OFF_SPEED_MIN = 100
+ const float FALL_OFF_SPEED_MAX = 300
+ const float FALL_OFF_ACCEL = 300
+ const float FALL_OFF_INTERVAL = 0.1
+
+ while ( IsAlive( player ) && trigger.IsTouching( player ) )
+ {
+ if ( player.IsOnGround() )
+ {
+ vector vel = player.GetVelocity()
+
+ float len = vel.Length2D()
+ while ( len < 1.0 )
+ {
+ vel.x = RandomFloatRange( -100, 100 )
+ vel.y = RandomFloatRange( -100, 100 )
+ len = vel.Length2D()
+ }
+
+ if ( len < FALL_OFF_SPEED_MIN )
+ {
+ float scale = FALL_OFF_SPEED_MIN / len
+ vel.x *= scale
+ vel.y *= scale
+ }
+ else if ( len < FALL_OFF_SPEED_MAX )
+ {
+ float newlen = len + FALL_OFF_INTERVAL * FALL_OFF_ACCEL
+ newlen = min( newlen, FALL_OFF_SPEED_MAX )
+ float scale = newlen / len
+ vel.x *= scale
+ vel.y *= scale
+ }
+
+ player.SetVelocity( vel )
+ }
+
+ WaitFrame()
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_side_notifications.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_side_notifications.gnut
new file mode 100644
index 00000000..2b3d3993
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_side_notifications.gnut
@@ -0,0 +1,6 @@
+global function PROTO_PlayLoadoutNotification
+
+void function PROTO_PlayLoadoutNotification(string weapon, entity player)
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_store.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_store.gnut
new file mode 100644
index 00000000..5ebf090a
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_store.gnut
@@ -0,0 +1,38 @@
+//todo some of the stuff here should probably get moved to other scripts
+
+global function PIN_BuyItemWithRealMoney
+global function PIN_BuyItem
+global function PIN_GiveItem
+global function PIN_GiveCredits
+global function PIN_ConsumeItem
+global function PIN_AddToPlayerCountStat
+
+void function PIN_BuyItemWithRealMoney(entity player, bool _0, string name, int cost)
+{
+
+}
+
+void function PIN_BuyItem(entity player, bool _0, string name, int cost)
+{
+
+}
+
+void function PIN_GiveItem(entity player, bool _0, string name, int count)
+{
+
+}
+
+void function PIN_GiveCredits(entity player, int count)
+{
+
+}
+
+void function PIN_ConsumeItem(entity player, string name)
+{
+
+}
+
+void function PIN_AddToPlayerCountStat(entity player, string name)
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_trigger_functions.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_trigger_functions.gnut
new file mode 100644
index 00000000..0f82d9a6
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_trigger_functions.gnut
@@ -0,0 +1,585 @@
+untyped
+
+global function TriggerFunctions_Init
+
+global function InitFlagMaskTriggers
+global function TriggerInit
+global function AddToFlagTriggers
+global function AddTriggerEditorClassFunc
+global function UpdateTriggerStatusFromFlagChange
+
+const DEBUG_DEADLY_FOG = false
+const asset BIRD_ALERT_FX = $"P_bird_alert_white"
+struct BirdAlertInfo
+{
+ entity scriptRef //Also FX location
+ array<entity> triggers
+ float lastUseTime
+}
+
+struct
+{
+ //bool checkpointReached
+ //vector checkpointOrigin
+ //vector checkpointAngles
+ table<string, array<void functionref( entity )> > triggerEditorClassFunctions
+
+ table<string, int> flagTriggerEntArrayIndex
+ array<BirdAlertInfo> birdAlerts
+} file
+
+function TriggerFunctions_Init()
+{
+ AddCallback_EntitiesDidLoad( InitFlagMaskTriggers )
+
+ level._flagTriggers <- {} // triggers that can be enabled/disabled via flag
+
+ AddTriggerEditorClassFunc( "trigger_flag_set", TriggerSetFlagOnTrigger )
+ AddTriggerEditorClassFunc( "trigger_flag_clear", TriggerClearFlagOnTrigger )
+ AddTriggerEditorClassFunc( "trigger_flag_touching", TriggerTouchingFlagOnTrigger )
+
+ AddSpawnCallback( "trigger_multiple", TriggerInit )
+ AddSpawnCallback( "trigger_once", TriggerInit )
+ AddSpawnCallback( "trigger_hurt", TriggerInit )
+
+ AddTriggerEditorClassFunc( "trigger_death_fall", TriggerDeathFall )
+ AddSpawnCallbackEditorClass( "trigger_multiple", "trigger_deadly_fog", DeadlyFogTriggerInit )
+
+ AddSpawnCallbackEditorClass( "script_ref", "script_bird_alert", BirdAlertInit )
+
+ PrecacheParticleSystem( BIRD_ALERT_FX )
+
+ RegisterSignal( "OutOfDeadlyFog" )
+
+ PrecacheParticleSystem( $"Swtch_Elec_hzd_rope_end" )
+}
+
+void function AddTriggerEditorClassFunc( string editorClass, void functionref( entity ) triggerFunc )
+{
+ if ( !( editorClass in file.triggerEditorClassFunctions ) )
+ file.triggerEditorClassFunctions[ editorClass ] <- []
+
+ file.triggerEditorClassFunctions[ editorClass ].append( triggerFunc )
+}
+
+void function AddKeyPairFunctionality( entity trigger )
+{
+ table< string, void functionref( entity, string )> funcs
+ funcs[ "scr_flagSet" ] <- TriggerFlagSet
+ funcs[ "scr_flagClear" ] <- TriggerFlagClear
+
+ foreach ( key, func in funcs )
+ {
+ if ( trigger.HasKey( key ) )
+ {
+ thread func( trigger, expect string( trigger.kv[ key ] ) )
+ }
+ }
+}
+
+function AddToFlagTriggers( entity self )
+{
+ level._flagTriggers[ self ] <- self
+}
+
+function GetFlagTriggers()
+{
+ foreach ( entity guy in clone level._flagTriggers )
+ {
+ if ( IsValid_ThisFrame( guy ) )
+ continue
+
+ delete level._flagTriggers[ guy ]
+ }
+
+ return level._flagTriggers
+}
+
+
+function AddKeyPairFunctionToClass( funcs, classname )
+{
+ array<entity> triggers = GetEntArrayByClass_Expensive( classname )
+
+ foreach ( trigger in triggers )
+ {
+ foreach ( key, func in funcs )
+ {
+ if ( trigger.HasKey( key ) )
+ {
+ thread func( trigger, trigger.kv[ key ] )
+ }
+ }
+ }
+}
+
+void function TriggerChangesFlagOnTrigger( entity trigger, string flag, void functionref( string ) func )
+{
+ trigger.EndSignal( "OnDestroy" )
+
+ array<string> flags = GetFlagsFromString( flag )
+
+ for ( ;; )
+ {
+ trigger.WaitSignal( "OnTrigger" )
+
+ foreach ( flag in flags )
+ {
+ func( flag )
+ }
+ }
+}
+
+void function TriggerFlagSet( entity trigger, string flagString )
+{
+ thread TriggerChangesFlagOnTrigger( trigger, flagString, FlagSet )
+}
+
+void function TriggerFlagClear( entity trigger, string flagString )
+{
+ thread TriggerChangesFlagOnTrigger( trigger, flagString, FlagClear )
+}
+
+void function TriggerInit( entity trigger )
+{
+ if ( trigger.HasKey( "editorclass" ) )
+ RunTriggerEditorClassFunctions( trigger )
+
+ InitFlagsFromTrigger( trigger )
+ AddKeyPairFunctionality( trigger )
+ AddToFlagTriggers( trigger )
+}
+
+function RunTriggerEditorClassFunctions( entity trigger )
+{
+ string editorClass = expect string( trigger.kv.editorclass )
+ if ( !( editorClass in file.triggerEditorClassFunctions ) )
+ return
+
+ foreach ( func in file.triggerEditorClassFunctions[ editorClass ] )
+ {
+ thread func( trigger )
+ }
+}
+
+void function TriggerSetFlagOnTrigger( entity trigger )
+{
+ trigger.EndSignal( "OnDestroy" )
+
+ string flag
+ if ( trigger.HasKey( "script_flag" ) )
+ flag = expect string( trigger.kv.script_flag )
+ else if ( trigger.HasKey( "scr_flagSet" ) )
+ flag = expect string( trigger.kv.scr_flagSet )
+
+ bool triggerOnce = trigger.HasKey( "trigger_once" ) && trigger.kv.trigger_once == "1"
+
+ Assert( flag != "", "Trigger " + GetEditorClass( trigger ) + " at " + trigger.GetOrigin() + "has empty flag value" )
+
+ while ( true )
+ {
+ trigger.WaitSignal( "OnTrigger" )
+ FlagSet( flag )
+
+ if ( triggerOnce )
+ return
+
+ FlagWaitClear( flag )
+ }
+}
+
+void function TriggerClearFlagOnTrigger( entity trigger )
+{
+ string flag
+ if ( trigger.HasKey( "script_flag" ) )
+ flag = expect string( trigger.kv.script_flag )
+ else if ( trigger.HasKey( "scr_flagClear" ) )
+ flag = expect string( trigger.kv.scr_flagClear )
+
+ Assert( flag != "" )
+
+ thread TriggerFlagClear( trigger, flag )
+}
+
+void function TriggerTouchingFlagOnTrigger( entity trigger )
+{
+ trigger.EndSignal( "OnDestroy" )
+ string flag = expect string( trigger.kv.script_flag )
+ Assert( flag != "" )
+
+ while ( true )
+ {
+ if ( !trigger.IsTouched() )
+ trigger.WaitSignal( "OnStartTouch" )
+
+ FlagSet( flag )
+
+ if ( trigger.IsTouched() )
+ trigger.WaitSignal( "OnEndTouchAll" )
+
+ FlagClear( flag )
+ }
+}
+
+array<string> function GetFlagRelatedKeys()
+{
+ array<string> check
+ check.append( "scr_flagTrueAll" )
+ check.append( "scr_flagTrueAny" )
+ check.append( "scr_flagFalseAll" )
+ check.append( "scr_flagFalseAny" )
+ check.append( "scr_flag" )
+ check.append( "script_flag" )
+ check.append( "scr_flagSet" )
+ check.append( "scr_flagClear" )
+
+ return check
+}
+
+void function InitFlagMaskTriggers()
+{
+ local triggers = GetFlagTriggers()
+ array<string> check = GetFlagRelatedKeys()
+ array<string> flags
+ local allTriggersWithFlags = {}
+
+ foreach ( trigger in triggers )
+ {
+ if ( trigger.HasKey( "scr_flagTrueAll" ) )
+ {
+ Assert( !trigger.HasKey( "scr_flagTrueAny" ), "Trigger at " + trigger.GetOrigin() + " has flag all and flag any" )
+ }
+ else
+ if ( trigger.HasKey( "scr_flagTrueAny" ) )
+ {
+ Assert( !trigger.HasKey( "scr_flagTrueAll" ), "Trigger at " + trigger.GetOrigin() + " has flag all and flag any" )
+ }
+
+ if ( trigger.HasKey( "scr_flagFalseAll" ) )
+ {
+ Assert( !trigger.HasKey( "scr_flagFalseAny" ), "Trigger at " + trigger.GetOrigin() + " has flag all and flag any" )
+ }
+ else
+ if ( trigger.HasKey( "scr_flagFalseAny" ) )
+ {
+ Assert( !trigger.HasKey( "scr_flagFalseAll" ), "Trigger at " + trigger.GetOrigin() + " has flag all and flag any" )
+ }
+
+ foreach ( field in check )
+ {
+ if ( trigger.HasKey( field ) )
+ {
+ allTriggersWithFlags[ trigger ] <- true
+ flags = GetFlagsFromField( trigger, field )
+
+ foreach ( flag in flags )
+ {
+ if ( !( flag in file.flagTriggerEntArrayIndex ) )
+ file.flagTriggerEntArrayIndex[ flag ] <- CreateScriptManagedEntArray()
+
+ AddToScriptManagedEntArray( file.flagTriggerEntArrayIndex[ flag ], trigger )
+
+ // init the flag so these flags an be used in hammer more easily
+ FlagInit( flag )
+ }
+ }
+ }
+ }
+
+ foreach ( trigger, _ in allTriggersWithFlags )
+ {
+ expect entity( trigger )
+ SetTriggerEnableFromFlag( trigger )
+ }
+}
+
+void function SetTriggerEnableFromFlag( entity trigger )
+{
+ if ( GetTriggerEnabled( trigger ) )
+ trigger.Fire( "Enable" )
+ else
+ trigger.Fire( "Disable" )
+}
+
+void function UpdateTriggerStatusFromFlagChange( string flag )
+{
+ // enable or disable triggers based on flag settings
+ if ( !( flag in file.flagTriggerEntArrayIndex ) )
+ return
+
+ array<entity> triggers = GetScriptManagedEntArray( file.flagTriggerEntArrayIndex[ flag ] )
+ foreach ( trigger in triggers )
+ {
+ SetTriggerEnableFromFlag( trigger )
+ }
+}
+
+function InitFlagsFromTrigger( entity trigger )
+{
+ array<string> check = GetFlagRelatedKeys()
+ array<string> flags
+
+ foreach ( field in check )
+ {
+ if ( !trigger.HasKey( field ) )
+ continue
+ flags = GetFlagsFromField( trigger, field )
+
+ foreach ( flag in flags )
+ {
+ // init the flag so these flags an be used in hammer more easily
+ FlagInit( flag )
+ }
+ }
+}
+
+
+void function DeadlyFogTriggerInit( entity trigger )
+{
+ trigger.ConnectOutput( "OnStartTouch", DeadlyFogStartTouch )
+ trigger.ConnectOutput( "OnEndTouch", DeadlyFogEndTouch )
+
+ if ( trigger.HasKey( "electricEffect" ) && trigger.kv.electricEffect == "1" )
+ thread DeadlyFogVisuals( trigger )
+}
+
+void function DeadlyFogStartTouch( entity trigger, entity ent, entity caller, var value )
+{
+ thread DeadlyFogDamagedEntity( trigger, ent )
+}
+
+void function DeadlyFogDamagedEntity( entity trigger, entity ent )
+{
+ if ( !IsAlive( ent ) || !IsValid( trigger ) )
+ return
+
+ EndSignal( ent, "OutOfDeadlyFog" )
+ EndSignal( trigger, "OnDestroy" )
+ EndSignal( ent, "OnDeath" )
+
+ bool damagePilots = trigger.kv.damagePilots == "1"
+ bool damageTitans = trigger.kv.damageTitans == "1"
+ if ( !damagePilots && !damageTitans )
+ return
+
+ float tickTime = 0.5
+ float timeTillDeath = 4.0
+
+ entity worldSpawn = GetEnt( "worldspawn" )
+ while( true )
+ {
+ if ( !IsValid( ent ) )
+ {
+ wait 0.5
+ continue
+ }
+
+ if ( IsPilot( ent ) && !damagePilots )
+ {
+ wait 0.5
+ continue
+ }
+
+ if ( ent.IsTitan() && !damageTitans )
+ {
+ wait 0.5
+ continue
+ }
+
+ local isTitan = ent.IsTitan()
+ local damageOrigin = ent.GetOrigin() + ( isTitan ? < 0.0, 0.0, 0.0 > : < 0.0 , 0.0, -200.0 > )
+ damageOrigin += < RandomFloatRange( -300.0, 300.0 ), RandomFloatRange( -300.0, 300.0 ), RandomFloatRange( -100.0, 100.0 ) >
+ local scriptTypeMask = damageTypes.dissolve | DF_STOPS_TITAN_REGEN
+
+ local damageAmount = ( ent.GetMaxHealth() / ( timeTillDeath / tickTime ) )
+
+ ent.TakeDamage( damageAmount, worldSpawn, worldSpawn, { origin = damageOrigin, scriptType = scriptTypeMask, damageSourceId = eDamageSourceId.deadly_fog } )
+
+ if ( ent.IsPlayer() )
+ StatusEffect_AddTimed( ent, eStatusEffect.emp, 1.0, 1.0, 0.5 )
+
+ wait 0.5
+ }
+}
+
+void function DeadlyFogEndTouch( entity trigger, entity ent, entity caller, var value )
+{
+ if ( IsValid( ent ) )
+ Signal( ent, "OutOfDeadlyFog" )
+}
+
+void function DeadlyFogVisuals( entity trigger )
+{
+ wait 0.5
+
+ // Get the trigger bounds
+ vector triggerMins = trigger.GetBoundingMins()
+ vector triggerMaxs = trigger.GetBoundingMaxs()
+ vector triggerOrigin = trigger.GetOrigin()
+ if ( DEBUG_DEADLY_FOG )
+ {
+ DebugDrawBox( triggerOrigin, triggerMins, triggerMaxs, 255, 255, 0, 1, 60.0 )
+ DebugDrawSphere( triggerOrigin, 25.0, 255, 200, 0, true, 60.0 )
+ }
+
+ // Divide the trigger into smaller squares
+ vector triggerDimension = triggerMaxs - triggerMins
+
+ int segmentSizeX = int( max( triggerDimension.x / 2000, 1500 ) )
+ int segmentSizeY = int( max( triggerDimension.y / 2000, 1500 ) )
+ int segmentSizeZ = int( min( 300, triggerDimension.z ) )
+
+ vector segmentSize = Vector( segmentSizeX, segmentSizeY, segmentSizeZ )
+ vector segmentCount = Vector( triggerDimension.x / segmentSize.x, triggerDimension.y / segmentSize.y, triggerDimension.z / segmentSize.z )
+
+ segmentCount.x = floor( segmentCount.x )
+ segmentCount.y = floor( segmentCount.y )
+ segmentCount.z = floor( segmentCount.z )
+ segmentCount.x = segmentCount.x < 1.0 ? 1.0 : segmentCount.x
+ segmentCount.y = segmentCount.y < 1.0 ? 1.0 : segmentCount.y
+ segmentCount.z = segmentCount.z < 1.0 ? 1.0 : segmentCount.z
+
+ vector startPos = triggerOrigin + triggerMins + segmentSize * 0.5
+ startPos.x += (triggerDimension.x - (segmentCount.x * segmentSize.x)) * 0.5
+ startPos.y += (triggerDimension.y - (segmentCount.y * segmentSize.y)) * 0.5
+ startPos.z += (triggerDimension.z - (segmentCount.z * segmentSize.z)) * 0.5
+
+ vector segmentPos = startPos
+ for ( int z = 0 ; z < segmentCount.z ; z++ )
+ {
+ // Only do effects on the top layer of the trigger
+ if ( z < ( segmentCount.z - 1 ) )
+ continue
+
+ for ( int y = 0 ; y < floor(segmentCount.y) ; y++ )
+ {
+ for ( int x = 0 ; x < floor(segmentCount.x) ; x++ )
+ {
+ vector segmentPos = startPos + Vector( segmentSize.x * x, segmentSize.y * y, segmentSize.z * z )
+ thread DeadlyFogEffect( segmentPos, segmentSize )
+ }
+ }
+ }
+}
+
+void function DeadlyFogEffect( vector origin, vector segmentSize )
+{
+ entity effect = CreateEntity( "info_particle_system" )
+ effect.SetValueForEffectNameKey( $"Swtch_Elec_hzd_rope_end" )
+ effect.kv.start_active = 0
+ effect.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ effect.SetOrigin( origin )
+ DispatchSpawn( effect )
+
+ vector mins = origin + (segmentSize * -0.5)
+ vector maxs = origin + (segmentSize * 0.5)
+ if ( DEBUG_DEADLY_FOG )
+ {
+ DebugDrawBox( origin, mins - origin, maxs - origin, 255, 255, 0, 1, 60.0 )
+ DebugDrawSphere( origin, 25.0, 255, 200, 0, true, 60.0 )
+ }
+
+ while( true )
+ {
+ wait RandomFloatRange( 2.1, 2.6 )
+
+ vector org = Vector( RandomFloatRange( mins.x, maxs.x ), RandomFloatRange( mins.y, maxs.y ), RandomFloatRange( mins.z, maxs.z ) )
+
+ if ( DEBUG_DEADLY_FOG )
+ DebugDrawLine( origin, org, 255, 0, 0, true, 2.0 )
+
+ effect.SetOrigin( org )
+ effect.Fire( "Start" )
+ effect.Fire( "StopPlayEndCap", "", 2.0 )
+ }
+}
+
+void function TriggerDeathFall( entity trigger )
+{
+ EndSignal( trigger, "OnDestroy" )
+
+ while ( true )
+ {
+ table results = trigger.WaitSignal( "OnTrigger" )
+ entity player = expect entity( results.activator )
+ if ( !IsValid( player ) || !player.IsPlayer() || !IsAlive( player ) )
+ continue
+
+ if ( player.IsGodMode() )
+ {
+ printt( "GOD MODE PLAYER CANT DIE" )
+ continue
+ }
+
+ if ( player.p.doingQuickDeath )
+ continue
+
+ if ( IsSingleplayer() )
+ FlagClear( "SaveGame_Enabled" ) // no more saving, you have lost
+
+ player.EndSignal( "OnDeath" )
+
+ // Go to black and fade it out after a pause
+ float fadeTime = 0.5
+ float holdTime = 999
+ ScreenFade( player, 0, 1, 0, 255, fadeTime, holdTime, FFADE_OUT | FFADE_PURGE )
+
+ float deathTime = Time() + fadeTime
+ while ( Time() <= deathTime )
+ {
+ if ( player.IsOnGround() || player.IsWallRunning() || player.IsWallHanging() || player.p.doingQuickDeath )
+ break
+ WaitFrame()
+ }
+
+ if ( player.p.doingQuickDeath )
+ continue
+
+ if ( IsAlive( player ) )
+ {
+ KillPlayer( player, eDamageSourceId.fall )
+ return
+ }
+ }
+}
+
+
+void function BirdAlertInit( entity ref )
+{
+ BirdAlertInfo info
+ info.scriptRef = ref
+ array<entity> linkedEntities = ref.GetLinkEntArray()
+ foreach ( trigger in linkedEntities )
+ {
+ info.triggers.append( trigger )
+ trigger.ConnectOutput( "OnStartTouch", BirdAlertStartTouch )
+ }
+ file.birdAlerts.append( info )
+}
+
+void function BirdAlertStartTouch( entity trigger, entity ent, entity caller, var value )
+{
+ array<BirdAlertInfo> birdAlerts = GetBirdAlertInfoFromTrigger( trigger )
+ foreach( alert in birdAlerts )
+ {
+ float debounceTime = 6.0
+ if ( alert.lastUseTime + debounceTime > Time() )
+ return
+
+ StartParticleEffectInWorld( GetParticleSystemIndex( BIRD_ALERT_FX ), alert.scriptRef.GetOrigin(), alert.scriptRef.GetAngles() )
+ alert.lastUseTime = Time()
+ }
+}
+
+array<BirdAlertInfo> function GetBirdAlertInfoFromTrigger( entity trigger )
+{
+ array<BirdAlertInfo> birdAlerts
+ foreach ( infoStruct in file.birdAlerts )
+ {
+ if ( infoStruct.triggers.contains( trigger ) )
+ birdAlerts.append( infoStruct )
+ }
+
+ return birdAlerts
+ unreachable
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_utility.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_utility.gnut
new file mode 100644
index 00000000..50851dae
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_utility.gnut
@@ -0,0 +1,4394 @@
+
+globalize_all_functions
+
+//=========================================================
+// _utility
+//
+//=========================================================
+
+int functionref( bool = false ) ornull te = null
+int EntTracker = 0
+
+global const C_PLAYFX_SINGLE = 0
+global const C_PLAYFX_MULTIPLE = 1
+global const C_PLAYFX_LOOP = 2
+
+global const MUTEALLFADEIN = 2
+
+global struct ZipLine
+{
+ entity start
+ entity mid
+ entity end
+}
+
+global int HUMAN_RAGDOLL_IMPACT_TABLE_IDX = -1
+
+global struct ArrayDotResultStruct
+{
+ entity ent
+ float dot
+}
+
+global struct ShieldDamageModifier
+{
+ float permanentDamageFrac = TITAN_SHIELD_PERMAMENT_DAMAGE_FRAC
+ bool normalizeShieldDamage = false
+ float damageScale = 1.0
+}
+
+
+struct
+{
+ bool isSkyboxView = false
+} file
+
+void function Utility_Init()
+{
+ te = TotalEnts
+ EntTracker = 0
+
+ #document( "SetDeathFuncName", "Sets the name of a function that runs when the NPC dies." )
+ #document( "CenterPrint", "Print to the screen" )
+ #document( "GetAllSoldiers", "Get all living soldiers." )
+ #document( "ClearDeathFuncName", "Clears the script death function." )
+
+ HUMAN_RAGDOLL_IMPACT_TABLE_IDX = PrecacheImpactEffectTable( "ragdoll_human" )
+
+ RegisterSignal( "PetTitanUpdated" )
+ RegisterSignal( "WaitDeadTimeOut" )
+ RegisterSignal( "InventoryChanged" )
+
+ AddClientCommandCallback( "OnDevnetBugScreenshot", ClientCommand_OnDevnetBugScreenshot )
+ #if DEV
+ FlagInit( "AimAssistSwitchTest_Enabled" )
+ AddClientCommandCallback( "DoomTitan", ClientCommand_DoomTitan )
+ #endif
+}
+
+void function GiveAllTitans()
+{
+
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ #if MP
+ GiveTitanToPlayer( player )
+ #endif
+
+ if ( player.IsTitan() )
+ {
+ entity soul = player.GetTitanSoul()
+ if ( soul )
+ {
+ SoulTitanCore_SetNextAvailableTime( soul, 1 )
+ }
+ }
+ }
+
+ array<entity> titans = GetNPCArrayByClass( "npc_titan" )
+ foreach ( titan in titans )
+ {
+ entity soul = titan.GetTitanSoul()
+ if ( soul )
+ SoulTitanCore_SetNextAvailableTime( soul, 1 )
+ }
+}
+
+#if DEV
+
+void function KillAllBadguys()
+{
+ array<entity> npcs = GetNPCArrayOfEnemies( GetPlayerArray()[0].GetTeam() )
+ foreach ( n in npcs )
+ {
+ if ( n.GetClassName() == "npc_bullseye" )
+ continue
+
+ if ( !IsAlive( n ) )
+ continue
+ n.Die()
+ }
+}
+
+bool function ClientCommand_DoomTitan( entity player, array<string> args )
+{
+ entity titan
+ if ( player.IsTitan() )
+ titan = player
+ else
+ titan = player.GetPetTitan()
+
+ if ( !IsAlive( titan ) )
+ return true
+
+ if ( GetDoomedState( titan ) )
+ return true
+
+ entity soul = titan.GetTitanSoul()
+ soul.SetShieldHealth( 0 )
+
+ titan.TakeDamage( titan.GetHealth(), null, null, { damageSourceId=damagedef_suicide, scriptType = DF_SKIP_DAMAGE_PROT } )
+
+ return true
+}
+#endif
+
+void function PrintPlaylists()
+{
+ printt( "=== PLAYLIST NAMES: ===" )
+
+ int count = GetPlaylistCount()
+ for ( int i = 0; i < count; i++ )
+ {
+ printt( "--", GetPlaylistName( i ) )
+ }
+}
+
+entity function CreateEntity( string name )
+{
+// if ( name == "npc_titan" )
+// {
+// DumpStack(3)
+// }
+// if ( name == "info_particle_system" )
+// {
+// printl( " " )
+// DumpStack(3)
+// }
+ return Entities_CreateByClassname( name )
+}
+
+// used from the console to quick-test fields
+void function setnpcfields( string key, var val )
+{
+ array<entity> npcs = GetNPCArrayByClass( "npc_soldier" )
+ foreach ( npc in npcs )
+ {
+ npc.kv[ key ] = val
+ }
+}
+
+void function WaitUntilNumDead( array<entity> guys, int numDead )
+{
+ Assert( numDead <= guys.len(), "asked for " + numDead + " guys to die, but only passed in an array of " + guys.len() + " guys." )
+ float timeout = -1
+ __WaitUntilDeadInternal( guys, numDead, timeout, __WaitUntilDeadTracker )
+}
+
+void function WaitUntilNumDeadWithTimeout( array<entity> guys, int numDead, float timeout )
+{
+ Assert( numDead <= guys.len(), "asked for " + numDead + " guys to die, but only passed in an array of " + guys.len() + " guys." )
+ Assert( timeout > 0 )
+ __WaitUntilDeadInternal( guys, numDead, timeout, __WaitUntilDeadTracker )
+}
+
+void function WaitUntilAllDead( array<entity> guys )
+{
+ int count = guys.len()
+ float timeout = -1
+ __WaitUntilDeadInternal( guys, count, timeout, __WaitUntilDeadTracker )
+}
+
+void function WaitUntilAllDeadWithTimeout( array<entity> guys, float timeout )
+{
+ int count = guys.len()
+ Assert( timeout > 0 )
+ __WaitUntilDeadInternal( guys, count, timeout, __WaitUntilDeadTracker )
+}
+
+void function WaitUntilNumDeadOrLeeched( array<entity> guys, int numDead )
+{
+ Assert( numDead <= guys.len(), "asked for " + numDead + " guys to die, but only passed in an array of " + guys.len() + " guys." )
+ float timeout = -1
+ __WaitUntilDeadInternal( guys, numDead, timeout, __WaitUntilDeadOrLeechedTracker )
+}
+
+void function WaitUntilNumDeadOrLeechedWithTimeout( array<entity> guys, int numDead, float timeout )
+{
+ Assert( numDead <= guys.len(), "asked for " + numDead + " guys to die, but only passed in an array of " + guys.len() + " guys." )
+ Assert( timeout > 0 )
+ __WaitUntilDeadInternal( guys, numDead, timeout, __WaitUntilDeadOrLeechedTracker )
+}
+
+void function WaitUntilAllDeadOrLeeched( array<entity> guys )
+{
+ int count = guys.len()
+ float timeout = -1
+ __WaitUntilDeadInternal( guys, count, timeout, __WaitUntilDeadOrLeechedTracker )
+}
+
+void function WaitUntilAllDeadOrLeechedWithTimeout( array<entity> guys, float timeout )
+{
+ int count = guys.len()
+ Assert( timeout > 0 )
+ __WaitUntilDeadInternal( guys, count, timeout, __WaitUntilDeadOrLeechedTracker )
+}
+
+void function __WaitUntilDeadInternal( array<entity> guys, int numDead, float timeout, void functionref( entity, table<string, int> ) deathTrackerFunc )
+{
+ table<string, int> master = { count = numDead }
+
+ if ( timeout > 0.0 )
+ thread __WaitUntilDeadTrackerDelayedSignal( master, "WaitDeadTimeOut", timeout )
+
+ //when the thread ends, let child threads know
+ OnThreadEnd(
+ function() : ( master )
+ {
+ Signal( master, "OnDestroy" )
+ }
+ )
+
+ foreach ( guy in guys )
+ thread deathTrackerFunc( guy, master )
+
+ while ( master.count > 0 )
+ {
+ table result = WaitSignal( master, "OnDeath", "WaitDeadTimeOut" )
+ if ( result.signal == "WaitDeadTimeOut" ) //can't do endsignal, because it will kill the calling function too
+ return
+ }
+}
+
+void function __WaitUntilDeadTrackerDelayedSignal( table<string, int> master, string signal, float delay )
+{
+ EndSignal( master, signal )
+
+ wait delay
+ if ( IsValid( master ) )
+ Signal( master, signal )
+}
+
+void function __WaitUntilDeadTracker( entity guy, table<string, int> master )
+{
+ EndSignal( master, "OnDestroy" )
+ if ( IsAlive( guy ) )
+ WaitSignal( guy, "OnDeath", "OnDestroy" )
+
+ master.count--
+ Signal( master, "OnDeath" )
+}
+
+void function __WaitUntilDeadOrLeechedTracker( entity guy, table<string, int> master )
+{
+ EndSignal( master, "OnDestroy" )
+ if ( IsAlive( guy ) )
+ WaitSignal( guy, "OnDeath", "OnDestroy", "OnLeeched" )
+
+ master.count--
+ Signal( master, "OnDeath" )
+}
+
+void function SetRebreatherMaskVisible( entity ent, bool visible )
+{
+ asset modelname = ent.GetModelName()
+
+ int maskIdx = ent.FindBodyGroup( "mask" )
+ if ( maskIdx == -1 )
+ return
+
+ int visibleIdx = 1
+ if ( !visible )
+ visibleIdx = 0
+
+ ent.SetBodygroup( maskIdx, visibleIdx )
+}
+
+int function EntHasSpawnflag( entity ent, int spawnflagHexVal )
+{
+ return ( expect int( ent.kv.spawnflags ) & spawnflagHexVal )
+}
+
+entity function CreateInfoTarget( vector origin = <0,0,0>, vector angles = <0,0,0> )
+{
+ entity info_target = CreateEntity( "info_target" )
+ info_target.SetOrigin( origin )
+ info_target.SetAngles( angles )
+ DispatchSpawn( info_target )
+ return info_target
+}
+
+entity function CreateExpensiveScriptMover( vector origin = <0.0, 0.0, 0.0>, vector angles = <0.0, 0.0, 0.0>, int solidType = 0 )
+{
+ entity script_mover = CreateEntity( "script_mover" )
+ script_mover.kv.solid = solidType
+ script_mover.SetValueForModelKey( $"models/dev/empty_model.mdl" )
+ script_mover.kv.SpawnAsPhysicsMover = 0
+ script_mover.SetOrigin( origin )
+ script_mover.SetAngles( angles )
+
+ DispatchSpawn( script_mover )
+ return script_mover
+}
+
+entity function CreateScriptMover( vector origin = <0.0, 0.0, 0.0>, vector angles = <0.0, 0.0, 0.0>, int solidType = 0 )
+{
+ entity script_mover = CreateEntity( "script_mover_lightweight" )
+ script_mover.kv.solid = solidType
+ script_mover.SetValueForModelKey( $"models/dev/empty_model.mdl" )
+ script_mover.kv.SpawnAsPhysicsMover = 0
+ script_mover.SetOrigin( origin )
+ script_mover.SetAngles( angles )
+ DispatchSpawn( script_mover )
+ return script_mover
+}
+
+entity function CreateScriptMoverModel( asset model, vector origin = <0.0, 0.0, 0.0>, vector angles = <0.0, 0.0, 0.0>, int solidType = 0, float fadeDist = -1 )
+{
+ entity script_mover = CreateEntity( "script_mover_lightweight" )
+ script_mover.kv.solid = solidType
+ script_mover.kv.fadedist = fadeDist
+ script_mover.SetValueForModelKey( model )
+ script_mover.kv.SpawnAsPhysicsMover = 0
+ script_mover.SetOrigin( origin )
+ script_mover.SetAngles( angles )
+ DispatchSpawn( script_mover )
+ return script_mover
+}
+
+entity function CreateExpensiveScriptMoverModel( asset model, vector origin = <0.0, 0.0, 0.0>, vector angles = <0.0, 0.0, 0.0>, int solidType = 0, float fadeDist = -1 )
+{
+ entity script_mover = CreateEntity( "script_mover" )
+ script_mover.kv.solid = solidType
+ script_mover.kv.fadedist = fadeDist
+ script_mover.SetValueForModelKey( model )
+ script_mover.kv.SpawnAsPhysicsMover = 0
+ script_mover.SetOrigin( origin )
+ script_mover.SetAngles( angles )
+ DispatchSpawn( script_mover )
+ return script_mover
+}
+
+entity function CreateOwnedScriptMover( entity owner )
+{
+ entity script_mover = CreateEntity( "script_mover" )
+ script_mover.kv.solid = 0
+ script_mover.SetValueForModelKey( $"models/dev/empty_model.mdl" )
+ script_mover.kv.SpawnAsPhysicsMover = 0
+ script_mover.SetOrigin( owner.GetOrigin() )
+ script_mover.SetAngles( owner.GetAngles() )
+ DispatchSpawn( script_mover )
+ script_mover.Hide()
+
+ script_mover.SetOwner( owner )
+ return script_mover
+}
+
+// useful for finding out what ents are in a level. Should only be used for debugging.
+int function TotalEnts( bool hidden = false )
+{
+ array<entity> entities
+
+ EntTracker++
+
+ entity ent = Entities_FindInSphere( null, < 0, 0, 0 >, 90000 )
+ string name
+ for ( ;; )
+ {
+ if ( ent == null )
+ break
+
+ entities.append( ent )
+
+ name = ent.GetTargetName()
+
+ string strPrefix = "Old ent"
+ if ( ent.e.totalEntsStoredID == 0 )
+ {
+ string strPrefix = "* New ent"
+ ent.e.totalEntsStoredID = EntTracker
+ }
+
+ string strPostfix = ""
+ if ( name != "" )
+ strPostfix = " \"" + name + "\""
+
+ if ( !hidden )
+ printl( strPrefix + " (" + ent.e.totalEntsStoredID + "): " + ent + strPostfix )
+
+ ent = Entities_FindInSphere( ent, < 0, 0, 0 >, 90000 )
+ }
+
+ if ( !hidden )
+ printl( "Total entities " + entities.len() )
+
+ return entities.len()
+}
+
+bool function IsThreadTop()
+{
+ return getstackinfos( 3 ) == null
+}
+
+string function ThisFunc()
+{
+ return expect string( expect table( getstackinfos( 2 ) )[ "func" ] )
+}
+
+entity function ge( int index ) // shorthand version for typing from console
+{
+ return GetEntByIndex( index )
+}
+
+entity function GetEnt( string name )
+{
+ entity ent = Entities_FindByName( null, name )
+ if ( ent == null )
+ {
+ ent = Entities_FindByClassname( null, name )
+ Assert( Entities_FindByClassname( ent, name ) == null, "Tried to GetEnt but there were multiple entities with that name" )
+ }
+ else
+ {
+ Assert( Entities_FindByName( ent, name ) == null, "Tried to GetEnt but there were multiple entities with name " + name )
+ }
+
+ return ent
+}
+
+array<entity> function ArrayWithinCenter( array<entity> ents, vector start, int range )
+{
+ array<entity> Array
+ foreach ( ent in ents )
+ {
+ if ( Distance( start, ent.GetWorldSpaceCenter() ) > range )
+ continue
+
+ Array.append( ent )
+ }
+
+ return Array
+}
+
+vector function GetCenter( array<entity> ents )
+{
+ vector total
+
+ foreach ( ent in ents )
+ {
+ total += ent.GetOrigin()
+ }
+
+ total.x /= float( ents.len() )
+ total.y /= float( ents.len() )
+ total.z /= float( ents.len() )
+
+ return total
+}
+
+void function TableRemoveInvalid( table<entity, entity> Table )
+{
+ array<entity> deleteKey = []
+
+ foreach ( entity key, entity value in Table )
+ {
+ if ( !IsValid_ThisFrame( key ) )
+ deleteKey.append( key )
+
+ if ( !IsValid_ThisFrame( value ) )
+ deleteKey.append( key )
+ }
+
+ foreach ( key in deleteKey )
+ {
+ // in this search, two things could end up on the same key
+ if ( key in Table )
+ delete Table[ key ]
+ }
+}
+
+void function TableRemoveInvalidByValue( table<entity, entity> Table )
+{
+ array<entity> deleteKey = []
+
+ foreach ( key, entity value in Table )
+ {
+ if ( !IsValid_ThisFrame( value ) )
+ deleteKey.append( key )
+ }
+
+ foreach ( key in deleteKey )
+ {
+ delete Table[ key ]
+ }
+}
+
+void function TableRemoveDeadByKey( table<entity, entity> Table )
+{
+ array<entity> deleteKey = []
+
+ foreach ( key, value in Table )
+ {
+ if ( !IsAlive( key ) )
+ deleteKey.append( key )
+ }
+
+ foreach ( key in deleteKey )
+ {
+ delete Table[ key ]
+ }
+}
+
+
+void function ArrayDump( array<var> Array )
+{
+ for ( int i = 0; i < Array.len(); i++ )
+ {
+ printl( "index " + i + " is: " + Array[i] )
+ }
+}
+
+
+int function DotCompareLargest( ArrayDotResultStruct a, ArrayDotResultStruct b )
+{
+ if ( a.dot < b.dot )
+ return 1
+ else if ( a.dot > b.dot )
+ return -1
+
+ return 0
+}
+
+int function DotCompareSmallest( ArrayDotResultStruct a, ArrayDotResultStruct b )
+{
+ if ( a.dot > b.dot )
+ return 1
+ else if ( a.dot < b.dot )
+ return -1
+
+ return 0
+}
+
+array<ArrayDotResultStruct> function ArrayDotResults( array<entity> Array, entity ent )
+{
+ array<ArrayDotResultStruct> allResults
+
+ foreach ( arrayEnt in Array )
+ {
+ ArrayDotResultStruct results
+
+ results.dot = VectorDot_EntToEnt( ent, arrayEnt )
+ results.ent = arrayEnt
+ allResults.append( results )
+ }
+
+ return allResults
+}
+
+
+// Return an array of entities ordered from closest to furthest from the facing of the entity
+array<entity> function ArrayClosestToView( array<entity> Array, entity ent )
+{
+ Assert( type( Array ) == "array" )
+ array<ArrayDotResultStruct> allResults = ArrayDotResults( Array, ent )
+
+ allResults.sort( DotCompareLargest )
+
+ array<entity> returnEntities = []
+
+ foreach ( index, result in allResults )
+ {
+ //printl( "Results are " + result.dot )
+ returnEntities.insert( index, result.ent )
+ }
+
+ // the actual distances aren't returned
+ return returnEntities
+}
+
+
+entity function SpawnRefEnt( entity ent )
+{
+ printl( "Ent model " + ent.GetValueForModelKey() )
+ int attach = ent.LookupAttachment( "ref" )
+ vector origin = ent.GetAttachmentOrigin( attach )
+ vector angles = ent.GetAttachmentAngles( attach )
+
+ entity ref = CreateEntity( "prop_dynamic" )
+ //ref.kv.SpawnAsPhysicsMover = 0
+ ref.SetValueForModelKey( $"models/dev/empty_model.mdl" )
+ DispatchSpawn( ref )
+
+ ref.SetOrigin( origin )
+ ref.SetAngles( angles )
+ ref.Hide()
+ return ref
+}
+
+entity function CreateScriptRef( vector ornull origin = null, vector ornull angles = null )
+{
+ entity ent = CreateEntity( "script_ref" )
+
+ if ( origin )
+ ent.SetOrigin( expect vector( origin ) )
+
+ if ( angles )
+ ent.SetAngles( expect vector( angles ) )
+
+ DispatchSpawn( ent )
+ return ent
+}
+
+entity function CreateScriptRefMinimap( vector origin, vector angles )
+{
+ entity ent = CreateEntity( "script_ref_minimap" )
+
+ ent.SetOrigin( origin )
+ ent.SetAngles( angles )
+
+ DispatchSpawn( ent )
+
+ return ent
+}
+
+bool function exists( table tbl, string val )
+{
+ if ( !(val in tbl) )
+ return false
+
+ return tbl[ val ] != null
+}
+
+var function TableRandomIndex( table Table )
+{
+ array Array = []
+
+ foreach ( index, _ in Table )
+ {
+ Array.append( index )
+ }
+
+ return Array.getrandom()
+}
+
+// should improve this
+float function YawDifference( float yaw1, float yaw2 )
+{
+ Assert( yaw1 >= 0 )
+ Assert( yaw1 <= 360 )
+ Assert( yaw2 >= 0 )
+ Assert( yaw2 <= 360 )
+
+ float diff = fabs( yaw1 - yaw2 )
+
+ if ( diff > 180 )
+ return 360 - diff
+ else
+ return diff
+
+ unreachable
+}
+
+
+
+/*function TrackIsTouching( ent )
+{
+ return // now uses IsTouching
+
+ //ent.s.touching <- {}
+ //ent.ConnectOutput( "OnStartTouch", TrackIsTouching_OnStartTouch )
+ //ent.ConnectOutput( "OnEndTouch", TrackIsTouching_OnEndTouch )
+}
+
+void function TrackIsTouching_OnStartTouch( entity self, entity activator, entity caller, var value )
+{
+ if ( activator )
+ {
+ self.s.touching[ activator ] <- true
+ }
+}
+
+void function TrackIsTouching_OnEndTouch( entity self, entity activator, entity caller, var value )
+{
+ if ( activator )
+ {
+ if ( activator in self.s.touching )
+ {
+ delete self.s.touching[ activator ]
+ }
+ }
+}*/
+
+void function NPC_NoTarget( entity self )
+{
+ self.SetNoTarget( true )
+ self.SetNoTargetSmartAmmo( true )
+}
+
+vector function GetSimpleTraceEnd( vector start, vector end, float frac )
+{
+ vector vec = end - start
+ vec *= frac
+ return start + vec
+}
+
+bool function LoadedMain()
+{
+ if ( "LoadedMain" in level )
+ return true
+
+ level.LoadedMain <- true
+
+ return false
+}
+
+void function Warning( string msg )
+{
+ printl( "*** WARNING ***" )
+ printl( msg )
+ DumpStack()
+ printl( "*** WARNING ***" )
+}
+
+void function TimeOut( float time )
+{
+ table Table = {}
+ EndSignal( Table, "OnDeath" )
+ delaythread( time ) Signal( Table, "OnDeath" )
+}
+
+string function GetActiveWeaponClass( entity player )
+{
+ entity weapon = player.GetActiveWeapon()
+ Assert( weapon != null )
+
+ string weaponclass = weapon.GetWeaponClassName()
+ return weaponclass
+}
+
+bool function HasWeapon( entity ent, string weaponClassName, array<string> mods = [] )
+{
+ Assert( ent.IsPlayer() || ent.IsNPC() )
+
+ array<entity> weaponArray = ent.GetMainWeapons()
+ foreach ( weapon in weaponArray )
+ {
+ if ( weapon.GetWeaponClassName() == weaponClassName )
+ {
+ if ( WeaponHasSameMods( weapon, mods ) )
+ return true
+ }
+ }
+
+ return false
+}
+
+bool function HasOrdnance( entity ent, string weaponClassName, array<string> mods = [] )
+{
+ return HasOffhandForSlot( ent, OFFHAND_ORDNANCE, weaponClassName, mods )
+}
+
+bool function HasCoreAbility( entity ent, string weaponClassName, array<string> mods = [] )
+{
+ return HasOffhandForSlot( ent, OFFHAND_EQUIPMENT, weaponClassName, mods )
+}
+
+bool function HasSpecial( entity ent, string weaponClassName, array<string> mods = [] )
+{
+ return HasOffhandForSlot( ent, OFFHAND_SPECIAL, weaponClassName, mods )
+}
+
+bool function HasAntiRodeo( entity ent, string weaponClassName, array<string> mods = [] )
+{
+ return HasOffhandForSlot( ent, OFFHAND_ANTIRODEO, weaponClassName, mods )
+}
+
+bool function HasMelee( entity ent, string weaponClassName, array<string> mods = [] )
+{
+ return HasOffhandForSlot( ent, OFFHAND_MELEE, weaponClassName, mods )
+}
+
+bool function HasOffhandForSlot( entity ent, int slot, string weaponClassName, array<string> mods = [] )
+{
+ Assert( ent.IsPlayer() || ent.IsNPC() )
+
+ entity weapon = ent.GetOffhandWeapon( slot )
+ if ( !IsValid( weapon ) )
+ return false
+
+ if ( weapon.GetWeaponClassName() != weaponClassName )
+ return false
+
+ return WeaponHasSameMods( weapon, mods )
+}
+
+bool function WeaponHasSameMods( entity weapon, array<string> mods = [] )
+{
+ array hasMods = clone mods
+ foreach ( mod in weapon.GetMods() )
+ {
+ hasMods.removebyvalue( mod )
+ }
+
+ // has all the same mods.
+ return hasMods.len() == 0
+}
+
+bool function HasOffhandWeapon( entity ent, string weaponClassName )
+{
+ Assert( ent.IsPlayer() || ent.IsNPC() )
+
+ array<entity> weaponArray = ent.GetOffhandWeapons()
+ foreach ( weapon in weaponArray )
+ {
+ if ( weapon.GetWeaponClassName() == weaponClassName )
+ return true
+ }
+
+ return false
+}
+
+float function GetFraction( float value, float min, float max )
+{
+ return ( value - min ) / ( max - min )
+}
+
+float function GetFractionClamped( float value, float min, float max )
+{
+ float frac = GetFraction( value, min, max )
+ return clamp( frac, 0.0, 1.0 )
+}
+
+float function GetValueFromFraction( float value, float value_min, float value_max, float return_min, float return_max )
+{
+ float frac = GetFractionClamped( value, value_min, value_max )
+ float retVal = return_min + ( ( return_max - return_min ) * frac )
+ return clamp( retVal, return_min, return_max )
+}
+
+bool function VectorCompare( vector vec1, vector vec2 )
+{
+ if ( vec1.x != vec2.x )
+ return false
+
+ if ( vec1.y != vec2.y )
+ return false
+
+ return vec1.z == vec2.z
+}
+
+// returns vectordot from viewEnt to targetEnt
+float function VectorDot_EntToEnt( entity viewEnt, entity targetEnt )
+{
+ vector maxs = targetEnt.GetBoundingMaxs()
+ vector mins = targetEnt.GetBoundingMins()
+ maxs += mins
+ maxs.x *= 0.5
+ maxs.y *= 0.5
+ maxs.z *= 0.5
+ vector targetOrg = targetEnt.GetOrigin() + maxs
+
+ maxs = viewEnt.GetBoundingMaxs()
+ mins = viewEnt.GetBoundingMins()
+ maxs += mins
+ maxs.x *= 0.5
+ maxs.y *= 0.5
+ maxs.z *= 0.5
+ vector viewOrg = viewEnt.GetOrigin() + maxs
+
+ //DebugDrawLine( targetOrg, viewOrg, 255, 255, 255, true, 0.5 )
+ vector vecToEnt = ( targetOrg - viewOrg )
+ vecToEnt = Normalize( vecToEnt )
+
+ float dotVal = DotProduct( vecToEnt, viewEnt.GetForwardVector() )
+ return dotVal
+}
+
+void function PrecacheEffect( asset effectName )
+{
+ entity warningParticle = CreateEntity( "info_particle_system" )
+ warningParticle.SetValueForEffectNameKey( effectName )
+ warningParticle.kv.start_active = 0
+ DispatchSpawn( warningParticle )
+ warningParticle.Destroy()
+}
+
+void function PrecacheEntity( string entName, asset model = $"" )
+{
+ entity tempEnt = CreateEntity( entName )
+
+ if ( model != $"" )
+ tempEnt.SetValueForModelKey( model )
+
+ tempEnt.kv.spawnflags = SF_NPC_ALLOW_SPAWN_SOLID
+
+ DispatchSpawn( tempEnt )
+ tempEnt.Destroy()
+}
+
+void function PrecacheProjectileEntity( string entName, string weaponClassName, asset model = $"" )
+{
+ entity tempEnt = Entities_CreateProjectileByClassname( entName, weaponClassName )
+
+ if ( model != $"" )
+ tempEnt.SetValueForModelKey( model )
+
+ tempEnt.kv.spawnflags = SF_NPC_ALLOW_SPAWN_SOLID
+
+ DispatchSpawn( tempEnt )
+ tempEnt.Destroy()
+}
+
+void function PrecacheSprite( asset spriteName )
+{
+ entity sprite = CreateEntity( "env_sprite_oriented" )
+ sprite.SetValueForModelKey( spriteName )
+ sprite.kv.spawnflags = 1
+ DispatchSpawn( sprite )
+ sprite.Destroy()
+}
+
+entity function CreatePointMessage( string msg, vector origin, int displayRadius = 512 )
+{
+ entity point_message = CreateEntity( "point_message" )
+ point_message.SetOrigin( origin )
+ point_message.kv.message = msg
+ point_message.kv.radius = displayRadius
+
+ DispatchSpawn( point_message )
+
+ return point_message
+}
+
+entity function CreateGameText( string msg, float xPos, float yPos, int channel, string color = "255 255 255", float fadein = 2, float fadeout = 0.5, float holdtime = 2 )
+{
+ entity game_text = CreateEntity( "game_text" )
+
+ game_text.SetScriptName( "gt" + UniqueString() )
+ game_text.kv.message = msg
+ game_text.kv["x"] = xPos
+ game_text.kv["y"] = yPos
+ game_text.kv.channel = channel
+ game_text.kv.color = color
+ game_text.kv.color2 = "240 110 0" // doesn't appear to do anything atm, not supporting in params
+ game_text.kv.fadein = fadein
+ game_text.kv.fadeout = fadeout
+ game_text.kv.holdtime = holdtime
+ game_text.kv.fxtime = "0.25"
+
+ DispatchSpawn( game_text )
+
+ return game_text
+}
+
+// pass the origin where the player's feet would be
+// tests a player sized box for any collision and returns true only if it's clear
+bool function PlayerCanTeleportHere( entity player, vector testOrg, entity ignoreEnt = null ) //TODO: This is a copy of SP's PlayerPosInSolid(). Not changing it to avoid patching SP. Merge into one function next game
+{
+ int solidMask = TRACE_MASK_PLAYERSOLID
+ vector mins
+ vector maxs
+ int collisionGroup = TRACE_COLLISION_GROUP_PLAYER
+ array<entity> ignoreEnts = [ player ]
+
+ if ( IsValid( ignoreEnt ) )
+ ignoreEnts.append( ignoreEnt )
+ TraceResults result
+
+ mins = player.GetPlayerMins()
+ maxs = player.GetPlayerMaxs()
+ result = TraceHull( testOrg, testOrg + < 0, 0, 1 >, mins, maxs, ignoreEnts, solidMask, collisionGroup )
+
+ if ( result.startSolid )
+ return false
+
+ return true
+}
+
+
+enum eAttach
+{
+ No
+ ViewAndTeleport
+ View
+ Teleport
+ ThirdPersonView
+}
+
+
+void function LoadDiamond()
+{
+ printl( " " )
+ printl( " " )
+ printl( " " )
+
+ // Draw a diamond of a random size and phase (of the moon) so it is easy to separate sections of logs.
+ int random_spread = RandomIntRange( 4, 7 )
+ float random_fullness = RandomFloat( 2.0 )
+ bool functionref( int, int ) compare_func
+ string msg
+
+ if ( RandomFloat( 1.0 ) > 0.5 )
+ {
+ compare_func = bool function( int a, int b )
+ {
+ return a <= b
+ }
+ }
+ else
+ {
+ compare_func = bool function( int a, int b )
+ {
+ return a >= b
+ }
+ }
+
+ for ( int i = 0; i <= random_spread - 2; i++ )
+ {
+ msg = ""
+
+ for ( int p = 0; p <= random_spread - i; p++ )
+ {
+ msg = msg + " "
+ }
+
+ for ( int p = 0; p <= i * 2; p++ )
+ {
+ if ( p == i * 2 || p == 0 )
+ {
+ msg = msg + "*"
+ }
+ else
+ {
+ int an_int = int( i * random_fullness )
+
+ if ( compare_func( p, an_int ) )
+ msg = msg + "*"
+ else
+ msg = msg + " "
+ }
+ }
+
+ printl( msg )
+ }
+
+ for ( int i = random_spread - 1; i >= 0; i-- )
+ {
+ msg = ""
+
+ for ( int p = 0; p <= random_spread - i; p++ )
+ {
+ msg = msg + " "
+ }
+
+
+ for ( int p = 0; p <= i * 2; p++ )
+ {
+ if ( p == i * 2 || p == 0 )
+ {
+ msg = msg + "*"
+ }
+ else
+ {
+ if ( compare_func( p, int( i * random_fullness ) ) )
+ {
+ msg = msg + "*"
+ }
+ else
+ {
+ msg = msg + " "
+ }
+ }
+ }
+
+ printl( msg )
+ }
+
+ printl( " " )
+ printl( " " )
+ printl( " " )
+}
+
+// this will clear all dropped weapons in the map
+void function ClearDroppedWeapons( float delayTime = 0.0 )
+{
+ if ( delayTime > 0 )
+ wait delayTime
+
+ bool onlyNotOwnedWeapons = true // don't get the ones in guys' hands
+ array<entity> weapons = GetWeaponArray( onlyNotOwnedWeapons )
+
+ foreach ( weapon in weapons )
+ {
+ // don't clean up weapon pickups that were placed in leveled
+ int spawnflags = expect string( weapon.kv.spawnflags ).tointeger()
+ if ( spawnflags & SF_WEAPON_START_CONSTRAINED )
+ continue
+
+ weapon.Destroy()
+ }
+}
+
+void function ClearActiveProjectilesForTeam( int team, vector searchOrigin = <0,0,0>, float searchDist = -1 )
+{
+ array<entity> projectiles = GetProjectileArrayEx( "any", team, TEAM_ANY, searchOrigin, searchDist )
+
+ printt( "cleaning up", projectiles.len(), "weapon projectiles for team", team )
+
+ foreach ( proj in projectiles )
+ {
+ if( !IsValid( proj ) )
+ continue
+
+ proj.Destroy()
+ }
+}
+
+void function RestockPlayerAmmo_Silent( entity player = null )
+{
+ RestockPlayerAmmo( player, true )
+}
+
+void function RestockPlayerAmmo( entity player = null, bool isSilent = false )
+{
+ array<entity> players
+ if ( IsAlive( player ) )
+ players.append( player )
+ else
+ players = GetPlayerArray_Alive()
+
+ foreach( player in players )
+ {
+ player.RefillAllAmmo()
+
+ if ( !isSilent )
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Coop_AmmoBox_AmmoRefill" )
+ }
+}
+
+entity function CreateLightSprite( vector origin, vector angles, string lightcolor = "255 0 0", float scale = 0.5 )
+{
+ // attach a light so we can see it
+ entity env_sprite = CreateEntity( "env_sprite" )
+ env_sprite.SetScriptName( UniqueString( "molotov_sprite" ) )
+ env_sprite.kv.rendermode = 5
+ env_sprite.kv.origin = origin
+ env_sprite.kv.angles = angles
+ env_sprite.kv.rendercolor = lightcolor
+ env_sprite.kv.renderamt = 255
+ env_sprite.kv.framerate = "10.0"
+ env_sprite.SetValueForModelKey( $"sprites/glow_05.vmt" )
+ env_sprite.kv.scale = string( scale )
+ env_sprite.kv.spawnflags = 1
+ env_sprite.kv.GlowProxySize = 16.0
+ env_sprite.kv.HDRColorScale = 1.0
+ DispatchSpawn( env_sprite )
+ EntFireByHandle( env_sprite, "ShowSprite", "", 0, null, null )
+
+ return env_sprite
+}
+
+// defaultWinner: if it's a tie, return this value
+int function GetCurrentWinner( int defaultWinner = TEAM_MILITIA )
+{
+ int imcScore
+ int militiaScore
+
+ if ( IsRoundBased() )
+ {
+ imcScore = GameRules_GetTeamScore2( TEAM_IMC )
+ militiaScore = GameRules_GetTeamScore2( TEAM_MILITIA )
+
+ if ( IsRoundBasedUsingTeamScore() && ( imcScore == militiaScore ) )
+ {
+ imcScore = GameRules_GetTeamScore( TEAM_IMC )
+ militiaScore = GameRules_GetTeamScore( TEAM_MILITIA )
+ }
+ }
+ else
+ {
+ imcScore = GameRules_GetTeamScore( TEAM_IMC )
+ militiaScore = GameRules_GetTeamScore( TEAM_MILITIA )
+ }
+
+ int currentWinner = defaultWinner
+
+ if ( militiaScore > imcScore )
+ currentWinner = TEAM_MILITIA
+ else if ( imcScore > militiaScore )
+ currentWinner = TEAM_IMC
+
+ return currentWinner
+}
+
+void function SetNpcFollowsPlayerOverride( entity player, void functionref( entity, entity ) override )
+{
+ player.p.followPlayerOverride = override
+}
+
+void function ClearNpcFollowsPlayerOverride( entity player )
+{
+ player.p.followPlayerOverride = null
+}
+
+void function AddCallback_GameStateEnter( int gameState, void functionref() callbackFunc )
+{
+ Assert( gameState < svGlobal.gameStateEnterCallbacks.len() )
+
+ Assert( !svGlobal.gameStateEnterCallbacks[ gameState ].contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_GameStateEnter" )
+
+ svGlobal.gameStateEnterCallbacks[ gameState ].append( callbackFunc )
+}
+
+void function GM_SetObserverFunc( void functionref( entity ) callbackFunc )
+{
+ svGlobal.observerFunc = callbackFunc
+}
+
+void function GM_AddPlayingThinkFunc( void functionref() callbackFunc )
+{
+ Assert( !svGlobal.playingThinkFuncTable.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with GM_AddPlayingThinkFunc" )
+
+ svGlobal.playingThinkFuncTable.append( callbackFunc )
+}
+
+void function GM_AddThirtySecondsLeftFunc( void functionref() callbackFunc )
+{
+ Assert( !svGlobal.thirtySecondsLeftFuncTable.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with GM_AddThirtySecondsLeftFunc" )
+
+ svGlobal.thirtySecondsLeftFuncTable.append( callbackFunc )
+}
+
+void function GM_SetMatchProgressAnnounceFunc( void functionref( int ) callbackFunc )
+{
+ svGlobal.matchProgressAnnounceFunc = callbackFunc
+}
+
+// Get an absolute offset poistion to an entity even if it's been rotated in world space
+vector function GetEntPosPlusOffset( entity ent, float offsetX, float offsetY, float offsetZ )
+{
+ vector entAngles = ent.GetAngles()
+ vector entOrg = ent.GetOrigin()
+
+ vector right = AnglesToRight( entAngles )
+ right = Normalize( right )
+ vector pos = entOrg + ( right * offsetY )
+
+ vector forward = AnglesToForward( entAngles )
+ forward = Normalize( forward )
+ pos = pos + ( forward * offsetX )
+
+ vector up = AnglesToUp( entAngles )
+ up = Normalize( up )
+ pos = pos + ( up * offsetZ )
+
+ return pos
+}
+
+void function EmitSoundToTeamPlayers( string alias, int team )
+{
+ array<entity> players = GetPlayerArrayOfTeam( team )
+
+ foreach ( player in players )
+ EmitSoundOnEntityOnlyToPlayer( player, player, alias )
+}
+
+void function EmitDifferentSoundsAtPositionForPlayerAndWorld( string soundForPlayer, string soundForWorld, vector position, entity player, int teamNum )
+{
+ if ( IsValid( player ) && player.IsPlayer() )
+ {
+ EmitSoundAtPositionExceptToPlayer( teamNum, position, player, soundForWorld )
+ EmitSoundAtPositionOnlyToPlayer( teamNum, position, player, soundForPlayer)
+ }
+ else
+ {
+ EmitSoundAtPosition( teamNum, position, soundForWorld )
+ }
+}
+
+void function EmitDifferentSoundsOnEntityForPlayerAndWorld( string soundForPlayer, string soundForWorld, entity soundEnt, entity player )
+{
+ if ( IsValid( player ) && player.IsPlayer() )
+ {
+ EmitSoundOnEntityExceptToPlayerNotPredicted( soundEnt, player, soundForWorld )
+ EmitSoundOnEntityOnlyToPlayer(soundEnt, player, soundForPlayer)
+ }
+ else
+ {
+ EmitSoundOnEntity( soundEnt, soundForWorld )
+ }
+}
+
+// Drop an entity to the ground by tracing straight down from its z-axis
+void function DropToGround( entity ent )
+{
+ vector targetOrigin = OriginToGround( ent.GetOrigin() + <0,0,1> )
+ ent.SetOrigin( targetOrigin )
+}
+
+void function DropTitanToGround( entity titan, array<entity> ignoreEnts )
+{
+ vector endOrigin = titan.GetOrigin() - < 0, 0, 20000 >
+ vector mins = GetBoundsMin( HULL_TITAN )
+ vector maxs = GetBoundsMax( HULL_TITAN )
+ TraceResults traceResult = TraceHull( titan.GetOrigin(), endOrigin, mins, maxs, ignoreEnts, TRACE_MASK_TITANSOLID, TRACE_COLLISION_GROUP_NONE )
+
+ titan.SetOrigin( traceResult.endPos )
+}
+
+vector function OriginToGround( vector origin )
+{
+ vector endOrigin = origin - < 0, 0, 20000 >
+ TraceResults traceResult = TraceLine( origin, endOrigin, [], TRACE_MASK_NPCWORLDSTATIC, TRACE_COLLISION_GROUP_NONE )
+
+ return traceResult.endPos
+}
+
+float function GetVerticalClearance( vector origin )
+{
+ vector endOrigin = origin + < 0, 0, 20000 >
+ TraceResults traceResult = TraceLine( origin, endOrigin, [], TRACE_MASK_NPCWORLDSTATIC, TRACE_COLLISION_GROUP_NONE )
+ vector endPos = traceResult.endPos
+ float zDelta = ( endPos.z - origin.z )
+
+ return zDelta
+}
+
+// ---------------------------------------------------------------------
+// Determine if an entity is a valid player spawnpoint
+// ---------------------------------------------------------------------
+bool function PlayerSpawnpointIsValid( entity ent )
+{
+ if ( ent.GetClassName() != "prop_dynamic" )
+ return false
+ if ( ent.GetValueForModelKey() != $"models/humans/pete/mri_male.mdl" )
+ return false
+
+ return true
+}
+
+// ---------------------------------------------------------------------
+// Make an NPC or the player invincible (true/false)
+// (_npc.nut intercepts incoming damage and negates it if the ent is tagged as invincible)
+// ---------------------------------------------------------------------
+void function MakeInvincible( entity ent )
+{
+ Assert( IsValid( ent ), "Tried to make invalid " + ent + " invincible" )
+ Assert( ent.IsNPC() || ent.IsPlayer(), "MakeInvincible() can only be called on NPCs and the player" )
+ Assert( IsAlive( ent ), "Tried to make dead ent " + ent + " invincible" )
+
+ ent.SetInvulnerable()
+}
+
+void function ClearInvincible( entity ent )
+{
+ Assert( IsValid( ent ), "Tried to clear invalid " + ent + " invincible" )
+
+ ent.ClearInvulnerable()
+}
+
+bool function IsInvincible( entity ent )
+{
+ return ent.IsInvulnerable()
+}
+
+//-------------------------------------
+// Teleport an entity (teleporter) to an entity's org and angles (ent)
+//--------------------------------------
+void function TeleportToEnt( entity teleporter, entity ent )
+{
+ Assert( teleporter != null, "Unable to teleport null entity" )
+ Assert( ent != null, "Unable to teleport to a null entity" )
+ teleporter.SetOrigin( ent.GetOrigin() )
+ teleporter.SetAngles( ent.GetAngles() )
+}
+
+//-----------------------------------------------------------
+// CreateShake() - create and fire an env_shake at a specified origin
+// - returns the shake in case you want to parent it
+//------------------------------------------------------------
+
+entity function CreateShake_internal( vector org, float amplitude, float frequency, float duration, float radius, int spawnFlags )
+{
+ entity env_shake = CreateEntity( "env_shake" )
+ env_shake.kv.amplitude = amplitude
+ env_shake.kv.radius = radius
+ env_shake.kv.duration = duration
+ env_shake.kv.frequency = frequency
+ env_shake.kv.spawnflags = spawnFlags
+
+ DispatchSpawn( env_shake )
+
+ env_shake.SetOrigin( org )
+
+ EntFireByHandle( env_shake, "StartShake", "", 0, null, null )
+ EntFireByHandle( env_shake, "Kill", "", ( duration + 1 ), null, null )
+
+ return env_shake
+}
+
+entity function CreateShake( vector org, float amplitude = 16, float frequency = 150, float duration = 1.5, float radius = 2048 )
+{
+ return CreateShake_internal( org, amplitude, frequency, duration, radius, 0 );
+}
+
+entity function CreateShakeRumbleOnly( vector org, float amplitude = 16, float frequency = 150, float duration = 1.5, float radius = 2048 )
+{
+ return CreateShake_internal( org, amplitude, frequency, duration, radius, SF_SHAKE_RUMBLE_ONLY );
+}
+
+entity function CreateShakeNoRumble( vector org, float amplitude = 16, float frequency = 150, float duration = 1.5, float radius = 2048 )
+{
+ return CreateShake_internal( org, amplitude, frequency, duration, radius, SF_SHAKE_NO_RUMBLE );
+}
+
+entity function CreateAirShake( vector org, float amplitude = 16, float frequency = 150, float duration = 1.5, float radius = 2048 )
+{
+ return CreateShake_internal( org, amplitude, frequency, duration, radius, SF_SHAKE_INAIR );
+}
+
+entity function CreateAirShakeRumbleOnly( vector org, float amplitude = 16, float frequency = 150, float duration = 1.5, float radius = 2048 )
+{
+ return CreateShake_internal( org, amplitude, frequency, duration, radius, (SF_SHAKE_INAIR | SF_SHAKE_RUMBLE_ONLY) );
+}
+
+entity function CreateAirShakeNoRumble( vector org, float amplitude = 16, float frequency = 150, float duration = 1.5, float radius = 2048 )
+{
+ return CreateShake_internal( org, amplitude, frequency, duration, radius, (SF_SHAKE_INAIR | SF_SHAKE_NO_RUMBLE) );
+}
+
+//-------------------------------------
+// CreatePhysExplosion - physExplosion...small, medium or large
+//--------------------------------------
+entity function CreatePhysExplosion( vector org, float radius, int magnitude = 1, int flags = 1, bool dealsDamage = true )
+{
+ entity env_physexplosion = CreateEntity( "env_physexplosion" )
+ env_physexplosion.kv.spawnflags = flags // default 1 = No Damage - Only Force
+ env_physexplosion.kv.magnitude = magnitude
+ env_physexplosion.kv.radius = string( radius )
+ env_physexplosion.SetOrigin( org )
+ env_physexplosion.kv.scriptDamageType = damageTypes.explosive
+ DispatchSpawn( env_physexplosion )
+
+ EntFireByHandle( env_physexplosion, "Explode", "", 0, null, null )
+ EntFireByHandle( env_physexplosion, "Kill", "", 2, null, null )
+}
+
+//-----------------------------------------------------------
+// CreatePropDynamic( model ) - create a generic prop_dynamic with default properties
+//------------------------------------------------------------
+entity function CreatePropDynamic( asset model, vector ornull origin = null, vector ornull angles = null, var solidType = 0, float fadeDist = -1 )
+{
+ entity prop_dynamic = CreateEntity( "prop_dynamic" )
+ prop_dynamic.SetValueForModelKey( model )
+ prop_dynamic.kv.fadedist = fadeDist
+ prop_dynamic.kv.renderamt = 255
+ prop_dynamic.kv.rendercolor = "255 255 255"
+ prop_dynamic.kv.solid = solidType // 0 = no collision, 2 = bounding box, 6 = use vPhysics, 8 = hitboxes only
+ if ( origin )
+ {
+ // hack: Setting origin twice. SetOrigin needs to happen before DispatchSpawn, otherwise the prop may not touch triggers
+ prop_dynamic.SetOrigin( expect vector( origin ) )
+ if ( angles )
+ prop_dynamic.SetAngles( expect vector( angles ) )
+ }
+ DispatchSpawn( prop_dynamic )
+ if ( origin )
+ {
+ // hack: Setting origin twice. SetOrigin needs to happen after DispatchSpawn, otherwise origin is snapped to nearest whole unit
+ prop_dynamic.SetOrigin( expect vector( origin ) )
+ if ( angles )
+ prop_dynamic.SetAngles( expect vector( angles ) )
+ }
+
+ return prop_dynamic
+}
+
+
+//-----------------------------------------------------------
+// CreatePropDynamicLightweight( model ) - create a generic prop_dynamic_lightweight with default properties
+//------------------------------------------------------------
+entity function CreatePropDynamicLightweight( asset model, vector ornull origin = null, vector ornull angles = null, var solidType = 0, float fadeDist = -1 )
+{
+ entity prop_dynamic = CreateEntity( "prop_dynamic_lightweight" )
+ prop_dynamic.SetValueForModelKey( model )
+ prop_dynamic.kv.fadedist = fadeDist
+ prop_dynamic.kv.renderamt = 255
+ prop_dynamic.kv.rendercolor = "255 255 255"
+ prop_dynamic.kv.solid = solidType // 0 = no collision, 2 = bounding box, 6 = use vPhysics, 8 = hitboxes only
+ if ( origin )
+ {
+ // hack: Setting origin twice. SetOrigin needs to happen before DispatchSpawn, otherwise the prop may not touch triggers
+ prop_dynamic.SetOrigin( expect vector( origin ) )
+ if ( angles )
+ prop_dynamic.SetAngles( expect vector( angles ) )
+ }
+ DispatchSpawn( prop_dynamic )
+ if ( origin )
+ {
+ // hack: Setting origin twice. SetOrigin needs to happen after DispatchSpawn, otherwise origin is snapped to nearest whole unit
+ prop_dynamic.SetOrigin( expect vector( origin ) )
+ if ( angles )
+ prop_dynamic.SetAngles( expect vector( angles ) )
+ }
+
+ return prop_dynamic
+}
+
+
+//-----------------------------------------------------------
+// CreatePropScript( model ) - create a generic prop_script with default properties
+//------------------------------------------------------------
+entity function CreatePropScript( asset model, vector ornull origin = null, vector ornull angles = null, int solidType = 0, float fadeDist = -1 )
+{
+ entity prop_script = CreateEntity( "prop_script" )
+ prop_script.SetValueForModelKey( model )
+ prop_script.kv.fadedist = fadeDist
+ prop_script.kv.renderamt = 255
+ prop_script.kv.rendercolor = "255 255 255"
+ prop_script.kv.solid = solidType // 0 = no collision, 2 = bounding box, 6 = use vPhysics, 8 = hitboxes only
+ if ( origin )
+ {
+ // hack: Setting origin twice. SetOrigin needs to happen before DispatchSpawn, otherwise the prop may not touch triggers
+ prop_script.SetOrigin( expect vector( origin ) )
+ if ( angles )
+ prop_script.SetAngles( expect vector( angles ) )
+ }
+ DispatchSpawn( prop_script )
+ if ( origin )
+ {
+ // hack: Setting origin twice. SetOrigin needs to happen after DispatchSpawn, otherwise origin is snapped to nearest whole unit
+ prop_script.SetOrigin( expect vector( origin ) )
+ if ( angles )
+ prop_script.SetAngles( expect vector( angles ) )
+ }
+
+ return prop_script
+}
+
+
+
+//-----------------------------------------------------------
+// CreatePropPhysics( model ) - create a generic prop_physics with default properties
+//------------------------------------------------------------
+entity function CreatePropPhysics( asset model, vector origin, vector angles )
+{
+ entity prop_physics = CreateEntity( "prop_physics" )
+ prop_physics.SetValueForModelKey( model )
+ prop_physics.kv.spawnflags = 0
+ prop_physics.kv.fadedist = -1
+ prop_physics.kv.physdamagescale = 0.1
+ prop_physics.kv.inertiaScale = 1.0
+ prop_physics.kv.renderamt = 255
+ prop_physics.kv.rendercolor = "255 255 255"
+ SetTeam( prop_physics, TEAM_BOTH ) // need to have a team other then 0 or it won't take impact damage
+
+ prop_physics.SetOrigin( origin )
+ prop_physics.SetAngles( angles )
+ DispatchSpawn( prop_physics )
+
+ return prop_physics
+}
+
+//-----------------------------------------------------------
+// SpawnBullseye() - creates a npc_bullseye and attaches it to an entity
+//------------------------------------------------------------
+entity function SpawnBullseye( int team, entity ent = null )
+{
+ entity bullseye = CreateEntity( "npc_bullseye" )
+ bullseye.SetScriptName( UniqueString( "bullseye" ) )
+ bullseye.kv.rendercolor = "255 255 255"
+ bullseye.kv.renderamt = 0
+ bullseye.kv.health = 9999
+ bullseye.kv.max_health = -1
+ bullseye.kv.spawnflags = 516
+ bullseye.kv.FieldOfView = 0.5
+ bullseye.kv.FieldOfViewAlert = 0.2
+ bullseye.kv.AccuracyMultiplier = 1.0
+ bullseye.kv.physdamagescale = 1.0
+ bullseye.kv.WeaponProficiency = eWeaponProficiency.VERYGOOD
+ bullseye.kv.minangle = "360"
+ DispatchSpawn( bullseye )
+
+ SetTeam( bullseye, team )
+
+ if ( ent )
+ {
+ vector bounds = ent.GetBoundingMaxs()
+ bullseye.SetOrigin( ent.GetOrigin() + < 0, 0, bounds.z * 0.5 > )
+ bullseye.SetParent( ent )
+ }
+
+ return bullseye
+}
+
+void function CenterPrint( ... )
+{
+ string msg = ""
+ for ( int i = 0; i < vargc; i++ )
+ msg = ( msg + " " + string( vargv[ i ] ) )
+
+ int words = expect int( vargc )
+ if ( words < 1 )
+ words = 1
+
+ float delay = GraphCapped( float( words ), 2.0, 8.0, 2.1, 3.5 )
+
+ entity ent = CreateGameText( msg, -1, 0.5, 10, "255 255 255", 0.25, 0.25, delay )
+ EntFireByHandle( ent, "Display", "", 0, null, null )
+
+ thread DestroyCenterPrint( ent, delay )
+}
+
+void function DestroyCenterPrint( entity ent, float delay )
+{
+ wait( delay )
+ ent.Destroy()
+}
+
+bool function IsValidPlayer( entity player )
+{
+ if ( !IsValid( player ) )
+ return false
+
+ if ( !player.IsPlayer() )
+ return false
+
+ if ( IsDisconnected( player ) )
+ return false
+
+ return true
+}
+
+bool function IsDisconnected( entity player )
+{
+ return player.p.isDisconnected
+}
+
+/****************************************************************************************************\
+/*
+|* PLAY FX
+\*
+\****************************************************************************************************/
+
+entity function PlayFX( asset effectName, vector org, vector ornull optionalAng = null, vector ornull overrideAngle = null )
+{
+ return __CreateFxInternal( effectName, null, "", org, optionalAng, C_PLAYFX_SINGLE, null, -1, null, overrideAngle )
+}
+
+entity function PlayFXWithControlPoint( asset effectName, vector org, entity cpoint1, int visibilityFlagOverride = -1, entity visibilityFlagEntOverride = null, vector ornull overrideAngle = null, int _type = C_PLAYFX_SINGLE )
+{
+ return __CreateFxInternal( effectName, null, "", org, null, _type, cpoint1, visibilityFlagOverride, visibilityFlagEntOverride, overrideAngle)
+}
+
+entity function PlayFXOnEntityWithControlPoint( asset effectName, entity ent, entity cpoint1, int visibilityFlagOverride = -1, entity visibilityFlagEntOverride = null, vector ornull overrideAngle = null, int _type = C_PLAYFX_SINGLE )
+{
+ return __CreateFxInternal( effectName, ent, "", null, null, _type, cpoint1, visibilityFlagOverride, visibilityFlagEntOverride, overrideAngle)
+}
+
+entity function PlayFXOnEntity( asset effectName, entity ent, string optionalTag = "", vector ornull optionalTranslation = null, vector ornull optionalRotation = null, int visibilityFlagOverride = -1, entity visibilityFlagEntOverride = null, vector ornull overrideAngle = null )
+{
+ return __CreateFxInternal( effectName, ent, optionalTag, optionalTranslation, optionalRotation, C_PLAYFX_SINGLE, null, visibilityFlagOverride, visibilityFlagEntOverride, overrideAngle )
+}
+
+entity function PlayFXForPlayer( asset effectName, entity player, vector ornull org, vector ornull optionalAng = null )
+{
+ return __CreateFxInternal( effectName, null, "", org, optionalAng, C_PLAYFX_SINGLE, null, ENTITY_VISIBLE_TO_OWNER, player )
+}
+
+entity function PlayFXOnEntityForEveryoneExceptPlayer( asset effectName, entity ent, entity player, string optionalTag = "", vector ornull optionalTranslation = null, vector ornull optionalRotation = null )
+{
+ return __CreateFxInternal( effectName, ent, optionalTag, optionalTranslation, optionalRotation, C_PLAYFX_SINGLE, null, (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY), player )
+}
+
+entity function PlayFXForEveryoneExceptPlayer( asset effectName, entity player, vector ornull org, vector ornull optionalAng = null )
+{
+ return __CreateFxInternal( effectName, null, "", org, optionalAng, C_PLAYFX_SINGLE, null, (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY), player )
+}
+
+void function PlayFXOnTitanPlayerForTime( asset effectName, entity titan, string attachment, float duration )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "DisembarkingTitan" )
+ titan.EndSignal( "TitanEjectionStarted" )
+
+ entity fx = PlayFXOnEntityForEveryoneExceptPlayer( effectName, titan, titan, attachment )
+
+ OnThreadEnd(
+ function() : ( fx )
+ {
+ if ( IsValid(fx) )
+ {
+ fx.Destroy()
+ }
+ }
+ )
+
+ wait duration
+}
+
+entity function ClientStylePlayFXOnEntity( asset effectName, entity ent, string tag, float duration = 2.0 )
+{
+ string name = ent.GetScriptName()
+ ent.SetScriptName( UniqueString() ) // hack because you can only specify control points by name
+ // hack this is also not quite right because we can't specify the attachment type on the server... should be trivial to add in code:
+ // change DEFINE_FIELD( m_parentAttachmentType, FIELD_INTEGER ), to DEFINE_KEYFIELD( m_parentAttachmentType, FIELD_INTEGER, "attachmentType" ),
+ entity result = __CreateFxInternal( effectName, ent, tag, null, null, C_PLAYFX_SINGLE, ent, (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY), ent )
+ EntFireByHandle( result, "Kill", "", duration, null, null )
+ ent.SetScriptName( name )
+
+ return result
+}
+
+entity function PlayLoopFX( asset effectName, vector ornull org, vector ornull optionalAng = null )
+{
+ return __CreateFxInternal( effectName, null, "", org, optionalAng, C_PLAYFX_LOOP )
+}
+
+entity function PlayLoopFXOnEntity( asset effectName, entity ent, string optionalTag = "", vector ornull optionalTranslation = null, vector ornull optionalRotation = null, int visibilityFlagOverride = -1, entity visibilityFlagEntOverride = null )
+{
+ return __CreateFxInternal( effectName, ent, optionalTag, optionalTranslation, optionalRotation, C_PLAYFX_LOOP, null, visibilityFlagOverride, visibilityFlagEntOverride )
+}
+
+entity function __CreateFxInternal( asset effectName, entity ent, string optionalTag = "", vector ornull optionalTranslation = <0,0,0>, vector ornull optionalRotation = <0,0,0>,
+ int _type = C_PLAYFX_SINGLE, entity cpointEnt1 = null, int visibilityFlagOverride = -1, entity visibilityFlagEntOverride = null, vector ornull overrideAngle = null )
+{
+ entity fx = CreateEntity( "info_particle_system" )
+ fx.SetValueForEffectNameKey( effectName )
+ if( visibilityFlagOverride != -1 )
+ fx.kv.VisibilityFlags = visibilityFlagOverride
+ else
+ fx.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ fx.kv.start_active = 1
+ fx.e.fxType = _type
+
+ if ( _type == C_PLAYFX_SINGLE )
+ fx.DoNotCreateFXOnRestore();
+
+ vector coreOrg
+ vector coreAng
+
+ //are we attaching to an ent?
+ if ( ent )
+ {
+ //are we attaching to a tag on an ent
+ if ( optionalTag != "" )
+ {
+ int attachID = ent.LookupAttachment( optionalTag )
+ coreOrg = ent.GetAttachmentOrigin( attachID )
+ coreAng = ent.GetAttachmentAngles( attachID )
+ }
+ else
+ {
+ coreOrg = ent.GetOrigin()
+ coreAng = ent.GetAngles()
+ }
+
+ fx.Code_SetTeam( ent.GetTeam() );
+ }
+ else //if not we're just playing in space
+ {
+ optionalTag = ""
+ coreOrg = < 0, 0, 0 >
+ coreAng = < 0, 0, 0 >
+ }
+
+ if ( optionalTranslation )
+ {
+ expect vector( optionalTranslation )
+ if ( ent )
+ coreOrg = PositionOffsetFromEnt( ent, optionalTranslation.x, optionalTranslation.y, optionalTranslation.z )
+ else
+ coreOrg = coreOrg + optionalTranslation
+ }
+
+ coreOrg = ClampToWorldspace( coreOrg )
+ fx.SetOrigin( coreOrg )
+
+ if ( overrideAngle )
+ {
+ expect vector( overrideAngle )
+ fx.SetAngles( overrideAngle )
+ }
+ else if ( optionalRotation )
+ {
+ expect vector( optionalRotation )
+ fx.SetAngles( coreAng + optionalRotation )
+ }
+ else
+ {
+ fx.SetAngles( coreAng )
+ }
+
+ if ( ent )
+ {
+ if ( !ent.IsMarkedForDeletion() ) // TODO: This is a hack for shipping. The real solution is to spawn the FX before deleting the parent entity.
+ {
+ fx.SetParent( ent, optionalTag, true )
+ }
+ }
+
+ if ( visibilityFlagEntOverride != null )
+ fx.SetOwner( visibilityFlagEntOverride )
+
+ if ( cpointEnt1 )
+ fx.kv.cpoint1 = cpointEnt1.GetTargetName()
+
+ DispatchSpawn( fx )
+ thread __DeleteFxInternal( fx )
+
+ //SetTargetName( fx, "FX_" + effectName )
+ return fx
+}
+
+void function __DeleteFxInternal( entity fx )
+{
+ //if it loops or is multiple then don't delete internally
+ if ( fx.e.fxType == C_PLAYFX_MULTIPLE )
+ return
+
+ if ( fx.e.fxType == C_PLAYFX_LOOP )
+ return
+
+ wait 30 //no way to know when an effect is over
+ if ( !IsValid( fx ) )
+ return
+
+ fx.ClearParent()
+ fx.Destroy()
+}
+
+void function StartFX( entity fx )
+{
+ Assert( fx.e.fxType == C_PLAYFX_LOOP, "Tried to use StartFX() on effect that is not LOOPING" )
+ EntFireByHandle( fx, "Start", "", 0, null, null )
+}
+
+void function StopFX( entity fx )
+{
+ Assert( fx.e.fxType == C_PLAYFX_LOOP, "Tried to use StopFX() on effect that is not LOOPING" )
+ EntFireByHandle( fx, "Stop", "", 0, null, null )
+}
+
+void function ReplayFX( entity fx )
+{
+ Assert( fx.e.fxType == C_PLAYFX_MULTIPLE, "Tried to use ReplayFX() on effect that is not MULTIPLE" )
+ //thread it because there is a WaitFrame() inside the function
+ thread __ReplayFXInternal( fx )
+}
+
+void function __ReplayFXInternal( entity fx )
+{
+ //for non-looping fx, we must stop first before we can fire again
+ EntFireByHandle( fx, "Stop", "", 0, null, null )
+ //we can't start in the same frame, WaitFrame() skips 1 false
+ //it should be noted that "WaitFrame()" doesn't work with a timescale above 1
+ WaitFrame()
+ //may have died since the last frame
+ if ( IsValid( fx ) )
+ EntFireByHandle( fx, "Start", "", 0, null, null )
+}
+
+void function Entity_StopFXArray( entity ent )
+{
+ foreach( fx in ent.e.fxArray )
+ {
+ if ( IsValid( fx ) )
+ {
+ StopFX( fx )
+ fx.Destroy()
+ }
+ }
+}
+
+/****************************************************************************************************\
+|* end play fx
+\****************************************************************************************************/
+
+
+table function GetDamageTableFromInfo( var damageInfo )
+{
+ table Table = {}
+ Table.damageSourceId <- DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ Table.origin <- DamageInfo_GetDamagePosition( damageInfo )
+ Table.force <- DamageInfo_GetDamageForce( damageInfo )
+ Table.scriptType <- DamageInfo_GetCustomDamageType( damageInfo )
+
+ return Table
+}
+
+bool function EntityInSolid( entity ent, entity ignoreEnt = null, int buffer = 0 ) //TODO: This function returns true for a player standing inside a friendly grunt. It also returns true if you are right up against a ceiling.Needs fixing for next game
+{
+ Assert( IsValid( ent ) )
+ int solidMask
+ vector mins
+ vector maxs
+ int collisionGroup
+ array<entity> ignoreEnts = []
+
+ ignoreEnts.append( ent )
+
+ if ( IsValid( ignoreEnt ) )
+ {
+ ignoreEnts.append( ignoreEnt )
+ }
+
+ if ( ent.IsTitan() )
+ solidMask = TRACE_MASK_TITANSOLID
+ else if ( ent.IsPlayer() )
+ solidMask = TRACE_MASK_PLAYERSOLID
+ else
+ solidMask = TRACE_MASK_NPCSOLID
+
+ if ( ent.IsPlayer() )
+ {
+ mins = ent.GetPlayerMins()
+ maxs = ent.GetPlayerMaxs()
+ collisionGroup = TRACE_COLLISION_GROUP_PLAYER
+ }
+ else
+ {
+ Assert( ent.IsNPC() )
+ mins = ent.GetBoundingMins()
+ maxs = ent.GetBoundingMaxs()
+ collisionGroup = TRACE_COLLISION_GROUP_NONE
+ }
+
+ if ( buffer > 0 )
+ {
+ mins.x -= float( buffer )
+ mins.y -= float( buffer )
+ maxs.x += float( buffer )
+ maxs.y += float( buffer )
+ }
+
+ // if we got into solid, teleport back to safe place
+ vector currentOrigin = ent.GetOrigin()
+ TraceResults result = TraceHull( currentOrigin, currentOrigin + < 0, 0, 1 >, mins, maxs, ignoreEnts, solidMask, collisionGroup )
+ //PrintTable( result )
+ //DrawArrow( result.endPos, Vector(0,0,0), 5, 150 )
+ if ( result.startSolid )
+ return true
+
+ return result.fraction < 1.0 // TODO: Probably not needed according to Jiesang. Fix after ship.
+}
+
+bool function EntityInSpecifiedEnt( entity ent, entity specifiedEnt, int buffer = 0 )
+{
+ Assert( IsValid( ent ) )
+ Assert( IsValid( specifiedEnt ) )
+
+ int solidMask
+ vector mins
+ vector maxs
+ int collisionGroup
+
+ if ( ent.IsTitan() )
+ solidMask = TRACE_MASK_TITANSOLID
+ else if ( ent.IsPlayer() )
+ solidMask = TRACE_MASK_PLAYERSOLID
+ else
+ solidMask = TRACE_MASK_NPCSOLID
+
+ if ( ent.IsPlayer() )
+ {
+ mins = ent.GetPlayerMins()
+ maxs = ent.GetPlayerMaxs()
+ collisionGroup = TRACE_COLLISION_GROUP_PLAYER
+ }
+ else
+ {
+ Assert( ent.IsNPC() )
+ mins = ent.GetBoundingMins()
+ maxs = ent.GetBoundingMaxs()
+ collisionGroup = TRACE_COLLISION_GROUP_NONE
+ }
+
+ if ( buffer > 0 )
+ {
+ mins.x -= float( buffer )
+ mins.y -= float( buffer )
+ maxs.x += float( buffer )
+ maxs.y += float( buffer )
+ }
+
+ // if we got into solid, teleport back to safe place
+ vector currentOrigin = ent.GetOrigin()
+ TraceResults result = TraceHull( currentOrigin, currentOrigin + < 0, 0, 1 >, mins, maxs, null, solidMask, collisionGroup )
+ //PrintTable( result )
+ //DrawArrow( result.endPos, Vector(0,0,0), 5, 150 )
+ if ( result.startSolid == false )
+ return false
+
+ return result.hitEnt == specifiedEnt
+}
+
+void function KillFromInfo( entity ent, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ //float amount = DamageInfo_GetDamage( damageInfo )
+
+ // JFS: if the player became invalid, the attacker becomes the projectile which is bad
+ if ( attacker.IsProjectile() )
+ attacker = svGlobal.worldspawn
+
+ if ( !weapon )
+ weapon = attacker
+
+ table Table = GetDamageTableFromInfo( damageInfo )
+ Table.forceKill <- true
+
+ ent.TakeDamage( 9999, attacker, weapon, Table )
+}
+
+
+entity function GetPlayerTitanInMap( entity player )
+{
+ // temporarily flipped
+ entity petTitan = player.GetPetTitan()
+ if ( IsValid( petTitan ) && IsAlive( petTitan ) )
+ return petTitan
+
+ // first try to return the player's actual titan
+ if ( player.IsTitan() )
+ return player
+
+ return null
+}
+
+
+entity function GetPlayerTitanFromSouls( entity player )
+{
+ // returns the first owned titan found
+ array<entity> souls = GetTitanSoulArray()
+ foreach ( soul in souls )
+ {
+ if ( !IsValid( soul ) )
+ continue
+
+ if ( soul.GetBossPlayer() != player )
+ continue
+
+ if ( !IsAlive( soul.GetTitan() ) )
+ continue
+
+ return soul.GetTitan()
+ }
+
+ return null
+}
+
+void function DisableTitanShield( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+
+ soul.SetShieldHealth( 0 )
+ soul.SetShieldHealthMax( 0 )
+}
+
+void function DisableShield( entity ent )
+{
+ entity soul = ent.GetTitanSoul()
+
+ if ( soul )
+ {
+ DisableTitanShield( ent )
+ return
+ }
+
+ ent.SetShieldHealth( 0 )
+ ent.SetShieldHealthMax( 0 )
+}
+
+void function SetVelocityTowardsEntity( entity entToMove, entity targetEnt, float speed )
+{
+ Assert( speed > 0 )
+ Assert( IsValid( entToMove ) )
+ Assert( IsValid( targetEnt ) )
+
+ vector direction = ( targetEnt.GetWorldSpaceCenter() - entToMove.GetOrigin() )
+ direction = Normalize( direction ) * speed
+ entToMove.SetVelocity( direction )
+}
+
+void function SetVelocityTowardsEntityTag( entity entToMove, entity targetEnt, string targetTag, float speed )
+{
+ Assert( speed > 0 )
+ Assert( IsValid( entToMove ) )
+ Assert( IsValid( targetEnt ) )
+
+ int attachID = targetEnt.LookupAttachment( targetTag )
+ vector attachOrigin = targetEnt.GetAttachmentOrigin( attachID )
+
+ vector direction = ( attachOrigin - entToMove.GetOrigin() )
+ direction = Normalize( direction ) * speed
+ entToMove.SetVelocity( direction )
+}
+
+void function EntityDemigod_TryAdjustDamageInfo( entity ent, var damageInfo )
+{
+ float dmg = DamageInfo_GetDamage( damageInfo )
+ if ( dmg <= 0 )
+ return
+
+ if ( ent.IsTitan() && !GetDoomedState( ent ) ) //Allow demigod titans to go into doomed
+ return
+
+ int bottomLimit = 5
+ if ( ent.GetHealth() <= bottomLimit )
+ ent.SetHealth( bottomLimit + 1 ) //Set it up so that you at least take 1 damage, for hit indicators etc to trigger
+
+ int health = ent.GetHealth()
+
+ if ( health - dmg <= bottomLimit )
+ {
+ int newdmg = health - bottomLimit
+ DamageInfo_SetDamage( damageInfo, newdmg )
+ //printt( "setting damage to ", newdmg )
+ }
+}
+
+void function DebugDamageInfo( entity ent, var damageInfo )
+{
+ printt( "damage to " + ent + ": " + DamageInfo_GetDamage( damageInfo ) )
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ printt( "explosive: " + ( damageType & DF_EXPLOSION ) )
+ printt( "bullet: " + ( damageType & DF_BULLET ) )
+ printt( "gib: " + ( damageType & DF_GIB ) )
+
+ vector dampos = DamageInfo_GetDamagePosition( damageInfo )
+ vector org = DamageInfo_GetInflictor( damageInfo ).GetOrigin()
+ DebugDrawLine( dampos, org, 255, 0, 0, true, 10.0 )
+ DebugDrawLine( org, ent.GetOrigin(), 255, 255, 0, true, 10.0 )
+}
+
+vector function RandomVecInDome( vector dir )
+{
+ vector angles = VectorToAngles( dir )
+ vector forward = AnglesToForward( angles )
+ vector right = AnglesToRight( angles )
+ vector up = AnglesToUp( angles )
+
+ float offsetRight = RandomFloatRange( -1, 1 )
+ float offsetUp = RandomFloatRange( -1, 1 )
+ float offsetForward = RandomFloat( 1.0 )
+
+ vector endPos = < 0, 0, 0 >
+ endPos += forward * offsetForward
+ endPos += up * offsetUp
+ endPos += right * offsetRight
+ endPos.Norm()
+
+ return endPos
+}
+
+float function GetAnimEventTime( asset modelname, string anim, string event )
+{
+ entity dummy = CreatePropDynamic( modelname )
+ dummy.Hide()
+
+ float duration = dummy.GetSequenceDuration( anim )
+ float frac = dummy.GetScriptedAnimEventCycleFrac( anim, event )
+
+ dummy.Destroy()
+
+ //this might cause some issues in R2 - but we'll fix them as we go - it's important this doesn't silently fail
+ Assert( frac > 0.0, "event: " + event + " doesn't exist in animation: " + anim )
+ Assert( frac < 1.0, "event: " + event + " doesn't exist in animation: " + anim )
+
+ return duration * frac
+}
+
+void function SetDeathFuncName( entity npc, string functionNameString )
+{
+ Assert( npc.kv.deathScriptFuncName == "", "deathScriptFuncName was already set" )
+ npc.kv.deathScriptFuncName = functionNameString
+}
+
+void function ClearDeathFuncName( entity npc )
+{
+ npc.kv.deathScriptFuncName = ""
+}
+
+/*function PushSunLightAngles( x, y, z )
+{
+ entity clight = GetEnt( "env_cascade_light" )
+ Assert( clight )
+
+ clight.PushAngles( x, y, z )
+}
+
+function PopSunLightAngles()
+{
+ entity clight = GetEnt( "env_cascade_light" )
+ Assert( clight )
+
+ clight.PopAngles()
+}*/
+
+entity function GetPilotAntiPersonnelWeapon( entity player )
+{
+ array<entity> weaponsArray = player.GetMainWeapons()
+ foreach( weapon in weaponsArray )
+ {
+ int weaponType = weapon.GetWeaponType()
+ if ( weaponType == WT_SIDEARM || weaponType == WT_ANTITITAN )
+ continue;
+
+ return weapon
+ }
+
+ return null
+}
+
+entity function GetPilotSideArmWeapon( entity player )
+{
+ array<entity> weaponsArray = player.GetMainWeapons()
+ foreach( weapon in weaponsArray )
+ {
+ if ( weapon.GetWeaponType() == WT_SIDEARM )
+ return weapon
+ }
+
+ return null
+}
+
+entity function GetPilotAntiTitanWeapon( entity player )
+{
+ array<entity> weaponsArray = player.GetMainWeapons()
+ foreach( weapon in weaponsArray )
+ {
+ if ( weapon.GetWeaponType() == WT_ANTITITAN )
+ return weapon
+ }
+
+ return null
+}
+
+bool function PilotHasSniperWeapon( entity player )
+{
+ array<entity> weaponsArray = player.GetMainWeapons()
+ foreach ( weapon in weaponsArray )
+ {
+ if ( IsValid( weapon ) && weapon.GetWeaponInfoFileKeyField( "is_sniper" ) == 1 )
+ return true
+ }
+
+ return false
+}
+
+bool function PilotActiveWeaponIsSniper( entity player )
+{
+ entity weapon = player.GetActiveWeapon()
+
+ if ( IsValid( weapon ) && weapon.GetWeaponInfoFileKeyField( "is_sniper" ) == 1 )
+ return true
+
+ return false
+}
+
+void function ScreenFadeToColor( entity player, float r, float g, float b, float a, float fadeTime = 1.7, float holdTime = 0.0 )
+{
+ Assert( IsValid( player ) )
+
+ ScreenFade( player, r, g, b, a, fadeTime, holdTime, FFADE_OUT | FFADE_PURGE )
+}
+
+
+void function ScreenFadeFromColor( entity player, float r, float g, float b, float a, float fadeTime = 2.0, float holdTime = 2.0 )
+{
+ Assert( IsValid( player ) )
+
+ ScreenFade( player, r, g, b, a, fadeTime, holdTime, FFADE_IN | FFADE_PURGE )
+}
+
+void function ScreenFadeToBlack( entity player, float fadeTime, float holdTime )
+{
+ Assert( IsValid( player ) )
+
+ ScreenFade( player, 0, 0, 1, 255, fadeTime, holdTime, FFADE_OUT | FFADE_PURGE )
+}
+
+void function ScreenFadeFromBlack( entity player, float fadeTime = 2.0, float holdTime = 2.0 )
+{
+ Assert( IsValid( player ) )
+
+ ScreenFade( player, 0, 1, 0, 255, fadeTime, holdTime, FFADE_IN | FFADE_PURGE )
+}
+
+void function ScreenFadeToBlackForever( entity player, float fadeTime = 1.7 )
+{
+ Assert( IsValid( player ) )
+
+ ScreenFade( player, 0, 0, 1, 255, fadeTime, 0, FFADE_OUT | FFADE_STAYOUT )
+}
+
+/*******************************************************
+/ Server Effects
+/
+/ CreateServerEffect_Friendly( effectName, team )
+/ CreateServerEffect_Enemy( effectName, team )
+/ CreateServerEffect_Owner( effectName, owner )
+/ SetServerEffectControlPoint( effectEnt, controlPoint, vecValue )
+/ StartServerEffect( effectEnt )
+/ StartServerEffectInWorld( effectEnt, origin, angles )
+/ StartServerEffectOnEntity( effectEnt, ent, tag = null )
+/
+*******************************************************/
+
+// NOTE: this does not play the effect, use StartEffectOnEntity
+entity function CreateServerEffect_Friendly( asset effectName, int team )
+{
+ entity friendlyEffect = _CreateServerEffect( effectName, 2 ) // ENTITY_VISIBLE_TO_FRIENDLY
+ friendlyEffect.kv.TeamNum = team
+ friendlyEffect.kv.teamnumber = team
+
+ return friendlyEffect
+}
+
+// NOTE: this does not play the effect, use StartEffectOnEntity
+entity function CreateServerEffect_Enemy( asset effectName, int team )
+{
+ entity enemyEffect = _CreateServerEffect( effectName, 4 ) // ENTITY_VISIBLE_TO_ENEMY
+ enemyEffect.kv.TeamNum = team
+ enemyEffect.kv.teamnumber = team
+
+ return enemyEffect
+}
+
+// NOTE: this does not play the effect, use StartEffectOnEntity
+entity function CreateServerEffect_Owner( asset effectName, entity owner )
+{
+ Assert( IsValid( owner ) )
+
+ entity ownerEffect = _CreateServerEffect( effectName, 1 ) // ENTITY_VISIBLE_TO_OWNER
+ ownerEffect.kv.TeamNum = owner.GetTeam()
+ ownerEffect.SetOwner( owner )
+
+ return ownerEffect
+}
+
+entity function _CreateServerEffect( asset effectName, int visFlags )
+{
+ entity serverEffect = CreateEntity( "info_particle_system" )
+ serverEffect.SetOrigin( <0, 0, 0> )
+ serverEffect.SetAngles( <0, 0, 0> )
+ serverEffect.SetValueForEffectNameKey( effectName )
+ serverEffect.kv.start_active = 1
+ serverEffect.kv.VisibilityFlags = visFlags
+
+ thread _ServerEffectCleanup( serverEffect )
+ return serverEffect
+}
+
+entity function SetServerEffectControlPoint( entity effectEnt, int controlPoint, vector vecValue ) // for now, only support static
+{
+ entity helper = CreateEntity( "info_placement_helper" )
+ helper.SetOrigin( vecValue )
+ effectEnt.SetControlPointEnt( controlPoint, helper )
+ effectEnt.e.fxControlPoints.append( helper )
+
+ return helper
+}
+
+void function StartServerEffect( entity effectEnt )
+{
+ DispatchSpawn( effectEnt )
+}
+
+void function StartServerEffectInWorld( entity effectEnt, vector origin, vector angles )
+{
+ effectEnt.SetOrigin( origin )
+ effectEnt.SetAngles( angles )
+ DispatchSpawn( effectEnt )
+}
+
+void function StartServerEffectOnEntity( entity effectEnt, entity ent, string tag = "" )
+{
+ Assert( IsValid( effectEnt ) )
+ Assert( IsValid( ent ) )
+
+ if ( tag != "" )
+ {
+ int attachID = ent.LookupAttachment( tag )
+ vector origin = ent.GetAttachmentOrigin( attachID )
+ vector angles = ent.GetAttachmentAngles( attachID )
+
+ origin = ClampToWorldspace( origin )
+ effectEnt.SetOrigin( origin )
+ effectEnt.SetAngles( angles )
+ effectEnt.SetParent( ent, tag, true )
+ }
+ else
+ {
+ effectEnt.SetParent( ent )
+ }
+
+ DispatchSpawn( effectEnt )
+}
+
+void function _ServerEffectCleanup( entity effectEnt )
+{
+ effectEnt.WaitSignal( "OnDestroy" )
+
+ foreach ( entity controlPoint in effectEnt.e.fxControlPoints )
+ {
+ controlPoint.Destroy()
+ }
+}
+
+float function GetYaw( vector org1, vector org2 )
+{
+ vector vec = org2 - org1
+ vector angles = VectorToAngles( vec )
+ return angles.y
+}
+
+void function HideName( entity ent )
+{
+ ent.SetNameVisibleToFriendly( false )
+ ent.SetNameVisibleToEnemy( false )
+ ent.SetNameVisibleToNeutral( false )
+ ent.SetNameVisibleToOwner( false )
+}
+
+void function ShowName( entity ent )
+{
+ ent.SetNameVisibleToFriendly( true )
+ ent.SetNameVisibleToEnemy( true )
+ ent.SetNameVisibleToNeutral( true )
+ ent.SetNameVisibleToOwner( true )
+}
+
+void function ShowNameToAllExceptOwner( entity ent )
+{
+ ent.SetNameVisibleToFriendly( true )
+ ent.SetNameVisibleToEnemy( true )
+ ent.SetNameVisibleToNeutral( true )
+ ent.SetNameVisibleToOwner( false )
+}
+
+void function EmitSoundOnEntityToTeamExceptPlayer( entity ent, string sound, int team, entity excludePlayer )
+{
+ array<entity> players = GetPlayerArrayOfTeam( team )
+
+ foreach ( player in players )
+ {
+ if ( player == excludePlayer )
+ continue
+
+ EmitSoundOnEntityOnlyToPlayer( ent, player, sound )
+ }
+}
+
+#if DEV
+// DEV function to toggle player view between the skybox and the real world.
+void function ToggleSkyboxView( float scale = 0.001 )
+{
+ entity player = GetEntByIndex( 1 )
+
+ entity skyboxCamLevel = GetEnt( "skybox_cam_level" )
+
+ Assert( IsValid( skyboxCamLevel ), "Could not find a sky_camera entity named \"skybox_cam_level\" in this map." )
+
+ vector skyOrigin = skyboxCamLevel.GetOrigin()
+
+ if ( !file.isSkyboxView )
+ {
+ if ( !player.IsNoclipping() )
+ {
+ ClientCommand( player, "noclip" )
+ wait( 0.25 )
+ }
+
+ ClientCommand( player, "sv_noclipspeed 0.1" )
+ file.isSkyboxView = true
+ vector offset = player.GetOrigin()
+ offset *= scale
+
+ player.SetOrigin( skyOrigin + offset - < 0.0, 0.0, 60.0 - (60.0 * scale) > )
+ }
+ else
+ {
+ ClientCommand( player, "sv_noclipspeed 5" )
+ file.isSkyboxView = false
+ vector offset = player.GetOrigin() - skyOrigin + < 0.0, 0.0, 60.0 - (60.0 * scale) >
+ offset *= 1.0 / scale
+
+ offset = ClampToWorldspace( offset )
+
+ player.SetOrigin( offset )
+ }
+}
+
+void function DamageRange( float value, float headShotMultiplier, int playerHealth = 200 )
+{
+ printt( "Damage Range: ", value, headShotMultiplier )
+
+ float bodyShot = value
+ float headShot = value * headShotMultiplier
+
+ int maxHeadshots = 0
+
+ int simHealth = playerHealth
+ while ( simHealth > 0 )
+ {
+ simHealth = (simHealth.tofloat() - headShot).tointeger()
+ maxHeadshots++
+ }
+
+ printt( "HeadShots: BodyShots: Total:" )
+
+ simHealth = playerHealth
+ int numHeadshots = 0
+ while ( numHeadshots < maxHeadshots )
+ {
+ simHealth = playerHealth
+ for ( int hsIdx = 0; hsIdx < numHeadshots; hsIdx++ )
+ {
+ simHealth = (simHealth.tofloat() - headShot).tointeger()
+ }
+
+ int numBodyShots = 0
+ while ( simHealth > 0 )
+ {
+ simHealth = (simHealth.tofloat() - bodyShot).tointeger()
+ numBodyShots++
+ }
+ printt( format( "%i %i %i", numHeadshots, numBodyShots, numHeadshots + numBodyShots ) )
+ numHeadshots++
+ }
+
+ printt( format( "%i %i %i", numHeadshots, 0, numHeadshots ) )
+}
+
+#endif // DEV
+
+void function MuteAll( entity player, int fadeOutTime = 2 )
+{
+ //DumpStack(2)
+ Assert( player.IsPlayer() )
+
+ Assert( fadeOutTime >= 1 && fadeOutTime <= 4 , "Only have 4 kinds of fadeout to play, time must be in the range [1,4]" )
+
+ string fadeoutSoundString
+
+ switch( fadeOutTime )
+ {
+ case 1:
+ fadeoutSoundString = "1_second_fadeout"
+ break
+
+ case 2:
+ fadeoutSoundString = "2_second_fadeout"
+ break
+
+ case 3:
+ fadeoutSoundString = "3_second_fadeout"
+ break
+
+ case 4:
+ fadeoutSoundString = "4_second_fadeout"
+ break
+
+ default:
+ unreachable
+
+ }
+
+ printt( "Apply " + fadeoutSoundString + " to player: " + player )
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, fadeoutSoundString )
+}
+
+//Mutes all except halftime sounds and dialogue
+void function MuteHalfTime( entity player )
+{
+ Assert( player.IsPlayer() )
+ printt( "Apply HalfTime_fadeout to player: " + player )
+ EmitSoundOnEntityOnlyToPlayer( player, player, "HalfTime_fadeout" )
+}
+
+void function UnMuteAll( entity player )
+{
+ //DumpStack(2)
+ Assert( player.IsPlayer() )
+
+ //Just stop all the possible fadeout sounds.
+ printt( "Stopping all fadeout for player: " + player )
+ StopSoundOnEntity( player, "1_second_fadeout" )
+ StopSoundOnEntity( player, "2_second_fadeout" )
+ StopSoundOnEntity( player, "3_second_fadeout" )
+ StopSoundOnEntity( player, "4_second_fadeout" )
+ StopSoundOnEntity( player, "HalfTime_fadeout" )
+}
+
+void function AllPlayersMuteAll( int time = MUTEALLFADEIN )
+{
+ array<entity> players = GetPlayerArray()
+
+ foreach ( player in players )
+ MuteAll( player, time )
+}
+
+void function AllPlayersUnMuteAll()
+{
+ array<entity> players = GetPlayerArray()
+
+ foreach ( player in players )
+ UnMuteAll( player )
+}
+
+void function TakeAmmoFromPlayer( entity player )
+{
+ array<entity> mainWeapons = player.GetMainWeapons()
+ array<entity> offhandWeapons = player.GetOffhandWeapons()
+
+ foreach ( weapon in mainWeapons )
+ {
+ weapon.SetWeaponPrimaryAmmoCount( 0 )
+
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( 0 )
+ }
+
+ foreach ( weapon in offhandWeapons )
+ {
+ weapon.SetWeaponPrimaryAmmoCount( 0 )
+
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( 0 )
+ }
+}
+
+
+bool function NearFlagSpawnPoint( vector dropPoint )
+{
+ if ( "flagSpawnPoint" in level && IsValid( level.flagSpawnPoint ) )
+ {
+ vector fspOrigin = expect entity( level.flagSpawnPoint ).GetOrigin()
+ if ( Distance( fspOrigin, dropPoint ) < SAFE_TITANFALL_DISTANCE_CTF )
+ return true
+ }
+
+ if ( "flagReturnPoint" in level && IsValid( level.flagReturnPoint ) )
+ {
+ vector fspOrigin = expect entity( level.flagReturnPoint ).GetOrigin()
+ if ( Distance( fspOrigin, dropPoint ) < SAFE_TITANFALL_DISTANCE_CTF )
+ return true
+ }
+
+ if ( "flagSpawnPoints" in level )
+ {
+ foreach ( flagSpawnPoint in svGlobal.flagSpawnPoints )
+ {
+ vector fspOrigin = flagSpawnPoint.GetOrigin()
+ if ( Distance( fspOrigin, dropPoint ) < SAFE_TITANFALL_DISTANCE_CTF )
+ return true
+ }
+ }
+
+ return false
+}
+
+bool function HasCinematicFlag( entity player, int flag )
+{
+ Assert( player.IsPlayer() )
+ Assert( IsValid( player ) )
+ return ( player.GetCinematicEventFlags() & flag ) != 0
+}
+
+void function AddCinematicFlag( entity player, int flag )
+{
+ Assert( player.IsPlayer() )
+ Assert( IsValid( player ) )
+ player.SetCinematicEventFlags( player.GetCinematicEventFlags() | flag )
+ player.Signal( "CE_FLAGS_CHANGED" )
+}
+
+void function RemoveCinematicFlag( entity player, int flag )
+{
+ Assert( player.IsPlayer() )
+ Assert( IsValid( player ) )
+ player.SetCinematicEventFlags( player.GetCinematicEventFlags() & ( ~flag ) )
+ player.Signal( "CE_FLAGS_CHANGED" )
+}
+
+void function SkyScaleDefault( entity ent, float time = 1.0 )
+{
+ if ( IsValid( ent ) )
+ ent.LerpSkyScale( SKYSCALE_DEFAULT, time )
+}
+
+void function MoveSpawn( string targetName, vector origin, vector angles )
+{
+ entity ent = GetEnt( targetName )
+ ent.SetOrigin( origin )
+ ent.SetAngles( angles )
+}
+
+/*
+function CheckDailyChallengeAchievement( entity player )
+{
+ if ( player.GetPersistentVar( "cu8achievement.ach_allDailyChallengesForDay" ) == true )
+ return
+
+ int maxRefs = PersistenceGetArrayCount( "activeDailyChallenges" )
+ int todaysDailiesComplete = 0
+ int today = Daily_GetDayForCurrentTime()
+ for ( int i = 0; i < maxRefs; i++ )
+ {
+ int day = player.GetPersistentVarAsInt( "activeDailyChallenges[" + i + "].day" )
+ if ( day != today )
+ continue
+
+ local ref = player.GetPersistentVar( "activeDailyChallenges[" + i + "].ref" )
+ if ( !IsChallengeComplete( ref, player ) )
+ continue
+
+ todaysDailiesComplete++
+ }
+
+ if ( todaysDailiesComplete >= 3 )
+ player.SetPersistentVar( "cu8achievement.ach_allDailyChallengesForDay", true )
+}
+*/
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Get an array of all linked entities targeted to one after the other down the chain
+array<entity> function GetEntityTargetChain_Deprecated( entity ent )
+{
+ array<entity> entityChain = []
+ entity currentEnt = ent
+ entity nextEnt
+
+ while ( true )
+ {
+ nextEnt = GetEnt( currentEnt.GetTarget_Deprecated() )
+ if ( IsValid( nextEnt ) )
+ entityChain.append( nextEnt )
+ else
+ return entityChain
+ currentEnt = nextEnt
+ }
+
+ unreachable
+}
+
+void function PlayImpactFXTable( vector origin, entity owner, string impactFX, int flags = 0 )
+{
+ Explosion(
+ origin, //center,
+ owner, //attacker,
+ owner, //inflictor,flags
+ 0, //damage,
+ 0, //damageHeavyArmor,
+ 1, //innerRadius,
+ 1, //outerRadius,
+ flags, //flags,
+ origin, //projectileLaunchOrigin,
+ 0, //explosionForce,
+ damageTypes.explosive, //scriptDamageFlags,
+ -1, //scriptDamageSourceIdentifier,
+ impactFX ) //impactEffectTableName
+}
+
+void function SetSignalDelayed( entity ent, string signal, float delay )
+{
+ thread __SetSignalDelayedThread( ent, signal, delay )
+}
+
+void function __SetSignalDelayedThread( entity ent, string signal, float delay )
+{
+ EndSignal( ent, signal ) // so that if we call this again with the same signal on the same ent we won't get multiple signal events.
+
+ wait delay
+ if ( IsValid( ent ) )
+ Signal( ent, signal )
+}
+
+#if DEV
+table function GetPlayerPos( entity player = null )
+{
+ if ( !player )
+ player = gp()[0]
+
+ vector org = player.GetOrigin()
+ vector vec = player.GetViewVector()
+ vector ang = VectorToAngles( vec )
+
+ return { origin = org, angles = ang }
+}
+
+string function GetScriptPos( entity player = null )
+{
+ table playerPos = GetPlayerPos( player )
+ vector origin = expect vector( playerPos.origin )
+ vector angles = expect vector( playerPos.angles )
+
+ string returnStr = CreateOriginAnglesString( origin, <0, angles.y, 0> )
+ return returnStr
+}
+
+string function CreateOriginAnglesString( vector origin, vector angles )
+{
+ string returnStr = "< " + origin.x + ", " + origin.y + ", " + origin.z + " >, < " + angles.x + ", " + angles.y + ", " + angles.z + " >"
+ return returnStr
+}
+
+void function DistCheck_SetTestPoint()
+{
+ svGlobal.distCheckTestPoint = expect vector( GetPlayerPos().origin )
+ printt( "DistCheck test point set to:", svGlobal.distCheckTestPoint )
+}
+
+void function DistCheck()
+{
+ vector here = expect vector( GetPlayerPos().origin )
+ float dist = Distance( here, svGlobal.distCheckTestPoint )
+ printt( "Distance:", dist, "units from", svGlobal.distCheckTestPoint )
+}
+#endif // DEV
+
+void function ClearChildren( entity parentEnt ) //Probably should have code give us a GetChildren() function that returns a list instead of having to iterate through NextMovePeer
+{
+ entity childEnt = parentEnt.FirstMoveChild()
+ entity nextChildEnt
+
+ while ( childEnt != null )
+ {
+ nextChildEnt = childEnt.NextMovePeer()
+ childEnt.ClearParent()
+
+ childEnt = nextChildEnt
+ }
+}
+
+void function ForceTimeLimitDone()
+{
+ level.devForcedWin = true
+ level.devForcedTimeLimit = true
+ svGlobal.levelEnt.Signal( "devForcedWin" )
+ ServerCommand( "mp_enabletimelimit 1" )
+ ServerCommand( "mp_enablematchending 1" )
+}
+
+
+void function UpdateBadRepPresent()
+{
+ array<entity> players = GetPlayerArray()
+ bool found = false
+/* always set to false for now
+ foreach ( player in players )
+ {
+ if ( player.HasBadReputation() )
+ {
+ found = true
+ break
+ }
+ }
+*/
+ level.nv.badRepPresent = found
+ level.ui.badRepPresent = found
+}
+
+void function Dev_PrintMessage( entity player, string text, string subText = "", float duration = 7.0, string soundAlias = "" )
+{
+ #if DEV
+ // Build the message on the client
+ string sendMessage
+ for ( int textType = 0 ; textType < 2 ; textType++ )
+ {
+ sendMessage = textType == 0 ? text : subText
+
+ for ( int i = 0; i < sendMessage.len(); i++ )
+ {
+ Remote_CallFunction_NonReplay( player, "Dev_BuildClientMessage", textType, sendMessage[i] )
+ }
+ }
+ Remote_CallFunction_NonReplay( player, "Dev_PrintClientMessage", duration )
+ if ( soundAlias != "" )
+ EmitSoundOnEntity( player, soundAlias )
+ #endif
+}
+
+bool function IsAttackDefendBased() //If needed to, we can make this a .nv and then move this function into utility_shared
+{
+ return expect bool( level.attackDefendBased )
+}
+
+bool function IsRoundBasedUsingTeamScore() //If needed to, we can make this a .nv and then move this function into utility_shared
+{
+ return IsRoundBased() && expect bool( level.roundBasedUsingTeamScore )
+}
+
+bool function ShouldResetRoundBasedTeamScore()
+{
+ return IsRoundBased() && svGlobal.roundBasedTeamScore_RoundReset
+}
+
+void function CreateZipline( vector startPos, vector endPos )
+{
+ string startpointName = UniqueString( "rope_startpoint" )
+ string endpointName = UniqueString( "rope_endpoint" )
+
+ entity rope_start = CreateEntity( "move_rope" )
+ SetTargetName( rope_start, startpointName )
+ rope_start.kv.NextKey = endpointName
+ rope_start.kv.MoveSpeed = 64
+ rope_start.kv.Slack = 25
+ rope_start.kv.Subdiv = "2"
+ rope_start.kv.Width = "2"
+ rope_start.kv.Type = "0"
+ rope_start.kv.TextureScale = "1"
+ rope_start.kv.RopeMaterial = "cable/zipline.vmt"
+ rope_start.kv.PositionInterpolator = 2
+ rope_start.kv.Zipline = "1"
+ rope_start.kv.ZiplineAutoDetachDistance = "150"
+ rope_start.kv.ZiplineSagEnable = "0"
+ rope_start.kv.ZiplineSagHeight = "50"
+ rope_start.SetOrigin( startPos )
+
+ entity rope_end = CreateEntity( "keyframe_rope" )
+ SetTargetName( rope_end, endpointName )
+ rope_end.kv.MoveSpeed = 64
+ rope_end.kv.Slack = 25
+ rope_end.kv.Subdiv = "2"
+ rope_end.kv.Width = "2"
+ rope_end.kv.Type = "0"
+ rope_end.kv.TextureScale = "1"
+ rope_end.kv.RopeMaterial = "cable/zipline.vmt"
+ rope_end.kv.PositionInterpolator = 2
+ rope_end.kv.Zipline = "1"
+ rope_end.kv.ZiplineAutoDetachDistance = "150"
+ rope_end.kv.ZiplineSagEnable = "0"
+ rope_end.kv.ZiplineSagHeight = "50"
+ rope_end.SetOrigin( endPos )
+
+ DispatchSpawn( rope_start )
+ DispatchSpawn( rope_end )
+}
+
+string function GetNPCTitanSettingFile( entity titan )
+{
+ Assert( titan.IsTitan(), titan + " is not a titan" )
+ return titan.ai.titanSettings.titanSetFile
+}
+
+void function DecodeBitField( int bitField )
+{
+ for ( int bitIndex = 0; bitIndex < 32; bitIndex++ )
+ {
+ if ( bitField & (1 << bitIndex) )
+ {
+ printt( "Comparison: ", bitField, "& ( 1 <<", bitIndex, ") = ", bitField & (1 << bitIndex) )
+ printt( "BIT SET: ", bitIndex, bitField, 1 << bitIndex )
+ }
+ }
+}
+
+void function DropWeapon( entity npc )
+{
+ entity weapon = npc.GetActiveWeapon()
+ if ( !weapon )
+ return
+
+ string name = weapon.GetWeaponClassName()
+
+ // giving the weapon you have drops a new one in its place
+ npc.GiveWeapon( name )
+ npc.TakeActiveWeapon()
+}
+
+void function TestGiveGunTo( int index, string weaponName )
+{
+ entity ent = GetEntByIndex( index )
+ if ( !ent )
+ {
+ printt( "No entity for index:", index )
+ return;
+ }
+
+ TakePrimaryWeapon( ent )
+ ent.GiveWeapon( weaponName )
+ ent.SetActiveWeaponByName( weaponName )
+}
+
+string function GetEditorClass( entity self )
+{
+ if ( self.HasKey( "editorclass" ) )
+ return expect string( self.kv.editorclass )
+
+ return ""
+}
+
+bool function TitanHasRegenningShield( entity soul )
+{
+ if ( !TitanShieldRegenEnabled() )
+ return false
+
+ if ( !TitanShieldDecayEnabled() )
+ return true
+
+ if ( SoulHasPassive( soul, ePassives.PAS_SHIELD_BOOST ) )
+ return true
+
+ return false
+}
+
+void function DelayShieldDecayTime( entity soul, float delay )
+{
+ soul.e.nextShieldDecayTime = Time() + delay
+}
+
+void function HighlightWeapon( entity weapon )
+{
+#if HAS_WEAPON_PICKUP_HIGHLIGHT
+ if ( weapon.IsLoadoutPickup() )
+ {
+ Highlight_SetOwnedHighlight( weapon, "sp_loadout_pickup" )
+ Highlight_SetNeutralHighlight( weapon, "sp_loadout_pickup" )
+ }
+ else
+ {
+ Highlight_SetOwnedHighlight( weapon, "weapon_drop_active" )
+ Highlight_SetNeutralHighlight( weapon, "weapon_drop_normal" )
+ }
+#endif // #if HAS_WEAPON_PICKUP_HIGHLIGHT
+}
+
+void function WaitTillLookingAt( entity player, entity ent, bool doTrace, float degrees, float minDist = 0, float timeOut = 0, entity trigger = null, string failsafeFlag = "" )
+{
+ EndSignal( ent, "OnDestroy" )
+ EndSignal( player, "OnDeath" )
+
+ //trigger = the trigger ther player must be touching while doing the check
+ //failsafeFlag = bypass everything if this flag gets set
+
+ if ( failsafeFlag != "" )
+ EndSignal( level, failsafeFlag )
+
+ float minDistSqr = minDist * minDist
+ Assert( minDistSqr >= 0 )
+ float timeoutTime = Time() + timeOut
+
+ while( true )
+ {
+
+ if ( timeOut > 0 && Time() > timeoutTime )
+ break
+
+ if ( failsafeFlag != "" && Flag( failsafeFlag ) )
+ break
+
+ // Within range?
+ if ( minDistSqr > 0 && DistanceSqr( player.GetOrigin(), ent.GetOrigin() ) > minDistSqr )
+ {
+ WaitFrame()
+ continue
+ }
+
+ // Touching trigger?
+ if ( ( trigger != null ) && ( !trigger.IsTouching( player ) ) )
+ {
+ WaitFrame()
+ continue
+ }
+
+ if ( PlayerCanSee( player, ent, doTrace, degrees ) )
+ break
+
+ WaitFrame()
+ }
+}
+
+void function SetTargetName( entity ent, string name )
+{
+ ent.SetValueForKey( "targetname", name )
+}
+
+ZipLine function CreateZipLine( vector start, vector end, int autoDetachDistance = 150, float ziplineMoveSpeedScale = 1.0 )
+{
+ string midpointName = UniqueString( "rope_midpoint" )
+ string endpointName = UniqueString( "rope_endpoint" )
+
+ entity rope_start = CreateEntity( "move_rope" )
+ rope_start.kv.NextKey = midpointName
+ rope_start.kv.MoveSpeed = 0
+ rope_start.kv.ZiplineMoveSpeedScale = ziplineMoveSpeedScale
+ rope_start.kv.Slack = 0
+ rope_start.kv.Subdiv = 0
+ rope_start.kv.Width = "2"
+ rope_start.kv.TextureScale = "1"
+ rope_start.kv.RopeMaterial = "cable/zipline.vmt"
+ rope_start.kv.PositionInterpolator = 2
+ rope_start.kv.Zipline = "1"
+ rope_start.kv.ZiplineAutoDetachDistance = string( autoDetachDistance )
+ rope_start.kv.ZiplineSagEnable = "0"
+ rope_start.kv.ZiplineSagHeight = "0"
+ rope_start.SetOrigin( start )
+
+ entity rope_mid = CreateEntity( "keyframe_rope" )
+ SetTargetName( rope_mid, midpointName )
+ rope_start.kv.NextKey = endpointName
+ rope_mid.SetOrigin( ( start + end ) * 0.5 )
+ //rope_mid.SetOrigin( start )
+
+ entity rope_end = CreateEntity( "keyframe_rope" )
+ SetTargetName( rope_end, endpointName )
+ rope_end.SetOrigin( end )
+
+ // Dispatch spawn entities
+ DispatchSpawn( rope_start )
+ DispatchSpawn( rope_mid )
+ DispatchSpawn( rope_end )
+
+ ZipLine zipLine
+ zipLine.start = rope_start
+ zipLine.mid = rope_mid
+ zipLine.end = rope_end
+
+ return zipLine
+}
+
+entity function GetPlayerFromEntity( entity ent )
+{
+ entity player = null
+
+ if ( ent.IsPlayer() )
+ {
+ player = ent
+ }
+ else if ( ent.IsNPC() )
+ {
+ player = ent.GetBossPlayer()
+ }
+ else
+ {
+ player = ent.GetOwner()
+ if ( !player || !player.IsPlayer() )
+ return null
+ }
+
+ if ( IsValid_ThisFrame( player ) )
+ return player
+
+ return null
+}
+
+void function SetHumanRagdollImpactTable( entity ent )
+{
+ ent.SetRagdollImpactFX( HUMAN_RAGDOLL_IMPACT_TABLE_IDX )
+}
+
+bool function ScriptManagedEntArrayContains( int handle, entity ent )
+{
+ array< entity > ents = GetScriptManagedEntArray( handle )
+ foreach ( ent in ents )
+ {
+ if ( ent == ent )
+ return true
+ }
+
+ return false
+}
+
+void function HideCrit( entity ent )
+{
+ int bodyGroupIndex = ent.FindBodyGroup( "hitpoints" )
+
+ if ( bodyGroupIndex == -1 )
+ {
+ return
+ }
+
+ ent.SetBodygroup( bodyGroupIndex, 1 )
+}
+
+void function ShowCrit( entity ent )
+{
+ int bodyGroupIndex = ent.FindBodyGroup( "hitpoints" )
+
+ if ( bodyGroupIndex == -1 )
+ {
+ return
+ }
+
+ ent.SetBodygroup( bodyGroupIndex, 0 )
+}
+
+
+#if DEV
+void function TeleportEnemyBotToView()
+{
+ entity player = gp()[0]
+
+ TraceResults traceResults = PlayerViewTrace( player )
+
+ if ( traceResults.fraction >= 1.0 )
+ return
+
+ array<entity> players = GetPlayerArrayOfEnemies_Alive( player.GetTeam() )
+ foreach ( enemy in players )
+ {
+ if ( !enemy.IsBot() )
+ continue
+
+ enemy.SetOrigin( traceResults.endPos )
+ return
+ }
+}
+
+void function TeleportEntityToView( entity ent )
+{
+ entity player = gp()[0]
+
+ TraceResults traceResults = PlayerViewTrace( player )
+
+ if ( traceResults.fraction >= 1.0 )
+ return
+
+ ent.SetOrigin( traceResults.endPos )
+ //traceResults.surfaceNormal
+}
+
+void function TeleportFriendlyBotToView()
+{
+ entity player = gp()[0]
+
+ TraceResults traceResults = PlayerViewTrace( player )
+
+ if ( traceResults.fraction >= 1.0 )
+ return
+
+ array<entity> players = GetPlayerArrayOfTeam_Alive( player.GetTeam() )
+ foreach ( enemy in players )
+ {
+ if ( !enemy.IsBot() )
+ continue
+
+ enemy.SetOrigin( traceResults.endPos )
+ return
+ }
+}
+
+void function TeleportBotToAbove()
+{
+ entity player = gp()[0]
+
+ array<entity> players = GetPlayerArray_AlivePilots()
+ foreach ( enemy in players )
+ {
+ if ( !enemy.IsBot() )
+ continue
+
+ enemy.SetOrigin( player.GetOrigin() + < 0, 0, 512 > )
+ return
+ }
+}
+
+TraceResults function PlayerViewTrace( entity player, float distance = 10000 )
+{
+ vector eyePosition = player.EyePosition()
+ vector viewVector = player.GetViewVector()
+
+ TraceResults traceResults = TraceLine( eyePosition, eyePosition + viewVector * distance, player, TRACE_MASK_SHOT_BRUSHONLY, TRACE_COLLISION_GROUP_NONE )
+
+ return traceResults
+}
+#endif
+
+void function ClearPlayerAnimViewEntity( entity player, float time = 0.3 )
+{
+ entity viewEnt = player.GetFirstPersonProxy()
+ viewEnt.HideFirstPersonProxy()
+ viewEnt.Anim_Stop()
+
+ player.AnimViewEntity_SetLerpOutTime( time )
+ player.AnimViewEntity_Clear()
+ player.p.currViewConeFunction = null
+}
+
+
+void function BrushMoves( entity brush )
+{
+ float moveTime = float( brush.kv.move_time )
+ int movedir = int( brush.kv.movedirection )
+
+ BrushMovesInDirection( brush, movedir, moveTime )
+}
+
+
+void function BrushMovesInDirection( entity ent, int dir, float moveTime = 0, float blendIn = 0, float blendOut = 0, float lip = 8 )
+{
+ entity mover = CreateOwnedScriptMover( ent )
+ OnThreadEnd(
+ function() : ( ent, mover )
+ {
+ if ( IsValid( ent ) )
+ ent.ClearParent()
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ }
+ )
+
+ dir %= 360
+ if ( dir > 180 )
+ dir -= 360
+ else if ( dir < -180 )
+ dir += 360
+
+ string moveAxis = GetMoveAxisFromDir( dir )
+
+ ent.SetParent( mover )
+ vector origin = ent.GetOrigin()
+ float moveAmount
+ if ( ent.HasKey( "move_amount" ) )
+ {
+ moveAmount = float( ent.kv.move_amount ) - lip
+ switch ( moveAxis )
+ {
+ case "x":
+ case "y":
+ if ( dir < 0 )
+ moveAmount *= -1
+ break
+
+ case "z":
+ if ( dir == -1 )
+ moveAmount *= -1
+ break
+ }
+ }
+ else
+ {
+ switch ( moveAxis )
+ {
+ case "x":
+ moveAmount = GetEntWidth( ent ) - lip
+ if ( dir < 0 )
+ moveAmount *= -1
+ break
+
+ case "y":
+ moveAmount = GetEntDepth( ent ) - lip
+ if ( dir < 0 )
+ moveAmount *= -1
+ break
+
+ case "z":
+ moveAmount = GetEntHeight( ent ) - lip
+ if ( dir == -1 )
+ moveAmount *= -1
+ break
+ }
+ }
+
+ switch ( moveAxis )
+ {
+ case "x":
+ origin.x += moveAmount
+ break
+ case "y":
+ origin.y += moveAmount
+ break
+ case "z":
+ origin.z += moveAmount
+ break
+ }
+
+ if ( moveTime > 0 )
+ {
+ mover.NonPhysicsMoveTo( origin, moveTime, blendIn, blendOut )
+ wait moveTime
+ }
+ else
+ {
+ mover.SetOrigin( origin )
+ }
+}
+
+string function GetMoveAxisFromDir( int dir )
+{
+ if ( dir == 1 || dir == -1 )
+ return "z"
+
+ if ( dir % 180 == 0 )
+ return "x"
+
+ return "y"
+}
+
+
+float function GetEntHeight( entity ent )
+{
+ return ent.GetBoundingMaxs().z - ent.GetBoundingMins().z
+}
+
+float function GetEntWidth( entity ent )
+{
+ return ent.GetBoundingMaxs().x - ent.GetBoundingMins().x
+}
+
+float function GetEntDepth( entity ent )
+{
+ return ent.GetBoundingMaxs().y - ent.GetBoundingMins().y
+}
+
+void function PushEntWithVelocity( entity ent, vector velocity )
+{
+ if ( !ent.IsPlayer() && !ent.IsNPC() )
+ return
+
+ if ( !IsAlive( ent ) )
+ return
+
+ float scale = 1.0
+ float pushbackScale = 1.0
+ if ( ent.IsTitan() )
+ {
+ entity soul = ent.GetTitanSoul()
+ if ( soul != null ) // defensive fix
+ {
+ string settings = GetSoulPlayerSettings( soul )
+ var scale = Dev_GetPlayerSettingByKeyField_Global( settings, "pushbackScale" )
+ if ( scale != null )
+ {
+ pushbackScale = expect float( scale )
+ }
+ }
+ }
+
+ scale = 1.0 - StatusEffect_Get( ent, eStatusEffect.pushback_dampen )
+ scale = scale * pushbackScale
+
+ velocity *= scale
+
+ ent.SetVelocity( velocity )
+}
+
+
+
+bool function IsPlayerMalePilot( entity player )
+{
+ Assert( player.IsPlayer() )
+
+ if ( !IsPilot( player ) )
+ return false
+
+ return !IsPlayerFemale( player )
+}
+
+bool function IsPlayerFemalePilot( entity player )
+{
+ Assert( player.IsPlayer() )
+
+ if ( !IsPilot( player ) )
+ return false
+
+ return IsPlayerFemale( player )
+}
+
+bool function IsFacingEnemy( entity guy, entity enemy, int viewAngle = 75 )
+{
+ vector dir = enemy.GetOrigin() - guy.GetOrigin()
+ dir = Normalize( dir )
+ float dot = DotProduct( guy.GetPlayerOrNPCViewVector(), dir )
+ float yaw = DotToAngle( dot )
+
+ return ( yaw < viewAngle )
+}
+
+void function SetSquad( entity guy, string squadName )
+{
+ Assert( IsValid( guy ) )
+
+ if ( guy.kv.squadname == squadName )
+ return
+
+ // we only want squads containing NPCs of the same class
+ #if HAS_AI_SQUAD_LIMITS
+ Assert( SquadValidForClass( squadName, guy.GetClassName() ), "Can't put AI " + guy + " in squad " + squadName + ", because it contains one or more AI with a different class." )
+ Assert( SquadCanAcceptNewMembers( guy, squadName ), "Can't add AI " + guy + " to squad " + squadName + ", because that squad already has " + SQUAD_SIZE + " slots filled or reserved." )
+ #endif
+
+ guy.SetSquad( squadName )
+}
+
+void function PushPlayersApart( entity target, entity attacker, float speed )
+{
+ vector dif = Normalize( target.GetOrigin() - attacker.GetOrigin() )
+ dif *= speed
+ PushPlayerAway( target, dif )
+ PushPlayerAway( attacker, -dif )
+}
+
+void function PushPlayerAway( entity target, vector velocity )
+{
+ #if MP
+ if ( !target.IsPlayer() && !target.IsNPC() )
+ return
+ #endif
+
+ vector result = velocity // + target.GetVelocity()
+ result.z = max( 200, fabs( velocity.z ) )
+ target.SetVelocity( result )
+ //DebugDrawLine( target.GetOrigin(), target.GetOrigin() + result * 5, 255, 0, 0, true, 5.0 )
+}
+
+
+int function SortBySpawnTime( entity ent1, entity ent2 )
+{
+ if ( ent1.e.spawnTime > ent2.e.spawnTime )
+ return 1
+
+ if ( ent2.e.spawnTime > ent1.e.spawnTime )
+ return -1
+
+ return 0
+}
+
+void function HolsterAndDisableWeapons( entity player )
+{
+ player.HolsterWeapon()
+ DisableOffhandWeapons( player )
+}
+
+void function HolsterViewModelAndDisableWeapons( entity player ) //Note that this skips the first person holster animation, and it appears to 3p observers you still have a gun out
+{
+ player.DisableWeaponViewModel()
+ DisableOffhandWeapons( player )
+}
+
+
+void function DeployAndEnableWeapons( entity player )
+{
+ player.DeployWeapon()
+ EnableOffhandWeapons( player )
+}
+
+void function DeployViewModelAndEnableWeapons( entity player )
+{
+ if ( IsAlive( player ) )
+ player.EnableWeaponViewModel()
+ EnableOffhandWeapons( player )
+}
+
+//Investigate: This might be getting called without enableoffhandweapons being called. If so, Server_TurnOffhandWeaponsDisabledOn() should be used instead of this stack system.
+void function DisableOffhandWeapons( entity player )
+{
+ player.Server_TurnOffhandWeaponsDisabledOn()
+ player.p.disableOffhandWeaponsStackCount++
+}
+
+void function EnableOffhandWeapons( entity player )
+{
+ player.p.disableOffhandWeaponsStackCount--
+ if ( player.p.disableOffhandWeaponsStackCount <= 0 )
+ player.Server_TurnOffhandWeaponsDisabledOff()
+
+ Assert( player.p.disableOffhandWeaponsStackCount >= 0, "Warning! Called EnableOffhandWeapons() but the weapons aren't disabled!" )
+}
+
+void function PushEntWithDamageInfoAndDistanceScale( entity ent, var damageInfo, float nearRange, float farRange, float nearScale, float farScale, float forceMultiplier_dotBase = 0.5 )
+{
+ float scale = GraphCapped( DamageInfo_GetDistFromAttackOrigin( damageInfo ), nearRange, farRange, nearScale, farScale )
+
+ if ( scale > 0 )
+ PushEntWithDamageInfo( ent, damageInfo, forceMultiplier_dotBase, scale )
+}
+
+void function PushEntWithDamageInfo( entity ent, var damageInfo, float forceMultiplier_dotBase = 0.5, float forceMultiplier_dotScale = 0.5 )
+{
+ int source = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ switch ( source )
+ {
+ case eDamageSourceId.mp_titanweapon_vortex_shield:
+ case eDamageSourceId.mp_titanweapon_vortex_shield_ion:
+ return
+ }
+
+ entity projectile = DamageInfo_GetInflictor( damageInfo )
+ if ( !IsValid( projectile ) )
+ return
+
+ vector attackDirection = Normalize( projectile.GetVelocity() )
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ PushEntWithDamageFromDirection( ent, damage, attackDirection, forceMultiplier_dotBase, forceMultiplier_dotScale )
+}
+
+void function PushEntWithDamageFromDirection( entity ent, float damage, vector attackDirection, float forceMultiplier_dotBase = 0.5, float forceMultiplier_dotScale = 0.5 )
+{
+
+ float speed
+ if ( damage < 900 )
+ speed = GraphCapped( damage, 0, 900, 0, 650 )
+ else
+ speed = GraphCapped( damage, 900, 1400, 650, 1400 )
+
+ vector direction = attackDirection + <0,0,0>
+ direction.z *= 0.25
+ vector force = direction * speed
+
+ force += < 0, 0, fabs( direction.z ) * 0.25 >
+
+ vector velocity = ent.GetVelocity()
+ vector baseVel = Normalize( velocity + <0,0,0> )
+
+ float dot = DotProduct( baseVel, attackDirection ) * -1
+ float dotMultiplier
+ if ( dot > 0 )
+ {
+ dot *= forceMultiplier_dotScale
+ }
+ else
+ {
+ dot = 0
+ }
+
+ force *= ( forceMultiplier_dotBase + dot )
+ //printt( "force " + Length( force ) )
+ velocity += force
+ PushEntWithVelocity( ent, velocity )
+}
+
+
+void function SetPlayerAnimViewEntity( entity player, entity model )
+{
+ // clear any attempts to hide the view anim entity
+ player.Signal( "NewViewAnimEntity" )
+ player.AnimViewEntity_SetEntity( model )
+}
+
+
+void function RandomizeHead( entity model ) //Randomize head across all available heads
+{
+ int headIndex = model.FindBodyGroup( "head" )
+ if ( headIndex == -1 )
+ {
+ //printt( "HeadIndex == -1, returning" )
+ return
+ }
+ int numOfHeads = model.GetBodyGroupModelCount( headIndex ) - 1 // last one is no head
+ //printt( "Num of Heads: " + numOfHeads )
+
+ if ( HasTeamSkin( model ) )
+ {
+ RandomizeHeadByTeam( model, headIndex, numOfHeads )
+ return
+ }
+ else
+ {
+ int randomHeadIndex = RandomInt( numOfHeads )
+ //printt( "Set head to: : " + randomHeadIndex )
+ model.SetBodygroup( headIndex, randomHeadIndex )
+ }
+}
+
+bool function HasTeamSkin( entity model )
+{
+ return "teamSkin" in model.CreateTableFromModelKeyValues()
+}
+
+void function RandomizeHeadByTeam( entity model, int headIndex, int numOfHeads ) //Randomize head across heads available to a particular team. Assumes for a model all imc heads are first, then all militia heads are later.
+{
+ float midPoint = float( numOfHeads / 2 )
+
+ int randomHeadIndex = 0
+ if ( model.GetTeam() == TEAM_IMC )
+ {
+ randomHeadIndex = RandomInt( midPoint )
+ }
+ else if ( model.GetTeam() == TEAM_MILITIA )
+ {
+ randomHeadIndex = RandomIntRange( midPoint, numOfHeads )
+ }
+ //printt( "Model ", model.GetModelName(), " is using ", numOfHeads, " randomHeadIndex")
+
+ //printt( "Set head to: : " + randomHeadIndex )
+ model.SetBodygroup( headIndex, randomHeadIndex )
+}
+
+void function TakeWeaponsForArray( entity ent, array<entity> weapons )
+{
+ foreach ( weapon in weapons )
+ {
+ ent.TakeWeaponNow( weapon.GetWeaponClassName() )
+ }
+}
+
+void function ScaleHealth( entity ent, float scale )
+{
+ Assert( IsAlive( ent ) )
+
+ int maxHealth = ent.GetMaxHealth()
+ float healthRatio = float( ent.GetHealth() ) / maxHealth
+ maxHealth = int( maxHealth * scale )
+ ent.SetHealth( maxHealth * healthRatio )
+ ent.SetMaxHealth( maxHealth )
+}
+
+void function TeleportPlayerToEnt( entity player, entity org )
+{
+ if ( !IsValid( player ) )
+ return
+ Assert( player.IsPlayer() )
+ player.SetOrigin( org.GetOrigin() )
+ player.SetAngles( org.GetAngles() )
+}
+
+float function ShieldModifyDamage( entity ent, var damageInfo )
+{
+ entity victim
+ if ( ent.IsTitan() )
+ victim = ent.GetTitanSoul()
+ else
+ victim = ent
+
+ int shieldHealth = victim.GetShieldHealth()
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ int damageSourceIdentifier = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ ShieldDamageModifier damageModifier = GetShieldDamageModifier( damageInfo )
+ damage *= damageModifier.damageScale
+
+ float healthFrac = GetHealthFrac( victim )
+
+ float permanentDamage = (damage * damageModifier.permanentDamageFrac * healthFrac)
+
+ float shieldDamage
+
+ if ( damageSourceIdentifier == eDamageSourceId.titanEmpField )
+ {
+ shieldDamage = min( 1000.0, float( shieldHealth ) )
+ }
+ else
+ {
+ if ( damageModifier.normalizeShieldDamage )
+ shieldDamage = damage * 0.5
+ else
+ shieldDamage = damage - permanentDamage
+
+ // if ( IsSoul( victim ) && SoulHasPassive( victim, ePassives.PAS_SHIELD_BOOST ) )
+ // shieldDamage *= SHIELD_BOOST_DAMAGE_DAMPEN
+
+ if ( IsSoul( victim ) && SoulHasPassive( victim, ePassives.PAS_BERSERKER ) )
+ shieldDamage *= BERSERKER_INCOMING_DAMAGE_DAMPEN
+ }
+
+ float newShieldHealth = shieldHealth - shieldDamage
+
+ victim.SetShieldHealth( max( 0, newShieldHealth ) )
+
+ if ( shieldHealth > 0 && newShieldHealth <= 0 )
+ {
+ if ( ent.IsPlayer() )
+ {
+ EmitSoundOnEntityExceptToPlayer( ent, ent, "titan_energyshield_down_3P" )
+ EmitSoundOnEntityOnlyToPlayer( ent, ent, "titan_energyshield_down_1P" )
+ }
+ else if ( ent.GetScriptName() == "fw_team_tower" )
+ {
+ EmitSoundOnEntity( ent, "TitanWar_Harvester_ShieldDown" )
+
+ #if FACTION_DIALOGUE_ENABLED
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownFriendly", ent.GetTeam() )
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownEnemy", GetOtherTeam( ent.GetTeam() ) )
+ #endif
+ }
+ else
+ {
+ EmitSoundOnEntity( ent, "titan_energyshield_down_3P" )
+ }
+ }
+
+ DamageInfo_AddCustomDamageType( damageInfo, DF_SHIELD_DAMAGE )
+
+ if ( newShieldHealth < 0 )
+ {
+ DamageInfo_SetDamage( damageInfo, fabs( newShieldHealth ) + permanentDamage )
+ }
+ else
+ {
+ if ( permanentDamage == 0 )
+ {
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ vector damageOrigin = GetDamageOrigin( damageInfo, ent )
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ int attackerEHandle = attacker.GetEncodedEHandle()
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ if ( attacker.IsPlayer() )
+ attacker.NotifyDidDamage( ent, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamagePosition( damageInfo ), damageType, shieldDamage, DamageInfo_GetDamageFlags( damageInfo ), DamageInfo_GetHitGroup( damageInfo ), DamageInfo_GetWeapon( damageInfo ), DamageInfo_GetDistFromAttackOrigin( damageInfo ) )
+ if ( ent.IsPlayer() )
+ Remote_CallFunction_Replay( ent, "ServerCallback_TitanTookDamage", shieldDamage, damageOrigin.x, damageOrigin.y, damageOrigin.z, damageType, damageSourceId, attackerEHandle, null, false, 0 )
+ }
+ DamageInfo_SetDamage( damageInfo, permanentDamage )
+ }
+
+ float actualShieldDamage = min( shieldHealth, shieldDamage )
+
+ if ( actualShieldDamage > 0 )
+ {
+ foreach ( func in ent.e.entPostShieldDamageCallbacks )
+ {
+ func( ent, damageInfo, actualShieldDamage )
+ }
+ }
+
+ return actualShieldDamage
+}
+
+ShieldDamageModifier function GetShieldDamageModifier( var damageInfo )
+{
+ ShieldDamageModifier damageModifier
+
+ // Disabling Shield Damage Modifiers and rebalancing the weapons. The below mechanics seem cool in an R1 style system though so leaving them commented out.
+ // NOTE: Changing Damage Scale has a buggy interaction with permanent damage that must be fixed if we re-enable this.
+ /*
+ int damageSourceIdentifier = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ switch ( damageSourceIdentifier )
+ {
+ case eDamageSourceId.mp_weapon_thermite_grenade:
+ damageModifier.permanentDamageFrac = 0.9
+ break
+ }
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_ELECTRICAL )
+ {
+ // amped version
+ if ( damageSourceIdentifier == eDamageSourceId.mp_titanweapon_xo16 )
+ damageModifier.damageScale *= 1.5
+
+ // amped version
+ if ( damageSourceIdentifier == eDamageSourceId.mp_titanweapon_triple_threat )
+ damageModifier.damageScale *= 1.5
+
+ if ( damageSourceIdentifier == eDamageSourceId.mp_titanweapon_arc_cannon )
+ damageModifier.damageScale *= 1.5
+ }
+ */
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_ELECTRICAL )
+ {
+ int damageSourceIdentifier = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ // Vanguard Arc Rounds
+ if ( damageSourceIdentifier == eDamageSourceId.mp_titanweapon_xo16_vanguard )
+ damageModifier.damageScale *= 1.5
+ }
+
+
+ return damageModifier
+}
+
+
+
+void function AddCallback_NPCLeeched( void functionref( entity, entity ) callbackFunc )
+{
+ Assert( !( svGlobal.onLeechedCustomCallbackFunc.contains( callbackFunc ) ) )
+ svGlobal.onLeechedCustomCallbackFunc.append( callbackFunc )
+}
+
+void function MessageToPlayer( entity player, int eventID, entity ent = null, var eventVal = null )
+{
+ var eHandle = null
+ if ( ent )
+ eHandle = ent.GetEncodedEHandle()
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_EventNotification", eventID, eHandle, eventVal )
+ //SendHudMessage( player, message, 0.33, 0.28, 255, 255, 255, 255, 0.15, 3.0, 0.5 )
+}
+
+
+void function MessageToTeam( int team, int eventID, entity excludePlayer = null, entity ent = null, var eventVal = null )
+{
+ array<entity> players = GetPlayerArray()
+
+ foreach ( player in players )
+ {
+ if ( player.GetTeam() != team )
+ continue
+
+ if ( player == excludePlayer )
+ continue
+
+ MessageToPlayer( player, eventID, ent, eventVal )
+ }
+}
+
+void function MessageToAll( int eventID, entity excludePlayer = null, entity ent = null, var eventVal = null )
+{
+ array<entity> players = GetPlayerArray()
+
+ foreach ( player in players )
+ {
+ if ( player == excludePlayer )
+ continue
+
+ MessageToPlayer( player, eventID, ent, eventVal )
+ }
+}
+
+
+
+string function ReloadScriptsInternal()
+{
+ reloadingScripts = true
+ reloadedScripts = true
+ ReloadingScriptsBegin()
+
+ if ( IsMenuLevel() )
+ {
+ reloadingScripts = false
+ ReloadingScriptsEnd()
+ return ""
+ }
+
+ TitanEmbark_Init()
+
+ ReloadScriptCallbacks()
+
+ reloadingScripts = false
+ ReloadingScriptsEnd()
+
+ return ( "reloaded server scripts" )
+}
+
+string function ReloadScripts()
+{
+ ServerCommand( "fs_report_sync_opens 0" ) // makes reload scripts slow
+ delaythread ( 0 ) ReloadScriptsInternal()
+
+ return ( "reloaded server scripts" )
+}
+
+int function GameTime_TimeLimitSeconds()
+{
+ if ( IsRoundBased() )
+ {
+ return ( GetRoundTimeLimit_ForGameMode() * 60.0 ).tointeger()
+ }
+ else
+ {
+ if ( IsSuddenDeathGameMode() && GetGameState() == eGameState.SuddenDeath )
+ return ( GetTimeLimit_ForGameMode() * 60.0 ).tointeger() + ( GetSuddenDeathTimeLimit_ForGameMode() * 60.0 ).tointeger()
+ else
+ return ( GetTimeLimit_ForGameMode() * 60.0 ).tointeger()
+ }
+ unreachable
+}
+
+int function GetSuddenDeathTimeLimit_ForGameMode()
+{
+ string mode = GameRules_GetGameMode()
+ string playlistString = "suddendeath_timelimit"
+
+ return GetCurrentPlaylistVarInt( playlistString, 4 )
+}
+
+int function GameTime_TimeLimitMinutes()
+{
+ if ( IsRoundBased() )
+ return floor( GetRoundTimeLimit_ForGameMode() ).tointeger()
+ else
+ return floor( GetTimeLimit_ForGameMode() ).tointeger()
+ unreachable
+}
+
+int function GameTime_TimeLeftMinutes()
+{
+ if ( GetGameState() == eGameState.WaitingForPlayers )
+ return 0
+ if ( GetGameState() == eGameState.Prematch )
+ return int( ( expect float( GetServerVar( "gameStartTime" ) ) - Time()) / 60.0 )
+
+ return floor( GameTime_TimeLimitMinutes() - GameTime_PlayingTime() / 60 ).tointeger()
+}
+
+int function GameTime_TimeLeftSeconds()
+{
+ if ( GetGameState() == eGameState.Prematch )
+ return int( expect float( GetServerVar( "gameStartTime" ) ) - Time() )
+
+ return floor( GameTime_TimeLimitSeconds() - GameTime_PlayingTime() ).tointeger()
+}
+
+int function GameTime_Seconds()
+{
+ return floor( Time() ).tointeger()
+}
+
+int function GameTime_Minutes()
+{
+ return int( floor( GameTime_Seconds() / 60 ) )
+}
+
+float function GameTime_PlayingTime()
+{
+ return GameTime_PlayingTimeSince( Time() )
+}
+
+float function GameTime_PlayingTimeSince( float sinceTime )
+{
+ int gameState = GetGameState()
+
+ // temp fix because i have no fucking clue why this crashes
+
+ if ( gameState < eGameState.Playing )
+ return 0
+
+ if ( IsRoundBased() )
+ {
+ if ( gameState > eGameState.SuddenDeath )
+ return (expect float( GetServerVar( "roundEndTime" ) ) - expect float( GetServerVar( "roundStartTime" ) ) )
+ else
+ return sinceTime - expect float( GetServerVar( "roundStartTime" ) )
+
+ }
+ else
+ {
+ if ( gameState > eGameState.SuddenDeath )
+ return (expect float( GetServerVar( "gameEndTime" ) ) - expect float( GetServerVar( "gameStartTime" ) ) )
+ else
+ return sinceTime - expect float( GetServerVar( "gameStartTime" ) )
+ }
+
+ unreachable
+}
+
+float function GameTime_TimeSpentInCurrentState()
+{
+ return Time() - expect float( GetServerVar( "gameStateChangeTime" ) )
+}
+
+int function GameScore_GetFirstToScoreLimit()
+{
+ return expect int( level.firstToScoreLimit )
+}
+
+bool function GameScore_AllowPointsOverLimit()
+{
+ return svGlobal.allowPointsOverLimit
+}
+
+int function GameScore_GetWinningTeam()
+{
+ if ( GameScore_GetFirstToScoreLimit() )
+ return GameScore_GetFirstToScoreLimit()
+
+ if ( IsRoundBased() )
+ {
+ if ( GameRules_GetTeamScore2( TEAM_IMC ) > GameRules_GetTeamScore2( TEAM_MILITIA ) )
+ return TEAM_IMC
+ else if ( GameRules_GetTeamScore2( TEAM_MILITIA ) > GameRules_GetTeamScore2( TEAM_IMC ) )
+ return TEAM_MILITIA
+ }
+ else
+ {
+ if ( GameRules_GetTeamScore( TEAM_IMC ) > GameRules_GetTeamScore( TEAM_MILITIA ) )
+ return TEAM_IMC
+ else if ( GameRules_GetTeamScore( TEAM_MILITIA ) > GameRules_GetTeamScore( TEAM_IMC ) )
+ return TEAM_MILITIA
+ }
+
+ return TEAM_UNASSIGNED
+}
+
+int function GameScore_GetWinningTeam_ThisRound()
+{
+ if ( GameScore_GetFirstToScoreLimit() )
+ return GameScore_GetFirstToScoreLimit()
+
+ Assert ( IsRoundBased() )
+
+ if ( GameRules_GetTeamScore( TEAM_IMC ) > GameRules_GetTeamScore( TEAM_MILITIA ) )
+ return TEAM_IMC
+ else if ( GameRules_GetTeamScore( TEAM_MILITIA ) > GameRules_GetTeamScore( TEAM_IMC ) )
+ return TEAM_MILITIA
+
+ return TEAM_UNASSIGNED
+}
+
+#if DEV
+void function KillIMC()
+{
+ array<entity> enemies = GetNPCArrayOfTeam( TEAM_IMC )
+ foreach ( enemy in enemies )
+ {
+ enemy.Die()
+ }
+}
+
+void function killtitans()
+{
+ printt( "Script command: Kill all titans" )
+ array<entity> titans = GetTitanArray()
+ foreach ( titan in titans )
+ titan.Die()
+}
+
+void function killminions()
+{
+ printt( "Script command: Kill all minions" )
+ array<entity> minions = GetAllMinions()
+ foreach ( minion in minions )
+ {
+ minion.Die()
+ }
+}
+#endif
+
+
+array<entity> function GetTeamMinions( int team )
+{
+ array<entity> ai = GetNPCArrayByClass( "npc_soldier" )
+ ai.extend( GetNPCArrayByClass( "npc_spectre" ) )
+
+ for ( int i = 0; i < ai.len(); i++ )
+ {
+ if ( ai[i].GetTeam() != team )
+ {
+ ai.remove(i)
+ i--
+ }
+ }
+
+ return ai
+}
+
+array<entity> function GetAllMinions()
+{
+ array<entity> ai = GetNPCArrayByClass( "npc_soldier" )
+ ai.extend( GetNPCArrayByClass( "npc_spectre" ) )
+ ai.extend( GetNPCArrayByClass( "npc_drone" ) )
+
+ return ai
+}
+
+
+bool function GameScore_IsLowScoreDifference()
+{
+ int winningTeam = GameScore_GetWinningTeam()
+
+ if ( !winningTeam )
+ return true
+
+ int losingTeam = GetOtherTeam( winningTeam )
+
+ int winningTeamScore
+ int losingTeamScore
+
+ if ( IsRoundBased() )
+ {
+ winningTeamScore = GameRules_GetTeamScore2( winningTeam )
+ losingTeamScore = GameRules_GetTeamScore2( losingTeam )
+ }
+ else
+ {
+ winningTeamScore = GameRules_GetTeamScore( winningTeam )
+ losingTeamScore = GameRules_GetTeamScore( losingTeam )
+ }
+
+ return ( winningTeamScore - losingTeamScore < 2 )
+}
+
+bool function IsFastPilot( entity player )
+{
+ Assert( IsPilot( player ), "Pilot only check" )
+
+ if ( player.IsWallHanging() )
+ return false
+
+ if ( player.IsWallRunning() )
+ return true
+
+ if ( !player.IsOnGround() )
+ return true
+
+ if ( LengthSqr( player.GetSmoothedVelocity() ) > 180*180 || LengthSqr( player.GetVelocity() ) > 180*180 )
+ return true
+
+ return false
+}
+
+void function KillPlayer( entity player, int damageSource )
+{
+ #if DEV
+ printt( "Played Killed from script: " )
+ DumpStack()
+ #endif
+
+ Assert( IsAlive( player ) )
+ Assert( player.IsPlayer() )
+ player.Die( svGlobal.worldspawn, svGlobal.worldspawn, { damageSourceId = damageSource, scriptType=DF_SKIP_DAMAGE_PROT | DF_SKIPS_DOOMED_STATE } )
+}
+
+
+//////////////////////////////////////////////////////////
+void function TurretChangeTeam( entity turret, int team )
+{
+ if ( team != TEAM_UNASSIGNED )
+ {
+ // If a turret is on some player's team it should never be invulnerable
+ MakeTurretVulnerable( turret )
+ }
+
+ SetTeam( turret, team )
+
+ // refresh the turret client side particle effects
+ UpdateTurretClientSideParticleEffects( turret )
+}
+
+void function MakeTurretInvulnerable( entity turret )
+{
+ Assert( IsValid( turret ) )
+ turret.SetInvulnerable()
+ turret.SetNoTarget(true)
+ turret.SetNoTargetSmartAmmo(true)
+}
+
+void function MakeTurretVulnerable( entity turret )
+{
+ Assert( IsValid( turret ) )
+ turret.ClearInvulnerable()
+ turret.SetNoTarget(false)
+ turret.SetNoTargetSmartAmmo(false)
+}
+
+
+void function UpdateTurretClientSideParticleEffects( entity turret )
+{
+ if ( !IsValid( turret ) )
+ return
+
+ int turretEHandle = turret.GetEncodedEHandle()
+ array<entity> players = GetPlayerArray()
+ foreach( player in players )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_TurretRefresh", turretEHandle )
+ }
+}
+
+
+
+bool function TakePrimaryWeapon( entity player )
+{
+ array<entity> weapons = player.GetMainWeapons()
+ foreach ( index, weaponEnt in weapons )
+ {
+ int weaponType = weaponEnt.GetWeaponType()
+ if ( weaponType == WT_SIDEARM || weaponType == WT_ANTITITAN )
+ continue;
+
+ string weapon = weaponEnt.GetWeaponClassName()
+ player.TakeWeaponNow( weapon )
+ return true
+ }
+ return false
+}
+
+bool function TakeSecondaryWeapon( entity player )
+{
+ array<entity> weapons = player.GetMainWeapons()
+ foreach ( index, weaponEnt in weapons )
+ {
+ if ( weaponEnt.GetWeaponType() != WT_ANTITITAN )
+ continue
+
+ string weapon = weaponEnt.GetWeaponClassName()
+ player.TakeWeaponNow( weapon )
+ return true
+ }
+ return false
+}
+
+bool function TakeSidearmWeapon( entity player )
+{
+ array<entity> weapons = player.GetMainWeapons()
+ foreach ( index, weaponEnt in weapons )
+ {
+ if ( weaponEnt.GetWeaponType() != WT_SIDEARM )
+ continue
+
+ string weapon = weaponEnt.GetWeaponClassName()
+ player.TakeWeaponNow( weapon )
+ return true
+ }
+ return false
+}
+
+void function TakeAllWeapons( entity ent )
+{
+ if ( ent.IsPlayer() )
+ {
+ ent.RemoveAllItems()
+ array<entity> weapons = ent.GetMainWeapons()
+ foreach ( weapon in weapons )
+ {
+ Assert( 0, ent + " still has weapon " + weapon.GetWeaponClassName() + " after doing takeallweapons" )
+ }
+ }
+ else
+ {
+ array<entity> weapons = ent.GetMainWeapons()
+ TakeWeaponsForArray( ent, weapons )
+
+ weapons = ent.GetOffhandWeapons()
+ foreach ( index, weapon in clone weapons )
+ {
+ ent.TakeOffhandWeapon( index )
+ }
+ TakeWeaponsForArray( ent, weapons )
+ }
+}
+
+
+void function SetSpawnflags( entity ent, int spawnFlags )
+{
+ ent.kv.spawnflags = spawnFlags
+}
+
+
+void function DestroyAfterDelay( entity ent, float delay )
+{
+ Assert( IsNewThread(), "Must be threaded off" )
+
+ ent.EndSignal( "OnDestroy" )
+
+ wait( delay )
+
+ ent.Destroy()
+}
+
+void function UnlockAchievement( entity player, int achievementID )
+{
+ Assert( IsValid( player ), "Can't unlock achievement on invalid player entity" )
+ Assert( player.IsPlayer(), "Can't unlock achivement on non-player entity" )
+ Assert( achievementID > 0 && achievementID < achievements.MAX_ACHIVEMENTS, "Tried to unlock achievement with invalid enum value" )
+
+ Remote_CallFunction_UI( player, "ScriptCallback_UnlockAchievement", achievementID )
+}
+
+void function UpdateHeroStatsForPlayer( entity player )
+{
+ if ( !IsValid( player ) )
+ return
+ Remote_CallFunction_NonReplay( player, "ServerCallback_UpdateHeroStats" )
+}
+
+void function TestDeathFall()
+{
+ entity trigger = GetEntByScriptName( "DeathFallTrigger" )
+ table results = WaitSignal( trigger, "OnTrigger" )
+ printt( "DEATH FALL TRIGGERED" )
+ PrintTable( results )
+}
+
+bool function PlayerHasTitan( entity player )
+{
+ entity titan
+ if ( player.IsTitan() )
+ titan = player
+ else
+ titan = player.GetPetTitan()
+
+ if ( IsAlive( titan ) )
+ return true
+
+ return false
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_utility_shared.nut b/Northstar.CustomServers/mod/scripts/vscripts/_utility_shared.nut
new file mode 100644
index 00000000..e3cb0dbf
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_utility_shared.nut
@@ -0,0 +1,4069 @@
+untyped
+
+globalize_all_functions
+
+const DEV_DRAWALLTRIGGERS = 0
+
+global const CHARGE_TOOL = "sp_weapon_arc_tool"
+
+global const TRIG_FLAG_NONE = 0
+global const TRIG_FLAG_PLAYERONLY = 0x0001
+global const TRIG_FLAG_NPCONLY = 0x0002
+global const TRIG_FLAG_NOCONTEXTBUSY = 0x0004
+global const TRIG_FLAG_ONCE = 0x0008
+global const TRIG_FLAG_EXCLUSIVE = 0x0010 // can only be triggered by entities passed in at creation
+global const TRIG_FLAG_DEVDRAW = 0x0020
+global const TRIG_FLAG_START_DISABLED = 0x0040
+global const TRIG_FLAG_NO_PHASE_SHIFT = 0x0080
+global const float MAP_EXTENTS = 128*128
+/*
+const TRIG_FLAG_ = 0x0080
+const TRIG_FLAG_ = 0x0100*/
+
+global const TRIGGER_INTERNAL_SIGNAL = "OnTrigger"
+
+global const CALCULATE_SEQUENCE_BLEND_TIME = -1.0
+
+global struct ArrayDistanceEntry
+{
+ float distanceSqr
+ entity ent
+ vector origin
+}
+
+global struct GravityLandData
+{
+ array<vector> points
+ TraceResults& traceResults
+ float elapsedTime
+}
+
+global struct FirstPersonSequenceStruct
+{
+ string firstPersonAnim = ""
+ string thirdPersonAnim = ""
+ string firstPersonAnimIdle = ""
+ string thirdPersonAnimIdle = ""
+ string relativeAnim = ""
+ string attachment = ""
+ bool teleport = false
+ bool noParent = false
+ float blendTime = CALCULATE_SEQUENCE_BLEND_TIME
+ float firstPersonBlendOutTime = -1.0
+ bool noViewLerp = false
+ bool hideProxy = false
+ void functionref( entity ) viewConeFunction = null
+ vector ornull origin = null
+ vector ornull angles = null
+ bool enablePlanting = false
+ float setInitialTime = 0.0 //set the starting point of the animation in seconds
+ bool useAnimatedRefAttachment = false //Position entity using ref every frame instead of using root motion
+ bool renderWithViewModels = false
+ bool gravity = false // force gravity command on sequence
+ bool playerPushable = false
+ array< string > thirdPersonCameraAttachments = []
+ bool thirdPersonCameraVisibilityChecks = false
+ entity thirdPersonCameraEntity = null
+ bool snapPlayerFeetToEyes = true
+}
+
+global struct FrontRightDotProductsStruct
+{
+ float forwardDot = 0.0
+ float rightDot = 0.0
+}
+
+global struct RaySphereIntersectStruct
+{
+ bool result
+ float enterFrac
+ float leaveFrac
+}
+
+void function Utility_Shared_Init()
+{
+ RegisterSignal( TRIGGER_INTERNAL_SIGNAL )
+ RegisterSignal( "devForcedWin" )
+
+ #document( "IsAlive", "Returns true if the given ent is not null, and is alive." )
+ #document( "ArrayWithin", "Remove ents from array that are out of range" )
+}
+
+#if DEV
+// short cut for the console
+// script gp()[0].Die( gp()[1] )
+array<entity> function gp()
+{
+ return GetPlayerArray()
+}
+#endif
+
+void function InitWeaponScripts()
+{
+ SmartAmmo_Init()
+
+ // WEAPON SCRIPTS
+ ArcCannon_Init()
+ Grenade_FileInit()
+ Vortex_Init()
+
+// #if SERVER
+// PrecacheProjectileEntity( "grenade_frag" )
+// PrecacheProjectileEntity( "crossbow_bolt" )
+// #endif
+
+ MpWeaponDroneBeam_Init()
+ MpWeaponDroneRocket_Init()
+ MpWeaponDronePlasma_Init()
+ MpWeaponTurretPlasma_Init()
+ MpWeaponTurretLaser_Init()
+ MpWeaponSuperSpectre_Init()
+ MpWeaponGunshipLauncher_Init()
+ MpWeaponFragDrone_Init()
+ MpAbilityShifter_Init()
+ MpTitanabilityBubbleShield_Init()
+ MpTitanabilityAmpedWall_Init()
+ MpTitanabilityFusionCore_Init()
+ MpTitanweapon40mm_Init()
+ MpTitanWeaponpredatorcannon_Init()
+ MpTitanweaponRocketeetRocketStream_Init()
+ MpTitanweaponMeteor_Init()
+ MpTitanWeapon_SniperInit()
+ MpTitanweaponVortexShield_Init()
+ MpTitanweaponXo16_Init()
+ MpWeaponDefender_Init()
+ MpWeaponDmr_Init()
+ MpWeaponProximityMine_Init()
+ MpWeaponRocketLauncher_Init()
+ MpWeaponNPCRocketLauncher_Init()
+ MpWeaponSatchel_Init()
+ MpWeaponSmartPistol_Init()
+ MpWeaponSniper_Init()
+ MpWeaponLSTAR_Init()
+ MpTitanWeaponParticleAccelerator_Init()
+ MpWeaponMegaTurret_Init()
+ MpWeaponZipline_Init()
+ SpWeaponHoldBeam_Init()
+ MpTitanweaponArcBall_Init()
+ MpWeaponDeployableCover_Init()
+ MpTitanAbilityBasicBlock_Init()
+ MpTitanAbilityLaserTrip_Init()
+ MpTitanWeaponArcWave_Init()
+ MpTitanWeaponFlameWave_Init()
+ MpWeaponAlternatorSMG_Init()
+ MpWeaponGreandeElectricSmoke_Init()
+ MpWeaponGrenadeGravity_Init()
+ MpWeaponDeployableCloakfield_Init()
+ MpWeaponTether_Init()
+ MpWeaponTripWire_Init()
+ MpTitanAbilitySmartCore_Init()
+ MpTitanAbilitySlowTrap_Init()
+ MpTitanAbilityPowerShot_Init()
+ MpTitanAbilityAmmoSwap_Init()
+ MpTitanAbilityRocketeerAmmoSwap_Init()
+ MpTitanAbilityHeatShield_Init()
+ SonarGrenade_Init()
+ MpTitanAbilityGunShield_Init()
+ MpTitanWeaponLaserLite_Init()
+ MpTitanWeaponSword_Init()
+ MpTitanAbilityHover_Init()
+ MpTitanWeaponTrackerRockets_Init()
+ MpTitanWeaponStunLaser_Init()
+ MpTitanWeaponShoulderRockets_Init()
+ MpTitanAbilitySmoke_Init()
+ #if MP
+ MpWeaponArcTrap_Init()
+ #endif
+
+ #if SERVER
+ BallLightning_Init()
+ #endif
+}
+
+float function GetCurrentPlaylistVarFloat( string val, float useVal )
+{
+ var result = GetCurrentPlaylistVarOrUseValue( val, useVal + "" )
+ if ( result == null || result == "" )
+ return 0.0
+
+ return float( result )
+}
+
+void function SetSkinForTeam( entity ent, int team )
+{
+ if ( team == TEAM_IMC )
+ ent.SetSkin( 0 )
+ else if ( team == TEAM_MILITIA )
+ ent.SetSkin( 1 )
+}
+
+void function TableDump( table Table, int depth = 0 )
+{
+ if ( depth > 4 )
+ return
+
+ foreach ( k, v in Table )
+ {
+ printl( "Key: " + k + " Value: " + v )
+ if ( type( v ) == "table" && depth )
+ TableDump( expect table( v ), depth + 1 )
+ }
+}
+
+/*entity function GetVortexWeapon( entity player )
+{
+ for ( int weaponIndex = 0; weaponIndex < 2; weaponIndex++ )
+ {
+ entity weapon = player.GetOffhandWeapon( weaponIndex )
+ if ( !IsValid( weapon ) )
+ continue
+ if ( weapon.GetWeaponClassName() != "mp_titanweapon_vortex_shield" )
+ continue
+ return weapon
+ }
+
+ Assert( false, "Vortex weapon not found!" )
+ unreachable
+}*/
+
+entity function GetClosest( array<entity> entArray, vector origin, float maxdist = -1.0 )
+{
+ Assert( entArray.len() > 0 )
+
+ entity bestEnt = entArray[ 0 ]
+ float bestDistSqr = DistanceSqr( bestEnt.GetOrigin(), origin )
+
+ for ( int i = 1; i < entArray.len(); i++ )
+ {
+ entity newEnt = entArray[ i ]
+ float newDistSqr = LengthSqr( newEnt.GetOrigin() - origin )
+
+ if ( newDistSqr < bestDistSqr )
+ {
+ bestEnt = newEnt
+ bestDistSqr = newDistSqr
+ }
+ }
+
+ if ( maxdist >= 0.0 )
+ {
+ if ( bestDistSqr > maxdist * maxdist )
+ return null
+ }
+
+ return bestEnt
+}
+
+entity function GetClosest2D( array<entity> entArray, vector origin, float maxdist = -1.0 )
+{
+ Assert( entArray.len() > 0, "Empty array!" )
+
+ entity bestEnt = entArray[ 0 ]
+ float bestDistSqr = DistanceSqr( bestEnt.GetOrigin(), origin )
+
+ for ( int i = 1; i < entArray.len(); i++ )
+ {
+ entity newEnt = entArray[ i ]
+ float newDistSqr = Length2DSqr( newEnt.GetOrigin() - origin )
+
+ if ( newDistSqr < bestDistSqr )
+ {
+ bestEnt = newEnt
+ bestDistSqr = newDistSqr
+ }
+ }
+
+ if ( maxdist >= 0.0 )
+ {
+ if ( bestDistSqr > maxdist * maxdist )
+ return null
+ }
+
+ return bestEnt
+}
+
+bool function GameModeHasCapturePoints()
+{
+ #if CLIENT
+ return clGlobal.hardpointStringIDs.len() > 0
+ #elseif SERVER
+ return svGlobal.hardpointStringIDs.len() > 0
+ #endif
+}
+
+entity function GetFarthest( array<entity> entArray, vector origin )
+{
+ Assert( entArray.len() > 0, "Empty array!" )
+
+ entity bestEnt = entArray[0]
+ float bestDistSqr = DistanceSqr( bestEnt.GetOrigin(), origin )
+
+ for ( int i = 1; i < entArray.len(); i++ )
+ {
+ entity newEnt = entArray[ i ]
+ float newDistSqr = DistanceSqr( newEnt.GetOrigin(), origin )
+
+ if ( newDistSqr > bestDistSqr )
+ {
+ bestEnt = newEnt
+ bestDistSqr = newDistSqr
+ }
+ }
+
+ return bestEnt
+}
+
+int function GetClosestIndex( array<entity> Array, vector origin )
+{
+ Assert( Array.len() > 0 )
+
+ int index = 0
+ float distSqr = LengthSqr( Array[ index ].GetOrigin() - origin )
+
+ entity newEnt
+ float newDistSqr
+ for ( int i = 1; i < Array.len(); i++ )
+ {
+ newEnt = Array[ i ]
+ newDistSqr = LengthSqr( newEnt.GetOrigin() - origin )
+
+ if ( newDistSqr < distSqr )
+ {
+ index = i
+ distSqr = newDistSqr
+ }
+ }
+
+ return index
+}
+
+// nothing in the game uses the format "Table.r/g/b/a"... wtf is the point of this function
+table function StringToColors( string colorString, string delimiter = " " )
+{
+ PerfStart( PerfIndexShared.StringToColors + SharedPerfIndexStart )
+ array<string> tokens = split( colorString, delimiter )
+
+ Assert( tokens.len() >= 3 )
+
+ table Table = {}
+ Table.r <- int( tokens[0] )
+ Table.g <- int( tokens[1] )
+ Table.b <- int( tokens[2] )
+
+ if ( tokens.len() == 4 )
+ Table.a <- int( tokens[3] )
+ else
+ Table.a <- 255
+
+ PerfEnd( PerfIndexShared.StringToColors + SharedPerfIndexStart )
+ return Table
+}
+
+// TODO: Set return type to array<int> when SetColor() accepts this type
+function ColorStringToArray( string colorString )
+{
+ array<string> tokens = split( colorString, " " )
+
+ Assert( tokens.len() >= 3 && tokens.len() <= 4 )
+
+ array colorArray
+ foreach ( token in tokens )
+ colorArray.append( int( token ) )
+
+ return colorArray
+}
+
+// Evaluate a generic order ( coefficientArray.len() - 1 ) polynomial
+// e.g. to evaluate (Ax + B), call EvaluatePolynomial(x, A, B)
+// Note that EvaluatePolynomial(x) returns 0 and
+// EvaluatePolynomial(x, A) returns A, which are technically correct
+// but perhaps not what you expect
+float function EvaluatePolynomial( float x, array<float> coefficientArray )
+{
+ float sum = 0.0
+
+ for ( int i = 0; i < coefficientArray.len() - 1; ++i )
+ sum += coefficientArray[ i ] * pow( x, coefficientArray.len() -1 - i )
+
+ if ( coefficientArray.len() >= 1 )
+ sum += coefficientArray[ coefficientArray.len() - 1 ]
+
+ return sum
+}
+
+void function WaitForever()
+{
+ #if SERVER
+ svGlobal.levelEnt.WaitSignal( "forever" )
+ #elseif CLIENT
+ clGlobal.levelEnt.WaitSignal( "forever" )
+ #endif
+}
+
+#if SERVER
+
+bool function ShouldDoReplay( entity player, entity attacker, float replayTime, int methodOfDeath )
+{
+ if ( ShouldDoReplayIsForcedByCode() )
+ {
+ print( "ShouldDoReplay(): Doing a replay because code forced it." );
+ return true
+ }
+
+ if ( GetCurrentPlaylistVarInt( "replay_disabled", 0 ) == 1 )
+ {
+ print( "ShouldDoReplay(): Not doing a replay because 'replay_disabled' is enabled in the current playlist.\n" );
+ return false
+ }
+
+ switch( methodOfDeath )
+ {
+ case eDamageSourceId.human_execution:
+ case eDamageSourceId.titan_execution:
+ {
+ print( "ShouldDoReplay(): Not doing a replay because the player died from an execution.\n" );
+ return false
+ }
+ }
+
+ if ( level.nv.replayDisabled )
+ {
+ print( "ShouldDoReplay(): Not doing a replay because replays are disabled for the level.\n" );
+ return false
+ }
+
+ if ( Time() - player.p.connectTime <= replayTime ) //Bad things happen if we try to do a kill replay that lasts longer than the player entity existing on the server
+ {
+ print( "ShouldDoReplay(): Not doing a replay because the player is not old enough.\n" );
+ return false
+ }
+
+ if ( player == attacker )
+ {
+ print( "ShouldDoReplay(): Not doing a replay because the attacker is the player.\n" );
+ return false
+ }
+
+ if ( player.IsBot() == true )
+ {
+ print( "ShouldDoReplay(): Not doing a replay because the player is a bot.\n" );
+ return false
+ }
+
+ return AttackerShouldTriggerReplay( attacker )
+}
+
+// Don't let things like killbrushes show replays
+bool function AttackerShouldTriggerReplay( entity attacker )
+{
+ if ( !IsValid( attacker ) )
+ {
+ print( "AttackerShouldTriggerReplay(): Not doing a replay because the attacker is not valid.\n" )
+ return false
+ }
+
+ if ( attacker.IsPlayer() )
+ {
+ print( "AttackerShouldTriggerReplay(): Doing a replay because the attacker is a player.\n" )
+ return true
+ }
+
+ if ( attacker.IsNPC() )
+ {
+ print( "AttackerShouldTriggerReplay(): Doing a replay because the attacker is an NPC.\n" )
+ return true
+ }
+
+ print( "AttackerShouldTriggerReplay(): Not doing a replay by default.\n" )
+ return false
+}
+#endif // #if SERVER
+
+vector function RandomVec( float range )
+{
+ // could rewrite so it doesnt make a box of random.
+ vector vec = Vector( 0, 0, 0 )
+ vec.x = RandomFloatRange( -range, range )
+ vec.y = RandomFloatRange( -range, range )
+ vec.z = RandomFloatRange( -range, range )
+
+ return vec
+}
+
+function ArrayValuesToTableKeys( arr )
+{
+ Assert( type( arr ) == "array", "Not an array" )
+
+ local resultTable = {}
+ for ( int i = 0; i < arr.len(); ++ i)
+ {
+ resultTable[ arr[ i ] ] <- 1
+ }
+
+ return resultTable
+}
+
+function TableKeysToArray( tab )
+{
+ Assert( type( tab ) == "table", "Not a table" )
+
+ local resultArray = []
+ resultArray.resize( tab.len() )
+ int currentArrayIndex = 0
+ foreach ( key, val in tab )
+ {
+ resultArray[ currentArrayIndex ] = key
+ ++currentArrayIndex
+ }
+
+ return resultArray
+}
+
+function TableRandom( Table )
+{
+ Assert( type( Table ) == "table", "Not a table" )
+
+ local Array = []
+
+ foreach ( entry, contents in Table )
+ {
+ Array.append( contents )
+ }
+
+ return Array.getrandom()
+}
+
+int function RandomWeightedIndex( array Array )
+{
+ int count = Array.len()
+ Assert( count != 0, "Array is empty" )
+
+ int sum = int( ( count * ( count + 1 ) ) / 2.0 ) // ( n * ( n + 1 ) ) / 2
+ int randInt = RandomInt( sum )
+ for ( int i = 0 ; i < count ; i++ )
+ {
+ int rangeForThisIndex = count - i
+ if ( randInt < rangeForThisIndex )
+ return i
+
+ randInt -= rangeForThisIndex
+ }
+
+ Assert( 0 )
+ unreachable
+}
+
+bool function IsValid_ThisFrame( entity ent )
+{
+ if ( ent == null )
+ return false
+
+ return expect bool( ent.IsValidInternal() )
+}
+
+bool function IsAlive( entity ent )
+{
+ if ( ent == null )
+ return false
+ if ( !ent.IsValidInternal() )
+ return false
+
+ return ent.IsEntAlive()
+}
+
+#if DEV && SERVER
+void function vduon()
+{
+ PlayConversationToAll( "TitanReplacement" )
+}
+
+void function playconvtest( string conv )
+{
+ entity player = GetPlayerArray()[0]
+ array<entity> guys = GetAllSoldiers()
+ if ( !guys.len() )
+ {
+ printt( "No AI!!" )
+ return
+ }
+ entity guy = GetClosest( guys, player.GetOrigin() )
+ if ( conv in player.s.lastAIConversationTime )
+ delete player.s.lastAIConversationTime[ conv ]
+
+ printt( "Play ai conversation " + conv )
+ PlaySquadConversationToAll( conv, guy )
+}
+#endif //DEV
+
+void function FighterExplodes( entity ship )
+{
+ vector origin = ship.GetOrigin()
+ vector angles = ship.GetAngles()
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, "AngelCity_Scr_RedeyeWeaponExplos" )
+ #if SERVER
+ PlayFX( FX_HORNET_DEATH, origin )
+ #else
+ int fxid = GetParticleSystemIndex( FX_HORNET_DEATH )
+ StartParticleEffectInWorld( fxid, origin, angles )
+ #endif
+}
+
+vector function PositionOffsetFromEnt( entity ent, float offsetX, float offsetY, float offsetZ )
+{
+ vector angles = ent.GetAngles()
+ vector origin = ent.GetOrigin()
+ origin += AnglesToForward( angles ) * offsetX
+ origin += AnglesToRight( angles ) * offsetY
+ origin += AnglesToUp( angles ) * offsetZ
+ return origin
+}
+
+vector function PositionOffsetFromOriginAngles( vector origin, vector angles, float offsetX, float offsetY, float offsetZ )
+{
+ origin += AnglesToForward( angles ) * offsetX
+ origin += AnglesToRight( angles ) * offsetY
+ origin += AnglesToUp( angles ) * offsetZ
+ return origin
+}
+
+
+bool function IsMenuLevel()
+{
+ return IsLobby()
+}
+
+function Dump( package, depth = 0 )
+{
+ if ( depth > 6 )
+ return
+
+ foreach ( k, v in package )
+ {
+ for ( int i = 0; i < depth; i++ )
+ print( " ")
+
+ if ( IsTable( package ) )
+ printl( "Key: " + k + " Value: " + v )
+ if ( IsArray( package ) )
+ printl( "Index: " + k + " Value: " + v )
+
+ if ( IsTable( v ) || IsArray( v ) )
+ Dump( v, depth + 1 )
+ }
+}
+
+bool function UseShortNPCTitles()
+{
+ return GetCurrentPlaylistVarInt( "npc_short_titles", 0 ) ? true : false
+}
+
+string function GetShortNPCTitle( int team )
+{
+ return GetTeamName( team )
+}
+
+bool function IsIMCOrMilitiaTeam( int team )
+{
+ return team == TEAM_MILITIA || team == TEAM_IMC
+}
+
+int function GetOtherTeam( int team )
+{
+ if ( team == TEAM_IMC )
+ return TEAM_MILITIA
+
+ if ( team == TEAM_MILITIA )
+ return TEAM_IMC
+
+ Assert( false, "Trying to GetOtherTeam() for team: " + team + " that is neither Militia nor IMC" )
+ unreachable
+}
+
+float function VectorDot_PlayerToOrigin( entity player, vector targetOrigin )
+{
+ vector playerEyePosition = player.EyePosition()
+ vector vecToEnt = ( targetOrigin - playerEyePosition )
+ vecToEnt.Norm()
+
+ // GetViewVector() only works on the player
+ float dotVal = vecToEnt.Dot( player.GetViewVector() )
+ return dotVal
+}
+
+float function VectorDot_DirectionToOrigin( entity player, vector direction, vector targetOrigin )
+{
+ vector playerEyePosition = player.EyePosition()
+ vector vecToEnt = ( targetOrigin - playerEyePosition )
+ vecToEnt.Norm()
+
+ // GetViewVector() only works on the player
+ float dotVal = DotProduct( vecToEnt, direction )
+ return dotVal
+}
+
+void function WaitUntilWithinDistance( entity player, entity titan, float dist )
+{
+ float distSqr = dist * dist
+ for ( ;; )
+ {
+ if ( !IsAlive( titan ) )
+ return
+
+ if ( IsAlive( player ) )
+ {
+ if ( DistanceSqr( player.GetOrigin(), titan.GetOrigin() ) <= distSqr )
+ return
+ }
+ wait 0.1
+ }
+}
+
+void function WaitUntilBeyondDistance( entity player, entity titan, float dist )
+{
+ float distSqr = dist * dist
+ for ( ;; )
+ {
+ if ( !IsAlive( titan ) )
+ return
+
+ if ( IsAlive( player ) )
+ {
+ if ( DistanceSqr( player.GetOrigin(), titan.GetOrigin() ) > distSqr )
+ return
+ }
+ wait 0.1
+ }
+}
+
+bool function IsModelViewer()
+{
+ return GetMapName() == "mp_model_viewer"
+}
+
+
+//----------------------------------//
+// Tweening functions //
+// Pass in a fraction 0.0 - 1.0 //
+// Get a fraction back 0.0 - 1.0 //
+//----------------------------------//
+
+// simple linear tweening - no easing, no acceleration
+float function Tween_Linear( float frac )
+{
+ Assert( frac >= 0.0 && frac <= 1.0 )
+ return frac
+}
+
+// quadratic easing out - decelerating to zero velocity
+float function Tween_QuadEaseOut( float frac )
+{
+ Assert( frac >= 0.0 && frac <= 1.0 )
+ return -1.0 * frac*(frac-2)
+}
+
+// exponential easing out - decelerating to zero velocity
+float function Tween_ExpoEaseOut( float frac )
+{
+ Assert( frac >= 0.0 && frac <= 1.0 )
+ return -pow( 2.0, -10.0 * frac ) + 1.0
+}
+
+float function Tween_ExpoEaseIn( float frac )
+{
+ Assert( frac >= 0.0 && frac <= 1.0 )
+ return pow( 2, 10 * ( frac - 1 ) );
+}
+
+bool function LegalOrigin( vector origin )
+{
+ if ( fabs( origin.x ) > MAX_WORLD_COORD )
+ return false
+
+ if ( fabs( origin.y ) > MAX_WORLD_COORD )
+ return false
+
+ if ( fabs( origin.z ) > MAX_WORLD_COORD )
+ return false
+
+ return true
+}
+
+vector function AnglesOnSurface( surfaceNormal, playerVelocity )
+{
+ playerVelocity.Norm()
+ vector right = CrossProduct( playerVelocity, surfaceNormal )
+ vector forward = CrossProduct( surfaceNormal, right )
+ vector angles = VectorToAngles( forward )
+ angles.z = atan2( right.z, surfaceNormal.z ) * RAD_TO_DEG
+
+ return angles
+}
+
+vector function ClampToWorldspace( vector origin )
+{
+ // temp solution for start positions that are outside the world bounds
+ origin.x = clamp( origin.x, -MAX_WORLD_COORD, MAX_WORLD_COORD )
+ origin.y = clamp( origin.y, -MAX_WORLD_COORD, MAX_WORLD_COORD )
+ origin.z = clamp( origin.z, -MAX_WORLD_COORD, MAX_WORLD_COORD )
+
+ return origin
+}
+
+function UseReturnTrue( user, usee )
+{
+ return true
+}
+
+function ControlPanel_CanUseFunction( playerUser, controlPanel )
+{
+ expect entity( playerUser )
+ expect entity( controlPanel )
+
+ // Does a simple cone FOV check from the screen to the player's eyes
+ int maxAngleToAxisAllowedDegrees = 60
+
+ vector playerEyePos = playerUser.EyePosition()
+ int attachmentIndex = controlPanel.LookupAttachment( "PANEL_SCREEN_MIDDLE" )
+
+ Assert( attachmentIndex != 0 )
+ vector controlPanelScreenPosition = controlPanel.GetAttachmentOrigin( attachmentIndex )
+ vector controlPanelScreenAngles = controlPanel.GetAttachmentAngles( attachmentIndex )
+ vector controlPanelScreenForward = AnglesToForward( controlPanelScreenAngles )
+
+ vector screenToPlayerEyes = Normalize( playerEyePos - controlPanelScreenPosition )
+
+ return DotProduct( screenToPlayerEyes, controlPanelScreenForward ) > deg_cos( maxAngleToAxisAllowedDegrees )
+}
+
+function SentryTurret_CanUseFunction( playerUser, sentryTurret )
+{
+ expect entity( playerUser )
+ expect entity( sentryTurret )
+
+ // Does a simple cone FOV check from the screen to the player's eyes
+ int maxAngleToAxisAllowedDegrees = 90
+
+ vector playerEyePos = playerUser.EyePosition()
+ int attachmentIndex = sentryTurret.LookupAttachment( "turret_player_use" )
+
+ Assert( attachmentIndex != 0 )
+ vector sentryTurretUsePosition = sentryTurret.GetAttachmentOrigin( attachmentIndex )
+ vector sentryTurretUseAngles = sentryTurret.GetAttachmentAngles( attachmentIndex )
+ vector sentryTurretUseForward = AnglesToForward( sentryTurretUseAngles )
+
+ vector useToPlayerEyes = Normalize( playerEyePos - sentryTurretUsePosition )
+
+ return DotProduct( useToPlayerEyes, sentryTurretUseForward ) > deg_cos( maxAngleToAxisAllowedDegrees )
+}
+
+void function ArrayRemoveInvalid( array<entity> ents )
+{
+ for ( int i = ents.len() - 1; i >= 0; i-- )
+ {
+ if ( !IsValid( ents[ i ] ) )
+ ents.remove( i )
+ }
+}
+
+bool function HasDamageStates( entity ent )
+{
+ if ( !IsValid( ent ) )
+ return false
+ return ( "damageStateInfo" in ent.s )
+}
+
+bool function HasHitData( entity ent )
+{
+ return ( "hasHitData" in ent.s && expect bool( ent.s.hasHitData ) )
+}
+
+FrontRightDotProductsStruct function GetFrontRightDots( entity baseEnt, entity relativeEnt, string optionalTag = "" )
+{
+ if ( optionalTag != "" )
+ {
+ int attachIndex = baseEnt.LookupAttachment( optionalTag )
+ vector origin = baseEnt.GetAttachmentOrigin( attachIndex )
+ vector angles = baseEnt.GetAttachmentAngles( attachIndex )
+ angles.x = 0
+ angles.z = 0
+ vector forward = AnglesToForward( angles )
+ vector right = AnglesToRight( angles )
+
+ vector targetOrg = relativeEnt.GetOrigin()
+ vector vecToEnt = ( targetOrg - origin )
+// printt( "vecToEnt ", vecToEnt )
+ vecToEnt.z = 0
+
+ vecToEnt.Norm()
+
+
+ FrontRightDotProductsStruct result
+ result.forwardDot = DotProduct( vecToEnt, forward )
+ result.rightDot = DotProduct( vecToEnt, right )
+
+ // red: forward for incoming ent
+ //DebugDrawLine( origin, origin + vecToEnt * 150, 255, 0, 0, true, 5 )
+
+ // green: tag forward
+ //DebugDrawLine( origin, origin + forward * 150, 0, 255, 0, true, 5 )
+
+ // blue: tag right
+ //DebugDrawLine( origin, origin + right * 150, 0, 0, 255, true, 5 )
+ return result
+ }
+
+ vector targetOrg = relativeEnt.GetOrigin()
+ vector origin = baseEnt.GetOrigin()
+ vector vecToEnt = ( targetOrg - origin )
+ vecToEnt.Norm()
+
+ FrontRightDotProductsStruct result
+ result.forwardDot = vecToEnt.Dot( baseEnt.GetForwardVector() )
+ result.rightDot = vecToEnt.Dot( baseEnt.GetRightVector() )
+ return result
+}
+
+
+
+array<vector> function GetAllPointsOnBezier( array<vector> points, int numSegments, float debugDrawTime = 0.0 )
+{
+ Assert( points.len() >= 2 )
+ Assert( numSegments > 0 )
+ array<vector> curvePoints = []
+
+ // Debug draw the points used for the curve
+ if ( debugDrawTime )
+ {
+ for ( int i = 0; i < points.len() - 1; i++ )
+ DebugDrawLine( points[i], points[i + 1], 150, 150, 150, true, debugDrawTime )
+ }
+
+ for ( int i = 0; i < numSegments; i++ )
+ {
+ float t = ( i.tofloat() / ( numSegments.tofloat() - 1.0 ) ).tofloat()
+ curvePoints.append( GetSinglePointOnBezier( points, t ) )
+ }
+
+ return curvePoints
+}
+
+vector function GetSinglePointOnBezier( array<vector> points, float t )
+{
+ // evaluate a point on a bezier-curve. t goes from 0 to 1.0
+
+ array<vector> lastPoints = clone points
+ for(;;)
+ {
+ array<vector> newPoints = []
+ for ( int i = 0; i < lastPoints.len() - 1; i++ )
+ newPoints.append( lastPoints[i] + ( lastPoints[i+1] - lastPoints[i] ) * t )
+
+ if ( newPoints.len() == 1 )
+ return newPoints[0]
+
+ lastPoints = newPoints
+ }
+
+ unreachable
+}
+
+bool function GetDoomedState( entity ent )
+{
+ entity soul = ent.GetTitanSoul()
+ if ( !IsValid( soul ) )
+ return false
+
+ return soul.IsDoomed()
+}
+
+bool function TitanCoreInUse( entity player )
+{
+ Assert( player.IsTitan() )
+
+ if ( !IsAlive( player ) )
+ return false
+
+ return Time() < SoulTitanCore_GetExpireTime( player.GetTitanSoul() )
+}
+
+
+// Return float or null
+function GetTitanCoreTimeRemaining( entity player )
+{
+ if ( !player.IsTitan() )
+ return null
+
+ entity soul = player.GetTitanSoul()
+
+ if ( !soul )
+ return null
+
+ return SoulTitanCore_GetExpireTime( soul ) - Time()
+}
+
+bool function CoreAvailableDuringDoomState()
+{
+ return true
+}
+
+bool function HasAntiTitanWeapon( entity guy )
+{
+ foreach ( weapon in guy.GetMainWeapons() )
+ {
+ if ( weapon.GetWeaponType() == WT_ANTITITAN )
+ return true
+ }
+ return false
+}
+
+float function GetTitanCoreActiveTime( entity player )
+{
+ entity weapon = player.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+
+ if ( !IsValid( weapon ) )
+ {
+ printt( "WARNING: tried to get core active time, but core weapon was invalid." )
+ printt( "titan is alive? " + IsAlive( player ) )
+ return 5.0 // default
+ }
+
+ return GetTitanCoreDurationFromWeapon( weapon )
+}
+
+float function GetTitanCoreChargeTime( entity player )
+{
+ entity weapon = player.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+
+ if ( !IsValid( weapon ) )
+ {
+ printt( "WARNING: tried to get core charge time, but core weapon was invalid." )
+ printt( "titan is alive? " + IsAlive( player ) )
+ return 1.0 // default
+ }
+
+ return GetTitanCoreChargeTimeFromWeapon( weapon )
+}
+
+float function GetTitanCoreChargeTimeFromWeapon( entity weapon )
+{
+ return expect float( weapon.GetWeaponInfoFileKeyField( "chargeup_time" ) )
+}
+
+float function GetTitanCoreBuildTimeFromWeapon( entity weapon )
+{
+ return expect float( weapon.GetWeaponInfoFileKeyField( "core_build_time" ).tofloat() )
+}
+
+float function GetTitanCoreDurationFromWeapon( entity weapon )
+{
+ float coreDuration = weapon.GetCoreDuration()
+
+ entity player = weapon.GetWeaponOwner()
+ if ( IsValid( player ) && player.IsPlayer() )
+ {
+ if ( PlayerHasPassive( player, ePassives.PAS_MARATHON_CORE ) )
+ coreDuration *= TITAN_CORE_MARATHON_CORE_MULTIPLIER
+ }
+
+ return coreDuration
+}
+
+float function GetCoreBuildTime( entity titan )
+{
+ if ( titan.IsPlayer() )
+ titan = GetTitanFromPlayer( titan )
+
+ Assert( titan != null )
+
+ entity coreWeapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+
+ if ( !IsValid( coreWeapon ) )
+ {
+ //printt( "WARNING: tried to set build timer, but core weapon was invalid." )
+ //printt( "titan is alive? " + IsAlive( titan ) )
+ return 200.0 // default
+ }
+
+
+ return GetTitanCoreBuildTimeFromWeapon( coreWeapon )
+}
+
+string function GetCoreShortName( entity titan )
+{
+ entity coreWeapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+
+ if ( !IsValid( coreWeapon ) )
+ {
+ printt( "WARNING: tried to get core name, but core weapon was invalid." )
+ printt( "titan is alive? " + IsAlive( titan ) )
+ return "#HUD_READY"
+ }
+
+ string name = expect string( coreWeapon.GetWeaponInfoFileKeyField( "shortprintname" ) )
+ return name
+}
+
+string ornull function GetCoreOSConversationName( entity titan, string event )
+{
+ entity coreWeapon = titan.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+
+ if ( !IsValid( coreWeapon ) )
+ {
+ printt( "WARNING: tried to get core sound for " + event + ", but core weapon was invalid." )
+ printt( "titan is alive? " + IsAlive( titan ) )
+ return null
+ }
+
+ var alias = coreWeapon.GetWeaponInfoFileKeyField( "dialog_" + event )
+
+ if ( alias == null )
+ return null
+
+ expect string( alias )
+
+ return alias
+}
+
+entity function GetTitanFromPlayer( entity player )
+{
+ Assert( player.IsPlayer() )
+ if ( player.IsTitan() )
+ return player
+
+ return player.GetPetTitan()
+}
+
+int function GetNuclearPayload( entity player )
+{
+ if ( !GetDoomedState( player ) )
+ return 0
+
+ int payload = 0
+ if ( PlayerHasPassive( player, ePassives.PAS_NUCLEAR_CORE ) )
+ payload += 2
+
+ if ( PlayerHasPassive( player, ePassives.PAS_BUILD_UP_NUCLEAR_CORE ) )
+ payload += 1
+
+ return payload
+}
+
+entity function GetCloak( entity ent )
+{
+ return GetOffhand( ent, "mp_ability_cloak" )
+}
+
+entity function GetOffhand( entity ent, string classname )
+{
+ entity offhand = ent.GetOffhandWeapon( OFFHAND_LEFT )
+ if ( IsValid( offhand ) && offhand.GetWeaponClassName() == classname )
+ return offhand
+
+ offhand = ent.GetOffhandWeapon( OFFHAND_RIGHT )
+ if ( IsValid( offhand ) && offhand.GetWeaponClassName() == classname )
+ return offhand
+
+ return null
+}
+
+bool function IsCloaked( entity ent )
+{
+ return ent.IsCloaked( true ) //pass true to ignore flicker time -
+}
+
+float function TimeSpentInCurrentState()
+{
+ return Time() - expect float( level.nv.gameStateChangeTime )
+}
+
+float function DotToAngle( float dot )
+{
+ return acos( dot ) * RAD_TO_DEG
+}
+
+float function AngleToDot( float angle )
+{
+ return cos( angle * DEG_TO_RAD )
+}
+
+int function GetGameState()
+{
+ return expect int( GetServerVar( "gameState" ) )
+}
+
+bool function GamePlaying()
+{
+ return GetGameState() == eGameState.Playing
+}
+
+bool function GamePlayingOrSuddenDeath()
+{
+ int gameState = GetGameState()
+ return gameState == eGameState.Playing || gameState == eGameState.SuddenDeath
+}
+
+bool function IsOdd( int num )
+{
+ return ( num % 2 ) == 1
+}
+
+bool function IsEven( int num )
+{
+ return !IsOdd( num )
+}
+
+vector function VectorReflectionAcrossNormal( vector vec, vector normal )
+{
+ return ( vec - normal * ( 2 * DotProduct( vec, normal ) ) )
+}
+
+// Return an array of entities ordered from farthest to closest to the specified origin
+array<entity> function ArrayFarthest( array<entity> entArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistanceResults( entArray, origin )
+
+ allResults.sort( DistanceCompareFarthest )
+
+ array<entity> returnEntities
+
+ foreach ( result in allResults )
+ returnEntities.append( result.ent )
+
+ // the actual distances aren't returned
+ return returnEntities
+}
+
+// Return an array of vectors ordered from closest to furthest from the specified origin
+array<vector> function ArrayFarthestVector( array<vector> vecArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistanceResultsVector( vecArray, origin )
+
+ allResults.sort( DistanceCompareFarthest )
+
+ array<vector> returnVecs
+
+ foreach ( result in allResults )
+ returnVecs.append( result.origin )
+
+ return returnVecs
+}
+
+// Return an array of entities ordered from closest to furthest from the specified origin
+array<entity> function ArrayClosest( array<entity> entArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistanceResults( entArray, origin )
+
+ allResults.sort( DistanceCompareClosest )
+
+ array<entity> returnEntities
+
+ foreach ( result in allResults )
+ returnEntities.append( result.ent )
+
+ return returnEntities
+}
+
+// Return an array of vectors ordered from closest to furthest from the specified origin
+array<vector> function ArrayClosestVector( array<vector> vecArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistanceResultsVector( vecArray, origin )
+
+ allResults.sort( DistanceCompareClosest )
+
+ array<vector> returnVecs
+
+ foreach ( result in allResults )
+ returnVecs.append( result.origin )
+
+ return returnVecs
+}
+
+array<entity> function ArrayClosestWithinDistance( array<entity> entArray, vector origin, float maxDistance )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistanceResults( entArray, origin )
+ float maxDistSq = maxDistance * maxDistance
+
+ allResults.sort( DistanceCompareClosest )
+
+ array<entity> returnEntities
+
+ foreach ( result in allResults )
+ {
+ if ( result.distanceSqr > maxDistSq )
+ break
+
+ returnEntities.append( result.ent )
+ }
+
+ return returnEntities
+}
+
+array<vector> function ArrayClosestVectorWithinDistance( array<vector> vecArray, vector origin, float maxDistance )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistanceResultsVector( vecArray, origin )
+ float maxDistSq = maxDistance * maxDistance
+
+ allResults.sort( DistanceCompareClosest )
+
+ array<vector> returnVecs
+
+ foreach ( result in allResults )
+ {
+ if ( result.distanceSqr > maxDistSq )
+ break
+
+ returnVecs.append( result.origin )
+ }
+
+ return returnVecs
+}
+
+// Return an array of entities ordered from closest to furthest from the specified origin, ignoring z
+array<entity> function ArrayClosest2D( array<entity> entArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistance2DResults( entArray, origin )
+
+ allResults.sort( DistanceCompareClosest )
+
+ array<entity> returnEntities
+
+ foreach ( result in allResults )
+ returnEntities.append( result.ent )
+
+ return returnEntities
+}
+
+// Return an array of entities ordered from closest to furthest from the specified origin, ignoring z
+array<vector> function ArrayClosest2DVector( array<vector> entArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistance2DResultsVector( entArray, origin )
+
+ allResults.sort( DistanceCompareClosest )
+
+ array<vector> returnVecs
+
+ foreach ( result in allResults )
+ returnVecs.append( result.origin )
+
+ return returnVecs
+}
+
+array<entity> function ArrayClosest2DWithinDistance( array<entity> entArray, vector origin, float maxDistance )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistance2DResults( entArray, origin )
+ float maxDistSq = maxDistance * maxDistance
+
+ allResults.sort( DistanceCompareClosest )
+
+ array<entity> returnEntities
+
+ foreach ( result in allResults )
+ {
+ if ( result.distanceSqr > maxDistSq )
+ break
+
+ returnEntities.append( result.ent )
+ }
+
+ return returnEntities
+}
+
+// Return an array of entities ordered from closest to furthest from the specified origin, ignoring z
+array<vector> function ArrayClosest2DVectorWithinDistance( array<vector> entArray, vector origin, float maxDistance )
+{
+ array<ArrayDistanceEntry> allResults = ArrayDistance2DResultsVector( entArray, origin )
+ float maxDistSq = maxDistance * maxDistance
+
+ allResults.sort( DistanceCompareClosest )
+
+ array<vector> returnVecs
+
+ foreach ( result in allResults )
+ {
+ if ( result.distanceSqr > maxDistSq )
+ break
+
+ returnVecs.append( result.origin )
+ }
+
+ return returnVecs
+}
+
+bool function ArrayEntityWithinDistance( array<entity> entArray, vector origin, float distance )
+{
+ float distSq = distance * distance
+ foreach( entity ent in entArray )
+ {
+ if ( DistanceSqr( ent.GetOrigin(), origin ) <= distSq )
+ return true
+ }
+ return false
+}
+
+function TableRemove( Table, entry )
+{
+ Assert( typeof Table == "table" )
+
+ foreach ( index, tableEntry in Table )
+ {
+ if ( tableEntry == entry )
+ {
+ Table[ index ] = null
+ }
+ }
+}
+
+function TableInvert( Table )
+{
+ table invertedTable = {}
+ foreach ( key, value in Table )
+ invertedTable[ value ] <- key
+
+ return invertedTable
+}
+
+int function DistanceCompareClosest( ArrayDistanceEntry a, ArrayDistanceEntry b )
+{
+ if ( a.distanceSqr > b.distanceSqr )
+ return 1
+ else if ( a.distanceSqr < b.distanceSqr )
+ return -1
+
+ return 0;
+}
+
+int function DistanceCompareFarthest( ArrayDistanceEntry a, ArrayDistanceEntry b )
+{
+ if ( a.distanceSqr < b.distanceSqr )
+ return 1
+ else if ( a.distanceSqr > b.distanceSqr )
+ return -1
+
+ return 0;
+}
+
+array<ArrayDistanceEntry> function ArrayDistanceResults( array<entity> entArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults
+
+ foreach ( ent in entArray )
+ {
+ ArrayDistanceEntry entry
+
+ vector entOrigin = ent.GetOrigin()
+ if ( IsSpawner( ent ) )
+ {
+ var spawnKVs = ent.GetSpawnEntityKeyValues()
+ entOrigin = StringToVector( string( spawnKVs.origin ) )
+ }
+ entry.distanceSqr = DistanceSqr( entOrigin, origin )
+ entry.ent = ent
+ entry.origin = entOrigin
+
+ allResults.append( entry )
+ }
+
+ return allResults
+}
+
+array<ArrayDistanceEntry> function ArrayDistanceResultsVector( array<vector> vecArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults
+
+ foreach ( vec in vecArray )
+ {
+ ArrayDistanceEntry entry
+
+ entry.distanceSqr = DistanceSqr( vec, origin )
+ entry.ent = null
+ entry.origin = vec
+
+ allResults.append( entry )
+ }
+
+ return allResults
+}
+
+array<ArrayDistanceEntry> function ArrayDistance2DResults( array<entity> entArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults
+
+ foreach ( ent in entArray )
+ {
+ ArrayDistanceEntry entry
+
+ vector entOrigin = ent.GetOrigin()
+
+ entry.distanceSqr = Distance2DSqr( entOrigin, origin )
+ entry.ent = ent
+ entry.origin = entOrigin
+
+ allResults.append( entry )
+ }
+
+ return allResults
+}
+
+array<ArrayDistanceEntry> function ArrayDistance2DResultsVector( array<vector> vecArray, vector origin )
+{
+ array<ArrayDistanceEntry> allResults
+
+ foreach ( vec in vecArray )
+ {
+ ArrayDistanceEntry entry
+
+ entry.distanceSqr = Distance2DSqr( vec, origin )
+ entry.ent = null
+ entry.origin = vec
+
+ allResults.append( entry )
+ }
+
+ return allResults
+}
+
+GravityLandData function GetGravityLandData( vector startPos, vector parentVelocity, vector objectVelocity, float timeLimit, bool bDrawPath = false, float bDrawPathDuration = 0.0, array pathColor = [ 255, 255, 0 ] )
+{
+ GravityLandData returnData
+
+ Assert( timeLimit > 0 )
+
+ float MAX_TIME_ELAPSE = 6.0
+ float timeElapsePerTrace = 0.1
+
+ float sv_gravity = 750.0
+ float ent_gravity = 1.0
+ float gravityScale = 1.0
+
+ vector traceStart = startPos
+ vector traceEnd = traceStart
+ float traceFrac
+ int traceCount = 0
+
+ objectVelocity += parentVelocity
+
+ while( returnData.elapsedTime <= timeLimit )
+ {
+ objectVelocity.z -= ( ent_gravity * sv_gravity * timeElapsePerTrace * gravityScale )
+
+ traceEnd += objectVelocity * timeElapsePerTrace
+ returnData.points.append( traceEnd )
+ if ( bDrawPath )
+ DebugDrawLine( traceStart, traceEnd, pathColor[0], pathColor[1], pathColor[2], false, bDrawPathDuration )
+
+ traceFrac = TraceLineSimple( traceStart, traceEnd, null )
+ traceCount++
+ if ( traceFrac < 1.0 )
+ {
+ returnData.traceResults = TraceLine( traceStart, traceEnd, null, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE )
+ return returnData
+ }
+ traceStart = traceEnd
+ returnData.elapsedTime += timeElapsePerTrace
+ }
+
+ return returnData
+}
+
+float function GetPulseFrac( rate = 1, startTime = 0 )
+{
+ return (1 - cos( ( Time() - startTime ) * (rate * (2*PI)) )) / 2
+}
+
+bool function IsPetTitan( titan )
+{
+ Assert( titan.IsTitan() )
+
+ return titan.GetTitanSoul().GetBossPlayer() != null
+}
+
+vector function StringToVector( string vecString, string delimiter = " " )
+{
+ array<string> tokens = split( vecString, delimiter )
+
+ Assert( tokens.len() >= 3 )
+
+ return Vector( float( tokens[0] ), float( tokens[1] ), float( tokens[2] ) )
+}
+
+float function GetShieldHealthFrac( entity ent )
+{
+ if ( !IsAlive( ent ) )
+ return 0.0
+
+ if ( HasSoul( ent ) )
+ {
+ entity soul = ent.GetTitanSoul()
+ if ( IsValid( soul ) )
+ ent = soul
+ }
+
+ int shieldHealth = ent.GetShieldHealth()
+ int shieldMaxHealth = ent.GetShieldHealthMax()
+
+ if ( shieldMaxHealth == 0 )
+ return 0.0
+
+ return float( shieldHealth ) / float( shieldMaxHealth )
+}
+
+vector function HackGetDeltaToRef( vector origin, vector angles, entity ent, string anim )
+{
+ AnimRefPoint animStartPos = ent.Anim_GetStartForRefPoint( anim, origin, angles )
+
+ vector delta = origin - animStartPos.origin
+ return origin + delta
+}
+
+vector function HackGetDeltaToRefOnPlane( vector origin, vector angles, entity ent, string anim, vector up )
+{
+ AnimRefPoint animStartPos = ent.Anim_GetStartForRefPoint( anim, origin, angles )
+
+ vector delta = origin - animStartPos.origin
+ vector nDelta = Normalize( delta )
+ vector xProd = CrossProduct( nDelta, up )
+ vector G = CrossProduct( up, xProd )
+ vector planarDelta = G * DotProduct( delta, G )
+ vector P = origin + planarDelta
+
+// DebugDrawLine( origin + delta, origin, 255, 0, 0, true, 1.0 )
+// DebugDrawLine( P, origin, 0,255, 100, true, 1.0 )
+
+ return P
+}
+
+TraceResults function GetViewTrace( entity ent )
+{
+ vector traceStart = ent.EyePosition()
+ vector traceEnd = traceStart + (ent.GetPlayerOrNPCViewVector() * 56756) // longest possible trace given our map size limits
+ array<entity> ignoreEnts = [ ent ]
+
+ return TraceLine( traceStart, traceEnd, ignoreEnts, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE )
+}
+
+function GetModSourceID( modString )
+{
+ foreach ( name, id in getconsttable().eModSourceId )
+ {
+ if ( string( name ) == modString )
+ return id
+ }
+
+ return null
+}
+
+void function ArrayRemoveDead( array<entity> entArray )
+{
+ for ( int i = entArray.len() - 1; i >= 0; i-- )
+ {
+ if ( !IsAlive( entArray[ i ] ) )
+ entArray.remove( i )
+ }
+}
+
+array<entity> function GetSortedPlayers( IntFromEntityCompare compareFunc, int team )
+{
+ array<entity> players
+
+ if ( team )
+ players = GetPlayerArrayOfTeam( team )
+ else
+ players = GetPlayerArray()
+
+ players.sort( compareFunc )
+
+ return players
+}
+
+
+// Sorts by kills and resolves ties in this order: fewest deaths, most titan kills, most assists
+int function CompareKills( entity a, entity b )
+{
+ int aVal = a.GetPlayerGameStat( PGS_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ aVal = a.GetPlayerGameStat( PGS_DEATHS )
+ bVal = b.GetPlayerGameStat( PGS_DEATHS )
+
+ if ( aVal > bVal )
+ return 1
+ else if ( aVal < bVal )
+ return -1
+
+ aVal = a.GetPlayerGameStat( PGS_TITAN_KILLS )
+ bVal = b.GetPlayerGameStat( PGS_TITAN_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ aVal = a.GetPlayerGameStat( PGS_ASSISTS )
+ bVal = b.GetPlayerGameStat( PGS_ASSISTS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ return 0
+}
+
+// Sorts by kills and resolves ties in this order: fewest deaths, most titan kills, most assists
+int function CompareAssaultScore( entity a, entity b )
+{
+ int aVal = a.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+ int bVal = b.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ return 0
+}
+
+int function CompareScore( entity a, entity b )
+{
+ int aVal = a.GetPlayerGameStat( PGS_SCORE )
+ int bVal = b.GetPlayerGameStat( PGS_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ return 0
+}
+
+int function CompareAssault( entity a, entity b )
+{
+ int aVal = a.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+ int bVal = b.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ return 0
+}
+
+int function CompareDefense( entity a, entity b )
+{
+ int aVal = a.GetPlayerGameStat( PGS_DEFENSE_SCORE )
+ int bVal = b.GetPlayerGameStat( PGS_DEFENSE_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ return 0
+}
+
+int function CompareLTS( entity a, entity b )
+{
+ int result = CompareTitanKills( a, b )
+ if ( result != 0 )
+ return result
+
+ int aVal = a.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+ int bVal = b.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ return 0
+}
+
+int function CompareCP( entity a, entity b )
+{
+ // Capture Point sorting. Sort priority = assault + defense > pilot kills > titan kills > death
+
+ {
+ int aVal = a.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+ int bVal = b.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+
+ aVal += a.GetPlayerGameStat( PGS_DEFENSE_SCORE )
+ bVal += b.GetPlayerGameStat( PGS_DEFENSE_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+ }
+
+ // 3) Pilot Kills
+ {
+ int aVal = a.GetPlayerGameStat( PGS_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+ }
+
+ // 3) Titan Kills
+ {
+ int aVal = a.GetPlayerGameStat( PGS_TITAN_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_TITAN_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+ }
+
+ // 4) Deaths
+ {
+ int aVal = a.GetPlayerGameStat( PGS_DEATHS )
+ int bVal = b.GetPlayerGameStat( PGS_DEATHS )
+
+ if ( aVal < bVal )
+ return -1
+ else if ( aVal > bVal )
+ return 1
+ }
+
+ return 0
+}
+
+
+int function CompareCTF( entity a, entity b )
+{
+ // Capture the flag sorting. Sort priority = flag captures > flag returns > pilot kills > titan kills > death
+
+ // 1) Flag Captures
+ int result = CompareAssault( a, b )
+ if ( result != 0 )
+ return result
+
+ // 2) Flag Returns
+ result = CompareDefense( a, b )
+ if ( result != 0 )
+ return result
+
+ // 3) Pilot Kills
+ int aVal = a.GetPlayerGameStat( PGS_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ // 3) Titan Kills
+ aVal = a.GetPlayerGameStat( PGS_TITAN_KILLS )
+ bVal = b.GetPlayerGameStat( PGS_TITAN_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ // 4) Deaths
+ aVal = a.GetPlayerGameStat( PGS_DEATHS )
+ bVal = b.GetPlayerGameStat( PGS_DEATHS )
+
+ if ( aVal < bVal )
+ return -1
+ else if ( aVal > bVal )
+ return 1
+
+ return 0
+}
+
+int function CompareSpeedball( entity a, entity b )
+{
+ // Capture the flag sorting. Sort priority = pilot kills > flag captures > death
+
+ // 1) Pilot Kills
+ int aVal = a.GetPlayerGameStat( PGS_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ // 2) Flag Captures
+ int result = CompareAssault( a, b )
+ if ( result != 0 )
+ return result
+
+ // 3) Deaths
+ aVal = a.GetPlayerGameStat( PGS_DEATHS )
+ bVal = b.GetPlayerGameStat( PGS_DEATHS )
+
+ if ( aVal < bVal )
+ return -1
+ else if ( aVal > bVal )
+ return 1
+
+ return 0
+}
+
+int function CompareMFD( entity a, entity b )
+{
+ // 1) Marks Killed
+ int result = CompareAssault( a, b )
+ if ( result != 0 )
+ return result
+
+ // 2) Marks Outlasted
+ result = CompareDefense( a, b )
+ if ( result != 0 )
+ return result
+
+ // 3) Pilot Kills
+ int aVal = a.GetPlayerGameStat( PGS_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ // 4) Titan Kills
+ aVal = a.GetPlayerGameStat( PGS_TITAN_KILLS )
+ bVal = b.GetPlayerGameStat( PGS_TITAN_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ // 5) Deaths
+ aVal = a.GetPlayerGameStat( PGS_DEATHS )
+ bVal = b.GetPlayerGameStat( PGS_DEATHS )
+
+ if ( aVal < bVal )
+ return -1
+ else if ( aVal > bVal )
+ return 1
+
+ return 0
+}
+
+int function CompareScavenger( entity a, entity b )
+{
+ // 1) Ore Captured
+ int result = CompareAssault( a, b )
+ if ( result != 0 )
+ return result
+
+ // 2) Pilot Kills
+ int aVal = a.GetPlayerGameStat( PGS_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ // 3) Titan Kills
+ aVal = a.GetPlayerGameStat( PGS_TITAN_KILLS )
+ bVal = b.GetPlayerGameStat( PGS_TITAN_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ // 4) Deaths
+ aVal = a.GetPlayerGameStat( PGS_DEATHS )
+ bVal = b.GetPlayerGameStat( PGS_DEATHS )
+
+ if ( aVal < bVal )
+ return -1
+ else if ( aVal > bVal )
+ return 1
+
+ return 0
+}
+
+int function CompareFW( entity a, entity b )
+{
+ // Capture Point sorting. Sort priority = assault + defense > pilot kills > titan kills > death
+
+ {
+ int aVal = a.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+ int bVal = b.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+
+ aVal += a.GetPlayerGameStat( PGS_DEFENSE_SCORE )
+ bVal += b.GetPlayerGameStat( PGS_DEFENSE_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+ }
+
+ // 3) Pilot Kills
+ {
+ int aVal = a.GetPlayerGameStat( PGS_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+ }
+
+ // 3) Titan Kills
+ {
+ int aVal = a.GetPlayerGameStat( PGS_TITAN_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_TITAN_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+ }
+
+ // 4) Deaths
+ {
+ int aVal = a.GetPlayerGameStat( PGS_DEATHS )
+ int bVal = b.GetPlayerGameStat( PGS_DEATHS )
+
+ if ( aVal < bVal )
+ return -1
+ else if ( aVal > bVal )
+ return 1
+ }
+
+ return 0
+}
+
+int function CompareHunter( entity a, entity b )
+{
+ // Capture Point sorting. Sort priority = assault + defense > pilot kills > titan kills > death
+
+ {
+ int aVal = a.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+ int bVal = b.GetPlayerGameStat( PGS_ASSAULT_SCORE )
+
+ aVal += a.GetPlayerGameStat( PGS_DEFENSE_SCORE )
+ bVal += b.GetPlayerGameStat( PGS_DEFENSE_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+ }
+
+ // 3) Pilot Kills
+ {
+ int aVal = a.GetPlayerGameStat( PGS_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+ }
+
+ // 3) Titan Kills
+ {
+ int aVal = a.GetPlayerGameStat( PGS_TITAN_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_TITAN_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+ }
+
+ // 4) Deaths
+ {
+ int aVal = a.GetPlayerGameStat( PGS_DEATHS )
+ int bVal = b.GetPlayerGameStat( PGS_DEATHS )
+
+ if ( aVal < bVal )
+ return -1
+ else if ( aVal > bVal )
+ return 1
+ }
+
+ return 0
+}
+
+// Sorts by kills, deaths and then cash
+int function CompareATCOOP( entity a, entity b )
+{
+ int aVal = a.GetPlayerGameStat( PGS_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ aVal = a.GetPlayerGameStat( PGS_DEATHS )
+ bVal = b.GetPlayerGameStat( PGS_DEATHS )
+
+ if ( aVal > bVal )
+ return 1
+ else if ( aVal < bVal )
+ return -1
+
+ aVal = a.GetPlayerGameStat( PGS_SCORE )
+ bVal = b.GetPlayerGameStat( PGS_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ return 0
+}
+
+int function CompareFD( entity a, entity b )
+{
+ int aVal = a.GetPlayerGameStat( PGS_DETONATION_SCORE )
+ int bVal = b.GetPlayerGameStat( PGS_DETONATION_SCORE )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ return 0
+}
+
+int function CompareTitanKills( entity a, entity b )
+{
+ int aVal = a.GetPlayerGameStat( PGS_TITAN_KILLS )
+ int bVal = b.GetPlayerGameStat( PGS_TITAN_KILLS )
+
+ if ( aVal < bVal )
+ return 1
+ else if ( aVal > bVal )
+ return -1
+
+ return 0
+}
+
+bool function TitanEjectIsDisabled()
+{
+ return GetGlobalNetBool( "titanEjectEnabled" ) == false
+}
+
+bool function IsHitEffectiveVsTitan( entity victim, int damageType )
+{
+ Assert( victim.IsTitan() )
+
+ if ( victim.IsPlayer() )
+ {
+ if ( PlayerHasPassive( victim, ePassives.PAS_BERSERKER ) )
+ return false
+ }
+
+ if ( !( damageType & DF_CRITICAL ) && ( damageType & DF_BULLET || damageType & DF_MAX_RANGE ) )
+ return false
+
+ return true
+}
+
+bool function IsHitEffectiveVsNonTitan( entity victim, int damageType )
+{
+ if ( damageType & DF_BULLET || damageType & DF_MAX_RANGE )
+ return false;
+
+ return true
+}
+
+bool function IsPilot( entity ent )
+{
+ if ( !IsValid( ent ) )
+ return false
+
+ if ( !ent.IsPlayer() )
+ return false
+
+ if ( ent.IsTitan() )
+ return false
+
+ return true
+}
+
+bool function IsPilotDecoy( entity ent )
+{
+ if ( !IsValid( ent ) )
+ return false
+
+ if ( ent.GetClassName() != "player_decoy" )
+ return false
+
+ return true
+}
+
+string function HardpointIDToString( int id )
+{
+ array<string> hardpointIDString = [ "a", "b", "c" ]
+
+ Assert( id >= 0 && id < hardpointIDString.len() )
+
+ return hardpointIDString[ id ]
+}
+
+string function Dev_TeamIDToString( id )
+{
+ if ( id == TEAM_IMC )
+ return "IMC"
+ if ( id == TEAM_MILITIA )
+ return "MIL"
+
+ return "UNASSIGNED/UNKNOWN TEAM NAME"
+}
+
+array<entity> function ArrayWithin( array<entity> Array, vector origin, float maxDist )
+{
+ float maxDistSqr = maxDist * maxDist
+
+ array<entity> resultArray = []
+ foreach ( ent in Array )
+ {
+ float distSqr = DistanceSqr( origin, ent.GetOrigin() )
+ if ( distSqr <= maxDistSqr )
+ resultArray.append( ent )
+ }
+ return resultArray
+}
+
+function GetTitanChassis( entity titan )
+{
+ if ( !("titanChassis" in titan.s ) )
+ {
+ if ( HasSoul( titan ) )
+ {
+ entity soul = titan.GetTitanSoul()
+ titan.s.titanChassis <- GetSoulTitanSubClass( soul )
+ }
+ else
+ {
+ return "Invalid Chassis"
+ }
+ }
+
+ return titan.s.titanChassis
+}
+
+vector function ClampVectorToCube( vector vecStart, vector vec, vector cubeOrigin, float cubeSize )
+{
+ float halfCubeSize = cubeSize * 0.5
+ vector cubeMins = < -halfCubeSize, -halfCubeSize, -halfCubeSize >
+ vector cubeMaxs = < halfCubeSize, halfCubeSize, halfCubeSize >
+
+ return ClampVectorToBox( vecStart, vec, cubeOrigin, cubeMins, cubeMaxs )
+}
+
+vector function ClampVectorToBox( vector vecStart, vector vec, vector cubeOrigin, vector cubeMins, vector cubeMaxs )
+{
+ float smallestClampScale = 1.0
+ vector vecEnd = vecStart + vec
+
+ smallestClampScale = ClampVectorComponentToCubeMax( cubeOrigin.x, cubeMaxs.x, vecStart.x, vecEnd.x, vec.x, smallestClampScale )
+ smallestClampScale = ClampVectorComponentToCubeMax( cubeOrigin.y, cubeMaxs.y, vecStart.y, vecEnd.y, vec.y, smallestClampScale )
+ smallestClampScale = ClampVectorComponentToCubeMax( cubeOrigin.z, cubeMaxs.z, vecStart.z, vecEnd.z, vec.z, smallestClampScale )
+ smallestClampScale = ClampVectorComponentToCubeMin( cubeOrigin.x, cubeMins.x, vecStart.x, vecEnd.x, vec.x, smallestClampScale )
+ smallestClampScale = ClampVectorComponentToCubeMin( cubeOrigin.y, cubeMins.y, vecStart.y, vecEnd.y, vec.y, smallestClampScale )
+ smallestClampScale = ClampVectorComponentToCubeMin( cubeOrigin.z, cubeMins.z, vecStart.z, vecEnd.z, vec.z, smallestClampScale )
+
+ return vec * smallestClampScale
+}
+
+float function ClampVectorComponentToCubeMax( float cubeOrigin, float cubeSize, float vecStart, float vecEnd, float vec, float smallestClampScale )
+{
+ float max = cubeOrigin + cubeSize
+ float clearance = fabs( vecStart - max )
+ if ( vecEnd > max )
+ {
+ float scale = fabs( clearance / ( ( vecStart + vec ) - vecStart ) )
+ if ( scale > 0 && scale < smallestClampScale )
+ return scale
+ }
+
+ return smallestClampScale
+}
+
+float function ClampVectorComponentToCubeMin( float cubeOrigin, float cubeSize, float vecStart, float vecEnd, float vec, float smallestClampScale )
+{
+ float min = cubeOrigin - cubeSize
+ float clearance = fabs( min - vecStart )
+ if ( vecEnd < min )
+ {
+ float scale = fabs( clearance / ( ( vecStart + vec ) - vecStart ) )
+ if ( scale > 0 && scale < smallestClampScale )
+ return scale
+ }
+
+ return smallestClampScale
+}
+
+bool function PointInCapsule( vector vecBottom, vector vecTop, float radius, vector point )
+{
+ return GetDistanceFromLineSegment( vecBottom, vecTop, point ) <= radius
+}
+
+bool function PointInCylinder( vector vecBottom, vector vecTop, float radius, vector point )
+{
+ if ( GetDistanceFromLineSegment( vecBottom, vecTop, point ) > radius )
+ return false
+
+ vector bottomVec = Normalize( vecTop - vecBottom )
+ vector pointToBottom = Normalize( point - vecBottom )
+
+ vector topVec = Normalize( vecBottom - vecTop )
+ vector pointToTop = Normalize( point - vecTop )
+
+ if ( DotProduct( bottomVec, pointToBottom ) < 0 )
+ return false
+
+ if ( DotProduct( topVec, pointToTop ) < 0.0 )
+ return false
+
+ return true
+}
+
+float function AngleDiff( float ang, float targetAng )
+{
+ float delta = ( targetAng - ang ) % 360.0
+ if ( targetAng > ang )
+ {
+ if ( delta >= 180.0 )
+ delta -= 360.0;
+ }
+ else
+ {
+ if ( delta <= -180.0 )
+ delta += 360.0;
+ }
+ return delta
+}
+
+
+float function ClampAngle( float ang )
+{
+ while( ang > 360 )
+ ang -= 360
+ while( ang < 0 )
+ ang += 360
+ return ang
+}
+
+float function ClampAngle180( float ang )
+{
+ while( ang > 180 )
+ ang -= 180
+ while( ang < -180 )
+ ang += 180
+ return ang
+}
+
+vector function ShortestRotation( vector ang, vector targetAng )
+{
+ return Vector( AngleDiff( ang.x, targetAng.x ), AngleDiff( ang.y, targetAng.y ), AngleDiff( ang.z, targetAng.z ) )
+}
+
+int function GetWinningTeam()
+{
+ if ( level.nv.winningTeam != null )
+ return expect int( level.nv.winningTeam )
+
+ if ( IsFFAGame() )
+ return GetWinningTeam_FFA()
+
+ if ( IsRoundBased() )
+ {
+ if ( GameRules_GetTeamScore2( TEAM_IMC ) > GameRules_GetTeamScore2( TEAM_MILITIA ) )
+ return TEAM_IMC
+
+ if ( GameRules_GetTeamScore2( TEAM_MILITIA ) > GameRules_GetTeamScore2( TEAM_IMC ) )
+ return TEAM_MILITIA
+ }
+ else
+ {
+ if ( GameRules_GetTeamScore( TEAM_IMC ) > GameRules_GetTeamScore( TEAM_MILITIA ) )
+ return TEAM_IMC
+
+ if ( GameRules_GetTeamScore( TEAM_MILITIA ) > GameRules_GetTeamScore( TEAM_IMC ) )
+ return TEAM_MILITIA
+ }
+
+ return TEAM_UNASSIGNED
+}
+
+int function GetWinningTeam_FFA()
+{
+ if ( level.nv.winningTeam != null )
+ return expect int( level.nv.winningTeam )
+
+ int maxScore = 0
+ int playerTeam
+ int currentScore
+ int winningTeam = TEAM_UNASSIGNED
+
+ foreach( player in GetPlayerArray() )
+ {
+ playerTeam = player.GetTeam()
+ if ( IsRoundBased() )
+ currentScore = GameRules_GetTeamScore2( playerTeam )
+ else
+ currentScore = GameRules_GetTeamScore( playerTeam )
+
+ if ( currentScore == maxScore) //Treat multiple teams as having the same score as no team winning
+ winningTeam = TEAM_UNASSIGNED
+
+ if ( currentScore > maxScore )
+ {
+ maxScore = currentScore
+ winningTeam = playerTeam
+ }
+ }
+
+ return winningTeam
+
+}
+
+void function EmitSkyboxSoundAtPosition( vector positionInSkybox, string sound, float skyboxScale = 0.001, bool clamp = false )
+{
+ if ( IsServer() )
+ clamp = true // sounds cannot play outside 16k limit on server
+ vector position = SkyboxToWorldPosition( positionInSkybox, skyboxScale, clamp )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, position, sound )
+}
+
+vector function SkyboxToWorldPosition( vector positionInSkybox, float skyboxScale = 0.001, bool clamp = true )
+{
+ Assert( skyboxScale > 0 )
+ Assert( "skyboxCamOrigin" in level )
+
+ vector position = Vector( 0.0, 0.0, 0.0 )
+ vector skyOrigin = expect vector( level.skyboxCamOrigin )
+
+ #if CLIENT
+ position = ( positionInSkybox - skyOrigin ) * ( 1.0 / skyboxScale )
+
+ if ( clamp )
+ {
+ entity localViewPlayer = GetLocalViewPlayer()
+ Assert( localViewPlayer )
+ vector localViewPlayerOrg = localViewPlayer.GetOrigin()
+
+ position = localViewPlayerOrg + ClampVectorToCube( localViewPlayerOrg, position - localViewPlayerOrg, Vector( 0.0, 0.0, 0.0 ), 32000.0 )
+ }
+ #else
+ position = ( positionInSkybox - skyOrigin ) * ( 1.0 / skyboxScale )
+
+ if ( clamp )
+ position = ClampVectorToCube( Vector( 0.0, 0.0, 0.0 ), position, Vector( 0.0, 0.0, 0.0 ), 32000.0 )
+ #endif // CLIENT
+
+ return position
+}
+
+void function FadeOutSoundOnEntityAfterDelay( entity ent, string soundAlias, float delay, float fadeTime )
+{
+ if ( !IsValid( ent ) )
+ return
+
+ ent.EndSignal( "OnDestroy" )
+ wait delay
+ FadeOutSoundOnEntity( ent, soundAlias, fadeTime )
+}
+
+function GetRandomKeyFromWeightedTable( Table )
+{
+ local weightTotal = 0.0
+ foreach ( key, value in Table )
+ {
+ weightTotal += value
+ }
+
+ local randomValue = RandomFloat( weightTotal )
+
+ foreach ( key, value in Table )
+ {
+ if ( randomValue <= weightTotal && randomValue >= weightTotal - value)
+ return key
+ weightTotal -= value
+ }
+}
+
+bool function IsMatchOver()
+{
+ if ( IsRoundBased() && level.nv.gameEndTime )
+ return true
+ else if ( !IsRoundBased() && level.nv.gameEndTime && Time() > level.nv.gameEndTime )
+ return true
+
+ return false
+}
+
+bool function IsScoringNonStandard()
+{
+ return expect bool( level.nv.nonStandardScoring )
+}
+
+bool function IsRoundBased()
+{
+ return expect bool( level.nv.roundBased )
+}
+
+int function GetRoundsPlayed()
+{
+ return expect int( level.nv.roundsPlayed )
+}
+
+bool function IsEliminationBased()
+{
+ return Riff_EliminationMode() != eEliminationMode.Default
+}
+
+bool function IsPilotEliminationBased()
+{
+ return ( Riff_EliminationMode() == eEliminationMode.Pilots || Riff_EliminationMode() == eEliminationMode.PilotsTitans )
+}
+
+bool function IsTitanEliminationBased()
+{
+ return ( Riff_EliminationMode() == eEliminationMode.Titans || Riff_EliminationMode() == eEliminationMode.PilotsTitans )
+}
+
+bool function IsSingleTeamMode()
+{
+ return ( 1 == GetCurrentPlaylistVarInt( "max_teams", 2 ) )
+}
+
+void function __WarpInEffectShared( vector origin, vector angles, string sfx, float preWaitOverride = -1.0 )
+{
+ float preWait = 2.0
+ float sfxWait = 0.1
+ float totalTime = WARPINFXTIME
+
+ if ( sfx == "" )
+ sfx = "dropship_warpin"
+
+ if ( preWaitOverride >= 0.0 )
+ wait preWaitOverride
+ else
+ wait preWait //this needs to go and the const for warpin fx time needs to change - but not this game - the intro system is too dependent on it
+
+ #if CLIENT
+ int fxIndex = GetParticleSystemIndex( FX_GUNSHIP_CRASH_EXPLOSION_ENTRANCE )
+ StartParticleEffectInWorld( fxIndex, origin, angles )
+ #else
+ entity fx = PlayFX( FX_GUNSHIP_CRASH_EXPLOSION_ENTRANCE, origin, angles )
+ fx.FXEnableRenderAlways()
+ fx.DisableHibernation()
+ #endif // CLIENT
+
+ wait sfxWait
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, sfx )
+
+ wait totalTime - preWait - sfxWait
+}
+
+void function __WarpOutEffectShared( entity dropship )
+{
+ int attach = dropship.LookupAttachment( "origin" )
+ vector origin = dropship.GetAttachmentOrigin( attach )
+ vector angles = dropship.GetAttachmentAngles( attach )
+
+ #if CLIENT
+ int fxIndex = GetParticleSystemIndex( FX_GUNSHIP_CRASH_EXPLOSION_EXIT )
+ StartParticleEffectInWorld( fxIndex, origin, angles )
+ #else
+ entity fx = PlayFX( FX_GUNSHIP_CRASH_EXPLOSION_EXIT, origin, angles )
+ fx.FXEnableRenderAlways()
+ fx.DisableHibernation()
+ #endif // CLIENT
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, "dropship_warpout" )
+}
+
+bool function IsSwitchSidesBased()
+{
+ return (level.nv.switchedSides != null)
+}
+
+int function HasSwitchedSides() //This returns an int instead of a bool! Should rewrite
+{
+ return expect int( level.nv.switchedSides )
+}
+
+bool function IsFirstRoundAfterSwitchingSides()
+{
+ if ( !IsSwitchSidesBased() )
+ return false
+
+ if ( IsRoundBased() )
+ return level.nv.switchedSides > 0 && GetRoundsPlayed() == level.nv.switchedSides
+ else
+ return level.nv.switchedSides > 0
+
+ unreachable
+}
+
+void function CamBlendFov( entity cam, float oldFov, float newFov, float transTime, float transAccel, float transDecel )
+{
+ if ( !IsValid( cam ) )
+ return
+
+ cam.EndSignal( "OnDestroy" )
+
+ float currentTime = Time()
+ float startTime = currentTime
+ float endTime = startTime + transTime
+
+ while ( endTime > currentTime )
+ {
+ float interp = Interpolate( startTime, endTime - startTime, transAccel, transDecel )
+ cam.SetFOV( GraphCapped( interp, 0.0, 1.0, oldFov, newFov ) )
+ wait( 0.0 )
+ currentTime = Time()
+ }
+}
+
+void function CamFollowEnt( entity cam, entity ent, float duration, vector offset = <0.0, 0.0, 0.0>, string attachment = "", bool isInSkybox = false )
+{
+ if ( !IsValid( cam ) )
+ return
+
+ cam.EndSignal( "OnDestroy" )
+
+ vector camOrg = Vector( 0.0, 0.0, 0.0 )
+
+ vector targetPos = Vector( 0.0, 0.0, 0.0 )
+ float currentTime = Time()
+ float startTime = currentTime
+ float endTime = startTime + duration
+ vector diff = Vector( 0.0, 0.0, 0.0 )
+ int attachID = ent.LookupAttachment( attachment )
+
+ while ( endTime > currentTime )
+ {
+ camOrg = cam.GetOrigin()
+
+ if ( attachID <= 0 )
+ targetPos = ent.GetOrigin()
+ else
+ targetPos = ent.GetAttachmentOrigin( attachID )
+
+ if ( isInSkybox )
+ targetPos = SkyboxToWorldPosition( targetPos )
+ diff = ( targetPos + offset ) - camOrg
+
+ cam.SetAngles( VectorToAngles( diff ) )
+
+ wait( 0.0 )
+
+ currentTime = Time()
+ }
+}
+
+void function CamFacePos( entity cam, vector pos, float duration )
+{
+ if ( !IsValid( cam ) )
+ return
+
+ cam.EndSignal( "OnDestroy" )
+
+ float currentTime = Time()
+ float startTime = currentTime
+ float endTime = startTime + duration
+ vector diff = Vector( 0.0, 0.0, 0.0 )
+
+ while ( endTime > currentTime )
+ {
+ diff = pos - cam.GetOrigin()
+
+ cam.SetAngles( VectorToAngles( diff ) )
+
+ wait( 0.0 )
+
+ currentTime = Time()
+ }
+}
+
+void function CamBlendFromFollowToAng( entity cam, entity ent, vector endAng, float transTime, float transAccel, float transDecel )
+{
+ if ( !IsValid( cam ) )
+ return
+
+ cam.EndSignal( "OnDestroy" )
+
+ vector camOrg = cam.GetOrigin()
+
+ float currentTime = Time()
+ float startTime = currentTime
+ float endTime = startTime + transTime
+
+ while ( endTime > currentTime )
+ {
+ vector diff = ent.GetOrigin() - camOrg
+ vector anglesToEnt = VectorToAngles( diff )
+
+ float frac = Interpolate( startTime, endTime - startTime, transAccel, transDecel )
+
+ vector newAngs = anglesToEnt + ShortestRotation( anglesToEnt, endAng ) * frac
+
+ cam.SetAngles( newAngs )
+
+ wait( 0.0 )
+
+ currentTime = Time()
+ }
+}
+
+void function CamBlendFromPosToPos( entity cam, vector startPos, vector endPos, float transTime, float transAccel, float transDecel )
+{
+ if ( !IsValid( cam ) )
+ return
+
+ cam.EndSignal( "OnDestroy" )
+
+ float currentTime = Time()
+ float startTime = currentTime
+ float endTime = startTime + transTime
+ vector diff = endPos - startPos
+
+ while ( endTime > currentTime )
+ {
+ float frac = Interpolate( startTime, endTime - startTime, transAccel, transDecel )
+
+ vector newAngs = startPos + diff * frac
+
+ cam.SetOrigin( newAngs )
+
+ wait( 0.0 )
+
+ currentTime = Time()
+ }
+}
+
+void function CamBlendFromAngToAng( entity cam, vector startAng, vector endAng, float transTime, float transAccel, float transDecel )
+{
+ if ( !IsValid( cam ) )
+ return
+
+ cam.EndSignal( "OnDestroy" )
+
+ float currentTime = Time()
+ float startTime = currentTime
+ float endTime = startTime + transTime
+
+ while ( endTime > currentTime )
+ {
+ float frac = Interpolate( startTime, endTime - startTime, transAccel, transDecel )
+
+ vector newAngs = startAng + ShortestRotation( startAng, endAng ) * frac
+
+ cam.SetAngles( newAngs )
+
+ wait( 0.0 )
+
+ currentTime = Time()
+ }
+}
+
+int function AddBitMask( int bitsExisting, int bitsToAdd )
+{
+ return bitsExisting | bitsToAdd
+}
+
+int function RemoveBitMask( int bitsExisting, int bitsToRemove )
+{
+ return bitsExisting & ( ~bitsToRemove )
+}
+
+bool function HasBitMask( int bitsExisting, int bitsToCheck )
+{
+ int bitsCommon = bitsExisting & bitsToCheck
+ return bitsCommon == bitsToCheck
+}
+
+float function GetDeathCamLength( entity player )
+{
+ if ( !GamePlayingOrSuddenDeath() )
+ return DEATHCAM_TIME_SHORT
+ else
+ return DEATHCAM_TIME
+
+ unreachable
+}
+
+float function GetRespawnButtonCamTime( player )
+{
+ if ( !GamePlayingOrSuddenDeath() )
+ return DEATHCAM_TIME_SHORT + RESPAWN_BUTTON_BUFFER
+ else
+ return DEATHCAM_TIME + RESPAWN_BUTTON_BUFFER
+
+ unreachable
+}
+
+float function GetKillReplayAfterTime( player )
+{
+ if ( IsSingleplayer() )
+ return 4.0
+
+ if ( !GamePlayingOrSuddenDeath() )
+ return KILL_REPLAY_AFTER_KILL_TIME_SHORT
+
+ return KILL_REPLAY_AFTER_KILL_TIME
+}
+
+function IntroPreviewOn()
+{
+ local bugnum = GetBugReproNum()
+ switch( bugnum )
+ {
+ case 1337:
+ case 13371:
+ case 13372:
+ case 13373:
+ case 1338:
+ case 13381:
+ case 13382:
+ case 13383:
+ return bugnum
+
+ default:
+ return null
+ }
+}
+
+bool function EntHasModelSet( entity ent )
+{
+ asset modelName = ent.GetModelName()
+
+ if ( modelName == $"" || modelName == $"?" )
+ return false
+
+ return true
+}
+
+string function GenerateTitanOSAlias( entity player, string aliasSuffix )
+{
+ //HACK: Temp fix for blocker bug. Fixing correctly next.
+ if ( IsSingleplayer() )
+ {
+ return "diag_gs_titanBt_" + aliasSuffix
+ }
+ else
+ {
+ entity titan
+ if ( player.IsTitan() )
+ titan = player
+ else
+ titan = player.GetPetTitan()
+
+ Assert( IsValid( titan ) )
+ string titanCharacterName = GetTitanCharacterName( titan )
+ string primeTitanString = ""
+
+ if ( IsTitanPrimeTitan( titan ) )
+ primeTitanString = "_prime"
+
+ string modifiedAlias = "diag_gs_titan" + titanCharacterName + primeTitanString + "_" + aliasSuffix
+ return modifiedAlias
+ }
+ unreachable
+}
+
+void function AddCallback_OnUseEntity( entity ent, callbackFunc )
+{
+ AssertParameters( callbackFunc, 2, "ent, player" )
+
+ if ( !( "onUseEntityCallbacks" in ent.s ) )
+ ent.s.onUseEntityCallbacks <- []
+
+ Assert( !ent.s.onUseEntityCallbacks.contains( callbackFunc ), "Already added " + FunctionToString( callbackFunc ) + " with AddCalback_OnUseEntity" )
+ ent.s.onUseEntityCallbacks.append( callbackFunc )
+}
+
+void function SetWaveSpawnType( int spawnType )
+{
+ shGlobal.waveSpawnType = spawnType
+}
+
+int function GetWaveSpawnType()
+{
+ return shGlobal.waveSpawnType
+}
+
+void function SetWaveSpawnInterval( float interval )
+{
+ shGlobal.waveSpawnInterval = interval
+}
+
+float function GetWaveSpawnInterval()
+{
+ return shGlobal.waveSpawnInterval
+}
+
+bool function IsArcTitan( entity npc )
+{
+ return npc.GetAISettingsName() == "npc_titan_arc"
+}
+
+bool function IsNukeTitan( entity npc )
+{
+ return npc.GetAISettingsName() == "npc_titan_nuke"
+}
+
+bool function IsMortarTitan( entity npc )
+{
+ return npc.GetAISettingsName() == "npc_titan_mortar"
+}
+
+bool function IsFragDrone( entity npc )
+{
+ #if SERVER
+ return npc.GetClassName() == "npc_frag_drone"
+ #endif
+
+ #if CLIENT
+ return npc.GetSignifierName() == "npc_frag_drone"
+ #endif
+}
+
+bool function IsSniperSpectre( entity npc )
+{
+ return false
+}
+
+bool function IsVortexSphere( entity ent )
+{
+ return ( ent.GetClassName() == "vortex_sphere" )
+}
+
+bool function PointIsWithinBounds( vector point, vector mins, vector maxs )
+{
+ Assert( mins.x < maxs.x )
+ Assert( mins.y < maxs.y )
+ Assert( mins.z < maxs.z )
+
+ return ( ( point.z >= mins.z && point.z <= maxs.z ) &&
+ ( point.x >= mins.x && point.x <= maxs.x ) &&
+ ( point.y >= mins.y && point.y <= maxs.y ) )
+}
+
+int function GetSpStartIndex()
+{
+ //HACK -> this should use some other code driven thing, not GetBugReproNum
+ int index = GetBugReproNum()
+
+ if ( index < 0 )
+ return 0
+
+ return index
+}
+
+// return all living soldiers
+array<entity> function GetAllSoldiers()
+{
+ return GetNPCArrayByClass( "npc_soldier" )
+}
+
+int function GameTeams_GetNumLivingPlayers( int teamIndex = TEAM_ANY )
+{
+ int noOfLivingPlayers = 0
+
+ array<entity> players
+ if ( teamIndex == TEAM_ANY )
+ players = GetPlayerArray()
+ else
+ players = GetPlayerArrayOfTeam( teamIndex )
+
+ foreach ( player in players )
+ {
+ if ( !IsAlive( player ) )
+ continue
+
+ ++noOfLivingPlayers
+ }
+
+ return noOfLivingPlayers
+}
+
+bool function GameTeams_TeamHasDeadPlayers( int team )
+{
+ array<entity> teamPlayers = GetPlayerArrayOfTeam( team )
+ foreach ( entity teamPlayer in teamPlayers )
+ {
+ if ( !IsAlive( teamPlayer ) )
+ return true
+ }
+ return false
+}
+
+typedef EntitiesDidLoadCallbackType void functionref()
+array<EntitiesDidLoadCallbackType> _EntitiesDidLoadTypedCallbacks
+
+void function RunCallbacks_EntitiesDidLoad()
+{
+ // reloading the level so don't do callbacks
+ if ( "forcedReloading" in level )
+ return
+
+ foreach ( callback in _EntitiesDidLoadTypedCallbacks )
+ {
+ thread callback()
+ }
+}
+
+void function AddCallback_EntitiesDidLoad( EntitiesDidLoadCallbackType callback )
+{
+ _EntitiesDidLoadTypedCallbacks.append( callback )
+}
+
+bool function IsTitanNPC( entity ent )
+{
+ return ent.IsTitan() && ent.IsNPC()
+}
+
+entity function InflictorOwner( entity inflictor )
+{
+ if ( IsValid( inflictor ) )
+ {
+ entity inflictorOwner = inflictor.GetOwner()
+ if ( IsValid( inflictorOwner ) )
+ inflictor = inflictorOwner
+ }
+
+ return inflictor
+}
+
+bool function IsPlayerControlledSpectre( entity ent )
+{
+ return ent.GetClassName() == "npc_spectre" && ent.GetBossPlayer() != null
+}
+
+bool function IsPlayerControlledTurret( entity ent )
+{
+ return IsTurret( ent ) && ent.GetBossPlayer() != null
+}
+
+bool function TitanShieldDecayEnabled()
+{
+ return ( GetCurrentPlaylistVarInt( "titan_shield_decay", 0 ) == 1 )
+}
+
+bool function TitanShieldRegenEnabled()
+{
+ return ( GetCurrentPlaylistVarInt( "titan_shield_regen", 0 ) == 1 )
+}
+
+bool function DoomStateDisabled()
+{
+ return ( GetCurrentPlaylistVarString( "titan_doomstate_variation", "default" ) == "disabled" || GetCurrentPlaylistVarString( "titan_doomstate_variation", "default" ) == "lastsegment" )
+}
+
+bool function NoWeaponDoomState()
+{
+ return ( GetCurrentPlaylistVarString( "titan_doomstate_variation", "default" ) == "noweapon" )
+}
+
+entity function GetPetTitanOwner( entity titan )
+{
+ array<entity> players = GetPlayerArray()
+ entity foundPlayer
+ foreach ( player in players )
+ {
+ if ( player.GetPetTitan() == titan )
+ {
+ Assert( foundPlayer == null, player + " and " + foundPlayer + " both own " + titan )
+ foundPlayer = player
+ }
+ }
+
+ return foundPlayer
+}
+
+entity function GetSoulFromPlayer( entity player )
+{
+ Assert( player.IsPlayer(), "argument should be a player" )
+
+ if ( player.IsTitan() )
+ return player.GetTitanSoul()
+ else if ( IsValid( player.GetPetTitan() ) )
+ return player.GetPetTitan().GetTitanSoul()
+
+ return null
+}
+
+string function GetPlayerBodyType( player )
+{
+ return expect string( player.GetPlayerSettingsField( "weaponClass" ) )
+}
+
+
+void function SetTeam( entity ent, int team )
+{
+ #if CLIENT
+ ent.Code_SetTeam( team )
+ #else
+ if ( ent.IsPlayer() )
+ {
+ ent.Code_SetTeam( team )
+ }
+ else if ( ent.IsNPC() )
+ {
+ int currentTeam = ent.GetTeam()
+ bool alreadyAssignedValidTeam = ( currentTeam == TEAM_IMC || currentTeam == TEAM_MILITIA )
+
+ ent.Code_SetTeam( team )
+
+ if ( ent.GetModelName() == $"" )
+ return
+
+ FixupTitle( ent )
+
+ if ( IsGrunt( ent ) || IsSpectre( ent ) )
+ {
+ if ( IsMultiplayer() )
+ {
+ int eHandle = ent.GetEncodedEHandle()
+
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_UpdateOverheadIconForNPC", eHandle )
+ }
+ }
+ }
+ else if ( IsShieldDrone( ent ) )
+ {
+ if ( team == 0 )
+ {
+ // anybody can use neutral shield drone
+ ent.SetUsable()
+ }
+ else
+ {
+ // only friendlies use a team shield drone
+ ent.SetUsableByGroup( "friendlies pilot" )
+ }
+ }
+
+ table modelTable = ent.CreateTableFromModelKeyValues()
+
+ if ( !( "teamSkin" in modelTable ) )
+ return
+
+ if ( alreadyAssignedValidTeam && ( !( "swapTeamOnLeech" in modelTable.teamSkin ) ) )
+ return
+
+ SetSkinForTeam( ent, team )
+ }
+ else
+ {
+ ent.Code_SetTeam( team )
+ }
+ #endif
+}
+
+void function PrintTraceResults( TraceResults results )
+{
+ printt( "TraceResults: " )
+ printt( "=========================" )
+ printt( "hitEnt: " + results.hitEnt )
+ printt( "endPos: " + results.endPos )
+ printt( "surfaceNormal: " + results.surfaceNormal )
+ printt( "surfaceName: " + results.surfaceName )
+ printt( "fraction: " + results.fraction )
+ printt( "fractionLeftSolid: " + results.fractionLeftSolid )
+ printt( "hitGroup: " + results.hitGroup )
+ printt( "startSolid: " + results.startSolid )
+ printt( "allSolid: " + results.allSolid )
+ printt( "hitSky: " + results.hitSky )
+ printt( "contents: " + results.contents )
+ printt( "=========================" )
+}
+
+bool function PROTO_AlternateDoomedState()
+{
+ return ( GetCurrentPlaylistVarInt( "infinite_doomed_state", 1 ) == 1 )
+}
+
+bool function PROTO_VariableRegenDelay()
+{
+ return ( GetCurrentPlaylistVarInt( "variable_regen_delay", 1 ) == 1 )
+}
+
+bool function PROTO_AutoTitansDisabled()
+{
+ return ( GetCurrentPlaylistVarInt( "always_enable_autotitans", 1 ) == 0 )
+}
+
+bool function TitanDamageRewardsTitanCoreTime()
+{
+ if ( GetCurrentPlaylistVarInt( "titan_core_from_titan_damage", 0 ) != 0 )
+ return true
+ return false
+}
+
+vector function ClampToMap( vector pos )
+{
+ return IterateAxis( pos, LimitAxisToMapExtents )
+}
+
+vector function IterateAxis( vector pos, float functionref( float ) func )
+{
+ pos.x = func( pos.x )
+ pos.y = func( pos.y )
+ pos.z = func( pos.z )
+ return pos
+}
+
+float function LimitAxisToMapExtents( float axisVal )
+{
+ if ( axisVal >= MAP_EXTENTS )
+ axisVal = MAP_EXTENTS - 1
+ else if ( axisVal <= -MAP_EXTENTS )
+ axisVal = -( MAP_EXTENTS - 1 )
+ return axisVal
+}
+
+bool function PilotSpawnOntoTitanIsEnabledInPlaylist( entity player )
+{
+ if ( GetCurrentPlaylistVarInt( "titan_spawn_deploy_enabled", 0 ) != 0 )
+ return true
+ return false
+}
+
+bool function PlayerCanSpawnIntoTitan( entity player )
+{
+ if ( !PilotSpawnOntoTitanIsEnabledInPlaylist( player ) )
+ return false
+
+ entity titan = player.GetPetTitan()
+
+ if ( !IsAlive( titan ) )
+ return false
+
+ if ( GetDoomedState( titan ) )
+ return false
+
+ if ( titan.ContextAction_IsActive() )
+ return false
+
+ return false // turned off until todd figures out how to enable
+}
+
+array< vector > function EntitiesToOrigins( array< entity > ents )
+{
+ array<vector> origins
+
+ foreach ( ent in ents )
+ {
+ origins.append( ent.GetOrigin() )
+ }
+
+ return origins
+}
+
+vector function GetMedianOriginOfEntities( array<entity> ents )
+{
+ array<vector> origins = EntitiesToOrigins( ents )
+ return GetMedianOrigin( origins )
+}
+
+vector function GetMedianOrigin( array<vector> origins )
+{
+ if ( origins.len() == 1 )
+ return origins[0]
+
+ vector median
+
+ int middleIndex1
+ int middleIndex2
+
+ if ( IsEven( origins.len() ) )
+ {
+ middleIndex1 = origins.len() / 2
+ middleIndex2 = middleIndex1
+ }
+ else
+ {
+ middleIndex1 = int( floor( origins.len() / 2.0 ) )
+ middleIndex2 = middleIndex1 + 1
+ }
+
+ origins.sort( CompareVecX )
+ median.x = ( origins[ middleIndex1 ].x + origins[ middleIndex2 ].x ) / 2.0
+
+ origins.sort( CompareVecY )
+ median.y = ( origins[ middleIndex1 ].y + origins[ middleIndex2 ].y ) / 2.0
+
+ origins.sort( CompareVecZ )
+ median.z = ( origins[ middleIndex1 ].z + origins[ middleIndex2 ].z ) / 2.0
+
+ return median
+}
+
+int function CompareVecX( vector a, vector b )
+{
+ if ( a.x > b.x )
+ return 1
+
+ return -1
+}
+
+int function CompareVecY( vector a, vector b )
+{
+ if ( a.y > b.y )
+ return 1
+
+ return -1
+}
+
+int function CompareVecZ( vector a, vector b )
+{
+ if ( a.z > b.z )
+ return 1
+
+ return -1
+}
+
+float function GetFractionAlongPath( array<entity> nodes, vector p )
+{
+ float totalDistance = GetPathDistance( nodes )
+
+ // See which segment we are currently on (closest to)
+ int closestSegment = -1
+ float closestDist = 9999
+ for( int i = 0 ; i < nodes.len() - 1; i++ )
+ {
+ float dist = GetDistanceSqrFromLineSegment( nodes[i].GetOrigin(), nodes[i + 1].GetOrigin(), p )
+ if ( closestSegment < 0 || dist < closestDist )
+ {
+ closestSegment = i
+ closestDist = dist
+ }
+ }
+ Assert( closestSegment >= 0 )
+ Assert( closestSegment < nodes.len() - 1 )
+
+ // Get the distance along the path already traveled
+ float distTraveled = 0.0
+ for( int i = 0 ; i < closestSegment; i++ )
+ {
+ //DebugDrawLine( nodes[i].GetOrigin(), nodes[i + 1].GetOrigin(), 255, 255, 0, true, 0.1 )
+ distTraveled += Distance( nodes[i].GetOrigin(), nodes[i+1].GetOrigin() )
+ }
+
+ // Add the distance traveled on current segment
+ vector closestPointOnSegment = GetClosestPointOnLineSegment( nodes[closestSegment].GetOrigin(), nodes[closestSegment + 1].GetOrigin(), p )
+ //DebugDrawLine( nodes[closestSegment].GetOrigin(), closestPointOnSegment, 255, 255, 0, true, 0.1 )
+ distTraveled += Distance( nodes[closestSegment].GetOrigin(), closestPointOnSegment )
+
+ return clamp( distTraveled / totalDistance, 0.0, 1.0 )
+}
+
+float function GetPathDistance( array<entity> nodes )
+{
+ float totalDist = 0.0
+ for( int i = 0 ; i < nodes.len() - 1; i++ )
+ {
+ //DebugDrawSphere( nodes[i].GetOrigin(), 16.0, 255, 0, 0, true, 0.1 )
+ totalDist += Distance( nodes[i].GetOrigin(), nodes[i+1].GetOrigin() )
+ }
+ //DebugDrawSphere( nodes[nodes.len() -1].GetOrigin(), 16.0, 255, 0, 0, true, 0.1 )
+
+ return totalDist
+}
+
+void function WaittillAnimDone( entity animatingEnt )
+{
+ waitthread WaittillAnimDone_Thread( animatingEnt )
+}
+
+void function WaittillAnimDone_Thread( entity animatingEnt )
+{
+ if ( animatingEnt.IsPlayer() )
+ animatingEnt.EndSignal( "OnDestroy" )
+
+ animatingEnt.EndSignal( "OnAnimationInterrupted" )
+ animatingEnt.WaitSignal( "OnAnimationDone" )
+}
+
+array<entity> function GetEntityLinkChain( entity startNode )
+{
+ Assert( IsValid( startNode ) )
+ array<entity> nodes
+ nodes.append( startNode )
+ while(true)
+ {
+ entity nextNode = nodes[nodes.len() - 1].GetLinkEnt()
+ if ( !IsValid( nextNode ) )
+ break
+ nodes.append( nextNode )
+ }
+ return nodes
+}
+
+float function HealthRatio( entity ent )
+{
+ int health = ent.GetHealth()
+ int maxHealth = ent.GetMaxHealth()
+ return float( health ) / maxHealth
+}
+
+vector function GetPointOnPathForFraction( array<entity> nodes, float frac )
+{
+ Assert( frac >= 0 )
+
+ float totalPathDist = GetPathDistance( nodes )
+ float distRemaining = totalPathDist * frac
+ vector point = nodes[0].GetOrigin()
+
+ for( int i = 0 ; i < nodes.len() - 1; i++ )
+ {
+ float segmentDist = Distance( nodes[i].GetOrigin(), nodes[i+1].GetOrigin() )
+ if ( segmentDist <= distRemaining )
+ {
+ // Add the whole segment
+ distRemaining -= segmentDist
+ point = nodes[i+1].GetOrigin()
+ }
+ else
+ {
+ // Fraction ends somewhere in this segment
+ vector dirVec = Normalize( nodes[i+1].GetOrigin() - nodes[i].GetOrigin() )
+ point = nodes[i].GetOrigin() + ( dirVec * distRemaining )
+ distRemaining = 0
+ }
+ if ( distRemaining <= 0 )
+ break
+ }
+
+ if ( frac > 1.0 && distRemaining > 0 )
+ {
+ vector dirVec = Normalize( nodes[nodes.len() - 1].GetOrigin() - nodes[nodes.len() - 2].GetOrigin() )
+ point = nodes[nodes.len() - 1].GetOrigin() + ( dirVec * distRemaining )
+ }
+
+ return point
+}
+
+bool function PlayerBlockedByTeamEMP( entity player )
+{
+ return ( player.nv.empEndTime > Time() )
+}
+
+#if SERVER
+void function Embark_Allow( entity player )
+{
+ player.SetTitanEmbarkEnabled( true )
+}
+
+void function Embark_Disallow( entity player )
+{
+ player.SetTitanEmbarkEnabled( false )
+}
+
+void function Disembark_Allow( entity player )
+{
+ player.SetTitanDisembarkEnabled( true )
+}
+
+void function Disembark_Disallow( entity player )
+{
+ player.SetTitanDisembarkEnabled( false )
+}
+#endif
+
+bool function CanEmbark( entity player )
+{
+ return player.GetTitanEmbarkEnabled()
+}
+
+bool function CanDisembark( entity player )
+{
+ return player.GetTitanDisembarkEnabled()
+}
+
+string function GetDroneType( entity npc )
+{
+ return expect string( npc.Dev_GetAISettingByKeyField( "drone_type" ) )
+}
+
+vector function FlattenVector( vector vec )
+{
+ return Vector( vec.x, vec.y, 0 )
+}
+
+vector function FlattenAngles( vector angles )
+{
+ return Vector( 0, angles.y, 0 )
+}
+
+bool function IsHumanSized( entity ent )
+{
+ if ( ent.IsPlayer() )
+ return ent.IsHuman()
+
+ if ( ent.IsNPC() )
+ {
+
+ if ( ent.GetAIClass() == AIC_SMALL_TURRET )
+ return true
+
+ string bodyType = ent.GetBodyType()
+ return bodyType == "human" || bodyType == "marvin"
+ }
+
+ return false
+}
+
+bool function IsDropship( entity ent )
+{
+#if SERVER
+ return ent.GetClassName() == "npc_dropship"
+#elseif CLIENT
+ if ( !ent.IsNPC() )
+ return false
+ //Probably should not use GetClassName, but npc_dropship isn't a class so can't use instanceof?
+ return ( ent.GetClassName() == "npc_dropship" || ent.GetSignifierName() == "npc_dropship" )
+#endif
+}
+
+bool function IsSpecialist( entity ent )
+{
+ return IsGrunt( ent ) && ent.IsMechanical()
+}
+
+bool function IsGrunt( entity ent )
+{
+#if SERVER
+ return ent.IsNPC() && ent.GetClassName() == "npc_soldier"
+#elseif CLIENT
+ return ent.IsNPC() && ent.GetSignifierName() == "npc_soldier"
+#endif
+}
+
+bool function IsMarvin( entity ent )
+{
+ return ent.IsNPC() && ent.GetAIClass() == AIC_MARVIN
+}
+
+bool function IsSpectre( entity ent )
+{
+ return ent.IsNPC() && ent.GetAIClass() == AIC_SPECTRE
+}
+
+bool function IsWorldSpawn( entity ent )
+{
+ #if SERVER
+ return ent.GetClassName() == "worldspawn"
+ #elseif CLIENT
+ return ent.GetSignifierName() == "worldspawn"
+ #endif
+}
+
+bool function IsEnvironment( entity ent )
+{
+ #if SERVER
+ return ent.GetClassName() == "trigger_hurt"
+ #elseif CLIENT
+ return ent.GetSignifierName() == "trigger_hurt"
+ #endif
+}
+
+bool function IsSuperSpectre( entity ent )
+{
+#if SERVER
+ return ent.GetClassName() == "npc_super_spectre"
+#elseif CLIENT
+ return ent.GetSignifierName() == "npc_super_spectre"
+#endif
+}
+
+bool function IsAndroidNPC( entity ent )
+{
+ return ( IsSpectre( ent ) || IsStalker( ent ) || IsMarvin( ent ) )
+}
+
+bool function IsStalker( entity ent )
+{
+ return ent.IsNPC() && ( ent.GetAIClass() == AIC_STALKER || ent.GetAIClass() == AIC_STALKER_CRAWLING )
+}
+
+bool function IsProwler( entity ent )
+{
+#if SERVER
+ return ent.GetClassName() == "npc_prowler"
+#elseif CLIENT
+ return ent.GetSignifierName() == "npc_prowler"
+#endif
+}
+
+bool function IsAirDrone( entity ent )
+{
+#if SERVER
+ return ent.GetClassName() == "npc_drone"
+#elseif CLIENT
+ return ent.GetSignifierName() == "npc_drone"
+#endif
+}
+
+bool function IsPilotElite( entity ent )
+{
+#if SERVER
+ return ent.GetClassName() == "npc_pilot_elite"
+#elseif CLIENT
+ return ent.GetSignifierName() == "npc_pilot_elite"
+#endif
+}
+
+bool function IsAttackDrone( entity ent )
+{
+ return ( ent.IsNPC() && !ent.IsNonCombatAI() && IsAirDrone( ent ) )
+}
+
+bool function IsGunship( entity ent )
+{
+#if SERVER
+ return ent.GetClassName() == "npc_gunship"
+#elseif CLIENT
+ return ent.GetSignifierName() == "npc_gunship"
+#endif
+}
+
+bool function IsMinion( entity ent )
+{
+ if ( IsGrunt( ent ) )
+ return true
+
+ if ( IsSpectre( ent ) )
+ return true
+
+ return false
+}
+
+bool function IsShieldDrone( entity ent )
+{
+#if SERVER
+ if ( ent.GetClassName() != "npc_drone" )
+ return false
+#elseif CLIENT
+ if ( ent.GetSignifierName() != "npc_drone" )
+ return false
+#endif
+
+ return GetDroneType( ent ) == "drone_type_shield"
+}
+
+#if SERVER
+bool function IsTick( entity ent )
+{
+ return (ent.IsNPC() && (ent.GetAIClass() == AIC_FRAG_DRONE))
+}
+
+bool function IsNPCTitan( entity ent )
+{
+ return ent.IsNPC() && ent.IsTitan()
+}
+#endif
+
+bool function NPC_GruntChatterSPEnabled( entity npc )
+{
+ if ( !IsSingleplayer() )
+ return false
+
+ if ( !npc.IsNPC() )
+ return false
+
+ if ( npc.GetClassName() != "npc_soldier" )
+ return false
+
+ return true
+}
+
+RaySphereIntersectStruct function IntersectRayWithSphere( vector rayStart, vector rayEnd, vector sphereOrigin, float sphereRadius )
+{
+ RaySphereIntersectStruct intersection
+
+ vector vecSphereToRay = rayStart - sphereOrigin
+
+ vector vecRayDelta = rayEnd - rayStart
+ float a = DotProduct( vecRayDelta, vecRayDelta )
+
+ if ( a == 0.0 )
+ {
+ intersection.result = LengthSqr( vecSphereToRay ) <= sphereRadius * sphereRadius
+ intersection.enterFrac = 0.0
+ intersection.leaveFrac = 0.0
+ return intersection
+ }
+
+ float b = 2 * DotProduct( vecSphereToRay, vecRayDelta )
+ float c = DotProduct( vecSphereToRay, vecSphereToRay ) - sphereRadius * sphereRadius
+ float discrim = b * b - 4 * a * c
+ if ( discrim < 0.0 )
+ {
+ intersection.result = false
+ return intersection
+ }
+
+ discrim = sqrt( discrim )
+ float oo2a = 0.5 / a
+ intersection.enterFrac = ( - b - discrim ) * oo2a
+ intersection.leaveFrac = ( - b + discrim ) * oo2a
+
+ if ( ( intersection.enterFrac > 1.0 ) || ( intersection.leaveFrac < 0.0 ) )
+ {
+ intersection.result = false
+ return intersection
+ }
+
+ if ( intersection.enterFrac < 0.0 )
+ intersection.enterFrac = 0.0
+ if ( intersection.leaveFrac > 1.0 )
+ intersection.leaveFrac = 1.0
+
+ intersection.result = true
+ return intersection
+}
+
+table function GetTableFromString( string inString )
+{
+ if ( inString.len() > 0 )
+ return expect table( getconsttable()[ inString ] )
+
+ return {}
+}
+
+int function GetWeaponDamageNear( entity weapon, entity victim )
+{
+ entity weaponOwner = weapon.GetWeaponOwner()
+ if ( weaponOwner.IsNPC() )
+ {
+ if ( victim.GetArmorType() == ARMOR_TYPE_HEAVY )
+ return weapon.GetWeaponSettingInt( eWeaponVar.npc_damage_near_value_titanarmor )
+ else
+ return weapon.GetWeaponSettingInt( eWeaponVar.npc_damage_near_value )
+ }
+ else
+ {
+ if ( victim.GetArmorType() == ARMOR_TYPE_HEAVY )
+ return weapon.GetWeaponSettingInt( eWeaponVar.damage_near_value_titanarmor )
+ else
+ return weapon.GetWeaponSettingInt( eWeaponVar.damage_near_value )
+ }
+
+ unreachable
+}
+
+void function PrintFirstPersonSequenceStruct( FirstPersonSequenceStruct fpsStruct )
+{
+ printt( "Printing FirstPersonSequenceStruct:" )
+
+ printt( "firstPersonAnim: " + fpsStruct.firstPersonAnim )
+ printt( "thirdPersonAnim: " + fpsStruct.thirdPersonAnim )
+ printt( "firstPersonAnimIdle: " + fpsStruct.firstPersonAnimIdle )
+ printt( "thirdPersonAnimIdle: " + fpsStruct.thirdPersonAnimIdle )
+ printt( "relativeAnim: " + fpsStruct.relativeAnim )
+ printt( "attachment: " + fpsStruct.attachment )
+ printt( "teleport: " + fpsStruct.teleport )
+ printt( "noParent: " + fpsStruct.noParent )
+ printt( "blendTime: " + fpsStruct.blendTime )
+ printt( "noViewLerp: " + fpsStruct.noViewLerp )
+ printt( "hideProxy: " + fpsStruct.hideProxy )
+ printt( "viewConeFunction: " + string( fpsStruct.viewConeFunction ) )
+ printt( "origin: " + string( fpsStruct.origin ) )
+ printt( "angles: " + string ( fpsStruct.angles ) )
+ printt( "enablePlanting: " + fpsStruct.enablePlanting )
+ printt( "setInitialTime: " + fpsStruct.setInitialTime )
+ printt( "useAnimatedRefAttachment: " + fpsStruct.useAnimatedRefAttachment )
+ printt( "renderWithViewModels: " + fpsStruct.renderWithViewModels )
+ printt( "gravity: " + fpsStruct.gravity )
+
+}
+
+void function WaitSignalOrTimeout( entity ent, float timeout, string signal1, string signal2 = "", string signal3 = "" )
+{
+ Assert( IsValid( ent ) )
+
+ ent.EndSignal( signal1 )
+
+ if ( signal2 != "" )
+ ent.EndSignal( signal2 )
+
+ if ( signal3 != "" )
+ ent.EndSignal( signal3 )
+
+ wait( timeout )
+}
+
+array<vector> function GetShortestLineSegmentConnectingLineSegments( vector line1Point1, vector line1Point2, vector line2Point1, vector line2Point2 )
+{
+ // From Paul Bourke's algorithm "The shortest line between two lines in 3D" at http://paulbourke.net/geometry/pointlineplane/
+
+ vector p1 = line1Point1
+ vector p2 = line1Point2
+ vector p3 = line2Point1
+ vector p4 = line2Point2
+ vector p13 = p1 - p3
+ vector p21 = p2 - p1
+ vector p43 = p4 - p3
+
+ if ( Length( p43 ) < 1.0 )
+ {
+ array<vector> resultVectors
+ resultVectors.append( p4 )
+ resultVectors.append( p3 )
+ return resultVectors
+ }
+
+ if ( Length( p21 ) < 1.0 )
+ {
+ array<vector> resultVectors
+ resultVectors.append( p2 )
+ resultVectors.append( p1 )
+ return resultVectors
+ }
+
+ float d1343 = p13.x * p43.x + p13.y * p43.y + p13.z * p43.z
+ float d4321 = p43.x * p21.x + p43.y * p21.y + p43.z * p21.z
+ float d1321 = p13.x * p21.x + p13.y * p21.y + p13.z * p21.z
+ float d4343 = p43.x * p43.x + p43.y * p43.y + p43.z * p43.z
+ float d2121 = p21.x * p21.x + p21.y * p21.y + p21.z * p21.z
+
+
+ float denom = d2121 * d4343 - d4321 * d4321
+ Assert( fabs( denom ) > 0.01 )
+ float numer = d1343 * d4321 - d1321 * d4343
+
+ float mua = numer / denom
+ float mub = (d1343 + d4321 * (mua)) / d4343
+
+ vector resultVec1
+ vector resultVec2
+ resultVec1.x = p1.x + mua * p21.x
+ resultVec1.y = p1.y + mua * p21.y
+ resultVec1.z = p1.z + mua * p21.z
+ resultVec2.x = p3.x + mub * p43.x
+ resultVec2.y = p3.y + mub * p43.y
+ resultVec2.z = p3.z + mub * p43.z
+
+ array<vector> resultVectors
+ resultVectors.append( resultVec1 )
+ resultVectors.append( resultVec2 )
+ return resultVectors
+}
+
+vector function GetClosestPointToLineSegments( vector line1Point1, vector line1Point2, vector line2Point1, vector line2Point2 )
+{
+ array<vector> results = GetShortestLineSegmentConnectingLineSegments( line1Point1, line1Point2, line2Point1, line2Point2 )
+ Assert( results.len() == 2 )
+ return ( results[0] + results[1] ) / 2.0
+}
+
+
+bool function PlayerCanSee( entity player, entity ent, bool doTrace, float degrees )
+{
+ float minDot = deg_cos( degrees )
+
+ // On screen?
+ float dot = DotProduct( Normalize( ent.GetWorldSpaceCenter() - player.EyePosition() ), player.GetViewVector() )
+ if ( dot < minDot )
+ return false
+
+ // Can trace to it?
+ if ( doTrace )
+ {
+ TraceResults trace = TraceLine( player.EyePosition(), ent.GetWorldSpaceCenter(), null, TRACE_MASK_BLOCKLOS, TRACE_COLLISION_GROUP_NONE )
+ if ( trace.hitEnt == ent || trace.fraction >= 0.99 )
+ return true
+ else
+ return false
+ }
+ else
+ return true
+
+ Assert( 0, "shouldn't ever get here")
+ unreachable
+}
+
+bool function PlayerCanSeePos( entity player, vector pos, bool doTrace, float degrees )
+{
+ float minDot = deg_cos( degrees )
+ float dot = DotProduct( Normalize( pos - player.EyePosition() ), player.GetViewVector() )
+ if ( dot < minDot )
+ return false
+
+ if ( doTrace )
+ {
+ TraceResults trace = TraceLine( player.EyePosition(), pos, null, TRACE_MASK_BLOCKLOS, TRACE_COLLISION_GROUP_NONE )
+ if ( trace.fraction < 0.99 )
+ return false
+ }
+
+ return true
+}
+
+bool function VectorsFacingSameDirection( vector v1, vector v2, float degreesThreshold )
+{
+ float minDot = deg_cos( degreesThreshold )
+ float dot = DotProduct( Normalize( v1 ), Normalize( v2 ) )
+ return ( dot >= minDot )
+}
+
+vector function GetRelativeDelta( vector origin, entity ref, string attachment = "" )
+{
+ vector pos
+ vector right
+ vector forward
+ vector up
+
+ if ( attachment != "" )
+ {
+ int attachID = ref.LookupAttachment( attachment )
+ pos = ref.GetAttachmentOrigin( attachID )
+ vector angles = ref.GetAttachmentAngles( attachID )
+ right = AnglesToRight( angles )
+ forward = AnglesToForward( angles )
+ up = AnglesToUp( angles )
+ }
+ else
+ {
+ pos = ref.GetOrigin()
+ right = ref.GetRightVector()
+ forward = ref.GetForwardVector()
+ up = ref.GetUpVector()
+ }
+
+ vector x = GetClosestPointOnLineSegment( pos + right * -16384, pos + right * 16384, origin )
+ vector y = GetClosestPointOnLineSegment( pos + forward * -16384, pos + forward * 16384, origin )
+ vector z = GetClosestPointOnLineSegment( pos + up * -16384, pos + up * 16384, origin )
+
+ float distx = Distance(pos, x)
+ float disty = Distance(pos, y)
+ float distz = Distance(pos, z)
+
+ if ( DotProduct( x - pos, right ) < 0 )
+ distx *= -1
+ if ( DotProduct( y - pos, forward ) < 0 )
+ disty *= -1
+ if ( DotProduct( z - pos, up ) < 0 )
+ distz *= -1
+
+ return Vector( distx, disty, distz )
+}
+
+#if SERVER
+float function GetRoundTimeLimit_ForGameMode()
+{
+ #if DEV
+ if ( level.devForcedTimeLimit )
+ {
+ //Make it needed to be called multiple times for RoundBasedGameModes
+ level.devForcedTimeLimit = 0
+ return 0.1
+ }
+ #endif
+
+ #if MP
+ if ( GameState_GetTimeLimitOverride() >= 0 )
+ return GameState_GetTimeLimitOverride()
+ #endif
+
+ if ( !GameMode_IsDefined( GAMETYPE ) )
+ return GetCurrentPlaylistVarFloat( "roundtimelimit", 10 )
+ else
+ return GameMode_GetRoundTimeLimit( GAMETYPE )
+
+ unreachable
+}
+#endif
+
+bool function HasIronRules()
+{
+ bool result = (GetCurrentPlaylistVarInt( "iron_rules", 0 ) != 0)
+ return result
+}
+
+vector function GetWorldOriginFromRelativeDelta( vector delta, entity ref )
+{
+ vector right = ref.GetRightVector() * delta.x
+ vector forward = ref.GetForwardVector() * delta.y
+ vector up = ref.GetUpVector() * delta.z
+
+ return ref.GetOrigin() + right + forward + up
+}
+
+bool function IsHardcoreGameMode()
+{
+ return GetCurrentPlaylistVarInt( "gm_hardcore_settings", 0 ) == 1
+}
+
+bool function PlayerHasWeapon( entity player, string weaponName )
+{
+ array<entity> weapons = player.GetMainWeapons()
+ weapons.extend( player.GetOffhandWeapons() )
+
+ foreach ( weapon in weapons )
+ {
+ if ( weapon.GetWeaponClassName() == weaponName )
+ return true
+ }
+
+ return false
+}
+
+bool function PlayerCanUseWeapon( entity player, string weaponClass )
+{
+ return ( ( player.IsTitan() && weaponClass == "titan" ) || ( !player.IsTitan() && weaponClass == "human" ) )
+}
+
+string function GetTitanCharacterName( entity titan )
+{
+ Assert( titan.IsTitan() )
+
+ string setFile
+
+ if ( titan.IsPlayer() )
+ {
+ setFile = titan.GetPlayerSettings()
+ }
+ else
+ {
+ string aiSettingsFile = titan.GetAISettingsName()
+ setFile = expect string( Dev_GetAISettingByKeyField_Global( aiSettingsFile, "npc_titan_player_settings" ) )
+ }
+
+ return GetTitanCharacterNameFromSetFile( setFile )
+}
+
+bool function IsTitanPrimeTitan( entity titan )
+{
+ Assert( titan.IsTitan() )
+ string setFile
+
+ if ( titan.IsPlayer() )
+ {
+ setFile = titan.GetPlayerSettings()
+ }
+ else
+ {
+ string aiSettingsFile = titan.GetAISettingsName()
+ setFile = expect string( Dev_GetAISettingByKeyField_Global( aiSettingsFile, "npc_titan_player_settings" ) )
+ }
+
+ return Dev_GetPlayerSettingByKeyField_Global( setFile, "isPrime" ) == 1
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_viewcone.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_viewcone.gnut
new file mode 100644
index 00000000..025c9dfd
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_viewcone.gnut
@@ -0,0 +1,520 @@
+
+global function Viewcone_Init
+
+ //DEFAULT: if you don't set one - defaults to this one
+global function ViewConeRampFree
+
+//misc
+global function ViewConeZero
+global function ViewConeZeroInstant
+global function ViewConeNarrow
+global function ViewConeTight
+global function ViewConeSmall
+global function ViewConeWide
+global function ViewConeFreeLookingForward
+global function ViewConeLockedForward
+global function ViewConeTDay
+global function ViewConeTDayZero
+global function ViewConeFastball
+
+//run out ramp
+global function ViewConeRampFrontLeft
+global function ViewConeRampFrontRight
+global function ViewConeRampBackLeft
+global function ViewConeRampBackRight
+
+//droppod
+global function ViewConeDropPodFrontR
+global function ViewConeDropPodFrontL
+global function ViewConeDropPodBackR
+global function ViewConeDropPodBackL
+global function ViewConeDropPod
+
+//right side jump
+global function ViewConeSideRightStandFront
+global function ViewConeSideRightStandBack
+global function ViewConeSideRightSitFront
+global function ViewConeSideRightSitBack
+
+//right side jump - focus on hero
+global function ViewConeSideRightWithHeroStandFront
+global function ViewConeSideRightWithHeroStandBack
+global function ViewConeSideRightWithHeroSitFront
+global function ViewConeSideRightWithHeroSitBack
+
+//right side jump - locked 180 view forward
+global function ViewConeSideRightLockedForwardStandFront
+global function ViewConeSideRightLockedForwardSitFront
+global function ViewConeSideRightLockedForwardStandBack
+global function ViewConeSideRightLockedForwardSitBack
+
+global function ViewConeSpSpawn
+global function InitView
+
+//evac
+global function ViewConeFree
+
+global function IsViewConeCurrent
+
+void function Viewcone_Init()
+{
+ AddGlobalAnimEvent( "ViewConeZero", ViewConeZero )
+ AddGlobalAnimEvent( "ViewConeTight", ViewConeTight )
+ AddGlobalAnimEvent( "ViewConeDropPod", ViewConeDropPod )
+ AddGlobalAnimEvent( "ViewConeDropPodFrontR", ViewConeDropPodFrontR )
+ AddGlobalAnimEvent( "ViewConeDropPodFrontL", ViewConeDropPodFrontL )
+ AddGlobalAnimEvent( "ViewConeDropPodBackR", ViewConeDropPodBackR )
+ AddGlobalAnimEvent( "ViewConeDropPodBackL", ViewConeDropPodBackL )
+ AddGlobalAnimEvent( "ViewConeNarrow", ViewConeNarrow )
+ AddGlobalAnimEvent( "ViewConeSmall", ViewConeSmall )
+ AddGlobalAnimEvent( "ViewConeRampFrontLeft", ViewConeRampFrontLeft )
+ AddGlobalAnimEvent( "ViewConeRampFrontRight", ViewConeRampFrontRight )
+ AddGlobalAnimEvent( "ViewConeRampBackLeft", ViewConeRampBackLeft )
+ AddGlobalAnimEvent( "ViewConeRampBackRight", ViewConeRampBackRight )
+ AddGlobalAnimEvent( "ViewConeRampFree", ViewConeRampFree )
+ AddGlobalAnimEvent( "ViewConeFree", ViewConeFree )
+ AddGlobalAnimEvent( "ViewConeFreeLookingForward", ViewConeFreeLookingForward )
+ AddGlobalAnimEvent( "ViewConeTDay", ViewConeTDay )
+ AddGlobalAnimEvent( "ViewConeTDayZero", ViewConeTDayZero )
+}
+
+void function ViewConeZero( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( 0 )
+ player.PlayerCone_SetMaxYaw( 0 )
+ player.PlayerCone_SetMinPitch( 0 )
+ player.PlayerCone_SetMaxPitch( 0 )
+}
+
+void function ViewConeZeroInstant( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.0 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( 0 )
+ player.PlayerCone_SetMaxYaw( 0 )
+ player.PlayerCone_SetMinPitch( 0 )
+ player.PlayerCone_SetMaxPitch( 0 )
+}
+
+void function ViewConeTight( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -15 )
+ player.PlayerCone_SetMaxYaw( 15 )
+ player.PlayerCone_SetMinPitch( -15 )
+ player.PlayerCone_SetMaxPitch( 15 )
+}
+
+void function ViewConeSmall( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -38 )
+ player.PlayerCone_SetMaxYaw( 38 )
+ player.PlayerCone_SetMinPitch( -25 )
+ player.PlayerCone_SetMaxPitch( 25 )
+}
+
+void function ViewConeWide( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -50 )
+ player.PlayerCone_SetMaxYaw( 50 )
+ player.PlayerCone_SetMinPitch( -35 )
+ player.PlayerCone_SetMaxPitch( 35 )
+}
+
+void function ViewConeDropPod( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -70 )
+ player.PlayerCone_SetMaxYaw( 70 )
+ player.PlayerCone_SetMinPitch( -30 )
+ player.PlayerCone_SetMaxPitch( 30 )
+}
+
+const FRONTDIF = -75
+const BACKDIF = -30
+
+void function ViewConeDropPodFrontR( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ //range is 140
+ player.PlayerCone_SetMinYaw( -70 - FRONTDIF )
+ player.PlayerCone_SetMaxYaw( 70 - FRONTDIF )
+ player.PlayerCone_SetMinPitch( -30 )
+ player.PlayerCone_SetMaxPitch( 30 )
+}
+
+void function ViewConeDropPodFrontL( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ //range is 140
+ player.PlayerCone_SetMinYaw( -70 + FRONTDIF )
+ player.PlayerCone_SetMaxYaw( 70 + FRONTDIF )
+ player.PlayerCone_SetMinPitch( -30 )
+ player.PlayerCone_SetMaxPitch( 30 )
+}
+
+void function ViewConeDropPodBackR( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ //range is 140
+ player.PlayerCone_SetMinYaw( -70 - BACKDIF )
+ player.PlayerCone_SetMaxYaw( 70 - BACKDIF )
+ player.PlayerCone_SetMinPitch( -30 )
+ player.PlayerCone_SetMaxPitch( 30 )
+}
+
+void function ViewConeDropPodBackL( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ //range is 140
+ player.PlayerCone_SetMinYaw( -70 + BACKDIF )
+ player.PlayerCone_SetMaxYaw( 70 + BACKDIF )
+ player.PlayerCone_SetMinPitch( -30 )
+ player.PlayerCone_SetMaxPitch( 30 )
+}
+
+void function ViewConeNarrow( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -60 )
+ player.PlayerCone_SetMaxYaw( 60 )
+ player.PlayerCone_SetMinPitch( -60 )
+ player.PlayerCone_SetMaxPitch( 60 )
+}
+
+void function ViewConeTDay( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 1.0 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -30 )
+ player.PlayerCone_SetMaxYaw( 30 )
+ player.PlayerCone_SetMinPitch( 0 )
+ player.PlayerCone_SetMaxPitch( 30 )
+}
+
+void function ViewConeTDayZero( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 1.5 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( 0 )
+ player.PlayerCone_SetMaxYaw( 0 )
+ player.PlayerCone_SetMinPitch( 0 )
+ player.PlayerCone_SetMaxPitch( 0 )
+}
+
+void function ViewConeFastball( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -25 )
+ player.PlayerCone_SetMaxYaw( 25 )
+ player.PlayerCone_SetMinPitch( -15 )
+ player.PlayerCone_SetMaxPitch( 15 )
+}
+
+void function ViewConeFreeLookingForward( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeFree( player )
+
+ thread InitView( player, 0, 180, ViewConeFreeLookingForward )
+}
+
+void function ViewConeSpSpawn( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+
+ player.PlayerCone_SetLerpTime( 0.25 )
+ player.PlayerCone_FromAnim()
+
+ player.PlayerCone_SetMinYaw( 125 )
+ player.PlayerCone_SetMaxYaw( 125 )
+ player.PlayerCone_SetMinPitch( 7 )
+ player.PlayerCone_SetMaxPitch( 7 )
+
+ thread InitView( player, 7, 125, ViewConeSpSpawn )
+}
+
+void function ViewConeSideRightLockedForwardStandFront( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeLockedForward( player )
+
+ thread InitView( player, 15, 20, ViewConeSideRightLockedForwardStandFront )
+}
+
+void function ViewConeSideRightLockedForwardSitFront( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeLockedForward( player )
+
+ thread InitView( player, 0, 50, ViewConeSideRightLockedForwardSitFront )
+}
+
+void function ViewConeSideRightLockedForwardStandBack( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeLockedForward( player )
+
+ thread InitView( player, 15, 45, ViewConeSideRightLockedForwardStandBack )
+}
+
+void function ViewConeSideRightLockedForwardSitBack( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeLockedForward( player )
+
+ thread InitView( player, 0, 50, ViewConeSideRightLockedForwardSitBack )
+}
+
+void function ViewConeSideRightWithHeroStandFront( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeFree( player )
+
+ thread InitView( player, 15, 20, ViewConeSideRightWithHeroStandFront )
+}
+
+void function ViewConeSideRightWithHeroSitFront( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeFree( player )
+
+ thread InitView( player, 0, 50, ViewConeSideRightWithHeroSitFront )
+}
+
+void function ViewConeSideRightWithHeroStandBack( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeFree( player )
+
+ thread InitView( player, 15, 45, ViewConeSideRightWithHeroStandBack )
+}
+
+void function ViewConeSideRightWithHeroSitBack( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeFree( player )
+
+ thread InitView( player, 0, 50, ViewConeSideRightWithHeroSitBack )
+}
+
+void function ViewConeSideRightStandFront( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeFree( player )
+
+ thread InitView( player, 30, -10, ViewConeSideRightStandFront )
+}
+
+void function ViewConeSideRightSitFront( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeFree( player )
+
+ thread InitView( player, 20, -20, ViewConeSideRightSitFront )
+}
+
+void function ViewConeSideRightStandBack( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeFree( player )
+
+ thread InitView( player, 30, 20, ViewConeSideRightStandBack )
+}
+
+void function ViewConeSideRightSitBack( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeFree( player )
+
+ thread InitView( player, 20, 35, ViewConeSideRightSitBack )
+}
+
+void function ViewConeRampFrontLeft( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeRampFree( player )
+
+ thread InitView( player, 5, 70, ViewConeRampFrontLeft )
+}
+
+void function ViewConeRampFrontRight( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeRampFree( player )
+
+ thread InitView( player, 5, -70, ViewConeRampFrontRight )
+}
+
+void function ViewConeRampBackLeft( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeRampFree( player )
+
+ thread InitView( player, 5, 100, ViewConeRampBackLeft )
+}
+
+void function ViewConeRampBackRight( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ ViewConeRampFree( player )
+
+ thread InitView( player, 5, -100, ViewConeRampBackRight )
+}
+
+void function ViewConeRampFree( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+
+ player.PlayerCone_SetMinYaw( -179 )
+ player.PlayerCone_SetMaxYaw( 181 )
+ player.PlayerCone_SetMinPitch( -30 )
+ player.PlayerCone_SetMaxPitch( 30 )
+}
+
+void function ViewConeLockedForward( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+
+ player.PlayerCone_SetMinYaw( -89 )
+ player.PlayerCone_SetMaxYaw( 81 )
+ player.PlayerCone_SetMinPitch( -30 )
+ player.PlayerCone_SetMaxPitch( 60 )
+}
+
+void function ViewConeFree( entity player )
+{
+ if ( !player.IsPlayer() )
+ return
+ player.PlayerCone_SetLerpTime( 0.5 )
+
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -179 )
+ player.PlayerCone_SetMaxYaw( 181 )
+ player.PlayerCone_SetMinPitch( -60 )
+ player.PlayerCone_SetMaxPitch( 60 )
+}
+
+void function InitView( entity player, int pitch, int yaw, void functionref( entity )callFunction )
+{
+ if ( !player.IsPlayer() )
+ return
+
+ //have we already init the viewcone from this function before
+ if ( IsViewConeCurrent( player, callFunction ) )
+ return
+
+ player.EndSignal( "OnDestroy" )
+
+ entity dropship = player.GetParent()
+
+ while( !dropship )
+ {
+ wait 0.05
+ dropship = player.GetParent()
+ }
+
+ for ( int i = 0; i < 5; i++ )
+ {
+ player.SetLocalAngles( Vector( pitch, yaw, 0 ) )
+ wait 0.1
+ }
+}
+
+bool function IsViewConeCurrent( entity actor, void functionref(entity ) func )
+{
+ entity player = actor
+
+ if ( !IsValid( player ) )
+ return false
+
+ Assert( player.IsPlayer() )
+
+ if ( player.p.currViewConeFunction == func )
+ return true
+
+ player.p.currViewConeFunction = func
+ return false
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_vscript.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_vscript.gnut
new file mode 100644
index 00000000..52b69c5d
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_vscript.gnut
@@ -0,0 +1,84 @@
+untyped
+
+//========== Copyright © 2008, Valve Corporation, All rights reserved. ========
+
+global function UniqueString
+global function EntFire
+global function __DumpScope
+
+
+int __uniqueStringId = 0
+string function UniqueString( string str = "" )
+{
+ return str + "_us" + __uniqueStringId++;
+}
+
+function EntFire( target, action, value = null, delay = 0.0, activator = null )
+{
+ if ( !value )
+ {
+ value = "";
+ }
+
+ local caller = null;
+ if ( "self" in this )
+ {
+ caller = this.self;
+ if ( !activator )
+ {
+ activator = this.self;
+ }
+ }
+
+ DoEntFire( string( target ), string( action ), string( value ), delay, activator, caller );
+}
+
+//---------------------------------------------------------
+// Text dump this scope's contents to the console.
+//---------------------------------------------------------
+void function __DumpScope( int depth, var Table )
+{
+ local indent=function( count )
+ {
+ local i;
+ for( i = 0 ; i < count ; i++ )
+ {
+ print(" ");
+ }
+ }
+
+ foreach(key, value in Table)
+ {
+ indent(depth);
+ print( key );
+ switch (type(value))
+ {
+ case "table":
+ print("(TABLE)\n");
+ indent(depth);
+ print("{\n");
+ __DumpScope( depth + 1, value);
+ indent(depth);
+ print("}");
+ break;
+ case "array":
+ print("(ARRAY)\n");
+ indent(depth);
+ print("[\n")
+ __DumpScope( depth + 1, value);
+ indent(depth);
+ print("]");
+ break;
+ case "string":
+ print(" = \"");
+ print(value);
+ print("\"");
+ break;
+ default:
+ print(" = ");
+ print(value);
+ break;
+ }
+ print("\n");
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_xp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_xp.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/_xp.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_boss_titan.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_boss_titan.gnut
new file mode 100644
index 00000000..da3058d7
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_boss_titan.gnut
@@ -0,0 +1,794 @@
+global function PlayerParentTest
+
+global function AIBossTitan_Init
+global function OnBossTitanPrimaryFire
+global function IsVDUTitan
+global function IsBossTitan
+global function GetBossTitanCharacterModel
+
+global function BossTitanRetreat
+global function BossTitanAdvance
+global function IsMercTitan
+global function GetMercCharacterID
+global function BossTitanIntro
+global function BossTitanVDUEnabled
+global function BossTitanPlayerView
+
+global function MakeMidHealthTitan
+
+global const float SLAMZOOM_TIME = 1.0
+global const float BOSS_TITAN_CORE_DAMAGE_SCALER_LOW = 0.6
+global const float BOSS_TITAN_CORE_DAMAGE_SCALER = 0.5
+
+void function AIBossTitan_Init()
+{
+ if ( IsMultiplayer() )
+ return
+
+ FlagInit( "BossTitanViewFollow" )
+
+ AddSpawnCallback( "npc_titan", NPCTitanSpawned )
+ AddDeathCallback( "npc_titan", OnBossTitanDeath )
+ AddCallback_OnTitanDoomed( OnBossTitanDoomed )
+ AddCallback_OnTitanHealthSegmentLost( OnTitanLostSegment )
+
+ AddSyncedMeleeServerCallback( GetSyncedMeleeChooser( "titan", "titan" ), OnBossTitanExecuted )
+
+ PrecacheParticleSystem( $"P_VDU_mflash" )
+
+ RegisterSignal( "BossTitanStartAnim" )
+ RegisterSignal( "BossTitanIntroEnded" )
+}
+
+void function OnBossTitanExecuted( SyncedMeleeChooser actions, SyncedMelee action, entity attacker, entity victim )
+{
+ if ( victim.IsNPC() && IsVDUTitan( victim ) && BossTitanVDUEnabled( victim ) )
+ {
+ string name = victim.ai.bossCharacterName == "" ? "Generic1" : victim.ai.bossCharacterName
+ int bossID = GetBossTitanID( name )
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( player == attacker || IsMercTitan( victim ) )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanDeath", victim.GetEncodedEHandle(), bossID )
+ }
+ }
+ }
+}
+
+void function OnBossTitanDeath( entity titan, var damageInfo )
+{
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( damageSourceId == eDamageSourceId.titan_execution )
+ return
+
+ entity soul = titan.GetTitanSoul()
+ if ( soul.IsEjecting() )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( IsVDUTitan( titan ) && BossTitanVDUEnabled( titan ) )
+ {
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( player == attacker || IsMercTitan( titan ) )
+ {
+ string name = titan.ai.bossCharacterName == "" ? "Generic1" : titan.ai.bossCharacterName
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanDeath", titan.GetEncodedEHandle(), GetBossTitanID( name ) )
+ }
+ }
+ }
+}
+
+void function OnBossTitanDoomed( entity titan, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( IsVDUTitan( titan ) && BossTitanVDUEnabled( titan ) )
+ {
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( player == attacker || IsMercTitan( titan ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanDoomed", titan.GetEncodedEHandle() )
+ }
+ }
+}
+
+void function OnBossTitanCoreMitigation( entity titan, var damageInfo )
+{
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ switch ( damageSourceID )
+ {
+ case eDamageSourceId.mp_titancore_salvo_core:
+ DamageInfo_ScaleDamage( damageInfo, BOSS_TITAN_CORE_DAMAGE_SCALER_LOW )
+ return
+
+ // case eDamageSourceId.mp_titancore_laser_cannon: laser core handles this in mp_titanweapon_lasercannon.nut
+ case eDamageSourceId.mp_titancore_flame_wave:
+ case eDamageSourceId.mp_titancore_flame_wave_secondary:
+ case eDamageSourceId.mp_titancore_shift_core:
+ case eDamageSourceId.mp_titanweapon_flightcore_rockets:
+ case eDamageSourceId.mp_titancore_amp_core:
+ case damagedef_nuclear_core:
+ DamageInfo_ScaleDamage( damageInfo, BOSS_TITAN_CORE_DAMAGE_SCALER )
+ return
+ }
+
+ // SMART CORE
+ array<string> weaponMods = GetWeaponModsFromDamageInfo( damageInfo )
+ if ( weaponMods.contains( "Smart_Core" ) )
+ {
+ DamageInfo_ScaleDamage( damageInfo, BOSS_TITAN_CORE_DAMAGE_SCALER )
+ // DamageInfo_ScaleDamage( damageInfo, BOSS_TITAN_CORE_DAMAGE_SCALER_LOW )
+ return
+ }
+}
+
+void function NPCTitanSpawned( entity titan )
+{
+ Assert( !IsMultiplayer() )
+
+ if ( titan.GetTeam() == TEAM_IMC )
+ {
+ switch ( titan.ai.bossTitanType )
+ {
+ case TITAN_WEAK:
+ case TITAN_HENCH:
+ MakeMidHealthTitan( titan )
+
+ case TITAN_BOSS:
+ RegisterBossTitan( titan )
+ ApplyTitanDamageState( titan )
+
+ if ( titan.ai.bossTitanType == TITAN_BOSS )
+ AddEntityCallback_OnDamaged( titan, OnBossTitanCoreMitigation )
+
+ if ( titan.HasKey( "skip_boss_intro" ) && titan.GetValueForKey( "skip_boss_intro" ) == "1" )
+ return
+ thread BossTitanNoIntro( titan )
+ break;
+
+
+ case TITAN_MERC:
+ // TODO: This SetSkin() call should move to RegisterBossTitan() when the above TITAN_BOSS stuff is cleaned up/removed.
+ titan.SetSkin( 1 ) // all titan models have a boss titan version of the skin at index 1
+ RegisterBossTitan( titan )
+ ApplyTitanDamageState( titan )
+
+ AddEntityCallback_OnDamaged( titan, OnBossTitanCoreMitigation )
+
+ if ( titan.HasKey( "skip_boss_intro" ) && titan.GetValueForKey( "skip_boss_intro" ) == "1" )
+ return
+
+ if ( !titan.ai.bossTitanPlayIntro )
+ return
+
+ foreach ( player in GetPlayerArray() )
+ {
+ thread BossTitanIntro( player, titan )
+ }
+ break
+
+ // case TITAN_WEAK:
+ // MakeLowHealthTitan( titan )
+ // break
+
+ case TITAN_AUTO:
+ if ( !IsMultiplayer() && GetMapName() == "sp_hub_timeshift" || GetMapName() == "sp_timeshift_spoke02" )
+ MakeLowHealthTitan( titan )
+ break
+ default:
+ return
+ }
+ }
+}
+
+void function BossTitanNoIntro( entity titan )
+{
+ FlagWait( "PlayerDidSpawn" )
+
+ entity player = GetPlayerArray()[0]
+
+ player.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDeath" )
+
+ // Wait until player sees the boss titan
+ waitthread WaitForHotdropToEnd( titan )
+
+ while ( 1 )
+ {
+ waitthread WaitTillLookingAt( player, titan, true, 60, 5100 )
+ if ( titan.GetEnemy() == null )
+ titan.WaitSignal( "OnSeeEnemy" )
+ else
+ break
+ }
+
+ if ( BossTitanVDUEnabled( titan ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanNoIntro", titan.GetEncodedEHandle() )
+ AddEntityCallback_OnDamaged( titan, OnBossTitanDamaged )
+ AddTitanCallback_OnHealthSegmentLost( titan, OnBossTitanLostSegment )
+}
+
+void function BossTitanIntro( entity player, entity titan, BossTitanIntroData ornull introdata = null )
+{
+ Assert( titan.IsNPC() )
+ Assert( titan.ai.bossCharacterName != "" )
+
+ if ( introdata == null )
+ {
+ BossTitanIntroData defaultData = GetBossTitanIntroData( titan.ai.bossCharacterName )
+ introdata = defaultData
+ }
+
+ expect BossTitanIntroData( introdata )
+
+ player.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDeath" )
+
+ HideCrit( titan )
+ titan.SetValidHealthBarTarget( false )
+ titan.SetInvulnerable()
+
+ // Wait until player sees the boss titan
+
+ while ( titan.e.isHotDropping )
+ {
+ WaitFrame()
+ }
+
+ HideName( titan )
+ titan.kv.allowshoot = 0
+
+ if ( introdata.waitToStartFlag != "" )
+ FlagWait( introdata.waitToStartFlag )
+
+ if ( introdata.waitForLookat )
+ waitthread WaitTillLookingAt( player, titan, introdata.lookatDoTrace, introdata.lookatDegrees, introdata.lookatMinDist )
+
+ while ( IsPlayerDisembarking( player ) || IsPlayerEmbarking( player ) )
+ {
+ WaitFrame()
+ }
+
+ BossTitanData bossTitanData = GetBossTitanData( titan.ai.bossCharacterName )
+
+ // Create a ref node to animate on
+ vector refPos
+ vector refAngles
+
+ if ( bossTitanData.introAnimTitanRef != "" )
+ {
+ entity titanAnimRef = GetEntByScriptName( bossTitanData.introAnimTitanRef )
+ refPos = titanAnimRef.GetOrigin()
+ refAngles = titanAnimRef.GetAngles()
+ }
+ else
+ {
+ refPos = titan.GetOrigin()
+
+ vector vecToPlayer = Normalize( player.GetOrigin() - titan.GetOrigin() )
+ refAngles = VectorToAngles( vecToPlayer )
+ refAngles = FlattenAngles( refAngles )
+ }
+
+ entity ref
+ if ( introdata.parentRef != null )
+ {
+ ref = introdata.parentRef
+ }
+ else
+ ref = CreateScriptRef( refPos, refAngles )
+
+ entity soul = titan.GetTitanSoul()
+ if ( IsValid( soul.soul.bubbleShield ) )
+ {
+ soul.soul.bubbleShield.Destroy()
+ }
+
+ // Freeze player and clear up the screen
+ StartBossIntro( player, titan, introdata )
+ player.Hide()
+ player.SetVelocity( <0,0,0> )
+ player.FreezeControlsOnServer()
+ player.SetNoTarget( true )
+ player.SetInvulnerable()
+
+ // Do special player view movement
+ FlagSet( "BossTitanViewFollow" )
+
+ // Animate the boss titan
+ entity pilot = CreatePropDynamic( GetBossTitanCharacterModel( titan ) )
+ if ( introdata.parentRef != null )
+ {
+ if ( introdata.parentAttach != "" )
+ {
+ pilot.SetParent( introdata.parentRef, introdata.parentAttach )
+ }
+ else
+ {
+ pilot.SetParent( introdata.parentRef )
+ }
+ }
+ SetTeam( pilot, TEAM_IMC )
+
+ string pilotAnimName = bossTitanData.introAnimPilot
+ string titanAnimName = bossTitanData.introAnimTitan
+
+ float introDuration = 6.0
+
+ Assert( titan.Anim_HasSequence( titanAnimName ), "Your boss titan does not have an intro animation set, or it is missing." )
+
+ introDuration = titan.GetSequenceDuration( titanAnimName )
+
+ svGlobal.levelEnt.Signal( "BossTitanStartAnim" )
+
+ if ( introdata.parentAttach != "" )
+ {
+ thread PlayAnim( pilot, pilotAnimName, ref, introdata.parentAttach, 0.0 )
+ thread PlayAnim( titan, titanAnimName, ref, introdata.parentAttach, 0.0 )
+ }
+ else
+ {
+ thread PlayAnim( pilot, pilotAnimName, ref, 0.0 )
+ thread PlayAnim( titan, titanAnimName, ref, 0.0 )
+ }
+
+ Objective_Hide( player )
+
+ thread BossTitanPlayerView( player, titan, ref, bossTitanData.titanCameraAttachment )
+
+ wait introDuration - SLAMZOOM_TIME
+
+ // Player view returns to normal
+ FlagClear( "BossTitanViewFollow" )
+ EndBossIntro( player, titan )
+
+ wait SLAMZOOM_TIME
+
+ // Return the player screen and movement back to normal
+ player.UnfreezeControlsOnServer()
+ player.SetNoTarget( false )
+ player.ClearInvulnerable()
+ player.Show()
+ pilot.Destroy()
+
+ if ( IsValid( titan ) )
+ {
+ titan.ClearInvulnerable()
+ titan.Solid()
+ AddEntityCallback_OnDamaged( titan, OnBossTitanDamaged )
+ AddTitanCallback_OnHealthSegmentLost( titan, OnBossTitanLostSegment )
+ ShowName( titan )
+ titan.SetValidHealthBarTarget( true )
+ ShowCrit( titan )
+ Signal( titan, "BossTitanIntroEnded" )
+ }
+
+ wait 0.5
+
+ if ( Flag( "AutomaticCheckpointsEnabled" ) )
+ {
+ if ( introdata.checkpointOnlyIfPlayerTitan )
+ {
+ if ( player.IsTitan() )
+ CheckPoint_Forced()
+ }
+ else
+ CheckPoint_Forced()
+ }
+
+ wait 1.0
+
+ titan.kv.allowshoot = 1
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanPostIntro", titan.GetEncodedEHandle(), BossTitanVDUEnabled( titan ) )
+}
+
+void function PlayerParentTest()
+{
+ entity player = GetPlayerArray()[0]
+
+ vector moverStartPos = player.EyePosition()
+ vector moverStartAng = FlattenAngles( player.GetAngles() )
+ entity mover = CreateScriptMover( moverStartPos, moverStartAng )
+
+ player.SnapEyeAngles( moverStartAng )
+ player.SetParent( mover, "", true )
+}
+
+void function BossTitanPlayerView( entity player, entity titan, entity ref, string titanCameraAttachment )
+{
+ bool hasTitanCameraAttachment = titanCameraAttachment != ""
+
+ EndSignal( player, "OnDeath" )
+ EndSignal( titan, "OnDeath" )
+
+ vector moverStartPos = player.CameraPosition()
+
+ vector camFeetDiff = < 0,0,-185 >//player.GetOrigin() - player.CameraPosition()
+
+ vector moverStartAng = player.CameraAngles()
+ entity mover = CreateScriptMover( moverStartPos, moverStartAng )
+
+ // player.SnapEyeAngles( moverStartAng )
+ // player.SetParent( mover, "", true )
+ // ViewConeZero( player )
+
+ entity camera = CreateEntity( "point_viewcontrol" )
+ camera.kv.spawnflags = 56 // infinite hold time, snap to goal angles, make player non-solid
+
+ camera.SetOrigin( player.CameraPosition() )
+ camera.SetAngles( player.CameraAngles() )
+ DispatchSpawn( camera )
+
+ camera.SetParent( mover, "", false )
+
+ OnThreadEnd(
+ function() : ( player, titan, mover, camera )
+ {
+ if ( IsValid( camera ) )
+ {
+ camera.Destroy()
+ }
+
+ mover.Destroy()
+
+ if ( IsValid( player ) )
+ {
+ player.ClearParent()
+ player.ClearViewEntity()
+ RemoveCinematicFlag( player, CE_FLAG_HIDE_MAIN_HUD )
+ RemoveCinematicFlag( player, CE_FLAG_TITAN_3P_CAM )
+ }
+
+ if ( IsAlive( titan ) && titan.IsNPC() )
+ {
+ titan.SetNoTarget( false )
+ titan.DisableNPCFlag( NPC_IGNORE_ALL )
+ }
+ }
+ )
+
+ // Slam Zoom In
+ float slamZoomTime = SLAMZOOM_TIME
+ float slamZoomTimeAccel = 0.3
+ float slamZoomTimeDecel = 0.3
+ vector viewOffset = < 200, 100, 160 >
+
+ vector viewPos = ref.GetOrigin() + ( AnglesToForward( ref.GetAngles() ) * viewOffset.x ) + ( AnglesToRight( ref.GetAngles() ) * viewOffset.y ) + ( AnglesToUp( ref.GetAngles() ) * viewOffset.z )
+ vector viewAngles = ref.GetAngles() + <0,180,0>
+ if ( hasTitanCameraAttachment )
+ {
+ WaitFrame()
+ int titanCameraAttachmentID = titan.LookupAttachment( titanCameraAttachment )
+ viewPos = titan.GetAttachmentOrigin( titanCameraAttachmentID )
+ viewAngles = titan.GetAttachmentAngles( titanCameraAttachmentID )
+ }
+
+ float blendTime = 0.5
+ float waittime = 0.3
+ float moveTime = slamZoomTime - blendTime - waittime
+
+ float startTime = Time()
+
+ player.SetVelocity( < 0,0,0 > )
+ player.MakeInvisible()
+ HolsterAndDisableWeapons( player )
+
+ wait waittime // wait for the AI to blend into the anim
+
+ if ( titan.IsNPC() )
+ {
+ titan.SetNoTarget( true )
+ titan.EnableNPCFlag( NPC_IGNORE_ALL )
+ }
+
+ AddCinematicFlag( player, CE_FLAG_HIDE_MAIN_HUD )
+ AddCinematicFlag( player, CE_FLAG_TITAN_3P_CAM )
+
+ mover.SetOrigin( player.CameraPosition() )
+ mover.SetAngles( player.CameraAngles() )
+ player.SetViewEntity( camera, true )
+
+ player.SetPredictionEnabled( false )
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ player.SetPredictionEnabled( true )
+ }
+ )
+
+ while ( Time() - startTime < moveTime )
+ {
+ if ( hasTitanCameraAttachment )
+ {
+ int titanCameraAttachmentID = titan.LookupAttachment( titanCameraAttachment )
+ viewPos = titan.GetAttachmentOrigin( titanCameraAttachmentID )
+ viewAngles = titan.GetAttachmentAngles( titanCameraAttachmentID )
+ }
+ mover.NonPhysicsMoveTo( viewPos, moveTime - (Time() - startTime), 0, 0 )
+ mover.NonPhysicsRotateTo( viewAngles, moveTime - (Time() - startTime), 0, 0 )
+ wait 0.1
+ }
+
+ if ( hasTitanCameraAttachment )
+ {
+ mover.SetParent( titan, titanCameraAttachment, false, blendTime )
+ }
+
+ wait 0.5
+
+ int tagID = titan.LookupAttachment( "CHESTFOCUS" )
+ while ( Flag( "BossTitanViewFollow" ) )
+ {
+ vector lookVec = Normalize( titan.GetAttachmentOrigin( tagID ) - mover.GetOrigin() )
+ vector angles = VectorToAngles( lookVec )
+ if ( !hasTitanCameraAttachment )
+ mover.NonPhysicsRotateTo( angles, 0.2, 0.0, 0.0 )
+ WaitFrame()
+ }
+
+ // Slam Zoom Out
+
+ mover.ClearParent()
+
+ startTime = Time()
+ while ( Time() - startTime < slamZoomTime )
+ {
+ moverStartPos = player.GetOrigin() - camFeetDiff
+ moverStartAng = FlattenAngles( player.GetAngles() )
+ mover.NonPhysicsMoveTo( moverStartPos, slamZoomTime - (Time() - startTime), 0, 0 )
+ mover.NonPhysicsRotateTo( moverStartAng, slamZoomTime - (Time() - startTime), 0, 0 )
+ wait 0.1
+ }
+
+ // mover.NonPhysicsMoveTo( moverStartPos, slamZoomTime, slamZoomTimeDecel, slamZoomTimeAccel )
+ // mover.NonPhysicsRotateTo( moverStartAng, slamZoomTime, slamZoomTimeDecel, slamZoomTimeAccel )
+ // wait slamZoomTime
+
+ ClearPlayerAnimViewEntity( player )
+ player.SnapEyeAngles( moverStartAng )
+ DeployAndEnableWeapons( player )
+ player.MakeVisible()
+
+ EmitSoundOnEntity( player, "UI_Lobby_RankChip_Disable" )
+}
+
+void function OnBossTitanDamaged( entity titan, var damageInfo )
+{
+}
+
+void function OnBossTitanLostSegment( entity titan, entity attacker )
+{
+ if ( !titan.IsNPC() || !BossTitanVDUEnabled( titan ) )
+ return
+
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( player == attacker || IsMercTitan( titan ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanLostSegment", titan.GetEncodedEHandle(), GetTitanCurrentRegenTab( titan ) )
+ }
+}
+
+void function OnBossTitanPrimaryFire( entity titan )
+{
+}
+
+bool function IsVDUTitan( entity titan )
+{
+ Assert( IsSingleplayer() )
+
+ if ( titan.GetTeam() != TEAM_IMC )
+ return false
+
+ switch ( titan.ai.bossTitanType )
+ {
+ case TITAN_AUTO:
+ case TITAN_WEAK:
+ return false
+
+ case TITAN_HENCH:
+ case TITAN_MERC:
+ case TITAN_BOSS:
+ return true
+ }
+
+ Assert( 0, "Unknown boss titan type " + titan.ai.bossTitanType )
+ unreachable
+}
+
+bool function IsBossTitan( entity titan )
+{
+ Assert( IsSingleplayer() )
+
+ if ( titan.GetTeam() != TEAM_IMC )
+ return false
+
+ switch ( titan.ai.bossTitanType )
+ {
+ case TITAN_MERC:
+ case TITAN_BOSS:
+ return true
+ }
+
+ return false
+}
+
+int function GetMercCharacterID( entity titan )
+{
+ return titan.ai.mercCharacterID
+}
+
+asset function GetBossTitanCharacterModel( entity titan )
+{
+ int mercCharacterID = GetMercCharacterID( titan )
+ return GetMercCharacterModel( mercCharacterID )
+}
+
+void function OnTitanLostSegment( entity titan, entity attacker )
+{
+ entity player
+
+ if ( !titan.IsPlayer() )
+ player = titan.GetBossPlayer()
+ else
+ player = titan
+
+ if ( !IsValid( player ) )
+ return
+
+ if ( !IsValid( attacker ) )
+ return
+
+ if ( !attacker.IsNPC() || !IsVDUTitan( attacker ) || !BossTitanVDUEnabled( attacker ) )
+ return
+
+ Remote_CallFunction_NonReplay( player, "BossTitanPlayerLostHealthSegment", GetSegmentHealthForTitan( titan ) )
+}
+
+void function BossTitanRetreat( entity titan )
+{
+ if ( !IsVDUTitan( titan ) || !BossTitanVDUEnabled( titan ) )
+ return
+
+ foreach ( player in GetPlayerArray() )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanRetreat", titan.GetEncodedEHandle() )
+ }
+}
+
+void function BossTitanAdvance( entity titan )
+{
+ if ( !IsVDUTitan( titan ) || !BossTitanVDUEnabled( titan ) )
+ return
+
+ foreach ( player in GetPlayerArray() )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanAdvance", titan.GetEncodedEHandle() )
+ }
+}
+
+/*
+------------------------------------------------------------
+Low Health Titans
+------------------------------------------------------------
+*/
+
+void function MakeLowHealthTitan( entity ent )
+{
+ entity soul = ent.GetTitanSoul()
+ soul.soul.regensHealth = false
+ thread SetHealthValuesForLowHealth( soul )
+ //ent.SetValidHealthBarTarget( false )
+
+ ent.TakeOffhandWeapon( OFFHAND_ORDNANCE )
+ ent.TakeOffhandWeapon( OFFHAND_ANTIRODEO )
+ ent.TakeOffhandWeapon( OFFHAND_EQUIPMENT )
+ ent.TakeOffhandWeapon( OFFHAND_SPECIAL )
+}
+
+void function MakeMidHealthTitan( entity ent )
+{
+ entity soul = ent.GetTitanSoul()
+ soul.soul.regensHealth = false
+ thread SetHealthValuesForMidHealth( soul )
+}
+
+void function SetHealthValuesForMidHealth( entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ WaitEndFrame() // wait for a bunch of variables to start up
+ soul.Signal( SIGNAL_TITAN_HEALTH_REGEN )
+ soul.Signal( "StopShieldRegen" )
+ soul.SetShieldHealth( 0 )
+
+ entity titan = soul.GetTitan()
+ int numSegments = ( titan.GetMaxHealth() / GetSegmentHealthForTitan( titan ) ) - 2
+ Assert( numSegments > 0 )
+ SetSoulBatteryCount( soul, numSegments )
+ if ( IsAlive( titan ) )
+ {
+ soul.soul.skipDoomState = true
+ int segmentHealth = GetSegmentHealthForTitan( titan ) * numSegments
+ titan.SetMaxHealth( segmentHealth )
+ titan.SetHealth( segmentHealth )
+ titan.kv.healthEvalMultiplier = 2
+ }
+
+ titan.Signal( "WeakTitanHealthInitialized" )
+
+ ApplyTitanDamageState( titan )
+}
+
+void function SetHealthValuesForLowHealth( entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ WaitEndFrame() // wait for a bunch of variables to start up
+ soul.Signal( SIGNAL_TITAN_HEALTH_REGEN )
+ soul.Signal( "StopShieldRegen" )
+ soul.SetShieldHealth( 0 )
+
+ int numSegments = 2
+
+ SetSoulBatteryCount( soul, numSegments )
+ entity titan = soul.GetTitan()
+ if ( IsAlive( titan ) )
+ {
+ soul.soul.skipDoomState = true
+ int segmentHealth = GetSegmentHealthForTitan( titan ) * numSegments
+ titan.SetMaxHealth( segmentHealth )
+ titan.SetHealth( segmentHealth )
+ titan.kv.healthEvalMultiplier = 2
+ }
+
+ titan.Signal( "WeakTitanHealthInitialized" )
+
+ ApplyTitanDamageState( titan )
+}
+
+void function ApplyTitanDamageState( entity titan )
+{
+ array<float> healthScale = [
+ 1.0,
+ 0.6,
+ 0.3,
+ 0.1
+ ]
+
+ int state = 0
+
+ if ( titan.HasKey( "DamageState" ) )
+ {
+ state = int( titan.GetValueForKey( "DamageState" ) )
+ }
+
+ titan.SetHealth( titan.GetMaxHealth() * healthScale[state] )
+
+ if ( state >= 1 )
+ {
+ string part = [
+ "left_arm",
+ "right_arm"
+ ].getrandom()
+ GibBodyPart( titan, part )
+ }
+
+ if ( state >= 2 )
+ GibBodyPart( titan, "torso" )
+}
+
+bool function IsMercTitan( entity titan )
+{
+ if ( IsMultiplayer() )
+ return false
+ if ( titan.GetTeam() != TEAM_IMC )
+ return false
+ return titan.ai.bossTitanType == TITAN_MERC
+}
+
+bool function BossTitanVDUEnabled( entity titan )
+{
+ return titan.ai.bossTitanVDUEnabled
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_chatter.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_chatter.gnut
new file mode 100644
index 00000000..0429895b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_chatter.gnut
@@ -0,0 +1,129 @@
+global function DialogueChatter_Init
+
+global function TitanVO_AlertTitansIfTargetWasKilled
+global function TitanVO_TellPlayersThatAreAlsoFightingThisTarget
+global function TitanVO_AlertTitansTargetingThisTitanOfRodeo
+global function TitanVO_DelayedTitanDown
+
+const TITAN_VO_DIST_SQR = 2000 * 2000
+
+const CHATTER_TIME_LAPSE = 30.0
+//const CHATTER_TIME_LAPSE = 5.0 //For testing
+//const CHATTER_TIME_LAPSE = 8.0 //For testing
+//const CHATTER_TIME_LAPSE = 15.0 //For testing
+
+void function DialogueChatter_Init()
+{
+}
+
+void function TitanVO_TellPlayersThatAreAlsoFightingThisTarget( entity attacker, entity soul )
+{
+ int voEnum
+ if ( attacker.IsTitan() )
+ voEnum = eTitanVO.FRIENDLY_TITAN_HELPING
+ else
+ voEnum = eTitanVO.PILOT_HELPING
+
+ bool atackerIsTitan = attacker.IsTitan()
+ int attackerTeam = attacker.GetTeam()
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ if ( !player.IsTitan() )
+ continue
+
+ if ( player.GetTeam() != attackerTeam )
+ continue
+ // attacker gets a score callout
+ if ( player == attacker )
+ continue
+
+ if ( soul != player.p.currentTargetPlayerOrSoul_Ent )
+ continue
+
+ float timeDif = Time() - player.p.currentTargetPlayerOrSoul_LastHitTime
+ if ( timeDif > CURRENT_TARGET_FORGET_TIME )
+ continue
+
+ // alert other player that cared about this target
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", voEnum )
+ }
+}
+
+void function TitanVO_AlertTitansTargetingThisTitanOfRodeo( entity rodeoer, entity soul )
+{
+ int team = rodeoer.GetTeam()
+
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ if ( !player.IsTitan() )
+ continue
+
+ if ( player.GetTeam() != team )
+ continue
+
+ if ( soul != player.p.currentTargetPlayerOrSoul_Ent )
+ continue
+
+ // if we havent hurt the target recently then forget about it
+ if ( Time() - player.p.currentTargetPlayerOrSoul_LastHitTime > CURRENT_TARGET_FORGET_TIME )
+ continue
+
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", eTitanVO.FRIENDLY_RODEOING_ENEMY )
+ }
+}
+
+void function TitanVO_DelayedTitanDown( entity ent )
+{
+ vector titanOrigin = ent.GetOrigin()
+ int team = ent.GetTeam()
+
+ wait 0.9
+
+ array<entity> playerArray = GetPlayerArray()
+ float dist = TITAN_VO_DIST_SQR
+
+ foreach ( player in playerArray )
+ {
+ // only titans get BB vo
+ if ( !player.IsTitan() )
+ continue
+
+ if ( DistanceSqr( titanOrigin, player.GetOrigin() ) > dist )
+ continue
+
+ if ( player.GetTeam() != team )
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", eTitanVO.ENEMY_TITAN_DEAD )
+ else
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", eTitanVO.FRIENDLY_TITAN_DEAD )
+ }
+}
+
+
+void function TitanVO_AlertTitansIfTargetWasKilled( entity victim, entity attacker )
+{
+ array<entity> enemyPlayers = GetPlayerArrayOfEnemies( victim.GetTeam() )
+
+ if ( victim.IsTitan() )
+ victim = victim.GetTitanSoul()
+
+ foreach ( player in enemyPlayers )
+ {
+ if ( !player.IsTitan() )
+ continue
+
+ // attacker gets a score callout
+ if ( player == attacker )
+ continue
+
+ if ( victim != player.p.currentTargetPlayerOrSoul_Ent )
+ continue
+
+ if ( Time() - player.p.currentTargetPlayerOrSoul_LastHitTime > CURRENT_TARGET_FORGET_TIME )
+ continue
+
+ // alert other player that cared about this target
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", eTitanVO.ENEMY_TARGET_ELIMINATED )
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_cloak_drone.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_cloak_drone.gnut
new file mode 100644
index 00000000..e3addf81
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_cloak_drone.gnut
@@ -0,0 +1,678 @@
+untyped
+
+global function CloakDrone_Init
+
+global function SpawnCloakDrone
+global function GetNPCCloakedDrones
+global function RemoveLeftoverCloakedDrones
+const FX_DRONE_CLOAK_BEAM = $"P_drone_cloak_beam"
+
+const float CLOAK_DRONE_REACHED_HARVESTER_DIST = 1300.0
+
+struct
+{
+ int cloakedDronesManagedEntArrayID
+ table<entity,string> cloakedDroneClaimedSquadList
+} file
+
+struct CloakDronePath
+{
+ vector start
+ vector goal
+ bool goalValid = false
+ float lastHeight
+}
+
+function CloakDrone_Init()
+{
+ PrecacheParticleSystem( FX_DRONE_CLOAK_BEAM )
+
+ file.cloakedDronesManagedEntArrayID = CreateScriptManagedEntArray()
+
+ RegisterSignal( "DroneCleanup" )
+ RegisterSignal( "DroneCrashing" )
+}
+
+entity function SpawnCloakDrone( int team, vector origin, vector angles, vector towerOrigin )
+{
+ int droneCount = GetNPCCloakedDrones().len()
+
+ // add some minor randomness to the spawn location as well as an offset based on number of drones in the world.
+ origin += < RandomIntRange( -64, 64 ), RandomIntRange( -64, 64 ), 300 + (droneCount * 128) >
+
+ entity cloakedDrone = CreateGenericDrone( team, origin, angles )
+ SetSpawnOption_AISettings( cloakedDrone, "npc_drone_cloaked" )
+
+ //these enable global damage callbacks for the cloakedDrone
+ cloakedDrone.s.isHidden <- false
+ cloakedDrone.s.fx <- null
+ cloakedDrone.s.towerOrigin <- towerOrigin
+
+ DispatchSpawn( cloakedDrone )
+ SetTeam( cloakedDrone, team )
+ SetTargetName( cloakedDrone, "Cloak Drone" )
+ cloakedDrone.SetTitle( "#NPC_CLOAK_DRONE" )
+ cloakedDrone.SetMaxHealth( 250 )
+ cloakedDrone.SetHealth( 250 )
+ cloakedDrone.SetTakeDamageType( DAMAGE_YES )
+ cloakedDrone.SetDamageNotifications( true )
+ cloakedDrone.SetDeathNotifications( true )
+ cloakedDrone.Solid()
+ cloakedDrone.Show()
+ cloakedDrone.EnableNPCFlag( NPC_IGNORE_ALL )
+
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_HOVER_LOOP_SFX )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_LOOPING_SFX )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_WARP_IN_SFX )
+
+ cloakedDrone.s.fx = CreateDroneCloakBeam( cloakedDrone )
+
+ SetVisibleEntitiesInConeQueriableEnabled( cloakedDrone, true )
+
+ thread CloakedDronePathThink( cloakedDrone )
+ thread CloakedDroneCloakThink( cloakedDrone )
+
+ #if R1_VGUI_MINIMAP
+ cloakedDrone.Minimap_SetDefaultMaterial( $"vgui/hud/cloak_drone_minimap_orange" )
+ #endif
+ cloakedDrone.Minimap_SetAlignUpright( true )
+ cloakedDrone.Minimap_AlwaysShow( TEAM_IMC, null )
+ cloakedDrone.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ cloakedDrone.Minimap_SetObjectScale( MINIMAP_CLOAKED_DRONE_SCALE )
+ cloakedDrone.Minimap_SetZOrder( MINIMAP_Z_NPC )
+
+ ShowName( cloakedDrone )
+
+ AddToGlobalCloakedDroneList( cloakedDrone )
+ return cloakedDrone
+}
+
+function AddToGlobalCloakedDroneList( cloakedDrone )
+{
+ AddToScriptManagedEntArray( file.cloakedDronesManagedEntArrayID, cloakedDrone )
+}
+
+array<entity> function GetNPCCloakedDrones()
+{
+ return GetScriptManagedEntArray( file.cloakedDronesManagedEntArrayID )
+}
+
+function RemoveLeftoverCloakedDrones()
+{
+ array<entity> droneArray = GetNPCCloakedDrones()
+ foreach ( cloakedDrone in droneArray )
+ {
+ thread CloakedDroneWarpOutAndDestroy( cloakedDrone )
+ }
+}
+
+void function CloakedDroneWarpOutAndDestroy( entity cloakedDrone )
+{
+ cloakedDrone.EndSignal( "OnDestroy" )
+ cloakedDrone.EndSignal( "OnDeath" )
+ cloakedDrone.SetInvulnerable()
+
+ CloakedDroneWarpOut( cloakedDrone, cloakedDrone.GetOrigin() )
+ cloakedDrone.Destroy()
+}
+
+/************************************************************************************************\
+
+ ###### ## ####### ### ## ## #### ## ## ######
+## ## ## ## ## ## ## ## ## ## ### ## ## ##
+## ## ## ## ## ## ## ## ## #### ## ##
+## ## ## ## ## ## ##### ## ## ## ## ## ####
+## ## ## ## ######### ## ## ## ## #### ## ##
+## ## ## ## ## ## ## ## ## ## ## ### ## ##
+ ###### ######## ####### ## ## ## ## #### ## ## ######
+
+\************************************************************************************************/
+//HACK - this should probably move into code
+function CloakedDroneCloakThink( cloakedDrone )
+{
+ expect entity( cloakedDrone )
+
+ cloakedDrone.EndSignal( "OnDestroy" )
+ cloakedDrone.EndSignal( "OnDeath" )
+ cloakedDrone.EndSignal( "DroneCrashing" )
+ cloakedDrone.EndSignal( "DroneCleanup" )
+
+ wait 2 // wait a few seconds since it would start cloaking before picking an npc to follow
+ // some npcs might not be picked since they where already cloaked by accident.
+
+ CloakerThink( cloakedDrone, 400.0, [ "any" ], < 0, 0, -350 >, CloakDroneShouldCloakGuy, 1.5 )
+}
+
+function CloakDroneShouldCloakGuy( cloakedDrone, guy )
+{
+ expect entity( guy )
+ if ( !( guy.IsTitan() || IsSpectre( guy ) || IsGrunt( guy ) || IsSuperSpectre( guy ) ) )
+ return false
+
+ if ( guy.GetTargetName() == "empTitan" )
+ return false
+
+ if ( IsSniperSpectre( guy ) )
+ return false
+
+ if ( IsValid( GetRodeoPilot( guy ) ) )
+ return false
+
+ if ( cloakedDrone.s.isHidden )
+ return false
+
+ if ( StatusEffect_Get( guy, eStatusEffect.sonar_detected ) )
+ return false
+
+ // if ( !cloakedDrone.CanSee( guy ) )
+ // return false
+
+ return true
+}
+
+/************************************************************************************************\
+
+######## ### ######## ## ## #### ## ## ######
+## ## ## ## ## ## ## ## ### ## ## ##
+## ## ## ## ## ## ## ## #### ## ##
+######## ## ## ## ######### ## ## ## ## ## ####
+## ######### ## ## ## ## ## #### ## ##
+## ## ## ## ## ## ## ## ### ## ##
+## ## ## ## ## ## #### ## ## ######
+
+\************************************************************************************************/
+//HACK -> this should probably move into code
+const VALIDPATHFRAC = 0.99
+
+void function CloakedDronePathThink( entity cloakedDrone )
+{
+ cloakedDrone.EndSignal( "OnDestroy" )
+ cloakedDrone.EndSignal( "OnDeath" )
+ cloakedDrone.EndSignal( "DroneCrashing" )
+ cloakedDrone.EndSignal( "DroneCleanup" )
+
+ entity goalNPC = null
+ entity previousNPC = null
+ vector spawnOrigin = cloakedDrone.GetOrigin()
+ vector lastOrigin = cloakedDrone.GetOrigin()
+ float stuckDistSqr = 64.0*64.0
+ float targetLostTime = Time()
+ array<entity> claimedGuys = []
+
+ while( 1 )
+ {
+ while( goalNPC == null )
+ {
+ wait 1.0
+ array<entity> testArray = GetNPCArrayEx( "any", cloakedDrone.GetTeam(), TEAM_ANY, < 0, 0, 0 >, -1 )
+
+ // remove guys already being followed by an cloakedDrone
+ // or in other ways not suitable
+ array<entity> NPCs = []
+ foreach ( guy in testArray )
+ {
+ if ( !IsAlive( guy ) )
+ continue
+
+ //Only cloak titans, spectres, grunts,
+ if ( !( guy.IsTitan() || IsSpectre( guy ) || IsGrunt( guy ) || IsSuperSpectre( guy ) ) )
+ continue
+
+ //Don't cloak arc titans
+ if ( guy.GetTargetName() == "empTitan" )
+ continue
+
+ if ( IsSniperSpectre( guy ) )
+ continue
+
+ if ( IsFragDrone( guy ) )
+ continue
+
+ if ( guy == previousNPC )
+ continue
+
+ if ( guy.ContextAction_IsBusy() )
+ continue
+
+ if ( guy.GetParent() != null )
+ continue
+
+ if ( IsCloaked( guy ) )
+ continue
+
+ if ( IsSquadCenterClose( guy ) == false )
+ continue
+
+ if ( "cloakedDrone" in guy.s && IsAlive( expect entity( guy.s.cloakedDrone ) ) )
+ continue
+
+ if ( CloakedDroneIsSquadClaimed( expect string( guy.kv.squadname ) ) )
+ continue
+
+ if ( IsValid( GetRodeoPilot( guy ) ) )
+ continue
+
+ if ( StatusEffect_Get( guy, eStatusEffect.sonar_detected ) )
+ continue
+
+ NPCs.append( guy )
+ }
+
+ if ( NPCs.len() == 0 )
+ {
+ previousNPC = null
+
+ if ( Time() - targetLostTime > 10 )
+ {
+ // couldn't find anything to cloak for 10 seconds so we'll warp out until we find something
+ if ( cloakedDrone.s.isHidden == false )
+ CloakedDroneWarpOut( cloakedDrone, spawnOrigin )
+ }
+ continue
+ }
+
+ goalNPC = FindBestCloakTarget( NPCs, cloakedDrone.GetOrigin(), cloakedDrone )
+ Assert( goalNPC )
+ }
+
+ CloakedDroneClaimSquad( cloakedDrone, expect string( goalNPC.kv.squadname ) )
+
+ waitthread CloakedDronePathFollowNPC( cloakedDrone, goalNPC )
+
+ CloakedDroneReleaseSquad( cloakedDrone )
+
+ previousNPC = goalNPC
+ goalNPC = null
+ targetLostTime = Time()
+
+ float distSqr = DistanceSqr( lastOrigin, cloakedDrone.GetOrigin() )
+ if ( distSqr < stuckDistSqr )
+ CloakedDroneWarpOut( cloakedDrone, spawnOrigin )
+
+ lastOrigin = cloakedDrone.GetOrigin()
+ }
+}
+
+void function CloakedDroneClaimSquad( entity cloakedDrone, string squadname )
+{
+ if ( GetNPCSquadSize( squadname ) )
+ file.cloakedDroneClaimedSquadList[ cloakedDrone ] <- squadname
+}
+
+void function CloakedDroneReleaseSquad( entity cloakedDrone )
+{
+ if ( cloakedDrone in file.cloakedDroneClaimedSquadList )
+ delete file.cloakedDroneClaimedSquadList[ cloakedDrone ]
+}
+
+bool function CloakedDroneIsSquadClaimed( string squadname )
+{
+ table<entity,string> cloneTable = clone file.cloakedDroneClaimedSquadList
+ foreach ( entity cloakedDrone, squad in cloneTable )
+ {
+ if ( !IsAlive( cloakedDrone ) )
+ delete file.cloakedDroneClaimedSquadList[ cloakedDrone ]
+ else if ( squad == squadname )
+ return true
+ }
+ return false
+}
+
+void function CloakedDronePathFollowNPC( entity cloakedDrone, entity goalNPC )
+{
+ cloakedDrone.EndSignal( "OnDestroy" )
+ cloakedDrone.EndSignal( "OnDeath" )
+ cloakedDrone.EndSignal( "DroneCrashing" )
+ goalNPC.EndSignal( "OnDeath" )
+ goalNPC.EndSignal( "OnDestroy" )
+
+ if ( !( "cloakedDrone" in goalNPC.s ) )
+ goalNPC.s.cloakedDrone <- null
+ goalNPC.s.cloakedDrone = cloakedDrone
+
+ OnThreadEnd(
+ function() : ( goalNPC )
+ {
+ if ( IsAlive( goalNPC ) )
+ goalNPC.s.cloakedDrone = null
+ }
+ )
+
+ int droneTeam = cloakedDrone.GetTeam()
+
+ //vector maxs = < 64, 64, 53.5 >//bigger than model to compensate for large effect
+ //vector mins = < -64, -64, -64 >
+
+ vector maxs = < 32, 32, 32 >//bigger than model to compensate for large effect
+ vector mins = < -32, -32, -32 >
+
+ int mask = cloakedDrone.GetPhysicsSolidMask()
+
+ float defaultHeight = 300
+ array<float> traceHeightsLow = [ -75.0, -150.0, -250.0 ]
+ array<float> traceHeightsHigh = [ 150.0, 300.0, 800.0, 1500.0 ]
+
+ float waitTime = 0.25
+
+ CloakDronePath path
+ path.goalValid = false
+ path.lastHeight = defaultHeight
+
+ //If drone is following titan wait for titan to leave bubble shield.
+ if ( goalNPC.IsTitan() )
+ WaitTillHotDropComplete( goalNPC )
+
+ while( goalNPC.GetTeam() == droneTeam )
+ {
+ if ( IsValid( GetRodeoPilot( goalNPC ) ) )
+ return
+
+ //If our target npc gets revealed by a sonar pulse, ditch that chump.
+ if ( StatusEffect_Get( goalNPC, eStatusEffect.sonar_detected ) )
+ return
+
+ float minDist = CLOAK_DRONE_REACHED_HARVESTER_DIST * CLOAK_DRONE_REACHED_HARVESTER_DIST
+ float distToGenerator = DistanceSqr( goalNPC.GetOrigin(), cloakedDrone.s.towerOrigin )
+ //if we've gotten our npc to the generator, go find someone farther out to escort.
+ if ( distToGenerator <= minDist )
+ return
+
+ //DebugDrawCircleOnEnt( goalNPC, 20, 255, 0, 0, 0.1 )
+
+ float startTime = Time()
+ path.goalValid = false
+
+ CloakedDroneFindPathDefault( path, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+
+ //find a new path if necessary
+ if ( !path.goalValid )
+ {
+ //lets check some heights and see if any are valid
+ CloakedDroneFindPathHorizontal( path, traceHeightsLow, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+
+ if ( !path.goalValid )
+ {
+ //OK so no way to directly go to those heights - lets see if we can move vertically down,
+ CloakedDroneFindPathVertical( path, traceHeightsLow, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+
+ if ( !path.goalValid )
+ {
+ //still no good...lets check up
+ CloakedDroneFindPathHorizontal( path, traceHeightsHigh, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+
+ if ( !path.goalValid )
+ {
+ //no direct shots up - lets try moving vertically up first
+ CloakedDroneFindPathVertical( path, traceHeightsHigh, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+ }
+ }
+ }
+ }
+
+ // if we can't find a valid path find a new goal
+ if ( !path.goalValid )
+ {
+ waitthread CloakedDroneWarpOut( cloakedDrone, GetCloakTargetOrigin( goalNPC ) + < 0, 0, defaultHeight > )
+ CloakedDroneWarpIn( cloakedDrone, GetCloakTargetOrigin( goalNPC ) + < 0, 0, defaultHeight > )
+ continue
+ }
+
+ if ( cloakedDrone.s.isHidden == true )
+ CloakedDroneWarpIn( cloakedDrone, cloakedDrone.GetOrigin() )
+
+ thread AssaultOrigin( cloakedDrone, path.goal )
+
+ float endTime = Time()
+ float elapsedTime = endTime - startTime
+ if ( elapsedTime < waitTime )
+ wait waitTime - elapsedTime
+ }
+}
+
+bool function CloakedDroneFindPathDefault( CloakDronePath path, float defaultHeight, vector mins, vector maxs, entity cloakedDrone, entity goalNPC, int mask )
+{
+ vector offset = < 0, 0, defaultHeight >
+ path.start = ( cloakedDrone.GetOrigin() ) + < 0, 0, 32 > //Offset so path start is just above drone instead at bottom of drone.
+ path.goal = GetCloakTargetOrigin( goalNPC ) + offset
+
+ //find out if we can get there using the default height
+ TraceResults result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ] , mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( path.start, path.goal, 50, 0, 0, true, 1.0 )
+ if ( result.fraction >= VALIDPATHFRAC )
+ {
+ path.lastHeight = defaultHeight
+ path.goalValid = true
+ }
+
+ return path.goalValid
+}
+
+bool function CloakedDroneFindPathHorizontal( CloakDronePath path, array<float> traceHeights, float defaultHeight, vector mins, vector maxs, entity cloakedDrone, entity goalNPC, int mask )
+{
+ wait 0.1
+
+ vector offset
+ float testHeight
+
+ //slight optimization... recheck if the last time was also not the default height
+ if ( path.lastHeight != defaultHeight )
+ {
+ offset = < 0, 0, defaultHeight + path.lastHeight >
+ path.start = ( cloakedDrone.GetOrigin() )
+ path.goal = GetCloakTargetOrigin( goalNPC ) + offset
+
+ TraceResults result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ], mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( path.start, path.goal, 0, 255, 0, true, 1.0 )
+ if ( result.fraction >= VALIDPATHFRAC )
+ {
+ path.goalValid = true
+ return path.goalValid
+ }
+ }
+
+ for ( int i = 0; i < traceHeights.len(); i++ )
+ {
+ testHeight = traceHeights[ i ]
+ if ( path.lastHeight == testHeight )
+ continue
+
+// wait 0.1
+
+ offset = < 0, 0, defaultHeight + testHeight >
+ path.start = ( cloakedDrone.GetOrigin() ) + ( testHeight > 0 ? < 0, 0, 0 > : < 0, 0, 32 > ) //Check from the top or bottom of the drone depending on if the drone is going up or down
+ path.goal = GetCloakTargetOrigin( goalNPC ) + offset
+
+ TraceResults result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ], mask, TRACE_COLLISION_GROUP_NONE )
+ if ( result.fraction < VALIDPATHFRAC )
+ {
+ //DebugDrawLine( path.start, path.goal, 200, 0, 0, true, 3.0 )
+ continue
+ }
+
+ //DebugDrawLine( path.start, path.goal, 0, 255, 0, true, 3.0 )
+
+ path.lastHeight = testHeight
+ path.goalValid = true
+ break
+ }
+
+ return path.goalValid
+}
+
+bool function CloakedDroneFindPathVertical( CloakDronePath path, array<float> traceHeights, float defaultHeight, vector mins, vector maxs, entity cloakedDrone, entity goalNPC, int mask )
+{
+ vector offset
+ vector origin
+ float testHeight
+
+ for ( int i = 0; i < traceHeights.len(); i++ )
+ {
+ wait 0.1
+
+ testHeight = traceHeights[ i ]
+ origin = cloakedDrone.GetOrigin()
+ offset = < 0, 0, defaultHeight + testHeight >
+ path.start = < origin.x, origin.y, defaultHeight + testHeight >
+ path.goal = GetCloakTargetOrigin( goalNPC ) + offset
+
+ TraceResults result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ], mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( path.start, path.goal, 50, 50, 100, true, 1.0 )
+ if ( result.fraction < VALIDPATHFRAC )
+ continue
+
+ //ok so it's valid - lets see if we can move to it from where we are
+// wait 0.1
+
+ path.goal = < path.start.x, path.start.y, path.start.z >
+ path.start = cloakedDrone.GetOrigin()
+
+ result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ], mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( path.start, path.goal, 255, 255, 0, true, 1.0 )
+ if ( result.fraction < VALIDPATHFRAC )
+ continue
+
+ path.lastHeight = testHeight
+ path.goalValid = true
+ break
+ }
+
+ return path.goalValid
+}
+
+void function CloakedDroneWarpOut( entity cloakedDrone, vector origin )
+{
+ if ( cloakedDrone.s.isHidden == false )
+ {
+ // only do this if we are not already hidden
+ FadeOutSoundOnEntity( cloakedDrone, CLOAKED_DRONE_LOOPING_SFX, 0.5 )
+ FadeOutSoundOnEntity( cloakedDrone, CLOAKED_DRONE_HOVER_LOOP_SFX, 0.5 )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_WARP_OUT_SFX )
+
+ cloakedDrone.s.fx.Fire( "StopPlayEndCap" )
+ cloakedDrone.SetTitle( "" )
+ cloakedDrone.s.isHidden = true
+ cloakedDrone.NotSolid()
+ cloakedDrone.Minimap_Hide( TEAM_IMC, null )
+ cloakedDrone.Minimap_Hide( TEAM_MILITIA, null )
+ cloakedDrone.SetNoTarget( true )
+ // let the beam fx end
+
+ if ( "smokeEffect" in cloakedDrone.s )
+ {
+ cloakedDrone.s.smokeEffect.Kill_Deprecated_UseDestroyInstead()
+ delete cloakedDrone.s.smokeEffect
+ }
+ UntrackAllToneMarks( cloakedDrone )
+
+ wait 0.3 // wait a bit before hidding the done so that the fx looks better
+ cloakedDrone.Hide()
+ }
+
+ wait 2.0
+
+ cloakedDrone.DisableBehavior( "Follow" )
+ thread AssaultOrigin( cloakedDrone, origin )
+ cloakedDrone.SetOrigin( origin )
+}
+
+void function CloakedDroneWarpIn( entity cloakedDrone, vector origin )
+{
+ cloakedDrone.DisableBehavior( "Follow" )
+ cloakedDrone.SetOrigin( origin )
+ PutEntityInSafeSpot( cloakedDrone, cloakedDrone, null, cloakedDrone.GetOrigin() + <0, 0, 32>, cloakedDrone.GetOrigin() )
+ thread AssaultOrigin( cloakedDrone, origin )
+
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_HOVER_LOOP_SFX )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_LOOPING_SFX )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_WARP_IN_SFX )
+
+ cloakedDrone.Show()
+ cloakedDrone.s.fx.Fire( "start" )
+ cloakedDrone.SetTitle( "#NPC_CLOAK_DRONE" )
+ cloakedDrone.s.isHidden = false
+ cloakedDrone.Solid()
+ cloakedDrone.Minimap_AlwaysShow( TEAM_IMC, null )
+ cloakedDrone.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ cloakedDrone.SetNoTarget( false )
+}
+
+
+entity function CreateDroneCloakBeam( entity cloakedDrone )
+{
+ entity fx = PlayLoopFXOnEntity( FX_DRONE_CLOAK_BEAM, cloakedDrone, "", null, < 90, 0, 0 > )//, visibilityFlagOverride = null, visibilityFlagEntOverride = null )
+ return fx
+}
+
+entity function FindBestCloakTarget( array<entity> npcArray, vector origin, entity drone )
+{
+ entity selectedNPC = null
+ float maxDist = 10000 * 10000
+ float minDist = 1300 * 1300
+ float highestScore = -1
+
+ foreach ( npc in npcArray )
+ {
+ float score = 0
+ float distToGenerator = DistanceSqr( npc.GetOrigin(), drone.s.towerOrigin )
+ if ( distToGenerator > minDist )
+ {
+ // only give dist bonus if we aren't to close to the generator.
+ local dist = DistanceSqr( npc.GetOrigin(), origin )
+ score = GraphCapped( dist, maxDist, minDist, 0, 1 )
+ }
+
+ if ( npc.IsTitan() )
+ {
+ score += 0.75
+ if ( IsArcTitan( npc ) )
+ score -= 0.1
+ if ( IsMortarTitan( npc ) )
+ score -= 0.2
+// if ( IsNukeTitan( npc ) )
+// score += 0.1
+ }
+ if ( score > highestScore )
+ {
+ highestScore = score
+ selectedNPC = npc
+ }
+ }
+
+ return selectedNPC
+}
+
+vector function GetCloakTargetOrigin( entity npc )
+{
+ // returns the center of squad if the npc is in one
+ // else returns a good spot to cloak a titan
+
+ vector origin
+
+ if ( GetNPCSquadSize( npc.kv.squadname ) == 0 )
+ {
+ origin = npc.GetOrigin() + npc.GetNPCVelocity()
+ }
+ else
+ origin = npc.GetSquadCentroid()
+
+ Assert( origin.x < ( 16384 * 100 ) );
+
+ // defensive hack
+ if ( origin.x > ( 16384 * 100 ) )
+ origin = npc.GetOrigin()
+
+ return origin
+}
+
+function IsSquadCenterClose( npc, dist = 256 )
+{
+ // return true if there is no squad
+ if ( GetNPCSquadSize( npc.kv.squadname ) == 0 )
+ return true
+
+ // return true if the squad isn't too spread out.
+ if ( DistanceSqr( npc.GetSquadCentroid(), npc.GetOrigin() ) <= ( dist * dist ) )
+ return true
+
+ return false
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut
new file mode 100644
index 00000000..c0d56de7
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut
@@ -0,0 +1,1388 @@
+untyped
+
+global function AiDrone_Init
+
+global function CreateDroneSquadString
+global function SetDroneSquadStringForOwner
+global function GetDroneSquadStringFromOwner
+global function DroneGruntThink
+global function RunDroneTypeThink
+global function DroneHasNoOwner
+global function CreateSingleDroneRope
+global function DroneDialogue
+global function IsDroneRebooting
+global function DroneOnLeeched
+global function SetRepairDroneTarget
+
+global const DRONE_SHIELD_COOLDOWN = 8
+global const DRONE_SHIELD_WALL_HEALTH = 200
+global const DRONE_SHIELD_WALL_RADIUS_TITAN = 200
+global const DRONE_SHIELD_WALL_RADIUS_HUMAN = 90
+global const DRONE_SHIELD_WALL_HEIGHT_TITAN = 450
+global const DRONE_SHIELD_WALL_HEIGHT_HUMAN = 190
+global const DRONE_SHIELD_WALL_FOV_TITAN = 115
+global const DRONE_SHIELD_WALL_FOV_HUMAN = 105
+global const DRONE_MINIMUM_DEPLOY_CLEARANCE_FROM_GROUND = 120
+global const MIN_DRONE_SHIELD_FROM_OWNER_DIST = 256 //if shield drone gets more than this distance away from host, will drop shield
+global const MIN_DRONE_SHIELD_FROM_OWNER_DIST_TITAN = 400 //if shield drone gets more than this distance away from host, will drop shield
+global const DRONE_LEASH_DISTANCE_SQR = 589824 // Further than this distance, drones will disengage from combat and go back to their owner.
+
+global const SOUND_DRONE_EXPLODE_DEFAULT = "Drone_DeathExplo"
+global const SOUND_DRONE_EXPLODE_CLOAK = "Drone_DeathExplo"
+
+global const FX_DRONE_SHIELD_WALL_TITAN = $"P_drone_shield_wall_XO"
+const FX_DRONE_SHIELD_WALL_HUMAN = $"P_drone_shield_wall"
+global const FX_DRONE_EXPLOSION = $"P_drone_exp_md"
+global const FX_DRONE_R_EXPLOSION = $"P_drone_exp_rocket"
+global const FX_DRONE_P_EXPLOSION = $"P_drone_exp_plasma"
+global const FX_DRONE_W_EXPLOSION = $"P_drone_exp_worker"
+global const FX_DRONE_SHIELD_ROPE_GLOW = $"acl_light_white"
+
+function AiDrone_Init()
+{
+ PrecacheParticleSystem( FX_DRONE_EXPLOSION )
+ PrecacheParticleSystem( FX_DRONE_R_EXPLOSION )
+ PrecacheParticleSystem( FX_DRONE_P_EXPLOSION )
+ PrecacheParticleSystem( FX_DRONE_W_EXPLOSION )
+ PrecacheParticleSystem( FX_DRONE_SHIELD_WALL_TITAN )
+ PrecacheParticleSystem( FX_DRONE_SHIELD_WALL_HUMAN )
+ PrecacheParticleSystem( FX_DRONE_SHIELD_ROPE_GLOW )
+
+ PrecacheModel( $"models/robots/drone_air_attack/drone_air_attack_rockets.mdl" )
+ PrecacheModel( $"models/robots/drone_air_attack/drone_air_attack_plasma.mdl" )
+
+ PrecacheMaterial( $"cable/cable_selfillum.vmt" )
+ PrecacheModel( $"cable/cable_selfillum.vmt" )
+ AddDeathCallback( "npc_drone", DroneDeath )
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Fallback behavior if we can't find a valid owner for an orphan Drone
+function DroneHasNoOwner( entity drone )
+{
+ switch ( GetDroneType( drone ) )
+ {
+ case "drone_type_shield":
+ //Transform into a Rocket drone and find some buddies
+ thread DroneTransformsToRocketClass( drone )
+ break
+
+ case "drone_type_engineer_combat":
+ case "drone_type_engineer_shield":
+ EngineerDroneHasNoOwner( drone )
+ break
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+void function DroneTransformsToRocketClass( entity drone )
+{
+ if ( !IsAlive( drone ) )
+ return
+
+ drone.EndSignal( "OnDeath" )
+ drone.EndSignal( "OnDestroy" )
+
+ wait 1.5
+
+ // dont do it if we're parented for some reason
+ if ( IsValid( drone.GetParent() ) )
+ return
+
+ DroneDialogue( drone, "transform_shield_to_assault" )
+ wait 3
+
+ // dont do it if we're parented for some reason
+ if ( IsValid( drone.GetParent() ) )
+ return
+
+ int team = drone.GetTeam()
+ int health = drone.GetHealth()
+ vector origin = drone.GetOrigin()
+ vector angles = drone.GetAngles()
+ angles.x = 0
+ angles.z = 0
+
+ entity newDrone = CreateRocketDrone( team, origin, angles )
+ DispatchSpawn( newDrone )
+ newDrone.SetHealth( health )
+
+ entity enemy = drone.GetEnemy()
+ if ( IsAlive( enemy ) )
+ newDrone.SetEnemyLKP( enemy, enemy.GetOrigin() )
+
+ drone.TransferChildrenTo( newDrone )
+
+ drone.Destroy()
+
+}
+
+function EngineerDroneHasNoOwner( drone )
+{
+ //TODO: Should probably protect nearest ally, and return to Engineer when he gets close.
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Change drone type on spawn or during gameplay (may transform from one to the other eventually)
+function RunDroneTypeThink( drone )
+{
+ expect entity( drone )
+ #if DEV
+ Assert( !( "RunDroneTypeThink" in drone.s ), "Already ran drone think!" )
+ drone.s.RunDroneTypeThink <- true
+ #endif
+ ////initialize it's type only after the anim is complete
+ //local delay = drone.GetSequenceDuration( spawnAnimDrone )
+ drone.EndSignal( "OnDeath" )
+
+ switch ( GetDroneType( drone ) )
+ {
+ case "drone_type_beam":
+ case "drone_type_rocket":
+ case "drone_type_plasma":
+ local owner = drone.GetFollowTarget()
+ if ( IsValid( owner ) )
+ owner.Signal( "OnEndFollow" )
+ DroneRocketThink( drone ) //may delay if it's waiting for a spawn anim to finish
+ break
+
+ case "drone_type_shield":
+ DroneShieldThink( drone ) //may delay if it's waiting for a spawn anim to finish
+ break
+
+ case "drone_type_engineer_combat":
+ EngineerCombatDroneThink( drone ) //may delay if it's waiting for a spawn anim to finish
+ break
+
+ case "drone_type_engineer_shield":
+ EngineerShieldDroneThink( drone ) //may delay if it's waiting for a spawn anim to finish
+ break
+
+ case "drone_type_repair":
+ RepairDroneThink( drone )
+ break
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function DroneRocketThink( entity drone )
+{
+ drone.EndSignal( "OnDeath" )
+
+ entity owner
+ entity currentTarget
+ local accuracyMultiplierBase = drone.kv.AccuracyMultiplier
+ local accuracyMultiplierAgainstDrones = 100
+
+ //--------------------------------------------
+ // transform if this used to be a shield drone
+ //--------------------------------------------
+ RemoveDroneRopes( drone )
+ drone.SetAttackMode( true )
+
+ while ( true )
+ {
+ wait 0.25
+
+ //----------------------------------
+ // Get owner and current enemy
+ //----------------------------------
+ currentTarget = drone.GetEnemy()
+ owner = drone.GetFollowTarget()
+
+ //----------------------------------
+ // Free roam if owner is dead or HasEnemy
+ //----------------------------------
+ if ( ( !IsAlive( owner ) ) || ( currentTarget != null ) )
+ {
+ drone.DisableBehavior( "Follow" )
+ }
+
+ //---------------------------------------------------------------------
+ // If owner is alive and no enemies in sight, go back and follow owner
+ //----------------------------------------------------------------------
+ if ( IsAlive( owner ) )
+ {
+ local distSqr = DistanceSqr( owner.GetOrigin(), drone.GetOrigin() )
+
+ if ( currentTarget == null || distSqr > DRONE_LEASH_DISTANCE_SQR )
+ {
+ drone.ClearEnemy()
+ drone.EnableBehavior( "Follow" )
+ }
+ }
+
+ //----------------------------------------------
+ // Jack up accuracy if targeting another drone
+ //----------------------------------------------
+ if ( ( currentTarget != null ) && ( IsAirDrone( currentTarget ) ) )
+ {
+ drone.kv.AccuracyMultiplier = accuracyMultiplierAgainstDrones
+ }
+ else
+ {
+ drone.kv.AccuracyMultiplier = accuracyMultiplierBase
+ }
+ }
+
+}
+
+function ShieldDroneShieldsUser( entity drone )
+{
+ for ( ;; )
+ {
+ var player = drone.WaitSignal( "OnPlayerUse" ).player
+
+ Assert( false, "REMOVED; see mp_pilot_ability_shield to ressurect" )
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function DroneShieldThink( drone )
+{
+ expect entity( drone )
+ if ( !IsValid( drone ) )
+ return
+ drone.EndSignal( "OnDestroy" )
+ drone.EndSignal( "OnDeath" )
+ //drone.EndSignal( "OnNewOwner" )
+
+ entity owner
+ local newOwner
+ string ownerSquadName = ""
+ local distSq
+ local distSqHuman = MIN_DRONE_SHIELD_FROM_OWNER_DIST * MIN_DRONE_SHIELD_FROM_OWNER_DIST
+ local distSqTitan = MIN_DRONE_SHIELD_FROM_OWNER_DIST_TITAN * MIN_DRONE_SHIELD_FROM_OWNER_DIST_TITAN
+ bool titanStateCurrent = false
+ bool titanStatePrevious = false
+ bool titanStateChanged = false
+ local e = {}
+ e.droneShieldTable <- null
+
+ drone.SetUsePrompts( "#SHIELD_DRONE_HOLD_USE", "#SHIELD_DRONE_PRESS_USE" )
+
+ //------------------------------------------
+ // Cleanup shield if Drone dies
+ //------------------------------------------
+ OnThreadEnd(
+ function() : ( e, drone )
+ {
+ DroneShieldDestroy( e.droneShieldTable )
+ if ( IsAlive( drone ) )
+ thread ShieldDroneLandsAfterLeaderDeath( drone )
+ }
+ )
+
+ thread ShieldDroneShieldsUser( drone )
+
+ //------------------------------------------
+ // Drone tentacles/ropes
+ //------------------------------------------
+ local droneRopeTable = CreateDroneRopes( drone )
+ if ( !( "droneRopeTable" in drone.s ) )
+ drone.s.droneRopeTable <- null
+ drone.s.droneRopeTable = droneRopeTable
+
+ //------------------------------------------
+ // Drone shield think loop
+ //------------------------------------------
+
+ while ( true )
+ {
+ wait 0.25
+
+ if ( GetDroneType( drone ) != "drone_type_shield" )
+ {
+ DroneShieldDestroy( e.droneShieldTable )
+ break
+ }
+
+ //------------------------------------------
+ // If rebooting from EMP blast, get rid of shield
+ //------------------------------------------
+ if ( IsDroneRebooting( drone ) )
+ {
+ DroneShieldDestroy( e.droneShieldTable )
+ continue
+ }
+ //------------------------------------------
+ // If owner dead, kill shield until new owner found
+ //------------------------------------------
+ owner = drone.GetFollowTarget()
+ if ( !IsAlive( owner ) )
+ {
+ DroneShieldDestroy( e.droneShieldTable )
+ break
+ }
+
+ //------------------------------------------
+ // Still no valid owner? End this thread
+ //------------------------------------------
+ if ( !IsValid( owner ) )
+ break
+
+ //ownerSquadName = owner.Get( "squadname" )
+
+ //------------------------------------------
+ // Owner is valid. Is it differnt owner?
+ //------------------------------------------
+ if ( newOwner != owner )
+ {
+ //Kill current shield since it will get redeployed on new owner
+ DroneShieldDestroy( e.droneShieldTable )
+ }
+
+ //------------------------------------------
+ // Owner is valid. Has owner changed Titan state?
+ //------------------------------------------
+ newOwner = owner
+ titanStatePrevious = titanStateCurrent //previous state is whatever current was set to last loop around
+
+ if ( owner.IsTitan() )
+ {
+ distSq = distSqTitan //adjust min dist for shield based on titan state
+ titanStateCurrent = true //toggle so we can see if owner just changed state
+ }
+ else
+ {
+ distSq = distSqHuman
+ titanStateCurrent = false
+ }
+
+ if ( titanStateCurrent != titanStatePrevious )
+ titanStateChanged = true
+ else
+ titanStateChanged = false
+
+ //--------------------------------------------------------------------------------------
+ // We have a valid owner and a valid shield, continue unless we have changed Titan state
+ //--------------------------------------------------------------------------------------
+ if ( ( DroneShieldExists( e.droneShieldTable ) ) && ( !titanStateChanged ) )
+ continue
+
+ //------------------------------------------
+ // Too far away from owner, destoy shield
+ //------------------------------------------
+ if ( DistanceSqr( drone.GetOrigin(), owner.GetOrigin() ) > distSq )
+ {
+ //printl( "Drone is too far away from host to create a shield")
+ DroneShieldDestroy( e.droneShieldTable )
+ continue
+ }
+
+ //------------------------------------------
+ // Owner embarked/disembarked in a Titan, destroy shield
+ //------------------------------------------
+ if ( titanStateChanged )
+ {
+ //printl( "Drone host embarked/disembarked a Titan, destroying shield")
+ DroneShieldDestroy( e.droneShieldTable )
+ continue
+ }
+ //----------------------------------------------------------
+ // Valid owner, valid dist, etc...make a shield for the current owner
+ //-----------------------------------------------------------
+ e.droneShieldTable = MakeDroneShield( drone, owner )
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function EngineerCombatDroneThink( entity drone )
+{
+ if ( !IsValid( drone ) )
+ return
+
+ drone.EndSignal( "OnDeath" )
+
+ entity owner
+ local currentTarget
+ local accuracyMultiplierPlayers = 50
+ local accuracyMultiplierAgainstNPC = 90
+
+ //--------------------------------------------
+ // transform if this used to be a shield drone
+ //--------------------------------------------
+ RemoveDroneRopes( drone )
+ drone.SetAttackMode( true )
+
+ while ( true )
+ {
+ wait 0.25
+
+ //----------------------------------
+ // Get owner and current enemy
+ //----------------------------------
+ currentTarget = drone.GetEnemy()
+ owner = drone.GetFollowTarget()
+
+ //----------------------------------
+ // Free roam if owner is dead or HasEnemy
+ //----------------------------------
+ if ( ( !IsAlive( owner ) ) || ( currentTarget != null ) )
+ {
+ drone.DisableBehavior( "Follow" )
+ }
+
+ //---------------------------------------------------------------------
+ // If owner is alive and no enemies in sight, go back and follow owner
+ //----------------------------------------------------------------------
+ if ( IsAlive( owner ) )
+ {
+ float distSqr = DistanceSqr( owner.GetOrigin(), drone.GetOrigin() )
+
+ if ( currentTarget == null || distSqr > DRONE_LEASH_DISTANCE_SQR )
+ {
+ drone.ClearEnemy()
+ drone.EnableBehavior( "Follow" )
+ }
+ }
+
+ //----------------------------------------------
+ // Jack up accuracy if targeting another drone
+ //----------------------------------------------
+ if ( ( currentTarget != null ) && ( currentTarget.IsNPC() ) )
+ {
+ drone.kv.AccuracyMultiplier = accuracyMultiplierAgainstNPC
+ }
+ else
+ {
+ drone.kv.AccuracyMultiplier = accuracyMultiplierPlayers
+ }
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function EngineerShieldDroneThink( drone )
+{
+ if ( !IsValid( drone ) )
+ return
+ drone.EndSignal( "OnDestroy" )
+ drone.EndSignal( "OnDeath" )
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function IsDroneRebooting( drone )
+{
+ if ( !( "rebooting" in drone.s ) )
+ return false
+
+ return drone.s.rebooting
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// HACK: may just use generic function "CreateShield()" from particle_wall.nut, but just in prototype mode now
+function MakeDroneShield( drone, owner )
+{
+ expect entity( owner )
+
+ if ( !( "shieldTable" in drone.s ) )
+ drone.s.shieldTable <- null
+ else
+ DroneShieldDestroy( drone.s.shieldTable )
+
+ //------------------------------
+ // Shield vars
+ //------------------------------
+ vector origin = owner.GetOrigin()
+ vector angles = owner.GetAngles() + Vector( 0, 0, 180 )
+ local attachmentTag
+ local DroneShieldTable = {}
+ DroneShieldTable.vortexSphere <- null
+ DroneShieldTable.shieldWallFX = null
+ DroneShieldTable.shieldRopes <- null
+
+ asset shieldFx
+ float wallFOV
+ float shieldWallRadius
+ float shieldWallHeight
+ if ( owner.IsTitan() )
+ {
+ shieldWallRadius = DRONE_SHIELD_WALL_RADIUS_TITAN
+ shieldFx = FX_DRONE_SHIELD_WALL_TITAN
+ wallFOV = DRONE_SHIELD_WALL_FOV_TITAN
+ shieldWallHeight = DRONE_SHIELD_WALL_HEIGHT_TITAN
+ }
+ else
+ {
+ shieldWallRadius = DRONE_SHIELD_WALL_RADIUS_HUMAN
+ shieldFx = FX_DRONE_SHIELD_WALL_HUMAN
+ wallFOV = DRONE_SHIELD_WALL_FOV_HUMAN
+ shieldWallHeight = DRONE_SHIELD_WALL_HEIGHT_HUMAN
+ }
+
+ local Spawn
+ //------------------------------
+ // Vortex to block the actual bullets
+ //------------------------------
+ entity vortexSphere = CreateEntity( "vortex_sphere" )
+
+ vortexSphere.kv.spawnflags = SF_ABSORB_BULLETS | SF_BLOCK_OWNER_WEAPON | SF_BLOCK_NPC_WEAPON_LOF | SF_ABSORB_CYLINDER
+ vortexSphere.kv.enabled = 0
+ vortexSphere.kv.radius = shieldWallRadius
+ vortexSphere.kv.height = shieldWallHeight
+ vortexSphere.kv.bullet_fov = wallFOV
+ vortexSphere.kv.physics_pull_strength = 25
+ vortexSphere.kv.physics_side_dampening = 6
+ vortexSphere.kv.physics_fov = 360
+ vortexSphere.kv.physics_max_mass = 2
+ vortexSphere.kv.physics_max_size = 6
+
+ vortexSphere.SetAngles( angles ) // viewvec?
+ vortexSphere.SetOrigin( origin + Vector( 0, 0, shieldWallRadius - 64 ) )
+ vortexSphere.SetMaxHealth( DRONE_SHIELD_WALL_HEALTH )
+ vortexSphere.SetHealth( DRONE_SHIELD_WALL_HEALTH )
+
+ if ( IsSingleplayer() )
+ {
+ thread PROTO_VortexSlowsPlayers( vortexSphere, owner )
+ }
+
+ DispatchSpawn( vortexSphere )
+
+ vortexSphere.Fire( "Enable" )
+
+ vortexSphere.SetInvulnerable() // make particle wall invulnerable to weapon damage. It will still drain over time
+
+ // Shield wall fx control point
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) )
+ DispatchSpawn( cpoint )
+
+ //------------------------------------------
+ // Shield wall fx for visuals/health drain
+ //------------------------------------------
+
+ entity shieldWallFX = PlayFXWithControlPoint( shieldFx, origin + Vector( 0, 0, -64 ), cpoint, -1, null, angles )
+ vortexSphere.e.shieldWallFX = shieldWallFX
+ SetVortexSphereShieldWallCPoint( vortexSphere, cpoint )
+
+ entity mover = CreateScriptMover()
+ mover.SetOrigin( owner.GetOrigin() )
+ mover.SetAngles( owner.GetAngles() )
+
+ //-----------------------
+ // Attach shield to owner
+ //------------------------
+ vortexSphere.SetParent( mover )
+ shieldWallFX.SetParent( mover )
+
+ thread ShieldMoverFollowsOwner( owner, mover, vortexSphere, shieldWallFX )
+
+ //-----------------------
+ // Rope attach to shield
+ //------------------------
+ local ropeAttachOrigin1 = PositionOffsetFromEnt( owner, shieldWallRadius -16, wallFOV -16, 128 )
+ local ropeAttachOrigin2 = PositionOffsetFromEnt( owner, shieldWallRadius -16, ( ( wallFOV - 16) * -1 ), 128 )
+ if ( owner.IsTitan() )
+ {
+ ropeAttachOrigin1 = PositionOffsetFromEnt( owner, shieldWallRadius - 78, wallFOV + 22, 256 )
+ ropeAttachOrigin2 = PositionOffsetFromEnt( owner, shieldWallRadius - 78, -( wallFOV + 22), 256 )
+ }
+
+ local shieldRopes = []
+ local shieldRope1 = CreateSingleDroneRope( drone, "ROPE_0", false )
+ local shieldRope2 = CreateSingleDroneRope( drone, "ROPE_0", false )
+ shieldRopes.append( shieldRope1 )
+ shieldRopes.append( shieldRope2 )
+ entity ropeEnt1 = CreateEntity( "info_target" )
+ entity ropeEnt2 = CreateEntity( "info_target" )
+ ropeEnt1.SetOrigin( ropeAttachOrigin1 )
+ ropeEnt2.SetOrigin( ropeAttachOrigin2 )
+ ropeEnt1.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ ropeEnt2.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ DispatchSpawn( ropeEnt1 )
+ DispatchSpawn( ropeEnt2 )
+
+ ropeEnt1.SetParent( vortexSphere )
+ ropeEnt2.SetParent( vortexSphere )
+ shieldRope1.s.ropeEnd.SetOrigin( ropeEnt1.GetOrigin() )
+ shieldRope2.s.ropeEnd.SetOrigin( ropeEnt2.GetOrigin() )
+ shieldRope1.s.ropeEnd.SetParent( ropeEnt1 )
+ shieldRope2.s.ropeEnd.SetParent( ropeEnt2 )
+
+ PlayFXOnEntity( FX_DRONE_SHIELD_ROPE_GLOW, ropeEnt1 )
+ PlayFXOnEntity( FX_DRONE_SHIELD_ROPE_GLOW, ropeEnt2 )
+
+ //-----------------------
+ // DroneShieldTable
+ //------------------------
+ DroneShieldTable.vortexSphere = vortexSphere
+ DroneShieldTable.shieldWallFX = shieldWallFX
+ DroneShieldTable.shieldRopes = shieldRopes
+
+ //-----------------------
+ // Health and cleanup
+ //------------------------
+ drone.s.shieldTable = DroneShieldTable
+ UpdateShieldWallColorForFrac( shieldWallFX, 1.0 )
+
+ return DroneShieldTable
+}
+
+void function ShieldMoverFollowsOwner( entity owner, entity mover, entity vortexSphere, entity shieldWallFX )
+{
+ vortexSphere.EndSignal( "OnDestroy" )
+ shieldWallFX.EndSignal( "OnDestroy" )
+ owner.EndSignal( "OnDeath" )
+ mover.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( mover )
+ {
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ }
+ )
+
+ for ( ;; )
+ {
+ UpdateMoverPosition( mover, owner )
+ }
+}
+
+void function UpdateMoverPosition( entity mover, entity owner )
+{
+ vector origin = owner.GetOrigin()
+ mover.NonPhysicsMoveTo( origin, 0.1, 0.0, 0.0 )
+ mover.NonPhysicsRotateTo( owner.GetAngles(), 0.75, 0.0, 0.0 )
+ WaitFrame()
+}
+
+void function PROTO_VortexSlowsPlayers( entity vortexSphere, entity owner )
+{
+ vortexSphere.EndSignal( "OnDestroy" )
+ owner.EndSignal( "OnDeath" )
+
+ float radius = float(vortexSphere.kv.radius )
+ float height = float(vortexSphere.kv.height )
+ float bullet_fov = float( vortexSphere.kv.bullet_fov )
+ float dot = cos( bullet_fov * 0.5 )
+
+ for ( ;; )
+ {
+ vector origin = vortexSphere.GetOrigin()
+ vector angles = vortexSphere.GetAngles()
+ vector forward = AnglesToForward( angles )
+ int team = owner.GetTeam()
+
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( player.GetTeam() == team )
+ continue
+ VortexStunCheck( player, origin, height, radius, bullet_fov, dot, forward )
+ }
+ WaitFrame()
+ }
+}
+
+void function VortexStunCheck( entity player, vector origin, float height, float radius, float bullet_fov, float dot, vector forward )
+{
+ if ( Time() - player.p.lastDroneShieldStunPushTime < 1.75 )
+ return
+
+ vector playerOrg = player.GetOrigin()
+ float dist2d = Distance2D( playerOrg, origin )
+
+ if ( dist2d > radius + 5 )
+ return
+ if ( dist2d < radius - 15 )
+ return
+
+ float heightOffset = fabs( playerOrg.z - origin.z )
+
+ if ( heightOffset < 0 || heightOffset > height )
+ return
+
+ vector dif = Normalize( playerOrg - origin )
+
+ if ( DotProduct2D( dif, forward ) < dot )
+ return
+
+ const float VORTEX_STUN_DURATION = 1.0
+ GiveEMPStunStatusEffects( player, VORTEX_STUN_DURATION + 0.5 )
+ float strength = 0.4
+ StatusEffect_AddTimed( player, eStatusEffect.emp, strength, VORTEX_STUN_DURATION, 0.5 )
+ thread TempLossOfAirControl( player, VORTEX_STUN_DURATION )
+ vector velocity = forward * 300
+ velocity.z = 400
+ player.p.lastDroneShieldStunPushTime = Time()
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "explo_proximityemp_impact_3p" )
+ player.SetVelocity( velocity )
+}
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function CreateDroneRopes( drone )
+{
+ local droneRopeTable = {}
+ droneRopeTable.rope01 <- CreateSingleDroneRope( drone, "ROPE_0" )
+ droneRopeTable.rope02 <- CreateSingleDroneRope( drone, "ROPE_0" )
+ droneRopeTable.rope03 <- CreateSingleDroneRope( drone, "ROPE_1" )
+ droneRopeTable.rope04 <- CreateSingleDroneRope( drone, "ROPE_2" )
+ droneRopeTable.rope05 <- CreateSingleDroneRope( drone, "ROPE_3" )
+ droneRopeTable.rope06 <- CreateSingleDroneRope( drone, "ROPE_4" )
+
+ return droneRopeTable
+}
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function RemoveDroneRopes( entity drone )
+{
+ if ( !( "droneRopeTable" in drone.s ) )
+ return
+
+ local droneRopeTable = drone.s.droneRopeTable
+ if ( IsValid( droneRopeTable.rope01.s.ropeEnd ) )
+ droneRopeTable.rope01.s.ropeEnd.Destroy()
+ if ( IsValid( droneRopeTable.rope02.s.ropeEnd ) )
+ droneRopeTable.rope02.s.ropeEnd.Destroy()
+ if ( IsValid( droneRopeTable.rope03.s.ropeEnd ) )
+ droneRopeTable.rope03.s.ropeEnd.Destroy()
+ if ( IsValid( droneRopeTable.rope04.s.ropeEnd ) )
+ droneRopeTable.rope04.s.ropeEnd.Destroy()
+ if ( IsValid( droneRopeTable.rope05.s.ropeEnd ) )
+ droneRopeTable.rope05.s.ropeEnd.Destroy()
+ if ( IsValid( droneRopeTable.rope06.s.ropeEnd ) )
+ droneRopeTable.rope06.s.ropeEnd.Destroy()
+ if ( IsValid( droneRopeTable.rope01 ) )
+ droneRopeTable.rope01.Destroy()
+ if ( IsValid( droneRopeTable.rope02 ) )
+ droneRopeTable.rope02.Destroy()
+ if ( IsValid( droneRopeTable.rope03 ) )
+ droneRopeTable.rope03.Destroy()
+ if ( IsValid( droneRopeTable.rope04 ) )
+ droneRopeTable.rope04.Destroy()
+ if ( IsValid( droneRopeTable.rope05 ) )
+ droneRopeTable.rope05.Destroy()
+ if ( IsValid( droneRopeTable.rope06 ) )
+ droneRopeTable.rope06.Destroy()
+
+}
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function CreateSingleDroneRope( drone, attachTag, dangling = true )
+{
+ local subdivisions = 15 // 25
+ local slack = 200 // 25
+ string startpointName = UniqueString( "rope_startpoint" )
+ string endpointName = UniqueString( "rope_endpoint" )
+
+ local attach_id = drone.LookupAttachment( attachTag )
+ Assert( attach_id > 0, "Invalid attachment: " + attachTag )
+ local attachPos = drone.GetAttachmentOrigin( attach_id )
+
+ entity rope_start = CreateEntity( "move_rope" )
+ SetTargetName( rope_start, startpointName )
+ rope_start.kv.NextKey = endpointName
+ rope_start.kv.MoveSpeed = 32
+ rope_start.kv.Slack = slack
+ rope_start.kv.Subdiv = subdivisions
+ rope_start.kv.Width = "1"
+ rope_start.kv.TextureScale = "1"
+ rope_start.kv.RopeMaterial = "cable/cable_selfillum.vmt"
+ rope_start.kv.PositionInterpolator = 2
+ rope_start.kv.dangling = dangling
+ rope_start.SetOrigin( attachPos )
+ rope_start.SetParent( drone, attachTag )
+
+ entity rope_end = CreateEntity( "keyframe_rope" )
+ SetTargetName( rope_end, endpointName )
+ rope_end.kv.MoveSpeed = 32
+ rope_end.kv.Slack = slack
+ rope_end.kv.Subdiv = subdivisions
+ rope_end.kv.Width = "1"
+ rope_end.kv.TextureScale = "1"
+ rope_end.kv.RopeMaterial = "cable/cable_selfillum.vmt"
+ rope_end.SetOrigin( attachPos )
+
+ DispatchSpawn( rope_start )
+ DispatchSpawn( rope_end )
+
+ rope_start.s.ropeEnd <- rope_end
+
+ return rope_start
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function DroneShieldDestroy( DroneShieldTable )
+{
+ if ( !IsValid( DroneShieldTable ) )
+ return
+
+ local vortexSphere = DroneShieldTable.vortexSphere
+ local shieldWallFX = DroneShieldTable.shieldWallFX
+ local ropes = DroneShieldTable.shieldRopes
+
+ StopShieldWallFX( expect entity( vortexSphere ) )
+ if ( IsValid( vortexSphere ) )
+ vortexSphere.Destroy()
+
+ if ( !IsValid( ropes ) )
+ return
+
+ foreach ( rope in ropes )
+ {
+ if ( IsValid( rope.s.ropeEnd ) )
+ rope.s.ropeEnd.Destroy()
+ if ( IsValid( rope ) )
+ rope.Destroy()
+ }
+
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function DroneShieldExists( DroneShieldTable )
+{
+ if ( !IsValid( DroneShieldTable) )
+ return false
+
+ Assert( "vortexSphere" in DroneShieldTable, "DroneShieldTable doesn't contain any valid entries for vortexSphere." )
+ Assert( "shieldWallFX" in DroneShieldTable, "DroneShieldTable doesn't contain any valid entries for shieldWallFX." )
+
+ if ( ( IsValid( DroneShieldTable.vortexSphere ) ) && ( IsValid( DroneShieldTable.shieldWallFX ) ) )
+ return true
+
+ return false
+}
+
+void function DroneThrow( entity npc, entity drone, string spawnAnimDrone )
+{
+ drone.EndSignal( "OnDeath" )
+
+ drone.EnableNPCFlag( NPC_DISABLE_SENSING )
+
+// EmitSoundOnEntity( drone, "Drone_Power_On" )
+
+ #if GRUNTCHATTER_ENABLED
+ if ( NPC_GruntChatterSPEnabled( npc ) )
+ GruntChatter_TryFriendlyEquipmentDeployed( npc, "npc_drone" )
+ #endif
+
+ vector origin = npc.GetOrigin()
+ vector angles = npc.GetAngles()
+
+ //animate the drone properly from the npc's hand
+ PlayAnimTeleport( drone, spawnAnimDrone, origin, angles )
+
+ if ( IsAlive( npc ) )
+ {
+ entity enemy = npc.GetEnemy()
+ if ( IsAlive( enemy ) )
+ drone.SetEnemyLKP( enemy, npc.GetEnemyLKP() )
+ }
+
+ drone.DisableNPCFlag( NPC_DISABLE_SENSING )
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#if !SP
+void function DroneCleanupOnOwnerDeath_Thread( entity owner, entity drone )
+{
+ drone.EndSignal( "OnDestroy" )
+ drone.EndSignal( "OnDeath" )
+
+ for ( ; ; )
+ {
+ if ( !IsAlive( owner ) )
+ break
+
+ WaitFrame()
+ }
+
+ wait RandomFloatRange( 2.0, 10.0 )
+ drone.Die()
+}
+#endif // #if !SP
+
+entity function SpawnDroneFromNPC( entity npc, string aiSettings )
+{
+ //he's busy right now
+ if ( !IsAlive( npc ) || !npc.IsInterruptable() )
+ return null
+
+ vector origin = npc.GetOrigin()
+ vector angles = npc.GetAngles()
+ int team = npc.GetTeam()
+ entity owner = npc
+ vector deployOrigin = PositionOffsetFromEnt( npc, 64, 0, 0 )
+ float verticalClearance = GetVerticalClearance( deployOrigin )
+ string spawnAnimDrone
+ string spawnAnimSoldier
+
+ //-------------------------------------------------------------------
+ // Make sure enough clearance to spawn drone, and get correct anim
+ //-------------------------------------------------------------------
+ if ( verticalClearance >= 256 )
+ {
+ spawnAnimDrone = "dr_activate_drone_spin"
+ spawnAnimSoldier = "pt_activate_drone_spin"
+ }
+ else if ( ( verticalClearance < 256 ) && ( verticalClearance > DRONE_MINIMUM_DEPLOY_CLEARANCE_FROM_GROUND ) )
+ {
+ spawnAnimDrone = "dr_activate_drone_indoor"
+ spawnAnimSoldier = "pt_activate_drone_indoor"
+ }
+ else
+ {
+ printt( "NPC at ", npc.GetOrigin(), " couldn't spawn drone because there is less than ", DRONE_MINIMUM_DEPLOY_CLEARANCE_FROM_GROUND, " units of clearance from his origin." )
+ return null
+ }
+
+ //------------------------------------------
+ // NPC throws drone into air
+ //------------------------------------------
+ entity drone = CreateNPC( "npc_drone", team, origin, angles )
+ SetSpawnOption_AISettings( drone, aiSettings )
+ DispatchSpawn( drone )
+
+ if ( !IsAlive( drone ) )
+ return null
+
+ drone.NotSolid()
+ thread PlayAnim( npc, spawnAnimSoldier, origin, angles )
+ thread DroneSolidDelayed( drone )
+ thread DroneThrow( npc, drone, spawnAnimDrone )
+
+#if !SP
+ thread DroneCleanupOnOwnerDeath_Thread( npc, drone )
+#endif // #if !SP
+
+ npc.EnableNPCFlag( NPC_PAIN_IN_SCRIPTED_ANIM )
+
+ return drone
+}
+
+void function DroneSolidDelayed( entity drone )
+{
+ drone.EndSignal( "OnDestroy" )
+ wait 3.0 // wait for custom scale to finish in the animation
+ drone.Solid()
+}
+
+void function ShieldDroneLandsAfterLeaderDeath( entity drone )
+{
+ Assert( IsNewThread(), "Must be threaded off" )
+ drone.EndSignal( "OnDeath" )
+
+ drone.DisableBehavior( "Follow" )
+ //SetTeam( drone, TEAM_UNASSIGNED )
+ vector start = drone.GetOrigin()
+ vector end = start + Vector(0,0,-5000)
+ vector mins = drone.GetBoundingMins()
+ vector maxs = drone.GetBoundingMaxs()
+
+ TraceResults traceResult = TraceHull( start, end, mins, maxs, null, TRACE_MASK_NPCWORLDSTATIC, TRACE_COLLISION_GROUP_NONE )
+ if ( traceResult.fraction >= 1.0 )
+ {
+ // cant touch ground
+ drone.Die()
+ return
+ }
+
+ RemoveDroneRopes( drone )
+
+ //drone.SetUsable()
+ drone.AssaultPoint( traceResult.endPos )
+ //drone.SetInvulnerable()
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function CreateDroneSquadString( owner )
+{
+ Assert( IsValid( owner ), "Trying to MakeDroneSquad name for an invalid entity." )
+
+ local squadName
+
+ if ( owner.IsPlayer() )
+ squadName = "player" + owner.entindex() + "droneSquad"
+ else if ( owner.IsNPC() )
+ squadName = "npc" + owner.entindex() + "droneSquad"
+ else
+ Assert( 0, "Trying to CreateDroneSquadString for a non-NPC non-player entity at " + owner.GetOrigin() )
+
+ return squadName
+}
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function SetDroneSquadStringForOwner( owner, squadName )
+{
+ Assert( IsValid( owner ), "Trying to SetDroneSquadStringForOwner name on an invalid entity." )
+
+ if ( !( "squadNameDrones" in owner.s ) )
+ owner.s.squadNameDrones <- null
+
+ owner.s.squadNameDrones = squadName
+}
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function GetDroneSquadStringFromOwner( owner )
+{
+ Assert( IsValid( owner ), "Trying to GetDroneSquadStringFromOwner name on an invalid entity." )
+ if ( !( "squadNameDrones" in owner.s ) )
+ return null
+ else
+ return owner.s.squadNameDrones
+}
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// DroneGrunt deploys drone after cooldown when drone is destroyed
+function DroneGruntThink( entity npc, string aiSettings )
+{
+ if ( !IsValid( npc ) )
+ return
+
+ npc.EndSignal( "OnDestroy" )
+ npc.EndSignal( "OnDeath" )
+
+ entity drone
+ float spawnCooldown
+ entity closestEnemy
+ npc.EnableNPCFlag( NPC_USE_SHOOTING_COVER | NPC_CROUCH_COMBAT )
+
+ while ( true )
+ {
+ //if ( npc.GetNPCState() == "idle" )
+ //{
+ // npc.WaitSignal( "OnStateChange" )
+ // continue
+ //}
+
+ wait ( RandomFloatRange( 0, 1.0 ) )
+
+ //dont do stuff when animating on a parent
+ if ( npc.GetParent() )
+ continue
+
+ // Don't deploy if would hit ceiling, droppod, etc
+ if ( !DroneHasEnoughRoomToDeployFromNPC( npc ) )
+ continue
+
+ entity enemy = npc.GetEnemy()
+ if ( !IsAlive( enemy ) )
+ continue
+
+ //vector pos = npc.LastKnownPosition( enemy )
+ //if ( !WithinEngagementRange( npc, pos ) )
+ // continue
+
+ drone = SpawnDroneFromNPC( npc, aiSettings )
+ if ( drone == null )
+ continue
+
+ waitthread DroneWaitTillDeadOrHacked( drone )
+
+ wait 15
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function DroneHasEnoughRoomToDeployFromNPC( npc )
+{
+ expect entity( npc )
+
+ if ( !IsValid( npc ) )
+ return false
+ //-----------------------------------------------
+ // Grunt throws drone a bit in front of him
+ //-----------------------------------------------
+ vector deployOrigin = PositionOffsetFromEnt( npc, 64, 0, 0 )
+
+ if ( GetVerticalClearance( deployOrigin ) < DRONE_MINIMUM_DEPLOY_CLEARANCE_FROM_GROUND )
+ return false
+ else
+ return true
+
+}
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function DroneWaitTillDeadOrHacked( drone )
+{
+ drone.EndSignal( "OnDestroy" )
+ drone.EndSignal( "OnDeath" )
+ drone.EndSignal( "OnNewOwner" )
+
+ WaitForever()
+}
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+void function DroneDeath( entity drone, var damageInfo )
+{
+ local deathFX
+
+ switch ( GetDroneType( drone ) )
+ {
+ case "drone_type_rocket":
+ deathFX = FX_DRONE_R_EXPLOSION
+ break
+ case "drone_type_plasma":
+ deathFX = FX_DRONE_P_EXPLOSION
+ break
+ case "drone_type_marvin":
+ deathFX = FX_DRONE_W_EXPLOSION
+ break
+ case "drone_type_shield":
+ case "drone_type_engineer_shield":
+ case "drone_type_engineer_combat":
+ default:
+ deathFX = FX_DRONE_EXPLOSION
+ break
+ }
+
+ // Explosion effect
+ entity explosion = CreateEntity( "info_particle_system" )
+ explosion.SetOrigin( drone.GetWorldSpaceCenter() )
+ explosion.SetAngles( drone.GetAngles() )
+ explosion.SetValueForEffectNameKey( deathFX )
+ explosion.kv.start_active = 1
+ DispatchSpawn( explosion )
+
+ local deathSound
+
+ // this sound get should be moved to ai settings file
+ switch ( GetDroneType( drone ) )
+ {
+ case "drone_type_rocket":
+ case "drone_type_plasma":
+ case "drone_type_marvin":
+ case "drone_type_shield":
+ case "drone_type_engineer_shield":
+ case "drone_type_engineer_combat":
+ deathSound = SOUND_DRONE_EXPLODE_DEFAULT
+ break
+ default:
+ deathSound = SOUND_DRONE_EXPLODE_DEFAULT
+ break
+ }
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, drone.GetOrigin(), deathSound )
+ explosion.Kill_Deprecated_UseDestroyInstead( 3 )
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//
+function DroneDialogue( drone, event, player = null )
+{
+ expect entity( drone )
+ expect entity( player )
+
+ if ( !IsAlive( drone ) )
+ return
+
+ if ( player != null )
+ {
+ if ( !IsAlive( player ) )
+ return
+ }
+
+ local alias
+ bool playToPlayerOnly = true
+
+ switch ( event )
+ {
+ case "smoke_deploy":
+ //Foreign entity attached, deploying countermeasures.
+ alias = "diag_gs_drone_detectEnemyRodeo"
+ break
+ case "hack_success":
+ //New host accepted.
+ alias = "diag_gs_drone_hostAcceptNew"
+
+ //Foreign host accepted.
+ if ( CoinFlip() )
+ alias = "diag_gs_drone_hostAcceptForeign"
+ break
+ case "transform_shield_to_assault":
+ //Drone host eliminated, engaging assault mode
+ alias = "diag_gs_drone_elimHost"
+ playToPlayerOnly = false
+ break
+ default:
+ Assert( 0, "Invalid DroneDialogue event: " + event )
+ }
+
+ if ( playToPlayerOnly )
+ EmitSoundOnEntityOnlyToPlayer( drone, player, alias )
+ else
+ EmitSoundOnEntity( drone, alias )
+
+
+/*
+Hostiles detected, marking targets
+diag_gs_drone_detectHostileTargets
+
+Drone targets marked
+diag_gs_drone_targetsMarked
+
+Escort drone destroyed
+diag_gs_drone_escortDestroyed
+
+Multiple escort drones combined. Shield radius increased
+diag_gs_drone_combinedShieldRadius
+
+Multiple escort drones combined. Projectile accuracy increased
+diag_gs_drone_combinedWpnAccuracy
+
+Recharging drone shield
+diag_gs_drone_rechargingShield
+
+Target lost
+diag_gs_drone_targetLost
+
+Target acquired
+diag_gs_drone_targetAcquired
+*/
+
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+function DroneOnLeeched( drone, player )
+{
+ //global behavior when this npc gets leeched
+ delaythread ( 1 ) DroneDialogue( drone, "hack_success", player )
+}
+
+function DroneSelfDestruct( drone, delay )
+{
+ drone.EndSignal( "OnDeath" )
+ wait delay
+ drone.Die()
+}
+
+function RepairDroneThink( entity drone )
+{
+ drone.EndSignal( "OnDeath" )
+ local attachID
+ EmitSoundOnEntity( drone, "colony_spectre_initialize_beep" )
+ thread DroneSelfDestruct( drone, 60 )
+
+ for ( ;; )
+ {
+ if ( drone.e.repairSoul == null )
+ {
+ wait 1
+ continue
+ }
+
+ string attachName = "HIJACK"
+ entity repairTitan = drone.e.repairSoul.GetTitan()
+
+ /*
+ if ( IsSoul( repairTarget ) )
+ {
+ repairTarget = repairTarget.GetTitan()
+ attachName = "HIJACK"
+ }
+ else
+ {
+ Assert( !repairTarget.IsTitan() )
+ attachName = "ORIGIN"
+ }
+
+ if ( !IsAlive( repairTitan ) )
+ {
+ wait 2
+ continue
+ }
+ */
+
+ drone.SetOwner( repairTitan )
+
+ if ( DroneCanRepairTarget( drone, repairTitan, attachName ) )
+ {
+ // close enough to repair?
+ //P_wpn_defender_beam
+ waitthread DroneRepairsTarget( drone, repairTitan, attachName )
+ }
+ WaitFrame()
+ }
+}
+
+bool function DroneCanRepairTarget( drone, ent, attachName )
+{
+ expect entity( ent )
+
+ if ( !IsAlive( ent ) )
+ return false
+
+ if ( ent.GetHealth() >= ent.GetMaxHealth() )
+ return false
+
+ local attachID = ent.LookupAttachment( attachName )
+ local origin = ent.GetAttachmentOrigin( attachID )
+ local droneOrigin = drone.GetOrigin()
+ if ( Distance( droneOrigin, origin ) > 600 )
+ return false
+
+ float trace = TraceLineSimple( droneOrigin, origin, ent )
+ return trace == 1.0
+}
+
+function DroneRepairsTarget( drone, ent, attachName )
+{
+ expect entity( drone )
+ expect entity( ent )
+
+ drone.EndSignal( "OnDestroy" )
+ EmitSoundOnEntity( drone, "EMP_Titan_Electrical_Field" )
+
+ OnThreadEnd(
+ function() : ( drone )
+ {
+ if ( IsValid( drone ) )
+ StopSoundOnEntity( drone, "EMP_Titan_Electrical_Field" )
+ }
+ )
+
+ int followBehavior = GetDefaultNPCFollowBehavior( drone )
+ drone.SetOwner( ent )
+ drone.InitFollowBehavior( ent, followBehavior )
+ drone.EnableBehavior( "Follow" )
+
+ for ( ;; )
+ {
+ if ( !DroneCanRepairTarget( drone, ent, attachName ) )
+ return
+
+ DroneRepairFX( drone, ent, attachName )
+
+ local maxHealth = ent.GetMaxHealth()
+ local healAmount = maxHealth * 0.015 // 0.005
+ float healTime = RandomFloatRange( 0.8, 1.2 )
+
+ for ( float i = 0.0; i < healTime; i++ )
+ {
+ if ( !IsAlive( ent ) )
+ return
+
+ local newHealth = ent.GetHealth() + healAmount
+ newHealth = min( newHealth, maxHealth )
+ ent.SetHealth( newHealth )
+ WaitFrame()
+ }
+ }
+}
+
+function DroneRepairFX( drone, ent, attachName )
+{
+ // Control point sets the end position of the effect
+ entity cpEnd = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpEnd, UniqueString( "arc_cannon_beam_cpEnd" ) )
+ cpEnd.SetParent( ent, attachName, false, 0.0 )
+ DispatchSpawn( cpEnd )
+
+ entity zapBeam = CreateEntity( "info_particle_system" )
+ zapBeam.kv.cpoint1 = cpEnd.GetTargetName()
+
+ zapBeam.SetValueForEffectNameKey( ARC_CANNON_BEAM_EFFECT )
+ zapBeam.kv.start_active = 0
+ zapBeam.SetOwner( drone )
+ zapBeam.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY)
+ zapBeam.SetParent( drone, "ORIGIN", false, 0.0 )
+ DispatchSpawn( zapBeam )
+
+ zapBeam.Fire( "Start" )
+ zapBeam.Fire( "StopPlayEndCap", "", 2.0 )
+ zapBeam.Kill_Deprecated_UseDestroyInstead( 2.0 )
+ cpEnd.Kill_Deprecated_UseDestroyInstead( 2.0 )
+}
+
+
+function SetRepairDroneTarget( entity drone, entity repairTitan )
+{
+ Assert( IsAlive( repairTitan ), "Repair target " + repairTitan + " is dead" )
+ Assert( repairTitan.IsTitan() )
+ drone.e.repairSoul = repairTitan.GetTitanSoul()
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_emp_titans.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_emp_titans.gnut
new file mode 100644
index 00000000..638166c8
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_emp_titans.gnut
@@ -0,0 +1,181 @@
+untyped
+
+global function EmpTitans_Init
+
+global function EMPTitanThinkConstant
+
+const DAMAGE_AGAINST_TITANS = 150
+const DAMAGE_AGAINST_PILOTS = 40
+
+const EMP_DAMAGE_TICK_RATE = 0.3
+const FX_EMP_FIELD = $"P_xo_emp_field"
+const FX_EMP_FIELD_1P = $"P_body_emp_1P"
+
+function EmpTitans_Init()
+{
+ AddDamageCallbackSourceID( eDamageSourceId.titanEmpField, EmpField_DamagedEntity )
+ PrecacheParticleSystem( FX_EMP_FIELD )
+ PrecacheParticleSystem( FX_EMP_FIELD_1P )
+
+ RegisterSignal( "StopEMPField" )
+}
+
+void function EMPTitanThinkConstant( entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "Doomed" )
+ titan.EndSignal( "StopEMPField" )
+
+ //We don't want pilots accidently rodeoing an electrified titan.
+ DisableTitanRodeo( titan )
+
+ //Used to identify this titan as an arc titan
+ SetTargetName( titan, "empTitan" )
+
+ //Wait for titan to stand up and exit bubble shield before deploying arc ability.
+ WaitTillHotDropComplete( titan )
+
+ if ( HasSoul( titan ) )
+ {
+ entity soul = titan.GetTitanSoul()
+ soul.EndSignal( "StopEMPField" )
+ }
+
+ local attachment = GetEMPAttachmentForTitan( titan )
+
+ local attachID = titan.LookupAttachment( attachment )
+
+ EmitSoundOnEntity( titan, "EMP_Titan_Electrical_Field" )
+
+ array<entity> particles = []
+
+ //emp field fx
+ vector origin = titan.GetAttachmentOrigin( attachID )
+ if ( titan.IsPlayer() )
+ {
+ entity particleSystem = CreateEntity( "info_particle_system" )
+ particleSystem.kv.start_active = 1
+ particleSystem.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER
+ particleSystem.SetValueForEffectNameKey( FX_EMP_FIELD_1P )
+
+ particleSystem.SetOrigin( origin )
+ particleSystem.SetOwner( titan )
+ DispatchSpawn( particleSystem )
+ particleSystem.SetParent( titan, GetEMPAttachmentForTitan( titan ) )
+ particles.append( particleSystem )
+ }
+
+ entity particleSystem = CreateEntity( "info_particle_system" )
+ particleSystem.kv.start_active = 1
+ if ( titan.IsPlayer() )
+ particleSystem.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // everyone but owner
+ else
+ particleSystem.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ particleSystem.SetValueForEffectNameKey( FX_EMP_FIELD )
+ particleSystem.SetOwner( titan )
+ particleSystem.SetOrigin( origin )
+ DispatchSpawn( particleSystem )
+ particleSystem.SetParent( titan, GetEMPAttachmentForTitan( titan ) )
+ particles.append( particleSystem )
+
+ titan.SetDangerousAreaRadius( ARC_TITAN_EMP_FIELD_RADIUS )
+
+ OnThreadEnd(
+ function () : ( titan, particles )
+ {
+ if ( IsValid( titan ) )
+ {
+ StopSoundOnEntity( titan, "EMP_Titan_Electrical_Field" )
+ EnableTitanRodeo( titan ) //Make the arc titan rodeoable now that it is no longer electrified.
+ }
+
+ foreach ( particleSystem in particles )
+ {
+ if ( IsValid_ThisFrame( particleSystem ) )
+ {
+ particleSystem.ClearParent()
+ particleSystem.Fire( "StopPlayEndCap" )
+ particleSystem.Kill_Deprecated_UseDestroyInstead( 1.0 )
+ }
+ }
+ }
+ )
+
+ wait RandomFloat( EMP_DAMAGE_TICK_RATE )
+
+ while ( true )
+ {
+ origin = titan.GetAttachmentOrigin( attachID )
+
+ RadiusDamage(
+ origin, // center
+ titan, // attacker
+ titan, // inflictor
+ DAMAGE_AGAINST_PILOTS, // damage
+ DAMAGE_AGAINST_TITANS, // damageHeavyArmor
+ ARC_TITAN_EMP_FIELD_INNER_RADIUS, // innerRadius
+ ARC_TITAN_EMP_FIELD_RADIUS, // outerRadius
+ SF_ENVEXPLOSION_NO_DAMAGEOWNER, // flags
+ 0, // distanceFromAttacker
+ DAMAGE_AGAINST_PILOTS, // explosionForce
+ DF_ELECTRICAL | DF_STOPS_TITAN_REGEN, // scriptDamageFlags
+ eDamageSourceId.titanEmpField ) // scriptDamageSourceIdentifier
+
+ wait EMP_DAMAGE_TICK_RATE
+ }
+}
+
+void function EmpField_DamagedEntity( entity target, var damageInfo )
+{
+ if ( !IsAlive( target ) )
+ return
+
+ entity titan = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !IsValid( titan ) )
+ return
+
+ local className = target.GetClassName()
+ if ( className == "rpg_missile" || className == "npc_turret_sentry" || className == "grenade" )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ if ( DamageInfo_GetDamage( damageInfo ) <= 0 )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ if ( target.IsPlayer() )
+ {
+ if ( !titan.IsPlayer() && IsArcTitan( titan ) )
+ {
+ if ( !titan.s.electrocutedPlayers.contains( target ) )
+ titan.s.electrocutedPlayers.append( target )
+ }
+
+ const ARC_TITAN_SCREEN_EFFECTS = 0.085
+ const ARC_TITAN_EMP_DURATION = 0.35
+ const ARC_TITAN_EMP_FADEOUT_DURATION = 0.35
+
+ local attachID = titan.LookupAttachment( "hijack" )
+ local origin = titan.GetAttachmentOrigin( attachID )
+ local distSqr = DistanceSqr( origin, target.GetOrigin() )
+
+ local minDist = ARC_TITAN_EMP_FIELD_INNER_RADIUS_SQR
+ local maxDist = ARC_TITAN_EMP_FIELD_RADIUS_SQR
+ local empFxHigh = ARC_TITAN_SCREEN_EFFECTS
+ local empFxLow = ( ARC_TITAN_SCREEN_EFFECTS * 0.6 )
+ float screenEffectAmplitude = GraphCapped( distSqr, minDist, maxDist, empFxHigh, empFxLow )
+
+ StatusEffect_AddTimed( target, eStatusEffect.emp, screenEffectAmplitude, ARC_TITAN_EMP_DURATION, ARC_TITAN_EMP_FADEOUT_DURATION )
+ }
+}
+
+string function GetEMPAttachmentForTitan( entity titan )
+{
+ return "hijack"
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_gunship.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_gunship.gnut
new file mode 100644
index 00000000..2f1fdc96
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_gunship.gnut
@@ -0,0 +1,97 @@
+untyped
+
+global function AiGunship_Init
+
+global function GunshipThink
+
+global const SOUND_GUNSHIP_HOVER = "Gunship_Hover"
+global const SOUND_GUNSHIP_EXPLODE_DEFAULT = "Gunship_Explode"
+global const FX_GUNSHIP_EXPLOSION = $"P_veh_exp_crow"
+
+function AiGunship_Init()
+{
+ PrecacheParticleSystem( FX_GUNSHIP_EXPLOSION )
+ AddDeathCallback( "npc_gunship", GunshipDeath )
+}
+
+
+function GunshipThink( gunship )
+{
+ gunship.EndSignal( "OnDeath" )
+
+ entity owner
+ entity currentTarget
+ local accuracyMultiplierBase = gunship.kv.AccuracyMultiplier
+ local accuracyMultiplierAgainstDrones = 100
+
+ while( true )
+ {
+ wait 0.25
+
+ //----------------------------------
+ // Get owner and current enemy
+ //----------------------------------
+ currentTarget = expect entity( gunship.GetEnemy() )
+ owner = expect entity( gunship.GetFollowTarget() )
+
+ //----------------------------------
+ // Free roam if owner is dead or HasEnemy
+ //----------------------------------
+ if ( ( !IsAlive( owner ) ) || ( currentTarget != null ) )
+ {
+ gunship.DisableBehavior( "Follow" )
+ }
+
+ //---------------------------------------------------------------------
+ // If owner is alive and no enemies in sight, go back and follow owner
+ //----------------------------------------------------------------------
+ if ( ( IsAlive( owner ) ) && ( currentTarget == null ) )
+ {
+ gunship.EnableBehavior( "Follow" )
+ }
+
+
+ //----------------------------------------------
+ // Jack up accuracy if targeting a small target (like a drone)
+ //----------------------------------------------
+ if ( ( currentTarget != null ) && ( IsAirDrone( currentTarget ) ) )
+ {
+ gunship.kv.AccuracyMultiplier = accuracyMultiplierAgainstDrones
+ }
+ else
+ {
+ gunship.kv.AccuracyMultiplier = accuracyMultiplierBase
+ }
+ }
+
+}
+
+
+void function GunshipDeath( entity gunship, var damageInfo )
+{
+ /*
+ Script errors
+
+ // Explosion effect
+ entity explosion = CreateEntity( "info_particle_system" )
+ explosion.SetOrigin( gunship.GetWorldSpaceCenter() )
+ explosion.SetAngles( gunship.GetAngles() )
+ explosion.SetValueForEffectNameKey( FX_GUNSHIP_EXPLOSION )
+ explosion.kv.start_active = 1
+ DispatchSpawn( explosion )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, gunship.GetOrigin(), SOUND_GUNSHIP_EXPLODE_DEFAULT )
+ explosion.destroy( 3 )
+
+ gunship.Destroy()
+
+ P_veh_exp_hornet, TAG_ORIGIN, attach
+
+ */
+
+ //TEMP
+ PlayFX( FX_GUNSHIP_EXPLOSION, gunship.GetOrigin() )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, gunship.GetOrigin(), "Goblin_Dropship_Explode" )
+ gunship.Destroy()
+}
+
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_lethality.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_lethality.gnut
new file mode 100644
index 00000000..771fe6d9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_lethality.gnut
@@ -0,0 +1,97 @@
+untyped
+
+global enum eAILethality
+{
+ VeryLow,
+ Low,
+ Medium,
+ High,
+ VeryHigh
+}
+
+global function SetAILethality
+
+global function UpdateNPCForAILethality
+
+function SetAILethality( aiLethality )
+{
+ Assert( IsMultiplayer() )
+ level.nv.aiLethality = aiLethality
+
+ switch ( aiLethality )
+ {
+ case eAILethality.Medium:
+ break
+
+ case eAILethality.High:
+ NPCSetAimConeFocusParams( 6, 2.5 )
+ NPCSetAimPatternFocusParams( 4, 0.3, 0.8 )
+ break
+ case eAILethality.VeryHigh:
+ NPCSetAimConeFocusParams( 5, 2.0 )
+ NPCSetAimPatternFocusParams( 4, 0.3, 0.8 )
+ break
+ }
+
+ // reset ai lethality
+
+ array<entity> npcs = GetNPCArray()
+ foreach ( npc in npcs )
+ {
+ UpdateNPCForAILethality( npc )
+ }
+}
+
+
+function SetTitanAccuracyAndProficiency( entity npcTitan )
+{
+ Assert( IsMultiplayer() )
+ int lethality = Riff_AILethality()
+ float accuracyMultiplier = 1.0
+ int weaponProficiency = eWeaponProficiency.GOOD
+
+ entity player = GetPetTitanOwner( npcTitan )
+ entity soul = npcTitan.GetTitanSoul()
+
+ // auto titans have lower proficiency
+ if ( player && soul == null)
+ {
+ soul = player.GetTitanSoul() // in mid transfer
+ }
+
+ if ( IsValid( soul ) )
+ {
+ if ( SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ {
+ weaponProficiency = eWeaponProficiency.GOOD
+ }
+ else if ( player )
+ {
+ weaponProficiency = eWeaponProficiency.AVERAGE
+ entity ordnanceWeapon = npcTitan.GetOffhandWeapon( OFFHAND_ORDNANCE )
+ if ( IsValid( ordnanceWeapon ) )
+ ordnanceWeapon.AllowUse( false )
+
+ entity centerWeapon = npcTitan.GetOffhandWeapon( OFFHAND_TITAN_CENTER )
+ if ( IsValid( centerWeapon ) )
+ centerWeapon.AllowUse( false )
+ }
+ }
+
+ npcTitan.kv.AccuracyMultiplier = accuracyMultiplier
+ npcTitan.kv.WeaponProficiency = weaponProficiency
+}
+
+function UpdateNPCForAILethality( entity npc )
+{
+ Assert( IsMultiplayer() )
+ if ( npc.IsTitan() )
+ {
+ SetTitanAccuracyAndProficiency( npc )
+ return
+ }
+
+ if ( IsMinion( npc ) )
+ SetProficiency( npc )
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_faces.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_faces.gnut
new file mode 100644
index 00000000..e6d3bcf0
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_faces.gnut
@@ -0,0 +1,226 @@
+untyped
+
+global function MarvinFaces_Init
+
+global function MarvinFace
+global function MarvinThinksAwhile
+global function MarvinFaceExists
+global function SetMarvinBodyType
+global function MarvinSetFace
+
+function MarvinFaces_Init()
+{
+
+ RegisterSignal( "StopThinking" )
+
+ AddSpawnCallback( "npc_marvin", MarvinSpawnCallback )
+
+ SetupMarvinFaces()
+}
+
+function SetupMarvinFaces()
+{
+ // setup marvin face mappings
+ level.marvinFaces <- {}
+ level.marvinFaces[ MARVIN_TYPE_WORKER ] <-
+ {
+ none = 0
+ happy = 1
+ sad = 2
+ angry = 3
+ think1 = 4
+ think2 = 5
+ question = 6
+ }
+
+ // Use the yellow worker marvin skins since shooters are from SP and shooter value = 0 which is LevelEd's default.
+ // Real shooter skins are 7-13. If we want real skins, we can add them here and adjust the marvin spawn points per level.
+ level.marvinFaces[ MARVIN_TYPE_SHOOTER ] <-
+ {
+ none = 0
+ happy = 1
+ sad = 2
+ angry = 3
+ think1 = 4
+ think2 = 5
+ question = 6
+ }
+
+ level.marvinFaces[ MARVIN_TYPE_FIREFIGHTER ] <-
+ {
+ none = 14
+ happy = 15
+ sad = 16
+ angry = 17
+ think1 = 18
+ think2 = 19
+ question = 20
+ }
+
+ // No idea what this type of marvin is...legacy stuff. Just make them yellow.
+ level.marvinFaces[ MARVIN_TYPE_MARVINONE ] <-
+ {
+ none = 0
+ happy = 1
+ sad = 2
+ angry = 3
+ think1 = 4
+ think2 = 5
+ question = 6
+ }
+
+// Nothing uses this except debug statements that are commented out
+/*
+ level.marvinFaceNames <- {}
+ // invert map for tests
+ foreach ( key, val in level.marvinFace )
+ {
+ level.marvinFaceNames[ val ] <- key
+ }
+*/
+}
+
+void function MarvinFace( entity marvin )
+{
+ thread MarvinFaceThink( marvin )
+}
+
+void function MarvinFaceThink( entity marvin )
+{
+ //printl( "Setting up marvin face for " + marvin )
+ for ( ;; )
+ {
+ waitthread MarvinUndamagedFacePicker( marvin )
+
+// printl( "damaged " + marvin )
+ if ( !IsAlive( marvin ) )
+ break
+
+ waitthread MarvinWounded( marvin )
+
+ if ( !IsAlive( marvin ) )
+ break
+ }
+
+ if ( IsValid_ThisFrame( marvin ) )
+ MarvinSetFace( marvin, "none" )
+}
+
+function MarvinWounded( marvin )
+{
+ marvin.EndSignal( "OnDeath" )
+ MarvinSetFace( marvin, "sad" )
+ wait 2.3
+ waitthread MarvinThinksAwhile( marvin, RandomFloatRange( 2, 4 ) )
+}
+
+void function EntSignals( entity ent, string signal )
+{
+ if ( IsValid_ThisFrame( ent ) )
+ ent.Signal( signal )
+}
+
+function MarvinThinksAwhile( marvin, time )
+{
+ expect entity( marvin )
+
+ marvin.EndSignal( "StopThinking" )
+ delaythread( time ) EntSignals( marvin, "StopThinking" )
+
+ // think for a bit
+ for ( ;; )
+ {
+ MarvinSetFace( marvin, "think1" )
+ wait 0.4
+ MarvinSetFace( marvin, "think2" )
+ wait 0.4
+ }
+}
+
+function MarvinUndamagedFacePicker( marvin )
+{
+ marvin.EndSignal( "OnDeath" )
+ marvin.EndSignal( "OnDamaged" )
+ local i
+
+ for ( ;; )
+ {
+ if ( !marvin.GetEnemy() )
+ {
+ MarvinSetFace( marvin, "happy" )
+ marvin.WaitSignal( "OnFoundEnemy" )
+ }
+
+ waitthread MarvinThinksAwhile( marvin, RandomFloatRange( 2, 4 ) )
+
+ if ( marvin.GetEnemy() )
+ {
+ MarvinSetFace( marvin, "angry" )
+ marvin.WaitSignal( "OnLostEnemy" )
+ }
+ }
+}
+
+function MarvinSetFace( self, face )
+{
+// printl( self + " got face " + face )
+ Assert( MarvinFaceExists( self, face ), "No face " + face + " in level.marvinFace" )
+
+ //prin( "Changing " + self + " face from " + level.marvinFaceNames[ skin ] + " to " + face )
+ self.SetSkin( GetMarvinFace( self, face ) )
+ self.Signal( "StopThinking" )
+}
+
+function MarvinFaceExists( npc_marvin, face )
+{
+ local marvinType = GetMarvinBodyType( npc_marvin )
+
+ if ( marvinType in level.marvinFaces )
+ return true
+
+// return ( face in level.marvinFaces[ marvinType ] )
+}
+
+function GetMarvinFace( npc_marvin, face )
+{
+ local marvinType = GetMarvinBodyType( npc_marvin )
+
+ Assert( MarvinFaceExists( npc_marvin, face ), "No face " + face + " in level.marvinFace" )
+
+ local faceID = level.marvinFaces[ marvinType ][ face ]
+
+ return faceID
+}
+
+function SetMarvinBodyType( npc_marvin )
+{
+ if( "bodytype" in npc_marvin.s )
+ {
+ Assert( npc_marvin.s.bodytype >= MARVIN_TYPE_SHOOTER && npc_marvin.s.bodytype <= MARVIN_TYPE_FIREFIGHTER, "Specified invalid body type index " + npc_marvin.s.bodytype + ", Use values from 0-2 instead." )
+
+ switch( npc_marvin.s.bodytype )
+ {
+ case MARVIN_TYPE_FIREFIGHTER:
+ local index = npc_marvin.FindBodyGroup( "firefighter" )
+ local state = 1
+ npc_marvin.SetBodygroup( index, state )
+ break
+ }
+ }
+}
+
+function GetMarvinBodyType( npc_marvin )
+{
+ local bodyType = MARVIN_TYPE_WORKER
+
+ if( "bodytype" in npc_marvin.s )
+ bodyType = npc_marvin.s.bodytype
+
+ return bodyType
+}
+
+void function MarvinSpawnCallback( entity npc_marvin )
+{
+ SetMarvinBodyType( npc_marvin )
+ npc_marvin.SetDeathNotifications( true ) //Primarily so we can do HandleDeathPackage for Marvins. Can just add a deathcallback if this is too expensive
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_jobs.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_jobs.gnut
new file mode 100644
index 00000000..588b4d75
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_jobs.gnut
@@ -0,0 +1,600 @@
+
+/*
+ ToDo:
+ -if marvin has no jobs to go to make him to back to spawn position instead of standing at last node
+*/
+
+global function MarvinJobs_Init
+global function MarvinJobThink
+global function GetMarvinType
+
+const DEBUG_MARVIN_JOBS = false
+const MAX_JOB_SEARCH_DIST_SQR = 1000 * 1000
+const JOB_NODE_COOLDOWN_TIME = 15.0
+
+struct MarvinJob
+{
+ string validMarvinType
+ entity node
+ entity user
+ string jobType
+ bool tempJob
+ float nextUsableTime = 0
+ entity barrel
+}
+
+struct
+{
+ array<MarvinJob> marvinJobs
+ table<string,void functionref( entity,MarvinJob)> jobFunctions
+} file
+
+
+
+
+// ██╗███╗ ██╗██╗████████╗
+// ██║████╗ ██║██║╚â•â•â–ˆâ–ˆâ•”â•â•â•
+// ██║██╔██╗ ██║██║ ██║
+// ██║██║╚██╗██║██║ ██║
+// ██║██║ ╚████║██║ ██║
+// â•šâ•â•â•šâ•â• â•šâ•â•â•â•â•šâ•â• â•šâ•â•
+
+void function MarvinJobs_Init()
+{
+ file.jobFunctions[ "welding" ] <- SimpleJobAnims
+ file.jobFunctions[ "welding_under" ] <- SimpleJobAnims
+ file.jobFunctions[ "window" ] <- SimpleJobAnims
+ file.jobFunctions[ "fightFire" ] <- SimpleJobAnims
+ file.jobFunctions[ "barrel_pickup" ] <- MarvinPicksUpBarrel
+ file.jobFunctions[ "barrel_putdown" ] <- MarvinPutsDownBarrel
+ file.jobFunctions[ "repair_over_edge" ] <- SimpleJobAnims
+ file.jobFunctions[ "repair_above" ] <- SimpleJobAnims
+ file.jobFunctions[ "repair_under" ] <- SimpleJobAnims
+ file.jobFunctions[ "datacards" ] <- SimpleJobAnims
+
+ file.jobFunctions[ "drone_welding" ] <- SimpleJobAnims
+ file.jobFunctions[ "drone_inspect" ] <- SimpleJobAnims
+
+ RegisterSignal( "pickup_barrel" )
+ RegisterSignal( "putdown_barrel" )
+ RegisterSignal( "JobStarted" )
+ RegisterSignal( "StopDoingJobs" )
+
+ AddSpawnCallback( "script_marvin_job", InitMarvinJob )
+
+ AddCallback_EntitiesDidLoad( MarvinJobsEntitiesDidLoad )
+}
+
+void function InitMarvinJob( entity node )
+{
+ Assert( node.HasKey( "job" ) )
+ Assert( node.kv.job != "" )
+ Assert( string( node.kv.job ) in file.jobFunctions, "Marvin job node at " + node.GetOrigin() + " has unhandled job type " + string( node.kv.job ) )
+ string editorClass = GetEditorClass( node )
+
+ // Drop node to ground for certain types or if checked on the entity
+ if ( editorClass == "" )
+ {
+ if ( !node.HasKey( "hover" ) || node.kv.hover != "1" )
+ DropToGround( node )
+ }
+
+ if ( DEBUG_MARVIN_JOBS )
+ DebugDrawAngles( node.GetOrigin(), node.GetAngles() )
+
+ // Create marvin job struct
+ MarvinJob marvinJob
+ marvinJob.node = node
+ marvinJob.jobType = string( node.kv.job )
+ marvinJob.tempJob = node.HasKey( "tempJob" ) && node.kv.tempJob == "1"
+
+ if ( marvinJob.jobType == "barrel_pickup" )
+ marvinJob.barrel = CreateBarrel( node )
+
+ // Set what marvin_type of NPC can use this job
+ switch ( editorClass )
+ {
+ case "script_marvin_drone_job":
+ marvinJob.validMarvinType = "marvin_type_drone"
+ break
+ default:
+ marvinJob.validMarvinType = "marvin_type_walker"
+ break
+ }
+
+ file.marvinJobs.append( marvinJob )
+}
+
+void function MarvinJobsEntitiesDidLoad()
+{
+ if ( DEBUG_MARVIN_JOBS )
+ DebugMarvinJobs()
+}
+
+
+
+
+
+// ████████╗██╗ ██╗██╗███╗ ██╗██╗ ██╗
+// â•šâ•â•â–ˆâ–ˆâ•”â•â•â•â–ˆâ–ˆâ•‘ ██║██║████╗ ██║██║ ██╔â•
+// ██║ ███████║██║██╔██╗ ██║█████╔â•
+// ██║ ██╔â•â•â–ˆâ–ˆâ•‘██║██║╚██╗██║██╔â•â–ˆâ–ˆâ•—
+// ██║ ██║ ██║██║██║ ╚████║██║ ██╗
+// â•šâ•â• â•šâ•â• â•šâ•â•â•šâ•â•â•šâ•â• â•šâ•â•â•â•â•šâ•â• â•šâ•â•
+
+void function MarvinJobThink( entity marvin )
+{
+ EndSignal( marvin, "OnDeath" )
+ EndSignal( marvin, "OnDestroy" )
+ EndSignal( marvin, "StopDoingJobs" )
+
+ // Wait a frame because npcs that are spawned at map load may run this function before job nodes are finished being initialized
+ WaitFrame()
+
+ // Get all jobs this marvin can do
+ array<MarvinJob> jobs = GetJobsForMarvin( marvin )
+ if ( jobs.len() == 0 )
+ return
+
+ OnThreadEnd(
+ function() : ( marvin )
+ {
+ Assert( !IsAlive( marvin ), "MarvinJobThink ended but the marvin is still alive" )
+ }
+ )
+
+ while ( true )
+ {
+ foreach ( MarvinJob job in jobs )
+ {
+ waitthread MarvinDoJob( marvin, job )
+ WaitFrame()
+ }
+
+ jobs.randomize()
+ WaitFrame()
+ }
+}
+
+void function MarvinDoJob( entity marvin, MarvinJob job )
+{
+ Assert( IsAlive( marvin ), "Marvin " + marvin + " is not alive" )
+ EndSignal( marvin, "OnFailedToPath" )
+ EndSignal( marvin, "OnDeath" )
+
+ // Don't do a job that's already in use or not ready to be used again
+ if ( IsValid( job.user ) || Time() < job.nextUsableTime )
+ return
+
+ // Don't use a barrel put down job if you can'r carrying a barrel
+ if ( job.jobType == "barrel_putdown" && !IsValid( marvin.ai.carryBarrel ) )
+ return
+
+ // If you're carrying a barrel, only do a barrel put down job
+ if ( IsValid( marvin.ai.carryBarrel ) && job.jobType != "barrel_putdown" )
+ return
+
+ OnThreadEnd(
+ function() : ( job )
+ {
+ job.user = null
+ job.nextUsableTime = Time() + JOB_NODE_COOLDOWN_TIME
+ }
+ )
+
+ // Default walk anim
+ MarvinDefaultMoveAnim( marvin )
+
+ // Node gets occupied
+ job.user = marvin
+
+ if ( DEBUG_MARVIN_JOBS )
+ DebugDrawLine( marvin.GetWorldSpaceCenter(), job.node.GetOrigin(), 255, 0, 0, true, 3.0 )
+
+ // Run the job function
+ thread DontDisableJobOnPathFailOrDeath( marvin, job )
+ waitthread file.jobFunctions[ job.jobType ]( marvin, job )
+ if ( IsValid( marvin ) )
+ marvin.Anim_Stop()
+}
+
+void function DontDisableJobOnPathFailOrDeath( entity marvin, MarvinJob job )
+{
+ EndSignal( marvin, "JobStarted" )
+ WaitSignal( marvin, "OnFailedToPath", "OnDeath" )
+ job.nextUsableTime = Time()
+}
+
+
+
+
+
+// ██╗ ██████╗ ██████╗ ███████╗██╗ ██╗███╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗███████╗
+// ██║██╔â•â•â•â–ˆâ–ˆâ•—██╔â•â•â–ˆâ–ˆâ•— ██╔â•â•â•â•â•â–ˆâ–ˆâ•‘ ██║████╗ ██║██╔â•â•â•â•â•â•šâ•â•â–ˆâ–ˆâ•”â•â•â•â–ˆâ–ˆâ•‘██╔â•â•â•â–ˆâ–ˆâ•—████╗ ██║██╔â•â•â•â•â•
+// ██║██║ ██║██████╔╠█████╗ ██║ ██║██╔██╗ ██║██║ ██║ ██║██║ ██║██╔██╗ ██║███████╗
+// ██ ██║██║ ██║██╔â•â•â–ˆâ–ˆâ•— ██╔â•â•â• ██║ ██║██║╚██╗██║██║ ██║ ██║██║ ██║██║╚██╗██║╚â•â•â•â•â–ˆâ–ˆâ•‘
+// ╚█████╔â•â•šâ–ˆâ–ˆâ–ˆâ–ˆâ–ˆâ–ˆâ•”â•â–ˆâ–ˆâ–ˆâ–ˆâ–ˆâ–ˆâ•”╠██║ ╚██████╔â•â–ˆâ–ˆâ•‘ ╚████║╚██████╗ ██║ ██║╚██████╔â•â–ˆâ–ˆâ•‘ ╚████║███████║
+// â•šâ•â•â•â•â• â•šâ•â•â•â•â•â• â•šâ•â•â•â•â•â• â•šâ•â• â•šâ•â•â•â•â•â• â•šâ•â• â•šâ•â•â•â• â•šâ•â•â•â•â•â• â•šâ•â• â•šâ•â• â•šâ•â•â•â•â•â• â•šâ•â• â•šâ•â•â•â•â•šâ•â•â•â•â•â•â•
+
+void function SimpleJobAnims( entity marvin, MarvinJob job )
+{
+ // Get the anims to use for the job
+ array<string> anims
+ switch ( job.jobType )
+ {
+ // Marvin jobs
+ case "welding":
+ anims.append( "mv_idle_weld" )
+ break
+ case "welding_under":
+ anims.append( "mv_weld_under" )
+ anims.append( "mv_weld_under" )
+ anims.append( "mv_weld_under_stumble" )
+ break
+ case "window":
+ anims.append( "mv_idle_wash_window_noloop" )
+ anims.append( "mv_idle_buff_window_noloop" )
+ break
+ case "fightFire":
+ anims.append( "mv_fireman_idle" )
+ anims.append( "mv_fireman_shift" )
+ break
+ case "repair_over_edge":
+ anims.append( "mv_repair_overedge" )
+ anims.append( "mv_repair_overedge" )
+ anims.append( "mv_repair_overedge_stumble" )
+ break
+ case "repair_above":
+ anims.append( "mv_repair_ship_above" )
+ break
+ case "repair_under":
+ anims.append( "mv_repair_under" )
+ anims.append( "mv_repair_under_stumble" )
+ break
+ case "datacards":
+ anims.append( "mv_job_replace_datacards" )
+ break
+
+ // Marvin drone jobs
+ case "drone_welding":
+ anims.append( "dw_jobs_welding_wallpanel" )
+ break
+ case "drone_inspect":
+ anims.append( "inspect1" )
+ anims.append( "inspect2" )
+ break
+ }
+ Assert( anims.len() > 0 )
+
+ if ( IsMarvinWalker( marvin ) )
+ waitthread MarvinRunToAnimStart( marvin, anims[0], job.node )
+ else
+ waitthread MarvinFlyToAnimStart( marvin, anims[0], job.node )
+
+ Signal( marvin, "JobStarted" )
+
+ while ( true )
+ {
+ anims.randomize()
+ foreach ( string anim in anims )
+ {
+ float animLength = marvin.GetSequenceDuration( anim ) // wait anim length because some anims may be looping so we can't wait for them to end
+
+ if ( IsMarvinDrone( marvin ) )
+ thread PlayAnimTeleport( marvin, anim, job.node )
+ else
+ thread PlayAnim( marvin, anim, job.node, null, 0.6 )
+
+ wait animLength
+ }
+ if ( job.tempJob )
+ break
+ }
+}
+
+void function MarvinPicksUpBarrel( entity marvin, MarvinJob job )
+{
+ // Don't try to pick up a barrel if there isn't one nearby
+ if ( !IsValid( job.barrel ) )
+ return
+ if ( Distance( job.node.GetOrigin(), job.barrel.GetOrigin() ) > 25 )
+ return
+
+ EndSignal( job.barrel, "OnDestroy" )
+
+ entity info_target = CreateEntity( "info_target" )
+ DispatchSpawn( info_target )
+
+ OnThreadEnd(
+ function () : ( info_target )
+ {
+ info_target.Destroy()
+ }
+ )
+
+ vector barrelFlatAngles = job.barrel.GetAngles()
+ barrelFlatAngles.x = 0
+ barrelFlatAngles.z = 0
+
+ info_target.SetOrigin( job.barrel.GetOrigin() )
+ info_target.SetAngles( barrelFlatAngles )
+
+ DropToGround( info_target )
+
+ if ( info_target.GetOrigin().z < -MAX_WORLD_COORD )
+ return // Fell through map
+
+ if ( DEBUG_MARVIN_JOBS )
+ thread DrawAnglesForMovingEnt( info_target, 30.0 )
+
+
+ // Go to the barrel
+ MarvinRunToAnimStart( marvin, "mv_carry_barrel_pickup", info_target )
+
+ // Try to pick it up
+ thread PlayAnim( marvin, "mv_carry_barrel_pickup", info_target, null, 0.6 )
+
+ // Wait until animation should pick up the barrel
+ marvin.WaitSignal( "pickup_barrel" )
+
+ // Get attachment info
+ string attachment = "PROPGUN"
+ int attachIndex = marvin.LookupAttachment( attachment )
+ vector attachOrigin = marvin.GetAttachmentOrigin( attachIndex )
+
+ // Make sure the barrel is close when it's time to parent the barrel
+ if ( Distance( attachOrigin, job.barrel.GetOrigin() ) > 25 )
+ {
+ marvin.Anim_Stop()
+ return
+ }
+
+ // Marvin picks up the barrel and carries it
+ thread MarvinCarryBarrel( marvin, job.barrel )
+
+ marvin.WaitSignal( "OnAnimationDone" )
+}
+
+void function MarvinCarryBarrel( entity marvin, entity barrel )
+{
+ marvin.EndSignal( "OnDeath" )
+ marvin.EndSignal( "OnDamaged" )
+ marvin.EndSignal( "putdown_barrel" )
+
+ OnThreadEnd(
+ function () : ( marvin, barrel )
+ {
+ if ( IsValid( barrel ) )
+ {
+ barrel.kv.solid = SOLID_VPHYSICS
+ barrel.ClearParent()
+ barrel.SetOwner( null )
+ EntFireByHandle( barrel, "wake", "", 0, null, null )
+ EntFireByHandle( barrel, "enablemotion", "", 0, null, null )
+ }
+
+ if ( IsAlive( marvin ) )
+ {
+ MarvinDefaultMoveAnim( marvin )
+ marvin.ClearIdleAnim()
+ marvin.ai.carryBarrel = null
+ }
+ }
+ )
+
+ string attachment = "PROPGUN"
+ marvin.SetMoveAnim( "mv_carry_barrel_walk" )
+ marvin.SetIdleAnim( "mv_carry_barrel_idle" )
+ barrel.SetParent( marvin, attachment, false, 0.5 )
+ barrel.SetOwner( marvin )
+
+ barrel.kv.solid = 0 // not solid
+
+ marvin.ai.carryBarrel = barrel
+
+ WaitSignal( marvin, "OnDestroy" )
+}
+
+void function MarvinPutsDownBarrel( entity marvin, MarvinJob job )
+{
+ Assert( IsValid( marvin.ai.carryBarrel ) )
+
+ // Don't place a barrel here if there is already one
+ if ( IsValid( job.barrel ) )
+ {
+ if ( Distance( job.node.GetOrigin(), job.barrel.GetOrigin() ) <= 25 )
+ return
+ }
+
+ EndSignal( marvin.ai.carryBarrel, "OnDestroy" )
+
+ marvin.SetMoveAnim( "mv_carry_barrel_walk" )
+ marvin.SetIdleAnim( "mv_carry_barrel_idle" )
+
+ // Walk to the put down spot
+ MarvinRunToAnimStart( marvin, "mv_carry_barrel_putdown", job.node )
+
+ // Put down the barrel
+ thread PlayAnim( marvin, "mv_carry_barrel_putdown", job.node, null, 0.6 )
+
+ // Wait for release
+ marvin.WaitSignal( "putdown_barrel" )
+
+ marvin.WaitSignal( "OnAnimationDone" )
+}
+
+
+
+
+// ██╗ ██╗████████╗██╗██╗ ██╗████████╗██╗ ██╗
+// ██║ ██║╚â•â•â–ˆâ–ˆâ•”â•â•â•â–ˆâ–ˆâ•‘██║ ██║╚â•â•â–ˆâ–ˆâ•”â•â•â•â•šâ–ˆâ–ˆâ•— ██╔â•
+// ██║ ██║ ██║ ██║██║ ██║ ██║ ╚████╔â•
+// ██║ ██║ ██║ ██║██║ ██║ ██║ ╚██╔â•
+// ╚██████╔╠██║ ██║███████╗██║ ██║ ██║
+// â•šâ•â•â•â•â•â• â•šâ•â• â•šâ•â•â•šâ•â•â•â•â•â•â•â•šâ•â• â•šâ•â• â•šâ•â•
+
+bool function IsMarvinWalker( entity marvin )
+{
+ return GetMarvinType( marvin ) == "marvin_type_walker"
+}
+
+bool function IsMarvinDrone( entity marvin )
+{
+ return GetMarvinType( marvin ) == "marvin_type_drone"
+}
+
+string function GetMarvinType( entity npc )
+{
+ var marvinType = npc.Dev_GetAISettingByKeyField( "marvin_type" )
+ if ( marvinType == null )
+ return "not_marvin"
+
+ return expect string( marvinType )
+}
+
+bool function IsJobNode( entity node )
+{
+ if ( node.GetClassName() == "script_marvin_job" )
+ return true
+ if ( GetEditorClass( node ) == "script_marvin_drone_job" )
+ return true
+ return false
+}
+
+void function MarvinDefaultMoveAnim( entity marvin )
+{
+ if ( IsMarvinWalker( marvin ) )
+ {
+ marvin.SetNPCMoveSpeedScale( 1.0 )
+ marvin.SetMoveAnim( "walk_all" )
+ }
+}
+
+array<MarvinJob> function GetJobsForMarvin( entity marvin )
+{
+ string marvinType = GetMarvinType( marvin )
+
+ // Get jobs this marvin links to, if any, and randomize
+ array<MarvinJob> linkedJobs
+ array<entity> linkedEnts = marvin.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ if ( IsJobNode( ent ) )
+ {
+ MarvinJob linkedJob = GetMarvinJobForNode( ent )
+ Assert( IsValid( linkedJob.node ) )
+
+ // Error if we are linking to the wrong type of job node
+ Assert( marvinType == linkedJob.validMarvinType, "npc_marvin at " + marvin.GetOrigin() + " links to a marvin job of the wrong marvin_type" )
+
+ linkedJobs.append( linkedJob )
+ }
+ }
+ linkedJobs.randomize()
+
+ // If marvin was linked to jobs we only consider those
+ if ( marvin.HasKey( "LinkedJobsOnly" ) && marvin.kv.LinkedJobsOnly == "1" )
+ {
+ Assert( linkedJobs.len() > 0, "marvin at " + marvin.GetOrigin() + " has LinkedJobsOnly marked but does not link to any job nodes" )
+ return linkedJobs
+ }
+
+ // Add all jobs within valid distance and randomize
+ array<MarvinJob> jobs
+ foreach ( MarvinJob marvinJob in file.marvinJobs )
+ {
+ if ( marvinType != marvinJob.validMarvinType )
+ continue
+
+ // Don't re-add a job that was linked to
+ if ( linkedJobs.contains( marvinJob ) )
+ continue
+
+ // Teleport nodes are for special case jobs with no nav mesh do son't consider them automatically
+ if ( marvinJob.node.HasKey( "teleport" ) && marvinJob.node.kv.teleport == "1" )
+ continue
+
+ // Only search for jobs within a max distance
+ if ( DistanceSqr( marvinJob.node.GetOrigin(), marvin.GetOrigin() ) <= MAX_JOB_SEARCH_DIST_SQR )
+ jobs.append( marvinJob )
+ }
+
+ // Randomize the order so the marvin does them out of order
+ jobs.randomize()
+
+ // Add the linked jobs to the list, and put them at the beginning of the priority
+ foreach ( MarvinJob linkedJob in linkedJobs )
+ jobs.insert( 0, linkedJob )
+
+ // Debug draw jobs this marvin can take
+ if ( DEBUG_MARVIN_JOBS )
+ {
+ foreach ( MarvinJob job in jobs )
+ {
+ if ( linkedJobs.contains( job ) )
+ DebugDrawLine( marvin.GetOrigin(), job.node.GetOrigin(), 255, 255, 0, true, 10.0 )
+ else
+ DebugDrawLine( marvin.GetOrigin(), job.node.GetOrigin(), 200, 200, 200, true, 10.0 )
+ }
+ }
+
+ return jobs
+}
+
+void function DebugMarvinJobs()
+{
+ while ( true )
+ {
+ foreach ( MarvinJob marvinJob in file.marvinJobs )
+ {
+ string appendText = "AVAILABLE"
+ float timeTillNextUse = marvinJob.nextUsableTime - Time()
+ if ( IsValid( marvinJob.user ) )
+ appendText = "RESERVED"
+ else if ( timeTillNextUse > 0 )
+ appendText = format( "%.1f", timeTillNextUse )
+ DebugDrawText( marvinJob.node.GetOrigin(), marvinJob.jobType + " (" + appendText + ")", true, 0.1 )
+ }
+ wait 0.05
+ }
+}
+
+MarvinJob function GetMarvinJobForNode( entity node )
+{
+ MarvinJob marvinJob
+ foreach ( MarvinJob marvinJob in file.marvinJobs )
+ {
+ if ( marvinJob.node == node )
+ return marvinJob
+ }
+ return marvinJob
+}
+
+entity function CreateBarrel( entity node )
+{
+ return CreatePropPhysics( node.GetModelName(), node.GetOrigin(), node.GetAngles() )
+}
+
+void function MarvinRunToAnimStart( entity marvin, string anim, entity jobNode )
+{
+ if ( jobNode.HasKey( "teleport" ) && jobNode.kv.teleport == "1" )
+ wait 0.1
+ else
+ RunToAnimStartPos( marvin, anim, jobNode )
+}
+
+void function MarvinFlyToAnimStart( entity marvin, string anim, entity jobNode )
+{
+ if ( jobNode.HasKey( "teleport" ) && jobNode.kv.teleport == "1" )
+ {
+ wait 0.1
+ return
+ }
+
+ AnimRefPoint animStartInfo = marvin.Anim_GetStartForRefPoint( anim, jobNode.GetOrigin(), jobNode.GetAngles() )
+
+ marvin.AssaultPoint( animStartInfo.origin )
+ marvin.AssaultSetAngles( animStartInfo.angles, true )
+ marvin.AssaultSetArrivalTolerance( 16 )
+ marvin.WaitSignal( "OnFinishedAssault" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvins.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvins.gnut
new file mode 100644
index 00000000..fc8b7d1e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvins.gnut
@@ -0,0 +1,141 @@
+untyped
+
+global function AiMarvins_Init
+
+
+function AiMarvins_Init()
+{
+ FlagInit( "Disable_Marvins" )
+ FlagSet( "Disable_Marvins" )
+
+ level.livingMarvins <- {}
+ AddSpawnCallback( "npc_marvin", LivingMarvinSpawned )
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+}
+
+void function EntitiesDidLoad()
+{
+ if ( IsAutoPopulateEnabled() == false )
+ return
+
+ FlagEnd( "disable_npcs" )
+
+ array<entity> marvin_spawners = GetEntArrayByClass_Expensive( "info_spawnpoint_marvin" )
+
+ if ( !marvin_spawners.len() )
+ return
+
+ for ( ;; )
+ {
+ wait 3
+
+ if ( !Flag( "Disable_Marvins" ) )
+ {
+ if ( TotalLivingMarvins() < 5 )
+ {
+ SpawnRandomMarvin( marvin_spawners )
+ }
+ }
+ }
+}
+
+void function LivingMarvinSpawned( entity self )
+{
+ level.livingMarvins[ self ] <- self
+}
+
+function TotalLivingMarvins()
+{
+ local count = 0
+ foreach ( entity marvin in clone level.livingMarvins )
+ {
+ if ( IsAlive( marvin ) )
+ {
+ count++
+ continue
+ }
+
+ // cleanup dead marvins
+ delete level.livingMarvins[ marvin ]
+ }
+ return count
+}
+
+entity function SpawnRandomMarvin( array<entity> marvin_spawners )
+{
+ marvin_spawners.randomize()
+ entity spawnpoint = marvin_spawners[0] // if no valid spawn is found use this one
+ for ( int i = 0; i < marvin_spawners.len(); i++ )
+ {
+ if ( IsMarvinSpawnpointValid( marvin_spawners[ i ] ) )
+ {
+ spawnpoint = marvin_spawners[ i ]
+ break
+ }
+ }
+
+ entity marvin = SpawnAmbientMarvin( spawnpoint )
+ return marvin
+}
+
+bool function IsMarvinSpawnpointValid( entity spawnpoint )
+{
+ // ensure spawnpoint is not occupied (i.e. would spawn inside another player or object )
+ if ( spawnpoint.IsOccupied() )
+ return false
+
+ bool visible = spawnpoint.IsVisibleToEnemies( TEAM_IMC ) || spawnpoint.IsVisibleToEnemies( TEAM_MILITIA )
+ if ( visible )
+ return false
+
+ return true
+}
+
+entity function SpawnAmbientMarvin( entity spawnpoint )
+{
+ entity npc_marvin = CreateEntity( "npc_marvin" )
+ SetTargetName( npc_marvin, UniqueString( "mp_random_marvin") )
+ npc_marvin.SetOrigin( spawnpoint.GetOrigin() )
+ npc_marvin.SetAngles( spawnpoint.GetAngles() )
+ //npc_marvin.kv.rendercolor = "255 255 255"
+ npc_marvin.kv.health = -1
+ npc_marvin.kv.max_health = -1
+ npc_marvin.kv.spawnflags = 516 // Fall to ground, Fade Corpse
+ //npc_marvin.kv.FieldOfView = 0.5
+ //npc_marvin.kv.FieldOfViewAlert = 0.2
+ npc_marvin.kv.AccuracyMultiplier = 1.0
+ npc_marvin.kv.physdamagescale = 1.0
+ npc_marvin.kv.WeaponProficiency = eWeaponProficiency.GOOD
+
+ Marvin_SetModels( npc_marvin, spawnpoint )
+
+ DispatchSpawn( npc_marvin )
+
+ SetTeam( npc_marvin, TEAM_UNASSIGNED )
+
+ return npc_marvin
+}
+
+function Marvin_SetModels( entity npc_marvin, entity spawnpoint )
+{
+ //default
+ npc_marvin.s.bodytype <- MARVIN_TYPE_WORKER
+
+ // set body and head based on KVP
+ if ( spawnpoint.HasKey( "bodytype" ) )
+ {
+ local bodytype = spawnpoint.GetValueForKey( "bodytype" ).tointeger()
+
+ Assert( bodytype >= MARVIN_TYPE_SHOOTER && bodytype <= MARVIN_TYPE_FIREFIGHTER, "Specified invalid body type index " + bodytype + " for info_spawnpoint_marvin " + spawnpoint + ", Use values from 0-2 instead." )
+
+ npc_marvin.s.bodytype = bodytype
+ }
+
+
+ if ( spawnpoint.HasKey( "headtype" ) )
+ {
+ local headtype = spawnpoint.GetValueForKey( "headtype" )
+ npc_marvin.kv.body = headtype
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_spectres.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_spectres.gnut
new file mode 100644
index 00000000..4aa3ac30
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_spectres.gnut
@@ -0,0 +1,7 @@
+global function MortarSpectreGetSquadFiringPositions
+
+array<vector> function MortarSpectreGetSquadFiringPositions(vector origin, vector testTarget)
+{
+ array< vector > ret
+ return ret
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_titans.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_titans.gnut
new file mode 100644
index 00000000..08598808
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_titans.gnut
@@ -0,0 +1,395 @@
+untyped
+
+global function MortarTitanThink
+global function MortarTitans_Init
+
+global function MortarTitanDeathCleanup
+global function MortarMissileFiredCallback
+global function MoveToMortarPosition
+
+global function MortarTitanKneelToAttack
+
+global function MortarTitanAttack
+
+global function MortarTitanStopAttack
+
+//global function MortarAIWaitToEngage
+
+const float MORTAR_TITAN_ABORT_ATTACK_HEALTH_FRAC = 0.90 // will stop mortar attack if he's health gets below 90% of his current health.
+const float MORTAR_TITAN_POSITION_SEARCH_RANGE = 1024 //3072 // How far away from his spawn point a mortar titan will look for positions to mortar from.
+const float MORTAR_TITAN_ENGAGE_DELAY = 3.0 // How long before a mortar titan start to attack the generator if he's taken damage getting to his mortar position.
+const float MORTAR_TITAN_REENGAGE_DELAY = 7.0 // How long before a mortar titan goes back to attacking the generator after breaking of an attack.
+
+// --------------------------------------------------------------------
+// MORTAR TITAN LOGIC
+// --------------------------------------------------------------------
+
+function MortarTitans_Init()
+{
+ RegisterSignal( "InterruptMortarAttack" )
+ RegisterSignal( "BeginMortarAttack" )
+}
+
+void function MortarTitanDeathCleanup( entity titan )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( titan )
+ {
+ entity animEnt = titan.ai.carryBarrel
+
+ if ( IsValid( animEnt ) )
+ animEnt.Destroy()
+
+ if ( IsAlive( titan ) )
+ {
+ titan.Signal( "InterruptMortarAttack" )
+ titan.Anim_Stop()
+ }
+ }
+ )
+
+ WaitForever()
+}
+
+void function MortarMissileFiredCallback( entity missile, entity weaponOwner )
+{
+ thread MortarMissileThink( missile, weaponOwner )
+}
+
+void function MortarMissileThink( entity missile, entity weaponOwner )
+{
+ Assert( IsValid( missile ) )
+
+ missile.EndSignal( "OnDestroy" )
+ missile.EndSignal( "OnDeath" )
+
+ if ( !IsValid( weaponOwner.ai.mortarTarget ) )
+ return
+
+ entity targetEnt = weaponOwner.ai.mortarTarget
+
+ missile.DamageAliveOnly( true )
+ missile.kv.lifetime = 6.0
+ missile.s.mortar <- true
+ vector startPos = missile.GetOrigin()
+
+ // made a hacky way to get the mortar arc to go higher and still have it hit it's target.
+
+ float dist = Distance( startPos, targetEnt.GetOrigin() )
+
+ // radius tightens over time
+ float radius = GraphCapped( Time() - weaponOwner.ai.spawnTime, 60.0, 180.0, 220, 100 )
+ missile.SetMissileTarget( targetEnt, < RandomFloatRange( -radius, radius ), RandomFloatRange( -radius, radius ), 0 > )
+
+ string sound = "weapon_spectremortar_projectile"
+ if ( weaponOwner.IsTitan() )
+ sound = "Weapon_FlightCore_Incoming_Projectile"
+
+ EmitSoundAtPosition( weaponOwner.GetTeam(), targetEnt.GetOrigin(), sound )
+
+ float homingSpeedMin = 10.0
+ float homingSpeedMax = Graph( dist, 2500, 7000, 400, 200 )
+ float estTravelTime = GraphCapped( dist, 0, 7000, 0, 5 )
+
+ float startTime = Time()
+ while( true )
+ {
+ float frac = min( 1, pow( ( Time() - startTime ) / estTravelTime, 2.0 ) )
+
+ if ( frac > 1.0 )
+ break
+
+ float homingSpeed = GraphCapped( frac, 0, 1, homingSpeedMin, homingSpeedMax )
+
+ missile.SetHomingSpeeds( homingSpeed, 0 )
+
+ wait 0.25
+ }
+
+ missile.ClearMissileTargetPosition()
+}
+
+void function MoveToMortarPosition( entity titan, vector origin, entity target )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ titan.SetLookDistOverride( 320 )
+ titan.SetHearingSensitivity( 0 )
+ titan.EnableNPCMoveFlag( NPCMF_PREFER_SPRINT )
+
+ local animEnt = titan.ai.carryBarrel
+
+ local dir = target.GetOrigin() - origin
+ local dist = dir.Norm()
+ local angles = VectorToAngles( dir )
+ angles.x = 0
+ angles.z = 0
+
+ float frac = TraceLineSimple( origin + < 0, 0, 32 >, origin + < 0, 0, -32 >, titan )
+ if ( frac > 0 && frac < 1 )
+ origin = origin + < 0, 0, 32 > - < 0, 0, 64 * frac >
+
+ animEnt.SetOrigin( origin )
+ animEnt.SetAngles( angles )
+
+ float goalRadius = titan.GetMinGoalRadius()
+
+ OnThreadEnd(
+ function() : ( titan )
+ {
+ if ( !IsValid( titan ) )
+ return
+
+ local classname = titan.GetClassName()
+ titan.DisableLookDistOverride()
+ titan.SetHearingSensitivity( 1 )
+ titan.DisableNPCMoveFlag( NPCMF_PREFER_SPRINT )
+ }
+ )
+
+ local tries = 0
+ while( true )
+ {
+ local dist = Distance( titan.GetOrigin(), origin )
+ if ( dist <= goalRadius * 2 )
+ break
+
+ printt( "Mortar titan moving toward his goal", dist, tries++ )
+ titan.AssaultPoint( origin )
+ titan.AssaultSetGoalRadius( goalRadius )
+
+ local result = WaitSignal( titan, "OnFinishedAssault", "OnEnterGoalRadius" )
+ }
+}
+
+void function MortarTitanKneelToAttack( entity titan )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ entity animEnt = titan.ai.carryBarrel
+ waitthread PlayAnim( titan, "at_mortar_stand2knee", animEnt )
+}
+
+function MortarTitanAttack( entity titan, entity target )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "InterruptMortarAttack" )
+
+ OnThreadEnd(
+ function() : ( titan )
+ {
+ if ( !IsValid( titan ) )
+ return
+
+ if ( "selectedPosition" in titan.s )
+ {
+ titan.s.selectedPosition.inUse = false
+ delete titan.s.selectedPosition
+ }
+
+ if ( IsAlive( titan ) )
+ thread MortarTitanAttackEnd( titan )
+ }
+ )
+
+ titan.ai.mortarTarget = target
+ entity animEnt = titan.ai.carryBarrel
+
+ entity weapon = titan.GetActiveWeapon()
+
+ while ( weapon.IsWeaponOffhand() )
+ {
+ WaitFrame()
+ weapon = titan.GetActiveWeapon()
+ }
+
+ weapon.SetMods( [ "coop_mortar_titan" ] )
+
+ while( true )
+ {
+ waitthread PlayAnim( titan, "at_mortar_knee", animEnt )
+ }
+}
+
+function MortarTitanAttackEnd( entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ entity animEnt = titan.ai.carryBarrel
+
+ // remove the mortar mod, we do this so that we don't get mortar sound and fx when firing normal
+ entity weapon = titan.GetActiveWeapon()
+
+ while ( weapon.IsWeaponOffhand() )
+ {
+ WaitFrame()
+ weapon = titan.GetActiveWeapon()
+ }
+
+ weapon.SetMods( [] )
+
+ WaitEndFrame() // if I didn't add this PlayAnim, below, would return immediately for some unknown reason.
+
+ if ( IsValid( animEnt ) && IsAlive( titan ) )
+ waitthread PlayAnim( titan, "at_mortar_knee2stand", animEnt )
+}
+
+function MortarTitanStopAttack( titan )
+{
+ titan.Signal( "InterruptMortarAttack" )
+}
+
+function MortarTitanStopAttack_Internal( titan )
+{
+ titan.Signal( "InterruptMortarAttack" )
+ titan.Anim_Stop()
+}
+
+void function MortarAIWaitToEngage( entity titan, float timeFrame, int minDamage = 75 )
+{
+ entity soul = titan.GetTitanSoul()
+ float endtime = Time() + timeFrame
+ int lastHealth = titan.GetHealth() + soul.GetShieldHealth()
+ float tickTime = 1.0
+
+ while ( Time() < endtime )
+ {
+ wait tickTime
+
+ int currentHealth = titan.GetHealth() + soul.GetShieldHealth()
+ if ( lastHealth > ( currentHealth + minDamage ) ) // add minDamage so that we ignore low amounts of damage.
+ {
+ lastHealth = currentHealth
+ endtime = Time() + timeFrame
+ }
+ }
+}
+
+
+/*******************************************************************\
+ MORTAR TITANS
+\*******************************************************************/
+//Function assumes that given Titan is spawned as npc_titan_atlas_tracker_mortar. Changing the Titan's AISettings post-spawn
+//disrupts the Titan's titanfall animations and can result in the Titan landing outside the level.
+void function MortarTitanThink( entity titan, entity generator )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ entity soul = titan.GetTitanSoul()
+ soul.EndSignal( "OnDestroy" )
+
+ titan.ai.carryBarrel = CreateScriptRef()
+ titan.TakeWeaponNow( titan.GetActiveWeapon().GetWeaponClassName() )
+ titan.GiveWeapon( "mp_titanweapon_rocketeer_rocketstream" )
+ titan.SetActiveWeaponByName( "mp_titanweapon_rocketeer_rocketstream" )
+ titan.SetScriptName( "mortar_titan" )
+
+ entity weapon = titan.GetActiveWeapon()
+ weapon.w.missileFiredCallback = MortarMissileFiredCallback
+ thread MortarTitanDeathCleanup( titan )
+
+ WaitTillHotDropComplete( titan )
+
+ float minEngagementDuration = 5
+ StationaryAIPosition ornull mortarPosition = GetRandomStationaryPosition( titan.GetOrigin(), MORTAR_TITAN_POSITION_SEARCH_RANGE, eStationaryAIPositionTypes.MORTAR_TITAN )
+ while ( mortarPosition == null )
+ {
+ // incase all stationary titan positions are in use wait for one to become available
+ wait 5
+ mortarPosition = GetRandomStationaryPosition( titan.GetOrigin(), MORTAR_TITAN_POSITION_SEARCH_RANGE, eStationaryAIPositionTypes.MORTAR_TITAN )
+ }
+
+ expect StationaryAIPosition( mortarPosition )
+
+ ClaimStationaryAIPosition( mortarPosition )
+
+ OnThreadEnd(
+ function() : ( mortarPosition )
+ {
+ // release mortar position when dead
+ ReleaseStationaryAIPosition( mortarPosition )
+ }
+ )
+
+ float minDamage = 75 // so that the titan doesn't care about small amounts of damage.
+
+ while( true )
+ {
+ vector origin = mortarPosition.origin
+
+ float startHealth = float( titan.GetHealth() + soul.GetShieldHealth() )
+ waitthread MoveToMortarPosition( titan, origin, generator )
+
+ if ( startHealth > ( ( titan.GetHealth() + soul.GetShieldHealth() ) + minDamage ) || !titan.IsInterruptable() )
+ {
+ // we took damage getting to the mortar location lets wait until we stop taking damage
+ waitthread MortarAIWaitToEngage( titan, MORTAR_TITAN_ENGAGE_DELAY )
+ continue
+ }
+
+ waitthread MortarTitanKneelToAttack( titan )
+ thread MortarTitanAttack( titan, generator )
+
+ wait minEngagementDuration // aways mortar the target for a while before potentially breaking out
+
+ // wait for interruption
+ waitthread WaitForInteruption( titan )
+
+ MortarTitanStopAttack_Internal( titan )
+
+ // lets wait until we stop taking damage before going back to attacking the generator
+ waitthread MortarAIWaitToEngage( titan, MORTAR_TITAN_REENGAGE_DELAY )
+ }
+}
+
+void function WaitForInteruption( entity titan )
+{
+ Assert( IsNewThread(), "Must be threaded off" )
+
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "InterruptMortarAttack" )
+
+ entity soul = titan.GetTitanSoul()
+ soul.EndSignal( "OnDestroy" )
+
+ float playerProximityDistSqr = pow( 256, 2 )
+ float healthBreakOff = ( titan.GetHealth() + soul.GetShieldHealth() ) * MORTAR_TITAN_ABORT_ATTACK_HEALTH_FRAC
+
+ while( true )
+ {
+ if ( IsEnemyWithinDist( titan, playerProximityDistSqr ) )
+ break
+ if ( ( titan.GetHealth() + soul.GetShieldHealth() ) < healthBreakOff )
+ break
+ wait 1
+ }
+}
+
+bool function IsEnemyWithinDist( entity titan, float dist )
+{
+ vector origin = titan.GetOrigin()
+ array<entity> players = GetPlayerArrayOfEnemies_Alive( titan.GetTeam() )
+
+ foreach( player in players )
+ {
+ if ( DistanceSqr( player.GetOrigin(), origin ) < dist )
+ return true
+ }
+
+ return false
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_nuke_titans.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_nuke_titans.gnut
new file mode 100644
index 00000000..0d4b43c9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_nuke_titans.gnut
@@ -0,0 +1,129 @@
+untyped
+
+global function NukeTitanThink
+
+global function AutoTitan_SelfDestruct
+
+const NUKE_TITAN_PLAYER_DETECT_RANGE = 500
+const NUKE_TITAN_RANGE_CHECK_SLEEP_SECS = 1.0
+
+void function AutoTitan_SelfDestruct( entity titan )
+{
+ if ( titan.ContextAction_IsBusy() )
+ titan.ContextAction_ClearBusy()
+
+ thread TitanEjectPlayer( titan )
+}
+
+void function NukeTitanThink( entity titan, entity generator )
+{
+ //Function assumes that given Titan is spawned as npc_titan_ogre_meteor_nuke. Changing the Titan's AISettings post-spawn
+ //disrupts the Titan's titanfall animations and can result in the Titan landing outside the level.
+ NPC_SetNuclearPayload( titan )
+ AddEntityCallback_OnPostDamaged( titan, AutoTitan_NuclearPayload_PostDamageCallback )
+
+ WaitTillHotDropComplete( titan )
+
+ thread NukeTitanSeekOutGenerator( titan, generator )
+}
+
+
+void function NukeTitanSeekOutGenerator( entity titan, entity generator )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "Doomed" )
+
+ WaitSignal( titan, "FD_ReachedHarvester", "OnFailedToPath" )
+
+ float goalRadius = 100
+ float checkRadiusSqr = 400 * 400
+
+ //array<vector> pos = NavMesh_RandomPositions( generator.GetOrigin(), HULL_TITAN, 5, 250, 350 )
+ array<vector> pos = NavMesh_GetNeighborPositions( generator.GetOrigin(), HULL_TITAN, 5 )
+ pos = ArrayClosestVector( pos, titan.GetOrigin() )
+
+ array<vector> validPos
+ foreach ( point in pos )
+ {
+ if ( DistanceSqr( generator.GetOrigin(), point ) <= checkRadiusSqr && NavMesh_IsPosReachableForAI( titan, point ) )
+ {
+ validPos.append( point )
+ //DebugDrawSphere( point, 32, 255, 0, 0, true, 60 )
+ }
+ }
+
+ int posLen = validPos.len()
+ while( posLen >= 1 )
+ {
+ titan.SetEnemy( generator )
+ thread AssaultOrigin( titan, validPos[0], goalRadius )
+ titan.AssaultSetFightRadius( goalRadius )
+
+ wait 0.5
+
+ if ( DistanceSqr( titan.GetOrigin(), generator.GetOrigin() ) > checkRadiusSqr )
+ continue
+
+ break
+ }
+
+ thread AutoTitan_SelfDestruct( titan )
+}
+
+// intercept damage to nuke titans in damage callback so we can nuke them before death 100% of the time
+void function AutoTitan_NuclearPayload_PostDamageCallback( entity titan, var damageInfo )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ entity titanOwner = titan.GetBossPlayer()
+ if ( IsValid( titanOwner ) )
+ {
+ Assert( titanOwner.IsPlayer() )
+ Assert( GetPlayerTitanInMap( titanOwner ) == titan )
+ return
+ }
+
+ int nuclearPayload = NPC_GetNuclearPayload( titan )
+ if ( nuclearPayload == 0 )
+ return
+
+ if ( !GetDoomedState( titan ) )
+ return
+
+ if ( titan.GetTitanSoul().IsEjecting() )
+ return
+
+ // Nuke eject as soon as the titan enters doom state.
+ if ( !( "doomedStateNukeTriggerHealth" in titan.s ) )
+ {
+ titan.s.doomedStateNukeTriggerHealth <- titan.GetMaxHealth()
+ }
+
+ if ( titan.GetHealth() > titan.s.doomedStateNukeTriggerHealth )
+ {
+ //printt( "titan health:", titan.GetHealth(), "health to nuke:", titan.s.doomedStateNukeTriggerHealth )
+ return
+ }
+
+ printt( "NUKE TITAN DOOMED TRIGGER HEALTH REACHED, NUKING! Health:", titan.s.doomedStateNukeTriggerHealth )
+
+ thread AutoTitan_SelfDestruct( titan )
+}
+
+function AutoTitan_CanDoRangeCheck( autoTitan )
+{
+ if ( !( "nextPlayerTitanRangeCheckTime" in autoTitan.s ) )
+ autoTitan.s.nextPlayerTitanRangeCheckTime <- -1
+
+ if ( Time() < autoTitan.s.nextPlayerTitanRangeCheckTime )
+ {
+ return false
+ }
+ else
+ {
+ autoTitan.s.nextPlayerTitanRangeCheckTime = Time() + NUKE_TITAN_RANGE_CHECK_SLEEP_SECS
+ return true
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_personal_shield.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_personal_shield.gnut
new file mode 100644
index 00000000..f1fbdb80
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_personal_shield.gnut
@@ -0,0 +1,371 @@
+global function AiPersonalShield
+global function ActivatePersonalShield
+const FX_DRONE_SHIELD_WALL_HUMAN = $"P_drone_shield_wall_sm"
+const SHIELD_BREAK_FX = $"P_xo_armor_break_CP"
+const SHIELD_HEALTH = 620
+global const AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE = true
+const float PERSONAL_SHIELD_HEALTH_FRAC_DAMAGED = 0.5 // below what frac of total health will the personal shield owner want to chatter about shield damage?
+
+struct
+{
+ table<entity, entity> npcVortexSpheres
+} file
+
+
+void function AiPersonalShield()
+{
+ PrecacheParticleSystem( FX_DRONE_SHIELD_WALL_HUMAN )
+ PrecacheParticleSystem( SHIELD_BREAK_FX )
+ AddSyncedMeleeServerCallback( GetSyncedMeleeChooser( "human", "human" ), DisableShieldOnExecution )
+}
+
+void function DisableShieldOnExecution( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity target )
+{
+ if ( !( target in file.npcVortexSpheres ) )
+ return
+
+ entity vortex = file.npcVortexSpheres[ target ]
+ vortex.Destroy()
+}
+
+void function ActivatePersonalShield( entity owner )
+{
+ owner.EndSignal( "OnDeath" )
+ for ( ;; )
+ {
+ waitthread ActivatePersonalShield_Recreate( owner )
+
+ // got stunned? make new shield after awhile
+ wait 15
+ }
+}
+
+void function ShieldProtectsOwnerFromMelee( entity ent, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsAlive( attacker ) )
+ return
+ if ( !attacker.IsPlayer() )
+ return
+ if ( !IsPilot( attacker ) )
+ return
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ if ( !IsValid( weapon ) )
+ weapon = attacker.GetActiveWeapon()
+ if ( !IsValid( weapon ) )
+ return
+ var weaponType = weapon.GetWeaponInfoFileKeyField( "weaponType" )
+ if ( weaponType != "melee" )
+ return
+
+ Assert( ent in file.npcVortexSpheres )
+ entity vortexSphere = file.npcVortexSpheres[ ent ]
+
+ float radius = float( vortexSphere.kv.radius )
+ float height = float( vortexSphere.kv.height )
+ float bullet_fov = float( vortexSphere.kv.bullet_fov )
+ float dot = cos( bullet_fov * 0.5 )
+
+ vector origin = vortexSphere.GetOrigin()
+ vector angles = vortexSphere.GetAngles()
+ vector forward = AnglesToForward( angles )
+ int team = vortexSphere.GetTeam()
+
+ if ( ProtectedFromShield( attacker, origin, height, radius, bullet_fov, dot, forward ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ StunPushBack( attacker, forward )
+ }
+}
+
+entity function ActivatePersonalShield_Recreate( entity owner )
+{
+ if ( !AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ AddEntityCallback_OnDamaged( owner, ShieldProtectsOwnerFromMelee )
+ //------------------------------
+ // Shield vars
+ //------------------------------
+ vector origin = owner.GetOrigin()
+ vector angles = owner.GetAngles() + Vector( 0, 0, 180 )
+
+ float shieldWallRadius = 45 // 90
+ asset shieldFx = FX_DRONE_SHIELD_WALL_HUMAN
+ float wallFOV = DRONE_SHIELD_WALL_FOV_HUMAN
+ float shieldWallHeight = 102
+
+ //------------------------------
+ // Vortex to block the actual bullets
+ //------------------------------
+ entity vortexSphere = CreateEntity( "vortex_sphere" )
+
+ Assert( !( owner in file.npcVortexSpheres ), owner + " already has a shield" )
+ file.npcVortexSpheres[ owner ] <- vortexSphere
+ vortexSphere.kv.spawnflags = SF_ABSORB_BULLETS | SF_BLOCK_OWNER_WEAPON | SF_BLOCK_NPC_WEAPON_LOF | SF_ABSORB_CYLINDER
+ vortexSphere.kv.enabled = 0
+ vortexSphere.kv.radius = shieldWallRadius
+ vortexSphere.kv.height = shieldWallHeight
+ vortexSphere.kv.bullet_fov = wallFOV
+ vortexSphere.kv.physics_pull_strength = 25
+ vortexSphere.kv.physics_side_dampening = 6
+ vortexSphere.kv.physics_fov = 360
+ vortexSphere.kv.physics_max_mass = 2
+ vortexSphere.kv.physics_max_size = 6
+
+ StatusEffect_AddEndless( vortexSphere, eStatusEffect.destroyed_by_emp, 1.0 )
+
+ vortexSphere.SetAngles( angles ) // viewvec?
+ vortexSphere.SetOrigin( origin + Vector( 0, 0, shieldWallRadius - 64 ) )
+ vortexSphere.SetMaxHealth( SHIELD_HEALTH )
+ vortexSphere.SetHealth( SHIELD_HEALTH )
+ SetTeam( vortexSphere, owner.GetTeam() )
+
+ thread PROTO_VortexSlowsPlayers_PersonalShield( owner, vortexSphere )
+
+ DispatchSpawn( vortexSphere )
+
+ EntFireByHandle( vortexSphere, "Enable", "", 0, null, null )
+
+ vortexSphere.SetTakeDamageType( DAMAGE_YES )
+ vortexSphere.ClearInvulnerable() // make particle wall invulnerable to weapon damage. It will still drain over time
+
+ //------------------------------------------
+ // Shield wall fx for visuals/health drain
+ //------------------------------------------
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) )
+ DispatchSpawn( cpoint )
+
+ entity mover = CreateScriptMover()
+ mover.SetOrigin( owner.GetOrigin() )
+ vector moverAngles = owner.GetAngles()
+ mover.SetAngles( AnglesCompose( moverAngles, <0,0,180> ) )
+
+ int fxid = GetParticleSystemIndex( FX_DRONE_SHIELD_WALL_HUMAN )
+ entity shieldWallFX = StartParticleEffectOnEntity_ReturnEntity( mover, fxid, FX_PATTACH_ABSORIGIN_FOLLOW, 0 )
+ shieldWallFX.DisableHibernation()
+ EffectSetControlPointEntity( shieldWallFX, 0, mover )
+
+ //thread DrawArrowOnTag( mover )
+ vortexSphere.e.shieldWallFX = shieldWallFX
+ vector color = GetShieldTriLerpColor( 0.0 )
+
+ cpoint.SetOrigin( color )
+ EffectSetControlPointEntity( shieldWallFX, 1, cpoint )
+ SetVortexSphereShieldWallCPoint( vortexSphere, cpoint )
+
+ #if GRUNTCHATTER_ENABLED
+ // have to do this, vortex shield isn't an entity that works with AddEntityCallback_OnDamaged
+ thread PersonalShieldOwner_ReactsToDamage( owner, vortexSphere )
+ #endif
+
+ //-----------------------
+ // Attach shield to owner
+ //------------------------
+ vortexSphere.SetParent( mover )
+
+ vortexSphere.EndSignal( "OnDestroy" )
+ Assert( IsAlive( owner ) )
+ owner.EndSignal( "OnDeath" )
+ owner.EndSignal( "ArcStunned" )
+ mover.EndSignal( "OnDestroy" )
+ #if MP
+ shieldWallFX.EndSignal( "OnDestroy" )
+ #endif
+
+ OnThreadEnd(
+ function() : ( owner, mover, vortexSphere )
+ {
+ delete file.npcVortexSpheres[ owner ]
+ if ( IsValid( owner ) )
+ {
+ owner.kv.defenseActive = false
+ if ( !AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ RemoveEntityCallback_OnDamaged( owner, ShieldProtectsOwnerFromMelee )
+ }
+
+ StopShieldWallFX( vortexSphere )
+
+ if ( IsValid( vortexSphere ) )
+ vortexSphere.Destroy()
+
+ if ( IsValid( mover ) )
+ {
+ //PlayFX( SHIELD_BREAK_FX, mover.GetOrigin(), mover.GetAngles() )
+ mover.Destroy()
+ }
+ }
+ )
+
+ owner.kv.defenseActive = true
+
+ for ( ;; )
+ {
+ Assert( IsAlive( owner ) )
+ UpdateShieldPosition( mover, owner )
+
+ #if MP
+ if ( IsCloaked( owner ) )
+ EntFireByHandle( shieldWallFX, "Stop", "", 0, null, null )
+ else
+ EntFireByHandle( shieldWallFX, "Start", "", 0, null, null )
+ #endif
+ }
+}
+
+#if GRUNTCHATTER_ENABLED
+void function PersonalShieldOwner_ReactsToDamage( entity owner, entity vortexSphere )
+{
+ EndSignal( owner, "OnDeath" )
+ EndSignal( vortexSphere, "OnDestroy" )
+
+ float alertHealth = vortexSphere.GetMaxHealth() * PERSONAL_SHIELD_HEALTH_FRAC_DAMAGED
+
+ while ( vortexSphere.GetHealth() >= alertHealth )
+ wait 0.25
+
+ GruntChatter_TryPersonalShieldDamaged( owner ) //Commenting out to unblock tree. See bug 186062
+}
+#endif
+
+float function GetYawForEnemyOrLKP( entity owner )
+{
+ entity enemy = owner.GetEnemy()
+ if ( !IsValid( enemy ) )
+ return owner.GetAngles().y
+
+ vector ornull lkp = owner.LastKnownPosition( enemy )
+ if ( lkp == null )
+ return owner.GetAngles().y
+
+ expect vector( lkp )
+ vector dif = lkp - owner.GetOrigin()
+ return VectorToAngles( dif ).y
+}
+
+void function UpdateShieldPosition( entity mover, entity owner )
+{
+ mover.NonPhysicsMoveTo( owner.GetOrigin(), 0.1, 0.0, 0.0 )
+ vector angles = owner.EyeAngles()
+ float yaw = angles.y
+ yaw %= 360
+ mover.NonPhysicsRotateTo( <0,yaw,180>, 1.35, 0, 0 )
+
+// float yaw = GetYawForEnemyOrLKP( owner )
+// float boost = sin( Time() * 1.5 ) * 65
+// yaw += boost
+// yaw %= 360
+// mover.NonPhysicsRotateTo( <0,yaw,0>, 0.95, 0, 0 )
+
+ WaitFrame()
+}
+
+void function PROTO_VortexSlowsPlayers_PersonalShield( entity owner, entity vortexSphere )
+{
+ owner.EndSignal( "OnDeath" )
+ vortexSphere.EndSignal( "OnDestroy" )
+
+ float radius = float(vortexSphere.kv.radius )
+ float height = float(vortexSphere.kv.height )
+ float bullet_fov = float( vortexSphere.kv.bullet_fov )
+ float dot = cos( bullet_fov * 0.5 )
+
+ for ( ;; )
+ {
+ vector origin = vortexSphere.GetOrigin()
+ vector angles = vortexSphere.GetAngles()
+ vector forward = AnglesToForward( angles )
+ int team = vortexSphere.GetTeam()
+
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( !IsAlive( player ) )
+ continue
+ if ( player.GetTeam() == team )
+ continue
+ if ( VortexStunCheck_PersonalShield( player, origin, height, radius, bullet_fov, dot, forward ) )
+ {
+ player.p.lastDroneShieldStunPushTime = Time()
+
+ if ( AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ {
+ Explosion_DamageDefSimple( damagedef_shield_captain_arc_shield, player.GetOrigin(),owner, owner, player.GetOrigin() )
+ }
+ }
+ }
+ WaitFrame()
+ }
+}
+
+bool function ProtectedFromShield( entity player, vector origin, float height, float radius, float bullet_fov, float dotLimit, vector forward )
+{
+ vector playerOrg = player.GetOrigin()
+ vector dif = Normalize( playerOrg - origin )
+
+ float dot = DotProduct2D( dif, forward )
+ return dot >= dotLimit
+}
+
+bool function VortexStunCheck_PersonalShield( entity player, vector origin, float height, float radius, float bullet_fov, float dot, vector forward )
+{
+ if ( !IsPilot( player ) )
+ return false
+
+ if ( player.IsGodMode() )
+ return false
+
+ if ( AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ {
+ if ( Time() - player.p.lastDroneShieldStunPushTime < 1.00 )
+ return false
+ }
+ else
+ {
+ if ( Time() - player.p.lastDroneShieldStunPushTime < 1.75 )
+ return false
+ }
+
+ vector playerOrg = player.GetOrigin()
+ float dist2d = Distance2D( playerOrg, origin )
+
+ if ( dist2d > radius + 5 )
+ return false
+ if ( dist2d < radius - 15 )
+ return false
+
+ float heightOffset = fabs( playerOrg.z - origin.z )
+
+ if ( heightOffset < 0 || heightOffset > height )
+ return false
+
+ if ( !ProtectedFromShield( player, origin, height, radius, bullet_fov, dot, forward ) )
+ return false
+
+ if ( AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ {
+ const float VORTEX_STUN_DURATION = 1.0
+ GiveEMPStunStatusEffects( player, VORTEX_STUN_DURATION + 0.5 )
+ float strength = 0.4
+ StatusEffect_AddTimed( player, eStatusEffect.emp, strength, VORTEX_STUN_DURATION, 0.5 )
+ EmitSoundOnEntityOnlyToPlayer( player, player, "flesh_electrical_damage_1p" )
+ }
+ else
+ {
+ StunPushBack( player, forward )
+ }
+
+ return true
+}
+
+void function StunPushBack( entity player, vector forward )
+{
+ const float VORTEX_STUN_DURATION = 1.0
+ GiveEMPStunStatusEffects( player, VORTEX_STUN_DURATION + 0.5 )
+ float strength = 0.4
+ StatusEffect_AddTimed( player, eStatusEffect.emp, strength, VORTEX_STUN_DURATION, 0.5 )
+ thread TempLossOfAirControl( player, VORTEX_STUN_DURATION )
+ vector velocity = forward * 300
+ velocity.z = 400
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "flesh_electrical_damage_1p" )
+ player.SetVelocity( velocity )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_pilots.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_pilots.gnut
new file mode 100644
index 00000000..3c2e36ce
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_pilots.gnut
@@ -0,0 +1,808 @@
+untyped
+
+global const NPC_TITAN_PILOT_PROTOTYPE = 0
+global function AiPilots_Init
+
+global function CaptainThink
+
+
+#if NPC_TITAN_PILOT_PROTOTYPE
+global function NpcPilotCallTitanThink
+global function NpcPilotStopCallTitanThink
+global function NpcPilotCallsInAndEmbarksTitan
+global function NpcPilotRunsToAndEmbarksFallingTitan
+global function NpcPilotCallsInTitan
+global function NpcPilotRunsToEmbarkTitan
+global function NpcPilotEmbarksTitan
+global function NpcPilotDisembarksTitan
+global function NpcPilotBecomesTitan
+global function NpcTitanBecomesPilot
+global function TitanHasNpcPilot
+global function NpcPilotGetPetTitan
+global function NpcPilotSetPetTitan
+#endif
+
+global function NpcSetNextTitanRespawnAvailable
+global function NpcResetNextTitanRespawnAvailable
+
+global function AddCallback_OnNpcTitanBecomesPilot
+global function AddCallback_OnNpcPilotBecomesTitan
+
+global struct NPCPilotStruct
+{
+ bool isValid = false
+
+ int team
+ int spawnflags
+ float accuracy
+ float proficieny
+ float health
+ float physDamageScale
+ string weapon
+ string squadName
+
+ asset modelAsset
+ string title
+
+ bool isInvulnerable
+}
+
+const NPC_NEXT_TITANTIME_RESET = -1
+const NPC_NEXT_TITANTIME_MIN = 45
+const NPC_NEXT_TITANTIME_MAX = 60
+const NPC_NEXT_TITANTIME_INTERUPT = 15
+
+function AiPilots_Init()
+{
+ RegisterSignal( "grenade_throw" )
+ RegisterSignal( "NpcPilotBecomesTitan" )
+ RegisterSignal( "NpcTitanBecomesPilot" )
+ RegisterSignal( "StopCallTitanThink" )
+ RegisterSignal( "NpcTitanRespawnAvailableUpdated" )
+
+ level.onNpcPilotBecomesTitanCallbacks <- []
+ level.onNpcTitanBecomesPilotCallbacks <- []
+
+}
+
+function ScriptCallback_OnNpcPilotBecomesTitan( pilot, titan )
+{
+ local result = { pilot = pilot, titan = titan }
+ Signal( pilot, "NpcPilotBecomesTitan", result )
+ Signal( titan, "NpcPilotBecomesTitan", result )
+
+ foreach ( callbackFunc in level.onNpcPilotBecomesTitanCallbacks )
+ {
+ callbackFunc( pilot, titan )
+ }
+}
+
+function ScriptCallback_OnNpcTitanBecomesPilot( pilot, titan )
+{
+ local result = { pilot = pilot, titan = titan }
+ Signal( pilot, "NpcTitanBecomesPilot", result )
+ Signal( titan, "NpcTitanBecomesPilot", result )
+
+ foreach ( callbackFunc in level.onNpcTitanBecomesPilotCallbacks )
+ {
+ callbackFunc( pilot, titan )
+ }
+}
+
+function AddCallback_OnNpcPilotBecomesTitan( callbackFunc )
+{
+ Assert( "onNpcPilotBecomesTitanCallbacks" in level )
+ AssertParameters( callbackFunc, 2, "pilotNPC, titanNPC" )
+
+ level.onNpcPilotBecomesTitanCallbacks.append( callbackFunc )
+}
+
+function AddCallback_OnNpcTitanBecomesPilot( callbackFunc )
+{
+ Assert( "onNpcTitanBecomesPilotCallbacks" in level )
+ AssertParameters( callbackFunc, 2, "pilotNPC, titanNPC" )
+
+ level.onNpcTitanBecomesPilotCallbacks.append( callbackFunc )
+}
+
+function NpcSetNextTitanRespawnAvailable( npc, time )
+{
+ Assert( "nextTitanRespawnAvailable" in npc.s )
+ npc.s.nextTitanRespawnAvailable = time
+ npc.Signal( "NpcTitanRespawnAvailableUpdated" )
+}
+
+function NpcResetNextTitanRespawnAvailable( npc )
+{
+ Assert( "nextTitanRespawnAvailable" in npc.s )
+ npc.s.nextTitanRespawnAvailable = NPC_NEXT_TITANTIME_RESET
+ npc.Signal( "NpcTitanRespawnAvailableUpdated" )
+}
+
+function NpcPilotStopCallTitanThink( pilot )
+{
+ pilot.Signal( "StopCallTitanThink" )
+}
+
+/************************************************************************************************\
+
+######## #### ## ####### ######## ######## ## ## #### ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ### ## ## ##
+## ## ## ## ## ## ## ## ## ## ## #### ## ## ##
+######## ## ## ## ## ## ## ######### ## ## ## ## #####
+## ## ## ## ## ## ## ## ## ## ## #### ## ##
+## ## ## ## ## ## ## ## ## ## ## ### ## ##
+## #### ######## ####### ## ## ## ## #### ## ## ## ##
+
+\************************************************************************************************/
+function CaptainThink( entity npc )
+{
+ npc.EndSignal( "OnDestroy" )
+ npc.EndSignal( "OnDeath" )
+
+ Assert( !( "nextTitanRespawnAvailable" in npc.s ) )
+ Assert( !( "petTitan" in npc.s ) )
+
+ npc.s.petTitan <- null
+ npc.s.nextTitanRespawnAvailable <- null
+
+ //wait for in combat...
+ WaitForNpcInCombat( npc )
+
+ //... before we call in a titan
+ if ( npc.s.nextTitanRespawnAvailable == null )
+ npc.s.nextTitanRespawnAvailable = Time() + RandomFloatRange( 2, 10 )
+
+ WaitEndFrame() //wait a frame for things like petTitan and nextTitanRespawnAvailable to have a chance to be set from custom scripts
+ #if NPC_TITAN_PILOT_PROTOTYPE
+ thread NpcPilotCallTitanThink( npc )
+ #endif
+}
+
+#if NPC_TITAN_PILOT_PROTOTYPE
+
+function NpcPilotCallTitanThink( entity pilot )
+{
+ Assert( pilot.IsNPC() )
+ Assert( IsAlive( pilot ) )
+ Assert ( !pilot.IsTitan() )
+
+ pilot.EndSignal( "OnDestroy" )
+ pilot.EndSignal( "OnDeath" )
+ pilot.Signal( "StopCallTitanThink" )
+ pilot.EndSignal( "StopCallTitanThink" )
+
+
+ string title = pilot.GetTitle() + "'s Titan"
+ local count = 1 //1 titan call in at a time
+
+ while ( true ) //this loop usually only happens once, unless the titan called in is destroyed before the living pilot can get to it
+ {
+ entity titan = NpcPilotGetPetTitan( pilot )
+ if ( !IsAlive( titan ) )
+ {
+ //wait for ready titan
+ waitthread __WaitforTitanCallinReady( pilot )
+
+ //ready to call in - look for a good spot
+ SpawnPointFP spawnPoint
+ while ( true )
+ {
+ wait ( RandomFloatRange( 1, 2 ) )
+
+ //dont do stuff when animating on a parent
+ if ( pilot.GetParent() )
+ continue
+
+ //Don't deploy if too close to an enemy
+ if ( HasEnemyWithinDist( pilot, 300.0 ) )
+ continue
+
+ // DO the opposite - only deploy if has an enemy within this distance
+ // if ( !HasEnemyWithinDist( pilot, 2000.0 ) )
+ // continue
+
+ //don't do stuff if you dont have a spawnPoint
+ spawnPoint = FindSpawnPointForNpcCallin( pilot, TITAN_MEDIUM_AJAX_MODEL, HOTDROP_TURBO_ANIM )
+ if ( !spawnPoint.valid )
+ continue
+
+ break
+ }
+
+ //call in a titan, run to it, and embark
+ //in SP by default, the friendlys do NOT do the beacon tell
+ titan = NpcPilotCallsInAndEmbarksTitan( pilot, spawnPoint.origin, spawnPoint.angles )
+ titan.SetTitle( title )
+ }
+ else
+ {
+ Assert( IsAlive( titan ) )
+
+ if ( HasEnemyRodeo( titan ) )
+ {
+ while ( HasEnemyRodeo( titan ) )
+ {
+ WaitSignal( titan.GetTitanSoul(), "RodeoRiderChanged", "OnDestroy" )
+ }
+
+ wait 4 //don't pop back in immediately
+ }
+
+ if ( !IsAlive( titan ) )
+ continue //the titan didn't make it, lets loop back up and try again
+
+ if ( titan.GetTitanSoul().IsDoomed() )
+ {
+ titan.WaitSignal( "OnDestroy" )
+ continue //the titan didn't make it, lets loop back up and try again
+ }
+
+ //start running to titan as it kneels
+ thread NpcPilotRunsToEmbarkTitan( pilot, titan )
+ thread __TitanKneelsForPilot( pilot, titan )
+ wait 2.0 //wait for titan to be in position
+
+ if ( !IsAlive( titan ) )
+ continue //the titan didn't make it, lets loop back up and try again
+
+ //run to the titan
+ waitthread NpcPilotRunsToEmbarkTitan( pilot, titan )
+
+ if ( !IsAlive( titan ) )
+ continue //the titan didn't make it, lets loop back up and try again
+
+ //embark titan
+ thread NpcPilotEmbarksTitan( pilot, titan )
+ }
+
+ local result = WaitSignal( titan, "NpcPilotBecomesTitan", "OnDeath", "OnDestroy" )
+ if ( result.signal != "NpcPilotBecomesTitan" )
+ continue //the titan didn't make it, lets loop back up and try again
+ }
+}
+
+/************************************************************************************************\
+
+ ###### ### ## ## #### ## ## ######## #### ######## ### ## ##
+## ## ## ## ## ## ## ### ## ## ## ## ## ## ### ##
+## ## ## ## ## ## #### ## ## ## ## ## ## #### ##
+## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
+## ######### ## ## ## ## #### ## ## ## ######### ## ####
+## ## ## ## ## ## ## ## ### ## ## ## ## ## ## ###
+ ###### ## ## ######## ######## #### ## ## ## #### ## ## ## ## ##
+
+\************************************************************************************************/
+
+
+entity function NpcPilotCallsInAndEmbarksTitan( entity pilot, vector origin, vector angles )
+{
+ entity titan = NpcPilotCallsInTitan( pilot, origin, angles )
+ thread NpcPilotRunsToAndEmbarksFallingTitan( pilot, titan )
+
+ return titan
+}
+
+function NpcPilotRunsToAndEmbarksFallingTitan( entity pilot, entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+
+ //wait for it to land
+ waitthread WaitTillHotDropComplete( titan )
+ ShowName( titan )
+
+ if ( !IsAlive( titan ) )
+ return
+ titan.EndSignal( "OnDeath" )
+
+ //titan is alive on land so clean it up on thread end
+ OnThreadEnd(
+ function () : ( titan )
+ {
+ if ( !IsAlive( titan ) )
+ return
+
+ SetStanceStand( titan.GetTitanSoul() )
+
+ //the pilot never made it to embark - lets stand our titan up so he can fight
+ if ( !TitanHasNpcPilot( titan ) )
+ {
+ thread PlayAnimGravity( titan, "at_hotdrop_quickstand" )
+ HideName( titan )
+ }
+ }
+ )
+
+ //if the pilot has died, early out
+ if ( !IsAlive( pilot ) )
+ return
+
+ pilot.EndSignal( "OnDeath" )
+
+ //run to the titan
+ waitthread NpcPilotRunsToEmbarkTitan( pilot, titan )
+
+ //embark titan
+ waitthread NpcPilotEmbarksTitan( pilot, titan )
+}
+
+entity function NpcPilotCallsInTitan( entity pilot, vector origin, vector angles )
+{
+ Assert( !pilot.IsTitan() )
+ Assert( IsAlive( pilot ) )
+ Assert( !NpcPilotGetPetTitan( pilot ) )
+
+ //reset the next titan callin timer
+ NpcResetNextTitanRespawnAvailable( pilot )
+
+ //spawn a titan
+ array<string> settingsArray = GetAllowedTitanAISettings()
+
+ string titanSettings = settingsArray.getrandom()
+ entity titan = CreateNPC( "npc_titan", pilot.GetTeam(), origin, angles )
+ SetSpawnOption_AISettings( titan, titanSettings )
+ DispatchSpawn( titan )
+
+ NpcPilotSetPetTitan( pilot, titan )
+
+ //call it in
+ thread NPCTitanHotdrops( titan, false, "at_hotdrop_drop_2knee_turbo_upgraded" )
+ thread __TitanKneelOrStandAfterDropin( titan, pilot )
+
+ //get the titan ready to be embarked
+ SetStanceKneel( titan.GetTitanSoul() )
+ titan.SetTitle( pilot.GetTitle() + "'s Titan" )
+ UpdateEnemyMemoryFromTeammates( titan )
+
+ return titan
+}
+
+void function __TitanKneelOrStandAfterDropin( entity titan, entity pilot )
+{
+ Assert( IsAlive( titan ) )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ titan.WaitSignal( "TitanHotDropComplete" )
+
+ if ( IsAlive( pilot ) )
+ thread PlayAnimGravity( titan, "at_MP_embark_idle" )
+ //else the titan will automatically stand up
+}
+
+//HACK -> this behavior should be completely in code
+void function NpcPilotRunsToEmbarkTitan( entity pilot, entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ pilot.EndSignal( "OnDeath" )
+ pilot.EndSignal( "OnDestroy" )
+
+ pilot.SetNoTarget( true )
+ pilot.Anim_Stop()
+ pilot.DisableNPCMoveFlag( NPCMF_INDOOR_ACTIVITY_OVERRIDE )
+ pilot.EnableNPCMoveFlag( NPCMF_IGNORE_CLUSTER_DANGER_TIME | NPCMF_PREFER_SPRINT )
+ pilot.DisableArrivalOnce( true )
+ bool canMoveAndShoot = pilot.GetCapabilityFlag( bits_CAP_MOVE_SHOOT )
+ pilot.SetCapabilityFlag( bits_CAP_MOVE_SHOOT, false )
+
+ OnThreadEnd(
+ function () : ( pilot, canMoveAndShoot )
+ {
+ if ( !IsAlive( pilot ) )
+ return
+
+ pilot.SetNoTarget( false )
+ pilot.EnableNPCMoveFlag( NPCMF_INDOOR_ACTIVITY_OVERRIDE )
+ pilot.DisableNPCMoveFlag( NPCMF_IGNORE_CLUSTER_DANGER_TIME | NPCMF_PREFER_SPRINT )
+ pilot.SetCapabilityFlag( bits_CAP_MOVE_SHOOT, canMoveAndShoot )
+ }
+ )
+
+ local titanSubClass = GetSoulTitanSubClass( titan.GetTitanSoul() )
+ local embarkSet = FindBestEmbarkForNpcAnim( pilot, titan )
+ string pilotAnim = GetAnimFromAlias( titanSubClass, embarkSet.animSet.thirdPersonKneelingAlias )
+
+ pilot.ClearAllEnemyMemory()
+ waitthread RunToAnimStartForced_Deprecated( pilot, pilotAnim, titan, "hijack" )
+}
+
+/************************************************************************************************\
+
+ ###### ## ## #### ######## ###### ## ##
+## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ##
+ ###### ## ## ## ## ## ## #########
+ ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ##
+ ###### ### ### #### ## ###### ## ##
+
+\************************************************************************************************/
+function NpcPilotEmbarksTitan( entity pilot, entity titan )
+{
+ Assert( IsAlive( pilot ) )
+ Assert( IsAlive( titan ) )
+ Assert( !pilot.IsTitan() )
+ Assert( titan.IsTitan() )
+
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "OnDeath" )
+
+ OnThreadEnd(
+ function () : ( titan, pilot )
+ {
+ if ( IsAlive( titan ) )
+ {
+ if ( titan.ContextAction_IsBusy() )
+ titan.ContextAction_ClearBusy()
+ titan.ClearInvulnerable()
+
+ Assert( !IsAlive( pilot ) )
+ }
+ }
+ )
+
+ local isInvulnerable = pilot.IsInvulnerable()
+ pilot.SetInvulnerable()
+ titan.SetInvulnerable()
+
+ local titanSubClass = GetSoulTitanSubClass( titan.GetTitanSoul() )
+ local embarkSet = FindBestEmbark( pilot, titan )
+
+ while ( embarkSet == null )
+ {
+ wait 1.0
+ embarkSet = FindBestEmbark( pilot, titan )
+ }
+
+ local pilotAnim = GetAnimFromAlias( titanSubClass, embarkSet.animSet.thirdPersonKneelingAlias )
+ local titanAnim = embarkSet.animSet.titanKneelingAnim
+
+ if ( !titan.ContextAction_IsBusy() ) //might be set from kneeling
+ titan.ContextAction_SetBusy()
+ pilot.ContextAction_SetBusy()
+
+ if ( IsCloaked( pilot ) )
+ pilot.SetCloakDuration( 0, 0, 1.5 )
+
+ //pilot.SetParent( titan, "hijack", false, 0.5 ) //the time is just in case their not exactly at the right starting position
+ EmitSoundOnEntity( titan, embarkSet.audioSet.thirdPersonKneelingAudioAlias )
+ thread PlayAnim( pilot, pilotAnim, titan, "hijack" )
+ waitthread PlayAnim( titan, titanAnim )
+
+ if ( !isInvulnerable )
+ pilot.ClearInvulnerable()
+
+ NpcPilotBecomesTitan( pilot, titan )
+}
+
+entity function NpcPilotDisembarksTitan( entity titan )
+{
+ Assert( titan.IsTitan() )
+ Assert( TitanHasNpcPilot( titan ) )
+
+ entity pilot = NpcTitanBecomesPilot( titan )
+ Assert( !pilot.IsTitan() )
+
+ NpcPilotSetPetTitan( pilot, titan )
+
+ thread __NpcPilotDisembarksTitan( pilot, titan )
+
+ return pilot
+}
+
+function __NpcPilotDisembarksTitan( pilot, titan )
+{
+ expect entity( pilot )
+ expect entity( titan )
+
+ titan.ContextAction_SetBusy()
+ pilot.ContextAction_SetBusy()
+
+ if ( pilot.GetTitle() != "" )
+ {
+ titan.SetTitle( pilot.GetTitle() + "'s Titan" )
+ }
+
+ local isInvulnerable = pilot.IsInvulnerable()
+ pilot.SetInvulnerable()
+ titan.SetInvulnerable()
+
+ local pilot3pAnim, pilot3pAudio, titanDisembarkAnim
+ local titanSubClass = GetSoulTitanSubClass( titan.GetTitanSoul() )
+ local standing = titan.GetTitanSoul().GetStance() >= STANCE_STANDING // STANCE_STANDING = 2, STANCE_STAND = 3
+
+ if ( standing )
+ {
+ titanDisembarkAnim = "at_dismount_stand"
+ pilot3pAnim = "pt_dismount_" + titanSubClass + "_stand"
+ pilot3pAudio = titanSubClass + "_Disembark_Standing_3P"
+ }
+ else
+ {
+ titanDisembarkAnim = "at_dismount_crouch"
+ pilot3pAnim = "pt_dismount_" + titanSubClass + "_crouch"
+ pilot3pAudio = titanSubClass + "_Disembark_Kneeling_3P"
+ }
+
+// pilot.SetParent( titan, "hijack" )
+ EmitSoundOnEntity( titan, pilot3pAudio )
+ thread PlayAnim( titan, titanDisembarkAnim )
+ waitthread PlayAnim( pilot, pilot3pAnim, titan, "hijack" )
+
+ //pilot.ClearParent()
+ titan.ContextAction_ClearBusy()
+ pilot.ContextAction_ClearBusy()
+ if ( !isInvulnerable )
+ pilot.ClearInvulnerable()
+ titan.ClearInvulnerable()
+
+ if ( !standing )
+ SetStanceKneel( titan.GetTitanSoul() )
+}
+
+void function NpcPilotBecomesTitan( entity pilot, entity titan )
+{
+ Assert( IsAlive( pilot ) )
+ Assert( IsAlive( titan ) )
+ Assert( IsGrunt( pilot ) || IsPilotElite( pilot ) )
+ Assert( titan.IsTitan() )
+
+ entity titanSoul = titan.GetTitanSoul()
+
+ titanSoul.soul.seatedNpcPilot.isValid = true
+
+ titanSoul.soul.seatedNpcPilot.team = pilot.GetTeam()
+ titanSoul.soul.seatedNpcPilot.spawnflags = expect int( pilot.kv.spawnflags )
+ titanSoul.soul.seatedNpcPilot.accuracy = expect float( pilot.kv.AccuracyMultiplier )
+ titanSoul.soul.seatedNpcPilot.proficieny = expect float( pilot.kv.WeaponProficiency )
+ titanSoul.soul.seatedNpcPilot.health = expect float( pilot.kv.max_health )
+ titanSoul.soul.seatedNpcPilot.physDamageScale = expect float( pilot.kv.physdamagescale )
+ titanSoul.soul.seatedNpcPilot.weapon = pilot.GetMainWeapons()[0].GetWeaponClassName()
+ titanSoul.soul.seatedNpcPilot.squadName = expect string( pilot.kv.squadname )
+
+ titanSoul.soul.seatedNpcPilot.modelAsset = pilot.GetModelName()
+ titanSoul.soul.seatedNpcPilot.title = pilot.GetTitle()
+
+ titanSoul.soul.seatedNpcPilot.isInvulnerable = pilot.IsInvulnerable()
+
+ titan.SetTitle( titanSoul.soul.seatedNpcPilot.title )
+
+ thread __TitanPilotRodeoCounter( titan )
+
+ ScriptCallback_OnNpcPilotBecomesTitan( pilot, titan )
+
+ pilot.Destroy()
+}
+
+entity function NpcTitanBecomesPilot( entity titan )
+{
+ Assert( IsValid( titan ) )
+ Assert( titan.IsTitan() )
+
+ entity titanSoul = titan.GetTitanSoul()
+ titanSoul.soul.seatedNpcPilot.isValid = false
+
+ string weapon = titanSoul.soul.seatedNpcPilot.weapon
+ string squadName = titanSoul.soul.seatedNpcPilot.squadName
+ asset model = titanSoul.soul.seatedNpcPilot.modelAsset
+ string title = titanSoul.soul.seatedNpcPilot.title
+ int team = titanSoul.soul.seatedNpcPilot.team
+ vector origin = titan.GetOrigin()
+ vector angles = titan.GetAngles()
+ entity pilot = CreateElitePilot( team, origin, angles )
+
+ SetSpawnOption_Weapon( pilot, weapon )
+ SetSpawnOption_SquadName( pilot, squadName )
+ pilot.SetValueForModelKey( model )
+ DispatchSpawn( pilot )
+ pilot.SetModel( model ) // this is a hack, trying to avoid having a model spawn option because its easy to abuse
+
+ NpcPilotSetPetTitan( pilot, titan )
+ NpcResetNextTitanRespawnAvailable( pilot )
+
+ pilot.kv.spawnflags = titanSoul.soul.seatedNpcPilot.spawnflags
+ pilot.kv.AccuracyMultiplier = titanSoul.soul.seatedNpcPilot.accuracy
+ pilot.kv.WeaponProficiency = titanSoul.soul.seatedNpcPilot.proficieny
+ pilot.kv.health = titanSoul.soul.seatedNpcPilot.health
+ pilot.kv.max_health = titanSoul.soul.seatedNpcPilot.health
+ pilot.kv.physDamageScale = titanSoul.soul.seatedNpcPilot.physDamageScale
+
+ if ( titanSoul.soul.seatedNpcPilot.isInvulnerable )
+ pilot.SetInvulnerable()
+
+ titan.SetOwner( pilot )
+ NPCFollowsNPC( titan, pilot )
+
+ UpdateEnemyMemoryFromTeammates( pilot )
+ thread __TitanStanceThink( pilot, titan )
+
+ ScriptCallback_OnNpcTitanBecomesPilot( pilot, titan )
+
+ return pilot
+}
+
+bool function TitanHasNpcPilot( entity titan )
+{
+ Assert( titan.IsTitan() )
+
+ entity titanSoul = titan.GetTitanSoul()
+ if ( !IsValid( titanSoul ) )
+ return false
+
+ if ( !titanSoul.soul.seatedNpcPilot.isValid )
+ return false
+
+ return true
+}
+
+entity function NpcPilotGetPetTitan( entity pilot )
+{
+ Assert( !pilot.IsTitan() )
+ Assert( "petTitan" in pilot.s )
+
+ if ( !IsAlive( expect entity( pilot.s.petTitan ) ) )
+ return null
+
+ Assert( pilot.s.petTitan.IsTitan() )
+ return expect entity( pilot.s.petTitan )
+}
+
+void function NpcPilotSetPetTitan( entity pilot, entity titan )
+{
+ Assert( !pilot.IsTitan() )
+ Assert( titan.IsTitan() )
+ Assert( "petTitan" in pilot.s )
+
+ pilot.s.petTitan = titan
+ pilot.Signal( "PetTitanUpdated" )
+}
+#endif // NPC_TITAN_PILOT_PROTOTYPE
+
+function __TitanStanceThink( entity pilot, entity titan )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ if ( titan.GetTitanSoul().IsDoomed() )
+ return
+
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "NpcPilotBecomesTitan" )
+
+ WaittillAnimDone( titan ) //wait for disembark anim
+
+ // kneel in certain circumstances
+ while ( IsAlive( pilot ) )
+ {
+ if ( !ChangedStance( titan ) )
+ waitthread TitanWaitsToChangeStance_or_PilotDeath( pilot, titan )
+ }
+
+ if ( titan.GetTitanSoul().GetStance() < STANCE_STANDING )
+ {
+ while ( !TitanCanStand( titan ) )
+ wait 2
+
+ TitanStandUp( titan )
+ }
+}
+
+function TitanWaitsToChangeStance_or_PilotDeath( pilot, titan )
+{
+ pilot.EndSignal( "OnDeath" )
+ pilot.EndSignal( "OnDestroy" )
+
+ TitanWaitsToChangeStance( titan )
+}
+
+/************************************************************************************************\
+
+######## ####### ####### ## ######
+ ## ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ######
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ## ##
+ ## ####### ####### ######## ######
+
+\************************************************************************************************/
+
+function __WaitforTitanCallinReady( entity pilot )
+{
+ pilot.EndSignal( "OnDeath" )
+ pilot.EndSignal( "OnDestroy" )
+
+ //HACK TODO: handle eTitanAvailability.Default vs custom and none, AND ALSO make a way to kill this thread
+
+ while ( true )
+ {
+ if ( pilot.s.nextTitanRespawnAvailable == NPC_NEXT_TITANTIME_RESET )
+ pilot.s.nextTitanRespawnAvailable = Time() + RandomFloatRange( NPC_NEXT_TITANTIME_MIN, NPC_NEXT_TITANTIME_MAX ) //this is just a random number - maybe in the future it will be based on the npc's kills...maybe also on the players if it's a slot
+
+ if ( pilot.s.nextTitanRespawnAvailable <= Time() )
+ break
+
+ float delay = max( pilot.s.nextTitanRespawnAvailable - Time(), 0.1 ) //make sure min delay of 0.1 to account for floating point error
+
+ thread SetSignalDelayed( pilot, "NpcTitanRespawnAvailableUpdated", delay )
+ pilot.WaitSignal( "NpcTitanRespawnAvailableUpdated" )
+
+ //keep looping backup just in case this value changes outside this function, we get an update
+ continue
+ }
+
+ Assert( Time() >= pilot.s.nextTitanRespawnAvailable )
+ Assert( pilot.s.nextTitanRespawnAvailable != NPC_NEXT_TITANTIME_RESET )
+}
+
+function __TitanKneelsForPilot( pilot, titan )
+{
+ expect entity( pilot )
+ expect entity( titan )
+
+ pilot.EndSignal( "OnDeath" )
+ pilot.EndSignal( "OnDestroy" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function () : ( pilot, titan )
+ {
+ if ( !IsAlive( titan ) )
+ return
+
+ SetStanceStand( titan.GetTitanSoul() )
+
+ //the pilot never made it to embark - lets stand our titan up so he can fight
+ if ( !IsAlive( pilot ) )
+ {
+ thread PlayAnimGravity( titan, "at_hotdrop_quickstand" )
+ HideName( titan )
+ titan.ContextAction_ClearBusy()
+ }
+ }
+ )
+
+ if ( !titan.ContextAction_IsBusy() ) //might be set from kneeling
+ titan.ContextAction_SetBusy()
+ SetStanceKneel( titan.GetTitanSoul() )
+
+ waitthread PlayAnimGravity( titan, "at_MP_stand2knee_straight" )
+ waitthread PlayAnim( titan, "at_MP_embark_idle" )
+}
+
+function HasEnemyRodeo( titan )
+{
+ expect entity( titan )
+
+ if ( !IsAlive( titan ) )
+ return false
+
+ if ( IsValid( GetEnemyRodeoPilot( titan ) ) )
+ return true
+
+ return false
+}
+
+function __TitanPilotRodeoCounter( entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ while ( true )
+ {
+ while ( !HasEnemyRodeo( titan ) )
+ titan.GetTitanSoul().WaitSignal( "RodeoRiderChanged" )
+
+ wait RandomFloatRange( 3, 6 ) //give some time for debounce in case the rider jumps right off
+ if ( !HasEnemyRodeo( titan ) )
+ continue
+
+ #if NPC_TITAN_PILOT_PROTOTYPE
+ thread NpcPilotDisembarksTitan( titan )
+ return
+ #endif
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_sniper_titans.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_sniper_titans.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_sniper_titans.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut
new file mode 100644
index 00000000..9717c76d
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut
@@ -0,0 +1,787 @@
+untyped
+
+global const RPG_USE_ALWAYS = 2
+
+global const STANDARDGOALRADIUS = 100
+
+global function AiSoldiers_Init
+
+global function MakeSquadName
+global function GetPlayerSpectreSquadName
+global function disable_npcs
+global function disable_new_npcs
+global function Disable_IMC
+global function Disable_MILITIA
+
+global function CommonMinionInit
+global function DisableMinionUsesHeavyWeapons
+global function SetupMinionForRPGs
+global function IsNPCSpawningEnabled
+global function EnableAutoPopulate
+global function DisableAutoPopulate
+global function OnEnemyChanged_MinionSwitchToHeavyArmorWeapon
+global function OnEnemyChanged_MinionUpdateAimSettingsForEnemy
+global function OnEnemyChanged_TryHeavyArmorWeapon
+global function ResetNPCs
+global function IsValidRocketTarget
+global function GetMilitiaTitle
+
+global function AssaultOrigin
+global function SquadAssaultOrigin
+
+global function ClientCommand_SpawnViewGrunt
+
+global function OnSoldierSeeEnemy
+global function TryFriendlyPassingNearby
+
+global function OnSpectreSeeEnemy
+
+global function onlyimc // debug
+global function onlymilitia // debug
+
+global function SetGlobalNPCHealth //debug
+
+
+//=========================================================
+// MP ai soldier
+//
+//=========================================================
+
+struct
+{
+ int militiaTitlesIndex
+ array<string> militiaTitles
+} file
+
+function AiSoldiers_Init()
+{
+ level.COOP_AT_WEAPON_RATES <- {}
+ level.COOP_AT_WEAPON_RATES[ "mp_weapon_rocket_launcher" ] <- 0.5
+ level.COOP_AT_WEAPON_RATES[ "mp_weapon_smr" ] <- 0.4
+ level.COOP_AT_WEAPON_RATES[ "mp_weapon_mgl" ] <- 0.1
+
+ PrecacheSprite( $"sprites/glow_05.vmt" )
+ FlagInit( "disable_npcs" )
+ FlagInit( "Disable_IMC" )
+ FlagInit( "Disable_MILITIA" )
+
+ level.onlySpawn <- null
+
+ level.spectreSpawnStyle <- eSpectreSpawnStyle.MORE_FOR_ENEMY_TITANS
+
+ FlagInit( "AllSpectre" )
+ FlagInit( "AllSpectreIMC" )
+ FlagInit( "AllSpectreMilitia" )
+ FlagInit( "NoSpectreIMC" )
+ FlagInit( "NoSpectreMilitia" )
+
+ RegisterSignal( "OnSendAIToAssaultPoint" )
+
+ InitMilitiaTitles()
+
+ AddCallback_OnClientConnecting( AiSoldiers_InitPlayer )
+
+ if ( GetDeveloperLevel() > 0 )
+ AddClientCommandCallback( "SpawnViewGrunt", ClientCommand_SpawnViewGrunt )
+
+}
+
+bool function ClientCommand_SpawnViewGrunt( entity player, array<string> args )
+{
+ int team = args[0].tointeger()
+ if ( GetDeveloperLevel() < 1 )
+ return true
+
+ vector origin = player.EyePosition()
+ vector angles = player.EyeAngles()
+ vector forward = AnglesToForward( angles )
+ TraceResults result = TraceLine( origin, origin + forward * 2000, player )
+ angles.x = 0
+ angles.z = 0
+
+ entity guy = CreateSoldier( team, result.endPos, angles )
+ DispatchSpawn( guy )
+ return true
+}
+
+// debug commands
+function onlyimc()
+{
+ level.onlySpawn = TEAM_IMC
+ printt( "Only spawning IMC AI" )
+}
+
+// debug commands
+function onlymilitia()
+{
+ level.onlySpawn = TEAM_MILITIA
+ printt( "Only spawning Militia AI" )
+}
+
+//////////////////////////////////////////////////////////
+void function AiSoldiers_InitPlayer( entity player )
+{
+ player.s.next_ai_callout_time <- -1
+
+ string squadName = GetPlayerSpectreSquadName( player )
+ player.p.spectreSquad = squadName
+}
+
+//////////////////////////////////////////////////////////
+string function MakeSquadName( int team, string msg )
+{
+ string teamStr
+
+ if ( team == TEAM_IMC )
+ teamStr = "imc"
+ else if ( team == TEAM_MILITIA )
+ teamStr = "militia"
+ else
+ teamStr = "default"
+
+ return "squad_" + teamStr + msg
+}
+
+//////////////////////////////////////////////////////////
+
+
+//////////////////////////////////////////////////////////
+// common init for grunts and spectres
+void function CommonMinionInit( entity npc )
+{
+ RandomizeHead( npc )
+
+ if ( IsMultiplayer() )
+ {
+ npc.kv.alwaysAlert = 1
+ npc.EnableNPCFlag( NPC_STAY_CLOSE_TO_SQUAD | NPC_NEW_ENEMY_FROM_SOUND )
+ npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+ }
+
+ npc.s.cpState <- eNPCStateCP.NONE
+
+ if ( npc.kv.alwaysalert.tointeger() == 1 )
+ npc.SetDefaultSchedule( "SCHED_ALERT_SCAN" )
+}
+
+function SetupMinionForRPGs( entity soldier )
+{
+ soldier.SetEnemyChangeCallback( OnEnemyChanged_MinionSwitchToHeavyArmorWeapon )
+}
+
+
+void function OnSoldierSeeEnemy( entity guy )
+{
+ guy.EndSignal( "OnDeath" )
+
+ if ( NPC_GruntChatterSPEnabled( guy ) )
+ return
+
+ while ( true )
+ {
+ var results = WaitSignal( guy, "OnSeeEnemy" )
+
+ if ( !IsValid( guy ) )
+ return
+
+ TrySpottedCallout( guy, expect entity( results.activator ) )
+ }
+}
+
+void function TryFriendlyPassingNearby( entity grunt )
+{
+ grunt.EndSignal( "OnDeath" )
+
+ if ( NPC_GruntChatterSPEnabled( grunt ) )
+ return
+
+ while ( true )
+ {
+ wait 5
+
+ if ( !IsValid( grunt ) )
+ return
+
+ #if GRUNT_CHATTER_MP_ENABLED
+ // only do this a minute into the match
+ if ( Time() > 60.0 && TryFriendlyCallout( grunt, "pilot", "bc_reactFriendlyPilot" , 500 ) )
+ continue
+ if ( TryFriendlyCallout( grunt, "titan", "bc_reactTitanfallFriendlyArrives" , 500 ) )
+ continue
+ if ( TryFriendlyCallout( grunt, "npc_super_spectre", "bc_reactReaperFriendlyArrives" , 500 ) )
+ continue
+ if ( TryFriendlyCallout( grunt, "npc_frag_drone", "bc_reactTickSpawnFriendly" , 500 ) )
+ continue
+ if ( IsAlive( grunt.GetEnemy() ) )
+ {
+ entity enemy = grunt.GetEnemy()
+ if ( enemy.IsTitan() )
+ PlayGruntChatterMPLine( grunt, "bc_generalCombatTitan" )
+ else
+ PlayGruntChatterMPLine( grunt, "bc_generalCombat" )
+ }
+ else
+ {
+ PlayGruntChatterMPLine( grunt, "bc_generalNonCombat" )
+ }
+ #endif
+ }
+}
+
+#if GRUNT_CHATTER_MP_ENABLED
+bool function TryFriendlyCallout( entity grunt, string npcClassname, string callout, float dist )
+{
+ array<entity> nearbyFriendlies
+ float distSq = dist*dist
+ if ( npcClassname == "pilot" )
+ {
+ array<entity> players = GetPlayerArrayOfTeam_AlivePilots( grunt.GetTeam() )
+ foreach( p in players )
+ {
+ if ( DistanceSqr( p.GetOrigin(), grunt.GetOrigin() ) > distSq )
+ continue
+ nearbyFriendlies.append( p )
+ }
+ }
+ else if ( npcClassname == "titan" )
+ {
+ nearbyFriendlies = GetNPCArrayEx( "npc_titan", grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), dist )
+ array<entity> players = GetPlayerArrayOfTeam_Alive( grunt.GetTeam() )
+ foreach( p in players )
+ {
+ if ( !p.IsTitan() )
+ continue
+ if ( DistanceSqr( p.GetOrigin(), grunt.GetOrigin() ) > distSq )
+ continue
+ nearbyFriendlies.append( p )
+ }
+ }
+ else
+ {
+ nearbyFriendlies = GetNPCArrayEx( npcClassname, grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), dist )
+ }
+
+ foreach ( friendly in nearbyFriendlies )
+ {
+ if ( !IsAlive( friendly ) )
+ continue
+
+ if ( GetDoomedState( friendly ) )
+ continue
+
+ PlayGruntChatterMPLine( grunt, callout )
+ return true
+ }
+
+ return false
+}
+#endif
+
+void function OnSpectreSeeEnemy( entity guy )
+{
+ guy.EndSignal( "OnDeath" )
+
+ while ( true )
+ {
+ var results = WaitSignal( guy, "OnGainEnemyLOS" )
+
+ TrySpottedCallout( guy, expect entity( results.activator ) )
+ }
+}
+
+
+//////////////////////////////////////////////////////////
+bool function IsValidRocketTarget( entity enemy )
+{
+ return enemy.GetArmorType() == ARMOR_TYPE_HEAVY
+}
+
+//////////////////////////////////////////////////////////
+function DisableMinionUsesHeavyWeapons( entity soldier )
+{
+ soldier.SetEnemyChangeCallback( OnEnemyChanged_MinionUpdateAimSettingsForEnemy )
+}
+
+void function OnEnemyChanged_MinionSwitchToHeavyArmorWeapon( entity soldier )
+{
+ OnEnemyChanged_TryHeavyArmorWeapon( soldier )
+ OnEnemyChanged_MinionUpdateAimSettingsForEnemy( soldier )
+}
+
+//////////////////////////////////////////////////////////
+void function OnEnemyChanged_MinionUpdateAimSettingsForEnemy( entity soldier )
+{
+ SetProficiency( soldier )
+}
+
+
+bool function AssignNPCAppropriateWeaponFromWeapons( entity npc, array<entity> weapons, bool isRocketTarget )
+{
+ // first try to find an appropriate weapon
+ foreach ( weapon in weapons )
+ {
+ bool isAntiTitan = weapon.GetWeaponType() == WT_ANTITITAN
+ if ( isAntiTitan == isRocketTarget )
+ {
+ // found a weapon to use
+ npc.SetActiveWeaponByName( weapon.GetWeaponClassName() )
+ return true
+ }
+ }
+ return false
+}
+
+//////////////////////////////////////////////////////////
+void function OnEnemyChanged_TryHeavyArmorWeapon( entity npc )
+{
+ entity enemy = npc.GetEnemy()
+ if ( !IsAlive( enemy ) )
+ return
+
+ array<entity> weapons = npc.GetMainWeapons()
+
+ // do we have a weapon to switch to?
+ if ( !weapons.len() )
+ return
+
+ entity activeWeapon = npc.GetActiveWeapon()
+ bool isRocketTarget = IsValidRocketTarget( enemy )
+
+ if ( activeWeapon == null )
+ {
+ if ( AssignNPCAppropriateWeaponFromWeapons( npc, weapons, isRocketTarget ) )
+ return
+
+ // if that fails, use the first weapon, so we do consistent behavior
+ npc.SetActiveWeaponByName( weapons[0].GetWeaponClassName() )
+ return
+ }
+
+ bool isActiveWeapon_AntiTitan = activeWeapon.GetWeaponType() == WT_ANTITITAN
+
+ // already using an appropriate weapon?
+ if ( isActiveWeapon_AntiTitan == isRocketTarget )
+ return
+
+ AssignNPCAppropriateWeaponFromWeapons( npc, weapons, isRocketTarget )
+}
+
+const float NPC_CLOSE_DISTANCE_SQR_THRESHOLD = 1000.0 * 1000.0
+
+//////////////////////////////////////////////////////////
+void function TrySpottedCallout( entity guy, entity enemy )
+{
+ if ( !IsAlive( guy ) )
+ return
+
+ if ( !IsAlive( enemy ) )
+ return
+
+ float distanceSqr = DistanceSqr( guy.GetOrigin(), enemy.GetOrigin() )
+ bool isClose = distanceSqr <= NPC_CLOSE_DISTANCE_SQR_THRESHOLD
+
+ if ( enemy.IsTitan() )
+ {
+ if ( IsSpectre( guy ) ) //Spectre callouts
+ {
+ #if SPECTRE_CHATTER_MP_ENABLED
+ PlaySpectreChatterMPLine( guy, "diag_imc_spectre_gs_spotclosetitancall_01" )
+ #else
+ if ( isClose )
+ PlaySpectreChatterToAll( "spectre_gs_spotclosetitancall_01", guy )
+ else
+ PlaySpectreChatterToAll( "spectre_gs_spotfartitan_1_1", guy )
+ #endif
+
+ }
+ else //Grunt callouts
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ PlayGruntChatterMPLine( guy, "bc_enemytitanspotcall" )
+ #endif
+ }
+ }
+ else if ( enemy.IsPlayer() )
+ {
+ if ( IsSpectre( guy ) ) //Spectre callouts
+ {
+ #if SPECTRE_CHATTER_MP_ENABLED
+ PlaySpectreChatterMPLine( guy, "diag_imc_spectre_gs_engagepilotenemy_01_1" )
+ #else
+ if ( isClose )
+ PlaySpectreChatterToAll( "spectre_gs_engagepilotenemy_01_1", guy )
+ else
+ PlaySpectreChatterToAll( "spectre_gs_spotenemypilot_01_1", guy )
+ #endif
+ }
+ else //Grunt callouts
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ if ( isClose )
+ PlayGruntChatterMPLine( guy, "bc_spotenemypilot" )
+ else
+ PlayGruntChatterMPLine( guy, "bc_engagepilotenemy" )
+ #endif
+ }
+ }
+ else if ( IsSuperSpectre( enemy ) )
+ {
+ if ( !IsSpectre( guy ) ) //Spectre callouts
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ PlayGruntChatterMPLine( guy, "bc_reactEnemyReaper" )
+ #endif
+ }
+ }
+ else
+ {
+ if ( !IsSpectre( guy ) ) //Spectre callouts
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ PlayGruntChatterMPLine( guy, "bc_reactEnemySpotted" )
+ #endif
+ }
+ }
+}
+
+
+//////////////////////////////////////////////////////////
+string function GetPlayerSpectreSquadName( entity player )
+{
+ return "player" + player.entindex() + "spectreSquad"
+}
+
+
+//////////////////////////////////////////////////////////
+
+string function GetMilitiaTitle()
+{
+ file.militiaTitlesIndex++
+ if ( file.militiaTitlesIndex >= file.militiaTitles.len() )
+ file.militiaTitlesIndex = 0
+
+ return file.militiaTitles[ file.militiaTitlesIndex ]
+}
+
+void function InitMilitiaTitles()
+{
+ file.militiaTitles = [
+ "#NPC_MILITIA_NAME_AND_RANK_0",
+ "#NPC_MILITIA_NAME_AND_RANK_1",
+ "#NPC_MILITIA_NAME_AND_RANK_2",
+ "#NPC_MILITIA_NAME_AND_RANK_3",
+ "#NPC_MILITIA_NAME_AND_RANK_4",
+ "#NPC_MILITIA_NAME_AND_RANK_5",
+ "#NPC_MILITIA_NAME_AND_RANK_6",
+ "#NPC_MILITIA_NAME_AND_RANK_7",
+ "#NPC_MILITIA_NAME_AND_RANK_8",
+ "#NPC_MILITIA_NAME_AND_RANK_9",
+ "#NPC_MILITIA_NAME_AND_RANK_10",
+ "#NPC_MILITIA_NAME_AND_RANK_11",
+ "#NPC_MILITIA_NAME_AND_RANK_12",
+ "#NPC_MILITIA_NAME_AND_RANK_13",
+ "#NPC_MILITIA_NAME_AND_RANK_14",
+ "#NPC_MILITIA_NAME_AND_RANK_15",
+ "#NPC_MILITIA_NAME_AND_RANK_16",
+ "#NPC_MILITIA_NAME_AND_RANK_17",
+ "#NPC_MILITIA_NAME_AND_RANK_18",
+ "#NPC_MILITIA_NAME_AND_RANK_19",
+ "#NPC_MILITIA_NAME_AND_RANK_20",
+ "#NPC_MILITIA_NAME_AND_RANK_21",
+ "#NPC_MILITIA_NAME_AND_RANK_22",
+ "#NPC_MILITIA_NAME_AND_RANK_23",
+ "#NPC_MILITIA_NAME_AND_RANK_24",
+ "#NPC_MILITIA_NAME_AND_RANK_25",
+ "#NPC_MILITIA_NAME_AND_RANK_26",
+ "#NPC_MILITIA_NAME_AND_RANK_27",
+ "#NPC_MILITIA_NAME_AND_RANK_28",
+ "#NPC_MILITIA_NAME_AND_RANK_29",
+ "#NPC_MILITIA_NAME_AND_RANK_30",
+ "#NPC_MILITIA_NAME_AND_RANK_31",
+ "#NPC_MILITIA_NAME_AND_RANK_32",
+ "#NPC_MILITIA_NAME_AND_RANK_33",
+ "#NPC_MILITIA_NAME_AND_RANK_34",
+ "#NPC_MILITIA_NAME_AND_RANK_35",
+ "#NPC_MILITIA_NAME_AND_RANK_36",
+ "#NPC_MILITIA_NAME_AND_RANK_37",
+ "#NPC_MILITIA_NAME_AND_RANK_38",
+ "#NPC_MILITIA_NAME_AND_RANK_39",
+ "#NPC_MILITIA_NAME_AND_RANK_40",
+ "#NPC_MILITIA_NAME_AND_RANK_41",
+ "#NPC_MILITIA_NAME_AND_RANK_42",
+ "#NPC_MILITIA_NAME_AND_RANK_43",
+ "#NPC_MILITIA_NAME_AND_RANK_44",
+ "#NPC_MILITIA_NAME_AND_RANK_45",
+ "#NPC_MILITIA_NAME_AND_RANK_46",
+ "#NPC_MILITIA_NAME_AND_RANK_47",
+ "#NPC_MILITIA_NAME_AND_RANK_48",
+ "#NPC_MILITIA_NAME_AND_RANK_49",
+ "#NPC_MILITIA_NAME_AND_RANK_50",
+ "#NPC_MILITIA_NAME_AND_RANK_51",
+ "#NPC_MILITIA_NAME_AND_RANK_52",
+ "#NPC_MILITIA_NAME_AND_RANK_53",
+ "#NPC_MILITIA_NAME_AND_RANK_54",
+ "#NPC_MILITIA_NAME_AND_RANK_55",
+ "#NPC_MILITIA_NAME_AND_RANK_56",
+ "#NPC_MILITIA_NAME_AND_RANK_57",
+ "#NPC_MILITIA_NAME_AND_RANK_58",
+ "#NPC_MILITIA_NAME_AND_RANK_59",
+ "#NPC_MILITIA_NAME_AND_RANK_60",
+ "#NPC_MILITIA_NAME_AND_RANK_61",
+ "#NPC_MILITIA_NAME_AND_RANK_62",
+ "#NPC_MILITIA_NAME_AND_RANK_63",
+ "#NPC_MILITIA_NAME_AND_RANK_64",
+ "#NPC_MILITIA_NAME_AND_RANK_65",
+ "#NPC_MILITIA_NAME_AND_RANK_66",
+ "#NPC_MILITIA_NAME_AND_RANK_67",
+ "#NPC_MILITIA_NAME_AND_RANK_68",
+ "#NPC_MILITIA_NAME_AND_RANK_69",
+ "#NPC_MILITIA_NAME_AND_RANK_70",
+ "#NPC_MILITIA_NAME_AND_RANK_71",
+ "#NPC_MILITIA_NAME_AND_RANK_72",
+ "#NPC_MILITIA_NAME_AND_RANK_73",
+ "#NPC_MILITIA_NAME_AND_RANK_74",
+ "#NPC_MILITIA_NAME_AND_RANK_75",
+ "#NPC_MILITIA_NAME_AND_RANK_76",
+ "#NPC_MILITIA_NAME_AND_RANK_77",
+ "#NPC_MILITIA_NAME_AND_RANK_78",
+ "#NPC_MILITIA_NAME_AND_RANK_79",
+ "#NPC_MILITIA_NAME_AND_RANK_80",
+ "#NPC_MILITIA_NAME_AND_RANK_81",
+ "#NPC_MILITIA_NAME_AND_RANK_82",
+ "#NPC_MILITIA_NAME_AND_RANK_83",
+ "#NPC_MILITIA_NAME_AND_RANK_84",
+ "#NPC_MILITIA_NAME_AND_RANK_85",
+ "#NPC_MILITIA_NAME_AND_RANK_86",
+ "#NPC_MILITIA_NAME_AND_RANK_87",
+ "#NPC_MILITIA_NAME_AND_RANK_88",
+ "#NPC_MILITIA_NAME_AND_RANK_89",
+ "#NPC_MILITIA_NAME_AND_RANK_90",
+ "#NPC_MILITIA_NAME_AND_RANK_91",
+ "#NPC_MILITIA_NAME_AND_RANK_92",
+ "#NPC_MILITIA_NAME_AND_RANK_93",
+ "#NPC_MILITIA_NAME_AND_RANK_94",
+ "#NPC_MILITIA_NAME_AND_RANK_95"
+ "#NPC_MILITIA_NAME_AND_RANK_96",
+ "#NPC_MILITIA_NAME_AND_RANK_97",
+ "#NPC_MILITIA_NAME_AND_RANK_98",
+ "#NPC_MILITIA_NAME_AND_RANK_99"
+ ]
+
+ file.militiaTitles.randomize()
+ file.militiaTitlesIndex = 0
+}
+
+//////////////////////////////////////////////////////////
+function disable_npcs()
+{
+ FlagSet( "disable_npcs" )
+ printl( "disabling_npcs" )
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetClassName() == "npc_turret_mega" )
+ continue
+ if ( guy.GetClassName() == "npc_turret_sentry" )
+ continue
+ if ( guy.GetClassName() == "npc_titan" )
+ continue
+
+ guy.Destroy()
+ }
+}
+//////////////////////////////////////////////////////////
+// //hack - we want to toggle new AI on and off through the dev menu even though playlist defaults to use them all the time
+function disable_new_npcs()
+{
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetClassName() == "npc_turret_mega" )
+ continue
+ if ( guy.GetClassName() == "npc_turret_sentry" )
+ continue
+ if ( guy.GetClassName() == "npc_titan" )
+ continue
+
+ guy.Destroy()
+ }
+}
+
+function ResetNPCs()
+{
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetClassName() == "npc_turret_mega" )
+ continue
+ if ( guy.GetClassName() == "npc_turret_sentry" )
+ continue
+
+ if ( guy.GetClassName() == "npc_titan" && IsValid( guy.GetTitanSoul() ) )
+ {
+ guy.GetTitanSoul().Destroy()
+ }
+
+ guy.Destroy()
+ }
+}
+
+//////////////////////////////////////////////////////////
+function Disable_IMC()
+{
+ DisableAutoPopulate( TEAM_IMC )
+ printl( "Disable_IMC" )
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetTeam() == TEAM_IMC )
+ guy.Kill_Deprecated_UseDestroyInstead()
+ }
+}
+
+
+//////////////////////////////////////////////////////////
+function Disable_MILITIA()
+{
+ DisableAutoPopulate( TEAM_MILITIA )
+ printl( "Disable_MILITIA" )
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetTeam() == TEAM_MILITIA )
+ guy.Kill_Deprecated_UseDestroyInstead()
+ }
+}
+
+//////////////////////////////////////////////////////////
+function IsNPCSpawningEnabled()
+{
+ if ( Riff_AllowNPCs() != eAllowNPCs.Default )
+ {
+ if ( Riff_AllowNPCs() == eAllowNPCs.None )
+ return false
+
+ return true
+ }
+
+ return true
+}
+
+
+function DisableAutoPopulate( team )
+{
+ switch ( team )
+ {
+ case TEAM_IMC:
+ FlagSet( "Disable_IMC" )
+ break
+
+ case TEAM_MILITIA:
+ FlagSet( "Disable_MILITIA" )
+ break
+
+ default:
+ Assert( 0, "team number " + team + " not setup for autoPopulation.")
+ break
+ }
+}
+
+function EnableAutoPopulate( team )
+{
+ switch ( team )
+ {
+ case TEAM_IMC:
+ FlagClear( "Disable_IMC" )
+ break
+
+ case TEAM_MILITIA:
+ FlagClear( "Disable_MILITIA" )
+ break
+
+ default:
+ Assert( 0, "team number " + team + " not setup for autoPopulation.")
+ break
+ }
+}
+
+//////////////////////////////////////////////////////////
+
+
+function GuyTeleportsOnPathFail( guy, origin )
+{
+ expect entity( guy )
+
+ guy.EndSignal( "OnFailedToPath" )
+
+ local e = {}
+ e.waited <- false
+ OnThreadEnd(
+ function() : ( guy, origin, e )
+ {
+ if ( !IsAlive( guy ) )
+ return
+
+ // wait was cut off
+ if ( !e.waited )
+ guy.SetOrigin( origin )
+ }
+ )
+
+ wait 2
+ e.waited = true
+}
+
+void function SquadAssaultOrigin( array<entity> group, vector origin, float radius = STANDARDGOALRADIUS )
+{
+ foreach ( member in group )
+ {
+ thread AssaultOrigin( member, origin, radius )
+ }
+}
+
+void function AssaultOrigin( entity guy, vector origin, float radius = STANDARDGOALRADIUS )
+{
+ waitthread SendAIToAssaultPoint( guy, origin, <0,0,0>, radius )
+}
+
+void function SendAIToAssaultPoint( entity guy, vector origin, vector angles, float radius = STANDARDGOALRADIUS )
+{
+ Assert( IsAlive( guy ) )
+ guy.Signal( "OnSendAIToAssaultPoint" )
+ guy.Anim_Stop() // in case we were doing an anim already
+ guy.EndSignal( "OnDeath" )
+ guy.EndSignal( "OnSendAIToAssaultPoint" )
+
+ bool allowFlee = guy.GetNPCFlag( NPC_ALLOW_FLEE )
+ bool allowHandSignal = guy.GetNPCFlag( NPC_ALLOW_HAND_SIGNALS )
+
+ OnThreadEnd(
+ function() : ( guy, allowFlee, allowHandSignal )
+ {
+ if ( IsAlive( guy ) )
+ {
+ guy.SetNPCFlag( NPC_ALLOW_FLEE, allowFlee )
+ guy.SetNPCFlag( NPC_ALLOW_HAND_SIGNALS, allowHandSignal )
+ }
+ }
+ )
+
+ guy.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS )
+ guy.AssaultPoint( origin )
+ guy.AssaultSetGoalRadius( radius )
+ guy.WaitSignal( "OnFinishedAssault" )
+
+}
+
+function SetGlobalNPCHealth( healthValue ) //Debug, for trailer team
+{
+ array<entity> npcArray = GetNPCArray()
+
+ foreach ( npc in npcArray )
+ {
+ npc.SetMaxHealth( healthValue )
+ npc.SetHealth( healthValue )
+ }
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_mp.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_mp.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_sp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_sp.gnut
new file mode 100644
index 00000000..6faf6649
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_sp.gnut
@@ -0,0 +1,17 @@
+global function IsAutoPopulateEnabled
+
+bool function IsAutoPopulateEnabled( var team = null )
+{
+ if ( IsNPCSpawningEnabled() == false )
+ return false
+
+ if ( Flag( "disable_npcs" ) )
+ return false
+
+ if ( team == TEAM_MILITIA && Flag( "Disable_MILITIA" ) )
+ return false
+ if ( team == TEAM_IMC && Flag( "Disable_IMC" ) )
+ return false
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn.gnut
new file mode 100644
index 00000000..7e4d2cdd
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn.gnut
@@ -0,0 +1,696 @@
+untyped
+
+global function AiSpawn_Init
+
+global function __GetWeaponModel
+global function AssaultLinkedMoveTarget
+global function AssaultMoveTarget
+global function AutoSquadnameAssignment
+global function CreateArcTitan
+global function CreateAtlas
+global function CreateElitePilot
+global function CreateElitePilotAssassin
+global function CreateFragDrone
+global function CreateFragDroneCan
+global function CreateGenericDrone
+global function CreateGunship
+global function CreateHenchTitan
+global function CreateMarvin
+global function CreateNPC
+global function CreateNPCFromAISettings
+global function CreateNPCTitan
+global function CreateOgre
+global function CreateProwler
+global function CreateRocketDrone
+global function CreateRocketDroneGrunt
+global function CreateShieldDrone
+global function CreateShieldDroneGrunt
+global function CreateSoldier
+global function CreateSpectre
+global function CreateStalker
+global function CreateStryder
+global function CreateSuperSpectre
+global function CreateWorkerDrone
+global function CreateZombieStalker
+global function CreateZombieStalkerMossy
+global function StopAssaultMoveTarget
+
+global const HACK_CAP_BACK1 = $"models/sandtrap/sandtrap_wall_bracket.mdl"
+global const HACK_CAP_BACK2 = $"models/pipes/pipe_modular_grey_bracket_cap.mdl"
+global const HACK_CAP_BACK3 = $"models/lamps/office_lights_hanging_wire.mdl"
+global const HACK_DRONE_BACK1 = $"models/Weapons/ammoboxes/backpack_single.mdl"
+global const HACK_DRONE_BACK2 = $"models/barriers/fence_wire_holder_double.mdl"
+global const DEFAULT_TETHER_RADIUS = 1500
+global const DEFAULT_COVER_BEHAVIOR_CYLINDER_HEIGHT = 512
+
+struct
+{
+ array<string> moveTargetClasses
+} file
+
+void function AiSpawn_Init()
+{
+ PrecacheModel( HACK_CAP_BACK1 )
+ PrecacheModel( HACK_CAP_BACK2 )
+ PrecacheModel( HACK_CAP_BACK3 )
+ PrecacheModel( HACK_DRONE_BACK1 )
+ PrecacheModel( HACK_DRONE_BACK2 )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_LMG )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_RIFLE )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_ROCKET )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_SHOTGUN )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_SMG )
+
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_LMG )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_RIFLE )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_ROCKET )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_SHOTGUN )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_SMG )
+
+ file.moveTargetClasses = [ "info_move_target", "info_move_animation" ]
+ foreach ( movetargetClass in file.moveTargetClasses )
+ {
+ AddSpawnCallbackEditorClass( "info_target", movetargetClass, InitInfoMoveTargetFlags )
+ }
+
+ RegisterSignal( "StopAssaultMoveTarget" )
+ RegisterSignal( "OnFinishedAssaultChain" )
+
+ AiSpawnContent_Init()
+ #if DEV
+ // just to insure that ai settings are being setup properly.
+ InitNpcSettingsFileNamesForDevMenu()
+ SetupSpawnAIButtons( TEAM_MILITIA )
+ AddCallback_EntitiesDidLoad( AiSpawn_EntitiesDidLoad )
+ #endif
+}
+
+void function AiSpawn_EntitiesDidLoad()
+{
+ #if DEV
+ // On load in dev, verify that subclass matches leveled_aisettings. Subclass is being eradicated.
+ foreach ( spawner in GetSpawnerArrayByClassName( "npc_titan" ) )
+ {
+ table spawnerKeyValues = spawner.GetSpawnEntityKeyValues()
+ if ( "model" in spawnerKeyValues )
+ {
+ switch ( spawnerKeyValues.model.tolower() )
+ {
+ case "models/titans/atlas/atlas_titan.mdl":
+ case "models/titans/ogre/ogre_titan.mdl":
+ case "models/titans/stryder/stryder_titan.mdl":
+ CodeWarning( "Titan has deprecated model at " + spawnerKeyValues.origin )
+ break
+ }
+ }
+ }
+
+ foreach ( model in GetEntArrayByClass_Expensive( "prop_dynamic" ) )
+ {
+ switch ( model.GetModelName() )
+ {
+ case $"models/titans/atlas/atlas_titan.mdl":
+ case $"models/titans/ogre/ogre_titan.mdl":
+ case $"models/titans/stryder/stryder_titan.mdl":
+ CodeWarning( "Prop has deprecated model at " + model.GetOrigin() )
+ break
+ }
+ }
+
+ if ( IsSingleplayer() )
+ {
+ foreach ( spawner in GetSpawnerArrayByClassName( "npc_titan" ) )
+ {
+ table kvs = spawner.GetSpawnEntityKeyValues()
+ vector origin = StringToVector( expect string( kvs.origin ) )
+ if ( !( "leveled_aisettings" in kvs ) )
+ {
+ CodeWarning( "Titan Spawner at " + origin + " has no leveled_aisettings" )
+ continue
+ }
+
+ string aiSettings = expect string( kvs.leveled_aisettings )
+ string playerSettings = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" ) )
+ string playerModel = expect string( GetPlayerSettingsFieldForClassName( playerSettings, "bodymodel" ) )
+ string npcModel = expect string( kvs.model )
+ if ( npcModel != playerModel )
+ CodeWarning( "Titan spawner at " + origin + " has model " + npcModel + " that does not match player settings model " + playerModel )
+ }
+ }
+
+ #endif
+
+ table<entity, bool> foundSpawners
+ // precache weapons from the AI
+ foreach ( aiSettings in GetAllNPCSettings() )
+ {
+ // any of these spawned in the level?
+ string baseClass = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "BaseClass" ) )
+ array<entity> spawners = GetSpawnerArrayByClassName( baseClass )
+
+ foreach ( spawner in spawners )
+ {
+ if ( spawner in foundSpawners )
+ continue
+ foundSpawners[ spawner ] <- true
+ // this may be set on the entity in leveled
+ table kvs = spawner.GetSpawnEntityKeyValues()
+ if ( !( "subclass" in kvs ) )
+ continue
+
+ string origin = expect string( spawner.GetSpawnEntityKeyValues().origin )
+ string subclass = expect string( spawner.GetSpawnEntityKeyValues().subclass )
+ CodeWarning( "NPC spawner at " + origin + " has subclass " + subclass + ". Replace deprecated subclass key with leveled_aisettings." )
+ }
+ }
+
+}
+
+const ESCALATION_INCOMBAT_TIMEOUT = 180
+const ESCALATION_FRACTION_DEAD = 0.5
+
+
+/************************************************************************************************\
+
+######## #### ######## ### ## ##
+ ## ## ## ## ## ### ##
+ ## ## ## ## ## #### ##
+ ## ## ## ## ## ## ## ##
+ ## ## ## ######### ## ####
+ ## ## ## ## ## ## ###
+ ## #### ## ## ## ## ##
+
+\************************************************************************************************/
+
+//////////////////////////////////////////////////////////
+
+entity function CreateHenchTitan( string titanType, vector origin, vector angles )
+{
+ entity npc = CreateNPCTitan( titanType, TEAM_IMC, origin, angles, [] )
+ string settings = expect string( Dev_GetPlayerSettingByKeyField_Global( titanType, "sp_aiSettingsFile" ) )
+ SetSpawnOption_AISettings( npc, settings )
+ SetSpawnOption_Titanfall( npc )
+ SetSpawnOption_Alert( npc )
+ SetSpawnOption_NPCTitan( npc, TITAN_HENCH )
+ npc.ai.titanSpawnLoadout.setFile = titanType
+ OverwriteLoadoutWithDefaultsForSetFile( npc.ai.titanSpawnLoadout )
+ return npc
+}
+
+entity function CreateAtlas( int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateNPCTitan( "titan_atlas", team, origin, angles, settingsMods )
+ SetSpawnOption_AISettings( npc, "npc_titan_atlas" )
+ return npc
+}
+
+entity function CreateStryder( int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateNPCTitan( "titan_stryder", team, origin, angles, settingsMods )
+ SetSpawnOption_AISettings( npc, "npc_titan_stryder" )
+ return npc
+}
+
+entity function CreateOgre( int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateNPCTitan( "titan_ogre", team, origin, angles, settingsMods )
+ SetSpawnOption_AISettings( npc, "npc_titan_ogre" )
+ return npc
+}
+
+entity function CreateArcTitan( int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateNPCTitan( "titan_stryder", team, origin, angles, settingsMods )
+ SetSpawnOption_AISettings( npc, "npc_titan_arc" )
+ return npc
+}
+
+entity function CreateNPCTitan( string settings, int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateEntity( "npc_titan" )
+ npc.kv.origin = origin
+ npc.kv.angles = Vector( 0, angles.y, 0 )
+ npc.kv.teamnumber = team
+ SetTitanSettings( npc.ai.titanSettings, settings, settingsMods )
+ return npc
+}
+
+entity function CreateSpectre( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_spectre", team, origin, angles )
+}
+
+entity function CreateStalker( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_stalker", team, origin, angles )
+}
+
+entity function CreateZombieStalker( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_stalker", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_stalker_zombie" )
+ return npc
+}
+
+entity function CreateZombieStalkerMossy( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_stalker", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_stalker_zombie_mossy" )
+ return npc
+}
+
+entity function CreateSuperSpectre( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_super_spectre", team, origin, angles )
+}
+
+entity function CreateGunship( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_gunship", team, origin, angles )
+}
+
+entity function CreateSoldier( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_soldier", team, origin, angles )
+}
+
+entity function CreateProwler( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_prowler", team, origin, angles )
+}
+
+entity function CreateRocketDroneGrunt( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_soldier", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_soldier_drone_summoner_rocket" )
+ return npc
+}
+
+entity function CreateShieldDroneGrunt( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_soldier", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_soldier_drone_summoner" )
+ return npc
+}
+
+entity function CreateElitePilot( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_pilot_elite", team, origin, angles )
+}
+
+entity function CreateElitePilotAssassin( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_pilot_elite", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_pilot_elite_assassin" )
+ return npc
+}
+
+entity function CreateFragDrone( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_frag_drone", team, origin, angles )
+}
+
+entity function CreateFragDroneCan( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_frag_drone", team, origin, angles )
+ npc.ai.fragDroneArmed = false
+ return npc
+}
+
+entity function CreateRocketDrone( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_drone", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_drone_rocket" )
+ return npc
+}
+
+entity function CreateShieldDrone( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_drone", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_drone_shield" )
+ return npc
+}
+
+entity function CreateGenericDrone( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_drone", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_drone" )
+ return npc
+}
+
+entity function CreateWorkerDrone( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_drone", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_drone_worker" )
+ return npc
+}
+
+entity function CreateMarvin( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_marvin", team, origin, angles )
+}
+
+entity function CreateNPC( baseClass, team, origin, angles )
+{
+ entity npc = CreateEntity( expect string( baseClass ) )
+ npc.kv.teamnumber = team
+ npc.kv.origin = origin
+ npc.kv.angles = angles
+
+ return npc
+}
+
+entity function CreateNPCFromAISettings( string aiSettings, int team, vector origin, vector angles )
+{
+ string baseClass = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "BaseClass" ) )
+ entity npc = CreateNPC( baseClass, team, origin, angles )
+ SetSpawnOption_AISettings( npc, aiSettings )
+ return npc
+}
+
+
+
+/************************************************************************************************\
+
+ ###### ####### ## ## ## ## ####### ## ##
+## ## ## ## ### ### ### ### ## ## ### ##
+## ## ## #### #### #### #### ## ## #### ##
+## ## ## ## ### ## ## ### ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ####
+## ## ## ## ## ## ## ## ## ## ## ###
+ ###### ####### ## ## ## ## ####### ## ##
+
+\************************************************************************************************/
+
+entity function GetTargetOrLink( entity npc )
+{
+ string target = npc.GetTarget_Deprecated()
+ if ( target != "" )
+ return GetEnt( target )
+
+ array<entity> links = npc.GetLinkEntArray()
+ if ( links.len() )
+ return links.getrandom()
+
+ return null
+}
+
+bool function IsMoveTarget( entity ent )
+{
+ if ( !ent.HasKey( "editorclass" ) )
+ return false
+
+ string editorClass = expect string( ent.kv.editorclass )
+ foreach ( moveTargetClass in file.moveTargetClasses )
+ {
+ if ( editorClass == moveTargetClass )
+ return true
+ }
+ return false
+}
+
+bool function IsPotentialThreatTarget( entity ent )
+{
+ if ( !ent.HasKey( "editorclass" ) )
+ return false
+
+ string editorClass = expect string( ent.kv.editorclass )
+ if ( editorClass == "info_potential_threat_target" )
+ return true
+
+ return false
+}
+
+function AssaultLinkedMoveTarget( entity npc )
+{
+ entity ent = GetTargetOrLink( npc )
+ if ( ent == null )
+ return
+ if ( !IsMoveTarget( ent ) )
+ return
+
+ AssaultMoveTarget( npc, ent )
+}
+
+function AssaultMoveTarget( entity npc, entity ent )
+{
+ npc.EndSignal( "OnDeath" )
+ npc.EndSignal( "OnDestroy" )
+ npc.EndSignal( "StopAssaultMoveTarget" )
+ ent.EndSignal( "OnDestroy" )
+
+ Assert( IsMoveTarget( ent ) )
+
+ OnThreadEnd(
+ function() : ( npc )
+ {
+ if ( IsAlive( npc ) )
+ {
+ Signal( npc, "OnFinishedAssaultChain" )
+ }
+ }
+ )
+
+ for ( ;; )
+ {
+ vector origin = ent.GetOrigin()
+ vector angles = ent.GetAngles()
+ float radius = 750
+ float height = 750
+
+ if ( ent.HasKey( "script_predelay" ) )
+ {
+ float time = float( ent.GetValueForKey( "script_predelay" ) )
+ if ( time > 0.0 )
+ wait time
+ }
+
+ if ( ent.HasKey( "script_goal_radius" ) )
+ radius = float( ent.kv.script_goal_radius )
+
+ if ( ent.HasKey( "script_goal_height" ) )
+ height = float( ent.kv.script_goal_height )
+
+ npc.AssaultPointClamped( origin )
+ npc.AssaultSetGoalRadius( radius )
+ npc.AssaultSetGoalHeight( height )
+
+ if ( ent.HasKey( "face_angles" ) && ent.kv.face_angles == "1" )
+ npc.AssaultSetAngles( angles, true )
+
+ if ( ent.HasKey( "script_fight_radius" ) )
+ {
+ float fightRadius = float( ent.kv.script_fight_radius )
+ npc.AssaultSetFightRadius( fightRadius )
+ }
+
+ if ( npc.IsLinkedToEnt( ent ) && ent.HasKey( "unlink" ) && ent.kv.unlink == "1" )
+ npc.UnlinkFromEnt( ent )
+
+ array<entity> entChildren = ent.GetLinkEntArray()
+
+ bool finalDestination = entChildren.len() == 0
+ npc.AssaultSetFinalDestination( finalDestination ) // this doesn't seem to make any difference as far as I can tell. Bug #117062
+
+ if ( ent.HasKey( "clear_potential_threat_pos" ) && int( ent.kv.clear_potential_threat_pos ) == 1 )
+ npc.ClearPotentialThreatPos()
+
+ foreach ( ent in entChildren )
+ {
+ if ( IsPotentialThreatTarget( ent ) )
+ {
+ npc.SetPotentialThreatPos( ent.GetOrigin() )
+ break
+ }
+ }
+
+ table results
+
+ bool skipRunto = ent.HasKey( "skip_runto" ) && int( ent.kv.skip_runto ) == 1
+ if ( !skipRunto )
+ {
+ // If pathing fails we retry waiting for the other signals for 3 seconds.
+ // This solves an issue with npc that failed to path because they where falling.
+
+ const float RETRY_TIME = 3.0
+ float waitStartTime = Time()
+
+ while( true )
+ {
+ // activator, caller, self, signal, value
+ results = WaitSignal( npc, "OnFinishedAssault", "OnEnterGoalRadius", "OnFailedToPath" )
+
+ if ( results.signal != "OnFailedToPath" || waitStartTime + RETRY_TIME < Time() )
+ break
+ }
+ }
+
+ if ( ent.HasKey( "scr_signal" ) )
+ Signal( npc, ent.GetValueForKey( "scr_signal" ), { nodeSignal = results.signal, node = ent } )
+
+ if ( ent.HasKey( "leveled_animation" ) )
+ {
+ string animation = expect string( ent.kv.leveled_animation )
+ Assert( npc.Anim_HasSequence( animation ), "Npc " + npc + " with model " + npc.GetModelName() + " does not have animation sequence " + animation )
+ if ( skipRunto )
+ waitthread PlayAnimTeleport( npc, animation, ent )
+ else
+ waitthread PlayAnimRun( npc, animation, ent, false )
+ }
+
+ if ( ent.HasKey( "scr_flag_set" ) )
+ FlagSet( ent.GetValueForKey( "scr_flag_set" ) )
+
+ if ( ent.HasKey( "scr_flag_clear" ) )
+ FlagClear( ent.GetValueForKey( "scr_flag_clear" ) )
+
+ if ( ent.HasKey( "scr_flag_wait" ) )
+ FlagWait( ent.GetValueForKey( "scr_flag_wait" ) )
+
+ if ( ent.HasKey( "scr_flag_wait_clear" ) )
+ FlagWaitClear( ent.GetValueForKey( "scr_flag_wait_clear" ) )
+
+ if ( ent.HasKey( "path_wait" ) )
+ {
+ float time = float( ent.GetValueForKey( "path_wait" ) )
+ if ( time > 0.0 )
+ wait time
+ }
+
+ if ( ent.HasKey( "disable_assault_on_goal" ) && int( ent.kv.disable_assault_on_goal ) == 1 )
+ npc.DisableBehavior( "Assault" )
+
+ if ( entChildren.len() == 0 )
+ return
+
+ entChildren.randomize()
+ ent = null
+ foreach ( child in entChildren )
+ {
+ if ( IsMoveTarget( child ) )
+ {
+ ent = child
+ break
+ }
+ }
+
+ if ( ent == null )
+ return
+ }
+}
+
+void function StopAssaultMoveTarget( entity npc )
+{
+ npc.Signal( "StopAssaultMoveTarget" )
+}
+
+void function InitInfoMoveTargetFlags( entity infoMoveTarget )
+{
+ #if DEV
+ if ( infoMoveTarget.HasKey( "script_goal_radius" ) )
+ {
+ int radius = int( infoMoveTarget.kv.script_goal_radius )
+ if ( radius < 64 )
+ CodeWarning( "move target at " + infoMoveTarget.GetOrigin() + " had goal radius " + radius + " which is less than minimum 64" )
+ }
+ #endif
+ if ( infoMoveTarget.HasKey( "scr_flag_set" ) )
+ FlagInit( infoMoveTarget.GetValueForKey( "scr_flag_set" ) )
+ if ( infoMoveTarget.HasKey( "scr_flag_clear" ) )
+ FlagInit( infoMoveTarget.GetValueForKey( "scr_flag_clear" ) )
+ if ( infoMoveTarget.HasKey( "scr_flag_wait" ) )
+ FlagInit( infoMoveTarget.GetValueForKey( "scr_flag_wait" ) )
+ if ( infoMoveTarget.HasKey( "scr_flag_wait_clear" ) )
+ FlagInit( infoMoveTarget.GetValueForKey( "scr_flag_wait_clear" ) )
+
+ if ( infoMoveTarget.HasKey( "scr_signal" ) )
+ RegisterSignal( infoMoveTarget.GetValueForKey( "scr_signal" ) )
+}
+
+/************************************************************************************************\
+
+######## ####### ####### ## ######
+ ## ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ######
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ## ##
+ ## ####### ####### ######## ######
+
+\************************************************************************************************/
+asset function __GetWeaponModel( weapon )
+{
+ switch ( weapon )
+ {
+ case "mp_weapon_rspn101":
+ return $"models/weapons/rspn101/r101_ab_01.mdl"//$"models/weapons/rspn101/w_rspn101.mdl" --> this is the one I want to spawn, but I get a vague code error when I try
+ break
+
+ default:
+ Assert( 0, "weapon: " + weapon + " not handled to return a model" )
+ break
+ }
+ unreachable
+}
+
+void function AutoSquadnameAssignment( entity npc )
+{
+ int team = npc.GetTeam()
+ switch ( npc.GetClassName() )
+ {
+ case "npc_turret_sentry":
+ case "npc_turret_mega":
+ case "npc_dropship":
+ case "npc_dropship_hero":
+ return
+ }
+
+ switch ( npc.GetTeam() )
+ {
+ case TEAM_IMC:
+ case TEAM_MILITIA:
+ int index = svGlobal.npcsSpawnedThisFrame_scriptManagedArray[ team ]
+ if ( GetScriptManagedEntArrayLen( index ) == 0 )
+ {
+ thread AutosquadnameAssignment_Thread( index, npc, team )
+ }
+
+ AddToScriptManagedEntArray( index, npc )
+ break
+
+ default:
+ break
+ }
+}
+
+void function AutosquadnameAssignment_Thread( int scriptManagedArrayIndex, entity npc, int team )
+{
+ WaitEndFrame() // wait for everybody to spawn this frame
+
+ array<entity> entities = GetScriptManagedEntArray( scriptManagedArrayIndex )
+ if ( entities.len() <= 1 )
+ {
+ foreach ( npc in entities )
+ {
+ RemoveFromScriptManagedEntArray( scriptManagedArrayIndex, npc )
+ }
+ return
+ }
+
+ string squadName = UniqueString( "autosquad_team_" + team )
+
+ foreach ( npc in entities )
+ {
+ RemoveFromScriptManagedEntArray( scriptManagedArrayIndex, npc )
+ if ( !IsValid( npc ) )
+ continue
+ if ( npc.kv.squadname != "" )
+ continue
+ if ( !IsAlive( npc ) )
+ continue
+ SetSquad( npc, squadName )
+ }
+ Assert( GetScriptManagedEntArrayLen( scriptManagedArrayIndex ) == 0 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn_content.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn_content.gnut
new file mode 100644
index 00000000..c6e7f9f4
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn_content.gnut
@@ -0,0 +1,879 @@
+untyped
+
+global const PROTOTYPE_DEFAULT_TITAN_RODEO_SLOTS = 3 // Todo: remove and set this in titan_base.set
+
+global function CommonNPCOnSpawned
+global function ShouldSpawn
+global function AiSpawnContent_Init
+global function FixupTitle
+
+struct
+{
+ array<string> pilotAntiTitanWeapons
+ int nextAntiTitanWeaponAutoAssign
+} file
+
+function AiSpawnContent_Init()
+{
+ RegisterSignal( "Stop_SimulateGrenadeThink" )
+
+ if ( IsMultiplayer() )
+ file.pilotAntiTitanWeapons = [ "mp_weapon_rocket_launcher" ]
+
+ #if DEV
+ if ( IsSingleplayer() )
+ {
+ array<string> aiSettings = GetAllowedTitanAISettings()
+ foreach ( npcSettings in aiSettings )
+ {
+ asset npcModel = Dev_GetAISettingAssetByKeyField_Global( npcSettings, "DefaultModelName" )
+ string playerSettings = expect string( Dev_GetAISettingByKeyField_Global( npcSettings, "npc_titan_player_settings" ) )
+ asset playerModel = GetPlayerSettingsAssetForClassName( playerSettings, "bodymodel" )
+
+ Assert( npcModel == playerModel, "NPC settings " + npcSettings + " has model " + npcModel + ", which does not match player model for same titan " + playerModel )
+ }
+ }
+ #endif
+}
+
+
+function CommonNPCOnSpawned( entity npc )
+{
+ npc.ai.spawnTime = Time()
+ npc.ai.spawnOrigin = npc.GetOrigin()
+
+ if ( npc.HasKey( "script_goal_radius" ) )
+ {
+ var radius = npc.kv.script_goal_radius
+ if ( radius != null && radius != "" )
+ {
+ npc.AssaultSetGoalRadius( int( radius ) )
+ npc.AssaultPoint( npc.GetOrigin() )
+ }
+ }
+
+ if ( npc.HasKey( "script_goal_height" ) )
+ {
+ var height = npc.kv.script_goal_height
+ if ( height != null && height != "" )
+ {
+ npc.AssaultSetGoalHeight( int( height ) )
+ }
+ }
+
+ if ( npc.HasKey( "script_flag_killed" ) )
+ {
+ thread SetupFlagKilledForNPC( npc )
+ }
+
+ string aisetting = GetDefaultAISetting( npc )
+
+ SetAISettingsWrapper( npc, aisetting )
+
+ Assert( !npc.executedSpawnOptions, npc + " tried to spawn twice?" )
+ npc.executedSpawnOptions = true
+
+ if ( npc.Dev_GetAISettingByKeyField( "SpawnLimping" ) )
+ npc.SetActivityModifier( ACT_MODIFIER_STAGGER, true )
+
+ InitHighlightSettings( npc )
+
+ if ( npc.Dev_GetAISettingByKeyField( "DrawTargetHealthBar" ) )
+ npc.SetValidHealthBarTarget( true )
+
+ // baseclass logic
+ if ( npc.IsTitan() )
+ {
+ if ( !SpawnWithoutSoul( npc ) )
+ {
+ CreateTitanSoul( npc )
+ }
+ }
+
+ if ( npc.GetTeam() <= 0 )
+ {
+ SetTeam( npc, expect int( npc.kv.teamnumber.tointeger() ) )
+ }
+
+ if ( IsMinion( npc ) )
+ {
+ SetupMinionForRPGs( npc )
+ CommonMinionInit( npc )
+ }
+ else if ( !npc.IsTitan() )
+ {
+ npc.SetEnemyChangeCallback( OnEnemyChanged_MinionUpdateAimSettingsForEnemy )
+ }
+
+ if ( npc.GetTitle() == "" )
+ {
+ var title = npc.GetSettingTitle()
+ if ( title != null && title != "" )
+ npc.SetTitle( title )
+ }
+
+ // start alert
+ if ( npc.mySpawnOptions_alert != null )
+ npc.kv.alwaysalert = npc.mySpawnOptions_alert
+ else if ( npc.HasKey( "start_alert") )
+ npc.kv.alwaysalert = npc.kv.start_alert
+
+ npc.kv.physdamagescale = 1.0
+
+ if ( npc.HasKey( "script_buddha" ) && npc.kv.script_buddha == "1" )
+ {
+ npc.ai.buddhaMode = true
+ }
+
+ if ( npc.IsTitan() )
+ {
+ // set boss titan type before setting proficiency for Titans
+ if ( npc.HasKey( "TitanType" ) )
+ {
+ npc.ai.bossTitanType = int( npc.kv.TitanType )
+
+ // this is to get rid of all weak titans
+ if ( npc.ai.bossTitanType == TITAN_WEAK )
+ {
+ CodeWarning( "Spawned weak Titan at " + npc.GetOrigin() + ". Change TitanType to Henchman Titan." )
+
+ // GetSettingsTitle() is causing a script error. Removed for now.
+ // CodeWarning( "Spawned weak Titan " + npc.GetSettingsTitle() + " at " + npc.GetOrigin() + ". Change TitanType to Henchman Titan." )
+ npc.ai.bossTitanType = TITAN_HENCH
+ }
+ }
+
+ if ( npc.HasKey( "disable_vdu" ) )
+ npc.ai.bossTitanVDUEnabled = int( npc.kv.disable_vdu ) == 0
+ }
+
+ // Set proficiency before giving weapons
+ SPMP_UpdateNPCProficiency( npc )
+
+ if ( npc.IsTitan() )
+ {
+ UpdateTitanMinimapStatusToOtherPlayers( npc )
+ CommonNPCTitanOnSpawned( npc )
+// Assert( npc.Dev_GetAISettingByKeyField( "footstep_type" ) != "", "NPC " + npc + " has no footstep type set" )
+ }
+ else
+ {
+ UpdateAIMinimapStatusToOtherPlayers( npc )
+
+ if ( npc.ai.mySpawnOptions_weapon != null )
+ {
+ array<entity> weapons = npc.GetMainWeapons()
+ TakeWeaponsForArray( npc, weapons )
+
+ NPCDefaultWeapon spawnoptionsweapon = expect NPCDefaultWeapon( npc.ai.mySpawnOptions_weapon )
+ npc.GiveWeapon( spawnoptionsweapon.wep, spawnoptionsweapon.mods )
+ }
+
+ entity weapon = npc.GetActiveWeapon()
+ if ( weapon != null && weapon.GetWeaponType() == WT_SIDEARM )
+ npc.DisableNPCFlag( NPC_CROUCH_COMBAT )
+ }
+
+ if ( npc.HasKey( "drop_battery" ) )
+ {
+ npc.ai.shouldDropBattery = (npc.kv.drop_battery == "1")
+ }
+
+ switch ( npc.GetClassName() )
+ {
+ case "npc_bullseye":
+ npc.NotSolid()
+ npc.SetInvulnerable()
+ break
+
+ case "npc_drone":
+ InitMinimapSettings( npc )
+
+ if ( GetMarvinType( npc ) == "marvin_type_drone" )
+ {
+ thread MarvinJobThink( npc )
+ return
+ }
+
+ npc.s.rebooting <- null
+ npc.ai.preventOwnerDamage = true
+ npc.s.lastSmokeDeployTime <- Time()
+
+ thread RunDroneTypeThink( npc )
+
+ switch ( GetDroneType( npc ) )
+ {
+ case "drone_type_engineer_combat":
+ npc.kv.rendercolor = "0 0 0"
+ break
+
+ case "drone_type_engineer_shield":
+ npc.kv.rendercolor = "255 255 255"
+ break
+ }
+ break
+
+ case "npc_dropship":
+ npc.SetSkin( 1 ) //Use skin where the lights are on for dropship.
+ npc.EnableRenderAlways()
+ npc.SetAimAssistAllowed( false )
+ //npc.kv.CollisionGroup = TRACE_COLLISION_GROUP_BLOCK_WEAPONS
+ AddAnimEvent( npc, "dropship_warpout", WarpoutEffect )
+
+ InitLeanDropship( npc )
+ break
+
+
+ case "npc_frag_drone":
+ MakeSuicideSpectre( npc )
+ break
+
+ case "npc_gunship":
+ InitMinimapSettings( npc )
+ EmitSoundOnEntity( npc, SOUND_GUNSHIP_HOVER )
+
+ npc.ai.preventOwnerDamage = true
+ npc.s.rebooting <- null
+ npc.s.plantedMinesManagedEntArrayID <- CreateScriptManagedEntArray()
+
+ npc.kv.crashOnDeath = false
+ //npc.kv.secondaryWeaponName = "mp_weapon_gunship_missile"
+
+ EnableLeeching( npc )
+ npc.SetUsableByGroup( "enemies pilot" )
+
+ thread GunshipThink( npc )
+ break
+
+ case "npc_marvin":
+ asset model = npc.GetModelName()
+ npc.EnableNPCFlag( NPC_DISABLE_SENSING ) // don't do traces to look for enemies or players
+ thread MarvinFace( npc )
+ thread MarvinJobThink( npc )
+ break
+
+ case "npc_pilot_elite":
+ npc.kv.physdamagescale = 1.0
+ npc.kv.WeaponProficiency = eWeaponProficiency.VERYGOOD
+ break
+
+ case "npc_prowler":
+ npc.kv.disengageEnemyDist = 1500
+ npc.DisableNPCFlag( NPC_ALLOW_FLEE ) //HACK until we get a way to make last guy not run away and hide
+ //SetSquad( npc, spawnOptions.squadName ) //not sure why this is here - jake had it in his original spawn func, so I'm keeping it
+ //SetNPCSquadMode( spawnOptions.squadName, SQUAD_MODE_MULTIPRONGED_ATTACK )
+ break
+
+ case "npc_soldier":
+
+ InitMinimapSettings( npc )
+
+ SetHumanRagdollImpactTable( npc )
+
+ npc.EnableNPCFlag( NPC_CROUCH_COMBAT )
+
+ thread OnSoldierSeeEnemy( npc )
+ thread TryFriendlyPassingNearby( npc )
+
+ int team = npc.GetTeam()
+
+ //grunt specific
+ npc.SetDoFaceAnimations( true ) //HACK: assumption that militia are the only grunt models with faces ( will need a better thing for R2 )
+
+ bool alreadyGaveASecondary = false;
+ entity weapon = npc.GetActiveWeapon()
+ string weaponSubClass
+ if ( weapon )
+ weaponSubClass = string( weapon.GetWeaponInfoFileKeyField( "weaponSubClass" ) )
+
+ #if SP
+ if ( weaponSubClass == "sniper" )
+ {
+ if ( AssignDefaultNPCSidearm( npc ) )
+ alreadyGaveASecondary = true
+ }
+ #endif
+
+ if ( !alreadyGaveASecondary && SP_GetPilotAntiTitanWeapon( npc ) == null )
+ TryAutoAssignAntiTitanWeapon( npc )
+
+ if ( npc.Dev_GetAISettingByKeyField( "PersonalShield" ) != null )
+ {
+ npc.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS | NPC_USE_SHOOTING_COVER | NPC_CROUCH_COMBAT )
+ thread ActivatePersonalShield( npc )
+ }
+
+ if ( npc.ai.droneSpawnAISettings != "" )
+ {
+ thread DroneGruntThink( npc, npc.ai.droneSpawnAISettings )
+ }
+
+ AssignGruntModelForWeaponClass( npc, weapon, weaponSubClass )
+
+ break
+
+ case "npc_spectre":
+ InitMinimapSettings( npc )
+ thread OnSpectreSeeEnemy( npc )
+
+ if ( IsMultiplayer() )
+ {
+ npc.EnableNPCFlag( NPC_CROUCH_COMBAT )
+ //Only enable spectre hacking if the playlist var is enabled
+ if ( ( npc.GetTeam() == TEAM_IMC || npc.GetTeam() == TEAM_MILITIA ) && GetCurrentPlaylistVarInt( "enable_spectre_hacking", 0 ) == 1 )
+ {
+ EnableLeeching( npc )
+ npc.SetUsableByGroup( "enemies pilot" )
+ }
+ }
+ else
+ {
+ EnableLeeching( npc )
+ npc.SetUsableByGroup( "enemies pilot" )
+
+ if ( npc.HasKey( "carrying_battery" ) )
+ {
+ if ( npc.kv.carrying_battery == "1" )
+ {
+ thread NPCCarriesBattery( npc )
+ }
+ }
+ }
+
+ if ( SP_GetPilotAntiTitanWeapon( npc ) == null )
+ TryAutoAssignAntiTitanWeapon( npc )
+ break
+
+ case "npc_stalker":
+ InitMinimapSettings( npc )
+
+ if ( IsSingleplayer() && npc.kv.squadname != "" )
+ SetNPCSquadMode( npc.kv.squadname, SQUAD_MODE_MULTIPRONGED_ATTACK )
+
+ break
+
+ case "npc_super_spectre":
+
+ InitMinimapSettings( npc )
+
+ npc.GiveOffhandWeapon( "mp_weapon_spectre_spawner", 0 )
+
+ DisableLeeching( npc )
+
+ npc.SetCapabilityFlag( bits_CAP_NO_HIT_SQUADMATES, false )
+
+ npc.ai.preventOwnerDamage = true
+
+ npc.SetDeathNotifications( true )
+
+ AddAnimEvent( npc, "SuperSpectre_OnGroundSlamImpact", SuperSpectre_OnGroundSlamImpact )
+ AddAnimEvent( npc, "SuperSpectre_OnGroundLandImpact", SuperSpectre_OnGroundLandImpact )
+
+ thread SuperSpectreThink( npc )
+
+ SuperSpectreIntro( npc )
+ break
+
+
+ case "npc_titan":
+ InitMinimapSettings( npc )
+
+ // used so the titan can stand/kneel without cutting off functionality
+ npc.s.standQueued <- false
+ npc.ai.preventOwnerDamage = true
+ if ( IsMultiplayer() )
+ {
+ npc.e.hasDefaultEnemyHighlight = true
+ SetDefaultMPEnemyHighlight( npc )
+ }
+ break
+
+
+ case "npc_turret_mega":
+ InitMinimapSettings( npc )
+ npc.EnableNPCFlag( NPC_AIM_DIRECT_AT_ENEMY )
+ npc.SetAimAssistAllowed( false )
+ #if R1_VGUI_MINIMAP
+ npc.Minimap_SetDefaultMaterial( GetMinimapMaterial( "turret_neutral" ) )
+ npc.Minimap_SetFriendlyMaterial( GetMinimapMaterial( "turret_friendly" ) )
+ npc.Minimap_SetEnemyMaterial( GetMinimapMaterial( "turret_enemy" ) )
+ npc.Minimap_SetBossPlayerMaterial( GetMinimapMaterial( "turret_friendly" ) )
+ #endif
+ break
+
+ case "npc_turret_sentry":
+ InitMinimapSettings( npc )
+ npc.SetAimAssistAllowed( false )
+ break
+
+ }
+
+ thread AssaultLinkedMoveTarget( npc )
+
+ FixupTitle( npc )
+ #if DEV
+ // stop all the wandering in sp_enemies.
+ if ( GetMapName() == "sp_enemies" )
+ npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+ #endif
+}
+
+void function FixupTitle( entity npc )
+{
+ if ( IsMultiplayer() )
+ return
+
+ if ( !npc.IsTitan() )
+ npc.SetTitle( "" )
+ /*
+ if ( npc.GetTitle() == "" )
+ return
+ switch ( npc.GetTeam() )
+ {
+ case TEAM_UNASSIGNED:
+ case TEAM_MILITIA:
+ break
+ default:
+ npc.SetTitle( "" )
+ break
+ }
+ */
+}
+
+var function GetTitanHotdropSetting( entity npc )
+{
+ if ( npc.mySpawnOptions_titanfallSpawn != null )
+ return "titanfall"
+ if ( npc.mySpawnOptions_warpfallSpawn != null )
+ return "warpfall"
+
+ if ( npc.HasKey( "script_hotdrop" ) )
+ {
+ switch ( npc.kv.script_hotdrop )
+ {
+ case "0":
+ return null
+ case "3":
+ case "1":
+ return "titanfall"
+ case "4":
+ case "2":
+ return "warpfall"
+ }
+ }
+
+ return null
+}
+
+function CommonNPCTitanOnSpawned( entity npc )
+{
+ if ( npc.ai.titanSpawnLoadout.primary != "" )
+ {
+ Assert( npc.GetMainWeapons().len() == 0 )
+
+ // if designer overwrites weapons, apply them
+ GiveTitanLoadout( npc, npc.ai.titanSpawnLoadout )
+ }
+ else if ( npc.GetMainWeapons().len() == 0 )
+ {
+ GiveTitanLoadout( npc, npc.ai.titanSpawnLoadout )
+ }
+
+ //Assert( npc.ai.titanSpawnLoadout.setFile == npc.ai.titanSettings.titanSetFile )
+ string playerSettings = expect string( npc.Dev_GetAISettingByKeyField( "npc_titan_player_settings" ) )
+ asset modelName = GetPlayerSettingsAssetForClassName( playerSettings, "bodymodel" )
+ if ( npc.GetModelName() != modelName )
+ npc.SetModel( modelName )
+// Assert( npc.GetModelName() == modelName )
+
+ int camoIndex = GetTitanCamoIndexFromLoadoutAndPrimeStatus( npc.ai.titanSpawnLoadout )
+ int skinIndex = GetTitanSkinIndexFromLoadoutAndPrimeStatus( npc.ai.titanSpawnLoadout )
+ int decalIndex = GetTitanDecalIndexFromLoadoutAndPrimeStatus ( npc.ai.titanSpawnLoadout )
+
+ if ( camoIndex > 0 )
+ {
+ npc.SetSkin( TITAN_SKIN_INDEX_CAMO )
+ npc.SetCamo( camoIndex )
+ }
+ else
+ {
+ int skin
+ if ( npc.HasKey( "modelskin" ) )
+ skin = expect int( npc.kv.modelskin.tointeger() )
+
+ if ( skinIndex > 0 )
+ {
+ Assert( skin == 0, "Both npc.kv.modelskin and skinIndex were > 0. Pick one." )
+ skin = skinIndex
+ }
+
+ if ( skin > 0 )
+ npc.SetSkin( skin )
+ }
+
+ npc.SetDecal( decalIndex )
+
+ #if HAS_BOSS_AI
+ if ( IsMercTitan( npc ) )
+ {
+ array<entity> weapons = GetPrimaryWeapons( npc )
+ Assert( weapons.len() == 1 )
+ string character = GetMercCharacterForWeapon( weapons[0].GetWeaponClassName() )
+ npc.ai.bossCharacterName = character
+ npc.ai.mercCharacterID = GetBossTitanID( character )
+
+ int id = GetBossTitanID( character )
+ string title = GetBossTitleFromID( id )
+
+ npc.SetTitle( title )
+ }
+ #endif
+
+ // force sp titans to use specific loadouts
+ if ( !IsMultiplayer() )
+ ResetTitanLoadoutFromPrimary( npc )
+
+ npc.EnableNPCFlag( NPC_NO_MOVING_PLATFORM_DEATH )
+ //npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+
+ if ( IsMultiplayer() )
+ {
+ npc.kv.alwaysalert = 1
+ }
+ else
+ {
+ if ( npc.GetAIClass() == AIC_TITAN_BUDDY )
+ {
+ npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+ array<entity> enemies = GetNPCArrayEx( "any", TEAM_ANY, npc.GetTeam(), npc.GetOrigin(), 4000 )
+ if ( enemies.len() > 0 )
+ npc.SetAlert()
+
+ if ( npc.GetTitanSoul() )
+ {
+ // create buddy titan dialogue ent
+ // it will be transfered during embark and disembark automatically
+ entity dialogueEnt = CreateScriptMover()
+ dialogueEnt.DisableHibernation()
+ dialogueEnt.SetParent( npc, "HEADFOCUS", false, 0 )
+ npc.GetTitanSoul().SetTitanSoulNetEnt( "dialogueEnt", dialogueEnt )
+ }
+ }
+ }
+
+ local maxHealth = GetPlayerSettingsFieldForClassName_Health( npc.ai.titanSettings.titanSetFile ) //FD META - TO UPDATE with NPC equivalent of .GetPlayerModHealth()
+ if ( npc.ai.titanSpawnLoadout.setFileMods.contains( "fd_health_upgrade" ) )
+ maxHealth += 2500
+ //TEMP - GetPlayerSettingsFieldForClassName_Health doesn't return modded values.
+ if ( IsHardcoreGameMode() )
+ maxHealth *= 0.5
+
+ // this will override whatever health is set in the aisettings txt file.
+ npc.SetMaxHealth( maxHealth )
+ npc.SetHealth( maxHealth )
+ npc.SetValidHealthBarTarget( true )
+
+ #if HAS_BOSS_AI
+ UpdateMercTitanHealthForDifficulty( npc )
+ #endif
+
+ switch ( GetTitanHotdropSetting( npc ) )
+ {
+ case "titanfall":
+ thread NPCTitanHotdrops( npc, true )
+ break
+
+ case "warpfall":
+ thread NPCTitanHotdrops( npc, true, "at_hotdrop_drop_2knee_turbo_upgraded" )
+ break
+ }
+
+ // TODO: Have code allow us to put this in titan_base.set
+ npc.SetNumRodeoSlots( PROTOTYPE_DEFAULT_TITAN_RODEO_SLOTS )
+
+ if ( IsValid( npc.mySpawnOptions_ownerPlayer ) )
+ {
+ entity soul = npc.GetTitanSoul()
+ entity player = expect entity( npc.mySpawnOptions_ownerPlayer )
+
+ if ( IsValid( soul ) )
+ {
+ soul.soul.lastOwner = player
+ SoulBecomesOwnedByPlayer( soul, player )
+ }
+
+ SetupAutoTitan( npc, player )
+ }
+
+ if ( npc.HasKey( "disable_offhand_ordnance" ) )
+ {
+ if ( bool( npc.kv.disable_offhand_ordnance ) )
+ {
+ npc.TakeOffhandWeapon( OFFHAND_ORDNANCE )
+ }
+ }
+
+ if ( npc.HasKey( "disable_offhand_defense" ) )
+ {
+ if ( bool( npc.kv.disable_offhand_defense ) )
+ {
+ npc.TakeOffhandWeapon( OFFHAND_SPECIAL )
+ }
+ }
+
+ if ( npc.HasKey( "disable_offhand_tactical" ) )
+ {
+ if ( bool( npc.kv.disable_offhand_tactical ) )
+ {
+ entity weapon = npc.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ if ( weapon && weapon.GetWeaponClassName() == "mp_titanability_hover" )
+ npc.SetAllowSpecialJump( false )
+
+ npc.TakeOffhandWeapon( OFFHAND_ANTIRODEO )
+ }
+ }
+
+ if ( npc.HasKey( "disable_offhand_core" ) )
+ {
+ if ( bool( npc.kv.disable_offhand_core ) )
+ {
+ npc.TakeOffhandWeapon( OFFHAND_EQUIPMENT )
+ }
+ }
+
+ if ( npc.HasKey( "follow_mode" ) )
+ {
+ if ( bool( npc.kv.follow_mode ) )
+ {
+ entity player = GetPlayerArray()[0] // gross
+ int followBehavior = GetDefaultNPCFollowBehavior( npc )
+ npc.InitFollowBehavior( player, followBehavior )
+ npc.EnableBehavior( "Follow" )
+ npc.DisableBehavior( "Assault" )
+ }
+ }
+
+ var hasTraverse = npc.Dev_GetAISettingByKeyField( "can_traverse" )
+ if ( hasTraverse == null || expect int( hasTraverse ) == 0 )
+ {
+ npc.SetCapabilityFlag( bits_CAP_MOVE_TRAVERSE, false )
+ }
+
+ entity soul = npc.GetTitanSoul()
+ if ( IsValid( soul ) )
+ {
+ soul.soul.titanLoadout = npc.ai.titanSpawnLoadout
+ }
+}
+
+function ShouldSpawn( team, forced )
+{
+ //we're not allowed to spawn AI at all - return false
+ if ( !IsNPCSpawningEnabled() && !forced )
+ {
+ printt( "WARNING: tried to spawn an NPC but NPC Spawning is Disabled." )
+ return false
+ }
+ return true
+}
+
+
+function HACK_DroneGruntModel( grunt )
+{
+ string tag = "CHESTFOCUS"
+ int attachID = expect int( grunt.LookupAttachment( tag ) )
+ vector origin = expect vector( grunt.GetAttachmentOrigin( attachID ) )
+ vector angles = expect vector( grunt.GetAttachmentAngles( attachID ) )
+ vector forward = AnglesToForward( angles )
+ vector right = AnglesToRight( angles )
+ vector up = AnglesToUp( angles )
+
+ vector angles1 = AnglesCompose( angles, Vector( 0, -90, 90 ) )
+ vector origin1 = origin + ( forward * -4 ) + ( up * -1.5 )
+ entity back1 = CreatePropDynamic( HACK_DRONE_BACK1, origin1, angles1 )
+ back1.SetParent( grunt, tag, true, 0 )
+
+ vector angles2 = AnglesCompose( angles, Vector( 0, -90, 0 ) )
+ vector origin2 = origin + ( forward * -9 ) + ( up * 11 ) + ( right * -1 )
+ entity back2 = CreatePropDynamic( HACK_DRONE_BACK2, origin2, angles2 )
+ back2.SetParent( grunt, tag, true, 0 )
+}
+
+void function TryAutoAssignAntiTitanWeapon( entity npc )
+{
+ // disabling this while anti titan weapons settle down
+ if ( !IsMultiplayer() )
+ return
+
+ if ( file.pilotAntiTitanWeapons.len() == 0 )
+ return
+
+ Assert( !HasAntiTitanWeapon( npc ) )
+
+ // each 4th npc gets a rocket
+ file.nextAntiTitanWeaponAutoAssign--
+ if ( file.nextAntiTitanWeaponAutoAssign > 0 )
+ return
+
+ file.nextAntiTitanWeaponAutoAssign = 3
+
+ string weapon = file.pilotAntiTitanWeapons.getrandom()
+ npc.GiveWeapon( weapon )
+
+ if ( IsGrunt( npc ) )
+ {
+ // show rockets on the back
+ switch ( npc.GetTeam() )
+ {
+ case TEAM_IMC:
+ npc.SetModel( TEAM_IMC_GRUNT_MODEL_ROCKET )
+ break
+
+#if SP
+ case TEAM_MILITIA:
+ npc.SetModel( TEAM_MIL_GRUNT_MODEL_ROCKET )
+ break
+#endif
+ }
+ }
+}
+
+function SpawnWithoutSoul( ent )
+{
+ if ( ent.HasKey( "noSoul" ) )
+ {
+ return ent.kv.noSoul
+ }
+
+ return "spawnWithoutSoul" in ent.s
+}
+
+function DisableAimAssisst( self )
+{
+ self.SetAimAssistAllowed( false )
+}
+
+void function SuperSpectreIntro( entity npc )
+{
+ bool warpfall
+ if ( npc.mySpawnOptions_warpfallSpawn != null )
+ warpfall = true
+ else if ( npc.HasKey( "script_hotdrop" ) && npc.kv.script_hotdrop.tolower() == "warpfall" )
+ warpfall = true
+
+ if ( warpfall )
+ thread SuperSpectre_WarpFall( npc )
+}
+
+void function AssignGruntModelForWeaponClass( entity npc, entity weapon, string weaponSubClass )
+{
+ // We only have IMC grunt models for weapon class
+ if ( !npc.Dev_GetAISettingByKeyField( "IsGenericGrunt" ) )
+ return
+
+ asset model
+
+ switch ( npc.GetTeam() )
+ {
+//#if SP
+ case TEAM_MILITIA:
+ switch ( weaponSubClass )
+ {
+ case "lmg":
+ case "sniper":
+ model = TEAM_MIL_GRUNT_MODEL_LMG
+ break
+
+ case "rocket":
+ case "shotgun":
+ case "projectile_shotgun":
+ model = TEAM_MIL_GRUNT_MODEL_SHOTGUN
+ break
+
+ case "handgun":
+ case "smg":
+ case "sidearm":
+ model = TEAM_MIL_GRUNT_MODEL_SMG
+ break
+
+ case "rifle":
+ default:
+ model = TEAM_MIL_GRUNT_MODEL_RIFLE
+ break
+ }
+ break
+//#endif
+
+ case TEAM_IMC:
+ default:
+ switch ( weaponSubClass )
+ {
+ case "lmg":
+ case "sniper":
+ model = TEAM_IMC_GRUNT_MODEL_LMG
+ break
+
+ case "rocket":
+ case "shotgun":
+ case "projectile_shotgun":
+ model = TEAM_IMC_GRUNT_MODEL_SHOTGUN
+ break
+
+ case "handgun":
+ case "smg":
+ case "sidearm":
+ model = TEAM_IMC_GRUNT_MODEL_SMG
+ break
+
+ case "rifle":
+ default:
+#if SP
+ model = TEAM_IMC_GRUNT_MODEL_RIFLE
+#else
+ // no shotgun/smg grunts in MP right now
+ switch ( RandomInt( 3 ) )
+ {
+ case 0:
+ model = TEAM_IMC_GRUNT_MODEL_RIFLE
+ break
+ case 1:
+ model = TEAM_IMC_GRUNT_MODEL_SHOTGUN
+ break
+ case 2:
+ model = TEAM_IMC_GRUNT_MODEL_SMG
+ break
+ }
+#endif
+ break
+ }
+ break
+
+ }
+
+ if ( model != $"" )
+ {
+ npc.SetModel( model )
+ return
+ }
+
+ if ( IsValid( weapon ) )
+ CodeWarning( "Grunt at " + npc.GetOrigin() + " couldnt get assigned a body model for weapon " + weapon.GetWeaponClassName() + " because that weapon is missing or has invalid weaponSubClass field" )
+ else
+ CodeWarning( "Grunt at " + npc.GetOrigin() + " has no weapon" )
+}
+
+
+entity function SP_GetPilotAntiTitanWeapon( entity ent )
+{
+ array<entity> weaponsArray = ent.GetMainWeapons()
+ foreach ( weapon in weaponsArray )
+ {
+ foreach ( weaponName in file.pilotAntiTitanWeapons )
+ {
+ if ( weapon.GetWeaponClassName() == weaponName )
+ return weapon
+ }
+ }
+
+ return null
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spectre.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spectre.gnut
new file mode 100644
index 00000000..214aff96
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spectre.gnut
@@ -0,0 +1,131 @@
+global function AiSpectre_Init
+global function NPCCarriesBattery
+
+void function AiSpectre_Init()
+{
+ //AddDamageCallback( "npc_spectre", SpectreOnDamaged )
+ AddDeathCallback( "npc_spectre", SpectreOnDeath )
+ //AddSpawnCallback( "npc_spectre", SpectreOnSpawned )
+
+ #if !SPECTRE_CHATTER_MP_ENABLED
+ AddCallback_OnPlayerKilled( SpectreChatter_OnPlayerKilled )
+ AddCallback_OnNPCKilled( SpectreChatter_OnNPCKilled )
+ #endif
+}
+
+void function SpectreOnSpawned( entity npc )
+{
+
+}
+
+void function SpectreOnDeath( entity npc, var damageInfo )
+{
+ if ( !IsValidHeadShot( damageInfo, npc ) )
+ return
+
+ // Set these so cl_player knows to kill the eye glow and play the right SFX
+ DamageInfo_AddCustomDamageType( damageInfo, DF_HEADSHOT )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_KILLSHOT )
+// EmitSoundOnEntityExceptToPlayer( npc, attacker, "SuicideSpectre.BulletImpact_HeadShot_3P_vs_3P" )
+
+ int bodyGroupIndex = npc.FindBodyGroup( "removableHead" )
+ int stateIndex = 1 // 0 = show, 1 = hide
+ npc.SetBodygroup( bodyGroupIndex, stateIndex )
+
+ DamageInfo_SetDamage( damageInfo, npc.GetMaxHealth() )
+
+}
+
+// All damage to spectres comes here for modification and then either branches out to other npc types (Suicide, etc) for custom stuff or it just continues like normal.
+void function SpectreOnDamaged( entity npc, var damageInfo )
+{
+
+}
+
+void function SpectreChatter_OnPlayerKilled( entity playerKilled, entity attacker, var damageInfo )
+{
+ if ( !IsSpectre( attacker ) )
+ return
+
+ if ( playerKilled.IsTitan() )
+ thread PlaySpectreChatterAfterDelay( attacker, "spectre_gs_gruntkillstitan_02_1" )
+ else
+ thread PlaySpectreChatterAfterDelay( attacker, "spectre_gs_killenemypilot_01_1" )
+
+}
+
+void function SpectreChatter_OnNPCKilled( entity npcKilled, entity attacker, var damageInfo )
+{
+ if ( IsSpectre( npcKilled ) )
+ {
+ string deadGuySquadName = expect string( npcKilled.kv.squadname )
+ if ( deadGuySquadName == "" )
+ return
+
+ array<entity> squad = GetNPCArrayBySquad( deadGuySquadName )
+
+ entity speakingSquadMate = null
+
+ foreach( squadMate in squad )
+ {
+ if ( IsSpectre( squadMate ) )
+ {
+ speakingSquadMate = squadMate
+ break
+ }
+ }
+ if ( speakingSquadMate == null )
+ return
+
+ if ( squad.len() == 1 )
+ thread PlaySpectreChatterAfterDelay( speakingSquadMate, "spectre_gs_squaddeplete_01_1" )
+ else if ( squad.len() > 0 )
+ thread PlaySpectreChatterAfterDelay( speakingSquadMate, "spectre_gs_allygrundown_05_1" )
+ }
+ else
+ {
+ if ( !IsSpectre( attacker ) )
+ return
+
+ if ( npcKilled.IsTitan() )
+ thread PlaySpectreChatterAfterDelay( attacker, "spectre_gs_gruntkillstitan_02_1" )
+ }
+}
+
+void function PlaySpectreChatterAfterDelay( entity spectre, string chatterLine, float delay = 0.3 )
+{
+ wait delay
+
+ if ( !IsAlive( spectre ) ) //Really this is just an optimization thing, if the spectre is dead no point in running the same check for every player nearby in ShouldPlaySpectreChatterMPLine
+ return
+
+ PlaySpectreChatterToAll( chatterLine, spectre )
+}
+
+void function NPCCarriesBattery( entity npc )
+{
+ entity battery = Rodeo_CreateBatteryPack()
+ battery.SetParent( npc, "BATTERY_ATTACH" )
+ battery.MarkAsNonMovingAttachment()
+ thread SpectreBatteryThink( npc, battery )
+}
+
+void function SpectreBatteryThink( entity npc, entity battery )
+{
+ battery.EndSignal( "OnDestroy" )
+ npc.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( battery )
+ {
+ if ( IsValid( battery ) )
+ {
+ battery.ClearParent()
+ battery.SetAngles( < 0,0,0 > )
+ battery.SetVelocity( < 0,0,200 > )
+ }
+ }
+ )
+
+ npc.WaitSignal( "OnDeath" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut
new file mode 100644
index 00000000..f49560e0
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut
@@ -0,0 +1,606 @@
+global function AiStalker_Init
+global function GetDeathForce
+global function StalkerGearOverloads
+global function StalkerMeltingDown
+
+global function IsStalkerLimbBlownOff
+
+const float STALKER_DAMAGE_REQUIRED_TO_HEADSHOT = 0.3
+//
+// Base npc script shared between all npc types (regular, suicide, etc.)
+//
+
+const STALKER_REACTOR_CRITIMPACT_SOUND_1P_VS_3P = "ai_stalker_bulletimpact_nukecrit_1p_vs_3p"
+const STALKER_REACTOR_CRITIMPACT_SOUND_3P_VS_3P = "ai_stalker_bulletimpact_nukecrit_3p_vs_3p"
+const STALKER_REACTOR_CRITICAL_SOUND = "ai_stalker_nukedestruct_warmup_3p"
+const STALKER_REACTOR_CRITICAL_FX = $"P_spectre_suicide_warn"
+
+void function AiStalker_Init()
+{
+ PrecacheImpactEffectTable( "exp_stalker_powersupply" )
+ PrecacheImpactEffectTable( "exp_small_stalker_powersupply" )
+ PrecacheParticleSystem( STALKER_REACTOR_CRITICAL_FX )
+ AddDamageCallback( "npc_stalker", StalkerOnDamaged )
+ AddDeathCallback( "npc_stalker", StalkerOnDeath )
+ AddSpawnCallback( "npc_stalker", StalkerOnSpawned )
+}
+
+void function StalkerOnSpawned( entity npc )
+{
+ StalkerOnSpawned_Think( npc )
+}
+
+void function StalkerOnSpawned_Think( entity npc )
+{
+ npc.SetCanBeMeleeExecuted( false )
+
+ for ( int hitGroup = 0; hitGroup < HITGROUP_COUNT; hitGroup++ )
+ {
+ npc.ai.stalkerHitgroupDamageAccumulated[ hitGroup ] <- 0
+ npc.ai.stalkerHitgroupLastHitTime[ hitGroup ] <- 0
+ }
+
+ if ( npc.Dev_GetAISettingByKeyField( "ScriptSpawnAsCrawler" ) == 1 )
+ {
+ EnableStalkerCrawlingBehavior( npc )
+ PlayCrawlingAnim( npc, "ACT_RUN" )
+ npc.Anim_Stop() // start playing a crawl anim then cut it off so it doesnt loop
+ }
+}
+
+void function StalkerOnDeath( entity npc, var damageInfo )
+{
+ thread StalkerOnDeath_Internal( npc, damageInfo )
+
+ #if MP
+ int sourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( sourceId == eDamageSourceId.damagedef_titan_step )
+ {
+ Explosion_DamageDefSimple(
+ damagedef_stalker_powersupply_explosion_large_at,
+ npc.GetOrigin(),
+ npc,
+ npc,
+ npc.GetOrigin()
+ )
+ }
+ #endif
+
+}
+
+void function StalkerOnDeath_Internal( entity npc, var damageInfo )
+{
+ int customDamageFlags = DamageInfo_GetCustomDamageType( damageInfo )
+ bool allowDismemberment = bool( customDamageFlags & DF_DISMEMBERMENT )
+ if ( allowDismemberment )
+ {
+ int hitGroup = GetHitGroupFromDamageInfo( npc, damageInfo )
+ if ( hitGroup >= HITGROUP_GENERIC )
+ {
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ TryDismemberStalker( npc, damageInfo, attacker, hitGroup )
+ }
+ }
+
+ if ( IsCrawling( npc ) )
+ {
+ WaitFrame() // or head won't disappear
+ if ( IsValid( npc ) )
+ npc.BecomeRagdoll( Vector( 0, 0, 0 ), false )
+ return
+ }
+}
+
+
+// All damage to stalkers comes here for modification and then either branches out to other npc types (Suicide, etc) for custom stuff or it just continues like normal.
+void function StalkerOnDamaged( entity npc, var damageInfo )
+{
+ StalkerOnDamaged_Internal( npc, damageInfo )
+}
+
+void function StalkerOnDamaged_Internal( entity npc, var damageInfo )
+{
+ if ( !IsAlive( npc ) )
+ return
+
+ if ( StalkerMeltingDown( npc ) )
+ {
+ DamageInfo_ScaleDamage( damageInfo, 0.0 )
+ return
+ }
+
+ // can't shoot, don't blow off limbs
+ if ( IsCrawling( npc ) )
+ {
+ if ( Time() - npc.ai.startCrawlingTime < 0.75 )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ }
+
+ int hitGroup = GetHitGroupFromDamageInfo( npc, damageInfo )
+ if ( hitGroup < HITGROUP_GENERIC )
+ hitGroup = HITGROUP_GENERIC
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ // limb dead yet?
+ npc.ai.stalkerHitgroupDamageAccumulated[ hitGroup ] += int( damage )
+ npc.ai.stalkerHitgroupLastHitTime[ hitGroup ] = Time()
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( PlayerHitGear( npc, damageInfo, hitGroup ) )
+ {
+ // don't die from damage
+ float damage = DamageInfo_GetDamage( damageInfo )
+ damage = npc.GetHealth() - 1.0
+ DamageInfo_SetDamage( damageInfo, damage )
+
+ thread StalkerGearOverloads( npc, attacker )
+ return
+ }
+
+ int customDamageFlags = DamageInfo_GetCustomDamageType( damageInfo )
+ bool allowDismemberment = bool( customDamageFlags & DF_DISMEMBERMENT )
+ if ( !allowDismemberment )
+ return
+
+ bool canBeStaggered = TryDismemberStalker( npc, damageInfo, attacker, hitGroup )
+
+ if ( canBeStaggered && !IsCrawling( npc ) && !npc.ai.transitioningToCrawl )
+ {
+ if ( npc.GetHealth().tofloat() / npc.GetMaxHealth().tofloat() <= 0.5 )
+ {
+ thread AttemptStandToStaggerAnimation( npc )
+ npc.SetActivityModifier( ACT_MODIFIER_STAGGER, true )
+ }
+ }
+}
+
+bool function TryDismemberStalker( entity npc, var damageInfo, entity attacker, int hitGroup )
+{
+ string fpSound
+ string tpSound
+
+ switch ( hitGroup )
+ {
+ case HITGROUP_CHEST:
+ case HITGROUP_STOMACH:
+ fpSound = "AndroidArmored.BulletImpact_1P_vs_3P"
+ tpSound = "AndroidArmored.BulletImpact_3P_vs_3P"
+ break
+
+ default:
+ fpSound = "AndroidVulnerable.BulletImpact_1P_vs_3P"
+ tpSound = "AndroidVulnerable.BulletImpact_3P_vs_3P"
+ break
+ }
+
+ if ( IsAlive( attacker ) && attacker.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( npc, attacker, fpSound )
+ EmitSoundOnEntityExceptToPlayer( npc, attacker, tpSound )
+ }
+ else
+ {
+ EmitSoundOnEntity( npc, tpSound )
+ }
+
+ bool justAFleshWound = true
+
+ switch ( hitGroup )
+ {
+ case HITGROUP_HEAD:
+ thread StalkerHeadShot( npc, damageInfo, hitGroup )
+ justAFleshWound = false
+ break
+
+ case HITGROUP_LEFTARM:
+ if ( StalkerLimbBlownOff( npc, damageInfo, hitGroup, 0.085, "left_arm", [ "left_arm", "l_hand" ], "Spectre.Arm.Explode" ) )
+ {
+ npc.SetActivityModifier( ACT_MODIFIER_ONEHANDED, true )
+
+ // Some of his synced melees depend on using his left arm
+ npc.SetCapabilityFlag( bits_CAP_SYNCED_MELEE_ATTACK, false )
+ }
+ break
+
+ case HITGROUP_LEFTLEG:
+ justAFleshWound = TryLegBlownOff( npc, damageInfo, hitGroup, 0.17, "left_leg", [ "left_leg", "foot_L_sole" ], "Spectre.Leg.Explode" )
+ break
+
+ case HITGROUP_RIGHTLEG:
+ justAFleshWound = TryLegBlownOff( npc, damageInfo, hitGroup, 0.17, "right_leg", [ "right_leg", "foot_R_sole" ], "Spectre.Leg.Explode" )
+ break
+ }
+
+ return justAFleshWound
+}
+
+bool function PlayerHitGear( entity npc, var damageInfo, int hitGroup )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !attacker.IsPlayer() )
+ return false
+
+ if ( hitGroup != HITGROUP_GEAR )
+ return false
+
+ if ( !( DamageInfo_GetCustomDamageType( damageInfo ) & DF_BULLET ) )
+ return false
+
+ return true
+}
+
+int function GetHitGroupFromDamageInfo( entity npc, var damageInfo )
+{
+ int hitGroup = DamageInfo_GetHitGroup( damageInfo )
+
+ if ( hitGroup <= HITGROUP_GENERIC )
+ {
+ int hitBox = DamageInfo_GetHitBox( damageInfo )
+ if ( hitBox >= 0 )
+ return GetHitgroupForHitboxOnEntity( npc, hitBox )
+ }
+
+ return hitGroup
+}
+
+bool function StalkerMeltingDown( entity npc )
+{
+ int bodyGroup = npc.FindBodyGroup( "gear" )
+ Assert( bodyGroup != -1 )
+
+ // gear already blown up?
+ return npc.GetBodyGroupState( bodyGroup ) != 0
+}
+
+void function StalkerGearOverloads( entity npc, entity attacker = null )
+{
+ Assert( !StalkerMeltingDown( npc ) )
+
+ if ( !IsCrawling( npc ) && StalkerCanCrawl( npc ) )
+ thread FallAndBecomeCrawlingStalker( npc )
+
+ int bodyGroup = npc.FindBodyGroup( "gear" )
+
+ // hide gear
+ npc.SetBodygroup( bodyGroup, 1 )
+
+ string attachment = "CHESTFOCUS"
+
+ npc.EndSignal( "OnDestroy" )
+ npc.EndSignal( "OnDeath" )
+
+ entity nukeFXInfoTarget = CreateEntity( "info_target" )
+ nukeFXInfoTarget.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ DispatchSpawn( nukeFXInfoTarget )
+
+ nukeFXInfoTarget.SetParent( npc, attachment )
+
+ if ( attacker != null )
+ {
+ EmitSoundOnEntityOnlyToPlayer( nukeFXInfoTarget, attacker, STALKER_REACTOR_CRITIMPACT_SOUND_1P_VS_3P )
+ EmitSoundOnEntityExceptToPlayer( nukeFXInfoTarget, attacker, STALKER_REACTOR_CRITIMPACT_SOUND_3P_VS_3P )
+ }
+ else
+ {
+ EmitSoundOnEntity( nukeFXInfoTarget, STALKER_REACTOR_CRITIMPACT_SOUND_3P_VS_3P )
+ }
+
+ EmitSoundOnEntity( nukeFXInfoTarget, STALKER_REACTOR_CRITICAL_SOUND )
+
+ AI_CreateDangerousArea_DamageDef( damagedef_stalker_powersupply_explosion_small, nukeFXInfoTarget, TEAM_INVALID, true, false )
+
+ entity fx = PlayFXOnEntity( STALKER_REACTOR_CRITICAL_FX, nukeFXInfoTarget )
+
+ OnThreadEnd(
+ function() : ( nukeFXInfoTarget, fx, npc, attacker )
+ {
+ if ( IsValid( npc ) )
+ StopSoundOnEntity( nukeFXInfoTarget, STALKER_REACTOR_CRITICAL_SOUND )
+
+ if ( IsValid( nukeFXInfoTarget ) )
+ nukeFXInfoTarget.Destroy()
+
+ if ( IsValid( fx ) )
+ fx.Destroy()
+
+ if ( IsAlive( npc ) )
+ {
+ entity damageAttacker
+ if ( IsValid( attacker ) )
+ damageAttacker = attacker
+ else
+ damageAttacker = npc
+
+ vector force = GetDeathForce()
+ npc.Die( damageAttacker, npc, { force = force, scriptType = DF_GIB, damageSourceId = eDamageSourceId.suicideSpectreAoE } )
+ }
+ }
+ )
+
+ wait 1.0
+
+ float duration = 2.1
+ float endTime = Time() + duration
+ float startTime = Time()
+
+ int tagID = npc.LookupAttachment( "CHESTFOCUS" )
+
+ for ( ;; )
+ {
+ float timePassed = Time() - startTime
+ float explodeMin = Graph( timePassed, 0, duration, 0.4, 0.1 )
+ float explodeMax = explodeMin + Graph( timePassed, 0, duration, 0.21, 0.1 )
+ wait RandomFloatRange( explodeMin, explodeMax )
+
+ entity damageAttacker = GetNPCAttackerEnt( npc, attacker )
+
+ // origin = npc.GetWorldSpaceCenter()
+ vector origin = npc.GetAttachmentOrigin( tagID )
+
+ if ( Time() >= endTime )
+ {
+ Explosion_DamageDefSimple( damagedef_stalker_powersupply_explosion_large, origin, damageAttacker, npc, origin )
+ break
+ }
+ else
+ {
+ Explosion_DamageDefSimple( damagedef_stalker_powersupply_explosion_small, origin, damageAttacker, npc, origin )
+ }
+ }
+}
+
+bool function StalkerCanCrawl( entity npc )
+{
+ if ( !IsAlive( npc ) )
+ return false
+
+ if ( npc.Anim_IsActive() )
+ return false
+
+ return true
+}
+
+bool function TryLegBlownOff( entity npc, var damageInfo, int hitGroup, float limbHealthPercentOfMax, string leg, array<string> fxTags, string sound )
+{
+ if ( IsCrawling( npc ) )
+ {
+ // can blow off leg if stalker is already crawling
+ StalkerLimbBlownOff( npc, damageInfo, hitGroup, limbHealthPercentOfMax, leg, fxTags, sound )
+ return true
+ }
+
+ if ( !StalkerCanCrawl( npc ) )
+ return true
+
+ if ( StalkerLimbBlownOff( npc, damageInfo, hitGroup, limbHealthPercentOfMax, leg, fxTags, sound ) )
+ {
+ thread FallAndBecomeCrawlingStalker( npc )
+ return false
+ }
+
+ return true
+}
+
+void function EnableStalkerCrawlingBehavior( entity npc )
+{
+ Assert( StalkerCanCrawl( npc ) )
+ Assert( !IsCrawling( npc ) )
+
+ DisableLeeching( npc )
+
+ DisableMinionUsesHeavyWeapons( npc )
+
+ string crawlingSettings = string ( npc.Dev_GetAISettingByKeyField( "crawlingSettingsWrapper" ) )
+
+ // Changing the setting file includes changing the behavior file to "behavior_stalker_crawling"
+ SetAISettingsWrapper( npc, crawlingSettings )
+
+ npc.ai.crawling = true
+ npc.ai.startCrawlingTime = Time()
+ npc.DisableGrappleAttachment()
+ npc.EnableNPCMoveFlag( NPCMF_DISABLE_ARRIVALS )
+ npc.SetCapabilityFlag( bits_CAP_MOVE_TRAVERSE | bits_CAP_MOVE_SHOOT | bits_CAP_WEAPON_RANGE_ATTACK1 | bits_CAP_AIM_GUN, false )
+ npc.SetActivityModifier( ACT_MODIFIER_CRAWL, true )
+ npc.SetActivityModifier( ACT_MODIFIER_STAGGER, false )
+ npc.SetCanBeGroundExecuted( true )
+ npc.ClearMoveAnim()
+
+ npc.SetHealth( npc.GetMaxHealth() * 0.5 )
+
+ npc.SetAimAssistForcePullPitchEnabled( true )
+
+ thread SelfTerminateAfterDelay( npc )
+}
+
+void function SelfTerminateAfterDelay( entity npc )
+{
+ const float lifeSupportDuration = 8
+ float deathTime = Time() + (lifeSupportDuration * 2)
+
+ npc.EndSignal( "OnDeath" )
+ for ( ;; )
+ {
+ entity enemy = npc.GetEnemy()
+ if ( IsAlive( enemy ) )
+ {
+ if ( Distance( npc.GetEnemyLKP(), npc.GetOrigin() ) < 500 )
+ {
+ if ( npc.TimeSinceSeen( enemy ) < 3 )
+ deathTime = max( Time() + lifeSupportDuration, deathTime )
+ }
+ }
+
+ if ( Time() > deathTime )
+ {
+ npc.Die()
+ return
+ }
+
+ wait 1.0
+ }
+}
+
+void function FallAndBecomeCrawlingStalker( entity npc )
+{
+ // finish what he's doing
+ npc.EndSignal( "OnDeath" )
+
+ npc.ai.transitioningToCrawl = true
+
+ // Workaround for Bug 114372
+ WaitFrame()
+
+ for ( ;; )
+ {
+ if ( npc.IsInterruptable() )
+ break
+ WaitFrame()
+ }
+
+ if ( !StalkerCanCrawl( npc ) )
+ return
+
+ if ( IsCrawling( npc ) )
+ return
+
+ EnableStalkerCrawlingBehavior( npc )
+
+ npc.Anim_Stop() // stop leeching, etc.
+
+ PlayCrawlingAnim( npc, "ACT_STAND_TO_CRAWL" )
+}
+
+void function PlayCrawlingAnim( entity npc, string animation )
+{
+ npc.Anim_ScriptedPlayActivityByName( animation, true, 0.1 )
+ npc.UseSequenceBounds( true )
+}
+
+void function AttemptStandToStaggerAnimation( entity npc )
+{
+ // Check if we are already staggered
+ if ( npc.IsActivityModifierActive( ACT_MODIFIER_STAGGER ) )
+ return
+
+ if ( !npc.IsInterruptable() )
+ return
+
+ if ( npc.ContextAction_IsBusy() )
+ return
+
+ // Are we blocking additional pain animations
+ if ( npc.GetNPCFlag( NPC_NO_PAIN ) )
+ return
+
+ // finish what he's doing
+ npc.EndSignal( "OnDeath" )
+
+ // Workaround for Bug 114372
+ WaitFrame()
+
+ for ( ;; )
+ {
+ if ( npc.IsInterruptable() )
+ break
+
+ WaitFrame()
+ }
+
+ if ( IsCrawling( npc ) || npc.ai.transitioningToCrawl )
+ return
+
+ npc.Anim_ScriptedPlayActivityByName( "ACT_STAND_TO_STAGGER", true, 0.1 )
+ npc.UseSequenceBounds( true )
+ npc.EnableNPCFlag( NPC_PAIN_IN_SCRIPTED_ANIM )
+}
+
+bool function IsStalkerLimbBlownOff( entity npc, string limbName )
+{
+ int bodyGroup = npc.FindBodyGroup( limbName )
+ if ( npc.GetBodyGroupState( bodyGroup ) != 0 )
+ return true
+
+ return false
+}
+
+bool function StalkerLimbBlownOff( entity npc, var damageInfo, int hitGroup, float limbHealthPercentOfMax, string limbName, array<string> fxTags, string sound )
+{
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ switch ( damageSourceId )
+ {
+ case eDamageSourceId.mp_weapon_grenade_emp:
+ case eDamageSourceId.mp_weapon_proximity_mine:
+ return false
+ }
+
+ int bodyGroup = npc.FindBodyGroup( limbName )
+ if ( bodyGroup == -1 )
+ return false
+
+ if ( IsStalkerLimbBlownOff( npc, limbName ) )
+ return false
+
+ EmitSoundOnEntity( npc, sound )
+
+ // blow off limb
+ npc.SetBodygroup( bodyGroup, 1 )
+
+ return true
+}
+
+void function StalkerHeadShot( entity npc, var damageInfo, int hitGroup )
+{
+ // random chance to blow up head
+// if ( DamageInfo_GetDamage( damageInfo ) < 100 && RandomFloat( 100 ) <= 66 )
+// return
+
+ if ( !IsValidHeadShot( damageInfo, npc ) )
+ return
+
+ // only players score headshots
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsAlive( attacker ) )
+ return
+ if ( !attacker.IsPlayer() )
+ return
+
+ if ( DamageInfo_GetDamage( damageInfo ) < npc.GetHealth() )
+ {
+ // force lethal if we have done more than this much damage
+ if ( npc.ai.stalkerHitgroupDamageAccumulated[ hitGroup ] < npc.GetMaxHealth() * STALKER_DAMAGE_REQUIRED_TO_HEADSHOT )
+ return
+ }
+
+ npc.Anim_Stop() // stop leeching, etc.
+ npc.ClearParent()
+
+ //DisableLeeching( npc )
+
+ // No pain anims
+ //DamageInfo_AddDamageFlags( damageInfo, DAMAGEFLAG_NOPAIN )
+
+ // Set these so cl_player knows to kill the eye glow and play the right SFX
+ DamageInfo_AddCustomDamageType( damageInfo, DF_HEADSHOT )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_KILLSHOT )
+
+ EmitSoundOnEntityExceptToPlayer( npc, attacker, "SuicideSpectre.BulletImpact_HeadShot_3P_vs_3P" )
+
+ int bodyGroupIndex = npc.FindBodyGroup( "removableHead" )
+ int stateIndex = 1 // 0 = show, 1 = hide
+ npc.SetBodygroup( bodyGroupIndex, stateIndex )
+
+ DamageInfo_SetDamage( damageInfo, npc.GetMaxHealth() )
+}
+
+vector function GetDeathForce()
+{
+ vector angles = <RandomFloatRange(-45,-75),RandomFloat(360),0>
+ vector forward = AnglesToForward( angles )
+ return forward * RandomFloatRange( 0.25, 0.75 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stationary_firing_positions.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stationary_firing_positions.gnut
new file mode 100644
index 00000000..50b6cc75
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stationary_firing_positions.gnut
@@ -0,0 +1,261 @@
+global function AddStationaryAIPosition //Add stationary positions to pending list.
+global function AddTestTargetPosForStationaryPositionValidation //Add test target location for validating stationary positions.
+global function ValidateAndFinalizePendingStationaryPositions //Runs error-checking/validation logic on stationary positions and finalizes them for use by AI.
+global function GetRandomStationaryPosition
+global function GetClosestAvailableStationaryPosition
+global function ClaimStationaryAIPosition
+global function ReleaseStationaryAIPosition
+
+global enum eStationaryAIPositionTypes
+{
+ MORTAR_TITAN,
+ MORTAR_SPECTRE,
+ SNIPER_TITAN,
+ LAUNCHER_REAPER
+}
+
+global struct StationaryAIPosition
+{
+ vector origin
+ bool inUse
+}
+
+global struct ArrayDistanceEntryForStationaryAIPosition
+{
+ float distanceSqr
+ StationaryAIPosition& ent
+ vector origin
+}
+
+struct
+{
+ array<vector> validationTestTargets
+ table<int, array<vector> > pendingPositions
+ table<int, array<StationaryAIPosition> > stationaryPositions
+} file
+
+void function AddTestTargetPosForStationaryPositionValidation( vector origin )
+{
+ file.validationTestTargets.append( origin )
+}
+
+void function AddStationaryAIPosition( vector origin, int type )
+{
+ AddPendingStationaryAIPosition_Internal( origin, type )
+}
+
+void function AddStationaryAIPosition_Internal( vector origin, int type )
+{
+ StationaryAIPosition pos
+ pos.origin = origin
+ pos.inUse = false
+
+ //Throw warnings for bad positions
+ foreach ( vector testTarget in file.validationTestTargets )
+ {
+ switch( type )
+ {
+ case eStationaryAIPositionTypes.MORTAR_TITAN:
+ if ( NavMesh_ClampPointForHullWithExtents( origin, HULL_TITAN, <100, 100, 20> ) == null )
+ {
+ CodeWarning( "Mortar Titan Firing Position at " + origin + " does not have enough space to accomidate Titan, skipping." )
+ return
+ }
+ break
+
+ #if MP
+ case eStationaryAIPositionTypes.MORTAR_SPECTRE:
+
+ array<vector> testLocations = MortarSpectreGetSquadFiringPositions( origin, testTarget )
+
+ foreach ( vector testLocation in testLocations )
+ {
+ if ( NavMesh_ClampPointForHullWithExtents( testLocation, HULL_HUMAN, <100, 100, 20> ) == null )
+ {
+ CodeWarning( "Mortar Spectre Firing Position at " + origin + " does not have enough space to accomidate squad, skipping." )
+ return
+ }
+ }
+
+ break
+ #endif //MP
+
+ case eStationaryAIPositionTypes.SNIPER_TITAN:
+ if ( NavMesh_ClampPointForHullWithExtents( origin, HULL_TITAN, <100, 100, 20> ) == null )
+ {
+ CodeWarning( "Sniper Titan Firing Position at " + origin + " does not have enough space to accomidate Titan, skipping." )
+ return
+ }
+ break
+
+ case eStationaryAIPositionTypes.LAUNCHER_REAPER:
+ if ( NavMesh_ClampPointForHullWithExtents( origin, HULL_MEDIUM, <100, 100, 20> ) == null )
+ {
+ CodeWarning( "Tick Launching Reaper Firing Position at " + origin + " does not have enough space to accomidate Reaper, skipping." )
+ return
+ }
+ break
+ }
+ }
+
+ if ( !( type in file.stationaryPositions ) )
+ {
+ file.stationaryPositions[ type ] <- []
+ }
+
+ file.stationaryPositions[ type ].append( pos )
+}
+
+//Function tests stationary AI positions for given type relative to given mortar target.
+void function AddPendingStationaryAIPosition_Internal( vector origin, int type )
+{
+ if ( !( type in file.pendingPositions ) )
+ file.pendingPositions[ type ] <- []
+
+ //Add position to table so we can validate and add it when all entities finish loading.
+ file.pendingPositions[ type ].append( origin )
+}
+
+void function ValidateAndFinalizePendingStationaryPositions()
+{
+
+ Assert( file.validationTestTargets.len(), "Test targets are required to validate stationary positions. Use AddTestTargetPosForStationaryPositionValidation to add them before running validation." )
+
+ foreach ( type, origins in file.pendingPositions )
+ {
+ //Make sure we have pending positions for given ai type.
+ Assert( file.pendingPositions[ type ].len(), "Stationary Positions for type " + type + " could not be found in this map. Add Some." )
+
+ foreach ( vector origin in origins )
+ {
+ AddStationaryAIPosition_Internal( origin, type )
+ }
+
+ //Make sure we have positions for given AI type after we validate and finalize positions.
+ Assert( file.stationaryPositions[ type ].len(), "No valid stationary positions for type " + type + " remain after validation. Adjust positions and retry." )
+ }
+}
+
+StationaryAIPosition function GetClosestAvailableStationaryPosition( vector origin, float maxDist, int type )
+{
+
+ array<StationaryAIPosition> resultArray = []
+ float maxDistSqr = maxDist * maxDist
+
+ array<StationaryAIPosition> positions = file.stationaryPositions[type]
+
+ array<ArrayDistanceEntryForStationaryAIPosition> allResults = ArrayDistanceResultsForStationaryAIPosition( positions, origin )
+ allResults.sort( DistanceCompareClosestForStationaryAIPosition )
+
+ //Remove all in use stationary positions up front.
+ array<ArrayDistanceEntryForStationaryAIPosition> freePositions
+ foreach ( result in allResults )
+ {
+ StationaryAIPosition position = result.ent
+ if ( position.inUse )
+ continue
+
+ freePositions.append( result )
+ }
+
+ //Tell us if all spots for a given AI type are taken.
+ Assert( freePositions.len() > 0, "Could not find free mortar positions for type " + type + ", all positions are currently in use. Add more AddStationaryTitanPosition to the map." )
+
+ foreach( result in freePositions )
+ {
+ StationaryAIPosition position = result.ent
+
+ // if too far, throw warning and continue search beyond maxDist
+ if ( result.distanceSqr > maxDistSqr )
+ {
+ CodeWarning( "Couldn't find a mortar position within " + maxDist + " units for type " + type + " around " + origin.tostring() + " that wasn't in use. Expanding Search. Add more AddStationaryTitanPositions to the map near this point." )
+ }
+
+ return position
+ }
+
+ unreachable
+}
+
+StationaryAIPosition function GetRandomStationaryPosition( vector origin, float maxDist, int type )
+{
+ array<StationaryAIPosition> resultArray = []
+ array<StationaryAIPosition> positions = file.stationaryPositions[type]
+
+ //Remove all in use stationary positions up front.
+ array<StationaryAIPosition> freePositions
+ foreach ( position in positions )
+ {
+ if ( position.inUse )
+ continue
+
+ freePositions.append( position )
+ }
+
+ //Tell us if all spots for a given AI type are taken.
+ Assert( freePositions.len() > 0, "Could not find free mortar positions for type " + type + ", all positions are currently in use. Add more AddStationaryTitanPosition to the map." )
+
+ int attemptCount = 1
+ while ( resultArray.len() == 0 )
+ {
+
+ //Expand our search radius each time we reattempt our search.
+ float maxDistSqr = ( maxDist * attemptCount ) * ( maxDist * attemptCount )
+
+ foreach( position in freePositions )
+ {
+ float dist = Distance2DSqr( origin, position.origin )
+ if ( dist <= maxDistSqr )
+ resultArray.append( position )
+ }
+
+ if ( resultArray.len() == 0 )
+ {
+ CodeWarning( "Couldn't find a mortar position within " + maxDist + " units for type " + type + " around " + origin.tostring() + " that wasn't in use. Expanding Search. Add more AddStationaryTitanPositions to the map near this point." )
+ attemptCount += 1
+ }
+ }
+
+ return resultArray.getrandom()
+}
+
+void function ClaimStationaryAIPosition( StationaryAIPosition stationaryTitanPositions )
+{
+ Assert( stationaryTitanPositions.inUse == false )
+ stationaryTitanPositions.inUse = true
+}
+
+void function ReleaseStationaryAIPosition( StationaryAIPosition stationaryTitanPositions )
+{
+ Assert( stationaryTitanPositions.inUse == true )
+ stationaryTitanPositions.inUse = false
+}
+
+array<ArrayDistanceEntryForStationaryAIPosition> function ArrayDistanceResultsForStationaryAIPosition( array<StationaryAIPosition> entArray, vector origin )
+{
+ array<ArrayDistanceEntryForStationaryAIPosition> allResults
+
+ foreach ( ent in entArray )
+ {
+ ArrayDistanceEntryForStationaryAIPosition entry
+
+ vector entOrigin = ent.origin
+ entry.distanceSqr = DistanceSqr( entOrigin, origin )
+ entry.ent = ent
+ entry.origin = entOrigin
+
+ allResults.append( entry )
+ }
+
+ return allResults
+}
+
+int function DistanceCompareClosestForStationaryAIPosition( ArrayDistanceEntryForStationaryAIPosition a, ArrayDistanceEntryForStationaryAIPosition b )
+{
+ if ( a.distanceSqr > b.distanceSqr )
+ return 1
+ else if ( a.distanceSqr < b.distanceSqr )
+ return -1
+
+ return 0;
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_suicide_spectres.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_suicide_spectres.gnut
new file mode 100644
index 00000000..f8e0652c
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_suicide_spectres.gnut
@@ -0,0 +1,576 @@
+global function SuicideSpectres_Init
+global function MakeSuicideSpectre
+global function SpectreSuicideOnDamaged
+global function GetNPCAttackerEnt
+
+const FX_SPECTRE_EXPLOSION = $"P_drone_frag_exp"
+
+//
+// Suicide spectre script
+//
+
+const SPECTRE_EXPLOSION_DELAY = 0.25 // Delay for the first spectre in a chain to start exploding.
+const SPECTRE_DAMAGE_MULTIPLIER_BODY = 1.5
+const SPECTRE_DAMAGE_MULTIPLIER_HEAD = 6.0
+const SPECTRE_DAMAGE_MULTIPLIER_SMART_PISTOL = 2.0
+const SPECTRE_HEADSHOT_KEEP_WALKING_CHANCE = 100 // 35% chance to keep walking after a headshot to add variety
+
+struct
+{
+ int chainExplosionIndex
+ float lastChainExplosionTime
+
+ table< string, array<string> > spectreAnims
+ float nextOverloadTime
+
+} file
+
+const SFX_TICK_OVERLOAD = "corporate_spectre_overload_beep"
+const SFX_TICK_EXPLODE = "corporate_spectre_death_explode"
+
+const SFX_FRAGDRONE_OVERLOAD = "weapon_sentryfragdrone_preexplo"
+const SFX_FRAGDRONE_EXPLODE = "weapon_sentryfragdrone_explo"
+const SFX_FRAGDRONE_SUPERPURSUIT = "weapon_sentryfragdrone_superpursuit"
+
+const CHAIN_EXPLOSION_MAXINDEX = 10
+
+
+void function SuicideSpectres_Init()
+{
+ RegisterSignal( "SuicideSpectreForceExplode" )
+ RegisterSignal( "SuicideSpectreExploding" )
+ RegisterSignal( "SuicideGotEnemy" )
+ RegisterSignal( "SuicideLostEnemy" )
+
+ PrecacheParticleSystem( FX_SPECTRE_EXPLOSION )
+
+ file.spectreAnims[ "spectreSearch" ] <- []
+ file.spectreAnims[ "spectreSearch" ].append( "sp_suicide_spectre_search" )
+ file.spectreAnims[ "spectreSearch" ].append( "sp_suicide_spectre_search_B" )
+ file.spectreAnims[ "spectreSearch" ].append( "sp_suicide_spectre_search_C" )
+
+ AddDamageCallback( "npc_frag_drone", SpectreSuicideOnDamaged_Callback )
+ AddDeathCallback( "npc_frag_drone", FragDroneDeath )
+}
+
+/************************************************************************************************\
+
+ ###### ######## ######## ## ## ########
+## ## ## ## ## ## ## ##
+## ## ## ## ## ## ##
+ ###### ###### ## ## ## ########
+ ## ## ## ## ## ##
+## ## ## ## ## ## ##
+ ###### ######## ## ####### ##
+
+\************************************************************************************************/
+void function MakeSuicideSpectre( entity spectre )
+{
+ spectre.SetAimAssistAllowed( true )
+ spectre.SetAllowMelee( false )
+ DisableLeeching( spectre )
+
+ spectre.SetNPCMoveSpeedScale( 1.0 )
+
+ spectre.EnableNPCMoveFlag( NPCMF_IGNORE_CLUSTER_DANGER_TIME | NPCMF_PREFER_SPRINT )
+ spectre.DisableNPCMoveFlag( NPCMF_FOLLOW_SAFE_PATHS | NPCMF_INDOOR_ACTIVITY_OVERRIDE )
+
+ spectre.kv.allowShoot = 0
+
+ // Frag drones do suicide spectre behavior but we don't want them doing the enemy changed sounds so filter them out
+ if ( !IsFragDrone( spectre ) && !IsTick( spectre ) )
+ spectre.SetEnemyChangeCallback( SuicideSpectreEnemyChanged )
+
+ spectre.SetLookDistOverride( SPECTRE_MAX_SIGHT_DIST )
+ //spectre.SetHearingSensitivity( 10 ) //1 is default
+ spectre.EnableNPCFlag( NPC_MUTE_TEAMMATE )
+
+ spectre.ai.suicideSpectreExplosionDelay = -1
+
+ thread SpectreWaitToExplode( spectre )
+ AddAnimEvent( spectre, "frag_drone_armed", FragDroneArmed )
+}
+
+void function FragDroneArmed( entity npc )
+{
+ npc.ai.fragDroneArmed = true
+}
+
+void function FragDroneDeath( entity spectre, var damageInfo )
+{
+ FragDroneDeath_Think( spectre, damageInfo )
+}
+
+// for reloadscripts
+void function FragDroneDeath_Think( entity spectre, var damageInfo )
+{
+ vector pos = spectre.GetOrigin()
+ int tagID = spectre.LookupAttachment( "CHESTFOCUS" )
+ vector fxOrg = spectre.GetAttachmentOrigin( tagID )
+ string expSFX
+ if ( spectre.mySpawnOptions_aiSettings == "npc_frag_drone_throwable" )
+ expSFX = SFX_FRAGDRONE_EXPLODE
+ else
+ expSFX = SFX_TICK_EXPLODE
+ int expFX = GetParticleSystemIndex( FX_SPECTRE_EXPLOSION )
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity attackerEnt = GetNPCAttackerEnt( spectre, attacker )
+
+ int team = GetExplosionTeamBasedOnGamemode( spectre )
+
+ int damageDef = GetDamageDefForFragDrone( spectre )
+
+ RadiusDamage_DamageDefSimple( damageDef, pos, attackerEnt, spectre, 0 )
+ EmitSoundAtPosition( spectre.GetTeam(), pos, expSFX )
+ CreateShake( pos, 10, 105, 1.25, 768 )
+ StartParticleEffectInWorld( expFX, fxOrg, Vector( 0, 0, 0 ) )
+
+ spectre.Gib( <0, 0, 100> ) //Used to do .Destroy() on the frag drones immediately, but this meant you can't display the obiturary correctly. Instead, since it's dead already just hide it
+}
+
+entity function GetNPCAttackerEnt( entity npc, entity attacker )
+{
+ entity owner = npc.GetBossPlayer()
+ bool ownerIsPlayer = owner != null && owner.IsPlayer()
+
+ if ( IsMultiplayer() )
+ return ownerIsPlayer ? owner : npc
+
+ if ( !IsAlive( attacker ) )
+ return npc
+
+ // dont give player credit, since that does some bad things
+ if ( ownerIsPlayer )
+ return owner
+
+ if ( attacker.IsPlayer() )
+ return GetEnt( "worldspawn" )
+
+ return attacker
+}
+
+
+int function GetDamageDefForFragDrone( entity drone )
+{
+ var damageDef = drone.Dev_GetAISettingByKeyField( "damageDefOverride" )
+ if ( damageDef != null )
+ {
+ expect string( damageDef )
+ return eDamageSourceId[ damageDef ]
+ }
+
+ entity owner = drone.GetBossPlayer()
+ if ( owner != null && owner.IsPlayer() )
+ return damagedef_frag_drone_throwable_PLAYER
+
+ return damagedef_frag_drone_throwable_NPC
+}
+
+void function SuicideSpectreEnemyChanged( entity spectre )
+{
+ // Spectre "Speaks"
+ if ( ( RandomFloat( 1.0 ) ) < 0.02 )
+ EmitSoundOnEntity( spectre, "diag_imc_spectre_gs_spotenemypilot_01_1" )
+}
+
+/************************************************************************************************\
+
+######## ######## ####### ## ## #### ## ## #### ######## ## ##
+## ## ## ## ## ## ## ## ## ### ### ## ## ## ##
+## ## ## ## ## ## ## ## ## #### #### ## ## ####
+######## ######## ## ## ### ## ## ### ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ####### ## ## #### ## ## #### ## ##
+
+\************************************************************************************************/
+void function SpectreWaitToExplode( entity spectre )
+{
+ Assert( spectre.IsNPC() )
+ spectre.EndSignal( "OnDeath" )
+
+ waitthread SuicideSpectre_WaittillNearEnemyOrExploding( spectre )
+
+ if ( spectre.ai.suicideSpectreExplodingAttacker == null )
+ {
+ // not exploding, so overload
+ spectre.ai.suicideSpectreExplosionDelay = GetSpectreExplosionTime( spectre )
+ waitthread SpectreOverloads( spectre )
+ }
+
+ if ( spectre.ai.suicideSpectreExplosionDelay > 0 )
+ wait spectre.ai.suicideSpectreExplosionDelay
+
+ entity attacker = spectre.ai.suicideSpectreExplodingAttacker
+ if ( !IsValid( attacker ) )
+ {
+ entity lastAttacker = GetLastAttacker( spectre )
+ if ( IsValid( lastAttacker ) )
+ {
+ attacker = lastAttacker
+ }
+ else
+ {
+ attacker = spectre
+ }
+ }
+
+ vector force = GetDeathForce()
+
+ Assert( !attacker.IsProjectile(), "Suicide Spectre attacker was a projectile! Type: " + attacker.ProjectileGetWeaponClassName() )
+
+ // JFS: sometimes the attacker is a projectile, which can cause a script error.
+ // The real solution is to figure out which weapon is passing in the projectile as the attacker and correct that.
+ if ( attacker.IsProjectile() )
+ {
+ attacker = spectre
+ }
+
+ spectre.Die( attacker, attacker, { force = force, scriptType = DF_DOOMED_HEALTH_LOSS, damageSourceId = eDamageSourceId.suicideSpectreAoE } )
+}
+
+void function SetSuicideSpectreExploding( entity spectre, entity attacker, float explodingTime )
+{
+ Assert( spectre.ai.suicideSpectreExplodingAttacker == null )
+ spectre.ai.suicideSpectreExplodingAttacker = attacker
+ spectre.ai.suicideSpectreExplosionDelay = explodingTime
+
+ spectre.Signal( "SuicideSpectreExploding" )
+}
+
+float function GetSpectreExplosionTime( entity spectre )
+{
+ if ( Time() - file.lastChainExplosionTime > 1.0 )
+ file.chainExplosionIndex = 0
+
+ float waitTime = file.chainExplosionIndex * 0.14 // RandomFloatRange( CHAIN_EXPLOSION_INTERVALMIN, CHAIN_EXPLOSION_INTERVALMAX )
+ file.lastChainExplosionTime = Time()
+ file.chainExplosionIndex++
+ return waitTime
+}
+
+void function SuicideSpectre_WaittillNearEnemyOrExploding( entity spectre )
+{
+ spectre.EndSignal( "OnDeath" )
+ spectre.EndSignal( "SuicideSpectreExploding" )
+ spectre.EndSignal( "SuicideSpectreForceExplode" )
+
+ bool pursuitSoundPlaying = false
+
+ float minScale = expect float( spectre.Dev_GetAISettingByKeyField( "minSpeedScale" ) )
+ float maxScale = expect float( spectre.Dev_GetAISettingByKeyField( "maxSpeedScale" ) )
+
+ while ( true )
+ {
+ wait 0.1
+
+ if ( !spectre.ai.fragDroneArmed )
+ continue
+
+ if ( spectre.ai.suicideSpectreExplodingAttacker != null )
+ return
+
+ //If spectre is not interrruptable, don't bother
+ if ( !spectre.IsInterruptable() )
+ continue
+
+ //If spectre is parented, don't bother
+ if ( IsValid( spectre.GetParent() ) )
+ continue
+
+ // speed up when near enemy
+ entity enemy = spectre.GetEnemy()
+ if ( IsAlive( enemy ) )
+ {
+ float dist = Distance( enemy.GetOrigin(), spectre.GetOrigin() )
+ float maxDist = 850
+ if ( spectre.mySpawnOptions_aiSettings == "npc_frag_drone_throwable" )
+ {
+ if ( dist < maxDist )
+ {
+ if ( pursuitSoundPlaying == false )
+ {
+ EmitSoundOnEntity( spectre, SFX_FRAGDRONE_SUPERPURSUIT )
+ pursuitSoundPlaying = true
+ }
+ }
+ else
+ {
+ if ( pursuitSoundPlaying == true )
+ {
+ StopSoundOnEntity( spectre, SFX_FRAGDRONE_SUPERPURSUIT )
+ pursuitSoundPlaying = false
+ }
+ }
+ }
+ float speed = GraphCapped( dist, 200, 850, maxScale, minScale )
+ spectre.SetNPCMoveSpeedScale( speed )
+ }
+
+ // offset the overload time
+ if ( Time() < file.nextOverloadTime )
+ continue
+
+ entity attacker = SuicideSpectre_NearEnemy( spectre )
+ if ( attacker != null )
+ {
+ //SetSuicideSpectreOverloading( spectre, attacker )
+ //Assert( 0 ) // never reached
+ return
+ }
+ }
+}
+
+entity function SuicideSpectre_NearEnemy( entity spectre )
+{
+ // See if any player is close eneough to trigger self-destruct
+ array<entity> enemies
+ entity closestEnemy = spectre.GetClosestEnemy()
+ if ( closestEnemy )
+ enemies.append( closestEnemy )
+
+ entity currentEnemy = spectre.GetEnemy()
+ if ( currentEnemy && currentEnemy != closestEnemy )
+ enemies.append( currentEnemy )
+
+ vector origin = spectre.GetOrigin()
+ float dist = expect float( spectre.Dev_GetAISettingByKeyField( "suicideExplosionDistance" ) )
+ foreach ( enemy in enemies )
+ {
+ if ( !IsAlive( enemy ) )
+ continue
+ if ( enemy.IsCloaked( true ) )
+ continue
+ if ( enemy.GetNoTarget() )
+ continue
+ if ( enemy.IsPlayer() && enemy.IsPhaseShifted() )
+ continue
+
+ vector enemyOrigin = enemy.GetOrigin()
+
+ if ( Distance( origin, enemyOrigin ) > dist )
+ continue
+
+ float heightDiff = enemyOrigin.z - origin.z
+
+ // dont explode because you jump over me or I am on the floor above you
+ if ( fabs( heightDiff ) > 40 )
+ {
+ // unless enemy is standing on something slightly above you and there is a clear trace
+ float curTime = Time()
+ float timeDiff = curTime - spectre.ai.suicideSpectreExplosionTraceTime
+ const float TRACE_INTERVAL = 2
+
+ if ( heightDiff > 0 && timeDiff > TRACE_INTERVAL && enemy.IsOnGround() && spectre.CanSee( enemy ) )
+ {
+ spectre.ai.suicideSpectreExplosionTraceTime = curTime
+ float frac = TraceHullSimple( origin, < origin.x, origin.y, enemyOrigin.z >, spectre.GetBoundingMins(), spectre.GetBoundingMaxs(), spectre )
+ if ( frac == 1.0 )
+ return enemy
+ }
+ continue
+ }
+
+ return enemy
+ }
+
+ return null
+}
+
+void function SpectreOverloads( entity spectre )
+{
+ spectre.EndSignal( "SuicideSpectreExploding" )
+ file.nextOverloadTime = Time() + 0.05
+
+ #if MP
+ var chaseTime = spectre.Dev_GetAISettingByKeyField( "SuicideChaseTime" )
+ if ( chaseTime != null )
+ {
+ float maxScale = expect float( spectre.Dev_GetAISettingByKeyField( "maxSpeedScale" ) )
+ spectre.SetNPCMoveSpeedScale( maxScale )
+
+ expect float( chaseTime )
+ float endChaseTime = Time() + chaseTime
+
+ for ( ;; )
+ {
+ if ( Time() >= endChaseTime )
+ break
+
+ if ( !IsAlive( spectre.GetEnemy() ) )
+ break
+
+ entity nearEnemy = SuicideSpectre_NearEnemy( spectre )
+ if ( IsAlive( nearEnemy ) )
+ {
+ if ( nearEnemy.IsTitan() && spectre.IsInterruptable() )
+ {
+ JumpAtTitan( spectre, nearEnemy )
+ spectre.ai.suicideSpectreExplosionDelay = 0.0
+ return
+ }
+ break
+ }
+
+ WaitFrame()
+ }
+ }
+ #endif
+
+ for ( ;; )
+ {
+ #if SP
+ if ( spectre.IsInterruptable() && !spectre.Anim_IsActive() )
+ break
+ #elseif MP
+ if ( spectre.IsInterruptable() && !spectre.Anim_IsActive() && spectre.IsOnGround() )
+ break
+ #endif
+
+ WaitFrame()
+ }
+
+ string overloadSF
+ bool isFragDrone = spectre.mySpawnOptions_aiSettings == "npc_frag_drone_throwable"
+ if ( isFragDrone )
+ overloadSF = SFX_FRAGDRONE_OVERLOAD
+ else
+ overloadSF = SFX_TICK_OVERLOAD
+ // Overload Sound
+ EmitSoundOnEntity( spectre, overloadSF )
+
+ AI_CreateDangerousArea_DamageDef( damagedef_frag_drone_explode, spectre, TEAM_INVALID, true, false )
+
+ // Cleanup on thread end
+ OnThreadEnd(
+ function() : ( spectre, overloadSF )
+ {
+ if ( IsValid( spectre ) )
+ {
+ StopSoundOnEntity( spectre, overloadSF )
+ }
+ }
+ )
+
+ bool jumpAtTitans = spectre.Dev_GetAISettingByKeyField( "JumpAtTitans" ) == null || spectre.Dev_GetAISettingByKeyField( "JumpAtTitans" ) == 1
+
+ entity enemy = spectre.GetEnemy()
+ if ( enemy && enemy.IsTitan() && jumpAtTitans && !spectre.IsInterruptable() )
+ {
+ JumpAtTitan( spectre, enemy )
+ }
+ else
+ {
+ string anim = "sp_suicide_spectre_explode_stand"
+ var overrideAnim = spectre.Dev_GetAISettingByKeyField( "OverrideOverloadAnim" )
+
+ if ( overrideAnim != null )
+ {
+ anim = expect string( overrideAnim )
+ }
+
+ waitthread PlayAnim( spectre, anim )
+
+ if ( !isFragDrone )
+ wait 0.25
+ }
+}
+
+void function JumpAtTitan( entity spectre, entity enemy )
+{
+ vector myOrigin = spectre.GetOrigin()
+ vector dirToEnemy = enemy.EyePosition() - myOrigin
+
+ float dist = Length( dirToEnemy )
+ if ( dist > 0 )
+ {
+ const float MAX_DIST = 100
+ dirToEnemy *= min( MAX_DIST, dist ) / dist
+ }
+
+ vector refOrigin = myOrigin + Vector( dirToEnemy.x, dirToEnemy.y, 256 )
+ vector refAngles = spectre.GetAngles() + Vector( 0, 180, 0 )
+ spectre.Anim_ScriptedPlayWithRefPoint( "sd_jump_explode", refOrigin, refAngles, 0.3 )
+ WaittillAnimDone( spectre )
+ return
+}
+
+int function GetExplosionTeamBasedOnGamemode( entity spectre )
+{
+ return spectre.GetTeam()
+}
+
+
+/************************************************************************************************\
+
+######## ### ## ## ### ###### ########
+## ## ## ## ### ### ## ## ## ## ##
+## ## ## ## #### #### ## ## ## ##
+## ## ## ## ## ### ## ## ## ## #### ######
+## ## ######### ## ## ######### ## ## ##
+## ## ## ## ## ## ## ## ## ## ##
+######## ## ## ## ## ## ## ###### ########
+
+\************************************************************************************************/
+void function SpectreSuicideOnDamaged_Callback( entity spectre, var damageInfo )
+{
+ SpectreSuicideOnDamaged( spectre, damageInfo )
+}
+
+
+void function SpectreSuicideOnDamaged( entity spectre, var damageInfo )
+{
+ //Assert( IsSuicideSpectre( spectre ) )
+
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ DamageInfo_SetCustomDamageType( damageInfo, damageType )
+
+ if ( !IsAlive( spectre ) )
+ return
+
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ // Calculate build time credit
+ if ( attacker.IsPlayer() )
+ {
+ if ( GameModeRulesShouldGiveTimerCredit( attacker, spectre, damageInfo ) && !TitanDamageRewardsTitanCoreTime() )
+ {
+ float timerCredit = CalculateBuildTimeCredit( attacker, spectre, damage, spectre.GetHealth(), spectre.GetMaxHealth(), "spectre_kill_credit", 9 )
+ if ( timerCredit )
+ DecrementBuildTimer( attacker, timerCredit )
+ }
+ }
+
+ // No pain anims for suicide spectres
+ DamageInfo_AddDamageFlags( damageInfo, DAMAGEFLAG_NOPAIN )
+
+
+ spectre.Signal( "SuicideSpectreExploding" )
+
+ if ( !IsValid( inflictor ) || !inflictor.IsPlayer() )
+ {
+ if ( spectre.ai.suicideSpectreExplodingAttacker == null )
+ {
+ if ( spectre.GetHealth() - damage <= 0 || ( IsValid( inflictor ) && IsTick( inflictor ) ) )
+ {
+ float explosionTime = GetSpectreExplosionTime( spectre )
+ SetSuicideSpectreExploding( spectre, attacker, explosionTime )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ }
+ else
+ {
+ // already exploding
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ DamageInfo_SetDamage( damageInfo, damage )
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret.gnut
new file mode 100644
index 00000000..eca5849b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret.gnut
@@ -0,0 +1,24 @@
+global function AiTurret_Init
+global function GetMegaTurretLinkedToPanel
+global function MegaTurretUsabilityFunc
+global function SetUsePromptForPanel
+
+void function AiTurret_Init()
+{
+
+}
+
+entity function GetMegaTurretLinkedToPanel(entity panel)
+{
+ return null
+}
+
+string function MegaTurretUsabilityFunc(var turret, var panel)
+{
+ return "pilot"
+}
+
+void function SetUsePromptForPanel(var panel, var turret)
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret_sentry.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret_sentry.gnut
new file mode 100644
index 00000000..e34b3082
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret_sentry.gnut
@@ -0,0 +1,72 @@
+global function AiTurretSentry_Init
+
+const DEAD_SENTRY_TURRET_FX = $"P_impact_exp_med_air"
+const DEAD_SENTRY_TURRET_SFX = "SentryTurret_DeathExplo"
+const SENTRY_TURRET_AIM_FX_RED = $"P_wpn_lasercannon_aim_short"
+const SENTRY_TURRET_AIM_FX_BLUE = $"P_wpn_lasercannon_aim_short_blue"
+
+void function AiTurretSentry_Init()
+{
+ PrecacheParticleSystem( DEAD_SENTRY_TURRET_FX )
+ //PrecacheParticleSystem( SENTRY_TURRET_AIM_FX_RED )
+ //PrecacheParticleSystem( SENTRY_TURRET_AIM_FX_BLUE )
+ //PrecacheParticleSystem( SENTRY_TURRET_AIM_FX2 )
+
+ AddSpawnCallback( "npc_turret_sentry", LightTurretSpawnFunction )
+ AddDeathCallback( "npc_turret_sentry", LightTurretDeathFX )
+
+ //RegisterSignal( "TurretDisabled" )
+ //RegisterSignal( "HandleTargetDeath" )
+ //RegisterSignal( "OnPlayerDisconnectResetTurret" )
+ //RegisterSignal( "Deactivate_Turret" )
+ //RegisterSignal( "TurretShieldWallRelease")
+ //RegisterSignal( "DestroyShieldFX")
+}
+
+void function LightTurretDeathFX( entity turret, var damageInfo )
+{
+ turret.SetBodygroup( 0, 1 )
+
+ int turretEHandle = turret.GetEncodedEHandle()
+ array<entity> players = GetPlayerArray()
+ foreach( player in players )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_TurretRefresh", turretEHandle )
+ }
+
+ EmitSoundAtPosition( turret.GetTeam(), turret.GetOrigin(), DEAD_SENTRY_TURRET_SFX )
+ PlayFX( DEAD_SENTRY_TURRET_FX, turret.GetOrigin() + Vector( 0,0,38 ) ) // played with a slight offset as requested by BigRig
+}
+
+//////////////////////////////////////////////////////////
+void function LightTurretSpawnFunction( entity turret )
+{
+ turret.UnsetUsable()
+
+// float windupTime = TurretGetWindupTime( turret )
+// if ( windupTime > 0 )
+// thread HACK_TurretManagePreAttack( turret, OnWindupBegin_SentryTurret, OnWindupEnd_Turret )
+//
+ if ( turret.Dev_GetAISettingByKeyField( "aim_laser_disabled" ) )
+ return
+
+ thread SentryTurretAimLaser( turret )
+}
+
+void function SentryTurretAimLaser( entity turret )
+{
+ entity fx1 = PlayLoopFXOnEntity( SENTRY_TURRET_AIM_FX_RED, turret, "camera_glow", null, null, ENTITY_VISIBLE_TO_ENEMY )
+ entity fx2 = PlayLoopFXOnEntity( SENTRY_TURRET_AIM_FX_BLUE, turret, "camera_glow", null, null, ENTITY_VISIBLE_TO_FRIENDLY )
+
+ OnThreadEnd(
+ function() : ( fx1, fx2 )
+ {
+ if ( IsValid( fx1 ) )
+ EffectStop( fx1 )
+ if ( IsValid( fx2 ) )
+ EffectStop( fx2 )
+ }
+ )
+
+ WaitSignal( turret, "OnDeath" )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_utility.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_utility.gnut
new file mode 100644
index 00000000..67c68600
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_utility.gnut
@@ -0,0 +1,558 @@
+untyped
+
+globalize_all_functions
+
+function AiUtility_Init()
+{
+ RegisterSignal( "OnNewOwner" )
+ RegisterSignal( "squadInCombat" )
+ RegisterSignal( "OnEndFollow" )
+ RegisterSignal( "OnStunned" )
+
+}
+////////////////////////////////////////////////////////////////////////////////
+// Cloaks npc forever (to be used by anim events)
+function NpcCloakOn( npc )
+{
+ //SetCloakDuration( fade in, duration, fade out )
+ npc.SetCloakDuration( 2.0, -1, 0 )
+ EmitSoundOnEntity( npc, CLOAKED_DRONE_CLOAK_START_SFX )
+ EmitSoundOnEntity( npc, CLOAKED_DRONE_CLOAK_LOOP_SFX )
+ npc.Minimap_Hide( TEAM_IMC, null )
+ npc.Minimap_Hide( TEAM_MILITIA, null )
+}
+////////////////////////////////////////////////////////////////////////////////
+// De-cloaks npc
+function NpcCloakOff( npc)
+{
+ npc.SetCloakDuration( 0, 0, 1.5 )
+ StopSoundOnEntity( npc, CLOAKED_DRONE_CLOAK_LOOP_SFX )
+ npc.Minimap_AlwaysShow( TEAM_IMC, null )
+ npc.Minimap_AlwaysShow( TEAM_MILITIA, null )
+}
+
+int function GetDefaultNPCFollowBehavior( npc )
+{
+ switch ( npc.GetAIClass() )
+ {
+ case AIC_FLYING_DRONE:
+ return AIF_SUPPORT_DRONE
+
+ case AIC_VEHICLE:
+ return AIF_GUNSHIP
+
+ case AIC_TITAN:
+ case AIC_TITAN_BUDDY:
+ return AIF_TITAN_FOLLOW_PILOT
+ }
+
+ return AIF_FIRETEAM
+}
+
+void function DieOnPlayerDisconnect( entity npc, entity player )
+{
+ Assert( IsNewThread(), "Must be threaded off" )
+ Assert( npc.IsNPC() )
+ Assert( player.IsPlayer() )
+ Assert( IsAlive( npc ) )
+ Assert( npc.GetBossPlayer() == player )
+ Assert( !IsDisconnected( player ) )
+ npc.EndSignal( "OnDeath" )
+
+ player.WaitSignal( "OnDestroy" )
+
+ // my boss quit the server!
+ if ( IsAlive( npc ) && npc.GetBossPlayer() == player )
+ npc.Die()
+}
+
+void function NPCFollowsPlayer( entity npc, entity leader )
+{
+ Assert( IsAlive( npc ) )
+ Assert( leader.IsPlayer() )
+
+ npc.SetBossPlayer( leader )
+
+ // team
+ SetTeam( npc, leader.GetTeam() )
+
+ if ( IsSpectre( npc ) )
+ {
+ string squadName = GetPlayerSpectreSquadName( leader )
+ SetSquad( npc, squadName )
+ }
+
+ thread DieOnPlayerDisconnect( npc, leader )
+ #if SP
+ Highlight_SetFriendlyHighlight( npc, "friendly_ai" )
+ #else
+ Highlight_SetOwnedHighlight( npc, "friendly_ai" )
+ #endif
+
+ NpcFollowsEntity( npc, leader )
+}
+
+void function NPCFollowsNPC( entity npc, entity leader )
+{
+ Assert( IsAlive( npc ) )
+ Assert( IsAlive( leader ) )
+ Assert( leader.IsNPC() )
+
+ // team
+ SetTeam( npc, leader.GetTeam() )
+
+ // squad
+ string squadNameOwner = expect string( leader.Get( "squadname" ) )
+ if ( squadNameOwner != "" && leader.GetClassName() == npc.GetClassName() )
+ SetSquad( npc, squadNameOwner )
+
+ NpcFollowsEntity( npc, leader )
+}
+
+void function NpcFollowsEntity( entity npc, entity leader )
+{
+ // stop scripted things
+ if ( IsMultiplayer() )
+ npc.Signal( "StopHardpointBehavior" )
+
+ if ( leader.IsPlayer() && leader.p.followPlayerOverride != null )
+ {
+ leader.p.followPlayerOverride( npc, leader )
+ return
+ }
+
+ // follow!
+ int followBehavior = GetDefaultNPCFollowBehavior( npc )
+ npc.InitFollowBehavior( leader, followBehavior )
+ npc.DisableBehavior( "Assault" )
+ npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_USE_SHOOTING_COVER )
+ npc.EnableBehavior( "Follow" )
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////////
+bool function HasEnemyWithinDist( entity npc, float dist )
+{
+ float distSq = dist * dist
+
+ array<entity> enemies
+ entity closestEnemy = npc.GetClosestEnemy()
+ if ( closestEnemy )
+ enemies.append( closestEnemy )
+
+ entity currentEnemy = npc.GetEnemy()
+ if ( currentEnemy && currentEnemy != closestEnemy )
+ enemies.append( currentEnemy )
+
+ if ( !enemies.len() )
+ return false
+
+ vector origin = npc.GetOrigin()
+ foreach ( enemy in enemies )
+ {
+ if ( DistanceSqr( origin, enemy.GetOrigin() ) < distSq )
+ return true
+ }
+
+ return false
+}
+
+SpawnPointFP function FindSpawnPointForNpcCallin( entity npc, asset model, string anim )
+{
+ float yaw = npc.EyeAngles().y
+
+ vector npcView = AnglesToForward( npc.EyeAngles() )
+ FlightPath flightPath = GetAnalysisForModel( model, anim )
+
+ CallinData drop
+ InitCallinData( drop )
+ SetCallinStyle( drop, eDropStyle.NEAREST_YAW_FALLBACK )
+ SetCallinOwnerEyePos( drop, npc.EyePosition() )
+ drop.dist = 800
+ drop.origin = npc.GetOrigin() + npcView * 250
+ drop.yaw = yaw
+
+ vector angles = Vector( 0, yaw, 0 )
+ SpawnPointFP spawnPoint = GetSpawnPointForStyle( flightPath, drop )
+ if ( spawnPoint.valid )
+ return spawnPoint
+
+ //if it didn't find one where he was looking - try near him
+ drop.origin = npc.GetOrigin()
+ spawnPoint = GetSpawnPointForStyle( flightPath, drop )
+
+ return spawnPoint
+}
+
+function WaitForSquadInCombat( squad )
+{
+ local master = {}
+
+ //when the thread ends, let child threads now
+ OnThreadEnd(
+ function() : ( master )
+ {
+ Signal( master, "OnDestroy" )
+ }
+ )
+
+ // this internal function keeps track of each guy
+ local combatTracker =
+ function( guy, master )
+ {
+ expect entity( guy )
+ expect entity( master )
+
+ EndSignal( master, "OnDestroy" )
+ EndSignal( guy, "OnDeath", "OnDestroy" )
+ if ( !IsAlive( guy ) )
+ return
+
+ while ( guy.GetNPCState() != "combat" )
+ guy.WaitSignal( "OnStateChange" )
+
+ Signal( master, "squadInCombat" )
+ }
+
+ foreach ( guy in squad )
+ {
+ thread combatTracker( guy, master )
+ }
+
+ WaitSignal( master, "squadInCombat" )
+}
+
+function WaitForNpcInCombat( npc )
+{
+ while ( npc.GetNPCState() != "combat" )
+ npc.WaitSignal( "OnStateChange" )
+}
+
+int function GetNpcHullType( entity npc )
+{
+ string aiSettings = npc.GetAISettingsName()
+ return int ( Dev_GetAISettingByKeyField_Global( aiSettings, "HullType" ) )
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+// "SPAWN AI" DEV MENU Fuctions
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+const float CROSSHAIR_VERT_OFFSET = 32
+
+vector function GetPlayerCrosshairOriginRaw( entity player )
+{
+ vector angles = player.EyeAngles()
+ vector forward = AnglesToForward( angles )
+ vector origin = player.EyePosition()
+
+ vector start = origin
+ vector end = origin + forward * 50000
+ TraceResults result = TraceLine( start, end )
+ vector crosshairOrigin = result.endPos
+
+ return crosshairOrigin
+}
+
+vector function GetPlayerCrosshairOrigin( entity player )
+{
+ return (GetPlayerCrosshairOriginRaw( player ) + Vector( 0, 0, CROSSHAIR_VERT_OFFSET ))
+}
+
+void function DEV_SpawnBTAtCrosshair( bool hotdrop = false )
+{
+ DisablePrecacheErrors()
+ wait 0.2
+ entity player = GetPlayerArray()[ 0 ]
+
+ entity pet_titan = player.GetPetTitan()
+ if ( IsValid(pet_titan) )
+ pet_titan.Destroy()
+
+ vector origin = GetPlayerCrosshairOrigin( player )
+ vector angles = Vector( 0, 0, 0 )
+
+ TitanLoadoutDef loadout = GetTitanLoadoutForCurrentMap()
+ entity npc = CreateAutoTitanForPlayer_FromTitanLoadout( player, loadout, origin, angles )
+
+ SetSpawnOption_AISettings( npc, "npc_titan_buddy" )
+
+ DispatchSpawn( npc )
+
+ SetPlayerPetTitan( player, npc )
+
+ if ( hotdrop )
+ thread NPCTitanHotdrops( npc, false )
+}
+
+void function DEV_SpawnAllNPCsWithTeam( int team )
+{
+ printt( "script thread DEV_SpawnAllNPCsWithTeam( " + team + " )" )
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+ bool restoreHostThreadMode = GetConVarInt( "host_thread_mode" ) != 0
+ if ( restoreHostThreadMode )
+ {
+ DisablePrecacheErrors()
+ wait 0.5
+ }
+
+ entity player = GetPlayerArray()[ 0 ]
+ vector origin = GetPlayerCrosshairOrigin( player )
+ array<string> aiSettings = GetAllNPCSettings()
+
+ foreach ( settings in aiSettings )
+ {
+ vector angles = < 0, RandomFloat( 360 ), 0 >
+ entity npc = CreateNPCFromAISettings( settings, team, origin, angles )
+ DispatchSpawn( npc )
+ }
+
+ if ( restoreHostThreadMode )
+ {
+ wait 0.2
+ RestorePrecacheErrors()
+ }
+}
+
+void function DEV_SpawnNPCWithWeaponAtCrosshair( string baseClass, string aiSettings, int team, string weaponName = "" )
+{
+ printt( "script thread DEV_SpawnNPCWithWeaponAtCrosshair( \"" + baseClass + "\", \"" + aiSettings + "\", " + team + ", \"" + weaponName + "\")" )
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+ bool restoreHostThreadMode = GetConVarInt( "host_thread_mode" ) != 0
+ entity npc = DEV_SpawnNPCWithWeaponAtCrosshairStart( restoreHostThreadMode, baseClass, aiSettings, team, weaponName )
+ DispatchSpawn( npc )
+ DEV_SpawnNPCWithWeaponAtCrosshairEnd( restoreHostThreadMode )
+}
+
+void function DEV_SpawnMercTitanAtCrosshair( string mercName )
+{
+ printt( "script thread DEV_SpawnMercTitanAtCrosshair( \"" + mercName + "\")" )
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+ TitanLoadoutDef ornull loadout = GetTitanLoadoutForBossCharacter( mercName )
+ if ( loadout == null )
+ return
+ expect TitanLoadoutDef( loadout )
+ string baseClass = "npc_titan"
+ string aiSettings = GetNPCSettingsFileForTitanPlayerSetFile( loadout.setFile )
+
+ bool restoreHostThreadMode = GetConVarInt( "host_thread_mode" ) != 0
+ entity npc = DEV_SpawnNPCWithWeaponAtCrosshairStart( restoreHostThreadMode, baseClass, aiSettings, TEAM_IMC )
+ SetSpawnOption_NPCTitan( npc, TITAN_MERC )
+ SetSpawnOption_TitanLoadout( npc, loadout )
+ npc.ai.bossTitanPlayIntro = false
+
+ DispatchSpawn( npc )
+ DEV_SpawnNPCWithWeaponAtCrosshairEnd( restoreHostThreadMode )
+}
+
+void function DEV_SpawnWeaponAtCrosshair( string weaponName )
+{
+ printt( "script thread DEV_SpawnWeaponAtCrosshair( \"" + weaponName + "\")" )
+
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+
+ entity player = GetPlayerArray()[ 0 ]
+ if ( !IsValid( player ) )
+ return
+ vector origin = GetPlayerCrosshairOrigin( player )
+ vector angles = Vector( 0, 0, 0 )
+ entity weapon = CreateWeaponEntityByNameWithPhysics( weaponName, origin, angles )
+
+#if SP
+ bool isTitanWeapon = weaponName.find( "mp_titanweapon_" ) != null
+ if ( isTitanWeapon )
+ thread TitanLoadoutWaitsForPickup( weapon, SPTitanLoadoutPickup )
+#endif
+
+}
+
+string function GetAISettingsFromPlayerSetFile( string playerSetfile )
+{
+ TitanLoadoutDef ornull loadout = GetTitanLoadoutForColumn( "setFile", playerSetfile )
+ Assert( loadout != null, "Couldn't find loadout with set file " + playerSetfile )
+ expect TitanLoadoutDef( loadout )
+
+ return expect string( Dev_GetPlayerSettingByKeyField_Global( playerSetfile, GetAISettingsStringForMode() ) )
+}
+
+
+void function DEV_SpawnBossTitanAtCrosshair( string playerSetfile )
+{
+ string aiSettings = GetAISettingsFromPlayerSetFile( playerSetfile )
+ printt( "script thread DEV_SpawnBossTitanAtCrosshair( \"" + aiSettings + "\")" )
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+
+ string baseClass = "npc_titan"
+ bool restoreHostThreadMode = GetConVarInt( "host_thread_mode" ) != 0
+ entity npc = DEV_SpawnNPCWithWeaponAtCrosshairStart( restoreHostThreadMode, baseClass, aiSettings, TEAM_IMC )
+ SetSpawnOption_NPCTitan( npc, TITAN_BOSS )
+// SetSpawnOption_TitanLoadout( npc, loadout )
+
+ string builtInLoadout = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" ) )
+// SetTitanSettings( npc.ai.titanSettings, builtInLoadout )
+ npc.ai.titanSpawnLoadout.setFile = builtInLoadout
+ OverwriteLoadoutWithDefaultsForSetFile( npc.ai.titanSpawnLoadout ) // get the entire loadout, including defensive and tactical
+
+ DispatchSpawn( npc )
+ DEV_SpawnNPCWithWeaponAtCrosshairEnd( restoreHostThreadMode )
+}
+
+entity function DEV_SpawnNPCWithWeaponAtCrosshairStart( bool restoreHostThreadMode, string baseClass, string aiSettings, int team, string weaponName = "" )
+{
+ if ( restoreHostThreadMode )
+ {
+ DisablePrecacheErrors()
+ wait 0.5
+ }
+
+ float time = Time()
+ for ( ;; )
+ {
+ if ( Time() > time )
+ break
+ WaitFrame()
+ }
+ entity player = GetPlayerArray()[ 0 ]
+ if ( !IsValid( player ) )
+ return
+
+ vector origin = GetPlayerCrosshairOrigin( player )
+ vector angles = Vector( 0, 0, 0 )
+
+ entity npc = CreateNPC( baseClass, team, origin, angles )
+ if ( IsTurret( npc ) )
+ npc.kv.origin -= Vector( 0, 0, CROSSHAIR_VERT_OFFSET )
+ SetSpawnOption_AISettings( npc, aiSettings )
+
+ if ( npc.GetClassName() == "npc_soldier" || npc.GetClassName() == "npc_spectre" )
+ npc.kv.squadname = "crosshairSpawnSquad_team_" + team + "_" + baseClass + "_" + aiSettings
+
+ if ( weaponName != "" )
+ SetSpawnOption_Weapon( npc, weaponName )
+
+ if ( npc.GetClassName() == "npc_titan" )
+ {
+ string builtInLoadout = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" ) )
+ SetTitanSettings( npc.ai.titanSettings, builtInLoadout )
+ npc.ai.titanSpawnLoadout.setFile = builtInLoadout
+ OverwriteLoadoutWithDefaultsForSetFile( npc.ai.titanSpawnLoadout ) // get the entire loadout, including defensive and tactical
+ }
+
+ return npc
+}
+
+void function DEV_SpawnNPCWithWeaponAtCrosshairEnd( bool restoreHostThreadMode )
+{
+ if ( restoreHostThreadMode )
+ {
+ wait 0.2
+ RestorePrecacheErrors()
+ }
+}
+
+
+function SetAISettingsWrapper( entity npc, string settings )
+{
+ npc.SetAISettings( settings )
+ Assert( settings.find( npc.GetClassName() ) == 0, "NPC classname " + npc.GetClassName() + " not found in " + settings )
+
+ if ( IsSingleplayer() )
+ {
+ FixupTitle( npc )
+ }
+}
+
+bool function WithinEngagementRange( entity npc, vector origin )
+{
+ entity weapon = npc.GetActiveWeapon()
+ if ( weapon == null )
+ return false
+
+ float dist = Distance( npc.GetOrigin(), origin )
+ if ( dist < weapon.GetWeaponInfoFileKeyField( "npc_min_engage_range" ) )
+ return false
+
+ return dist <= weapon.GetWeaponInfoFileKeyField( "npc_max_engage_range" )
+}
+
+
+function DEV_AITitanDuel()
+{
+ thread DEV_AITitanDuelThread()
+}
+
+entity function DEV_AITitanDuelSpawn( entity player, int team, vector origin, vector angles, aiSetting )
+{
+ entity titan = CreateNPC( "npc_titan", team, origin, angles )
+ SetSpawnOption_AISettings( titan, aiSetting )
+ DispatchSpawn( titan )
+
+ vector ornull clampedPos = NavMesh_ClampPointForAI( origin, titan )
+ if ( clampedPos != null )
+ {
+ titan.SetOrigin( expect vector( clampedPos ) )
+ }
+ else
+ {
+ array<entity> spawnpoints = SpawnPoints_GetTitan()
+ if ( spawnpoints.len() )
+ {
+ entity spawnpoint = GetClosest( spawnpoints, origin )
+ titan.SetOrigin( spawnpoint.GetOrigin() )
+ }
+ }
+
+ return titan
+}
+
+function DEV_AITitanDuelThread()
+{
+ DisablePrecacheErrors()
+ wait 0.5
+
+ array<string> aiSettings = GetAllowedTitanAISettings()
+
+ aiSettings.randomize()
+
+ entity player = GetPlayerArray()[ 0 ]
+
+ entity imcTitan = null
+ entity militiaTitan = null
+
+ int currentSetting = 0
+
+
+ while ( 1 )
+ {
+ if ( !IsValid( imcTitan ) )
+ {
+ vector origin = GetPlayerCrosshairOrigin( player ) + < -300, -300, 0 >
+ vector angles = Vector( 0, 0, 0 )
+
+ imcTitan = DEV_AITitanDuelSpawn( player, TEAM_IMC, origin, angles, aiSettings[currentSetting] )
+ currentSetting = (currentSetting + 1) % aiSettings.len()
+
+ if ( IsValid( militiaTitan ) )
+ {
+ imcTitan.SetEnemyLKP( militiaTitan, militiaTitan.GetOrigin() )
+ militiaTitan.SetEnemyLKP( imcTitan, imcTitan.GetOrigin() )
+ }
+ }
+
+ if ( !IsValid( militiaTitan ) )
+ {
+ vector origin = GetPlayerCrosshairOrigin( player ) + < 300, 300, 0 >
+ vector angles = Vector( 0, 180, 0 )
+
+ militiaTitan = DEV_AITitanDuelSpawn( player, TEAM_MILITIA, origin, angles, aiSettings[currentSetting] )
+ currentSetting = (currentSetting + 1) % aiSettings.len()
+
+ if ( IsValid( imcTitan ) )
+ {
+ militiaTitan.SetEnemyLKP( imcTitan, imcTitan.GetOrigin() )
+ imcTitan.SetEnemyLKP( militiaTitan, militiaTitan.GetOrigin() )
+ }
+ }
+
+ wait 2
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod.gnut
new file mode 100644
index 00000000..40a7d932
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod.gnut
@@ -0,0 +1,187 @@
+untyped
+
+global function DropPod_Init
+
+global function CreateDropPod
+global function LaunchAnimDropPod
+global function GetDropPodAnimDuration
+global function CreateDropPodSmokeTrail
+
+const DP_COLL_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam_collision.mdl"
+const DROPPOD_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam.mdl"
+
+function DropPod_Init()
+{
+ PrecacheModel( DROPPOD_MODEL )
+
+ RegisterSignal( "OnLaunch" )
+ RegisterSignal( "OnImpact" )
+
+ PrecacheModel( DP_COLL_MODEL )
+
+ PrecacheEffect( $"droppod_trail" )
+ PrecacheEffect( $"droppod_impact" )
+}
+
+
+function GetDropPodAnimDuration()
+{
+ // hack seems bad to spawn an ent to get this info
+ entity dropPod = CreateDropPod()
+
+ local animDuration = dropPod.GetSequenceDuration( "pod_testpath" )
+ dropPod.Destroy()
+
+ return animDuration
+}
+
+function LaunchAnimDropPod( entity dropPod, string anim, vector targetOrigin, vector targetAngles )
+{
+ dropPod.EndSignal( "OnDestroy" )
+ dropPod.EnableRenderAlways()
+
+ dropPod.s.launchAnim <- anim
+
+ int team = dropPod.GetTeam()
+
+ entity ref = CreateOwnedScriptMover( dropPod )
+ ref.SetOrigin( targetOrigin )
+ ref.SetAngles( targetAngles )
+
+ OnThreadEnd(
+ function () : ( dropPod, ref )
+ {
+ if ( IsValid( dropPod ) )
+ {
+ dropPod.ClearParent()
+ }
+
+ if ( IsValid( ref ) )
+ ref.Kill_Deprecated_UseDestroyInstead()
+ }
+ )
+
+ local e = {}
+ e.targetOrigin <- targetOrigin
+ e.targetAngles <- targetAngles
+
+ AddAnimEvent( dropPod, "OnImpact", DropPodOnImpactFXAndShake, e )
+ EmitSoundOnEntity( dropPod, "spectre_drop_pod" )
+
+ FirstPersonSequenceStruct sequence
+ sequence.thirdPersonAnim = anim
+
+ sequence.blendTime = 0.0
+ sequence.attachment = "ref"
+ sequence.useAnimatedRefAttachment = true
+ //DrawArrow( ref.GetOrigin(), ref.GetAngles(), 5, 100 )
+ waitthread FirstPersonSequence( sequence, dropPod, ref )
+ dropPod.DisableRenderAlways()
+// WaitFrame()
+}
+
+function CheckPlayersIntersectingPod( pod, targetOrigin )
+{
+ array<entity> playerList = GetPlayerArray()
+
+ // Multiplying the bounds by 1.42 to ensure this encloses the droppod when it's rotated 45 degrees
+ local mins = pod.GetBoundingMins() * 1.42 + targetOrigin
+ local maxs = pod.GetBoundingMaxs() * 1.42 + targetOrigin
+ local safeRadiusSqr = 250 * 250
+
+ foreach ( player in playerList )
+ {
+ local playerOrigin = player.GetOrigin()
+
+ if ( DistanceSqr( targetOrigin, playerOrigin ) > safeRadiusSqr )
+ continue
+
+ local playerMins = player.GetBoundingMins() + playerOrigin
+ local playerMaxs = player.GetBoundingMaxs() + playerOrigin
+
+ if ( BoxIntersectsBox( mins, maxs, playerMins, playerMaxs ) )
+ return true
+ }
+
+ return false
+}
+
+entity function CreateDropPod( vector ornull origin = null, vector ornull angles = null )
+{
+ entity prop_dynamic = CreateEntity( "prop_dynamic" )
+ prop_dynamic.SetValueForModelKey( DROPPOD_MODEL )
+ prop_dynamic.kv.contents = int( prop_dynamic.kv.contents ) & ~CONTENTS_TITANCLIP
+ prop_dynamic.kv.fadedist = -1
+ prop_dynamic.kv.renderamt = 255
+ prop_dynamic.kv.rendercolor = "255 255 255"
+ prop_dynamic.kv.solid = 6 // 0 = no collision, 2 = bounding box, 6 = use vPhysics, 8 = hitboxes only
+ if ( origin )
+ {
+ prop_dynamic.SetOrigin( expect vector( origin ) )
+ if ( angles )
+ prop_dynamic.SetAngles( expect vector( angles ) )
+ }
+ DispatchSpawn( prop_dynamic )
+
+ return prop_dynamic
+}
+
+void function PushPlayerAndCreateDropPodCollision( entity pod, vector targetOrigin )
+{
+ pod.EndSignal( "OnDestroy" )
+
+ entity point_push = CreateEntity( "point_push" )
+ point_push.kv.spawnflags = 8
+ point_push.kv.enabled = 1
+ point_push.kv.magnitude = 140.0 * 0.75 //Compensate for reduced player gravity to match R1
+ point_push.kv.radius = 192.0
+ point_push.SetOrigin( targetOrigin + Vector( 0.0, 0.0, 32.0 ) )
+ DispatchSpawn( point_push )
+
+ OnThreadEnd(
+ function() : ( point_push )
+ {
+ point_push.Fire( "Kill", "", 0.0 )
+ }
+ )
+
+ while ( CheckPlayersIntersectingPod( pod, targetOrigin ) )
+ wait( 0.1 )
+
+ pod.Solid()
+}
+
+function DropPodOnImpactFX( droppod, e )
+{
+ PlayImpactFXTable( expect vector( e.targetOrigin ), expect entity( droppod ), HOTDROP_IMPACT_FX_TABLE )
+}
+
+void function DropPodOnImpactFXAndShake( entity droppod )
+{
+ var e = GetOptionalAnimEventVar( droppod, "OnImpact" )
+ DropPodOnImpactFX( droppod, e )
+ CreateShake( expect vector( e.targetOrigin ), 7, 0.15, 1.75, 768 )
+
+ // 1 - No Damage - Only Force
+ // 2 - Push players
+ // 8 - Test LOS before pushing
+ local flags = 11
+ local impactOrigin = e.targetOrigin + Vector( 0,0,10 )
+ local impactRadius = 192
+ thread PushPlayerAndCreateDropPodCollision( droppod, expect vector( e.targetOrigin ) )
+}
+
+
+function CreateDropPodSmokeTrail( pod )
+{
+ entity smokeTrail = CreateEntity( "info_particle_system" )
+ smokeTrail.SetValueForEffectNameKey( $"droppod_trail" )
+ smokeTrail.kv.start_active = 0
+ DispatchSpawn( smokeTrail )
+
+ smokeTrail.SetOrigin( pod.GetOrigin() + Vector( 0, 0, 152 ) )
+ smokeTrail.SetParent( pod )
+
+ return smokeTrail
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod_fireteam.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod_fireteam.gnut
new file mode 100644
index 00000000..b93631ac
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod_fireteam.gnut
@@ -0,0 +1,246 @@
+global function DropPodFireteam_Init
+
+global function InitFireteamDropPod
+global function ActivateFireteamDropPod
+global function DropPodActiveThink
+
+global function CreateDropPodDoor
+
+const DP_ARM_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam_arm.mdl"
+const DP_DOOR_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam_door.mdl"
+
+global enum eDropPodFlag
+{
+ DISSOLVE_AFTER_DISEMBARKS = (1<<0)
+}
+
+struct DroppodStruct
+{
+ entity door
+ bool openDoor = false
+ int numGuys = 0
+ int flags = 0
+}
+
+struct
+{
+ table<entity, DroppodStruct> droppodTable
+}
+file
+
+void function DropPodFireteam_Init()
+{
+ RegisterSignal( "OpenDoor" )
+
+ PrecacheModel( DP_DOOR_MODEL )
+ PrecacheModel( DP_ARM_MODEL )
+}
+
+void function InitFireteamDropPod( entity pod, int flags = 0 )
+{
+ pod.NotSolid()
+
+ DroppodStruct droppodData
+ droppodData.flags = flags
+ droppodData.door = CreateDropPodDoor( pod )
+ file.droppodTable[ pod ] <- droppodData
+
+ pod.Anim_Play( "idle" )
+}
+
+void function ActivateFireteamDropPod( entity pod, array<entity> guys )
+{
+ DroppodStruct droppodData = file.droppodTable[ pod ]
+ droppodData.openDoor = true
+ pod.Signal( "OpenDoor" )
+
+ if ( guys.len() >= 1 )
+ {
+ SetAnim( guys[0], "drop_pod_exit_anim", "pt_dp_exit_a" )
+ SetAnim( guys[0], "drop_pod_idle_anim", "pt_dp_idle_a" )
+ }
+
+ if ( guys.len() >= 2 )
+ {
+ SetAnim( guys[1], "drop_pod_exit_anim", "pt_dp_exit_b" )
+ SetAnim( guys[1], "drop_pod_idle_anim", "pt_dp_idle_b" )
+ }
+
+ if ( guys.len() >= 3 )
+ {
+ SetAnim( guys[2], "drop_pod_exit_anim", "pt_dp_exit_c" )
+ SetAnim( guys[2], "drop_pod_idle_anim", "pt_dp_idle_c" )
+ }
+
+ if ( guys.len() >= 4 )
+ {
+ SetAnim( guys[3], "drop_pod_exit_anim", "pt_dp_exit_d" )
+ SetAnim( guys[3], "drop_pod_idle_anim", "pt_dp_idle_d" )
+ }
+
+ foreach ( guy in guys )
+ {
+ if ( IsAlive( guy ) )
+ {
+ guy.MakeVisible()
+ entity weapon = guy.GetActiveWeapon()
+ if ( IsValid( weapon ) )
+ weapon.MakeVisible()
+
+ thread GuyHangsInPod( guy, pod )
+ }
+ }
+
+ thread DropPodActiveThink( pod )
+}
+
+void function DropPodActiveThink( entity pod )
+{
+ DroppodStruct droppodData = file.droppodTable[ pod ]
+
+ OnThreadEnd(
+ function() : ( pod )
+ {
+ DroppodStruct droppodData = file.droppodTable[pod]
+ if ( droppodData.flags & eDropPodFlag.DISSOLVE_AFTER_DISEMBARKS )
+ CleanupFireteamPod( pod )
+ else
+ delaythread( 10 ) CleanupFireteamPod( pod )
+ }
+ )
+
+ pod.EndSignal( "OnDestroy" )
+
+ if ( DropPodDoorInGround( pod ) )
+ droppodData.door.Destroy()
+ else
+ DropPodOpenDoor( pod, droppodData.door )
+
+ while ( droppodData.numGuys )
+ WaitFrame()
+}
+
+bool function DropPodDoorInGround( entity pod )
+{
+ string attachment = "hatch"
+ int attachIndex = pod.LookupAttachment( attachment )
+ vector end = pod.GetAttachmentOrigin( attachIndex )
+
+ string originAttachment = "origin"
+ int originAttachIndex = pod.LookupAttachment( originAttachment )
+ vector start = pod.GetAttachmentOrigin( originAttachIndex )
+
+ TraceResults result = TraceLine( start, end, pod, TRACE_MASK_SOLID, TRACE_COLLISION_GROUP_NONE )
+
+ return result.fraction < 1.0
+}
+
+void function CleanupFireteamPod( entity pod )
+{
+ DroppodStruct droppodData = file.droppodTable[ pod ]
+
+ if ( !IsValid( pod ) )
+ return
+
+ if ( IsValid( droppodData.door ) )
+ droppodData.door.Dissolve( ENTITY_DISSOLVE_CORE, Vector( 0, 0, 0 ), 500 )
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, pod.GetOrigin(), "droppod_dissolve" )
+
+ delete file.droppodTable[ pod ]
+
+ pod.NotSolid()
+ foreach( ent in pod.e.attachedEnts )
+ {
+ ent.NotSolid()
+ }
+ pod.Dissolve( ENTITY_DISSOLVE_CORE, Vector( 0, 0, 0 ), 500 )
+}
+
+entity function CreateDropPodDoor( entity pod )
+{
+ string attachment = "hatch"
+ int attachIndex = pod.LookupAttachment( attachment )
+ vector origin = pod.GetAttachmentOrigin( attachIndex )
+ vector angles = pod.GetAttachmentAngles( attachIndex )
+
+ entity prop_physics = CreateEntity( "prop_physics" )
+ SetTargetName( prop_physics, "door" + UniqueString() )
+ prop_physics.SetValueForModelKey( DP_DOOR_MODEL )
+ // Start Asleep
+ // Debris - Don't collide with the player or other debris
+ // Generate output on +USE
+ prop_physics.kv.spawnflags = 261 // non solid for now
+ prop_physics.kv.fadedist = -1
+ prop_physics.kv.physdamagescale = 0.1
+ prop_physics.kv.inertiaScale = 1.0
+ prop_physics.kv.renderamt = 0
+ prop_physics.kv.rendercolor = "255 255 255"
+
+ DispatchSpawn( prop_physics )
+
+ prop_physics.SetOrigin( origin )
+ prop_physics.SetAngles( angles )
+ prop_physics.SetParent( pod, "HATCH", false )
+ prop_physics.MarkAsNonMovingAttachment()
+
+ return prop_physics
+}
+
+void function DropPodOpenDoor( entity pod, entity door )
+{
+ door.ClearParent()
+ door.SetVelocity( door.GetForwardVector() * 500 )
+ EmitSoundOnEntity( pod, "droppod_door_open" )
+}
+
+void function GuyHangsInPod( entity guy, entity pod )
+{
+ DroppodStruct droppodData = file.droppodTable[ pod ]
+
+ guy.EndSignal( "OnDeath" )
+ guy.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( droppodData )
+ {
+ droppodData.numGuys--
+ }
+ )
+
+ droppodData.numGuys++
+
+ string idleAnim
+ string exitAnim
+
+ if ( !droppodData.openDoor )
+ {
+ guy.SetEfficientMode( true )
+
+ guy.SetParent( pod, "ATTACH", false )
+
+ idleAnim = expect string( GetAnim( guy, "drop_pod_idle_anim" ) )
+ if ( guy.LookupSequence( idleAnim ) != -1 )
+ guy.Anim_ScriptedPlay( idleAnim )
+
+ pod.WaitSignal( "OpenDoor" )
+
+ //wait POST_TURRET_DELAY
+
+ guy.SetEfficientMode( false )
+ }
+
+
+ guy.SetParent( pod, "ATTACH", false )
+
+ exitAnim = expect string ( GetAnim( guy, "drop_pod_exit_anim" ) )
+ bool exitAnimExists = guy.LookupSequence( exitAnim ) != -1
+ if ( exitAnimExists )
+ guy.Anim_ScriptedPlay( exitAnim )
+
+ guy.ClearParent()
+
+ if ( exitAnimExists )
+ WaittillAnimDone( guy )
+ guy.Signal( "npc_deployed" )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_grunt_chatter.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_grunt_chatter.gnut
new file mode 100644
index 00000000..f5c0c84d
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_grunt_chatter.gnut
@@ -0,0 +1,1786 @@
+// _grunt_chatter.gnut
+
+global function GruntChatter_Init
+global function GruntChatter_AddCategory
+global function GruntChatter_AddEvent
+global function GruntChatter_TryCloakedPilotSpotted
+global function GruntChatter_TryThrowingGrenade
+global function GruntChatter_TryFriendlyEquipmentDeployed
+global function GruntChatter_TryPersonalShieldDamaged
+global function GruntChatter_TryDisplacingFromDangerousArea
+global function GruntChatter_TryEnemyTimeShifted
+global function GruntChatter_TryIncomingSpawn
+global function GruntChatter_TryPlayerPilotReloading
+global function GruntChatter_TryGruntFlankedByPlayer
+
+const float CHATTER_THINK_WAIT = 1.0
+const float CHATTER_SIGNAL_INTERRUPT_WAIT = 1.0 // how often the grunts will interrupt their signal waiting thread to check their kv timers
+const float CHATTER_EVENT_EXPIRE_TIME = 3.0 // chatter events get thrown away when they're at least this old
+
+const float CHATTER_PLAYER_COMBAT_STATE_CHANGE_DEBOUNCE = 1.5
+
+const float CHATTER_PILOT_LOST_NEARBY_TEAMMATE_DIST = 1024.0
+const float CHATTER_PLAYER_CLOSE_MIN_DIST = 370.0 // all squad members have to be at least this far away from enemy to say they lost visual
+
+const float CHATTER_PILOT_SPOTTED_CLOSE_DIST = 600.0
+const float CHATTER_PILOT_SPOTTED_MID_DIST = 1100.0
+const float CHATTER_PILOT_SPOTTED_NEARBY_TEAMMATE_DIST = 1024.0
+
+const float CHATTER_PILOT_SPOTTED_MID_DIST_MOVING_MIN_SPEED = 170.0
+
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_MIN = 600.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_MAX = 1400.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_20 = 787.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_25 = 984.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_30 = 1181.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_35 = 1378.0
+
+const float CHATTER_PILOT_DECOY_SPOTTED_DIST_MAX = 1500.0
+
+const float CHATTER_ENEMY_GRUNT_SPOTTED_DIST = 1250.0
+const float CHATTER_ENEMY_TITAN_SPOTTED_DIST = 3000.0
+const float CHATTER_ENEMY_TITAN_SPOTTED_DIST_CLOSE = 1024.0
+const float CHATTER_ENEMY_SPECTRE_SPOTTED_DIST = 1250.0
+const float CHATTER_ENEMY_SPECTRE_SPOTTED_DIST_CLOSE = 650.0
+const float CHATTER_ENEMY_TICK_SPOTTED_DIST = 1024.0
+
+const float CHATTER_PILOT_SPOTTED_ABOVE_DIST_MIN = 128.0
+const float CHATTER_PILOT_SPOTTED_ABOVE_DIST_MAX = 1024.0
+const float CHATTER_PILOT_SPOTTED_ABOVE_RADIUS = 450.0
+const float CHATTER_PILOT_SPOTTED_BELOW_DIST_MIN = 128.0
+const float CHATTER_PILOT_SPOTTED_BELOW_DIST_MAX = 1024.0
+const float CHATTER_PILOT_SPOTTED_BELOW_RADIUS = 512.0
+
+const float CHATTER_GRUNT_ENEMY_OUT_OF_SIGHT_TIME = 15.0
+
+const float CHATTER_FRIENDLY_EQUIPMENT_DEPLOYED_NEARBY_DIST = 900.0 // distance from the Specialist that a Grunt will chatter about him deploying things
+
+const bool CHATTER_DO_UNSUSPECTING_PILOT_CALLOUTS = false // couldn't get it working well enough in time just in script... next game maybe
+const float CHATTER_UNSUSPECTING_PILOT_TARGET_DIST_MAX = 512.0
+const float CHATTER_UNSUSPECTING_PILOT_TARGET_MIN_DOT_REAR = 0.65
+const float CHATTER_UNSUSPECTING_PILOT_MAX_SPEED = 170.0 // player has to be below this speed to trigger "unsuspecting pilot"
+const float CHATTER_UNSUSPECTING_PILOT_STATETIME_MIN = 2.0 // how long the player has to be in "unsuspecting state" before we try to chatter about it
+
+const float CHATTER_SEE_CLOAKED_PILOT_MIN_DOT_REAR = 0.65
+
+const float CHATTER_SUPPRESSION_EXPIRE_TIME = 0.2 // secs after kv.lastSuppressionTime that we will be ok with adding a chatter event about it
+const float CHATTER_MISS_FAST_TARGET_EXPIRE_TIME = 0.5 // secs after kv.lastMissFastPlayerTime that we will be ok with adding a chatter event about it
+const float CHATTER_MISS_FAST_TARGET_MIN_SPEED = 350.0 // min "speed" that player needs to be moving to trigger a missing fast player callout
+
+const float CHATTER_PILOT_LOW_HEALTH_FRAC = 0.35 // below this fraction of pilot maxhealth, enemies can chatter about pilot low health
+const float CHATTER_PILOT_LOW_HEALTH_RANGE = 1024.0 // beyond this distance, enemies won't chatter about pilot low health
+const float CHATTER_PLAYER_RELOADING_RANGE = 800.0
+
+const float CHATTER_NEARBY_GRUNT_TRACEFRAC_MIN = 0.95 // for when we need "LOS" trace
+
+const float CHATTER_ENEMY_PILOT_MULTIKILL_EXPIRETIME = 4.5 // max time between kills to trigger multikill callout
+const int CHATTER_PILOT_MULTIKILL_MIN_KILLS = 3
+
+const float CHATTER_FRIENDLY_GRUNT_DOWN_DIST_MAX = 1100.0
+const float CHATTER_FRIENDLY_TITAN_DOWN_DIST_MAX = 1500.0
+const float CHATTER_ENEMY_PILOT_DOWN_DIST_MAX = 600.0
+const float CHATTER_ENEMY_GRUNT_DOWN_DIST_MAX = 800.0
+const float CHATTER_ENEMY_TITAN_DOWN_DIST_MAX = 1500.0
+const float CHATTER_ENEMY_SPECTRE_DOWN_DIST_MAX = 800.0
+
+const float CHATTER_NEARBY_TITAN_DIST = 1024.0
+const float CHATTER_NEARBY_REAPER_DIST = 1024.0
+const float CHATTER_NEARBY_SPECTRE_DIST = 800.0
+
+const float CHATTER_ENEMY_TIME_SHIFT_NEARBY_DIST = 700.0
+
+const float CHATTER_SQUAD_DEPLETED_FRIENDLY_NEARBY_DIST = 650.0 // if any other friendly grunt is within this dist, squad deplete chatter won't play
+
+const float CHATTER_DANGEROUS_AREA_NEARBY_RANGE = 512.0
+
+struct ChatterCategory
+{
+ string alias
+ int priority = -1
+ string timer
+ string secondaryTimer
+ bool trackEventTarget
+ bool resetTargetKillChain
+}
+
+struct ChatterEvent
+{
+ ChatterCategory& category
+ entity npc = null
+ bool hasNPC = false
+ entity target = null
+ bool hasTarget = false
+ bool isValid = false
+ float time = -1
+}
+
+struct
+{
+ array<ChatterEvent> chatterEvents = []
+ table< string, ChatterCategory > chatterCategories
+ int usedEventTargetsArrayHandle
+
+ int pilotKillChainCounter = 0
+ float lastPilotKillTime = -1
+
+ int debugLevel = 0
+} file
+
+void function GruntChatter_Init()
+{
+ Assert( IsSingleplayer(), "Grunt chatter is only set up for SP." )
+
+ AddSpawnCallback( "player", GruntChatter_OnPlayerSpawned )
+ AddSpawnCallback( "npc_soldier", GruntChatter_OnGruntSpawned )
+ AddSpawnCallback( "npc_turret_sentry", GruntChatter_OnSentryTurretSpawned )
+
+ RegisterSignal( "GruntChatter_CombatStateChangeThread" )
+ RegisterSignal( "GruntChatter_Interrupt" )
+
+ file.usedEventTargetsArrayHandle = CreateScriptManagedEntArray()
+
+ AddCallback_OnPlayerKilled( GruntChatter_OnPlayerOrNPCKilled )
+ AddCallback_OnNPCKilled( GruntChatter_OnPlayerOrNPCKilled )
+ AddDeathCallback( "player_decoy", GruntChatter_OnPilotDecoyKilled )
+
+ GruntChatter_SharedInit()
+}
+
+void function GruntChatter_OnPlayerSpawned( entity player )
+{
+ thread GruntChatter_PlayerThink( player )
+ thread GruntChatter_TrackGruntCombatStateVsPlayer( player )
+
+ if ( CHATTER_DO_UNSUSPECTING_PILOT_CALLOUTS )
+ thread GruntChatter_DetectPlayerPilotUnsuspecting( player )
+}
+
+void function GruntChatter_OnGruntSpawned( entity grunt )
+{
+ if( IsMultiplayer() )
+ return
+
+ if ( !GruntChatter_IsGruntTypeEligibleForChatter( grunt ) )
+ return
+
+ AddEntityCallback_OnDamaged( grunt, GruntChatter_OnGruntDamaged )
+
+ thread GruntChatter_GruntSignalWait( grunt )
+}
+
+void function GruntChatter_OnSentryTurretSpawned( entity turret )
+{
+ if ( turret.GetTeam() != TEAM_IMC )
+ return
+
+ thread GruntChatter_TurretSignalWait( turret )
+}
+
+// ==== chatter mission control ====
+void function GruntChatter_AddCategory( string chatterAlias, int priority, string timerAlias, string secondaryTimerAlias, bool trackEventTarget, bool resetTargetKillChain )
+{
+ Assert( !( chatterAlias in file.chatterCategories ), "Chatter alias already set up: " + chatterAlias )
+ Assert( TimerExists( timerAlias ), "Grunt chatter timer not set up in grunt_chatter_timers.csv: " + timerAlias )
+
+ ChatterCategory newCategory
+ newCategory.alias = chatterAlias
+ newCategory.priority = priority
+ newCategory.timer = timerAlias
+ newCategory.trackEventTarget = trackEventTarget
+ newCategory.resetTargetKillChain = resetTargetKillChain
+
+ if ( secondaryTimerAlias != "" )
+ newCategory.secondaryTimer = secondaryTimerAlias
+
+ file.chatterCategories[ chatterAlias ] <- newCategory
+}
+
+// add a grunt to have him chatter specifically
+// target: must be alive or else event won't fire
+void function GruntChatter_AddEvent( string alias, entity ornull npc = null, entity ornull target = null )
+{
+ Assert( alias in file.chatterCategories, "Couldn't find chatter category alias " + alias + ", was it set up?" )
+
+ ChatterEvent newEvent
+ newEvent.category = file.chatterCategories[ alias ]
+ newEvent.isValid = true
+ newEvent.time = Time()
+
+ if ( npc != null )
+ {
+ newEvent.npc = expect entity( npc )
+ newEvent.hasNPC = true
+ }
+
+ if ( file.chatterCategories[ alias ].trackEventTarget )
+ Assert( target != null, "Category " + file.chatterCategories[ alias ].alias + " requires a target to track for its events." )
+
+ if ( file.chatterCategories[ alias ].resetTargetKillChain )
+ Assert( target != null, "Category " + file.chatterCategories[ alias ].alias + " requires a target on which to record kill chains." )
+
+ if ( target != null )
+ {
+ newEvent.target = expect entity( target )
+ newEvent.hasTarget = true
+ }
+
+ if ( file.debugLevel > 1 )
+ printt( "ADDING EVENT:", newEvent.category.alias )
+
+ file.chatterEvents.append( newEvent )
+}
+
+void function GruntChatter_AddToUsedEventTargets( entity ent )
+{
+ Assert( !GruntChatter_EventTargetAlreadyUsed( ent ), "Ent already added to event targets: " + ent )
+ AddToScriptManagedEntArray( file.usedEventTargetsArrayHandle, ent )
+}
+
+bool function GruntChatter_EventTargetAlreadyUsed( entity ent )
+{
+ return ScriptManagedEntArrayContains( file.usedEventTargetsArrayHandle, ent )
+}
+
+void function GruntChatter_PlayerThink( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ while ( 1 )
+ {
+ wait CHATTER_THINK_WAIT
+
+ // squad conversations don't play to dead players
+ if ( !IsAlive( player ) )
+ continue
+
+ if ( player.GetForcedDialogueOnly() )
+ continue
+
+ if ( !file.chatterEvents.len() )
+ continue
+
+ if ( !TimerCheck( "chatter_global" ) )
+ continue
+
+ // prune expired chatter events if necessary
+ GruntChatter_RemoveExpiredEventsFromQueue()
+
+ // process chatter events
+ array< ChatterEvent > currChatterEvents = file.chatterEvents
+
+ ChatterEvent eventToPlay
+
+ foreach ( chatterEvent in currChatterEvents )
+ {
+ // check timer
+ if ( !TimerCheck( chatterEvent.category.timer ) )
+ continue
+
+ // check priority vs currently selected
+ if ( chatterEvent.category.priority < eventToPlay.category.priority )
+ continue
+
+ // check ents are still legit
+ if ( chatterEvent.hasNPC )
+ {
+ if ( !GruntChatter_CanGruntChatterNow( chatterEvent.npc ) )
+ continue
+
+ if ( !GruntChatter_CanGruntChatterToPlayer( chatterEvent.npc, player ) )
+ continue
+ }
+
+ if ( chatterEvent.hasTarget && !GruntChatter_CanChatterEventUseEnemyTarget( chatterEvent ) )
+ continue
+
+ // check which event is more current
+ if ( eventToPlay.time > chatterEvent.time )
+ continue
+
+ eventToPlay = chatterEvent
+ }
+
+ if ( eventToPlay.isValid )
+ {
+ string alias = eventToPlay.category.alias
+ string timer = eventToPlay.category.timer
+
+ entity grunt = eventToPlay.npc
+ // if the event didn't include a grunt, use the closest grunt as the source
+ if ( !IsValid( grunt ) )
+ {
+ // only human grunts should talk
+ array<entity> nearbyGrunts = GetNearbyEnemyHumanGrunts( player.GetOrigin(), player.GetTeam() )
+
+ if ( !nearbyGrunts.len() )
+ {
+ if ( file.debugLevel > 0 )
+ printt( "GRUNT CHATTER: can't play chatter event because nobody is close enough:", alias )
+
+ continue
+ }
+
+ nearbyGrunts = ArrayClosest( nearbyGrunts, player.GetOrigin() )
+ grunt = nearbyGrunts[0]
+ }
+
+ Assert( IsAlive( grunt ), "Grunt chatter error: need a grunt to talk" )
+
+ if ( file.debugLevel > 0 )
+ printt( "GRUNT CHATTER:", alias )
+
+ if ( eventToPlay.category.trackEventTarget )
+ GruntChatter_AddToUsedEventTargets( eventToPlay.target )
+
+ if ( eventToPlay.category.resetTargetKillChain )
+ GruntChatter_ResetPilotKillChain( eventToPlay.target )
+
+ PlaySquadConversationToAll( alias, grunt )
+ ChatterTimerReset( eventToPlay )
+
+ // throw away all the old chatter events now that we processed one
+ GruntChatter_FlushEventQueue()
+ }
+ }
+}
+
+void function GruntChatter_FlushEventQueue()
+{
+ file.chatterEvents = []
+}
+
+void function GruntChatter_RemoveExpiredEventsFromQueue()
+{
+ array< ChatterEvent > recentEvents = []
+ foreach ( event in file.chatterEvents )
+ {
+ if ( Time() - event.time >= CHATTER_EVENT_EXPIRE_TIME )
+ {
+ if ( file.debugLevel > 1 )
+ printt( "expired event:", event.category.alias, "time:", Time() - event.time )
+
+ continue
+ }
+
+ recentEvents.append( event )
+ }
+
+ file.chatterEvents = recentEvents
+}
+
+void function ChatterTimerReset( ChatterEvent event )
+{
+ TimerReset( "chatter_global" )
+ TimerReset( event.category.timer )
+
+ if ( event.category.secondaryTimer != "" )
+ TimerReset( event.category.secondaryTimer )
+}
+
+
+// ==== combat state tracking ====
+void function GruntChatter_TrackGruntCombatStateVsPlayer( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ while ( 1 )
+ {
+ wait 1.0
+
+ if ( !IsAlive( player ) )
+ continue
+
+ int currState = GruntChatter_GetGruntCombatStateVsPlayer( player )
+
+ if ( currState == svGlobalSP.gruntCombatState )
+ continue
+
+ if ( file.debugLevel > 1 )
+ printt( "combat state change:", currState )
+
+ thread GruntChatter_TryPlayerPilotCombatStateChange( player, currState, svGlobalSP.gruntCombatState )
+
+ svGlobalSP.gruntCombatState = currState
+ }
+}
+
+int function GruntChatter_GetGruntCombatStateVsPlayer( entity player )
+{
+ array<entity> enemies = GetNPCArrayEx( "npc_soldier", TEAM_ANY, player.GetTeam(), Vector( 0, 0, 0 ), -1 )
+ ArrayRemoveDead( enemies )
+
+ int currState = eGruntCombatState.IDLE
+
+ foreach ( npc in enemies )
+ {
+ if ( !IsAlive( npc ) )
+ continue
+
+ if ( npc.GetNPCState() == "alert" && currState != eGruntCombatState.COMBAT )
+ currState = eGruntCombatState.ALERT
+ else if ( npc.GetNPCState() == "combat" && npc.GetEnemy() == player )
+ return eGruntCombatState.COMBAT
+ }
+
+ return currState
+}
+
+
+// ==== player event handling ====
+// not currently used - I can't make it work well enough in script. Maybe code next game.
+void function GruntChatter_DetectPlayerPilotUnsuspecting( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ bool resetUnsuspectingTime = true
+ float unsuspectingTime = -1
+ array<entity> nearbyGrunts
+
+ while ( 1 )
+ {
+ if ( resetUnsuspectingTime )
+ {
+ if ( Time() - unsuspectingTime >= CHATTER_UNSUSPECTING_PILOT_STATETIME_MIN )
+ if ( file.debugLevel > 2 )
+ printt( "========== RESET UNSUSPECTING!" )
+
+ unsuspectingTime = Time()
+ }
+
+ wait 1.0
+
+ if ( !IsAlive( player ) )
+ continue
+
+ if ( !IsPilot( player ) )
+ continue
+
+ if ( Length( player.GetVelocity() ) > CHATTER_UNSUSPECTING_PILOT_MAX_SPEED )
+ continue
+
+ array<entity> validGrunts
+
+ nearbyGrunts = GetNearbyEnemyHumanGrunts( player.GetOrigin(), player.GetTeam(), CHATTER_UNSUSPECTING_PILOT_TARGET_DIST_MAX )
+ if ( !nearbyGrunts.len() )
+ continue
+
+ foreach ( grunt in nearbyGrunts )
+ {
+ if ( grunt.GetEnemy() != player )
+ continue
+
+ // don't care about facing direction, just if he can trace to the player
+ if ( !GruntChatter_CanGruntTraceToLocation( grunt, player.EyePosition() ) )
+ continue
+
+ if ( !GruntChatter_IsTargetFacingAway( grunt, player, CHATTER_UNSUSPECTING_PILOT_TARGET_MIN_DOT_REAR ) )
+ continue
+
+ validGrunts.append( grunt )
+ }
+
+ if ( !validGrunts.len() )
+ continue
+
+ resetUnsuspectingTime = false
+
+ if ( file.debugLevel > 2 )
+ printt( "========== PLAYER IS UNSUSPECTING!" )
+
+ if ( unsuspectingTime < Time() && Time() - unsuspectingTime < CHATTER_UNSUSPECTING_PILOT_STATETIME_MIN )
+ continue
+
+ if ( !TimerCheck( "chatter_pilot_target_unsuspecting" ) )
+ {
+ if ( file.debugLevel > 2 )
+ printt( "waiting for UNSUSPECTING chatter timer...")
+
+ continue
+ }
+
+ entity closestGrunt = GetClosest( validGrunts, player.GetOrigin() )
+ GruntChatter_AddEvent( "gruntchatter_pilot_target_unsuspecting", closestGrunt, player )
+
+ resetUnsuspectingTime = true
+ }
+}
+
+
+// ==== grunt event handling ====
+void function GruntChatter_GruntSignalWait( entity grunt )
+{
+ grunt.EndSignal( "OnDeath" )
+ grunt.EndSignal( "OnDestroy" )
+
+ while ( 1 )
+ {
+ thread GruntChatter_InterruptSignal( grunt )
+ table result = WaitSignal( grunt, "OnFoundEnemy", "OnSeeEnemy", "OnLostEnemy", "GruntChatter_Interrupt" )
+
+ string signal = expect string( result.signal )
+
+ switch( signal )
+ {
+ // Sees target for the first time, or switches back to a target
+ case "OnFoundEnemy":
+ entity enemy = expect entity( result.value )
+ GruntChatter_TryOnFoundEnemy( grunt, enemy )
+ break
+
+ // Sees active target ent again
+ case "OnSeeEnemy":
+ entity enemy = expect entity( result.activator )
+ GruntChatter_TryPlayerPilotSpotted( grunt, enemy, signal )
+ break
+
+ // can no longer see active target ent
+ case "OnLostEnemy":
+ entity lostEnemy = expect entity( result.activator )
+ GruntChatter_TryPilotLost( grunt, lostEnemy )
+
+ // Grunt will send OnLost and OnFound at the same time if switching targets
+ entity newEnemy = grunt.GetEnemy()
+ if ( IsAlive( newEnemy ) )
+ GruntChatter_TryOnFoundEnemy( grunt, newEnemy )
+ break
+
+ case "GruntChatter_Interrupt":
+ GruntChatter_CheckGruntForEvents( grunt )
+ break
+ }
+ }
+}
+
+void function GruntChatter_TryOnFoundEnemy( entity grunt, entity enemy )
+{
+ GruntChatter_TryPlayerPilotSpotted( grunt, enemy, "OnFoundEnemy" )
+ GruntChatter_TryEnemySpotted( grunt, enemy )
+}
+
+void function GruntChatter_InterruptSignal( entity grunt )
+{
+ grunt.EndSignal( "OnDeath" )
+ grunt.EndSignal( "OnDestroy" )
+
+ grunt.EndSignal( "OnFoundEnemy" )
+ grunt.EndSignal( "OnSeeEnemy" )
+ grunt.EndSignal( "OnLostEnemy" )
+
+ wait CHATTER_SIGNAL_INTERRUPT_WAIT
+ grunt.Signal( "GruntChatter_Interrupt" )
+}
+
+// tries to send all valid events, lets the priority system handle which one should play
+void function GruntChatter_CheckGruntForEvents( entity grunt )
+{
+ GruntChatter_TryFriendlyPassingNearby( grunt )
+
+ // everything below this cares about having a living target
+ entity target = grunt.GetEnemy()
+ if ( !IsAlive( target ) )
+ return
+
+ GruntChatter_HACK_TryPilotTargetOutOfSight( grunt, target )
+ GruntChatter_TrySuppressingPilotTarget( grunt, target )
+ GruntChatter_TryMissingFastTarget( grunt, target )
+ GruntChatter_TryPilotLowHealth( grunt, target )
+ GruntChatter_TryEngagingNonPilotTarget( grunt, target )
+}
+
+// HACK fakey pilot lost if player out of sight for a while
+void function GruntChatter_HACK_TryPilotTargetOutOfSight( entity grunt, entity target )
+{
+ entity gruntEnemy = grunt.GetEnemy()
+
+ if ( !IsAlive( gruntEnemy ) )
+ return
+
+ if ( !IsPilot( gruntEnemy ) )
+ return
+
+ if ( grunt.GetNPCState() != "combat" )
+ return
+
+ if ( grunt.GetEnemyLastTimeSeen() == 0 )
+ return
+
+ if ( Time() - grunt.GetEnemyLastTimeSeen() < CHATTER_GRUNT_ENEMY_OUT_OF_SIGHT_TIME )
+ return
+
+ //if ( file.debugLevel > 1 )
+ // printt( "FAKEY LOST TARGET" )
+
+ if ( !TimerCheck( "chatter_pilot_lost" ) )
+ return
+
+ GruntChatter_TryPilotLost( grunt, gruntEnemy )
+}
+
+void function GruntChatter_TryPlayerPilotCombatStateChange( entity player, int currState, int prevState )
+{
+ // these lines are mostly written as if the state changes are happening during combat vs a Pilot
+ if ( !IsPilot( player ) )
+ return
+
+ player.Signal( "GruntChatter_CombatStateChangeThread" )
+ player.EndSignal( "GruntChatter_CombatStateChangeThread" )
+ player.EndSignal( "OnDeath" )
+
+ wait CHATTER_PLAYER_COMBAT_STATE_CHANGE_DEBOUNCE
+
+ string alias = ""
+ switch ( currState )
+ {
+ case eGruntCombatState.ALERT:
+ alias = "gruntchatter_statechange_idle2alert"
+ if ( prevState == eGruntCombatState.COMBAT )
+ alias = "gruntchatter_statechange_combat2alert"
+ break
+
+ case eGruntCombatState.COMBAT:
+ alias = "gruntchatter_statechange_idle2combat"
+ if ( prevState == eGruntCombatState.ALERT )
+ alias = "gruntchatter_statechange_alert2combat"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias )
+}
+
+void function GruntChatter_TryPilotLost( entity grunt, entity enemy )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( enemy ) || !IsPilot( enemy ) )
+ return
+
+ if ( !TimerCheck( "chatter_pilot_lost" ) )
+ return
+
+ // if anyone near you can see the enemy, don't say we lost the target
+ if ( CanNearbyGruntTeammatesSeeEnemy( grunt, enemy, CHATTER_PILOT_LOST_NEARBY_TEAMMATE_DIST ) )
+ return
+
+ // if a nearby friendly grunt is close to the enemy don't chatter about losing sight of the enemy
+ if ( GruntChatter_IsFriendlyGruntCloseToLocation( grunt.GetTeam(), enemy.GetOrigin(), CHATTER_PLAYER_CLOSE_MIN_DIST ) )
+ return
+
+ string alias = "gruntchatter_pilot_lost"
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( grunt.GetOrigin(), grunt.GetTeam(), CHATTER_PILOT_LOST_NEARBY_TEAMMATE_DIST )
+ if ( nearbyGrunts.len() >= 2 && RandomInt( 100 ) < 40 )
+ alias = "gruntchatter_pilot_lost_neg"
+
+ GruntChatter_AddEvent( alias, grunt )
+}
+
+void function GruntChatter_TryPlayerPilotSpotted( entity grunt, entity player, string resultSignal )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( player ) || !player.IsPlayer() || !IsPilot( player ) )
+ return
+
+ if ( TimerCheck ( "chatter_pilot_spotted" ) )
+ {
+ string spottedAlias = "gruntchatter_pilot_spotted"
+
+ if ( resultSignal == "OnFoundEnemy" )
+ {
+ if ( svGlobalSP.gruntCombatState != eGruntCombatState.COMBAT )
+ {
+ spottedAlias = "gruntchatter_pilot_first_sighting"
+ }
+ }
+ else
+ {
+ float distToPilot = Distance( grunt.GetOrigin(), player.GetOrigin() )
+ bool canSeePilot = grunt.CanSee( player )
+ bool pilotIsMoving = Length( player.GetVelocity() ) >= CHATTER_PILOT_SPOTTED_MID_DIST_MOVING_MIN_SPEED
+
+ if ( canSeePilot )
+ {
+ if ( distToPilot <= CHATTER_PILOT_SPOTTED_CLOSE_DIST )
+ {
+ spottedAlias = "gruntchatter_pilot_spotted_close_range"
+ }
+ else if ( canSeePilot && distToPilot > CHATTER_PILOT_SPOTTED_CLOSE_DIST && distToPilot <= CHATTER_PILOT_SPOTTED_MID_DIST )
+ {
+ spottedAlias = "gruntchatter_pilot_spotted_mid_range"
+ if ( pilotIsMoving )
+ spottedAlias = "gruntchatter_pilot_spotted_mid_range_moving"
+ }
+
+ if ( TimerCheck( "chatter_pilot_spotted_specific_range" ) && RandomInt( 100 ) < 40 )
+ {
+ table<string, float> rangeDists
+ rangeDists["chatter_pilot_spotted_specific_range_20"] <- CHATTER_PILOT_SPOTTED_RANGE_DIST_20
+ rangeDists["chatter_pilot_spotted_specific_range_25"] <- CHATTER_PILOT_SPOTTED_RANGE_DIST_25
+ rangeDists["chatter_pilot_spotted_specific_range_30"] <- CHATTER_PILOT_SPOTTED_RANGE_DIST_30
+ rangeDists["chatter_pilot_spotted_specific_range_35"] <- CHATTER_PILOT_SPOTTED_RANGE_DIST_35
+
+ if ( distToPilot >= CHATTER_PILOT_SPOTTED_RANGE_DIST_MIN && distToPilot <= CHATTER_PILOT_SPOTTED_RANGE_DIST_MAX )
+ {
+ string closestAlias
+ float closestDist
+ foreach ( rangeAlias, rangeDist in rangeDists )
+ {
+ float thisDist = fabs( distToPilot - rangeDist )
+ if ( closestAlias == "" || thisDist < closestDist )
+ {
+ closestAlias = rangeAlias
+ closestDist = thisDist
+ }
+ }
+
+ spottedAlias = closestAlias
+ }
+ }
+ }
+ }
+
+ GruntChatter_AddEvent( spottedAlias, grunt )
+ }
+
+ if ( TimerCheck ( "chatter_pilot_spotted_abovebelow" ) )
+ {
+ bool isEnemyAbove = GruntChatter_IsEnemyAbove( grunt, player )
+ bool isEnemyBelow = GruntChatter_IsEnemyBelow( grunt, player )
+
+ if ( isEnemyAbove )
+ GruntChatter_AddEvent( "gruntchatter_pilot_spotted_above", grunt )
+ else if ( isEnemyBelow )
+ GruntChatter_AddEvent( "gruntchatter_pilot_spotted_below", grunt )
+ }
+}
+
+void function GruntChatter_TryEnemySpotted( entity grunt, entity spottedEnemy )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( spottedEnemy ) )
+ return
+
+ if ( spottedEnemy.GetTeam() == grunt.GetTeam() )
+ return
+
+ string alias = ""
+ float distToSpottedEnemy = Distance( grunt.GetOrigin(), spottedEnemy.GetOrigin() )
+
+ // TODO move to data files
+ if ( IsGrunt( spottedEnemy ) && TimerCheck( "chatter_enemy_grunt_spotted" ) && distToSpottedEnemy <= CHATTER_ENEMY_GRUNT_SPOTTED_DIST )
+ {
+ alias = "gruntchatter_enemy_grunt_spotted"
+ }
+ else if ( spottedEnemy.IsTitan() && TimerCheck( "chatter_enemy_titan_spotted" ) && distToSpottedEnemy <= CHATTER_ENEMY_TITAN_SPOTTED_DIST )
+ {
+ alias = "gruntchatter_enemy_titan_spotted"
+ if ( distToSpottedEnemy <= CHATTER_ENEMY_TITAN_SPOTTED_DIST_CLOSE )
+ alias = "gruntchatter_enemy_titan_spotted_close"
+ }
+ else if ( IsSpectre( spottedEnemy ) && TimerCheck( "chatter_enemy_spectre_spotted" ) && distToSpottedEnemy <= CHATTER_ENEMY_SPECTRE_SPOTTED_DIST )
+ {
+ alias = "gruntchatter_enemy_spectre_spotted"
+ if ( distToSpottedEnemy <= CHATTER_ENEMY_SPECTRE_SPOTTED_DIST_CLOSE )
+ alias = "gruntchatter_enemy_spectre_spotted_close"
+ }
+ else if ( IsTick( spottedEnemy ) && TimerCheck( "chatter_enemy_tick_spotted" ) && distToSpottedEnemy <= CHATTER_ENEMY_TICK_SPOTTED_DIST )
+ {
+ alias = "gruntchatter_enemy_tick_spotted"
+ }
+ else if ( IsPilotDecoy( spottedEnemy ) && TimerCheck( "chatter_enemy_pilot_decoy_spotted" ) && distToSpottedEnemy <= CHATTER_PILOT_DECOY_SPOTTED_DIST_MAX )
+ {
+ alias = "gruntchatter_enemy_pilot_decoy_spotted"
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias, grunt, spottedEnemy )
+}
+
+void function GruntChatter_TryEngagingNonPilotTarget( entity grunt, entity target )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ string alias = ""
+
+ if ( IsGrunt( target ) && TimerCheck( "chatter_engaging_grunt" ) )
+ {
+ alias = "gruntchatter_engaging_grunt"
+ }
+ else if ( IsSpectre( target ) && TimerCheck( "chatter_engaging_spectre" ) )
+ {
+ alias = "gruntchatter_engaging_spectre"
+ if ( IsValid( target.GetBossPlayer() ) )
+ alias = "gruntchatter_engaging_hacked_spectre"
+ }
+ else if ( IsProwler( target ) && TimerCheck( "chatter_engaging_prowler" ) )
+ {
+ alias = "gruntchatter_engaging_prowler"
+ }
+ else if ( IsStalker( target ) && TimerCheck( "chatter_engaging_stalker" ) )
+ {
+ alias = "gruntchatter_engaging_stalker"
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias, grunt, target )
+}
+
+void function GruntChatter_TryCloakedPilotSpotted( entity grunt, entity pilot )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( pilot ) )
+ return
+
+ if ( !IsCloaked( pilot ) )
+ return
+
+ // note: CanSee doesn't work when player is cloaked (as expected...)
+ if ( !GruntChatter_CanGruntTraceToLocation( grunt, pilot.EyePosition() ) )
+ return
+
+ if ( GruntChatter_IsTargetFacingAway( pilot, grunt, CHATTER_SEE_CLOAKED_PILOT_MIN_DOT_REAR ) )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_pilot_spotted_cloaked", grunt )
+}
+
+void function GruntChatter_TryPersonalShieldDamaged( entity shieldOwner )
+{
+ GruntChatter_AddEvent( "gruntchatter_personal_shield_damaged", shieldOwner )
+}
+
+void function GruntChatter_TryFriendlyEquipmentDeployed( entity deployer, string equipmentClassName )
+{
+ string alias = ""
+ string timerAlias = ""
+
+ // TODO move to data files
+ switch ( equipmentClassName )
+ {
+ case "npc_drone":
+ alias = "gruntchatter_friendly_drone_deployed"
+ timerAlias = "chatter_friendly_drone_deployed"
+ break
+
+ case "mp_weapon_frag_drone":
+ alias = "gruntchatter_friendly_tick_deployed"
+ timerAlias = "chatter_friendly_tick_deployed"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ if ( !TimerCheck( timerAlias ) )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( deployer.GetOrigin(), deployer.GetTeam(), CHATTER_FRIENDLY_EQUIPMENT_DEPLOYED_NEARBY_DIST )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( alias, closestGrunt )
+}
+
+void function GruntChatter_TryDisplacingFromDangerousArea( entity displacingGrunt )
+{
+ string dangerousAreaWeaponName = displacingGrunt.GetDangerousAreaWeapon()
+ GruntChatter_TryDangerousAreaWeapon( displacingGrunt, dangerousAreaWeaponName )
+}
+
+void function GruntChatter_TryDangerousAreaWeapon( entity grunt, string dangerousAreaWeaponName )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ string alias
+ string timerAlias
+
+ // TODO move to data files
+ switch ( dangerousAreaWeaponName )
+ {
+ case "mp_weapon_frag_grenade": //Since GruntChatter_TryDangerousAreaWeapon() is called from both CodeDialogue_DangerousAreaDisplace() and GruntChatter_OnGruntDamaged() this has bugs; a grunt who was not in the dangerous area created but took damage from the frag grenade will say VO like "Incoming Frag!! Take cover!". Not worth fixing this late in.
+ alias = "gruntchatter_dangerous_area_frag"
+ timerAlias = "chatter_dangerous_area_frag"
+ break
+
+ case "mp_weapon_grenade_emp": //This is triggered from GruntChatter_OnGruntDamaged(), since arc grenades don't create a dangerousarea
+ alias = "gruntchatter_dangerous_area_arc_grenade"
+ timerAlias = "chatter_dangerous_area_arc_grenade"
+ break
+
+ case "mp_weapon_thermite_grenade":
+ alias = "gruntchatter_dangerous_area_thermite"
+ timerAlias = "chatter_dangerous_area_thermite"
+ break
+
+ case "mp_weapon_grenade_gravity":
+ alias = "gruntchatter_dangerous_area_grav_grenade"
+ timerAlias = "chatter_dangerous_area_grav_grenade"
+ break
+
+ case "mp_weapon_grenade_electric_smoke":
+ alias = "gruntchatter_dangerous_area_esmoke"
+ timerAlias = "chatter_dangerous_area_esmoke"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ if ( !TimerCheck ( timerAlias ) )
+ return
+
+ // all grunts in the area will try to call it out, in case this guy dies
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( grunt.GetOrigin(), grunt.GetTeam(), CHATTER_DANGEROUS_AREA_NEARBY_RANGE )
+ foreach ( nearbyGrunt in nearbyGrunts )
+ GruntChatter_AddEvent( alias, nearbyGrunt )
+}
+
+void function GruntChatter_TryEnemyTimeShifted( entity timeShiftedEnemy )
+{
+ if ( !IsAlive( timeShiftedEnemy ) )
+ return
+
+ if ( !TimerCheck( "chatter_enemy_time_shifted" ) )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestEnemyHumanGrunt_LOS( timeShiftedEnemy.GetOrigin(), timeShiftedEnemy.GetTeam(), CHATTER_ENEMY_TIME_SHIFT_NEARBY_DIST )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_enemy_time_shifted", closestGrunt )
+}
+
+void function GruntChatter_OnGruntDamaged( entity grunt, var damageInfo )
+{
+ if ( !IsValid( grunt ) )
+ return
+
+ string damageWeaponName
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ table dmgSources = expect table( getconsttable().eDamageSourceId )
+ foreach ( name, id in dmgSources )
+ {
+ if ( id == damageSourceID )
+ {
+ damageWeaponName = expect string( name )
+ break
+ }
+ }
+
+ if ( damageWeaponName != "" )
+ GruntChatter_TryDangerousAreaWeapon( grunt, damageWeaponName )
+}
+
+void function GruntChatter_OnPlayerOrNPCKilled( entity deadGuy, entity attacker, var damageInfo )
+{
+ if ( !IsValid( deadGuy ) )
+ return
+
+ if ( deadGuy.GetTeam() == TEAM_IMC )
+ {
+ GruntChatter_TryEnemyPlayerPilot_Multikill( deadGuy, damageInfo )
+ GruntChatter_TryEnemyPlayerPilot_MobilityKill( deadGuy, damageInfo )
+ GruntChatter_TryFriendlyDown( deadGuy )
+ GruntChatter_TrySquadDepleted( deadGuy )
+ }
+ else
+ {
+ GruntChatter_TryEnemyDown( deadGuy )
+ }
+}
+
+void function GruntChatter_OnPilotDecoyKilled( entity decoy, var damageInfo )
+{
+ GruntChatter_TryEnemyDown( decoy )
+}
+
+void function GruntChatter_TryEnemyPlayerPilot_Multikill( entity deadGuy, var damageInfo )
+{
+ if ( !TimerCheck( "chatter_enemy_pilot_multikill" ) )
+ return
+
+ // don't worry about larger targets
+ if ( !IsHumanSized( deadGuy ) )
+ return
+
+ int customDamageType = DamageInfo_GetCustomDamageType( damageInfo )
+
+ // explosive kills don't count for pilot multikills
+ if ( customDamageType & DF_EXPLOSION )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsPilot( attacker ) )
+ return
+
+ // -- multikills --
+ if ( !GruntChatter_IsKillChainStillActive( attacker ) )
+ GruntChatter_ResetPilotKillChain( attacker )
+
+ GruntChatter_UpdatePilotKillChain( attacker )
+
+ if ( GruntChatter_GetPilotKillChain( attacker ) < CHATTER_PILOT_MULTIKILL_MIN_KILLS )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( deadGuy.GetOrigin(), deadGuy.GetTeam(), CHATTER_FRIENDLY_GRUNT_DOWN_DIST_MAX )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_enemy_pilot_multikill", closestGrunt, attacker )
+}
+
+void function GruntChatter_TryEnemyPlayerPilot_MobilityKill( entity deadGuy, var damageInfo )
+{
+ if ( !TimerCheck( "chatter_enemy_pilot_mobility_kill" ) )
+ return
+
+ // don't worry about larger targets
+ if ( !IsHumanSized( deadGuy ) )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsPilot( attacker ) )
+ return
+
+ if ( attacker.IsOnGround() )
+ return
+
+ float targetSpeed = Length( attacker.GetVelocity() )
+ if ( !attacker.IsWallRunning() && targetSpeed < CHATTER_MISS_FAST_TARGET_MIN_SPEED )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( deadGuy.GetOrigin(), deadGuy.GetTeam(), CHATTER_FRIENDLY_GRUNT_DOWN_DIST_MAX )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_enemy_pilot_mobility_kill", closestGrunt, attacker )
+}
+
+void function GruntChatter_TryFriendlyDown( entity deadGuy )
+{
+ string alias = ""
+ float searchRange = -1.0
+
+ if ( IsGrunt( deadGuy ) && TimerCheck( "chatter_friendly_grunt_down" ) )
+ {
+ alias = "gruntchatter_friendly_grunt_down"
+ if ( svGlobalSP.gruntCombatState == eGruntCombatState.IDLE )
+ alias = "gruntchatter_friendly_grunt_down_notarget"
+
+ searchRange = CHATTER_FRIENDLY_GRUNT_DOWN_DIST_MAX
+ }
+ else if ( deadGuy.IsTitan() && TimerCheck( "chatter_friendly_titan_down" ) )
+ {
+ alias = "gruntchatter_friendly_titan_down"
+ searchRange = CHATTER_FRIENDLY_TITAN_DOWN_DIST_MAX
+ }
+
+ if ( alias == "" )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( deadGuy.GetOrigin(), deadGuy.GetTeam(), searchRange )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( alias, closestGrunt )
+}
+
+void function GruntChatter_TrySquadDepleted( entity deadGuy )
+{
+ if ( !TimerCheck( "chatter_squad_depleted" ) )
+ return
+
+ if ( !IsGrunt( deadGuy ) )
+ return
+
+ string deadGuySquadName = GetSquadName( deadGuy )
+ if ( deadGuySquadName == "" )
+ return
+
+ array<entity> squad = GetNPCArrayBySquad( deadGuySquadName )
+ entity lastSquadMember
+ if ( squad.len() == 1 )
+ lastSquadMember = squad[0]
+
+ if ( !GruntChatter_CanGruntChatterNow( lastSquadMember ) )
+ return
+
+ // if state is idle, don't freak out about being alone
+ if ( lastSquadMember.GetNPCState() == "idle" )
+ return
+
+ // if another grunt from another squad is nearby, don't chatter about being alone
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( lastSquadMember.GetOrigin(), lastSquadMember.GetTeam(), CHATTER_SQUAD_DEPLETED_FRIENDLY_NEARBY_DIST )
+ nearbyGrunts.fastremovebyvalue( lastSquadMember )
+ if ( nearbyGrunts.len() )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_squad_depleted", lastSquadMember )
+}
+
+void function GruntChatter_TryEnemyDown( entity deadGuy )
+{
+ string alias = ""
+ float searchRange = -1.0
+
+ if ( IsPilot( deadGuy ) && TimerCheck( "chatter_enemy_pilot_down" ) )
+ {
+ alias = "gruntchatter_enemy_pilot_down"
+ searchRange = CHATTER_ENEMY_PILOT_DOWN_DIST_MAX
+ }
+ else if ( IsGrunt( deadGuy ) && TimerCheck( "chatter_enemy_grunt_down" ) )
+ {
+ alias = "gruntchatter_enemy_grunt_down"
+ searchRange = CHATTER_ENEMY_GRUNT_DOWN_DIST_MAX
+ }
+ else if ( deadGuy.IsTitan() && TimerCheck( "chatter_enemy_titan_down" ) )
+ {
+ alias = "gruntchatter_enemy_titan_down"
+ searchRange = CHATTER_ENEMY_TITAN_DOWN_DIST_MAX
+ }
+ else if ( IsSpectre( deadGuy ) && TimerCheck( "chatter_enemy_spectre_down" ) )
+ {
+ alias = "gruntchatter_enemy_spectre_down"
+ searchRange = CHATTER_ENEMY_SPECTRE_DOWN_DIST_MAX
+ }
+ else if ( IsPilotDecoy( deadGuy ) && TimerCheck( "chatter_enemy_pilot_decoy_revealed" ) )
+ {
+ alias = "gruntchatter_enemy_pilot_decoy_revealed"
+ searchRange = CHATTER_PILOT_DECOY_SPOTTED_DIST_MAX
+ }
+
+ if ( alias == "" )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestEnemyHumanGrunt_LOS( deadGuy.GetOrigin(), deadGuy.GetTeam(), searchRange )
+ if ( !closestGrunt )
+ return
+
+ // HACK- squad conversations don't play to dead players
+ if ( alias == "gruntchatter_enemy_pilot_down" )
+ {
+ HACK_GruntChatter_TryEnemyPilotDown( deadGuy, closestGrunt )
+ return
+ }
+
+ GruntChatter_AddEvent( alias, closestGrunt )
+}
+
+void function HACK_GruntChatter_TryEnemyPilotDown( entity deadGuy, entity closestGrunt )
+{
+ if ( !deadGuy.IsPlayer() )
+ return
+
+ if ( deadGuy.GetForcedDialogueOnly() )
+ return
+
+ TimerReset( "chatter_enemy_pilot_down" )
+
+ string rawAlias = "diag_imc_grunt1_bc_killenemypilot_01"
+ if ( CoinFlip() )
+ rawAlias = "diag_imc_grunt1_bc_killenemypilot_02"
+
+ EmitSoundOnEntity( closestGrunt, rawAlias )
+}
+
+void function GruntChatter_TryThrowingGrenade( entity grunt )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ entity enemy = grunt.GetEnemy()
+ if ( !IsAlive( enemy ) )
+ return
+
+ if ( !TimerCheck( "chatter_throwing_grenade" ) )
+ return
+
+ string alias = ""
+ // TODO move to data files
+ switch ( grunt.kv.grenadeWeaponName )
+ {
+ case "mp_weapon_frag_grenade":
+ alias = "gruntchatter_throwing_grenade_frag"
+ break
+
+ case "mp_weapon_grenade_electric_smoke":
+ alias = "gruntchatter_throwing_grenade_electric_smoke"
+ break
+
+ case "mp_weapon_thermite_grenade":
+ alias = "gruntchatter_throwing_grenade_thermite"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias, grunt )
+}
+
+// TODO move to data files
+void function GruntChatter_TryFriendlyPassingNearby( entity grunt )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ // these lines are written as if the grunts are in combat
+ if ( grunt.GetNPCState() != "combat" )
+ return
+
+ if ( TimerCheck( "chatter_nearby_friendly_titan" ) )
+ {
+ array<entity> nearbyTitans = GetNPCArrayEx( "npc_titan", grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), CHATTER_NEARBY_TITAN_DIST )
+ entity nearbyTitan
+ foreach ( titan in nearbyTitans )
+ {
+ if ( !IsAlive( titan ) )
+ continue
+
+ if ( GetDoomedState( titan ) )
+ continue
+
+ if ( GruntChatter_EventTargetAlreadyUsed( titan ) )
+ continue
+
+ nearbyTitan = titan
+ break
+ }
+
+ if ( nearbyTitan )
+ GruntChatter_AddEvent( "gruntchatter_nearby_friendly_titan", grunt, nearbyTitan )
+ }
+
+ if ( TimerCheck( "chatter_nearby_friendly_reaper" ) )
+ {
+ array<entity> nearbyReapers = GetNPCArrayEx( "npc_super_spectre", grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), CHATTER_NEARBY_REAPER_DIST )
+ foreach ( reaper in nearbyReapers )
+ {
+ if ( !IsAlive( reaper ) )
+ continue
+
+ if ( GetDoomedState( reaper ) )
+ continue
+
+ if ( GruntChatter_EventTargetAlreadyUsed( reaper ) )
+ continue
+
+ GruntChatter_AddEvent( "gruntchatter_nearby_friendly_reaper", grunt, reaper )
+ break
+ }
+ }
+
+ if ( TimerCheck( "chatter_nearby_friendly_spectre" ) )
+ {
+ array<entity> nearbySpectres = GetNPCArrayEx( "npc_spectre", grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), CHATTER_NEARBY_SPECTRE_DIST )
+ if ( nearbySpectres.len() )
+ {
+ entity closestSpectre = GetClosest( nearbySpectres, grunt.GetOrigin() )
+ GruntChatter_AddEvent( "gruntchatter_nearby_friendly_spectre", grunt, closestSpectre )
+ }
+ }
+}
+
+void function GruntChatter_TryIncomingSpawn( entity inboundEnt, vector arrivalLocation )
+{
+ if ( !IsValid( inboundEnt ) )
+ return
+
+ string alias
+ string timer
+ float nearbyRange
+ entity closestGrunt
+
+ // TODO move to data files
+ if ( inboundEnt.GetTeam() == TEAM_IMC )
+ {
+ switch ( inboundEnt.GetClassName() )
+ {
+ case "npc_titan":
+ alias = "gruntchatter_incoming_friendly_titanfall"
+ timer = "chatter_incoming_friendly_titanfall"
+ nearbyRange = CHATTER_NEARBY_TITAN_DIST
+ break
+
+ case "npc_super_spectre":
+ alias = "gruntchatter_incoming_friendly_reaperfall"
+ timer = "chatter_incoming_friendly_reaperfall"
+ nearbyRange = CHATTER_NEARBY_REAPER_DIST
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( arrivalLocation, inboundEnt.GetTeam(), nearbyRange )
+ if ( !closestGrunt )
+ return
+ }
+ else if ( inboundEnt.GetTeam() == TEAM_MILITIA )
+ {
+ switch ( inboundEnt.GetClassName() )
+ {
+ case "npc_titan":
+ alias = "gruntchatter_incoming_enemy_titanfall"
+ timer = "chatter_incoming_enemy_titanfall"
+ nearbyRange = CHATTER_NEARBY_TITAN_DIST
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ closestGrunt = GruntChatter_FindClosestEnemyHumanGrunt_LOS( arrivalLocation, inboundEnt.GetTeam(), nearbyRange )
+ if ( !closestGrunt )
+ return
+ }
+
+ // NOTE- can't send the target for these events because the distance check to where the titanfall starts will fail
+ GruntChatter_AddEvent( alias, closestGrunt )
+}
+
+void function GruntChatter_TrySuppressingPilotTarget( entity grunt, entity target )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ // this is mostly useful for players
+ if ( !target.IsPlayer() )
+ return
+
+ if ( !IsPilot( target ) )
+ return
+
+ if ( !TimerCheck( "chatter_suppressingLKP_start" ) )
+ return
+
+ string STR_lastSuppressionTime = expect string( grunt.kv.lastSuppressionTime ) // hacky
+ float lastSuppressionTime = STR_lastSuppressionTime.tofloat()
+ float validRecentWindow_suppression = Time() - CHATTER_SUPPRESSION_EXPIRE_TIME
+
+ if ( lastSuppressionTime < validRecentWindow_suppression )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_suppressingLKP_start", grunt )
+}
+
+void function GruntChatter_TryMissingFastTarget( entity grunt, entity target )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ if ( !IsPilot( target ) )
+ return
+
+ if ( !TimerCheck( "chatter_missing_fast_target" ) )
+ return
+
+ float targetSpeed = Length( target.GetVelocity() )
+ if ( targetSpeed < CHATTER_MISS_FAST_TARGET_MIN_SPEED )
+ return
+
+ string STR_lastMissFastPlayerTime = expect string( grunt.kv.lastMissFastPlayerTime ) // hacky
+ float lastMissFastPlayerTime = STR_lastMissFastPlayerTime.tofloat()
+ float validRecentWindow_missFastTarget = Time() - CHATTER_MISS_FAST_TARGET_EXPIRE_TIME
+
+ if ( lastMissFastPlayerTime < validRecentWindow_missFastTarget )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_missing_fast_target", grunt )
+}
+
+void function GruntChatter_TryPilotLowHealth( entity grunt, entity target )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ if ( !IsPilot( target ) )
+ return
+
+ if ( !TimerCheck( "chatter_pilot_low_health" ) )
+ return
+
+ if ( target.GetHealth().tofloat() / target.GetMaxHealth().tofloat() > CHATTER_PILOT_LOW_HEALTH_FRAC )
+ return
+
+ if ( Distance( grunt.GetOrigin(), target.GetOrigin() ) > CHATTER_PILOT_LOW_HEALTH_RANGE )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_pilot_low_health", grunt )
+}
+
+void function GruntChatter_TryPlayerPilotReloading( entity player )
+{
+ if ( !IsAlive( player ) || !IsPilot( player ) )
+ return
+
+ if ( !TimerCheck( "chatter_target_reloading" ) )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestEnemyHumanGrunt_LOS( player.GetOrigin(), player.GetTeam(), CHATTER_PLAYER_RELOADING_RANGE )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_target_reloading", closestGrunt, player )
+}
+
+
+// ==== turret event handling ====
+void function GruntChatter_TurretSignalWait( entity turret )
+{
+ turret.EndSignal( "OnDeath" )
+ turret.EndSignal( "OnDestroy" )
+
+ while ( 1 )
+ {
+ table result = WaitSignal( turret, "OnFoundEnemy", "OnSeeEnemy" )
+
+ string signal = expect string( result.signal )
+
+ switch( signal )
+ {
+ case "OnFoundEnemy":
+ entity enemy = expect entity( result.value )
+ GruntChatter_TryFriendlyTurretFoundTarget( turret, enemy )
+ break
+
+ case "OnSeeEnemy":
+ entity enemy = expect entity( result.activator )
+ GruntChatter_TryFriendlyTurretFoundTarget( turret, enemy )
+ break
+
+ }
+ }
+}
+
+void function GruntChatter_TryFriendlyTurretFoundTarget( entity turret, entity enemy )
+{
+ if ( !IsAlive( turret ) || !IsAlive( enemy ) )
+ return
+
+ if ( !TimerCheck( "chatter_friendly_turret_found_target") )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( turret.GetOrigin(), turret.GetTeam(), CHATTER_FRIENDLY_EQUIPMENT_DEPLOYED_NEARBY_DIST )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_friendly_turret_found_target", closestGrunt, enemy )
+}
+
+
+// ==== pilot kill chains ====
+// NOTE: don't technically require a pilot, but makes it easier to port to an MP environment
+void function GruntChatter_UpdatePilotKillChain( entity pilot )
+{
+ file.pilotKillChainCounter++
+ file.lastPilotKillTime = Time()
+}
+
+int function GruntChatter_GetPilotKillChain( entity pilot )
+{
+ return file.pilotKillChainCounter
+}
+
+bool function GruntChatter_IsKillChainStillActive( entity pilot )
+{
+ if ( file.lastPilotKillTime == -1 )
+ return true
+
+ return (Time() - file.lastPilotKillTime) < CHATTER_ENEMY_PILOT_MULTIKILL_EXPIRETIME
+}
+
+void function GruntChatter_ResetPilotKillChain( entity pilot )
+{
+ file.pilotKillChainCounter = 0
+}
+
+
+// ==== chatter util ====
+// won't return mechanicals like Specialists
+array<entity> function GetNearbyFriendlyHumanGrunts( vector searchOrigin, int friendlyTeam, float ornull searchRange = null )
+{
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( searchOrigin, friendlyTeam, searchRange )
+ array<entity> humanGrunts = []
+ foreach ( grunt in nearbyGrunts )
+ {
+ if ( grunt.IsMechanical() )
+ continue
+
+ humanGrunts.append( grunt )
+ }
+
+ return humanGrunts
+}
+
+// won't return mechanicals like Specialists
+array<entity> function GetNearbyEnemyHumanGrunts( vector searchOrigin, int enemyTeam, float ornull searchRange = null )
+{
+ array<entity> nearbyGrunts = GetNearbyEnemyGrunts( searchOrigin, enemyTeam, searchRange )
+ array<entity> humanGrunts = []
+ foreach ( grunt in nearbyGrunts )
+ {
+ if ( grunt.IsMechanical() )
+ continue
+
+ humanGrunts.append( grunt )
+ }
+
+ return humanGrunts
+}
+
+bool function GruntChatter_CanGruntChatterNow( entity grunt )
+{
+ if ( !IsAlive( grunt ) )
+ return false
+
+ if ( !GruntChatter_IsGruntTypeEligibleForChatter( grunt ) )
+ return false
+
+ if ( grunt.ContextAction_IsMeleeExecution() )
+ return false
+
+ // we only care about this because the grunt conversation system wants it
+ if ( GetSquadName( grunt ) == "" )
+ return false
+
+ return true
+}
+
+bool function GruntChatter_IsGruntTypeEligibleForChatter( entity grunt )
+{
+ if ( !IsGrunt( grunt ) )
+ return false
+
+ // mechanical grunts don't chatter
+ if ( grunt.IsMechanical() )
+ return false
+
+ if ( grunt.GetTeam() != TEAM_IMC )
+ return false
+
+ return true
+}
+
+bool function GruntChatter_CanGruntChatterToPlayer( entity grunt, entity player )
+{
+ if ( DistanceSqr( grunt.GetOrigin(), player.GetOrigin() ) > MAX_VOICE_DIST_SQRD )
+ return false
+
+ return true
+}
+
+bool function GruntChatter_CanChatterEventUseEnemyTarget( ChatterEvent chatterEvent )
+{
+ entity grunt = chatterEvent.npc
+ entity target = chatterEvent.target
+ bool trackEventTarget = chatterEvent.category.trackEventTarget
+
+ if ( !chatterEvent.hasTarget )
+ return false
+
+ if ( !IsAlive( target ) )
+ return false
+
+ if ( trackEventTarget && GruntChatter_EventTargetAlreadyUsed( target ) )
+ return false
+
+ float distToEnemySqr = DistanceSqr( grunt.GetOrigin(), target.GetOrigin() )
+ if ( distToEnemySqr > MAX_VOICE_DIST_SQRD )
+ return false
+
+ return true
+}
+
+bool function CanNearbyGruntTeammatesSeeEnemy( entity grunt, entity enemy, float nearbyRange )
+{
+ if ( !IsAlive( enemy ) )
+ return false
+
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( enemy.GetOrigin(), grunt.GetTeam(), nearbyRange )
+
+ foreach ( grunt in nearbyGrunts )
+ {
+ if ( grunt.CanSee( enemy ) )
+ return true
+ }
+
+ return false
+}
+
+bool function GruntChatter_IsFriendlyGruntCloseToLocation( int team, vector location, float nearbyRange )
+{
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( location, team, nearbyRange )
+
+ if ( nearbyGrunts.len() )
+ return true
+
+ return false
+}
+
+bool function GruntChatter_IsTargetFacingAway( entity grunt, entity target, float minDotRear )
+{
+ if ( !IsAlive( grunt ) || !IsAlive( target ) )
+ return false
+
+ vector viewAng = target.GetAngles() // overall body angles better for this than viewvec
+ vector viewVec = AnglesToForward( viewAng )
+ vector vecRear = viewVec * -1
+ vector angRear = VectorToAngles( vecRear )
+
+ vector vecToTarget = Normalize( grunt.EyePosition() - target.EyePosition() )
+ float dot2Grunt_rear = DotProduct( vecToTarget, vecRear )
+
+ //printt( "REAR dot to enemy:", dot2Grunt_rear )
+
+ return dot2Grunt_rear >= minDotRear
+}
+
+bool function GruntChatter_IsEnemyAbove( entity grunt, entity enemy )
+{
+ // Pilots jumping over guys gives false positives
+ if ( IsPilot( enemy ) && !enemy.IsOnGround() )
+ return false
+
+ vector gOrg = grunt.GetOrigin()
+ vector eOrg = enemy.GetOrigin()
+
+ vector cylinderBottom = gOrg + < 0, 0, CHATTER_PILOT_SPOTTED_ABOVE_DIST_MIN >
+ vector cylinderTop = gOrg + < 0, 0, CHATTER_PILOT_SPOTTED_ABOVE_DIST_MAX >
+
+ bool isAbove = PointInCylinder( cylinderBottom, cylinderTop, CHATTER_PILOT_SPOTTED_ABOVE_RADIUS, eOrg )
+ return isAbove
+}
+
+bool function GruntChatter_IsEnemyBelow( entity grunt, entity enemy )
+{
+ vector gOrg = grunt.GetOrigin()
+ vector eOrg = enemy.GetOrigin()
+
+ vector cylinderBottom = gOrg - < 0, 0, CHATTER_PILOT_SPOTTED_BELOW_DIST_MAX >
+ vector cylinderTop = gOrg - < 0, 0, CHATTER_PILOT_SPOTTED_BELOW_DIST_MIN >
+
+ bool isBelow = PointInCylinder( cylinderBottom, cylinderTop, CHATTER_PILOT_SPOTTED_BELOW_RADIUS, eOrg )
+ return isBelow
+}
+
+void function GruntChatter_TryGruntFlankedByPlayer( entity grunt, int aiSurprisedReactionType )
+{
+ if ( !GruntChatter_CanGruntDoFlankingCallout( grunt ) )
+ return
+
+ entity surprisingEnemy = grunt.GetEnemy()
+ if ( !IsPilot( surprisingEnemy ) || !surprisingEnemy.IsPlayer() )
+ return
+
+ string alias
+ switch ( aiSurprisedReactionType )
+ {
+ case RSR_REAR_FLANK:
+ //printt( "REAR FLANK!")
+ alias = "gruntchatter_pilot_spotted_flank_rear"
+ break
+
+ case RSR_SIDE_FLANK:
+ //printt( " SIDE FLANK!" )
+ alias = "gruntchatter_pilot_spotted_flank_side"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias, grunt, surprisingEnemy )
+}
+
+bool function GruntChatter_CanGruntDoFlankingCallout( entity grunt )
+{
+ if ( !TimerCheck( "chatter_pilot_flanking" ) )
+ return false
+
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return false
+
+ return true
+}
+
+entity function GruntChatter_FindClosestEnemyHumanGrunt_LOS( vector searchOrigin, int enemyTeam, float searchDist )
+{
+ array<entity> humanGrunts = GetNearbyEnemyHumanGrunts( searchOrigin, enemyTeam, searchDist )
+ return GruntChatter_GetClosestGrunt_LOS( humanGrunts, searchOrigin )
+}
+
+entity function GruntChatter_FindClosestFriendlyHumanGrunt_LOS( vector searchOrigin, int friendlyTeam, float searchDist )
+{
+ array<entity> humanGrunts = GetNearbyFriendlyHumanGrunts( searchOrigin, friendlyTeam, searchDist )
+ return GruntChatter_GetClosestGrunt_LOS( humanGrunts, searchOrigin )
+}
+
+entity function GruntChatter_GetClosestGrunt_LOS( array<entity> nearbyGrunts, vector searchOrigin )
+{
+ entity closestGrunt = null
+ float closestDist = 10000
+
+ foreach ( grunt in nearbyGrunts )
+ {
+ vector gruntOrigin = grunt.GetOrigin()
+
+ // CanSee doesn't return true if the target is dead
+ if ( !GruntChatter_CanGruntTraceToLocation( grunt, searchOrigin ) )
+ continue
+
+ if ( !closestGrunt )
+ {
+ closestGrunt = grunt
+ continue
+ }
+
+ float distFromSearchOrigin = Distance( grunt.GetOrigin(), searchOrigin )
+
+ if ( closestDist > distFromSearchOrigin )
+ continue
+
+ closestGrunt = grunt
+ closestDist = distFromSearchOrigin
+ }
+
+ return closestGrunt
+}
+
+bool function GruntChatter_CanGruntTraceToLocation( entity grunt, vector traceEnd )
+{
+ float traceFrac = TraceLineSimple( grunt.GetOrigin(), traceEnd, grunt )
+ return traceFrac > CHATTER_NEARBY_GRUNT_TRACEFRAC_MIN
+}
+
+string function GetSquadName( entity grunt )
+{
+ string squadName = expect string( grunt.kv.squadname )
+ return squadName
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_squad_spawn.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_squad_spawn.gnut
new file mode 100644
index 00000000..9dbdd699
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_squad_spawn.gnut
@@ -0,0 +1,167 @@
+
+
+
+global function GetNPCBaseClassFromSpawnFunc
+
+global function CreateZipLineSquadDropTable
+
+string function GetNPCBaseClassFromSpawnFunc( entity functionref( int, vector, vector ) spawnFunc )
+{
+ // temp spawn a guy to get his baseclass.
+ entity npc = spawnFunc( TEAM_IMC, Vector(0,0,0), Vector(0,0,0) )
+ string baseClass = npc.GetClassName()
+ npc.Destroy()
+ return baseClass
+}
+
+
+
+void function DropOffAISide_NPCThink( entity npc, int index, entity dropship, string attach )
+{
+ npc.EndSignal( "OnDeath" )
+
+ //init
+ npc.SetParent( dropship, attach )
+ npc.SetEfficientMode( true )
+
+ //deploy
+ array<string> deployAnims = DropOffAISide_GetDeployAnims()
+ array<float> seekTimes = DropOffAISide_GetSeekTimes()
+
+ thread PlayAnimTeleport( npc, deployAnims[ index ], dropship, attach )
+ npc.Anim_SetInitialTime( seekTimes[ index ] )
+ WaittillAnimDone( npc )
+
+ npc.SetEfficientMode( false )
+
+ //disperse
+ array<string> disperseAnims = DropOffAISide_GetDisperseAnims()
+ vector origin = HackGetDeltaToRef( npc.GetOrigin(), npc.GetAngles(), npc, disperseAnims[ index ] ) + Vector( 0,0,2 )
+ waitthread PlayAnimGravity( npc, disperseAnims[ index ], origin, npc.GetAngles() )
+}
+
+void function DropOffAISide_WarpOutShip( entity dropship, vector origin, vector angles )
+{
+ wait 1.5
+ dropship.EndSignal( "OnDeath" )
+
+ string anim = "cd_dropship_rescue_side_end"
+ thread PlayAnim( dropship, anim, origin, angles )
+
+ //blend
+ wait dropship.GetSequenceDuration( anim ) - 0.2
+
+ dropship.Hide()
+ thread WarpoutEffect( dropship )
+}
+
+float function GetInstantSpawnRadius( entity npc )
+{
+ float radius = 64
+
+ if ( npc )
+ {
+ switch ( npc.GetClassName() )
+ {
+ case "npc_gunship":
+ case "npc_dropship":
+ radius = 512
+ break
+
+ case "npc_titan":
+ radius = 256
+ break
+
+ case "npc_super_spectre":
+ case "npc_prowler":
+ radius = 128
+ break
+
+ default:
+ radius = 64
+ break
+ }
+ }
+
+ return radius
+}
+
+
+
+
+/************************************************************************************************\
+
+## ## #### ###### ###### ######## ####### ####### ## ######
+### ### ## ## ## ## ## ## ## ## ## ## ## ## ##
+#### #### ## ## ## ## ## ## ## ## ## ##
+## ### ## ## ###### ## ## ## ## ## ## ## ######
+## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
+## ## #### ###### ###### ## ####### ####### ######## ######
+
+\************************************************************************************************/
+
+
+
+
+array<string> function DropOffAISide_GetIdleAnims()
+{
+ array<string> anims = [
+ "pt_ds_side_intro_gen_idle_A", //standing right
+ "pt_ds_side_intro_gen_idle_B", //standing left
+ "pt_ds_side_intro_gen_idle_C", //sitting right
+ "pt_ds_side_intro_gen_idle_D" ] //sitting left
+
+ return anims
+}
+
+array<string> function DropOffAISide_GetDeployAnims()
+{
+ array<string> anims = [
+ "pt_generic_side_jumpLand_A", //standing right
+ "pt_generic_side_jumpLand_B", //standing left
+ "pt_generic_side_jumpLand_C", //sitting right
+ "pt_generic_side_jumpLand_D" ] //sitting left
+
+ return anims
+}
+
+array<string> function DropOffAISide_GetDisperseAnims()
+{
+ array<string> anims = [
+ "React_signal_thatway", //standing right
+ "React_spot_radio2", //standing left
+ "stand_2_run_45R", //sitting right
+ "stand_2_run_45L" ] //sitting left
+
+ return anims
+}
+
+array<float> function DropOffAISide_GetSeekTimes()
+{
+ array<float> anims = [
+ 9.75, //standing right
+ 10.0, //standing left
+ 10.5, //sitting right
+ 11.25 ] //sitting left
+
+ return anims
+}
+
+
+CallinData function CreateZipLineSquadDropTable( int team, int count, vector origin, vector angles, string squadName = "" )
+{
+ if ( squadName == "" )
+ squadName = MakeSquadName( team, UniqueString( "ZiplineTable" ) )
+
+ CallinData drop
+ drop.origin = origin
+ drop.yaw = angles.y
+ drop.dist = 768
+ drop.team = team
+ drop.squadname = squadName
+ SetDropTableSpawnFuncs( drop, CreateSoldier, count )
+ SetCallinStyle( drop, eDropStyle.ZIPLINE_NPC )
+
+ return drop
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_titan_npc_behavior.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_titan_npc_behavior.gnut
new file mode 100644
index 00000000..347cb644
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_titan_npc_behavior.gnut
@@ -0,0 +1,404 @@
+untyped
+
+global function TitanNpcBehavior_Init
+
+global function TitanNPC_Think
+global function TitanNPC_WaitForBubbleShield_StartAutoTitanBehavior
+global function TitanStandUp
+global function TitanKneel
+global function GetBubbleShieldDuration
+global function ShowMainTitanWeapons
+
+global function ChangedStance
+
+global function TitanWaitsToChangeStance
+global function ShouldBecomeAutoTitan
+
+function TitanNpcBehavior_Init()
+{
+ FlagInit( "DisableTitanKneelingEmbark" )
+
+ RegisterSignal( "TitanStopsThinking" )
+ RegisterSignal( "RodeoRiderChanged" )
+
+ if ( IsMultiplayer() )
+ {
+ AddCallback_OnTitanBecomesPilot( OnClassChangeBecomePilot )
+ AddCallback_OnPilotBecomesTitan( OnClassChangeBecomeTitan )
+ }
+}
+
+void function OnClassChangeBecomePilot( entity player, entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ {
+ entity ordnanceWeapon = titan.GetOffhandWeapon( OFFHAND_ORDNANCE )
+ if ( IsValid( ordnanceWeapon ) )
+ ordnanceWeapon.AllowUse( false )
+
+ entity centerWeapon = titan.GetOffhandWeapon( OFFHAND_TITAN_CENTER )
+ if ( IsValid( centerWeapon ) )
+ centerWeapon.AllowUse( false )
+ }
+}
+
+void function OnClassChangeBecomeTitan( entity player, entity titan )
+{
+ entity soul = player.GetTitanSoul()
+
+ entity ordnanceWeapon = player.GetOffhandWeapon( OFFHAND_ORDNANCE )
+ if ( IsValid( ordnanceWeapon ) )
+ ordnanceWeapon.AllowUse( true )
+
+ entity centerWeapon = player.GetOffhandWeapon( OFFHAND_TITAN_CENTER )
+ if ( IsValid( centerWeapon ) )
+ centerWeapon.AllowUse( true )
+}
+
+float function GetBubbleShieldDuration( entity player )
+{
+ if ( PlayerHasPassive( player, ePassives.PAS_LONGER_BUBBLE ) )
+ return EMBARK_TIMEOUT + 10.0
+ else
+ return EMBARK_TIMEOUT
+
+ unreachable
+}
+
+void function TitanNPC_WaitForBubbleShield_StartAutoTitanBehavior( entity titan )
+{
+ Assert( IsAlive( titan ) )
+
+ titan.Signal( "TitanStopsThinking" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "TitanStopsThinking" )
+ titan.EndSignal( "ContextAction_SetBusy" )
+
+ entity bossPlayer = titan.GetBossPlayer()
+ if ( !bossPlayer )
+ return
+
+ OnThreadEnd(
+ function () : ( titan )
+ {
+ if ( IsAlive( titan ) )
+ {
+ titan.SetNoTarget( false )
+ thread TitanNPC_Think( titan )
+ }
+ }
+ )
+
+ titan.EndSignal( "ChangedTitanMode" )
+
+ float timeout
+ if ( SoulHasPassive( titan.GetTitanSoul(), ePassives.PAS_BUBBLESHIELD ) )
+ {
+ entity player = titan.GetBossPlayer()
+ timeout = GetBubbleShieldDuration( player )
+ }
+ else
+ {
+ timeout = 0
+ }
+
+ wait timeout
+}
+
+function TitanNPC_Think( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+
+ // JFS - Shouldn't have to check for presence of soul.
+ // The real fix for next game would be to make sure no other script can run between transferring away a titan's soul and destroying the titan.
+ // This particular bug occurred if TitanNPC_WaitForBubbleShield_StartAutoTitanBehavior() was called before soul transferred from npc to player,
+ // in which case the soul transfer killed the thread via Signal( "TitanStopsThinking" ), which causes the OnThreadEnd() to run TitanNPC_Think().
+ if ( !IsValid( soul ) )
+ return;
+
+ if ( soul.capturable || !ShouldBecomeAutoTitan( titan ) )
+ {
+ // capturable titan just kneels
+ if ( soul.GetStance() > STANCE_KNEELING )
+ thread TitanKneel( titan )
+ return
+ }
+
+ Assert( IsAlive( titan ) )
+
+ if ( !TitanCanStand( titan ) )// sets the var
+ {
+ // try to put the titan on the navmesh
+ vector ornull clampedPos = NavMesh_ClampPointForAIWithExtents( titan.GetOrigin(), titan, < 100, 100, 100 > )
+ if ( clampedPos != null )
+ {
+ expect vector( clampedPos )
+ titan.SetOrigin( clampedPos )
+ TitanCanStand( titan )
+ }
+ }
+
+ if ( !titan.GetBossPlayer() )
+ {
+ titan.Signal( "TitanStopsThinking" )
+ return
+ }
+
+ if ( "disableAutoTitanConversation" in titan.s ) //At this point the Titan has stood up and is ready to talk
+ delete titan.s.disableAutoTitanConversation
+
+ titan.EndSignal( "TitanStopsThinking" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "player_embarks_titan" )
+
+ // kneel in certain circumstances
+ for ( ;; )
+ {
+ if ( !ChangedStance( titan ) )
+ waitthread TitanWaitsToChangeStance( titan )
+ }
+}
+
+bool function ChangedStance( entity titan )
+{
+ if ( GetEmbarkDisembarkPlayer( titan ) )
+ return false
+
+ local soul = titan.GetTitanSoul()
+
+ // in a scripted sequence?
+ if ( IsValid( titan.GetParent() ) )
+ return false
+
+ if ( soul.GetStance() > STANCE_KNEELING )
+ {
+ if ( TitanShouldKneel( titan ) )
+ {
+ //waitthread PlayAnimGravity( titan, "at_MP_stand2knee_straight" )
+ waitthread KneelToShowRider( titan )
+ thread PlayAnim( titan, "at_MP_embark_idle_blended" )
+ SetStanceKneel( soul )
+ return true
+ }
+ }
+ else
+ {
+ if ( !TitanShouldKneel( titan ) && TitanCanStand( titan ) )
+ {
+ waitthread TitanStandUp( titan )
+ return true
+ }
+
+ if ( soul.GetStance() == STANCE_KNEEL )
+ {
+ thread PlayAnim( titan, "at_MP_embark_idle_blended" )
+ }
+ }
+
+ return false
+}
+
+function TitanShouldKneel( entity titan )
+{
+ local soul = titan.GetTitanSoul()
+
+ if ( soul.capturable )
+ return true
+
+ //if( HasEnemyRodeoRiders( titan ) )
+ // return true
+ if ( !TitanCanStand( titan ) )
+ return false
+
+ if ( !ShouldBecomeAutoTitan( titan ) )
+ return true
+
+ return false
+}
+
+function TitanWaitsToChangeStance( titan )
+{
+ local soul = titan.GetTitanSoul()
+ soul.EndSignal( "RodeoRiderChanged" )
+
+ titan.EndSignal( "OnAnimationInterrupted" )
+ titan.EndSignal( "OnAnimationDone" )
+
+ WaitForever()
+}
+
+function TitanStandUp( titan )
+{
+ local soul = titan.GetTitanSoul()
+ // stand up
+ titan.s.standQueued = false
+ ShowMainTitanWeapons( titan )
+ titan.Anim_Stop()
+ waitthread PlayAnimGravity( titan, "at_hotdrop_quickstand" )
+ Assert( soul == titan.GetTitanSoul() )
+ SetStanceStand( soul )
+}
+
+
+void function TitanKneel( entity titan )
+{
+ titan.EndSignal( "TitanStopsThinking" )
+ titan.EndSignal( "OnDeath" )
+ Assert( IsAlive( titan ) )
+ local soul = titan.GetTitanSoul()
+
+ waitthread KneelToShowRider( titan )
+
+ thread PlayAnim( titan, "at_MP_embark_idle_blended" )
+ SetStanceKneel( soul )
+}
+
+
+/*
+function TitanWaittillShouldStand( entity titan )
+{
+ //Don't wait if player is dead - titan should just stand up immediately
+ local player = titan.GetBossPlayer()
+ if ( !IsAlive( player ) )
+ return
+
+ player.EndSignal( "OnDeath" )
+
+ for ( ;; )
+ {
+ if ( TitanCanStand( titan ) )
+ break
+
+ wait 5
+ }
+ if ( titan.s.standQueued )
+ return
+
+ titan.WaitSignal( "titanStand" )
+}
+*/
+
+void function KneelToShowRider( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+ entity player = soul.GetBossPlayer()
+ local animation
+ local yawDif
+
+ //if ( IsAlive( player ) )
+ //{
+ // local table = GetFrontRightDots( titan, player )
+ //
+ // local dotForward = Table.dotForward
+ // local dotRight = Table.dotRight
+ //
+ //// DebugDrawLine( titanOrg, titanOrg + titan.GetForwardVector() * 200, 255, 0, 0, true, 5 )
+ //// DebugDrawLine( titanOrg, titanOrg + vecToEnt * 200, 0, 255, 0, true, 5 )
+ //
+ // if ( dotForward > 0.88 )
+ // {
+ // animation = "at_MP_stand2knee_L90"
+ // yawDif = 0
+ // }
+ // else
+ // if ( dotForward < -0.88 )
+ // {
+ // animation = "at_MP_stand2knee_R90"
+ // yawDif = 180
+ // }
+ // else
+ // if ( dotRight > 0 )
+ // {
+ // animation = "at_MP_stand2knee_straight"
+ // yawDif = 90
+ // }
+ // else
+ // {
+ // animation = "at_MP_stand2knee_180"
+ // yawDif = -90
+ // }
+ //}
+ //else
+ {
+ animation = "at_MP_stand2knee_straight"
+ yawDif = 0
+ }
+
+ thread HideOgreMainWeaponFromEnemies( titan )
+
+ if ( !IsAlive( player ) )
+ {
+ waitthread PlayAnimGravity( titan, animation )
+ return
+ }
+
+ local titanOrg = titan.GetOrigin()
+ local playerOrg = player.GetOrigin()
+
+ /*
+ local vec = playerOrg - titanOrg
+ vec.z = 0
+
+ local angles = VectorToAngles( vec )
+
+ angles.y += yawDif
+ */
+
+ local angles = titan.GetAngles()
+
+ titan.Anim_ScriptedPlayWithRefPoint( animation, titanOrg, angles, 0.5 )
+ titan.Anim_EnablePlanting()
+
+ WaittillAnimDone( titan )
+}
+
+function HideOgreMainWeaponFromEnemies( titan )
+{
+ expect entity( titan )
+
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ wait 1.0
+
+ entity soul = titan.GetTitanSoul()
+
+ Assert( IsValid( soul ) )
+
+ local titanSubClass = GetSoulTitanSubClass( soul )
+ if ( titanSubClass == "ogre" )
+ {
+ if ( IsValid( GetEnemyRodeoPilot( titan ) ) )
+ HideMainWeaponsFromEnemies( titan )
+ }
+}
+
+function HideMainWeaponsFromEnemies( titan )
+{
+ local weapons = titan.GetMainWeapons()
+ foreach ( weapon in weapons )
+ weapon.kv.visibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY
+}
+
+function ShowMainTitanWeapons( titan )
+{
+ local weapons = titan.GetMainWeapons()
+ foreach ( weapon in weapons )
+ {
+ weapon.kv.visibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ }
+}
+
+bool function ShouldBecomeAutoTitan( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+
+ if ( soul != null )
+ {
+ if ( SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ return true
+ }
+
+ return ( !PROTO_AutoTitansDisabled() )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/burnmeter/_burnmeter.gnut b/Northstar.CustomServers/mod/scripts/vscripts/burnmeter/_burnmeter.gnut
new file mode 100644
index 00000000..8e1cb71f
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/burnmeter/_burnmeter.gnut
@@ -0,0 +1,42 @@
+global function BurnMeter_Init
+global function InitBurnMeterPersistentData
+global function BurnMeter_GiveRewardDirect
+global function RunBurnCardUseFunc
+global function UseBurnCardWeapon
+global function UseBurnCardWeaponInCriticalSection
+global function GetBurnCardWeaponSkin
+
+void function BurnMeter_Init()
+{
+
+}
+
+void function InitBurnMeterPersistentData(entity player)
+{
+
+}
+
+void function BurnMeter_GiveRewardDirect( entity player, string itemRef )
+{
+
+}
+
+void function RunBurnCardUseFunc(entity player, string itemRef)
+{
+
+}
+
+void function UseBurnCardWeapon( entity weapon, entity ownerPlayer )
+{
+
+}
+
+void function UseBurnCardWeaponInCriticalSection( entity weapon, entity ownerPlayer )
+{
+
+}
+
+int function GetBurnCardWeaponSkin(entity weapon)
+{
+ return 0
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/class/CHardPointEntity.nut b/Northstar.CustomServers/mod/scripts/vscripts/class/CHardPointEntity.nut
new file mode 100644
index 00000000..a340bc32
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/class/CHardPointEntity.nut
@@ -0,0 +1,16 @@
+untyped
+
+// note: had to rename all instances of C_HardPointEntity to CHardPointEntity here, unsure why this was even a thing?
+global function CodeCallback_RegisterClass_CHardPointEntity
+
+function CodeCallback_RegisterClass_CHardPointEntity()
+{
+ CHardPointEntity.ClassName <- "CHardPointEntity"
+
+
+ function CHardPointEntity::Enabled()
+ {
+ return this.GetHardpointID() >= 0
+ }
+ #document( "CHardPointEntity::Enabled", "Returns true if this hardpoint is enabled" )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/class/cai_basenpc.nut b/Northstar.CustomServers/mod/scripts/vscripts/class/cai_basenpc.nut
new file mode 100644
index 00000000..631e01fc
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/class/cai_basenpc.nut
@@ -0,0 +1,272 @@
+untyped
+
+global function IsCrawling
+global function CodeCallback_RegisterClass_CAI_BaseNPC
+global function SetSpawnOption_AISettings
+global function SetSpawnOption_Alert
+global function SetSpawnOption_NotAlert
+global function SetSpawnOption_Ordnance
+global function SetSpawnOption_OwnerPlayer
+global function SetSpawnOption_Sidearm
+global function SetSpawnOption_SquadName
+global function SetSpawnOption_Special
+global function SetSpawnOption_Melee
+global function SetSpawnOption_CoreAbility
+global function SetSpawnOption_Antirodeo
+global function SetSpawnOption_Titanfall
+global function SetSpawnOption_TitanSoulPassive1
+global function SetSpawnOption_TitanSoulPassive2
+global function SetSpawnOption_TitanSoulPassive3
+global function SetSpawnOption_TitanSoulPassive4
+global function SetSpawnOption_TitanSoulPassive5
+global function SetSpawnOption_TitanSoulPassive6
+global function SetSpawnOption_Warpfall
+global function SetSpawnOption_Weapon
+global function SetSpawnOption_NPCTitan
+global function SetSpawnOption_TitanLoadout
+
+function CodeCallback_RegisterClass_CAI_BaseNPC()
+{
+ #document( "SetSpawnOption_AISettings", "Specify AI Setting" )
+ #document( "SetSpawnOption_Alert", "Enable spawn alerted" )
+ #document( "SetSpawnOption_NotAlert", "Enable spawn alerted" )
+ #document( "SetSpawnOption_Ordnance", "Specify spawn ordnance" )
+ #document( "SetSpawnOption_OwnerPlayer", "This titan will be the auto titan of this player" )
+ #document( "SetSpawnOption_SquadName", "Specify spawn squadname" )
+ #document( "SetSpawnOption_Special", "Specify spawn tactical ability" )
+ #document( "SetSpawnOption_Titanfall", "npc titan will spawn via titanfall" )
+ #document( "SetSpawnOption_TitanSoulPassive1", "Set this passive on the titan soul" )
+ #document( "SetSpawnOption_TitanSoulPassive2", "Set this passive on the titan soul" )
+ #document( "SetSpawnOption_TitanSoulPassive3", "Set this passive on the titan soul" )
+ #document( "SetSpawnOption_TitanSoulPassive4", "Set this passive on the titan soul" )
+ #document( "SetSpawnOption_TitanSoulPassive5", "Set this passive on the titan soul" )
+ #document( "SetSpawnOption_TitanSoulPassive6", "Set this passive on the titan soul" )
+ #document( "SetSpawnOption_Warpfall", "Titan or super spectre will spawn via warpsfall" )
+ #document( "SetSpawnOption_Weapon", "Specify spawn weapon and mods" )
+ #document( "SetSpawnOption_NPCTitan", "Spawn titan of type" )
+
+
+ //printl( "Class Script: CAI_BaseNPC" )
+
+ CAI_BaseNPC.ClassName <- "CAI_BaseNPC"
+ CAI_BaseNPC.supportsXRay <- null
+
+ CAI_BaseNPC.mySpawnOptions_aiSettings <- null
+ CAI_BaseNPC.mySpawnOptions_alert <- null
+ CAI_BaseNPC.mySpawnOptions_sidearm <- null
+ CAI_BaseNPC.mySpawnOptions_titanfallSpawn <- null
+ CAI_BaseNPC.mySpawnOptions_warpfallSpawn <- null
+ CAI_BaseNPC.mySpawnOptions_routeTD <- null
+ CAI_BaseNPC.mySpawnOptions_ownerPlayer <- null
+ CAI_BaseNPC.executedSpawnOptions <- null
+
+ function CAI_BaseNPC::HasXRaySupport()
+ {
+ return ( this.supportsXRay != null )
+ }
+
+ function CAI_BaseNPC::ForceCombat()
+ {
+ this.FireNow( "UpdateEnemyMemory", "!player" )
+ }
+ #document( CAI_BaseNPC, "ForceCombat", "Force into combat state by updating NPC's memory of the player." )
+
+ function CAI_BaseNPC::InCombat()
+ {
+ entity enemy = expect entity( this ).GetEnemy()
+ if ( !IsValid( enemy ) )
+ return false
+
+ return this.CanSee( enemy )
+ }
+ #document( CAI_BaseNPC, "InCombat", "Returns true if NPC is in combat" )
+}
+
+
+
+function SetSpawnOption_AISettings( entity npc, setting )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.mySpawnOptions_aiSettings = setting
+}
+
+function SetSpawnOption_Alert( entity npc )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.mySpawnOptions_alert = true
+}
+
+function SetSpawnOption_NotAlert( entity npc )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.mySpawnOptions_alert = false
+}
+
+void function SetSpawnOption_Weapon( entity npc, string weapon, array<string> mods = [] )
+{
+ Assert( weapon != "", "Tried to assign no weapon as a spawn weapon" )
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+
+ if ( npc.IsTitan() )
+ {
+ npc.ai.titanSpawnLoadout.primary = weapon
+ npc.ai.titanSpawnLoadout.primaryMods = mods
+ }
+ else
+ {
+ NPCDefaultWeapon spawnoptionsweapon
+ spawnoptionsweapon.wep = weapon
+ spawnoptionsweapon.mods = mods
+
+ npc.ai.mySpawnOptions_weapon = spawnoptionsweapon
+ }
+}
+
+void function SetSpawnOption_Sidearm( entity npc, string weapon, array<string> mods = [])
+{
+ Assert( weapon != "", "Tried to assign no weapon as a spawn weapon" )
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+
+ if ( !npc.IsTitan() )
+ npc.mySpawnOptions_sidearm = { wep = weapon, mods = mods }
+}
+
+void function SetSpawnOption_Ordnance( entity npc, string ordnance, array<string> mods = [] )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.ordnance = ordnance
+ npc.ai.titanSpawnLoadout.ordnanceMods = mods
+}
+
+void function SetSpawnOption_Special( entity npc, string special, array<string> mods = [] )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.special = special
+ npc.ai.titanSpawnLoadout.specialMods = mods
+}
+
+void function SetSpawnOption_Antirodeo( entity npc, string antirodeo, array<string> mods = [] )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.antirodeo = antirodeo
+ npc.ai.titanSpawnLoadout.antirodeoMods = mods
+}
+
+void function SetSpawnOption_Melee( entity npc, string melee )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.melee = melee
+}
+
+void function SetSpawnOption_CoreAbility( entity npc, string core )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.coreAbility = core
+}
+
+function SetSpawnOption_SquadName( entity npc, squadName )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.kv.squadname = squadName
+}
+
+function SetSpawnOption_Titanfall( entity npc )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ Assert( npc.IsTitan(), "npc is for titans only" )
+ npc.mySpawnOptions_titanfallSpawn = true
+}
+
+function SetSpawnOption_Warpfall( entity npc )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ Assert( npc.IsTitan() || npc.GetClassName() == "npc_super_spectre", "npc is for titans and superspectres only" )
+ npc.mySpawnOptions_warpfallSpawn = true
+}
+
+function SetSpawnOption_OwnerPlayer( entity npc, entity player )
+{
+ Assert( IsValid( player ) )
+ Assert( player.IsPlayer() )
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.mySpawnOptions_ownerPlayer = player
+}
+
+function SetSpawnOption_TitanSoulPassive1( entity npc, string passive )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.passive1 = passive
+}
+
+function SetSpawnOption_TitanSoulPassive2( entity npc, string passive )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.passive2 = passive
+}
+
+function SetSpawnOption_TitanSoulPassive3( entity npc, string passive )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.passive3 = passive
+}
+
+function SetSpawnOption_TitanSoulPassive4( entity npc, string passive )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.passive4 = passive
+}
+
+function SetSpawnOption_TitanSoulPassive5( entity npc, string passive )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.passive5 = passive
+}
+
+function SetSpawnOption_TitanSoulPassive6( entity npc, string passive )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout.passive6 = passive
+}
+
+function SetSpawnOption_NPCTitan( entity npc, int type )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( npc.IsTitan(), npc + " is not a Titan!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.bossTitanType = type
+}
+
+
+function SetSpawnOption_TitanLoadout( entity npc, TitanLoadoutDef loadout )
+{
+ Assert( IsValid( npc ) && npc.IsNPC(), npc + " is not an npc!" )
+ Assert( npc.IsTitan(), npc + " is not a Titan!" )
+ Assert( !npc.executedSpawnOptions, npc + " tried to set spawn options after npc was dispatchspawned." )
+ npc.ai.titanSpawnLoadout = loadout
+}
+
+bool function IsCrawling( entity npc )
+{
+ return npc.ai.crawling
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/class/cbasecombatcharacter.nut b/Northstar.CustomServers/mod/scripts/vscripts/class/cbasecombatcharacter.nut
new file mode 100644
index 00000000..11018cea
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/class/cbasecombatcharacter.nut
@@ -0,0 +1,28 @@
+untyped
+
+global function CodeCallback_RegisterClass_CBaseCombatCharacter
+
+function CodeCallback_RegisterClass_CBaseCombatCharacter()
+{
+ CBaseCombatCharacter.ClassName <- "CBaseCombatCharacter"
+
+ RegisterSignal( "ContextAction_SetBusy" ) // signalled from ContextAction_SetBusy() in code
+
+
+ /*
+ CBaseCombatCharacter.__SetActiveWeaponByName <- CBaseCombatCharacter.SetActiveWeaponByName
+ function CBaseCombatCharacter::SetActiveWeaponByName( weapon )
+ {
+ printt( "set active weapon " + weapon + " for " + this )
+ return this.__SetActiveWeaponByName( weapon )
+ }
+
+ CBaseCombatCharacter.__TakeWeapon <- CBaseCombatCharacter.TakeWeapon
+ function CBaseCombatCharacter::TakeWeapon( weapon )
+ {
+ // printt( "Take weapon " + weapon + " from " + this )
+ return this.__TakeWeapon( weapon )
+ }
+
+ */
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/class/cbaseentity.nut b/Northstar.CustomServers/mod/scripts/vscripts/class/cbaseentity.nut
new file mode 100644
index 00000000..08d2b2e1
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/class/cbaseentity.nut
@@ -0,0 +1,229 @@
+untyped
+
+global function CodeCallback_RegisterClass_CBaseEntity
+
+//=========================================================
+// CBaseEntity
+// Properties and methods added here can be accessed on all script entities
+//=========================================================
+
+#if DEV
+table __scriptVarDelegate = {}
+#endif
+
+function CodeCallback_RegisterClass_CBaseEntity()
+{
+ //printl( "Class Script: CBaseEntity" )
+
+ CBaseEntity.ClassName <- "CBaseEntity"
+
+ // Script variables; initializing these to something other than "null" will cause them to be treated as a
+ // static variable on the class instead of a unique variable on each instance.
+ CBaseEntity.s <- null
+
+ CBaseEntity.funcsByString <- null
+
+ CBaseEntity.useFunction <- null // should match on server/client
+
+ CBaseEntity._entityVars <- null
+
+ CBaseEntity.invulnerable <- 0
+
+ // this replacement could just be made in the scripts where this call happens,
+ // but there's too many for me to replace right now
+ CBaseEntity.__KeyValueFromString <- CBaseEntity.SetValueForKey
+ CBaseEntity.__KeyValueFromInt <- CBaseEntity.SetValueForKey
+
+ function CBaseEntity::constructor()
+ {
+ #if DEV
+ this.s = delegate __scriptVarDelegate : {}
+ #else
+ this.s = {}
+ #endif
+
+ this.useFunction = UseReturnTrue // default use function
+
+ this.funcsByString = {}
+ }
+
+ #if DEV
+ function __scriptVarDelegate::_typeof()
+ {
+ return "ScriptVariableTable"
+ }
+
+ disableoverwrite( __scriptVarDelegate )
+ #endif
+
+ /*
+ Do not delete, thanks.
+
+ CBaseEntity.__SetOrigin <- CBaseEntity.SetOrigin
+ function CBaseEntity::SetOrigin( origin )
+ {
+ if ( this.GetTargetName() == "xauto_1" )
+ {
+ printl( "\n\n" )
+ DumpStack()
+ }
+ this.__SetOrigin( origin )
+ }
+ */
+
+ function CBaseEntity::_typeof()
+ {
+ return format( "[%d] %s: %s", this.entindex(), this.GetClassName(), this.GetTargetName() )
+ }
+
+ function CBaseEntity::Get( val )
+ {
+ return this.GetValueForKey( val )
+ }
+
+ function CBaseEntity::Set( key, val )
+ {
+ return this.SetValueForKey( key, val )
+ }
+
+ // This exists in code too and is only here for untyped entity variables
+ function CBaseEntity::WaitSignal( signalID )
+ {
+ return WaitSignal( this, signalID )
+ }
+
+ // This exists in code too and is only here for untyped entity variables
+ function CBaseEntity::EndSignal( signalID )
+ {
+ EndSignal( this, signalID )
+ }
+
+ // This exists in code too and is only here for untyped entity variables
+ function CBaseEntity::Signal( signalID, results = null )
+ {
+ Signal( this, signalID, results )
+ }
+
+ function CBaseEntity::DisableDraw()
+ {
+ this.FireNow( "DisableDraw" )
+ }
+ #document( "CBaseEntity::DisableDraw", "consider this the mega hide" )
+
+ function CBaseEntity::EnableDraw()
+ {
+ this.FireNow( "EnableDraw" )
+ }
+ #document( "CBaseEntity::EnableDraw", "its back!" )
+
+
+ // --------------------------------------------------------
+ function CBaseEntity::Kill_Deprecated_UseDestroyInstead( time = 0 )
+ {
+ EntFireByHandle( this, "kill", "", time, null, null )
+ }
+ #document( "CBaseEntity::Kill_Deprecated_UseDestroyInstead", "Kill this entity: this function is deprecated because it has a one-frame delay; instead, call ent.Destroy()" )
+
+ // --------------------------------------------------------
+ function CBaseEntity::Fire( output, param = "", delay = 0, activator = null, caller = null )
+ {
+ Assert( type( output ) == "string", "output type " + type( output ) + " is not a string" )
+ EntFireByHandle( this, output, string( param ), delay, activator, caller )
+ }
+ #document( "CBaseEntity::Fire", "Fire an output on this entity, with optional parm and delay" )
+
+ function CBaseEntity::FireNow( output, param = "", activator = null, caller = null )
+ {
+ Assert( type( output ) == "string" )
+ EntFireByHandleNow( this, output, string( param ), activator, caller )
+ }
+ #document( "CBaseEntity::FireNow", "Fire an output on this entity, with optional parm and delay (synchronous)" )
+
+ // --------------------------------------------------------
+ function CBaseEntity::AddOutput( outputName, target, inputName, parameter = "", delay = 0, maxFires = 0 )
+ {
+ local targetName = target
+
+ if ( type( target ) != "string" )
+ {
+ Assert( type( target ) == "instance" )
+ targetName = target.GetTargetName()
+ Assert( targetName.len(), "AddOutput: targetted entity must have a name!" )
+ }
+ Assert( targetName.len(), "Attemped to AddOutput on an unnamed target" )
+
+ local addOutputString = outputName + " " + targetName + ":" + inputName + ":" + parameter + ":" + delay + ":" + maxFires
+ //printl(" Added output string: " + addOutputString )
+
+ EntFireByHandle( this, "AddOutput", addOutputString, 0, null, null )
+ }
+ #document( "CBaseEntity::AddOutput", "Connects an output on this entity to an input on another entity via code. The \"target\" can be a name or a named entity." )
+
+ /*
+ function MoveTo()
+ */
+
+ function CBaseEntity::MoveTo( dest, time, easeIn = 0, easeOut = 0 )
+ {
+ if ( this.GetClassName() == "script_mover" )
+ {
+ this.NonPhysicsMoveTo( dest, time, easeIn, easeOut )
+ }
+ else
+ {
+ this.SetOrigin( dest )
+ CodeWarning( "Used moveto on non script_mover: " + this.GetClassName() + ", " + this )
+ }
+ }
+ #document( "CBaseEntity::MoveTo", "Move to the specified origin over time with ease in and ease out." )
+
+ /*
+ function RotateTo()
+ */
+
+ function CBaseEntity::RotateTo( dest, time, easeIn = 0, easeOut = 0 )
+ {
+ if ( this.GetClassName() == "script_mover" )
+ {
+ this.NonPhysicsRotateTo( dest, time, easeIn, easeOut )
+ }
+ else
+ {
+ this.SetAngles( dest )
+ CodeWarning( "Used rotateto on non script_mover: " + this.GetClassName() + ", " + this )
+ }
+ }
+ #document( "CBaseEntity::RotateTo", "Rotate to the specified angles over time with ease in and ease out." )
+
+ function CBaseEntity::AddVar( varname, value )
+ {
+ Assert( !( varname in this.s ), "Tried to add variable to " + this + " that already existed: " + varname )
+
+ this.s[ varname ] <- value
+ }
+
+ function CBaseEntity::CreateStringForFunction( func )
+ {
+ // this is a general purpose function that returns a string which, when executed,
+ // runs the given function on this entity.
+ // the function must be called (or the entity deleted) at some point to avoid leaking the new slot we make in this Table.
+
+ Assert( type( func ) == "function" )
+
+ string newGuid = UniqueString()
+ this.funcsByString[newGuid] <- func
+
+ return "_RunFunctionByString( \"" + newGuid + "\" )"
+ }
+
+ function _RunFunctionByString( guid )
+ {
+ local activator = this.activator
+ Assert( activator.funcsByString[guid] )
+
+ local func = activator.funcsByString[guid].bindenv( activator.scope() )
+ delete activator.funcsByString[guid]
+
+ func()
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/class/cplayer.nut b/Northstar.CustomServers/mod/scripts/vscripts/class/cplayer.nut
new file mode 100644
index 00000000..b9f8f7eb
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/class/cplayer.nut
@@ -0,0 +1,355 @@
+untyped
+
+
+global function CodeCallback_RegisterClass_CPlayer
+global function PlayerDropsScriptedItems
+global function IsDemigod
+global function EnableDemigod
+global function DisableDemigod
+
+int __nextInputHandle = 0
+
+
+global struct PlayerSlowDownEffect
+{
+ float slowEndTime
+ float speedCap // max speed multiplier if this slow effect is active
+}
+
+function CodeCallback_RegisterClass_CPlayer()
+{
+ //printl( "Class Script: CPlayer" )
+
+ CPlayer.ClassName <- "CPlayer"
+ CPlayer.hasSpawned <- null
+ CPlayer.hasConnected <- null
+ CPlayer.isSpawning <- null
+ CPlayer.isSpawningHotDroppingAsTitan <- false
+ CPlayer.disableWeaponSlots <- false
+ CPlayer.supportsXRay <- null
+
+ CPlayer.lastTitanTime <- 0
+
+ CPlayer.globalHint <- null
+ CPlayer.playerClassData <- null
+ CPlayer.escalation <- null
+ CPlayer.pilotAbility <- null
+ CPlayer.titansBuilt <- 0
+ CPlayer.spawnTime <- 0
+ CPlayer.serverFlags <- 0
+ CPlayer.watchingKillreplayEndTime <- 0.0
+ CPlayer.cloakedForever <- false
+ CPlayer.stimmedForever <- false
+
+ RegisterSignal( "OnRespawnPlayer" )
+ RegisterSignal( "NewViewAnimEntity" )
+ RegisterSignal( "PlayerDisconnected" )
+
+ function CPlayer::constructor()
+ {
+ CBaseEntity.constructor()
+ }
+
+ function CPlayer::RespawnPlayer( ent )
+ {
+ this.Signal( "OnRespawnPlayer", { ent = ent } )
+
+ // hack. Players should clear all these on spawn.
+ this.ViewOffsetEntity_Clear()
+ ClearPlayerAnimViewEntity( expect entity( this ) )
+ this.spawnTime = Time()
+
+ this.ClearReplayDelay()
+ this.ClearViewEntity()
+
+ // titan melee can set these vars, and they need to clear on respawn:
+ this.SetOwner( null )
+ this.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+
+
+ Assert( !this.GetParent(), this + " should not have a parent yet! - Parent: " + this.GetParent() )
+ Assert( this.s.respawnCount <= 1 || IsMultiplayer(), "Tried to respawn in single player, see callstack" )
+ this.Code_RespawnPlayer( ent )
+ }
+
+ /*
+ CPlayer.__SetTrackEntity <- CPlayer.SetTrackEntity
+ function CPlayer::SetTrackEntity( ent )
+ {
+ printl( "\nTime " + Time() + " Ent " + ent )
+
+ DumpStack()
+ this.__SetTrackEntity( ent )
+ }
+ */
+
+ function CPlayer::GetDropEntForPoint( origin )
+ {
+ return null
+ }
+
+
+ function CPlayer::GetPlayerClassData( myClass )
+ {
+ Assert( myClass in this.playerClassData, myClass + " not in playerClassData" )
+ return this.playerClassData[ myClass ]
+ }
+
+
+ function CPlayer::InitMPClasses()
+ {
+ this.playerClassData = {}
+
+ Titan_AddPlayer( this )
+ Wallrun_AddPlayer( this )
+ }
+
+
+ function CPlayer::InitSPClasses()
+ {
+ this.playerClassData = {}
+ SetTargetName( expect entity( this ), expect string( this.GetTargetName() + this.entindex() ) )
+
+ Titan_AddPlayer( this )
+ }
+
+
+ // function SpawnAsClass()
+ function CPlayer::SpawnAsClass( className = null )
+ {
+ if ( !className )
+ {
+ className = this.GetPlayerClass()
+ }
+
+ switch ( className )
+ {
+ case level.pilotClass:
+ Wallrun_OnPlayerSpawn( this )
+ break
+
+ default:
+ Assert( 0, "Tried to spawn as unsupported " + className )
+ }
+ }
+
+
+ function CPlayer::GiveScriptWeapon( weaponName, equipSlot = null )
+ {
+ this.scope().GiveScriptWeapon( weaponName, equipSlot )
+ }
+
+ function CPlayer::OnDeathAsClass( damageInfo )
+ {
+ switch ( this.GetPlayerClass() )
+ {
+ case "titan":
+ Titan_OnPlayerDeath( expect entity( this ), damageInfo )
+ break
+
+ case level.pilotClass:
+ Wallrun_OnPlayerDeath( expect entity( this ), damageInfo )
+ break
+ }
+ }
+
+ function CPlayer::Disconnected()
+ {
+ this.Signal( "_disconnectedInternal" )
+ svGlobal.levelEnt.Signal( "PlayerDisconnected" )
+
+ if ( HasSoul( expect entity( this ) ) )
+ {
+ thread SoulDies( expect entity( this ).GetTitanSoul(), null )
+ }
+
+ entity titan = GetPlayerTitanInMap( expect entity( this ) )
+ if ( IsAlive( titan ) && titan.IsNPC() )
+ {
+ local soul = titan.GetTitanSoul()
+ if ( IsValid( soul ) && soul.followOnly )
+ FreeAutoTitan( titan )
+ else
+ titan.Die( null, null, { damageSourceId = eDamageSourceId.damagedef_suicide } )
+ // titan.Die()
+ }
+
+ PROTO_CleanupTrackedProjectiles( expect entity( this ) )
+
+ if ( this.globalHint != null )
+ {
+ this.globalHint.Kill_Deprecated_UseDestroyInstead()
+ this.globalHint = null
+ }
+ }
+
+
+ function CPlayer::GetClassDataEnts()
+ {
+ local ents = []
+ local added
+
+ if ( this.playerClassData == null )
+ return ents;
+
+ foreach ( ent in this.playerClassData )
+ {
+ added = false
+
+ foreach ( newent in ents )
+ {
+ if ( newent == ent )
+ {
+ added = true
+ break
+ }
+ }
+
+ if ( !added )
+ ents.append( ent )
+ }
+
+ return ents
+ }
+
+
+ function CPlayer::CleanupMPClasses()
+ {
+ }
+
+ function CPlayer::HasXRaySupport()
+ {
+ return ( this.supportsXRay != null )
+ }
+
+ function CPlayer::GiveExtraWeaponMod( mod )
+ {
+ if ( this.HasExtraWeaponMod( mod ) )
+ return
+
+ local mods = this.GetExtraWeaponMods()
+ mods.append( mod )
+
+ this.SetExtraWeaponMods( mods )
+ }
+
+
+ function CPlayer::HasExtraWeaponMod( mod )
+ {
+ local mods = this.GetExtraWeaponMods()
+ foreach( _mod in mods )
+ {
+ if ( _mod == mod )
+ return true
+ }
+ return false
+ }
+
+
+ function CPlayer::TakeExtraWeaponMod( mod )
+ {
+ if ( !this.HasExtraWeaponMod( mod ) )
+ return
+
+ local mods = this.GetExtraWeaponMods()
+ mods.fastremovebyvalue( mod )
+
+ this.SetExtraWeaponMods( mods )
+ }
+
+ function CPlayer::ClearExtraWeaponMods()
+ {
+ this.SetExtraWeaponMods( [] )
+ }
+
+
+ function CPlayer::SetPlayerPilotSettings( settingsName )
+ {
+ this.SetPlayerRequestedSettings( settingsName )
+ }
+
+ function CPlayer::RecordLastMatchContribution( contribution )
+ {
+ // replace with code function
+ }
+
+ function CPlayer::RecordLastMatchPerformance( matchPerformance )
+ {
+ // replace with code function
+ }
+
+ function CPlayer::RecordSkill( skill )
+ {
+ // replace with code function
+ this.SetPersistentVar( "ranked.recordedSkill", skill )
+ }
+
+ function CPlayer::SetPlayerSettings( settings )
+ {
+ local oldPlayerClass = CPlayer.GetPlayerClass()
+
+ CPlayer.SetPlayerSettingsWithMods( settings, [] )
+
+ this.RunSettingsChangedFuncs( settings, oldPlayerClass )
+ }
+
+ function CPlayer::SetPlayerSettingsFromDataTable( pilotDataTable )
+ {
+ local oldPlayerClass = CPlayer.GetPlayerClass()
+
+ local settings = pilotDataTable.playerSetFile
+
+ local mods = pilotDataTable.playerSetFileMods
+
+ this.SetPlayerSettingsWithMods( settings, mods )
+
+ this.RunSettingsChangedFuncs( settings, oldPlayerClass )
+ }
+
+ function CPlayer::RunSettingsChangedFuncs( settings, oldPlayerClass )
+ {
+ if ( IsAlive( expect entity( this ) ) && !this.IsTitan() && GetCurrentPlaylistVarFloat( "pilot_health_multiplier", 0.0 ) != 0.0 )
+ {
+ float pilotHealthMultiplier = GetCurrentPlaylistVarFloat( "pilot_health_multiplier", 1.0 )
+ int pilotMaxHealth = int( this.GetMaxHealth() * pilotHealthMultiplier )
+ this.SetMaxHealth( pilotMaxHealth )
+ this.SetHealth( pilotMaxHealth )
+ }
+
+ if ( this.IsTitan() )
+ {
+ entity soul = expect entity ( this.GetTitanSoul() )
+ local index = PlayerSettingsNameToIndex( settings )
+ soul.SetPlayerSettingsNum( index )
+
+ foreach ( func in svGlobal.soulSettingsChangeFuncs )
+ {
+ func( soul )
+ }
+ }
+ }
+}
+
+void function PlayerDropsScriptedItems( entity player )
+{
+ foreach ( callbackFunc in svGlobal.onPlayerDropsScriptedItemsCallbacks )
+ callbackFunc( player )
+}
+
+bool function IsDemigod( entity player )
+{
+ return player.p.demigod
+}
+
+void function EnableDemigod( entity player )
+{
+ Assert( player.IsPlayer() )
+ player.p.demigod = true
+}
+
+void function DisableDemigod( entity player )
+{
+ Assert( player.IsPlayer() )
+ player.p.demigod = false
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/class/ctitansoul.nut b/Northstar.CustomServers/mod/scripts/vscripts/class/ctitansoul.nut
new file mode 100644
index 00000000..6f5ddb3e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/class/ctitansoul.nut
@@ -0,0 +1,50 @@
+untyped
+
+global function CodeCallback_RegisterClass_CTitanSoul
+
+function CodeCallback_RegisterClass_CTitanSoul()
+{
+ CTitanSoul.ClassName <- "CTitanSoul"
+
+ // all soul-specific vars should be created here
+ CTitanSoul.lastAttackInfo <- null
+ CTitanSoul.hijackProgress <- null
+ CTitanSoul.lastHijackTime <- null
+ CTitanSoul.capturable <- null
+ CTitanSoul.followOnly <- null
+ CTitanSoul.passives <- null
+ CTitanSoul.createTime <- null
+ CTitanSoul.rodeoRiderTracker <- null
+ CTitanSoul.doomedTime <- null
+ CTitanSoul.nextRegenTime <- 0.0
+ CTitanSoul.nextHealthRegenTime <- 0.0
+ CTitanSoul.rodeoReservedSlots <- null
+
+ // the functions below should not change
+
+ function CTitanSoul::constructor()
+ {
+ CBaseEntity.constructor()
+
+ this.lastAttackInfo = { time = 0 }
+ this.passives = arrayofsize( GetNumPassives(), false )
+ this.createTime = Time()
+ this.doomedTime = null
+ this.rodeoRiderTracker = {} // all players that rode this titan, so they cant get multiple score events
+ this.capturable = false
+ this.followOnly = false
+ this.rodeoReservedSlots = arrayofsize( PROTOTYPE_DEFAULT_TITAN_RODEO_SLOTS, null ) //hardcoded 3 slots for now!
+ }
+
+
+ // function SoulDeath()
+ function CTitanSoul::SoulDestroy()
+ {
+ // transfer the soul away from the last owner
+ entity titan = expect entity( this.GetTitan() )
+ foreach ( func in svGlobal.soulTransferFuncs )
+ {
+ func( expect entity( this ), null, titan )
+ }
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/conversation/_battle_chatter.gnut b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_battle_chatter.gnut
new file mode 100644
index 00000000..961816c7
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_battle_chatter.gnut
@@ -0,0 +1,25 @@
+global function BattleChatter_Init
+global function PlayBattleChatterLine
+global function TryPlayWeaponBattleChatterLine
+
+void function BattleChatter_Init()
+{
+ //ShBattleChatter_Init()
+}
+
+void function PlayBattleChatterLine( entity player, string conversationType )
+{
+ foreach( entity otherPlayer in GetPlayerArray() )
+ if ( ShouldPlayBattleChatter( conversationType, otherPlayer, player ) && player != otherPlayer )
+ Remote_CallFunction_NonReplay( otherPlayer, "ServerCallback_PlayBattleChatter", GetConversationIndex( conversationType ), player.GetEncodedEHandle() )
+}
+
+void function TryPlayWeaponBattleChatterLine( entity player, entity weapon )
+{
+ var chatterEvent = weapon.GetWeaponInfoFileKeyField( "battle_chatter_event" )
+ if ( chatterEvent == null )
+ return
+
+ expect string( chatterEvent )
+ PlayBattleChatterLine( player, chatterEvent )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/conversation/_conversation_schedule.gnut b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_conversation_schedule.gnut
new file mode 100644
index 00000000..089d4b71
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_conversation_schedule.gnut
@@ -0,0 +1,629 @@
+untyped
+
+global function DialogueScheduleServer_Init
+
+global function GetConversationIndex
+global function PlaySquadConversationToPlayer
+global function PlaySquadConversationToTeam
+global function PlaySquadConversationToAll
+global function PlaySpectreChatterToAll
+global function PlaySpectreChatterToTeam
+global function PlaySpectreChatterToPlayer
+global function PlaySquadConversation
+global function PlayConversationToPlayer
+global function Delayed_PlayConversationToPlayer
+global function PlayConversationToTeam
+global function PlayConversationToAll
+global function PlayConversationToAllExcept
+global function PlayConversationToTeamExceptPlayer
+global function ForcePlayConversationToPlayer
+global function ForcePlayConversationToAll
+global function ForcePlayConversationToTeam
+global function SetGlobalForcedDialogueOnly
+global function SetPlayerForcedDialogueOnly
+global function CodeCallback_ScriptedDialogue
+global function GetNearbyEnemyGrunts
+global function GetNearbyFriendlyGrunts
+global function CodeCallback_OnNPCLookAtHint
+
+global function ScriptDialog_PilotCloaked
+
+struct
+{
+ array< void functionref( entity ) > codeDialogueFunc
+
+} file
+
+void function DialogueScheduleServer_Init()
+{
+ #document( "PlayConversationToPlayer", " Play conversation passed in to player specified" )
+
+ // dialogue that comes from ai schedule notifies
+
+ // must match order of enum eCodeDialogueID
+ file.codeDialogueFunc = [
+ CodeDialogue_ManDown,
+ CodeDialogue_GruntSalute,
+ CodeDialogue_EnemyContact, //As per Conger's advice: Don't depend on this one. Use WaitSignal( guy, "OnFoundEnemy", "OnSeeEnemy", "OnLostEnemy" )
+ CodeDialogue_RunFromEnemy,
+ CodeDialogue_Reload,
+ CodeDialogue_MoveToAssault,
+ CodeDialogue_MoveToSquadLeader,
+ CodeDialogue_FanOut,
+ CodeDialogue_TakeCoverFromEnemy,
+ CodeDialogue_ChaseEnemy,
+ CodeDialogue_GrenadeOut,
+ CodeDialogue_DangerousAreaDisplace,
+ CodeDialogue_ReactSurprised,
+ ]
+
+ Assert( file.codeDialogueFunc.len() == eCodeDialogueID.DIALOGUE_COUNT )
+}
+
+void function ScriptDialog_PilotCloaked( entity guy, entity enemy )
+{
+ Assert( IsPilot( enemy ), "These dialog lines assume enemy is a pilot" )
+
+ if ( NPC_GruntChatterSPEnabled( guy ) )
+ {
+ #if GRUNTCHATTER_ENABLED
+ GruntChatter_TryCloakedPilotSpotted( guy, enemy )
+ #endif
+ }
+ else
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ PlayGruntChatterMPLine( guy, "bc_engageenemycloakedpilot" )
+ #endif
+ }
+}
+
+void function CodeDialogue_GruntSalute( entity guy )
+{
+ //EmitSoundOnEntity( guy, "grunt_salute" )
+ //PlaySquadConversationToAll( "grunt_salute" )
+}
+
+void function CodeDialogue_EnemyContact( entity guy ) //As per Conger's advice: Don't depend on this one. Use WaitSignal( guy, "OnFoundEnemy", "OnSeeEnemy", "OnLostEnemy" )
+{
+}
+
+
+void function CodeDialogue_RunFromEnemy( entity guy )
+{
+ //MP and SP use different systems.
+ #if GRUNT_CHATTER_MP_ENABLED
+ //MP, use PlayOneLinerConversationOnEntWithPriority() as base function
+ entity enemy = guy.GetEnemy()
+ if ( !IsAlive( enemy ) )
+ return
+
+ if ( enemy.IsTitan() )
+ PlayGruntChatterMPLine( guy, "bc_fleePlayerTitanCall" )
+ #else
+ //SP, use r1 style PlayConversation calls()
+ // only imc has these currently
+ if ( guy.GetTeam() != TEAM_IMC )
+ return
+
+ entity enemy = guy.GetEnemy()
+ if ( !IsAlive( enemy ) )
+ return
+
+ if ( enemy.IsTitan() )
+ {
+ local squadName = guy.Get( "squadname" )
+
+ bool isSquad = false
+
+ if ( squadName != "" )
+ {
+ array<entity> squad = GetNPCArrayBySquad( squadName )
+ isSquad = squad.len() > 1
+ }
+
+ if ( isSquad )
+ {
+ // has a safe hint? running to building
+ if ( guy.GetSafeHint() )
+ PlaySquadConversationToAll( "grunt_flees_titan_building", guy )
+ else
+ PlaySquadConversationToAll( "grunt_group_flees_titan", guy )
+ }
+ else
+ {
+ PlaySquadConversationToAll( "grunt_flees_titan", guy )
+ }
+ }
+ #endif
+}
+
+void function CodeDialogue_Reload( entity guy )
+{
+ //PlaySquadConversationToAll( "aichat_reload", guy )
+}
+
+void function CodeDialogue_FanOut( entity guy )
+{
+}
+
+void function CodeDialogue_MoveToSquadLeader( entity guy )
+{
+}
+
+void function CodeDialogue_MoveToAssault( entity guy )
+{
+}
+
+void function CodeDialogue_TakeCoverFromEnemy( entity guy )
+{
+ #if HAS_BOSS_AI
+ if ( guy.IsTitan() )
+ BossTitanRetreat( guy )
+ #endif
+}
+
+void function CodeDialogue_ChaseEnemy( entity guy )
+{
+ #if HAS_BOSS_AI
+ if ( guy.IsTitan() )
+ BossTitanAdvance( guy )
+ #endif
+}
+
+void function CodeDialogue_GrenadeOut( entity guy )
+{
+ if ( NPC_GruntChatterSPEnabled( guy ) )
+ {
+ #if GRUNTCHATTER_ENABLED
+ // Ticks are actually thrown like grenades, but the callouts work differently because only Specialists use them
+ // TODO- move this info to the weapon data file
+ if ( guy.kv.grenadeWeaponName == "mp_weapon_frag_drone" )
+ GruntChatter_TryFriendlyEquipmentDeployed( guy, "mp_weapon_frag_drone" )
+ else
+ GruntChatter_TryThrowingGrenade( guy )
+ #endif
+ }
+ else
+ {
+ if ( IsSpectre( guy ) )
+ {
+ #if SPECTRE_CHATTER_MP_ENABLED
+ PlaySpectreChatterMPLine( guy, "diag_imc_spectre_gs_grenadeout_01_1" )
+ #else
+ PlaySpectreChatterToAll( "spectre_gs_grenadeout_01_1", guy )
+
+ #endif
+ }
+ else if ( IsGrunt( guy ) )
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ PlayGruntChatterMPLine( guy, "bc_grenadeOutCall" )
+ #endif
+ }
+ }
+}
+
+void function CodeDialogue_DangerousAreaDisplace( entity guy )
+{
+ #if GRUNT_CHATTER_MP_ENABLED
+ //MP ONly
+ string dangerousAreaWeaponName = guy.GetDangerousAreaWeapon()
+ //printt( "CodeDialogue_DangerousAreaDisplace, Dangerous weapon name: " + dangerousAreaWeaponName )
+ string conversationName = ""
+ switch ( dangerousAreaWeaponName ) //String comparison, not great...
+ {
+ case "mp_weapon_frag_grenade":
+ conversationName = "bc_grenadecall"
+ break
+
+ case "mp_weapon_thermite_grenade":
+ conversationName = "bc_reactGrenadeThermite"
+ break
+
+ case "mp_weapon_grenade_gravity": //By the time this triggers it looks like they're already being sucked in.
+ conversationName = "bc_reactGrenadeGravity"
+ break
+
+ case "mp_weapon_grenade_electric_smoke":
+ conversationName = "bc_reactGrenadeElecSmoke"
+ break
+
+ //Arc grenades have their dialogue triggered by PlayGruntChatterMP_DamagedByEMP() since arc grenades don't create dangerous areas
+ }
+
+ if( conversationName != "" )
+ PlayGruntChatterMPLine( guy, conversationName )
+
+ #endif
+ #if GRUNTCHATTER_ENABLED
+ //SP Only
+ if ( NPC_GruntChatterSPEnabled( guy ) )
+ GruntChatter_TryDisplacingFromDangerousArea( guy )
+ #endif
+}
+
+void function CodeDialogue_ReactSurprised( entity guy )
+{
+ #if GRUNTCHATTER_ENABLED
+ if ( NPC_GruntChatterSPEnabled( guy ) )
+ {
+ int aiSurprisedReactionType = guy.GetSurprisedReactionReason()
+
+ switch ( aiSurprisedReactionType )
+ {
+ case RSR_SIDE_FLANK:
+ case RSR_REAR_FLANK:
+ GruntChatter_TryGruntFlankedByPlayer( guy, aiSurprisedReactionType )
+ break
+ }
+ }
+ #endif
+}
+
+void function CodeDialogue_ManDown( entity guy )
+{
+}
+
+void function SetGlobalForcedDialogueOnly( bool value )
+{
+ level.nv.forcedDialogueOnly = value
+}
+
+void function SetPlayerForcedDialogueOnly( entity player, bool value )
+{
+ player.SetForcedDialogueOnly( value )
+}
+
+void function Delayed_PlayConversationToPlayer( string conversation, entity player, float delay )
+{
+ player.EndSignal( "OnDeath" )
+ wait delay
+ PlayConversationToPlayer( conversation, player )
+}
+
+void function PlayConversationToPlayer( string conversationType, entity player )
+{
+ if ( IsForcedDialogueOnly( player ) )
+ {
+ printt( "ForcedDialogueOnly, not playing conversationType: " + conversationType )
+ return
+ }
+
+ PlayConversation_internal( conversationType, player )
+}
+
+void function PlayConversationToTeam( string conversationType, int team )
+{
+ array<entity> playerArr = GetPlayerArrayOfTeam( team )
+ foreach( player in playerArr )
+ PlayConversationToPlayer( conversationType, player )
+}
+
+void function PlayConversationToTeamExceptPlayer( string conversationType, int team, entity excludePlayer )
+{
+ array<entity> playerArr = GetPlayerArrayOfTeam( team )
+ foreach( player in playerArr )
+ {
+ if ( player == excludePlayer )
+ continue
+
+ PlayConversation_internal( conversationType, player )
+ }
+}
+
+void function PlayConversationToAll( string conversationType )
+{
+ array<entity> playerArr = GetPlayerArray()
+ foreach( player in playerArr )
+ PlayConversationToPlayer( conversationType, player )
+}
+
+void function PlayConversation_internal( string conversationType, entity player )
+{
+ #if FACTION_DIALOGUE_ENABLED
+ return
+ #endif
+
+ int conversationID = GetConversationIndex( conversationType )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlayConversation", conversationID )
+}
+
+void function ForcePlayConversationToAll( string conversationType )
+{
+ array<entity> playerArr = GetPlayerArray()
+ foreach( player in playerArr )
+ {
+ ForcePlayConversationToPlayer( conversationType, player )
+ }
+}
+
+void function ForcePlayConversationToTeam( string conversationType, team )
+{
+ array<entity> playerArr = GetPlayerArrayOfTeam( team )
+ foreach( player in playerArr )
+ {
+ ForcePlayConversationToPlayer( conversationType, player )
+ }
+}
+
+//Like PlayConversation, but no checking for flags
+void function ForcePlayConversationToPlayer( string conversationType, entity player )
+{
+ PlayConversation_internal( conversationType, player )
+}
+
+array<entity> function GetNearbyFriendlyGrunts( vector origin, int team, range = null )
+{
+ float searchRange = AI_CONVERSATION_RANGE
+ if ( range != null )
+ searchRange = expect float( range )
+
+ array<entity> guys
+ array<entity> ai = GetNPCArrayEx( "npc_soldier", team, TEAM_ANY, origin, searchRange )
+ foreach ( guy in ai )
+ {
+ if ( IsAlive( guy ) )
+ guys.append( guy )
+ }
+
+ return guys
+}
+
+array<entity> function GetNearbyEnemyGrunts( vector origin, int team, range = null )
+{
+ float searchRange = AI_CONVERSATION_RANGE
+ if ( range != null )
+ searchRange = expect float( range )
+
+ array<entity> guys
+ array<entity> ai = GetNPCArrayEx( "npc_soldier", TEAM_ANY, team, origin, searchRange )
+ foreach ( guy in ai )
+ {
+ if ( IsAlive( guy ) )
+ guys.append( guy )
+ }
+
+ return guys
+}
+
+bool function SquadExistsForConversation( entity ai, string conversationType )
+{
+ if ( !IsAlive( ai ) )
+ return false
+
+ // only soldiers play squad conversations
+ if ( !IsGrunt( ai ) )
+ return false
+
+ //Squadless AI don't play squad conversations
+ local squadName = ai.Get( "squadname" )
+ if ( squadName == "" )
+ return false
+
+ // only all-soldier squads can use squad conversations
+ array<entity> squad = GetNPCArrayBySquad( squadName )
+ if ( !squad.len() )
+ return false
+
+ bool foundNonSoldier = false
+ foreach ( guy in squad )
+ {
+ if ( !IsGrunt( guy ) )
+ {
+ foundNonSoldier = true
+ break
+ }
+ }
+
+ if ( !(DoesConversationExist( conversationType ) ))
+ {
+ printt( "*****CONVERSATION WARNING***** Conversation " + conversationType + " does not exist! Returning" )
+ return false
+ }
+
+ return true
+}
+
+function GetSquadEHandles( ai )
+{
+ expect entity( ai )
+
+ local aiHandles = [ null, null, null, null ]
+
+ string squadName = expect string( ai.Get( "squadname" ) )
+
+ if ( squadName == "" )
+ return aiHandles
+
+ array<entity> squad = GetNPCArrayBySquad( squadName )
+ squad.fastremovebyvalue( ai )
+ aiHandles[0] = ai.GetEncodedEHandle()
+
+ int nextIdx = 1
+
+ foreach ( guy in squad )
+ {
+ if ( !IsValid( guy ) )
+ continue
+
+ switch ( guy.GetClassName() )
+ {
+ case "npc_soldier":
+ aiHandles[ nextIdx ] = guy.GetEncodedEHandle()
+ ++nextIdx
+ break
+ }
+
+ if ( nextIdx >= aiHandles.len() )
+ break
+ }
+
+ return aiHandles
+}
+
+void function PlaySquadConversationToPlayer( string conversationType, entity player, entity ai, float rangeSqr = AI_CONVERSATION_RANGE_SQR )
+{
+ if ( SquadExistsForConversation( ai, conversationType ) )
+ {
+ local aiHandles = GetSquadEHandles( ai )
+ PlaySquadConversationToPlayer_Internal( conversationType, player, ai, rangeSqr, aiHandles )
+ }
+}
+
+// All PlaySquadConversation functions eventually funnel down to this.
+// Funciton is broken apart from PlaySquadConversationToPlayer since PlaySquadConversationToPlayer has
+// a few expensive checks that only need to be run once for every conversation we're trying to play,
+// as opposed to for every player we're trying to play a conversation to.
+void function PlaySquadConversationToPlayer_Internal( string conversationType, entity player, entity ai, float rangeSqr, aiHandles )
+{
+ #if GRUNT_CHATTER_MP_ENABLED
+ return
+ #endif
+
+ Assert( IsAlive( ai ), ai + " is dead." )
+ Assert( aiHandles.len() == 4 )
+ vector org = ai.GetOrigin()
+ float debounceTime = GetConversationDebounce( conversationType )
+ float allowedTime = Time() - debounceTime
+
+ // tell client to play conversation
+ int conversationID = GetConversationIndex( conversationType )
+ if ( !ShouldPlaySquadConversation( player, conversationType, allowedTime, org, rangeSqr ) )
+ return
+
+ UpdateConversationTracking( player, conversationType, Time() )
+ Remote_CallFunction_Replay( player, "ServerCallback_PlaySquadConversation", conversationID, aiHandles[0], aiHandles[1], aiHandles[2], aiHandles[3] )
+}
+
+void function PlaySquadConversation( string conversationType, entity ai )
+{
+ PlaySquadConversationToAll( conversationType, ai )
+}
+
+void function PlaySquadConversationToAll( string conversationType, entity ai, float rangeSqr = AI_CONVERSATION_RANGE_SQR )
+{
+ if ( !SquadExistsForConversation( ai, conversationType ) )
+ return
+
+ local aiHandles = GetSquadEHandles( ai )
+
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ PlaySquadConversationToPlayer_Internal( conversationType, player, ai, rangeSqr, aiHandles )
+ }
+}
+
+void function PlaySquadConversationToTeam( string conversationType, int team, entity ai, float rangeSqr = AI_CONVERSATION_RANGE_SQR )
+{
+ if ( !SquadExistsForConversation( ai, conversationType ) )
+ return
+
+ local aiHandles = GetSquadEHandles( ai )
+
+ array<entity> players = GetPlayerArrayOfTeam( team )
+ foreach ( player in players )
+ {
+ PlaySquadConversationToPlayer_Internal( conversationType, player, ai, rangeSqr, aiHandles )
+ }
+}
+
+void function PlaySpectreChatterToAll( string conversationType, entity spectre, float rangeSqr = AI_CONVERSATION_RANGE_SQR )
+{
+ PlaySpectreChatterToTeam( conversationType, TEAM_IMC, spectre, rangeSqr )
+ PlaySpectreChatterToTeam( conversationType, TEAM_MILITIA, spectre, rangeSqr )
+}
+
+void function PlaySpectreChatterToTeam( string conversationType, team, entity spectre, float rangeSqr = AI_CONVERSATION_RANGE_SQR )
+{
+ array<entity> players = GetPlayerArrayOfTeam( team )
+ foreach ( player in players )
+ {
+ PlaySpectreChatterToPlayer( conversationType, player, spectre, rangeSqr )
+ }
+}
+
+void function PlaySpectreChatterToPlayer( string conversationType, entity player, entity spectre, float rangeSqr = AI_CONVERSATION_RANGE_SQR )
+{
+ //PrintFunc()
+ vector spectreOrigin = spectre.GetOrigin()
+ float debounceTime = DEFAULT_CONVERSATION_DEBOUNCE_TIME // Spectre conversations aren't as real as the Grunt ones- they don't get registered bc they just EmitSound
+ float allowedTime = Time() - debounceTime
+
+ string teamSpecificSoundAlias = GetSpectreTeamSpecificSoundAlias( spectre, conversationType )
+
+ if ( teamSpecificSoundAlias == "" )
+ // neutral AI don't have dialog
+ return
+
+ Assert( DoesAliasExist( teamSpecificSoundAlias ) )
+
+ //printt( "Trying to play spectre chatter: " + teamSpecificSoundAlias + " to player: " + player)
+ if ( !ShouldPlaySquadConversation( player, teamSpecificSoundAlias, allowedTime, spectreOrigin, rangeSqr ) )
+ return
+
+ UpdateConversationTracking( player, teamSpecificSoundAlias, Time() )
+
+ EmitSoundOnEntityOnlyToPlayer( spectre, player, teamSpecificSoundAlias )
+}
+
+string function GetSpectreTeamSpecificSoundAlias( entity spectre, string partialConversationAlias )
+{
+ int spectreTeam = spectre.GetTeam()
+
+ if ( spectreTeam == TEAM_IMC )
+ return "diag_imc_" + partialConversationAlias
+ else if ( spectreTeam == TEAM_MILITIA )
+ return "diag_militia_" + partialConversationAlias
+
+ return ""
+}
+
+void function PlayConversationToAllExcept( string conversationType, array<entity> exceptions )
+{
+ array<entity> playerArr = GetPlayerArray()
+
+ table<entity, int> exceptionsTable
+ foreach( exceptionPlayer in exceptions )
+ {
+ exceptionsTable[ exceptionPlayer ] <- 1
+ }
+
+ foreach ( player in playerArr )
+ {
+ if ( player in exceptionsTable )
+ continue
+
+ PlayConversationToPlayer( conversationType, player )
+ }
+}
+
+void function CodeCallback_ScriptedDialogue( entity guy, int dialogueID )
+{
+ Assert( dialogueID < file.codeDialogueFunc.len() )
+
+ if ( dialogueID in file.codeDialogueFunc )
+ {
+ file.codeDialogueFunc[ dialogueID ]( guy )
+ }
+}
+
+function UpdateConversationTracking( player, conversationType, time )
+{
+ if ( !(conversationType in player.s.lastAIConversationTime) )
+ player.s.lastAIConversationTime[ conversationType ] <- time
+ else
+ player.s.lastAIConversationTime[ conversationType ] = time
+}
+
+int function GetConversationIndex( string conversation )
+{
+ Assert( conversation != "", "No conversation specified." )
+ Assert( typeof(conversation) == "string" )
+ return GetConversationToIndexTable()[ conversation ]
+}
+
+void function CodeCallback_OnNPCLookAtHint( entity npc, entity hint )
+{
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/conversation/_faction_dialogue.gnut b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_faction_dialogue.gnut
new file mode 100644
index 00000000..ccb5cd6e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_faction_dialogue.gnut
@@ -0,0 +1,46 @@
+global function FactionDialogue_Init
+global function InitFactionDialoguePersistence
+global function PlayFactionDialogueToPlayer
+global function PlayFactionDialogueToTeam
+global function PlayFactionDialogueToTeamExceptPlayer
+
+void function FactionDialogue_Init()
+{
+ AddCallback_OnClientConnected( AssignEnemyFactionToPlayer )
+}
+
+void function InitFactionDialoguePersistence( entity player )
+{
+ // doesn't seem to be used? required to compile tho
+}
+
+void function PlayFactionDialogueToPlayer( string conversationType, entity player )
+{
+ #if !FACTION_DIALOGUE_ENABLED
+ return
+ #endif
+
+ if ( !ShouldPlayFactionDialogue( conversationType, player ) )
+ return
+
+ int conversationIndex = GetConversationIndex( conversationType )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlayFactionDialogue", conversationIndex )
+}
+
+void function PlayFactionDialogueToTeam( string conversationType, int team )
+{
+ foreach ( entity player in GetPlayerArrayOfTeam( team ) )
+ PlayFactionDialogueToPlayer( conversationType, player )
+}
+
+void function PlayFactionDialogueToTeamExceptPlayer( string conversationType, int team, entity except )
+{
+ foreach ( entity player in GetPlayerArrayOfTeam( team ) )
+ if ( player != except )
+ PlayFactionDialogueToPlayer( conversationType, player )
+}
+
+void function AssignEnemyFactionToPlayer( entity player )
+{
+ AssignEnemyFaction( player, expect string( player.GetPersistentVar( "factionChoice" ) ) )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/conversation/_grunt_chatter_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_grunt_chatter_mp.gnut
new file mode 100644
index 00000000..b638e92b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_grunt_chatter_mp.gnut
@@ -0,0 +1,18 @@
+global function GruntChatter_MP_Init
+global function PlayGruntChatterMPLine
+
+void function GruntChatter_MP_Init()
+{
+ //ShGruntChatter_MP_Init()
+}
+
+void function PlayGruntChatterMPLine( entity grunt, string conversationType )
+{
+ #if !GRUNT_CHATTER_MP_ENABLED
+ return
+ #endif
+
+ foreach ( entity player in GetPlayerArray() )
+ if ( ShouldPlayGruntChatterMPLine( conversationType, player, grunt ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlayGruntChatterMP", GetConversationIndex( conversationType ), grunt.GetEncodedEHandle() )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/conversation/_spectre_chatter_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_spectre_chatter_mp.gnut
new file mode 100644
index 00000000..2f9e0f84
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/conversation/_spectre_chatter_mp.gnut
@@ -0,0 +1,18 @@
+global function SpectreChatter_MP_Init
+global function PlaySpectreChatterMPLine
+
+void function SpectreChatter_MP_Init()
+{
+ //ShSpectreChatter_MP_Init()
+}
+
+void function PlaySpectreChatterMPLine( entity spectre, string conversationType )
+{
+ #if !SPECTRE_CHATTER_MP_ENABLED
+ return
+ #endif
+
+ foreach ( entity player in GetPlayerArray() )
+ if ( ShouldPlaySpectreChatterMPLine( conversationType, player, spectre ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlaySpectreChatterMP", GetConversationIndex( conversationType ), spectre.GetEncodedEHandle() )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter.gnut b/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter.gnut
new file mode 100644
index 00000000..dda84976
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter.gnut
@@ -0,0 +1,508 @@
+global function Sv_EarnMeter_Init
+global function PlayerEarnMeter_SoftReset
+global function PlayerEarnMeter_SetOwnedFrac
+global function PlayerEarnMeter_Reset
+global function PlayerEarnMeter_Empty
+global function PlayerEarnMeter_AddEarnedFrac
+global function PlayerEarnMeter_AddOwnedFrac
+global function PlayerEarnMeter_AddEarnedAndOwned
+global function PlayerEarnMeter_SetMode
+global function PlayerEarnMeter_SetRewardFrac
+
+global function PlayerEarnMeter_GetPilotMultiplier
+global function PlayerEarnMeter_GetPilotOverdriveEnum
+
+global function PlayerEarnMeter_RefreshGoal
+
+global function PlayerEarnMeter_SetReward
+global function PlayerEarnMeter_SetGoal
+
+global function PlayerEarnMeter_SetGoalUsed
+global function PlayerEarnMeter_EnableGoal
+global function PlayerEarnMeter_DisableGoal
+
+global function PlayerEarnMeter_SetRewardUsed
+global function PlayerEarnMeter_DisableReward
+global function PlayerEarnMeter_EnableReward
+
+global function PlayerEarnMeter_CanEarn
+
+global function SetCallback_EarnMeterGoalEarned
+global function SetCallback_EarnMeterRewardEarned
+
+global function AddEarnMeterThresholdEarnedCallback
+
+global function JFS_PlayerEarnMeter_CoreRewardUpdate
+global function GiveOffhandElectricSmoke
+
+global function SharedEarnMeter_AddEarnedAndOwned
+global function PlayerEarnMeter_SetEnabled
+global function PlayerEarnMeter_Enabled
+
+global struct EarnMeterThresholdEarnedStruct
+{
+ float threshold
+ bool triggerFunctionOnFullEarnMeter = false
+ void functionref( entity player ) thresholdEarnedCallback
+}
+
+struct
+{
+ void functionref( entity player ) goalEarnedCallback
+ void functionref( entity player ) rewardEarnedCallback
+ array<EarnMeterThresholdEarnedStruct> thresholdEarnedCallbacks
+
+ float earn_meter_pilot_multiplier
+ int earn_meter_pilot_overdrive // ePilotOverdrive
+ bool earnMeterEnabled = true
+} file
+
+void function Sv_EarnMeter_Init()
+{
+ if ( !EARNMETER_ENABLED )
+ return
+
+ RegisterSignal( "EarnMeterDecayThink" )
+
+ SetCallback_EarnMeterGoalEarned( DummyGoalEarnedCallback )
+ SetCallback_EarnMeterRewardEarned( DummyRewardEarnedCallback )
+
+ file.earn_meter_pilot_multiplier = PlayerEarnMeter_GetPilotMultiplier()
+ file.earn_meter_pilot_overdrive = PlayerEarnMeter_GetPilotOverdriveEnum()
+}
+
+float function PlayerEarnMeter_GetPilotMultiplier()
+{
+ return GetCurrentPlaylistVarFloat( "earn_meter_pilot_multiplier", 1.0 )
+}
+
+int function PlayerEarnMeter_GetPilotOverdriveEnum()
+{
+ return GetCurrentPlaylistVarInt( "earn_meter_pilot_overdrive", ePilotOverdrive.Enabled )
+}
+
+void function AddEarnMeterThresholdEarnedCallback( float thresholdForCallback, void functionref( entity player ) callbackFunc, bool triggerFunctionOnFullEarnMeter = false )
+{
+ EarnMeterThresholdEarnedStruct thresholdStruct
+ thresholdStruct.threshold = thresholdForCallback
+ thresholdStruct.thresholdEarnedCallback = callbackFunc
+ thresholdStruct.triggerFunctionOnFullEarnMeter = triggerFunctionOnFullEarnMeter
+
+ Assert( !AlreadyContainsThresholdCallback( thresholdStruct ), "Already added " + string( callbackFunc ) + " with threshold " + thresholdForCallback )
+ file.thresholdEarnedCallbacks.append( thresholdStruct )
+}
+
+bool function AlreadyContainsThresholdCallback( EarnMeterThresholdEarnedStruct thresholdStruct )
+{
+ foreach( existingThresholdStruct in file.thresholdEarnedCallbacks )
+ {
+ if ( existingThresholdStruct.threshold != thresholdStruct.threshold )
+ continue
+
+ if ( existingThresholdStruct.thresholdEarnedCallback != thresholdStruct.thresholdEarnedCallback )
+ continue
+
+ if ( existingThresholdStruct.triggerFunctionOnFullEarnMeter != thresholdStruct.triggerFunctionOnFullEarnMeter )
+ continue
+
+ return true
+ }
+
+ return false
+}
+
+void function SetCallback_EarnMeterGoalEarned( void functionref( entity player ) callback )
+{
+ if ( file.goalEarnedCallback == null || file.goalEarnedCallback == DummyGoalEarnedCallback )
+ file.goalEarnedCallback = callback
+}
+
+void function SetCallback_EarnMeterRewardEarned( void functionref( entity player ) callback )
+{
+ if ( file.rewardEarnedCallback == null || file.rewardEarnedCallback == DummyRewardEarnedCallback )
+ file.rewardEarnedCallback = callback
+}
+
+
+void function PlayerEarnMeter_SetMode( entity player, int mode )
+{
+ player.SetPlayerNetInt( EARNMETER_MODE, mode )
+}
+
+
+void function PlayerEarnMeter_AddEarnedFrac( entity player, float earnedFrac )
+{
+ PlayerEarnMeter_AddEarnedAndOwned( player, earnedFrac, 0.0 )
+}
+
+
+void function PlayerEarnMeter_AddOwnedFrac( entity player, float addValue )
+{
+ PlayerEarnMeter_AddEarnedAndOwned( player, 0.0, addValue )
+}
+
+
+bool function PlayerEarnMeter_CanEarn( entity player )
+{
+ if ( PlayerEarnMeter_GetMode( player ) != eEarnMeterMode.DEFAULT || player.IsTitan() || IsValid( player.GetPetTitan() ) )
+ return false
+
+ return file.earnMeterEnabled
+}
+
+void function SharedEarnMeter_AddEarnedAndOwned( entity player, float addOverdriveValue, float addOwnedValue )
+{
+ int teamShareEarnMeter = Riff_TeamShareEarnMeter()
+ Assert( teamShareEarnMeter != eTeamShareEarnMeter.Disabled )
+
+ float sharedEarnMeterScale = GetCurrentPlaylistVarFloat( "riff_team_share_earn_meter_scale", 0.5 )
+
+ float overdriveValue = addOverdriveValue * sharedEarnMeterScale
+ float ownedValue = addOwnedValue * sharedEarnMeterScale
+
+ array<entity> teamPlayers = GetPlayerArrayOfTeam_Alive( player.GetTeam() )
+ foreach ( teamPlayer in teamPlayers )
+ {
+ if ( teamPlayer == player )
+ continue
+
+ if ( !PlayerEarnMeter_CanEarn( teamPlayer ) )
+ continue
+
+ if ( teamShareEarnMeter == eTeamShareEarnMeter.Enabled )
+ PlayerEarnMeter_AddEarnedAndOwned( teamPlayer, overdriveValue, ownedValue )
+ else if ( teamShareEarnMeter == eTeamShareEarnMeter.OwnedOnly )
+ PlayerEarnMeter_AddOwnedFrac( teamPlayer, ownedValue )
+ else if ( teamShareEarnMeter == eTeamShareEarnMeter.OverdriveOnly )
+ PlayerEarnMeter_AddEarnedFrac( teamPlayer, overdriveValue )
+ }
+}
+
+void function PlayerEarnMeter_AddEarnedAndOwned( entity player, float addOverdriveValue, float addOwnedValue )
+{
+ // TODO: Core Meter should be unified with earn meter so this can go away and we keep the hot streak concept for Titan Cores.
+ if ( player.IsTitan() )
+ {
+ AddCreditToTitanCoreBuilder( player, addOwnedValue )
+ return
+ }
+
+ if ( !PlayerEarnMeter_CanEarn( player ) )
+ return
+
+ if ( addOverdriveValue == 0 && addOwnedValue == 0 )
+ return
+
+ if ( file.earn_meter_pilot_overdrive == ePilotOverdrive.Only )
+ addOwnedValue = 0.0
+
+ if ( file.earn_meter_pilot_overdrive == ePilotOverdrive.Disabled )
+ addOverdriveValue = 0.0
+
+ float startingOverdriveValue = PlayerEarnMeter_GetEarnedFrac( player )
+ float startingOwnedValue = PlayerEarnMeter_GetOwnedFrac( player )
+ float startingOverdriveDiff = max( 0, startingOverdriveValue - startingOwnedValue )
+
+ float multipliedOwnedValue = addOwnedValue * file.earn_meter_pilot_multiplier
+ float newOwnedValue = min( startingOwnedValue + multipliedOwnedValue, 1.0 )
+ PlayerEarnMeter_SetOwnedFrac( player, min( newOwnedValue, 1.0 ) )
+
+ float multipliedOverdriveValue = addOverdriveValue * file.earn_meter_pilot_multiplier
+ float newOverdriveValue = max( min( newOwnedValue + startingOverdriveDiff + multipliedOverdriveValue, 1.0 ), 0.0 )
+ PlayerEarnMeter_SetEarnedFrac( player, newOverdriveValue )
+
+ if ( newOverdriveValue > startingOverdriveValue )
+ thread EarnMeterDecayThink( player )
+
+ foreach( thresholdStruct in file.thresholdEarnedCallbacks )
+ {
+ if ( newOverdriveValue < thresholdStruct.threshold ) //We're not past the threshold yet, don't run the function
+ continue
+
+ if ( startingOverdriveValue >= thresholdStruct.threshold ) //This isn't the first time we're past the threshold, don't run the function
+ continue
+
+ if ( newOwnedValue == 1.0 && thresholdStruct.triggerFunctionOnFullEarnMeter == false ) //We've earned enough earn meter to just fill out the bar, we should just run whatever functionality
+ continue
+
+ thresholdStruct.thresholdEarnedCallback( player )
+ }
+
+ if ( PlayerEarnMeter_IsRewardEnabled( player ) )
+ {
+ float rewardFrac = PlayerEarnMeter_GetRewardFrac( player )
+
+ // If we earned our reward
+ if ( (startingOverdriveValue < rewardFrac && newOverdriveValue >= rewardFrac) || (startingOwnedValue < rewardFrac && newOwnedValue >= rewardFrac) )
+ {
+ //if ( newOwnedValue < rewardFrac ) // if the owned portion isn't already maxed out, do so
+ // PlayerEarnMeter_SetOwnedFrac( player, rewardFrac )
+
+ PlayerEarnMeter_TryMakeRewardAvailable( player )
+ }
+ }
+
+ // If we earned our goal
+ if ( (startingOverdriveValue < 1.0 && newOverdriveValue >= 1.0) || (startingOwnedValue < 1.0 && newOwnedValue >= 1.0) )
+ {
+ if ( newOwnedValue < 1.0 ) // if the owned portion isn't already maxed out, do so
+ PlayerEarnMeter_SetOwnedFrac( player, 1.0 )
+
+ PlayerEarnMeter_TryMakeGoalAvailable( player )
+ }
+
+ //#if MP
+ // Remote_CallFunction_NonReplay( player, "ServerCallback_EarnMeterAwarded", addOverdriveValue, addOwnedValue )
+ //#endif
+}
+
+
+void function PlayerEarnMeter_RefreshGoal( entity player )
+{
+ if ( player.GetPlayerNetInt( "goalState" ) == eRewardState.AVAILABLE )
+ {
+ file.goalEarnedCallback( player )
+ }
+}
+
+
+void function PlayerEarnMeter_SetEarnedFrac( entity player, float value )
+{
+ player.p.earnMeterOverdriveFrac = value
+ player.SetPlayerNetFloat( EARNMETER_EARNEDFRAC, value )
+}
+
+
+void function PlayerEarnMeter_SetOwnedFrac( entity player, float value )
+{
+ player.p.earnMeterOwnedFrac = value
+ player.SetPlayerNetFloat( EARNMETER_OWNEDFRAC, value )
+}
+
+
+void function PlayerEarnMeter_SetRewardFrac( entity player, float value )
+{
+ player.p.earnMeterRewardFrac = value
+ player.SetPlayerNetFloat( EARNMETER_REWARDFRAC, value )
+}
+
+
+void function PlayerEarnMeter_SoftReset( entity player )
+{
+ float ownedFrac = PlayerEarnMeter_GetOwnedFrac( player )
+ PlayerEarnMeter_SetEarnedFrac( player, ownedFrac )
+}
+
+
+void function PlayerEarnMeter_Reset( entity player )
+{
+ player.Signal( "EarnMeterDecayThink" )
+
+ PlayerEarnMeter_SetEarnedFrac( player, 0.0 )
+ PlayerEarnMeter_SetOwnedFrac( player, 0.0 )
+ PlayerEarnMeter_SetRewardFrac( player, 0.0 )
+
+ player.SetPlayerNetInt( "goalState", eRewardState.DISABLED )
+ player.SetPlayerNetInt( "rewardState", eRewardState.DISABLED )
+}
+
+void function PlayerEarnMeter_Empty( entity player )
+{
+ player.Signal( "EarnMeterDecayThink" )
+
+ PlayerEarnMeter_SetEarnedFrac( player, 0.0 )
+ PlayerEarnMeter_SetOwnedFrac( player, 0.0 )
+ PlayerEarnMeter_SetRewardFrac( player, 0.0 )
+}
+
+
+void function EarnMeterDecayThink( entity player )
+{
+ player.EndSignal( "OnDeath" )
+ player.Signal( "EarnMeterDecayThink" )
+ player.EndSignal( "EarnMeterDecayThink" )
+
+ if ( EarnMeter_DecayHold() < 0 )
+ return
+
+ wait EarnMeter_DecayHold()
+
+ float earnedValue = PlayerEarnMeter_GetEarnedFrac( player )
+ float ownedValue = PlayerEarnMeter_GetOwnedFrac( player )
+
+ // 10% over 20 seconds
+ float decayRate = 1.0 / 135.0
+
+ //float startTime = Time()
+ while ( earnedValue > ownedValue )
+ {
+ //float frameTime = Time() - startTime
+ //startTime = Time()
+
+ PlayerEarnMeter_AddEarnedFrac( player, -(decayRate * 0.25) )
+
+ wait 0.25
+
+ earnedValue = PlayerEarnMeter_GetEarnedFrac( player )
+ ownedValue = PlayerEarnMeter_GetOwnedFrac( player )
+ }
+}
+
+
+bool function PlayerEarnMeter_TryMakeGoalAvailable( entity player )
+{
+ if ( player.GetPlayerNetInt( "goalState" ) == eRewardState.USED )
+ return false
+
+ if ( player.GetPlayerNetInt( "goalState" ) == eRewardState.DISABLED )
+ return false
+
+ if ( player.GetPlayerNetInt( "goalState" ) == eRewardState.AVAILABLE )
+ return false
+
+ player.SetPlayerNetInt( "goalState", eRewardState.AVAILABLE )
+
+ file.goalEarnedCallback( player )
+
+ return true
+}
+
+
+void function PlayerEarnMeter_DisableReward( entity player )
+{
+ player.SetPlayerNetInt( "rewardState", eRewardState.DISABLED )
+}
+
+
+void function PlayerEarnMeter_EnableReward( entity player )
+{
+ player.SetPlayerNetInt( "rewardState", eRewardState.UNAVAILABLE )
+}
+
+
+void function PlayerEarnMeter_SetRewardUsed( entity player )
+{
+ player.SetPlayerNetInt( "rewardState", eRewardState.USED )
+}
+
+
+void function PlayerEarnMeter_DisableGoal( entity player )
+{
+ player.SetPlayerNetInt( "goalState", eRewardState.DISABLED )
+}
+
+
+void function PlayerEarnMeter_EnableGoal( entity player )
+{
+ player.SetPlayerNetInt( "goalState", eRewardState.UNAVAILABLE )
+}
+
+
+void function PlayerEarnMeter_SetGoalUsed( entity player )
+{
+ player.SetPlayerNetInt( "goalState", eRewardState.USED )
+}
+
+
+bool function PlayerEarnMeter_TryMakeRewardAvailable( entity player )
+{
+ if ( player.GetPlayerNetInt( "rewardState" ) == eRewardState.USED )
+ return false
+
+ if ( player.GetPlayerNetInt( "rewardState" ) == eRewardState.DISABLED )
+ return false
+
+ if ( player.GetPlayerNetInt( "rewardState" ) == eRewardState.AVAILABLE )
+ return false
+
+ player.SetPlayerNetInt( "rewardState", eRewardState.AVAILABLE )
+
+ file.rewardEarnedCallback( player )
+ return true
+}
+
+
+void function PlayerEarnMeter_SetReward( entity player, EarnObject earnObject )
+{
+ Assert( earnObject.id > -1 )
+ Assert( earnObject.earnType == "REWARD" )
+
+ player.SetPlayerNetInt( EARNMETER_REWARDID, earnObject.id )
+}
+
+void function PlayerEarnMeter_SetGoal( entity player, EarnObject earnObject )
+{
+ Assert( earnObject.id > -1 )
+ //Assert( earnObject.earnType == "GOAL" )
+
+ player.SetPlayerNetInt( EARNMETER_GOALID, earnObject.id )
+}
+
+
+void function DummyRewardEarnedCallback( entity player )
+{
+ Assert( false, "Must set a reward earned callback with SetCallback_EarnMeterRewardEarned() if rewards are in use" )
+}
+
+
+void function DummyGoalEarnedCallback( entity player )
+{
+ Assert( false, "Must set a goal earned callback with SetCallback_EarnMeterGoalEarned() if meter is in use" )
+}
+
+// Hook into the existing core system until it can be replaced.
+void function JFS_PlayerEarnMeter_CoreRewardUpdate( entity titan, float startingCoreValue, float newCoreValue )
+{
+ #if ANTI_RODEO_SMOKE_ENABLED
+ if ( startingCoreValue < CORE_SMOKE_FRAC && newCoreValue >= CORE_SMOKE_FRAC )
+ {
+ GiveOffhandElectricSmoke( titan )
+
+ if ( titan.IsPlayer() )
+ Remote_CallFunction_NonReplay( titan, "ServerCallback_RewardReadyMessage", (Time() - GetPlayerLastRespawnTime( titan )) )
+
+ if ( titan.IsPlayer() )
+ PlayerEarnMeter_SetRewardUsed( titan )
+ }
+ #endif
+}
+
+void function GiveOffhandElectricSmoke( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+ bool hasAntiRodeoKit = IsValid( soul ) && SoulHasPassive( soul, ePassives.PAS_ANTI_RODEO )
+ if ( titan.GetOffhandWeapon( OFFHAND_INVENTORY ) != null )
+ {
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_INVENTORY )
+ if ( hasAntiRodeoKit )
+ weapon.SetWeaponPrimaryAmmoCount( weapon.GetWeaponPrimaryAmmoCount() + 2 )
+ else
+ weapon.SetWeaponPrimaryAmmoCount( weapon.GetWeaponPrimaryAmmoCount() + 1 )
+ }
+ else
+ {
+ titan.GiveOffhandWeapon( CORE_SMOKE_WEAPON, OFFHAND_INVENTORY )
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_INVENTORY )
+ if ( hasAntiRodeoKit )
+ {
+ weapon.SetWeaponPrimaryAmmoCount( weapon.GetWeaponPrimaryAmmoCount() + 1 )
+ }
+ if ( soul.GetTitanSoulNetInt( "upgradeCount" ) >= 2 && SoulHasPassive( soul, ePassives.PAS_VANGUARD_CORE5 ) )
+ {
+ entity weapon = titan.GetOffhandWeapon( OFFHAND_INVENTORY )
+ array<string> mods = weapon.GetMods()
+ mods.append( "maelstrom" )
+ weapon.SetMods( mods )
+ }
+ }
+}
+
+void function PlayerEarnMeter_SetEnabled( bool enabled )
+{
+ file.earnMeterEnabled = enabled
+}
+
+bool function PlayerEarnMeter_Enabled()
+{
+ return file.earnMeterEnabled
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut
new file mode 100644
index 00000000..b41640ad
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut
@@ -0,0 +1,136 @@
+global function Sv_EarnMeterMP_Init
+global function EarnMeterMP_SetTitanLoadout
+global function EarnMeterMP_SetPassiveMeterGainEnabled
+
+struct {
+ float playingStartTime
+ bool passiveMeterGainEnabled = true
+} file
+
+void function Sv_EarnMeterMP_Init()
+{
+ if ( !EARNMETER_ENABLED )
+ return
+
+ AddCallback_OnClientConnected( SetupPlayerEarnMeter )
+ AddCallback_GameStateEnter( eGameState.Playing, OnPlaying ) // can't change boost after prematch
+ AddCallback_OnPlayerRespawned( OnPlayerRespawned )
+}
+
+void function EarnMeterMP_SetTitanLoadout( entity player )
+{
+ if ( EarnMeterMP_IsTitanEarnGametype() )
+ PlayerEarnMeter_SetGoal( player, EarnObject_GetByRef( GetTitanLoadoutForPlayer( player ).titanClass ) )
+}
+
+void function EarnMeterMP_SetPassiveMeterGainEnabled( bool enabled )
+{
+ file.passiveMeterGainEnabled = enabled
+}
+
+void function SetupPlayerEarnMeter( entity player )
+{
+ PlayerEarnMeter_Reset( player )
+
+ // todo: need to do burnmeter stuff here ( e.g. rewards/boosts )
+ if ( EarnMeterMP_IsTitanEarnGametype() )
+ PlayerEarnMeter_SetGoal( player, EarnObject_GetByRef( GetTitanLoadoutForPlayer( player ).titanClass ) )
+
+ PlayerEarnMeter_EnableGoal( player ) // prevents goalstate from being set incorrectly
+
+ // catchup bonus for late joiners
+ // todo: maths on this is fine but for some reason it won't set correctly, could be getting reset somewhere?
+ PlayerEarnMeter_AddOwnedFrac( player, ( ( Time() - file.playingStartTime ) / 4.0 ) * 0.01 )
+}
+
+void function OnPlaying()
+{
+ file.playingStartTime = Time()
+ foreach ( entity player in GetPlayerArray() )
+ SetupPlayerEarnMeter( player )
+
+ // do this in playing so that gamemodes/maps can disable and this'll take affect
+ if ( EarnMeterMP_IsTitanEarnGametype() ) // settitanavailable when earnmeter full
+ {
+ Riff_ForceTitanAvailability( eTitanAvailability.Custom ) // doesn't seem to affect anything aside from preventing some annoying client stuff
+ svGlobal.titanAvailabilityCheck = IsTitanAvailable
+ AddEarnMeterThresholdEarnedCallback( 1.0, void function( entity player ) { SetTitanAvailable( player ) }, true )
+ }
+ else // if no titans from earnmeter in this mode, just reset when we finish meter
+ AddEarnMeterThresholdEarnedCallback( 1.0, PlayerEarnMeter_Reset, true )
+}
+
+void function OnPlayerRespawned( entity player )
+{
+ thread EarnMeterMP_PlayerLifeThink( player )
+}
+
+void function EarnMeterMP_PlayerLifeThink( entity player )
+{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnDestroy" )
+
+ int lastEarnMeterMode = PlayerEarnMeter_GetMode( player )
+ float lastPassiveGainTime = Time()
+
+ while ( true )
+ {
+ int desiredEarnMeterMode
+
+ if ( player.IsTitan() )
+ {
+ entity soul = player.GetTitanSoul()
+ if ( SoulTitanCore_GetExpireTime( soul ) > Time() )
+ desiredEarnMeterMode = eEarnMeterMode.CORE_ACTIVE
+ else
+ desiredEarnMeterMode = eEarnMeterMode.CORE
+ }
+ else if ( IsValid( player.GetPetTitan() ) )
+ desiredEarnMeterMode = eEarnMeterMode.PET
+ else
+ desiredEarnMeterMode = eEarnMeterMode.DEFAULT
+
+ if ( desiredEarnMeterMode != lastEarnMeterMode )
+ {
+ PlayerEarnMeter_SetMode( player, desiredEarnMeterMode )
+
+ if ( desiredEarnMeterMode == eEarnMeterMode.DEFAULT )
+ {
+ if ( !IsTitanAvailable( player ) && PlayerEarnMeter_GetOwnedFrac( player ) == 1.0 ) // this should only be the case after player has dropped their titan
+ PlayerEarnMeter_Reset( player )
+
+ if ( PlayerEarnMeter_GetRewardFrac( player ) != 0 )
+ PlayerEarnMeter_EnableReward( player )
+ }
+ else
+ {
+ PlayerEarnMeter_DisableGoal( player )
+ PlayerEarnMeter_DisableReward( player )
+ }
+
+ lastEarnMeterMode = desiredEarnMeterMode
+ }
+
+ if ( lastEarnMeterMode == eEarnMeterMode.DEFAULT )
+ {
+ if ( PlayerEarnMeter_GetOwnedFrac( player ) < 1.0 )
+ PlayerEarnMeter_DisableGoal( player )
+ else if ( player.GetPlayerNetInt( "goalState" ) != eRewardState.UNAVAILABLE )
+ {
+ // if goal is enabled then the client will show "titan ready" alerts even if it isn't
+ // the problem is that if the goal isn't available when we fill the earnmeter, then it won't make it available
+ // so unfortunately we have to do this manually
+ player.SetPlayerNetInt( "goalState", eRewardState.AVAILABLE )
+ PlayerEarnMeter_RefreshGoal( player )
+ }
+
+ if ( Time() - lastPassiveGainTime > 4.0 && file.passiveMeterGainEnabled ) // this might be 5.0
+ {
+ lastPassiveGainTime = Time()
+ PlayerEarnMeter_AddOwnedFrac( player, 0.01 )
+ }
+ }
+
+ WaitFrame()
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut b/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut
new file mode 100644
index 00000000..ba473cae
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut
@@ -0,0 +1,315 @@
+untyped
+
+global function Evac_Init
+global function Evac_AddLocation
+global function Evac_SetSpacePosition
+global function Evac_SetEnabled
+global function Evac_IsEnabled
+global function IsEvacDropship
+global function EvacMain
+
+const float EVAC_ARRIVAL_TIME = 40.0
+const float EVAC_WAIT_TIME = 18.0
+
+struct {
+ bool enabled = true
+
+ array<Point> evacPoints
+ Point spacePosition
+
+ entity evacDropship
+ array<entity> evacPlayers
+} file
+
+void function Evac_Init()
+{
+ EvacShared_Init()
+
+ AddCallback_GameStateEnter( eGameState.Epilogue, Evac_OnEpilogue )
+}
+
+void function Evac_SetEnabled( bool enabled )
+{
+ file.enabled = enabled
+}
+
+bool function Evac_IsEnabled()
+{
+ return false // shit is busted rn lol
+ //return file.enabled && GetClassicMPMode() && !IsRoundBased()
+}
+
+void function Evac_AddLocation( vector origin, vector angles )
+{
+ Point evacPoint
+ evacPoint.origin = origin
+ evacPoint.angles = angles
+
+ file.evacPoints.append( evacPoint )
+}
+
+void function Evac_SetSpacePosition( vector origin, vector angles )
+{
+ file.spacePosition.origin = origin
+ file.spacePosition.angles = angles
+}
+
+bool function IsEvacDropship( entity ent )
+{
+ return file.evacDropship == ent && IsValid( file.evacDropship )
+}
+
+void function Evac_OnEpilogue()
+{
+ if ( Evac_IsEnabled() )
+ thread EvacMain( GetOtherTeam( GameScore_GetWinningTeam() ) )
+}
+
+void function EvacMain( int winningTeam )
+{
+ if ( file.evacPoints.len() == 0 )
+ {
+ // automatically add evac locations if they aren't registered yet
+ int i = 1
+ entity current = null
+ while ( true )
+ {
+ current = GetEnt( "escape_node" + i )
+ print( current )
+
+ if ( current != null )
+ Evac_AddLocation( current.GetOrigin(), current.GetAngles() )
+ else
+ break
+
+ i++
+ }
+
+ if ( file.evacPoints.len() == 0 )
+ unreachable
+ }
+
+ if ( file.spacePosition.origin == < 0, 0, 0 > )
+ {
+ // automatically add a space node if not registered yet
+ entity defaultSpaceNode = GetEnt( "spaceNode" )
+ if ( defaultSpaceNode == null )
+ unreachable
+
+ Evac_SetSpacePosition( defaultSpaceNode.GetOrigin(), defaultSpaceNode.GetAngles() )
+ }
+
+ Point evacPoint = file.evacPoints[ RandomInt( file.evacPoints.len() ) ]
+
+ // create an entity for the evac point that clients will get
+ entity evacPointEntity = CreateEntity( MARKER_ENT_CLASSNAME )
+ evacPointEntity.SetOrigin( evacPoint.origin )
+ evacPointEntity.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ DispatchSpawn( evacPointEntity )
+ evacPointEntity.DisableHibernation()
+
+ // set objectives
+ //SetTeamActiveObjective( winningTeam, "EG_DropshipExtract", Time() + EVAC_ARRIVAL_TIME, evacPointEntity )
+ //SetTeamActiveObjective( GetOtherTeam( winningTeam ), "EG_StopExtract", Time() + EVAC_ARRIVAL_TIME, evacPointEntity )
+
+ // wanted to do this with an actual dropship to calculate embarkStartDelay but spawning it before it should exist ingame is weird
+ // could probably do it with a dummy entity but effort
+ wait EVAC_ARRIVAL_TIME - 4.33333//embarkStartDelay
+
+ // create dropship
+ entity dropship = CreateDropship( winningTeam, evacPoint.origin, evacPoint.angles )
+ file.evacDropship = dropship
+
+ DispatchSpawn( dropship )
+
+ dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) // gotta do this after dispatch for some reason
+ vector startPos = dropship.Anim_GetStartForRefEntity( "cd_dropship_rescue_side_start", evacPointEntity, "origin" ).origin
+ dropship.SetOrigin( startPos ) // set origin so the dropship isn't in the map
+ dropship.EndSignal( "OnDestroy" )
+
+ // calculate time until idle
+ float sequenceDuration = dropship.GetSequenceDuration( "cd_dropship_rescue_side_start" )
+ float cycleFrac = dropship.GetScriptedAnimEventCycleFrac( "cd_dropship_rescue_side_start", "ReadyToLoad" )
+ float embarkStartDelay = sequenceDuration * cycleFrac
+
+ // play anim
+ thread PlayAnim( dropship, "cd_dropship_rescue_side_start", evacPointEntity )
+ wait embarkStartDelay
+
+ print( "evac flyin done! ready to load players" )
+
+ // set objectives again
+ SetTeamActiveObjective( winningTeam, "EG_DropshipExtract2", Time() + EVAC_WAIT_TIME, evacPointEntity )
+ SetTeamActiveObjective( GetOtherTeam( winningTeam ), "EG_StopExtract2", Time() + EVAC_WAIT_TIME, evacPointEntity )
+
+ thread EvacShipThink( dropship ) // let people enter it
+
+ wait EVAC_WAIT_TIME
+
+ // fly away
+ thread PlayAnim( dropship, "cd_dropship_rescue_side_end", evacPointEntity )
+
+ // set objectives again
+ SetTeamActiveObjective( winningTeam, "EG_DropshipExtractDropshipFlyingAway" )
+ SetTeamActiveObjective( GetOtherTeam( winningTeam ), "EG_StopExtractDropshipFlyingAway" )
+
+ wait dropship.GetSequenceDuration( "cd_dropship_rescue_side_end" ) - WARPINFXTIME
+
+ foreach ( entity player in file.evacPlayers )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_PlayScreenFXWarpJump" )
+ }
+
+ // todo screen effects and shit
+ //WaittillAnimDone( dropship )
+ wait WARPINFXTIME
+
+ // space
+ dropship.SetOrigin( file.spacePosition.origin )
+ dropship.SetAngles( file.spacePosition.angles )
+ thread PlayAnim( dropship, "ds_space_flyby_dropshipA" )
+
+ // display player [Evacuated] in killfeed
+ foreach ( entity player in GetPlayerArray() )
+ {
+ foreach ( entity evacPlayer in file.evacPlayers )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_EvacObit", evacPlayer.GetEncodedEHandle() )
+ }
+
+ foreach ( entity player in file.evacPlayers )
+ {
+ // set skybox to space for all evac players
+ player.SetSkyCamera( GetEnt( "skybox_cam_intro" ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_DisableHudForEvac" )
+ }
+
+ wait 5.0
+
+ foreach ( entity player in GetPlayerArray() )
+ ScreenFadeToBlackForever( player, 2.0 )
+
+ wait 2.0
+
+ // end game lol
+ SetGameState( eGameState.Postmatch )
+}
+
+void function EvacShipThink( entity dropship )
+{
+ dropship.EndSignal( "OnDestroy" )
+
+ // this is the easiest way i could figure out to get a bounding box that's parented to the dropship
+ entity mover1 = CreateScriptMover( dropship.GetOrigin(), dropship.GetAngles() )
+ mover1.SetParent( dropship )
+ mover1.SetLocalOrigin( dropship.GetBoundingMaxs() - < 0, 0, 100> )
+
+ entity mover2 = CreateScriptMover( dropship.GetOrigin(), dropship.GetAngles() )
+ mover2.SetParent( dropship )
+ mover2.SetLocalOrigin( dropship.GetBoundingMins() - < 0, 0, 100 > )
+
+ while ( true )
+ {
+ foreach ( entity player in GetPlayerArrayOfTeam( dropship.GetTeam() ) )
+ {
+ if ( file.evacPlayers.contains( player ) || !IsAlive( player ) )
+ continue
+
+ vector playerPos = player.GetOrigin()
+
+ vector mover1Pos = mover1.GetOrigin()
+ vector mover2Pos = mover2.GetOrigin()
+ vector maxPos
+ maxPos.x = mover1Pos.x > mover2Pos.x ? mover1Pos.x : mover2Pos.x
+ maxPos.y = mover1Pos.y > mover2Pos.y ? mover1Pos.y : mover2Pos.y
+ maxPos.z = mover1Pos.z > mover2Pos.z ? mover1Pos.z : mover2Pos.z
+
+ vector minPos
+ minPos.x = mover1Pos.x < mover2Pos.x ? mover1Pos.x : mover2Pos.x
+ minPos.y = mover1Pos.y < mover2Pos.y ? mover1Pos.y : mover2Pos.y
+ minPos.z = mover1Pos.z < mover2Pos.z ? mover1Pos.z : mover2Pos.z
+
+ print( "\n" )
+ print( player )
+ print( playerPos )
+ print( minPos )
+ print( maxPos )
+
+ if ( playerPos.x > minPos.x && playerPos.y > minPos.y && playerPos.z > minPos.z &&
+ playerPos.x < maxPos.x && playerPos.y < maxPos.y && playerPos.z < maxPos.z )
+ {
+ print( player + " is evacuating!" )
+
+ file.evacPlayers.append( player )
+ player.SetParent( dropship )
+
+ // super duper temp
+ player.SetLocalOrigin( dropship.GetOrigin() - < 0, 10, 80 > )
+ }
+ }
+
+ WaitFrame()
+ }
+}
+
+/*void function TestEvac()
+{
+ if ( file.evacShipSpawns.len() == 0 )
+ Evac_AddLocation( GetEnt( "escape_node1" ).GetOrigin(), GetEnt( "escape_node1" ).GetAngles() )
+
+ Point shipSpawn = file.evacShipSpawns[ RandomInt( file.evacShipSpawns.len() ) ]
+
+ entity dropship = CreateDropship( GetPlayerArray()[0].GetTeam(), shipSpawn.origin, shipSpawn.angles )
+ file.evacDropship = dropship
+ DispatchSpawn( dropship )
+
+ dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" )
+
+ print( dropship.GetSequenceDuration( "cd_dropship_rescue_side_start" ) )
+ print( dropship.GetScriptedAnimEventCycleFrac( "cd_dropship_rescue_side_start", "ReadyToLoad" ) )
+
+ float embarkStart = dropship.GetSequenceDuration( "cd_dropship_rescue_side_start" ) * dropship.GetScriptedAnimEventCycleFrac( "cd_dropship_rescue_side_start", "ReadyToLoad" )
+ print( embarkStart )
+
+ thread PlayAnim( dropship, "cd_dropship_rescue_side_start" )
+ wait embarkStart
+ print( "evac start anim done" )
+ thread TestEvacThink( dropship )
+ SetTeamActiveObjective( GetPlayerArray()[0].GetTeam(), "EG_DropshipExtract2", Time() + 30, dropship )
+
+ thread PlayAnim( dropship, "cd_dropship_rescue_side_idle", GetEnt( "escape_node1" ) )
+}
+
+void function TestEvacThink( entity dropship )
+{
+ dropship.EndSignal( "OnDestroy" )
+
+ // these numbers are probably innacurate but there's no real way of getting accurate ones and these are good enough
+ entity mover = CreateScriptMover( dropship.GetOrigin(), dropship.GetAngles() )
+ mover.SetParent( dropship )
+ mover.SetLocalOrigin( dropship.GetBoundingMaxs() - < 0, 0, 100> )
+
+ entity mover2 = CreateScriptMover( dropship.GetOrigin(), dropship.GetAngles() )
+ mover2.SetParent( dropship )
+ mover2.SetLocalOrigin( dropship.GetBoundingMins() - < 0, 0, 100> )
+
+ while ( true )
+ {
+ foreach ( entity player in GetPlayerArrayOfTeam( dropship.GetTeam() ) )
+ {
+ if ( !IsAlive( player ) )
+ continue
+
+ vector playerOrigin = player.GetOrigin()
+
+ vector dropshipMax = mover.GetOrigin()
+ vector dropshipMin = mover2.GetOrigin()
+
+ // temp, might be permenant but idk if box triggers are a thing in script
+ if ( playerOrigin.x > dropshipMin.x && playerOrigin.y > dropshipMin.y && playerOrigin.z > dropshipMin.z &&
+ playerOrigin.x < dropshipMax.x && playerOrigin.y < dropshipMax.y && playerOrigin.z < dropshipMax.z )
+ player.Die()
+ }
+
+ WaitFrame()
+ }
+}*/ \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/faction_xp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/faction_xp.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/faction_xp.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_frontline.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_frontline.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_frontline.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut
new file mode 100644
index 00000000..cf7f7e15
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut
@@ -0,0 +1,6 @@
+global function AiGameModes_Init
+
+void function AiGameModes_Init()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_capture_point.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_capture_point.gnut
new file mode 100644
index 00000000..e02157d1
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_capture_point.gnut
@@ -0,0 +1 @@
+// not using this, everything is just in _hardpoints instead lol \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_featured_mode_settings.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_featured_mode_settings.gnut
new file mode 100644
index 00000000..090814cb
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_featured_mode_settings.gnut
@@ -0,0 +1,125 @@
+untyped
+global function FeaturedModeSettings_Init
+
+void function FeaturedModeSettings_Init()
+{
+ // if it's not super obvious at a glance this script is used for playlist vars with the prefix "featured_mode_"
+ // these often set loadouts and shit so they need a script
+ // note: for turbo_titans, the core multiplier is set in playlist
+
+ AddCallback_OnPlayerRespawned( FeaturedModeSettingsSetupPilotLoadouts )
+ AddCallback_OnPilotBecomesTitan( FeaturedModeSettingsSetupTitanLoadouts )
+}
+
+bool function IsFeaturedMode( string modeName )
+{
+ return GetCurrentPlaylistVar( "featured_mode_" + modeName ) == "1"
+}
+
+void function FeaturedModeSettingsSetupPilotLoadouts( entity player )
+{
+ bool shouldChangeLoadout = false
+
+ // create loadout struct
+ PilotLoadoutDef modifiedLoadout = clone GetActivePilotLoadout( player )
+
+ if ( IsFeaturedMode( "all_holopilot" ) )
+ {
+ shouldChangeLoadout = true
+
+ modifiedLoadout.special = "mp_ability_holopilot"
+ }
+
+ if ( IsFeaturedMode( "all_grapple" ) )
+ {
+ shouldChangeLoadout = true
+
+ modifiedLoadout.special = "mp_ability_grapple"
+ modifiedLoadout.specialMods = [ "all_grapple" ]
+ }
+
+ if ( IsFeaturedMode( "all_phase" ) )
+ {
+ shouldChangeLoadout = true
+
+ modifiedLoadout.special = "mp_ability_shifter"
+ modifiedLoadout.specialMods = [ "all_phase" ]
+ }
+
+ if ( IsFeaturedMode( "all_ticks" ) )
+ {
+ shouldChangeLoadout = true
+
+ modifiedLoadout.ordnance = "mp_weapon_frag_drone"
+ modifiedLoadout.ordnanceMods = [ "all_ticks" ]
+ }
+
+ if ( IsFeaturedMode( "rocket_arena" ) )
+ {
+ // this crashes sometimes for some reason
+
+ shouldChangeLoadout = true
+
+ modifiedLoadout.primary = "mp_weapon_epg"
+ modifiedLoadout.primaryMods = [ "rocket_arena" ]
+
+ // set secondary to whatever one is pistol
+ if ( GetWeaponInfoFileKeyField_Global( player.GetMainWeapons()[ 1 ].GetWeaponClassName(), "menu_category" ) == "at" )
+ {
+ modifiedLoadout.weapon3 = "mp_weapon_autopistol"
+ modifiedLoadout.weapon3Mods = [ "rocket_arena" ]
+ }
+ else
+ {
+ modifiedLoadout.secondary = "mp_weapon_autopistol"
+ modifiedLoadout.secondaryMods = [ "rocket_arena" ]
+ }
+
+ player.GiveExtraWeaponMod( "rocket_arena" )
+ }
+
+ if ( IsFeaturedMode( "shotguns_snipers" ) )
+ {
+
+ shouldChangeLoadout = true
+
+ // this one was never released, assuming it just gives you a mastiff and a kraber with quick swap
+ modifiedLoadout.primary = "mp_weapon_sniper"
+ modifiedLoadout.primaryMods = [ "pas_fast_swap", "pas_fast_ads" ]
+
+ // set secondary to whatever one is pistol
+ if ( GetWeaponInfoFileKeyField_Global( player.GetMainWeapons()[ 1 ].GetWeaponClassName(), "menu_category" ) == "at" )
+ {
+ modifiedLoadout.weapon3 = "mp_weapon_mastiff"
+ modifiedLoadout.weapon3Mods = [ "pas_fast_swap", "pas_run_and_gun" ]
+ }
+ else
+ {
+ modifiedLoadout.secondary = "mp_weapon_mastiff"
+ modifiedLoadout.secondaryMods = [ "pas_fast_swap", "pas_run_and_gun" ]
+ }
+ }
+
+ // dont wanna give a new loadout if it's not necessary, could break other callbacks
+ if ( shouldChangeLoadout )
+ GivePilotLoadout( player, modifiedLoadout )
+
+ if ( IsFeaturedMode( "tactikill" ) )
+ player.GiveExtraWeaponMod( "tactical_cdr_on_kill" )
+
+ if ( IsFeaturedMode( "amped_tacticals" ) )
+ player.GiveExtraWeaponMod( "amped_tacticals" )
+}
+
+void function FeaturedModeSettingsSetupTitanLoadouts( entity player, entity titan )
+{
+ // this doesn't work atm, figure out how it should work and fix at some point
+ entity soul = player.GetTitanSoul()
+ if ( IsFeaturedMode( "turbo_titans" ) )
+ {
+ if ( GetSoulTitanSubClass( soul ) == "stryder" || GetSoulTitanSubClass( soul ) == "atlas" )
+ GivePassive( player, ePassives.PAS_MOBILITY_DASH_CAPACITY )
+ else
+ GivePassive( player, ePassives.PAS_DASH_RECHARGE )
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_frontline.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_frontline.gnut
new file mode 100644
index 00000000..7ece7dc1
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_frontline.gnut
@@ -0,0 +1,159 @@
+untyped
+
+
+global function GetFrontline
+global function SetFrontline
+global function AddCalculateFrontlineCallback
+
+const DEBUG_FRONTLINE = false
+
+global struct Frontline
+{
+ vector origin = Vector( 0.0, 0.0, 0.0 )
+ vector combatDir = Vector( 0.0, 0.0, 0.0 )
+ vector line = Vector( 0.0, 0.0, 0.0 )
+ vector friendlyCenter = Vector( 0.0, 0.0, 0.0 )
+ vector enemyCenter = Vector( 0.0, 0.0, 0.0 )
+ float lastCalcTime = -1.0
+}
+
+struct
+{
+ Frontline frontline
+ array<void functionref()> calculateFrontlineCallbacks
+} file
+
+Frontline function GetFrontline( team )
+{
+ if ( file.frontline.lastCalcTime < Time() )
+ {
+ CalculateFrontline()
+ file.frontline.lastCalcTime = Time()
+ }
+
+ Frontline fl
+ fl = clone file.frontline
+
+ if ( team == TEAM_MILITIA )
+ {
+ fl.combatDir *= -1.0
+ vector temp = fl.friendlyCenter
+ fl.friendlyCenter = fl.enemyCenter
+ fl.enemyCenter = temp
+ }
+
+ return fl
+}
+
+void function AddCalculateFrontlineCallback( void functionref() callbackFunc )
+{
+ // Check if this function has already been added
+ #if DEV
+ foreach ( func in file.calculateFrontlineCallbacks )
+ {
+ Assert( func != callbackFunc )
+ }
+ #endif
+
+ file.calculateFrontlineCallbacks.append( callbackFunc )
+}
+
+void function CalculateFrontline()
+{
+ #if DEV
+ float debugTime = 0.2
+ #endif
+
+ if ( file.calculateFrontlineCallbacks.len() > 0 )
+ {
+ foreach ( callbackFunc in file.calculateFrontlineCallbacks )
+ {
+ callbackFunc()
+ }
+ }
+ else
+ {
+ vector militiaCenter = CalculateWeightedTeamCenter( TEAM_MILITIA )
+ vector imcCenter = CalculateWeightedTeamCenter( TEAM_IMC )
+
+ file.frontline.friendlyCenter = imcCenter // friendlyCenter is for TEAM_IMC by default
+ file.frontline.enemyCenter = militiaCenter
+
+ file.frontline.origin = ( militiaCenter + imcCenter ) * 0.5
+ file.frontline.combatDir = Normalize( militiaCenter - imcCenter ) // combatDir is for TEAM_IMC by default
+ file.frontline.line = CrossProduct( file.frontline.combatDir, Vector( 0.0, 0.0, 1.0 ) )
+
+ #if DEV
+ if ( DEBUG_FRONTLINE )
+ {
+ DrawBox( militiaCenter, Vector( -8.0, -8.0, -8.0 ), Vector( 8.0, 8.0, 8.0 ), 255, 102, 0, true, debugTime )
+ DrawBox( imcCenter, Vector( -8.0, -8.0, -8.0 ), Vector( 8.0, 8.0, 8.0 ), 0, 0, 255, true, debugTime )
+ DebugDrawLine( militiaCenter, imcCenter, 0, 255, 0, true, debugTime )
+ }
+ #endif
+ }
+
+ #if DEV
+ if ( DEBUG_FRONTLINE )
+ {
+ DrawBox( file.frontline.origin, Vector( -32.0, -32.0, -32.0 ), Vector( 32.0, 32.0, 32.0 ), 255, 0, 0, true, debugTime )
+ DebugDrawLine( file.frontline.origin - file.frontline.line * 500.0, file.frontline.origin + file.frontline.line * 500.0, 255, 0, 0, true, debugTime )
+ }
+ #endif
+}
+
+void function SetFrontline( vector origin, vector combatDir )
+{
+ file.frontline.origin = origin
+ file.frontline.combatDir = combatDir
+ file.frontline.line = CrossProduct( file.frontline.combatDir, Vector( 0.0, 0.0, 1.0 ) )
+}
+
+vector function CalculateWeightedTeamCenter( int team )
+{
+ array<entity> teamPlayers = GetPlayerArrayOfTeam_Alive( team )
+ int teamPlayersCount = teamPlayers.len()
+
+ if ( teamPlayersCount == 0 )
+ return Vector( 0.0, 0.0, 0.0 )
+
+ // find minimum distances between teammates
+ array<float> minTeammateDistances// = arrayofsize( teamPlayersCount, 99999.0 )
+ minTeammateDistances.resize( teamPlayersCount, 99999.0 )
+
+ for ( int i = 0; i < teamPlayersCount; i++ )
+ {
+ entity playerI = teamPlayers[ i ]
+
+ for ( int j = i + 1; j < teamPlayersCount; j++ )
+ {
+ entity playerJ = teamPlayers[ j ]
+ float distanceBetweenPlayers = Distance( playerI.GetOrigin(), playerJ.GetOrigin() )
+
+ if ( distanceBetweenPlayers < minTeammateDistances[ i ] )
+ minTeammateDistances[ i ] = distanceBetweenPlayers
+
+ if ( distanceBetweenPlayers < minTeammateDistances[ j ] )
+ minTeammateDistances[ j ] = distanceBetweenPlayers
+ }
+ }
+
+ vector weightedOrgSum = Vector( 0.0, 0.0, 0.0 )
+ float weightSum = 0.0
+ float weight = 0.0
+ float halfPi = 1.57 // passing a fraction of this value into sin which gives us the first part of a sin wave from 0 - 1
+ float maxPossibleDistance = MAX_WORLD_RANGE
+ float magicNumber = 14.0 // magic number gives the desired falloff
+
+ // calculate a weighted origin based on how close players are to teammates
+ foreach ( index, player in teamPlayers )
+ {
+ float radians = halfPi * ( minTeammateDistances[ index ] / maxPossibleDistance ) // radians will be a value between 0 - halfPi
+ weight = pow( ( 1.0 - sin( radians ) ), magicNumber ) // pow squashes the result so the curve has the falloff that's desired
+
+ weightedOrgSum += player.GetOrigin() * weight
+ weightSum += weight
+ }
+
+ return weightedOrgSum / weightSum
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut
new file mode 100644
index 00000000..a30944cf
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut
@@ -0,0 +1,12 @@
+global function GamemodeAITdm_Init
+global function RateSpawnpoints_Frontline
+
+void function GamemodeAITdm_Init()
+{
+
+}
+
+void function RateSpawnpoints_Frontline(int _0, array<entity> _1, int _2, entity _3)
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut
new file mode 100644
index 00000000..b75ed51b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut
@@ -0,0 +1,18 @@
+global function GamemodeAt_Init
+global function RateSpawnpoints_AT
+global function RateSpawnpoints_SpawnZones
+
+void function GamemodeAt_Init()
+{
+
+}
+
+void function RateSpawnpoints_AT( int checkclass, array<entity> spawnpoints, int team, entity player )
+{
+ RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) // temp
+}
+
+void function RateSpawnpoints_SpawnZones( int checkclass, array<entity> spawnpoints, int team, entity player )
+{
+ RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) // temp
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_coliseum.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_coliseum.nut
new file mode 100644
index 00000000..b1de4d4f
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_coliseum.nut
@@ -0,0 +1,98 @@
+untyped
+
+global function GamemodeColiseum_Init
+global function GamemodeColiseum_CustomIntro
+
+bool hasShownIntroScreen = false
+
+void function GamemodeColiseum_Init()
+{
+ // gamemode settings
+ SetRoundBased( true )
+ SetRespawnsEnabled( false )
+ SetShouldUseRoundWinningKillReplay( true )
+ Riff_ForceTitanAvailability( eTitanAvailability.Never )
+ Riff_ForceBoostAvailability( eBoostAvailability.Disabled )
+ Riff_ForceSetEliminationMode( eEliminationMode.Pilots )
+ SetLoadoutGracePeriodEnabled( false ) // prevent modifying loadouts with grace period
+ SetWeaponDropsEnabled( false )
+
+ ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, ClassicMP_DefaultNoIntro_GetLength() )
+ AddCallback_GameStateEnter( eGameState.Prematch, ShowColiseumIntroScreen )
+ AddCallback_OnPlayerRespawned( GivePlayerColiseumLoadout )
+}
+
+// stub function referenced in sh_gamemodes_mp
+void function GamemodeColiseum_CustomIntro( entity player )
+{}
+
+void function ShowColiseumIntroScreen()
+{
+ if ( !hasShownIntroScreen )
+ thread ShowColiseumIntroScreenThreaded()
+
+ hasShownIntroScreen = true
+}
+
+void function ShowColiseumIntroScreenThreaded()
+{
+ wait 5
+
+ foreach ( entity player in GetPlayerArray() )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_ColiseumIntro", 1, 1, 1 ) // stub numbers atm because lazy
+}
+
+void function GivePlayerColiseumLoadout( entity player )
+{
+ if ( GetCurrentPlaylistVarInt( "coliseum_loadouts_enabled", 1 ) == 0 )
+ return
+
+ // create loadout struct
+ PilotLoadoutDef coliseumLoadout = clone GetActivePilotLoadout( player )
+
+ /* from playlists.txt
+ coliseum_primary "mp_weapon_lstar"
+ coliseum_primary_attachment ""
+ coliseum_primary_mod1 ""
+ coliseum_primary_mod2 ""
+ coliseum_primary_mod3 ""
+ coliseum_secondary "mp_weapon_softball"
+ coliseum_secondary_mod1 ""
+ coliseum_secondary_mod2 ""
+ coliseum_secondary_mod3 ""
+ coliseum_weapon3 ""
+ coliseum_weapon3_mod1 ""
+ coliseum_weapon3_mod2 ""
+ coliseum_weapon3_mod3 ""
+ coliseum_melee "melee_pilot_emptyhanded"
+ coliseum_special "mp_ability_heal"
+ coliseum_ordnance "mp_weapon_frag_drone"
+ coliseum_passive1 "pas_fast_health_regen"
+ coliseum_passive2 "pas_wallhang"*/
+
+ coliseumLoadout.primary = GetColiseumItem( "primary" )
+ coliseumLoadout.primaryMods = [ GetColiseumItem( "primary_attachment" ), GetColiseumItem( "primary_mod1" ), GetColiseumItem( "primary_mod2" ), GetColiseumItem( "primary_mod3" ) ]
+
+ coliseumLoadout.secondary = GetColiseumItem( "secondary" )
+ coliseumLoadout.secondaryMods = [ GetColiseumItem( "secondary_mod1" ), GetColiseumItem( "secondary_mod2" ), GetColiseumItem( "secondary_mod3" ) ]
+
+ coliseumLoadout.weapon3 = GetColiseumItem( "weapon3" )
+ coliseumLoadout.weapon3Mods = [ GetColiseumItem( "weapon3_mod1" ), GetColiseumItem( "weapon3_mod2" ), GetColiseumItem( "weapon3_mod3" ) ]
+
+ coliseumLoadout.melee = GetColiseumItem( "melee" )
+ coliseumLoadout.special = GetColiseumItem( "special" )
+ coliseumLoadout.ordnance = GetColiseumItem( "ordnance" )
+ coliseumLoadout.passive1 = GetColiseumItem( "passive1" )
+ coliseumLoadout.passive2 = GetColiseumItem( "passive2" )
+
+ coliseumLoadout.setFile = GetSuitAndGenderBasedSetFile( "coliseum", coliseumLoadout.race == RACE_HUMAN_FEMALE ? "female" : "male" )
+
+ GivePilotLoadout( player, coliseumLoadout )
+}
+
+string function GetColiseumItem( string name )
+{
+ return expect string ( GetCurrentPlaylistVar( "coliseum_" + name ) )
+}
+
+// todo this needs the outro: unsure what anims it uses \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut
new file mode 100644
index 00000000..ddfe6ee6
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut
@@ -0,0 +1,282 @@
+untyped
+
+global function GamemodeCP_Init
+global function RateSpawnpoints_CP
+
+// needed for sh_gamemode_cp_dialogue
+global array<entity> HARDPOINTS
+
+struct HardpointStruct
+{
+ entity hardpoint
+ entity trigger
+ entity prop
+
+ array<entity> imcCappers
+ array<entity> militiaCappers
+}
+
+struct {
+ bool ampingEnabled = true
+
+ array<HardpointStruct> hardpoints
+} file
+
+void function GamemodeCP_Init()
+{
+ file.ampingEnabled = GetCurrentPlaylistVar( "amped_capture_points" ) == "1"
+
+ RegisterSignal( "HardpointCaptureStart" )
+
+ AddCallback_EntitiesDidLoad( SpawnHardpoints )
+ AddCallback_GameStateEnter( eGameState.Playing, StartHardpointThink )
+}
+
+void function RateSpawnpoints_CP( int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+
+}
+
+void function SpawnHardpoints()
+{
+ foreach ( entity spawnpoint in GetEntArrayByClass_Expensive( "info_hardpoint" ) )
+ {
+ if ( GameModeRemove( spawnpoint ) )
+ continue
+
+ // spawnpoints are CHardPoint entities
+ // init the hardpoint ent
+ int hardpointID = 0
+ if ( spawnpoint.kv.hardpointGroup == "B" )
+ hardpointID = 1
+ else if ( spawnpoint.kv.hardpointGroup == "C" )
+ hardpointID = 2
+
+ spawnpoint.SetHardpointID( hardpointID )
+
+ HardpointStruct hardpointStruct
+ hardpointStruct.hardpoint = spawnpoint
+ hardpointStruct.prop = CreatePropDynamic( spawnpoint.GetModelName(), spawnpoint.GetOrigin(), spawnpoint.GetAngles(), 6 )
+
+ entity trigger = GetEnt( expect string( spawnpoint.kv.triggerTarget ) )
+ hardpointStruct.trigger = trigger
+
+ file.hardpoints.append( hardpointStruct )
+ HARDPOINTS.append( spawnpoint ) // for vo script
+ spawnpoint.s.trigger <- trigger // also for vo script
+
+ SetGlobalNetEnt( "objective" + spawnpoint.kv.hardpointGroup + "Ent", spawnpoint )
+
+ // set up trigger functions
+ trigger.SetEnterCallback( OnHardpointEntered )
+ trigger.SetLeaveCallback( OnHardpointLeft )
+ }
+}
+
+// functions for handling hardpoint netvars
+void function SetHardpointState( HardpointStruct hardpoint, int state )
+{
+ SetGlobalNetInt( "objective" + hardpoint.hardpoint.kv.hardpointGroup + "State", state )
+ hardpoint.hardpoint.SetHardpointState( state )
+}
+
+int function GetHardpointState( HardpointStruct hardpoint )
+{
+ return GetGlobalNetInt( "objective" + hardpoint.hardpoint.kv.hardpointGroup + "State" )
+}
+
+void function SetHardpointCappingTeam( HardpointStruct hardpoint, int team )
+{
+ SetGlobalNetInt( "objective" + hardpoint.hardpoint.kv.hardpointGroup + "CappingTeam", team )
+}
+
+int function GetHardpointCappingTeam( HardpointStruct hardpoint )
+{
+ return GetGlobalNetInt( "objective" + hardpoint.hardpoint.kv.hardpointGroup + "CappingTeam" )
+}
+
+void function SetHardpointCaptureProgress( HardpointStruct hardpoint, float progress )
+{
+ SetGlobalNetFloat( "objective" + hardpoint.hardpoint.kv.hardpointGroup + "Progress", progress )
+}
+
+float function GetHardpointCaptureProgress( HardpointStruct hardpoint )
+{
+ return GetGlobalNetFloat( "objective" + hardpoint.hardpoint.kv.hardpointGroup + "Progress" )
+}
+
+
+void function StartHardpointThink()
+{
+ thread TrackChevronStates()
+
+ foreach ( HardpointStruct hardpoint in file.hardpoints )
+ thread HardpointThink( hardpoint )
+}
+
+void function HardpointThink( HardpointStruct hardpoint )
+{
+ entity hardpointEnt = hardpoint.hardpoint
+
+ float lastTime = Time()
+ float lastScoreTime = Time()
+
+ WaitFrame() // wait a frame so deltaTime is never zero
+ while ( GamePlayingOrSuddenDeath() )
+ {
+ int imcCappers = hardpoint.imcCappers.len()
+ int militiaCappers = hardpoint.militiaCappers.len()
+
+ float deltaTime = Time() - lastTime
+
+ int cappingTeam
+ if ( imcCappers > militiaCappers )
+ cappingTeam = TEAM_IMC
+ else if ( militiaCappers > imcCappers )
+ cappingTeam = TEAM_MILITIA
+
+ if ( cappingTeam != TEAM_UNASSIGNED )
+ {
+ // hardpoint is owned by controlling team
+ if ( hardpointEnt.GetTeam() == cappingTeam )
+ {
+ // hardpoint is being neutralised, reverse the neutralisation
+ if ( GetHardpointCappingTeam( hardpoint ) != cappingTeam || GetHardpointCaptureProgress( hardpoint ) < 1.0 )
+ {
+ SetHardpointCappingTeam( hardpoint, cappingTeam )
+ SetHardpointCaptureProgress( hardpoint, min( 1.0, GetHardpointCaptureProgress( hardpoint ) + ( deltaTime / CAPTURE_DURATION_CAPTURE ) ) )
+ }
+ // hardpoint is fully captured, start amping if amping is enabled
+ else if ( file.ampingEnabled && GetHardpointState( hardpoint ) < CAPTURE_POINT_STATE_AMPING )
+ SetHardpointState( hardpoint, CAPTURE_POINT_STATE_AMPING )
+
+ // amp the hardpoint
+ if ( GetHardpointState( hardpoint ) == CAPTURE_POINT_STATE_AMPING )
+ {
+ SetHardpointCaptureProgress( hardpoint, min( 2.0, GetHardpointCaptureProgress( hardpoint ) + ( deltaTime / HARDPOINT_AMPED_DELAY ) ) )
+ if ( GetHardpointCaptureProgress( hardpoint ) == 2.0 )
+ {
+ SetHardpointState( hardpoint, CAPTURE_POINT_STATE_AMPED )
+
+ // can't use the dialogue functions here because for some reason GamemodeCP_VO_Amped isn't global?
+ PlayFactionDialogueToTeam( "amphp_youAmped" + hardpointEnt.kv.hardpointGroup, cappingTeam )
+ PlayFactionDialogueToTeam( "amphp_enemyAmped" + hardpointEnt.kv.hardpointGroup, GetOtherTeam( cappingTeam ) )
+ }
+ }
+ }
+ else // we don't own this hardpoint, cap it
+ {
+ SetHardpointCappingTeam( hardpoint, cappingTeam )
+ GamemodeCP_VO_StartCapping( hardpointEnt ) // this doesn't consistently trigger for some reason
+
+ SetHardpointCaptureProgress( hardpoint, min( 1.0, GetHardpointCaptureProgress( hardpoint ) + ( deltaTime / CAPTURE_DURATION_CAPTURE ) ) )
+
+ if ( GetHardpointCaptureProgress( hardpoint ) >= 1.0 )
+ {
+ SetTeam( hardpointEnt, cappingTeam )
+ SetTeam( hardpoint.prop, cappingTeam )
+ SetHardpointState( hardpoint, CAPTURE_POINT_STATE_CAPTURED )
+
+ EmitSoundOnEntityToTeamExceptPlayer( hardpointEnt, "hardpoint_console_captured", cappingTeam, null )
+ GamemodeCP_VO_Captured( hardpointEnt )
+ }
+ }
+ }
+ // capture halting
+ else if ( imcCappers > 0 && imcCappers == militiaCappers )
+ SetHardpointState( hardpoint, CAPTURE_POINT_STATE_HALTED )
+ // amped decay
+ else if ( imcCappers == 0 && militiaCappers == 0 && GetHardpointState( hardpoint ) >= CAPTURE_POINT_STATE_AMPING )
+ {
+ // it seems like network vars won't change if they're too similar? often we get situations here where it's tryna change from 1.00098 to 1 which doesn't work
+ // so we need to check the "real" progress manually
+ // have only gotten this issue here so far, but in theory i think this could be an issue in a good few places, worth looking out for
+ // tho, idk might not be, we don't work with numbers at this small of a scale too often
+ float realProgress = max( 1.0, GetHardpointCaptureProgress( hardpoint ) - ( deltaTime / HARDPOINT_AMPED_DELAY ) )
+ SetHardpointCaptureProgress( hardpoint, realProgress )
+
+ if ( realProgress == 1 )
+ SetHardpointState( hardpoint, CAPTURE_POINT_STATE_CAPTURED )
+ // dont use unamping atm
+ //else
+ // SetHardpointState( hardpoint, CAPTURE_POINT_STATE_SELF_UNAMPING )
+ }
+
+ // scoring
+ if ( hardpointEnt.GetTeam() != TEAM_UNASSIGNED && GetHardpointState( hardpoint ) >= CAPTURE_POINT_STATE_CAPTURED && Time() - lastScoreTime >= TEAM_OWNED_SCORE_FREQ )
+ {
+ lastScoreTime = Time()
+
+ // 2x score if amped
+ if ( GetHardpointState( hardpoint ) == CAPTURE_POINT_STATE_AMPED )
+ AddTeamScore( hardpointEnt.GetTeam(), 2 )
+ else
+ AddTeamScore( hardpointEnt.GetTeam(), 1 )
+ }
+
+ lastTime = Time()
+ WaitFrame()
+ }
+}
+
+// doing this in HardpointThink is effort since it's for individual hardpoints
+// so we do it here instead
+void function TrackChevronStates()
+{
+ // you get 1 amped arrow for chevron / 4, 1 unamped arrow for every 1 the amped chevrons
+
+ while ( true )
+ {
+ int imcChevron
+ int militiaChevron
+
+ foreach ( HardpointStruct hardpoint in file.hardpoints )
+ {
+ if ( hardpoint.hardpoint.GetTeam() == TEAM_IMC )
+ {
+ if ( hardpoint.hardpoint.GetHardpointState() == CAPTURE_POINT_STATE_AMPED )
+ imcChevron += 4
+ else if ( hardpoint.hardpoint.GetHardpointState() >= CAPTURE_POINT_STATE_CAPTURED )
+ imcChevron++
+ }
+ else if ( hardpoint.hardpoint.GetTeam() == TEAM_MILITIA )
+ {
+ if ( hardpoint.hardpoint.GetHardpointState() == CAPTURE_POINT_STATE_AMPED )
+ militiaChevron += 4
+ else if ( hardpoint.hardpoint.GetHardpointState() >= CAPTURE_POINT_STATE_CAPTURED )
+ militiaChevron++
+ }
+ }
+
+ SetGlobalNetInt( "imcChevronState", imcChevron )
+ SetGlobalNetInt( "milChevronState", militiaChevron )
+
+ WaitFrame()
+ }
+}
+
+void function OnHardpointEntered( entity trigger, entity player )
+{
+ HardpointStruct hardpoint
+ foreach ( HardpointStruct hardpointStruct in file.hardpoints )
+ if ( hardpointStruct.trigger == trigger )
+ hardpoint = hardpointStruct
+
+ if ( player.GetTeam() == TEAM_IMC )
+ hardpoint.imcCappers.append( player )
+ else
+ hardpoint.militiaCappers.append( player )
+}
+
+void function OnHardpointLeft( entity trigger, entity player )
+{
+ HardpointStruct hardpoint
+ foreach ( HardpointStruct hardpointStruct in file.hardpoints )
+ if ( hardpointStruct.trigger == trigger )
+ hardpoint = hardpointStruct
+
+ if ( player.GetTeam() == TEAM_IMC )
+ hardpoint.imcCappers.remove( hardpoint.imcCappers.find( player ) )
+ else
+ hardpoint.militiaCappers.remove( hardpoint.militiaCappers.find( player ) )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut
new file mode 100644
index 00000000..704f55d3
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut
@@ -0,0 +1,518 @@
+untyped
+// this needs a refactor lol
+
+global function CaptureTheFlag_Init
+global function RateSpawnpoints_CTF
+
+const array<string> SWAP_FLAG_MAPS = [
+ "mp_forwardbase_kodai",
+ "mp_lf_meadow"
+]
+
+struct {
+ entity imcFlagSpawn
+ entity imcFlag
+ entity imcFlagReturnTrigger
+
+ entity militiaFlagSpawn
+ entity militiaFlag
+ entity militiaFlagReturnTrigger
+
+ array<entity> imcCaptureAssistList
+ array<entity> militiaCaptureAssistList
+} file
+
+void function CaptureTheFlag_Init()
+{
+ PrecacheModel( CTF_FLAG_MODEL )
+ PrecacheModel( CTF_FLAG_BASE_MODEL )
+
+ CaptureTheFlagShared_Init()
+ SetSwitchSidesBased( true )
+ SetSuddenDeathBased( true )
+ SetShouldUseRoundWinningKillReplay( true )
+ SetRoundWinningKillReplayKillClasses( false, false ) // make these fully manual
+
+ AddCallback_OnClientConnected( CTFInitPlayer )
+
+ AddCallback_GameStateEnter( eGameState.Prematch, CreateFlags )
+ AddCallback_OnTouchHealthKit( "item_flag", OnFlagCollected )
+ AddCallback_OnPlayerKilled( OnPlayerKilled )
+ AddCallback_OnPilotBecomesTitan( DropFlagForBecomingTitan )
+
+ RegisterSignal( "FlagReturnEnded" )
+ RegisterSignal( "ResetDropTimeout" )
+
+ // setup stuff for the functions in sh_gamemode_ctf
+ // don't really like using level for stuff but just how it be
+ level.teamFlags <- {}
+
+ // setup score event earnmeter values
+ ScoreEvent_SetEarnMeterValues( "KillPilot", 0.05, 0.20 )
+ ScoreEvent_SetEarnMeterValues( "Headshot", 0.0, 0.02 )
+ ScoreEvent_SetEarnMeterValues( "FirstStrike", 0.0, 0.05 )
+ ScoreEvent_SetEarnMeterValues( "KillTitan", 0.0, 0.25 )
+ ScoreEvent_SetEarnMeterValues( "PilotBatteryStolen", 0.0, 0.35 )
+
+ ScoreEvent_SetEarnMeterValues( "FlagCarrierKill", 0.0, 0.20 )
+ ScoreEvent_SetEarnMeterValues( "FlagTaken", 0.0, 0.10 )
+ ScoreEvent_SetEarnMeterValues( "FlagCapture", 0.0, 0.30 )
+ ScoreEvent_SetEarnMeterValues( "FlagCaptureAssist", 0.0, 0.20 )
+ ScoreEvent_SetEarnMeterValues( "FlagReturn", 0.0, 0.20 )
+}
+
+void function RateSpawnpoints_CTF( int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ // ok this is the 3rd time rewriting this due to not understanding ctf spawns properly
+ // legit just
+ // if there are no enemies in base, spawn them in base
+ // if there are, spawn them outside of it ( but ideally still close )
+ // max distance away should be like, angel city markets
+
+ int spawnTeam = team
+ if ( HasSwitchedSides() )
+ spawnTeam = GetOtherTeam( team )
+
+ array<entity> startSpawns = SpawnPoints_GetPilotStart( spawnTeam )
+ array<entity> enemyPlayers = GetPlayerArrayOfTeam_Alive( GetOtherTeam( spawnTeam ) )
+
+ vector startSpawnAverage
+ bool enemyInBase = false
+ foreach ( entity startSpawn in startSpawns )
+ {
+ startSpawnAverage += startSpawn.GetOrigin()
+
+ foreach ( entity enemy in enemyPlayers )
+ {
+ if ( Distance( startSpawn.GetOrigin(), enemy.GetOrigin() ) <= 1000.0 )
+ {
+ enemyInBase = true
+ break
+ }
+ }
+ }
+
+ startSpawnAverage /= startSpawns.len()
+
+ print( "spawn for " + player + " is there an enemy in base?" + enemyInBase )
+
+ foreach ( entity spawn in spawnpoints )
+ {
+ float rating = 0.0
+
+ bool isStart = false
+ foreach ( entity startSpawn in startSpawns )
+ {
+ if ( Distance2D( spawn.GetOrigin(), startSpawn.GetOrigin() ) < 1500.0 ) // this was for some reason the only distance i could get to work
+ {
+ isStart = true
+ break
+ }
+ }
+
+ if ( isStart )
+ {
+ if ( !enemyInBase )
+ rating = 1000 + RandomFloat( 100.0 )
+ else
+ rating = -1000.0
+ }
+ else if ( !isStart && enemyInBase )
+ {
+ entity friendlyFlag
+ entity enemyFlag
+ if ( team == TEAM_IMC )
+ {
+ friendlyFlag = file.imcFlagSpawn
+ enemyFlag = file.militiaFlagSpawn
+ }
+ else
+ {
+ friendlyFlag = file.militiaFlagSpawn
+ enemyFlag = file.imcFlagSpawn
+ }
+
+ float dist = Distance2D( spawn.GetOrigin(), enemyFlag.GetOrigin() )
+ float flagDist = Distance2D( startSpawnAverage, enemyFlag.GetOrigin() )
+
+ if ( dist < ( flagDist / 2 ) ) // spawns shouldn't be closer to enemies than they are to us
+ rating = -1000.0
+ if ( dist > flagDist * 1.1 ) // spawn is behind startspawns
+ rating = -1000.0
+ else
+ {
+ rating = dist // closer spawns are better
+
+ foreach( entity enemy in enemyPlayers ) // reduce rating if enemies are near by
+ if ( Distance( enemy.GetOrigin(), spawn.GetOrigin() ) < 500.0 )
+ rating /= 2
+ }
+ }
+
+ spawn.CalculateRating( checkClass, team, rating, rating )
+ }
+}
+
+void function CTFInitPlayer( entity player )
+{
+ if ( !IsValid( file.imcFlagSpawn ) )
+ return
+
+ vector imcSpawn = file.imcFlagSpawn.GetOrigin()
+ Remote_CallFunction_NonReplay( player, "ServerCallback_SetFlagHomeOrigin", TEAM_IMC, imcSpawn.x, imcSpawn.y, imcSpawn.z )
+
+ vector militiaSpawn = file.militiaFlagSpawn.GetOrigin()
+ Remote_CallFunction_NonReplay( player, "ServerCallback_SetFlagHomeOrigin", TEAM_MILITIA, militiaSpawn.x, militiaSpawn.y, militiaSpawn.z )
+}
+
+void function OnPlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ if ( GetFlagForTeam( GetOtherTeam( victim.GetTeam() ) ).GetParent() == victim )
+ {
+ if ( victim != attacker && attacker.IsPlayer() )
+ AddPlayerScore( attacker, "FlagCarrierKill", victim )
+
+ DropFlag( victim )
+ }
+}
+
+void function CreateFlags()
+{
+ if ( IsValid( file.imcFlagSpawn ) )
+ {
+ file.imcFlagSpawn.Destroy()
+ file.imcFlag.Destroy()
+ file.imcFlagReturnTrigger.Destroy()
+
+ file.militiaFlagSpawn.Destroy()
+ file.militiaFlag.Destroy()
+ file.militiaFlagReturnTrigger.Destroy()
+ }
+
+ foreach ( entity spawn in GetEntArrayByClass_Expensive( "info_spawnpoint_flag" ) )
+ {
+ // on some maps flags are on the opposite side from what they should be
+ // likely this is because respawn uses distance checks from spawns to check this in official
+ // but i don't like doing that so just using a list of maps to swap them on lol
+ bool switchedSides = HasSwitchedSides() == 1
+ bool shouldSwap = SWAP_FLAG_MAPS.contains( GetMapName() ) ? !switchedSides : switchedSides
+
+ int flagTeam = spawn.GetTeam()
+ if ( shouldSwap )
+ {
+ flagTeam = GetOtherTeam( flagTeam )
+ SetTeam( spawn, flagTeam )
+ }
+
+ // create flag base
+ entity base = CreatePropDynamic( CTF_FLAG_BASE_MODEL, spawn.GetOrigin(), spawn.GetAngles(), 0 )
+ SetTeam( base, spawn.GetTeam() )
+ svGlobal.flagSpawnPoints[ flagTeam ] = base
+
+ // create flag
+ entity flag = CreateEntity( "item_flag" )
+ flag.SetValueForModelKey( CTF_FLAG_MODEL )
+ SetTeam( flag, flagTeam )
+ flag.MarkAsNonMovingAttachment()
+ DispatchSpawn( flag )
+ flag.SetModel( CTF_FLAG_MODEL )
+ flag.SetOrigin( spawn.GetOrigin() + < 0, 0, base.GetBoundingMaxs().z * 2 > ) // ensure flag doesn't spawn clipped into geometry
+ flag.SetVelocity( < 0, 0, 1 > )
+
+ flag.s.canTake <- true
+ flag.s.playersReturning <- []
+
+ level.teamFlags[ flag.GetTeam() ] <- flag
+
+ entity returnTrigger = CreateEntity( "trigger_cylinder" )
+ SetTeam( returnTrigger, flagTeam )
+ returnTrigger.SetRadius( CTF_GetFlagReturnRadius() )
+ returnTrigger.SetAboveHeight( CTF_GetFlagReturnRadius() )
+ returnTrigger.SetBelowHeight( CTF_GetFlagReturnRadius() )
+
+ returnTrigger.SetEnterCallback( OnPlayerEntersFlagReturnTrigger )
+ returnTrigger.SetLeaveCallback( OnPlayerExitsFlagReturnTrigger )
+
+ DispatchSpawn( returnTrigger )
+
+ thread TrackFlagReturnTrigger( flag, returnTrigger )
+
+ if ( flagTeam == TEAM_IMC )
+ {
+ file.imcFlagSpawn = base
+ file.imcFlag = flag
+ file.imcFlagReturnTrigger = returnTrigger
+
+ SetGlobalNetEnt( "imcFlag", file.imcFlag )
+ SetGlobalNetEnt( "imcFlagHome", file.imcFlagSpawn )
+ }
+ else
+ {
+ file.militiaFlagSpawn = base
+ file.militiaFlag = flag
+ file.militiaFlagReturnTrigger = returnTrigger
+
+ SetGlobalNetEnt( "milFlag", file.militiaFlag )
+ SetGlobalNetEnt( "milFlagHome", file.militiaFlagSpawn )
+ }
+ }
+
+ foreach ( entity player in GetPlayerArray() )
+ CTFInitPlayer( player )
+}
+
+void function TrackFlagReturnTrigger( entity flag, entity returnTrigger )
+{
+ // this is a bit of a hack, it seems parenting the return trigger to the flag actually sets the pickup radius of the flag to be the same as the trigger
+ // this isn't wanted since only pickups should use that additional radius
+ flag.EndSignal( "OnDestroy" )
+
+ while ( true )
+ {
+ returnTrigger.SetOrigin( flag.GetOrigin() )
+ WaitFrame()
+ }
+}
+
+void function SetFlagStateForTeam( int team, int state )
+{
+ if ( state == eFlagState.Away ) // we tell the client the flag is the player carrying it if they're carrying it
+ SetGlobalNetEnt( team == TEAM_IMC ? "imcFlag" : "milFlag", ( team == TEAM_IMC ? file.imcFlag : file.militiaFlag ).GetParent() )
+ else
+ SetGlobalNetEnt( team == TEAM_IMC ? "imcFlag" : "milFlag", team == TEAM_IMC ? file.imcFlag : file.militiaFlag )
+
+ SetGlobalNetInt( team == TEAM_IMC ? "imcFlagState" : "milFlagState", state )
+}
+
+bool function OnFlagCollected( entity player, entity flag )
+{
+ if ( !IsAlive( player ) || flag.GetParent() != null || player.IsTitan() || player.IsPhaseShifted() )
+ return false
+
+ if ( player.GetTeam() != flag.GetTeam() && flag.s.canTake )
+ GiveFlag( player, flag ) // pickup enemy flag
+ else if ( player.GetTeam() == flag.GetTeam() && IsFlagHome( flag ) && PlayerHasEnemyFlag( player ) )
+ CaptureFlag( player, GetFlagForTeam( GetOtherTeam( flag.GetTeam() ) ) ) // cap the flag
+
+ return false // don't wanna delete the flag entity
+}
+
+void function GiveFlag( entity player, entity flag )
+{
+ print( player + " picked up the flag!" )
+ flag.Signal( "ResetDropTimeout" )
+
+ flag.SetParent( player, "FLAG" )
+ thread DropFlagIfPhased( player, flag )
+
+ // do notifications
+ MessageToPlayer( player, eEventNotifications.YouHaveTheEnemyFlag )
+ EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_GrabFlag" )
+ AddPlayerScore( player, "FlagTaken", player )
+ PlayFactionDialogueToPlayer( "ctf_flagPickupYou", player )
+
+ MessageToTeam( player.GetTeam(), eEventNotifications.PlayerHasEnemyFlag, player, player )
+ EmitSoundOnEntityToTeamExceptPlayer( flag, "UI_CTF_3P_TeamGrabFlag", player.GetTeam(), player )
+ PlayFactionDialogueToTeamExceptPlayer( "ctf_flagPickupFriendly", player.GetTeam(), player )
+
+ MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerHasFriendlyFlag, player, player )
+ EmitSoundOnEntityToTeam( flag, "UI_CTF_EnemyGrabFlag", flag.GetTeam() )
+
+ SetFlagStateForTeam( flag.GetTeam(), eFlagState.Away ) // used for held
+}
+
+void function DropFlagIfPhased( entity player, entity flag )
+{
+ player.EndSignal( "StartPhaseShift" )
+
+ OnThreadEnd( function() : ( player )
+ {
+ DropFlag( player, true )
+ })
+
+ while( flag.GetParent() == player )
+ WaitFrame()
+}
+
+void function DropFlagForBecomingTitan( entity pilot, entity titan )
+{
+ DropFlag( pilot, true )
+}
+
+void function DropFlag( entity player, bool realDrop = true )
+{
+ entity flag = GetFlagForTeam( GetOtherTeam( player.GetTeam() ) )
+
+ if ( flag.GetParent() != player )
+ return
+
+ print( player + " dropped the flag!" )
+
+ flag.ClearParent()
+ flag.SetAngles( < 0, 0, 0 > )
+ flag.SetVelocity( < 0, 0, 0 > )
+
+ if ( realDrop )
+ {
+ // start drop timeout countdown
+ thread TrackFlagDropTimeout( flag )
+
+ // add to capture assists
+ if ( player.GetTeam() == TEAM_IMC )
+ file.imcCaptureAssistList.append( player )
+ else
+ file.militiaCaptureAssistList.append( player )
+
+ // do notifications
+ MessageToPlayer( player, eEventNotifications.YouDroppedTheEnemyFlag )
+ EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_FlagDrop" )
+
+ MessageToTeam( player.GetTeam(), eEventNotifications.PlayerDroppedEnemyFlag, player, player )
+ // todo need a sound here maybe
+
+ MessageToTeam( GetOtherTeam( player.GetTeam() ), eEventNotifications.PlayerDroppedFriendlyFlag, player, player )
+ // todo need a sound here maybe
+ }
+
+ SetFlagStateForTeam( flag.GetTeam(), eFlagState.Home ) // used for return prompt
+}
+
+void function TrackFlagDropTimeout( entity flag )
+{
+ flag.EndSignal( "ResetDropTimeout" )
+
+ wait CTF_GetDropTimeout()
+
+ ResetFlag( flag )
+}
+
+void function ResetFlag( entity flag )
+{
+ // ensure we can't pickup the flag after it's been dropped but before it's been reset
+ flag.s.canTake = false
+
+ if ( flag.GetParent() != null )
+ DropFlag( flag.GetParent(), false )
+
+ entity spawn
+ if ( flag.GetTeam() == TEAM_IMC )
+ spawn = file.imcFlagSpawn
+ else
+ spawn = file.militiaFlagSpawn
+
+ flag.SetOrigin( spawn.GetOrigin() + < 0, 0, spawn.GetBoundingMaxs().z + 1 > )
+
+ // we can take it again now
+ flag.s.canTake = true
+
+ SetFlagStateForTeam( flag.GetTeam(), eFlagState.None ) // used for home
+
+ flag.Signal( "ResetDropTimeout" )
+}
+
+void function CaptureFlag( entity player, entity flag )
+{
+ // reset flag
+ ResetFlag( flag )
+
+ print( player + " captured the flag!" )
+
+ // score
+ int team = player.GetTeam()
+ AddTeamScore( team, 1 )
+ AddPlayerScore( player, "FlagCapture", player )
+ player.AddToPlayerGameStat( PGS_ASSAULT_SCORE, 1 ) // add 1 to captures on scoreboard
+ SetRoundWinningKillReplayAttacker( player ) // set attacker for last cap replay
+
+ array<entity> assistList
+ if ( player.GetTeam() == TEAM_IMC )
+ assistList = file.imcCaptureAssistList
+ else
+ assistList = file.militiaCaptureAssistList
+
+ foreach( entity assistPlayer in assistList )
+ if ( player != assistPlayer )
+ AddPlayerScore( assistPlayer, "FlagCaptureAssist", player )
+
+ assistList.clear()
+
+ // notifs
+ MessageToPlayer( player, eEventNotifications.YouCapturedTheEnemyFlag )
+ EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_PlayerScore" )
+
+ MessageToTeam( team, eEventNotifications.PlayerCapturedEnemyFlag, player, player )
+ EmitSoundOnEntityToTeamExceptPlayer( flag, "UI_CTF_3P_TeamScore", player.GetTeam(), player )
+
+ MessageToTeam( GetOtherTeam( team ), eEventNotifications.PlayerCapturedFriendlyFlag, player, player )
+ EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyScore", flag.GetTeam() )
+
+ if ( GameRules_GetTeamScore( team ) == GameMode_GetRoundScoreLimit( GAMETYPE ) - 1 )
+ {
+ PlayFactionDialogueToTeam( "ctf_notifyWin1more", team )
+ PlayFactionDialogueToTeam( "ctf_notifyLose1more", GetOtherTeam( team ) )
+ }
+}
+
+void function OnPlayerEntersFlagReturnTrigger( entity trigger, entity player )
+{
+ entity flag
+ if ( trigger.GetTeam() == TEAM_IMC )
+ flag = file.imcFlag
+ else
+ flag = file.militiaFlag
+
+ if ( !player.IsPlayer() || player.IsTitan() || player.GetTeam() != flag.GetTeam() || IsFlagHome( flag ) || flag.GetParent() != null )
+ return
+
+ thread TryReturnFlag( player, flag )
+}
+
+void function OnPlayerExitsFlagReturnTrigger( entity trigger, entity player )
+{
+ entity flag
+ if ( trigger.GetTeam() == TEAM_IMC )
+ flag = file.imcFlag
+ else
+ flag = file.militiaFlag
+
+ if ( !player.IsPlayer() || player.IsTitan() || player.GetTeam() != flag.GetTeam() || IsFlagHome( flag ) || flag.GetParent() != null )
+ return
+
+ player.Signal( "FlagReturnEnded" )
+}
+
+void function TryReturnFlag( entity player, entity flag )
+{
+ // start return progress bar
+ Remote_CallFunction_NonReplay( player, "ServerCallback_CTF_StartReturnFlagProgressBar", Time() + CTF_GetFlagReturnTime() )
+ EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_FlagReturnMeter" )
+
+ OnThreadEnd( function() : ( player )
+ {
+ // cleanup
+ Remote_CallFunction_NonReplay( player, "ServerCallback_CTF_StopReturnFlagProgressBar" )
+ StopSoundOnEntity( player, "UI_CTF_1P_FlagReturnMeter" )
+ })
+
+ player.EndSignal( "FlagReturnEnded" )
+ player.EndSignal( "OnDeath" )
+
+ wait CTF_GetFlagReturnTime()
+
+ // flag return succeeded
+ // return flag
+ ResetFlag( flag )
+
+ // do notifications for return
+ MessageToPlayer( player, eEventNotifications.YouReturnedFriendlyFlag )
+ AddPlayerScore( player, "FlagReturn", player )
+ player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, 1 )
+
+ MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerReturnedFriendlyFlag, null, player )
+ EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_TeamReturnsFlag", flag.GetTeam() )
+ PlayFactionDialogueToTeam( "ctf_flagReturnedFriendly", flag.GetTeam() )
+
+ MessageToTeam( GetOtherTeam( flag.GetTeam() ), eEventNotifications.PlayerReturnedEnemyFlag, null, player )
+ EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyReturnsFlag", GetOtherTeam( flag.GetTeam() ) )
+ PlayFactionDialogueToTeam( "ctf_flagReturnedEnemy", GetOtherTeam( flag.GetTeam() ) )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fd.nut
new file mode 100644
index 00000000..b5f700e5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fd.nut
@@ -0,0 +1,12 @@
+global function GamemodeFD_Init
+global function RateSpawnpoints_FD
+
+void function GamemodeFD_Init()
+{
+
+}
+
+void function RateSpawnpoints_FD(int _0, array<entity> _1, int _2, entity _3)
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut
new file mode 100644
index 00000000..932f14b7
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut
@@ -0,0 +1,17 @@
+global function FFA_Init
+
+void function FFA_Init()
+{
+ Evac_SetEnabled( false )
+
+ AddCallback_OnPlayerKilled( OnPlayerKilled )
+}
+
+void function OnPlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ if ( victim != attacker && victim.IsPlayer() && attacker.IsPlayer() && GetGameState() == eGameState.Playing )
+ {
+ AddTeamScore( attacker.GetTeam(), 1 )
+ attacker.AddToPlayerGameStat( PGS_SCORE, 1 )
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut
new file mode 100644
index 00000000..9d8f84b5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut
@@ -0,0 +1,27 @@
+global function GamemodeFRA_AddAdditionalInitCallback
+
+// fra doesn't register a gamemode init by default, adding one just so we can set stuff up for it
+void function GamemodeFRA_AddAdditionalInitCallback()
+{
+ AddCallback_OnCustomGamemodesInit( GamemodeFRA_AddAdditionalInit )
+}
+
+void function GamemodeFRA_AddAdditionalInit()
+{
+ GameMode_AddServerInit( FREE_AGENCY, GamemodeFRA_Init )
+}
+
+void function GamemodeFRA_Init()
+{
+ // need a way to disable passive earnmeter gain
+ ScoreEvent_SetEarnMeterValues( "PilotBatteryPickup", 0.0, 0.34 )
+ EarnMeterMP_SetPassiveMeterGainEnabled( false )
+ PilotBattery_SetMaxCount( 3 )
+
+ AddCallback_OnPlayerKilled( FRARemoveEarnMeter )
+}
+
+void function FRARemoveEarnMeter( entity victim, entity attacker, var damageInfo )
+{
+ PlayerEarnMeter_Reset( victim )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut
new file mode 100644
index 00000000..89f9c991
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut
@@ -0,0 +1,103 @@
+untyped
+global function GamemodeLts_Init
+
+struct {
+ entity lastDamageInfoVictim
+ entity lastDamageInfoAttacker
+ int lastDamageInfoMethodOfDeath
+ float lastDamageInfoTime
+
+ bool shouldDoHighlights
+} file
+
+void function GamemodeLts_Init()
+{
+ // gamemode settings
+ SetShouldUsePickLoadoutScreen( true )
+ SetSwitchSidesBased( true )
+ SetRoundBased( true )
+ SetRespawnsEnabled( false )
+ Riff_ForceSetEliminationMode( eEliminationMode.PilotsTitans )
+ Riff_ForceSetSpawnAsTitan( eSpawnAsTitan.Always )
+ SetShouldUseRoundWinningKillReplay( true )
+ SetRoundWinningKillReplayKillClasses( true, true ) // both titan and pilot kills are tracked
+ FlagSet( "ForceStartSpawn" )
+
+ AddCallback_OnPilotBecomesTitan( RefreshThirtySecondWallhackHighlight )
+ AddCallback_OnTitanBecomesPilot( RefreshThirtySecondWallhackHighlight )
+
+ SetTimeoutWinnerDecisionFunc( CheckTitanHealthForDraw )
+ TrackTitanDamageInPlayerGameStat( PGS_ASSAULT_SCORE )
+
+ ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, ClassicMP_DefaultNoIntro_GetLength() )
+ AddCallback_GameStateEnter( eGameState.Playing, WaitForThirtySecondsLeft )
+}
+
+void function WaitForThirtySecondsLeft()
+{
+ thread WaitForThirtySecondsLeftThreaded()
+}
+
+void function WaitForThirtySecondsLeftThreaded()
+{
+ svGlobal.levelEnt.EndSignal( "RoundEnd" ) // end this on round end
+
+ float endTime = expect float ( GetServerVar( "roundEndTime" ) )
+
+ // wait until 30sec left
+ wait ( endTime - 30 ) - Time()
+ foreach ( entity player in GetPlayerArray() )
+ {
+ // warn there's 30 seconds left
+ Remote_CallFunction_NonReplay( player, "ServerCallback_LTSThirtySecondWarning" )
+
+ // do initial highlight
+ RefreshThirtySecondWallhackHighlight( player, null )
+ }
+}
+
+void function RefreshThirtySecondWallhackHighlight( entity player, entity titan )
+{
+ if ( TimeSpentInCurrentState() < expect float ( GetServerVar( "roundEndTime" ) ) - 30.0 )
+ return
+
+ Highlight_SetEnemyHighlight( player, "enemy_sonar" ) // i think this needs a different effect, this works for now tho
+
+ if ( player.GetPetTitan() != null )
+ Highlight_SetEnemyHighlight( player.GetPetTitan(), "enemy_sonar" )
+}
+
+int function CheckTitanHealthForDraw()
+{
+ int militiaTitans
+ int imcTitans
+
+ float militiaHealth
+ float imcHealth
+
+ foreach ( entity titan in GetTitanArray() )
+ {
+ if ( titan.GetTeam() == TEAM_MILITIA )
+ {
+ // doomed is counted as 0 health
+ militiaHealth += titan.GetTitanSoul().IsDoomed() ? 0.0 : GetHealthFrac( titan )
+ militiaTitans++
+ }
+ else
+ {
+ // doomed is counted as 0 health in this
+ imcHealth += titan.GetTitanSoul().IsDoomed() ? 0.0 : GetHealthFrac( titan )
+ imcTitans++
+ }
+ }
+
+ // note: due to how stuff is set up rn, there's actually no way to do win/loss reasons outside of a SetWinner call, i.e. not in timeout winner decision
+ // as soon as there is, strings in question are "#GAMEMODE_TITAN_TITAN_ADVANTAGE" and "#GAMEMODE_TITAN_TITAN_DISADVANTAGE"
+
+ if ( militiaTitans != imcTitans )
+ return militiaTitans > imcTitans ? TEAM_MILITIA : TEAM_IMC
+ else if ( militiaHealth != imcHealth )
+ return militiaHealth > imcHealth ? TEAM_MILITIA : TEAM_IMC
+
+ return TEAM_UNASSIGNED
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut
new file mode 100644
index 00000000..8d0545cb
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut
@@ -0,0 +1,232 @@
+untyped
+global function GamemodeMfd_Init
+
+struct {
+ entity imcLastMark
+ entity militiaLastMark
+} file
+
+void function GamemodeMfd_Init()
+{
+ GamemodeMfdShared_Init()
+
+ RegisterSignal( "MarkKilled" )
+
+ AddCallback_OnPlayerKilled( UpdateMarksForKill )
+ AddCallback_GameStateEnter( eGameState.Playing, CreateInitialMarks )
+}
+
+void function CreateInitialMarks()
+{
+ entity imcMark = CreateEntity( MARKER_ENT_CLASSNAME )
+ imcMark.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ SetTeam( imcMark, TEAM_IMC )
+ SetTargetName( imcMark, MARKET_ENT_MARKED_NAME ) // why is it market_ent lol
+ DispatchSpawn( imcMark )
+ FillMFDMarkers( imcMark )
+
+ entity imcPendingMark = CreateEntity( MARKER_ENT_CLASSNAME )
+ imcPendingMark.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ SetTeam( imcPendingMark, TEAM_IMC )
+ SetTargetName( imcPendingMark, MARKET_ENT_PENDING_MARKED_NAME )
+ DispatchSpawn( imcPendingMark )
+ FillMFDMarkers( imcPendingMark )
+
+ entity militiaMark = CreateEntity( MARKER_ENT_CLASSNAME )
+ militiaMark.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ SetTeam( militiaMark, TEAM_MILITIA )
+ SetTargetName( militiaMark, MARKET_ENT_MARKED_NAME )
+ DispatchSpawn( militiaMark )
+ FillMFDMarkers( militiaMark )
+
+ entity militiaPendingMark = CreateEntity( MARKER_ENT_CLASSNAME )
+ militiaPendingMark.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ SetTeam( militiaPendingMark, TEAM_MILITIA )
+ SetTargetName( militiaPendingMark, MARKET_ENT_PENDING_MARKED_NAME )
+ DispatchSpawn( militiaPendingMark )
+ FillMFDMarkers( militiaPendingMark )
+
+ thread MFDThink()
+}
+
+void function MFDThink()
+{
+ svGlobal.levelEnt.EndSignal( "GameStateChanged" )
+
+ entity imcMark
+ entity militiaMark
+
+ while ( true )
+ {
+ if ( !TargetsMarkedImmediately() )
+ wait MFD_BETWEEN_MARKS_TIME
+
+ // wait for enough players to spawn
+ array<entity> imcPlayers
+ array<entity> militiaPlayers
+ while ( imcPlayers.len() == 0 || militiaPlayers.len() == 0 )
+ {
+ imcPlayers = GetPlayerArrayOfTeam( TEAM_IMC )
+ militiaPlayers = GetPlayerArrayOfTeam( TEAM_MILITIA )
+
+ WaitFrame()
+ }
+
+ // get marks, wanna increment the mark each mark, reset on player change
+ int imcIndex = imcPlayers.find( imcMark )
+ if ( imcIndex == -1 ) // last mark
+ imcIndex = 0
+ else
+ imcIndex = ( imcIndex + 1 ) % imcPlayers.len()
+
+ imcMark = imcPlayers[ imcIndex ]
+
+ int militiaIndex = militiaPlayers.find( imcMark )
+ if ( militiaIndex == -1 ) // last mark
+ militiaIndex = 0
+ else
+ militiaIndex = ( militiaIndex + 1 ) % militiaPlayers.len()
+
+ militiaMark = militiaPlayers[ militiaIndex ]
+
+ level.mfdPendingMarkedPlayerEnt[ TEAM_IMC ].SetOwner( imcMark )
+ level.mfdPendingMarkedPlayerEnt[ TEAM_MILITIA ].SetOwner( militiaMark )
+
+ foreach ( entity player in GetPlayerArray() )
+ {
+ Remote_CallFunction_NonReplay( player, "SCB_MarkedChanged" )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_MFD_StartNewMarkCountdown", Time() + MFD_COUNTDOWN_TIME )
+ }
+
+ // reset if mark leaves
+ bool shouldReset
+ float endTime = Time() + MFD_COUNTDOWN_TIME
+ while ( endTime > Time() || ( !IsAlive( imcMark ) || !IsAlive( militiaMark ) ) )
+ {
+ if ( !IsValid( imcMark ) || !IsValid( militiaMark ) )
+ {
+ shouldReset = true
+ break
+ }
+
+ WaitFrame()
+ }
+
+ if ( shouldReset )
+ continue
+
+ waitthread MarkPlayers( imcMark, militiaMark )
+ }
+}
+
+void function MarkPlayers( entity imcMark, entity militiaMark )
+{
+ imcMark.EndSignal( "OnDestroy" )
+ imcMark.EndSignal( "Disconnected" )
+
+ militiaMark.EndSignal( "OnDestroy" )
+ militiaMark.EndSignal( "Disconnected" )
+
+ OnThreadEnd( function() : ( imcMark, militiaMark )
+ {
+ // clear marks
+ level.mfdActiveMarkedPlayerEnt[ TEAM_IMC ].SetOwner( null )
+ level.mfdActiveMarkedPlayerEnt[ TEAM_MILITIA ].SetOwner( null )
+
+ foreach ( entity player in GetPlayerArray() )
+ Remote_CallFunction_NonReplay( player, "SCB_MarkedChanged" )
+ })
+
+ // clear pending marks
+ level.mfdPendingMarkedPlayerEnt[ TEAM_IMC ].SetOwner( null )
+ level.mfdPendingMarkedPlayerEnt[ TEAM_MILITIA ].SetOwner( null )
+
+ // set marks
+ level.mfdActiveMarkedPlayerEnt[ TEAM_IMC ].SetOwner( imcMark )
+ level.mfdActiveMarkedPlayerEnt[ TEAM_MILITIA ].SetOwner( militiaMark )
+
+ foreach ( entity player in GetPlayerArray() )
+ Remote_CallFunction_NonReplay( player, "SCB_MarkedChanged" )
+
+ // wait until mark dies
+ entity deadMark = expect entity( svGlobal.levelEnt.WaitSignal( "MarkKilled" ).mark )
+
+ // award points
+ entity livingMark = GetMarked( GetOtherTeam( deadMark.GetTeam() ) )
+ livingMark.SetPlayerGameStat( PGS_DEFENSE_SCORE, livingMark.GetPlayerGameStat( PGS_DEFENSE_SCORE ) + 1 )
+
+ // thread this so we don't kill our own thread
+ thread AddTeamScore( livingMark.GetTeam(), 1 )
+}
+
+void function UpdateMarksForKill( entity victim, entity attacker, var damageInfo )
+{
+ if ( victim == GetMarked( victim.GetTeam() ) )
+ {
+ svGlobal.levelEnt.Signal( "MarkKilled", { mark = victim } )
+
+ if ( attacker.IsPlayer() )
+ attacker.SetPlayerGameStat( PGS_ASSAULT_SCORE, attacker.GetPlayerGameStat( PGS_ASSAULT_SCORE ) + 1 )
+ }
+}
+
+/*
+void function MarkPlayers()
+{
+ // todo: need to handle disconnecting marks
+ if ( !TargetsMarkedImmediately() )
+ wait MFD_BETWEEN_MARKS_TIME
+
+
+ // wait until we actually have 2 valid players
+ array<entity> imcPlayers
+ array<entity> militiaPlayers
+ while ( imcPlayers.len() == 0 || militiaPlayers.len() == 0 )
+ {
+ imcPlayers = GetPlayerArrayOfTeam( TEAM_IMC )
+ militiaPlayers = GetPlayerArrayOfTeam( TEAM_MILITIA )
+
+ WaitFrame()
+ }
+
+ // decide marks
+ entity imcMark = imcPlayers[ RandomInt( imcPlayers.len() ) ]
+ level.mfdPendingMarkedPlayerEnt[ TEAM_IMC ].SetOwner( imcMark )
+
+ entity militiaMark = militiaPlayers[ RandomInt( militiaPlayers.len() ) ]
+ level.mfdPendingMarkedPlayerEnt[ TEAM_MILITIA ].SetOwner( militiaMark )
+
+ foreach ( entity player in GetPlayerArray() )
+ {
+ Remote_CallFunction_NonReplay( player, "SCB_MarkedChanged" )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_MFD_StartNewMarkCountdown", Time() + MFD_COUNTDOWN_TIME )
+ }
+
+ wait MFD_COUNTDOWN_TIME
+
+ while ( !IsAlive( imcMark ) || !IsAlive( militiaMark ) )
+ WaitFrame()
+
+ // clear pending marks
+ level.mfdPendingMarkedPlayerEnt[ TEAM_IMC ].SetOwner( null )
+ level.mfdPendingMarkedPlayerEnt[ TEAM_MILITIA ].SetOwner( null )
+
+ // set marks
+ level.mfdActiveMarkedPlayerEnt[ TEAM_IMC ].SetOwner( imcMark )
+ level.mfdActiveMarkedPlayerEnt[ TEAM_MILITIA ].SetOwner( militiaMark )
+
+ foreach ( entity player in GetPlayerArray() )
+ Remote_CallFunction_NonReplay( player, "SCB_MarkedChanged" )
+
+ while ( IsAlive( imcMark ) && IsAlive( militiaMark ) )
+ WaitFrame()
+
+ // clear marks
+ level.mfdActiveMarkedPlayerEnt[ TEAM_IMC ].SetOwner( null )
+ level.mfdActiveMarkedPlayerEnt[ TEAM_MILITIA ].SetOwner( null )
+
+ foreach ( entity player in GetPlayerArray() )
+ Remote_CallFunction_NonReplay( player, "SCB_MarkedChanged" )
+
+ thread MarkPlayers()
+}*/ \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut
new file mode 100644
index 00000000..3a852f91
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut
@@ -0,0 +1,12 @@
+global function GamemodePs_Init
+
+void function GamemodePs_Init()
+{
+ AddCallback_OnPlayerKilled( GiveScoreForPlayerKill )
+}
+
+void function GiveScoreForPlayerKill( entity victim, entity attacker, var damageInfo )
+{
+ if ( victim != attacker && victim.IsPlayer() && attacker.IsPlayer() )
+ AddTeamScore( attacker.GetTeam(), 1 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut
new file mode 100644
index 00000000..4532fb97
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut
@@ -0,0 +1,127 @@
+global function GamemodeSpeedball_Init
+
+struct {
+ entity flagBase
+ entity flag
+ entity flagCarrier
+} file
+
+void function GamemodeSpeedball_Init()
+{
+ PrecacheModel( CTF_FLAG_MODEL )
+ PrecacheModel( CTF_FLAG_BASE_MODEL )
+
+ // gamemode settings
+ SetRoundBased( true )
+ SetRespawnsEnabled( false )
+ SetShouldUseRoundWinningKillReplay( true )
+ Riff_ForceTitanAvailability( eTitanAvailability.Never )
+ Riff_ForceSetEliminationMode( eEliminationMode.Pilots )
+
+ AddSpawnCallbackEditorClass( "script_ref", "info_speedball_flag", CreateFlag )
+
+ AddCallback_GameStateEnter( eGameState.Playing, ResetFlag )
+ AddCallback_OnTouchHealthKit( "item_flag", OnFlagCollected )
+ AddCallback_OnPlayerKilled( OnPlayerKilled )
+ SetTimeoutWinnerDecisionFunc( TimeoutCheckFlagHolder )
+
+ ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, ClassicMP_DefaultNoIntro_GetLength() )
+}
+
+void function CreateFlag( entity flagSpawn )
+{
+ entity flagBase = CreatePropDynamic( CTF_FLAG_BASE_MODEL, flagSpawn.GetOrigin(), flagSpawn.GetAngles() )
+
+ entity flag = CreateEntity( "item_flag" )
+ flag.SetValueForModelKey( CTF_FLAG_MODEL )
+ flag.MarkAsNonMovingAttachment()
+ DispatchSpawn( flag )
+ flag.SetModel( CTF_FLAG_MODEL )
+ flag.SetOrigin( flagBase.GetOrigin() + < 0, 0, flagBase.GetBoundingMaxs().z + 1 > )
+ flag.SetVelocity( < 0, 0, 1 > )
+
+ file.flag = flag
+ file.flagBase = flagBase
+}
+
+bool function OnFlagCollected( entity player, entity flag )
+{
+ if ( !IsAlive( player ) || flag.GetParent() != null || player.IsTitan() || player.IsPhaseShifted() )
+ return false
+
+ GiveFlag( player )
+ return false // so flag ent doesn't despawn
+}
+
+void function OnPlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ if ( file.flagCarrier == victim )
+ DropFlag()
+
+ if ( victim.IsPlayer() && GetGameState() == eGameState.Playing )
+ if ( GetPlayerArrayOfTeam_Alive( victim.GetTeam() ).len() == 1 )
+ foreach ( entity player in GetPlayerArray() )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_SPEEDBALL_LastPlayer", player.GetTeam() != victim.GetTeam() )
+}
+
+void function GiveFlag( entity player )
+{
+ file.flag.SetParent( player, "FLAG" )
+ file.flagCarrier = player
+ SetGlobalNetEnt( "flagCarrier", player )
+ thread DropFlagIfPhased( player )
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_GrabFlag" )
+ foreach ( entity otherPlayer in GetPlayerArray() )
+ {
+ MessageToPlayer( otherPlayer, eEventNotifications.SPEEDBALL_FlagPickedUp, player )
+
+ if ( otherPlayer.GetTeam() == player.GetTeam() )
+ EmitSoundOnEntityToTeamExceptPlayer( file.flag, "UI_CTF_3P_TeamGrabFlag", player.GetTeam(), player )
+ }
+}
+
+void function DropFlagIfPhased( entity player )
+{
+ player.EndSignal( "StartPhaseShift" )
+
+ OnThreadEnd( function() : ( player )
+ {
+ if ( file.flag.GetParent() == player )
+ DropFlag()
+ })
+
+ while( file.flag.GetParent() == player )
+ WaitFrame()
+}
+
+void function DropFlag()
+{
+ file.flag.ClearParent()
+ file.flag.SetAngles( < 0, 0, 0 > )
+ SetGlobalNetEnt( "flagCarrier", file.flag )
+ EmitSoundOnEntityOnlyToPlayer( file.flagCarrier, file.flagCarrier, "UI_CTF_1P_FlagDrop" )
+
+ foreach ( entity player in GetPlayerArray() )
+ MessageToPlayer( player, eEventNotifications.SPEEDBALL_FlagDropped, file.flagCarrier )
+
+ file.flagCarrier = null
+}
+
+void function ResetFlag()
+{
+ file.flag.ClearParent()
+ file.flag.SetAngles( < 0, 0, 0 > )
+ file.flag.SetVelocity( < 0, 0, 1 > ) // hack: for some reason flag won't have gravity if i don't do this
+ file.flag.SetOrigin( file.flagBase.GetOrigin() + < 0, 0, file.flagBase.GetBoundingMaxs().z * 2 > )
+ file.flagCarrier = null
+ SetGlobalNetEnt( "flagCarrier", file.flag )
+}
+
+int function TimeoutCheckFlagHolder()
+{
+ if ( file.flagCarrier == null )
+ return TEAM_UNASSIGNED
+
+ return file.flagCarrier.GetTeam()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut
new file mode 100644
index 00000000..9e80b863
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut
@@ -0,0 +1,19 @@
+global function GamemodeTdm_Init
+global function RateSpawnpoints_Directional
+
+void function GamemodeTdm_Init()
+{
+ AddCallback_OnPlayerKilled( GiveScoreForPlayerKill )
+}
+
+void function GiveScoreForPlayerKill( entity victim, entity attacker, var damageInfo )
+{
+ if ( victim != attacker && victim.IsPlayer() && attacker.IsPlayer() )
+ AddTeamScore( attacker.GetTeam(), 1 )
+}
+
+void function RateSpawnpoints_Directional( int checkclass, array<entity> spawnpoints, int team, entity player )
+{
+ // temp
+ RateSpawnpoints_Generic( checkclass, spawnpoints, team, player )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut
new file mode 100644
index 00000000..faf3e5ca
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut
@@ -0,0 +1,73 @@
+global function GamemodeTTDM_Init
+
+const float TTDMIntroLength = 15.0
+
+void function GamemodeTTDM_Init()
+{
+ Riff_ForceSetSpawnAsTitan( eSpawnAsTitan.Always )
+ Riff_ForceTitanExitEnabled( eTitanExitEnabled.Never )
+ TrackTitanDamageInPlayerGameStat( PGS_ASSAULT_SCORE )
+
+ ClassicMP_SetCustomIntro( TTDMIntroSetup, TTDMIntroLength )
+
+ AddCallback_OnPlayerKilled( AddTeamScoreForPlayerKilled ) // dont have to track autotitan kills since you cant leave your titan in this mode
+
+ // probably needs scoreevent earnmeter values
+}
+
+void function TTDMIntroSetup()
+{
+ // this should show intermission cam for 15 sec in prematch, before spawning players as titans
+ AddCallback_GameStateEnter( eGameState.Prematch, TTDMIntroStart )
+ AddCallback_OnClientConnected( TTDMIntroShowIntermissionCam )
+}
+
+void function TTDMIntroStart()
+{
+ thread TTDMIntroStartThreaded()
+}
+
+void function TTDMIntroStartThreaded()
+{
+ ClassicMP_OnIntroStarted()
+
+ foreach ( entity player in GetPlayerArray() )
+ TTDMIntroShowIntermissionCam( player )
+
+ wait TTDMIntroLength
+
+ ClassicMP_OnIntroFinished()
+}
+
+void function TTDMIntroShowIntermissionCam( entity player )
+{
+ if ( GetGameState() != eGameState.Prematch )
+ return
+
+ thread PlayerWatchesTTDMIntroIntermissionCam( player )
+}
+
+void function PlayerWatchesTTDMIntroIntermissionCam( entity player )
+{
+ ScreenFadeFromBlack( player )
+
+ entity intermissionCam = GetEntArrayByClass_Expensive( "info_intermission" )[ 0 ]
+
+ // the angle set here seems sorta inconsistent as to whether it actually works or just stays at 0 for some reason
+ player.SetObserverModeStaticPosition( intermissionCam.GetOrigin() )
+ player.SetObserverModeStaticAngles( intermissionCam.GetAngles() )
+ player.StartObserverMode( OBS_MODE_STATIC_LOCKED )
+
+ wait TTDMIntroLength
+
+ RespawnAsTitan( player, false )
+ TryGameModeAnnouncement( player )
+}
+
+void function AddTeamScoreForPlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ if ( victim == attacker || !victim.IsPlayer() || !attacker.IsPlayer() )
+ return
+
+ AddTeamScore( GetOtherTeam( victim.GetTeam() ), 1 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_hardpoints.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_hardpoints.gnut
new file mode 100644
index 00000000..0a32f133
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_hardpoints.gnut
@@ -0,0 +1,35 @@
+// atm this is just a stub script since hardpoints are only really used in hardpoint
+// respawn probably tried to share this code across multiple modes but atm we just dont need to do that
+
+global function Hardpoints_Init
+
+global function CapturePoint_GetStartProgress
+global function CapturePoint_GetCappingTeam
+global function CapturePoint_GetOwningTeam
+global function CapturePoint_GetGoalProgress
+
+
+void function Hardpoints_Init()
+{
+
+}
+
+float function CapturePoint_GetStartProgress( entity hardpoint )
+{
+ return GetGlobalNetFloat( "objective" + hardpoint.kv.hardpointGroup + "Progress" )
+}
+
+int function CapturePoint_GetCappingTeam( entity hardpoint )
+{
+ return GetGlobalNetInt( "objective" + hardpoint.kv.hardpointGroup + "CappingTeam" )
+}
+
+int function CapturePoint_GetOwningTeam( entity hardpoint )
+{
+ return hardpoint.GetTeam()
+}
+
+float function CapturePoint_GetGoalProgress( entity hardpoint )
+{
+ return GetGlobalNetFloat( "objective" + hardpoint.kv.hardpointGroup + "Progress" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_riff_floor_is_lava.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_riff_floor_is_lava.nut
new file mode 100644
index 00000000..b660e89f
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_riff_floor_is_lava.nut
@@ -0,0 +1,102 @@
+global function RiffFloorIsLava_Init
+
+void function RiffFloorIsLava_Init()
+{
+ AddCallback_OnPlayerRespawned( FloorIsLava_PlayerRespawned )
+
+ AddSpawnCallback( "env_fog_controller", InitLavaFogController )
+ AddCallback_EntitiesDidLoad( CreateCustomSpawns )
+}
+
+void function InitLavaFogController( entity fogController )
+{
+ fogController.kv.fogztop = GetVisibleFogTop()
+ fogController.kv.fogzbottom = GetVisibleFogBottom()
+ fogController.kv.foghalfdisttop = "60000"
+ fogController.kv.foghalfdistbottom = "200"
+ fogController.kv.fogdistoffset = "0"
+ fogController.kv.fogdensity = ".85"
+
+ fogController.kv.forceontosky = true
+ //fogController.kv.foghalfdisttop = "10000"
+}
+
+void function CreateCustomSpawns()
+{
+ thread CreateCustomSpawns_Threaded()
+}
+
+void function CreateCustomSpawns_Threaded()
+{
+ WaitEndFrame() // wait for spawns to clear
+
+ float raycastTop = GetLethalFogTop() + 2500.0
+ array< vector > raycastPositions
+ foreach ( entity hardpoint in GetEntArrayByClass_Expensive( "info_hardpoint" ) )
+ {
+ if ( !hardpoint.HasKey( "hardpointGroup" ) )
+ continue
+
+ //if ( hardpoint.kv.hardpointGroup != "A" && hardpoint.kv.hardpointGroup != "B" && hardpoint.kv.hardpointGroup != "C" )
+ if ( hardpoint.kv.hardpointGroup != "B" ) // roughly map center
+ continue
+
+ vector pos = hardpoint.GetOrigin()
+ for ( int x = -2000; x < 2000; x += 200 )
+ for ( int y = -2000; y < 2000; y += 200 )
+ raycastPositions.append( < x, y, raycastTop > )
+ }
+
+ int validSpawnsCreated = 0
+ foreach ( vector raycastPos in raycastPositions )
+ {
+ //vector hardpoint = validHardpoints[ RandomInt( validHardpoints.len() ) ].GetOrigin()
+ //float a = RandomFloat( 1 ) * 2 * PI
+ //float r = 1000.0 * sqrt( RandomFloat( 1 ) )
+ //
+ //vector castStart = < hardpoint.x + r * cos( a ), hardpoint.y + r * sin( a ), >
+ //vector castEnd = < hardpoint.x + r * cos( a ), hardpoint.y + r * sin( a ), GetLethalFogBottom() >
+
+ TraceResults trace = TraceLine( raycastPos, < raycastPos.x, raycastPos.y, GetLethalFogBottom() >, [], TRACE_MASK_SOLID, TRACE_COLLISION_GROUP_NONE ) // should only hit world
+ print( "raycast: " + trace.endPos )
+ if ( trace.endPos.z >= GetLethalFogTop() )
+ {
+ print( "creating floor is lava spawn at " + trace.endPos )
+ validSpawnsCreated++
+
+ // valid spot, create a spawn
+ entity spawnpoint = CreateEntity( "info_spawnpoint_human" )
+ spawnpoint.SetOrigin( trace.endPos )
+ spawnpoint.kv.ignoreGamemode = 1
+ DispatchSpawn( spawnpoint )
+ }
+ }
+}
+
+void function FloorIsLava_PlayerRespawned( entity player )
+{
+ thread FloorIsLava_ThinkForPlayer( player )
+}
+
+void function FloorIsLava_ThinkForPlayer( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "OnDeath" )
+
+ while ( true )
+ {
+ WaitFrame()
+
+ if ( player.GetOrigin().z < GetLethalFogTop() )
+ {
+ // do damage
+ float damageMultiplier = 0.08
+ if ( player.IsTitan() )
+ damageMultiplier *= 0.05
+
+ player.TakeDamage( player.GetMaxHealth() * damageMultiplier, null, null, { damageSourceId = eDamageSourceId.floor_is_lava } )
+
+ wait 0.1
+ }
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_spawnpoints.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_spawnpoints.gnut
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_spawnpoints.gnut
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/sh_gamemodes.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/sh_gamemodes.gnut
new file mode 100644
index 00000000..9114fcad
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/sh_gamemodes.gnut
@@ -0,0 +1,819 @@
+
+global function GameModes_Init
+
+global function GameMode_Create
+global function GameMode_SetName
+global function GameMode_SetGameModeAttackAnnouncement
+global function GameMode_SetGameModeDefendAnnouncement
+global function GameMode_SetAttackDesc
+global function GameMode_SetDefendDesc
+global function GameMode_SetIcon
+global function GameMode_SetDefaultScoreLimits
+global function GameMode_AddScoreboardColumnData
+global function GameMode_SetGameModeAnnouncement
+global function GameMode_SetDefaultTimeLimits
+global function GameMode_SetDesc
+global function GameMode_SetColor
+global function GameMode_SetSuddenDeath
+
+global function GameMode_GetScoreLimit
+global function GameMode_GetRoundScoreLimit
+global function GameMode_GetTimeLimit
+global function GameMode_GetRoundTimeLimit
+global function GameMode_GetGameModeAnnouncement
+global function GameMode_GetGameModeAttackAnnouncement
+global function GameMode_GetGameModeDefendAnnouncement
+global function GameMode_GetDesc
+global function GameMode_GetName
+global function GameMode_GetIcon
+global function GameMode_GetColor
+global function GameMode_GetAttackDesc
+global function GameMode_GetDefendDesc
+global function GameMode_GetPilotSpawnpointsRatingFunc
+global function GameMode_GetTitanSpawnpointsRatingFunc
+global function GameMode_GetScoreCompareFunc
+global function GameMode_GetSuddenDeathEnabled
+global function GameMode_GetEvacEnabled
+global function GameMode_GetGameEndingWarning
+global function GameMode_GetGameEndingConversation
+global function GameMode_GetScoreboardColumnTitles
+global function GameMode_GetScoreboardColumnScoreTypes
+global function GameMode_GetScoreboardColumnNumDigits
+global function GameMode_GetCustomIntroAnnouncement
+global function GameMode_RunServerInits
+global function GameMode_RunClientInits
+global function GameMode_RunSharedInits
+global function GameMode_IsDefined
+
+global function GameMode_AddServerInit
+global function GameMode_AddClientInit
+global function GameMode_AddSharedInit
+global function GameMode_SetScoreCompareFunc
+global function GameMode_SetPilotSpawnpointsRatingFunc
+global function GameMode_SetTitanSpawnpointsRatingFunc
+global function GameMode_SetCustomIntroAnnouncement
+
+global function GameMode_GetGameModeId
+
+global function GameMode_SetEvacEnabled
+
+global function GameMode_GetLoadoutSelectTime
+
+global struct GamemodeSettings
+{
+ string name = ""
+ string name_localized = "Undefined Game Mode"
+ string desc_localized = "Undefined Game Mode Description"
+ string desc_attack = ""
+ string desc_defend = ""
+ string gameModeAnnoucement = ""
+ string gameModeAttackAnnoucement = ""
+ string gameModeDefendAnnoucement = ""
+ asset icon = $"ui/menu/playlist/classic"
+ array<int> color = [127, 127, 127, 255]
+ array< void functionref() > serverInits
+ array< void functionref() > clientInits
+ array< void functionref() > sharedInits
+ void functionref( int, array<entity>, int, entity ) pilotSpawnpointRatingFunc
+ void functionref( int, array<entity>, int, entity ) titanSpawnpointRatingFunc
+ IntFromEntityCompare scoreCompareFunc
+ int defaultScoreLimit = 100
+ int defaultTimeLimit = 10
+ int defaultRoundScoreLimit = 5
+ float defaultRoundTimeLimit = 5.0
+ bool evacEnabled = true
+ string gameModeEndingWarning = "#GAMEMODE_END_IN_N_SECONDS"
+ string gameModeEndingConversation = ""
+ bool suddenDeathEnabled = false
+ array<string> scoreboardColumnTitles
+ array<int> scoreboardColumnScoreTypes
+ array<int> scoreboardColumnNumDigits
+ void functionref(entity) customIntroAnnouncementFunc
+}
+
+
+
+// Don't remove items from this list once the game is in production
+// Durango online analytics needs the numbers for each mode to stay the same
+// DO NOT CHANGE THESE VALUES AFTER THEY HAVE GONE LIVE
+global enum eGameModes
+{
+ invalid = -1,
+ TEAM_DEATHMATCH_ID = 0,
+ CAPTURE_POINT_ID = 1,
+ ATTRITION_ID = 2,
+ CAPTURE_THE_FLAG_ID = 3,
+ MARKED_FOR_DEATH_ID = 4,
+ LAST_TITAN_STANDING_ID = 5,
+ WINGMAN_LAST_TITAN_STANDING_ID = 6,
+ PILOT_SKIRMISH_ID = 7,
+ MARKED_FOR_DEATH_PRO_ID = 8,
+ COOPERATIVE_ID = 9,
+ GAMEMODE_SP_ID = 10,
+ TITAN_BRAWL_ID = 11,
+ FFA_ID = 12,
+ PROTOTYPE2 = 13,
+ WINGMAN_PILOT_SKIRMISH_ID = 14,
+ PROTOTYPE3 = 15,
+ PROTOTYPE4 = 16,
+ FREE_AGENCY_ID = 17,
+ PROTOTYPE6 = 18,
+ COLISEUM_ID = 19,
+ PROTOTYPE7 = 20,
+ AI_TDM_ID = 21,
+ PROTOTYPE8 = 22,
+ PROTOTYPE9 = 23,
+ SPEEDBALL_ID = 24,
+ PROTOTYPE10 = 25,
+ PROTOTYPE11 = 26,
+ PROTOTYPE12 = 27,
+ FD_ID = 28,
+ PROTOTYPE14 = 29,
+}
+
+const table<string, int> gameModesStringToIdMap = {
+ [ TEAM_DEATHMATCH ] = eGameModes.TEAM_DEATHMATCH_ID,
+ [ PILOT_SKIRMISH ] = eGameModes.PILOT_SKIRMISH_ID,
+ [ CAPTURE_POINT ] = eGameModes.CAPTURE_POINT_ID,
+ [ ATTRITION ] = eGameModes.ATTRITION_ID,
+ [ CAPTURE_THE_FLAG ] = eGameModes.CAPTURE_THE_FLAG_ID,
+ [ LAST_TITAN_STANDING ] = eGameModes.LAST_TITAN_STANDING_ID,
+ [ GAMEMODE_SP ] = eGameModes.GAMEMODE_SP_ID,
+ [ FFA ] = eGameModes.FFA_ID,
+ [ COLISEUM ] = eGameModes.COLISEUM_ID,
+ [ AI_TDM ] = eGameModes.AI_TDM_ID,
+ [ SPEEDBALL ] = eGameModes.SPEEDBALL_ID,
+ [ MARKED_FOR_DEATH ] = eGameModes.MARKED_FOR_DEATH_ID,
+ [ TITAN_BRAWL ] = eGameModes.TITAN_BRAWL_ID,
+ [ FREE_AGENCY ] = eGameModes.FREE_AGENCY_ID,
+ [ FD ] = eGameModes.FD_ID,
+ [ FD_EASY ] = eGameModes.FD_ID,
+ [ FD_NORMAL ] = eGameModes.FD_ID,
+ [ FD_HARD ] = eGameModes.FD_ID,
+ [ FD_MASTER ] = eGameModes.FD_ID,
+ [ FD_INSANE ] = eGameModes.FD_ID,
+}
+
+struct
+{
+ table< string, GamemodeSettings > gameModeDefs
+} file
+
+void function GameModes_Init()
+{
+ string gameMode
+
+ gameMode = GAMEMODE_SP
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#GAMEMODE_SOLO" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/coop" ) //HACK TODO: get a sp icon
+ GameMode_SetDesc( gameMode, "#GAMEMODE_SOLO_HINT" )
+ GameMode_SetDefaultScoreLimits( gameMode, 0, 0 )
+ GameMode_SetDefaultTimeLimits( gameMode, 0, 0.0 )
+
+ gameMode = CAPTURE_POINT
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_hardpoint" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "hp_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_CP" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_hardpoint_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/cp" )
+ GameMode_SetDefaultScoreLimits( gameMode, 500, 500 )
+ GameMode_SetDefaultTimeLimits( gameMode, 15, 0.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_ASSAULT", PGS_ASSAULT_SCORE, 4 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_DEFENSE", PGS_DEFENSE_SCORE, 4 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_KILLS, 2 )
+ GameMode_SetColor( gameMode, [46, 188, 180, 255] )
+
+ gameMode = LAST_TITAN_STANDING
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_last_titan_standing" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "lts_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_LTS" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_last_titan_standing_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/lts" )
+ GameMode_SetDefaultScoreLimits( gameMode, 0, 4 )
+ GameMode_SetDefaultTimeLimits( gameMode, 5, 4.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_TITAN_KILLS", PGS_TITAN_KILLS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_TITAN_DAMAGE", PGS_ASSAULT_SCORE, 6 )
+ GameMode_SetColor( gameMode, [223, 94, 0, 255] )
+
+ gameMode = ATTRITION
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_attrition" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "bh_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_AT" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_attrition_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/at" )
+ GameMode_SetDefaultScoreLimits( gameMode, 5000, 0 )
+ GameMode_SetDefaultTimeLimits( gameMode, 15, 0.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_SCORE", PGS_ASSAULT_SCORE, 4 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_BONUS", PGS_SCORE, 4 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_KILLS, 2 )
+ GameMode_SetColor( gameMode, [88, 172, 67, 255] )
+
+ gameMode = TEAM_DEATHMATCH
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_pilot_hunter" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "phunt_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_TDM" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_pilot_hunter_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/tdm" )
+ GameMode_SetDefaultScoreLimits( gameMode, 50, 0 )
+ GameMode_SetDefaultTimeLimits( gameMode, 15, 0.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_PILOT_KILLS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_DEATHS", PGS_DEATHS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_ASSISTS", PGS_ASSISTS, 2 )
+ GameMode_SetColor( gameMode, [212, 83, 152, 255] )
+
+ gameMode = AI_TDM
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_aitdm" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "gnrc_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_TDM" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_aitdm_hint" )
+ GameMode_SetIcon( gameMode, FFA_MODE_ICON )
+ GameMode_SetDefaultScoreLimits( gameMode, 1, 0 )
+ GameMode_SetDefaultTimeLimits( gameMode, 15, 0.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_SCORE", PGS_ASSAULT_SCORE, 3 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_PILOT_KILLS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_TITAN_KILLS", PGS_TITAN_KILLS, 1 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_GRUNT_KILLS", PGS_NPC_KILLS, 2 )
+ // GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_DEATHS", PGS_DEATHS, 2 )
+ GameMode_SetColor( gameMode, [200, 40, 40, 255] )
+
+ gameMode = COLISEUM
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_coliseum" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "gnrc_modeDesc" ) //TODO: This is just the mode name as opposed to instructions...
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_PS" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_coliseum_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/tdm" )
+ GameMode_SetDefaultScoreLimits( gameMode, 15, 2 )
+ GameMode_SetDefaultTimeLimits( gameMode, 0, 4.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_KILLS, 2 )
+ GameMode_SetColor( gameMode, [151, 71, 175, 255] )
+
+ gameMode = PILOT_SKIRMISH
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_pilot_skirmish" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "pvp_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_PS" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_pilot_skirmish_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/tdm" )
+ GameMode_SetDefaultScoreLimits( gameMode, 100, 0 )
+ GameMode_SetDefaultTimeLimits( gameMode, 15, 0.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_KILLS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_DEATHS", PGS_DEATHS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_ASSISTS", PGS_ASSISTS, 2 )
+ GameMode_SetColor( gameMode, [207, 191, 59, 255] )
+
+ gameMode = CAPTURE_THE_FLAG
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_capture_the_flag" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "ctf_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_CTF" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_capture_the_flag_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/ctf" )
+ GameMode_SetSuddenDeath( gameMode, true )
+ GameMode_SetDefaultScoreLimits( gameMode, 0, 5 )
+ GameMode_SetDefaultTimeLimits( gameMode, 0, 3.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_CAPTURES", PGS_ASSAULT_SCORE, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_RETURNS", PGS_DEFENSE_SCORE, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_KILLS, 2 )
+ GameMode_SetColor( gameMode, [61, 117, 193, 255] )
+
+ gameMode = FFA
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_ffa" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "ffa_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_FFA" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_ffa_hint" )
+ GameMode_SetIcon( gameMode, FFA_MODE_ICON )
+ GameMode_SetDefaultScoreLimits( gameMode, 10, 0 )
+ GameMode_SetDefaultTimeLimits( gameMode, 10, 0.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_SCORE", PGS_ASSAULT_SCORE, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_PILOT_KILLS", PGS_PILOT_KILLS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_TITAN_KILLS", PGS_TITAN_KILLS, 2 )
+ GameMode_SetColor( gameMode, [147, 204, 57, 255] )
+
+ gameMode = FREE_AGENCY
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_free_agents" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "freea_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_FREE_AGENCY" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_free_agents_hint" )
+ GameMode_SetIcon( gameMode, FFA_MODE_ICON )
+ GameMode_SetDefaultScoreLimits( gameMode, 10, 0 )
+ GameMode_SetDefaultTimeLimits( gameMode, 15, 0.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_SCORE", PGS_ASSAULT_SCORE, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_PILOT_KILLS", PGS_PILOT_KILLS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_TITAN_KILLS", PGS_TITAN_KILLS, 2 )
+ GameMode_SetColor( gameMode, [127, 127, 127, 255] )
+
+ gameMode = SPEEDBALL
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_speedball" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "gnrc_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_CTF" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_speedball_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/ctf" )
+ GameMode_SetDefaultScoreLimits( gameMode, 0, 5 )
+ GameMode_SetDefaultTimeLimits( gameMode, 0, 1.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_KILLS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_FLAGS_SECURED", PGS_ASSAULT_SCORE, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_DEATHS", PGS_DEATHS, 2 )
+ GameMode_SetColor( gameMode, [225, 141, 8, 255] )
+
+ gameMode = MARKED_FOR_DEATH
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_marked_for_death" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "mfd_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_MFD" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_marked_for_death_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/mfd" )
+ GameMode_SetDefaultScoreLimits( gameMode, 10, 0 )
+ GameMode_SetDefaultTimeLimits( gameMode, 10, 0.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_MFD_SCORE", PGS_ASSAULT_SCORE, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_MFD_MARKS_OUTLASTED", PGS_DEFENSE_SCORE, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_KILLS, 2 )
+ GameMode_SetColor( gameMode, [127, 127, 127, 255] )
+
+ gameMode = TITAN_BRAWL
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_titan_brawl" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "lts_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_TTDM" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_titan_brawl_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/lts" )
+ GameMode_SetDefaultScoreLimits( gameMode, 30, 0 )
+ GameMode_SetDefaultTimeLimits( gameMode, 15, 0.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_KILLS", PGS_PILOT_KILLS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_DEATHS", PGS_DEATHS, 2 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_TITAN_DAMAGE", PGS_ASSAULT_SCORE, 6 )
+ GameMode_SetColor( gameMode, [83, 212, 152, 255] )
+
+ gameMode = FD
+ GameMode_Create( gameMode )
+ GameMode_SetName( gameMode, "#PL_fd" )
+ #if FACTION_DIALOGUE_ENABLED
+ GameMode_SetGameModeAnnouncement( gameMode, "fd_modeDesc" )
+ #else
+ GameMode_SetGameModeAnnouncement( gameMode, "GameModeAnnounce_PS" )
+ #endif
+ GameMode_SetDesc( gameMode, "#PL_fd_hint" )
+ GameMode_SetIcon( gameMode, $"ui/menu/playlist/tdm" )
+ GameMode_SetSuddenDeath( gameMode, true )
+ GameMode_SetDefaultScoreLimits( gameMode, 0, 5 )
+ GameMode_SetDefaultTimeLimits( gameMode, 0, 5.0 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_TOTAL_SCORE", PGS_DETONATION_SCORE, 4 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_COMBAT_SCORE", PGS_ASSAULT_SCORE, 4 )
+ //GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_HEALING_SCORE", PGS_DISTANCE_SCORE, 3 )
+ GameMode_AddScoreboardColumnData( gameMode, "#SCOREBOARD_SUPPORT_SCORE", PGS_DEFENSE_SCORE, 4 )
+
+ #if DEVSCRIPTS
+ DevGameModes_Init()
+ #endif
+
+ #if SERVER || CLIENT
+ // add modes/maps/playlists to private lobby that aren't there by default
+ PrivateMatchModesInit()
+
+ InitCustomGamemodes() // do custom gamemode callbacks
+ GameModes_Init_SV_CL()
+ #endif
+
+ ////
+ GameMode_VerifyModes()
+}
+
+// TODO: scoreboards
+
+/*************************************************************
+ Setters
+*************************************************************/
+
+GamemodeSettings function GameMode_Create( string gameModeName )
+{
+ Assert( !(gameModeName in file.gameModeDefs), "Gametype already defined!" )
+
+ GamemodeSettings settings
+ file.gameModeDefs[gameModeName] <- settings
+
+ return file.gameModeDefs[gameModeName]
+}
+
+void function GameMode_SetName( string gameModeName, string nameText )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut (" + gameModeName + ")" )
+ file.gameModeDefs[gameModeName].name_localized = nameText
+}
+
+void function GameMode_SetGameModeAnnouncement( string gameModeName, string gameModeAnnoucement ) //Note: Still need to register the conversation
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].gameModeAnnoucement = gameModeAnnoucement
+}
+
+void function GameMode_SetGameModeAttackAnnouncement( string gameModeName, string gameModeAttackAnnoucement ) //Note: Still need to register the conversation
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].gameModeAttackAnnoucement = gameModeAttackAnnoucement
+}
+
+void function GameMode_SetGameModeDefendAnnouncement( string gameModeName, string gameModeDefendAnnoucement )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" ) //Note: Still need to register the conversation
+ file.gameModeDefs[gameModeName].gameModeDefendAnnoucement = gameModeDefendAnnoucement
+}
+
+void function GameMode_SetDesc( string gameModeName, string descText )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].desc_localized = descText
+}
+
+void function GameMode_SetAttackDesc( string gameModeName, string descText )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].desc_attack = descText
+}
+
+void function GameMode_SetDefendDesc( string gameModeName, string descText )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].desc_defend = descText
+}
+
+void function GameMode_SetIcon( string gameModeName, asset icon )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].icon = icon
+}
+
+void function GameMode_SetColor( string gameModeName, array<int> color )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].color = color
+}
+
+void function GameMode_SetSuddenDeath( string gameModeName, bool state )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].suddenDeathEnabled = state
+}
+
+void function GameMode_AddServerInit( string gameModeName, void functionref() func )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].serverInits.append( func )
+}
+
+void function GameMode_AddClientInit( string gameModeName, void functionref() func )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].clientInits.append( func )
+}
+
+void function GameMode_AddSharedInit( string gameModeName, void functionref() func )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].sharedInits.append( func )
+}
+
+void function GameMode_SetPilotSpawnpointsRatingFunc( string gameModeName, void functionref( int, array<entity>, int, entity ) func )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].pilotSpawnpointRatingFunc = func
+}
+
+void function GameMode_SetTitanSpawnpointsRatingFunc( string gameModeName, void functionref( int, array<entity>, int, entity ) func )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].titanSpawnpointRatingFunc = func
+}
+
+void function GameMode_SetScoreCompareFunc( string gameModeName, int functionref( entity, entity ) func )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].scoreCompareFunc = func
+}
+
+void function GameMode_SetDefaultScoreLimits( string gameModeName, int scoreLimit, int roundScoreLimit )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].defaultScoreLimit = scoreLimit
+ file.gameModeDefs[gameModeName].defaultRoundScoreLimit = roundScoreLimit
+}
+
+void function GameMode_SetDefaultTimeLimits( string gameModeName, int timeLimit, float roundTimeLimit )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].defaultTimeLimit = timeLimit
+ file.gameModeDefs[gameModeName].defaultRoundTimeLimit = roundTimeLimit
+}
+
+void function GameMode_AddScoreboardColumnData( string gameModeName, string title, int scoreType, int numDigits )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].scoreboardColumnTitles.append( title )
+ file.gameModeDefs[gameModeName].scoreboardColumnScoreTypes.append( scoreType )
+ file.gameModeDefs[gameModeName].scoreboardColumnNumDigits.append( numDigits )
+}
+
+void function GameMode_SetEvacEnabled( string gameModeName, bool value )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].evacEnabled = value
+}
+
+void function GameMode_SetGameEndingWarning( string gameModeName, string warning )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].gameModeEndingWarning = warning
+}
+
+void function GameMode_SetGameEndingConversation( string gameModeName, string conversation )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].gameModeEndingConversation = conversation
+}
+
+void function GameMode_SetCustomIntroAnnouncement( string gameModeName, void functionref(entity) func )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ file.gameModeDefs[gameModeName].customIntroAnnouncementFunc = func
+}
+
+/*************************************************************
+ Getters
+*************************************************************/
+
+int function GameMode_GetScoreLimit( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return GetCurrentPlaylistVarInt( "scorelimit", file.gameModeDefs[gameModeName].defaultScoreLimit )
+}
+
+int function GameMode_GetRoundScoreLimit( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return GetCurrentPlaylistVarInt( "roundscorelimit", file.gameModeDefs[gameModeName].defaultRoundScoreLimit )
+}
+
+int function GameMode_GetTimeLimit( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return GetCurrentPlaylistVarInt( "timelimit", file.gameModeDefs[gameModeName].defaultTimeLimit )
+}
+
+float function GameMode_GetRoundTimeLimit( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return GetCurrentPlaylistVarFloat( "roundtimelimit", file.gameModeDefs[gameModeName].defaultRoundTimeLimit )
+}
+
+string function GameMode_GetGameModeAnnouncement( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].gameModeAnnoucement
+}
+
+string function GameMode_GetGameModeAttackAnnouncement( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].gameModeAttackAnnoucement
+}
+
+string function GameMode_GetGameModeDefendAnnouncement( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].gameModeDefendAnnoucement
+}
+
+string function GameMode_GetDesc( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].desc_localized
+}
+
+string function GameMode_GetName( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].name_localized
+}
+
+asset function GameMode_GetIcon( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].icon
+}
+
+array<int> function GameMode_GetColor( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].color
+}
+
+string function GameMode_GetAttackDesc( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].desc_attack
+}
+
+string function GameMode_GetDefendDesc( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].desc_defend
+}
+
+void functionref( int, array<entity>, int, entity ) function GameMode_GetPilotSpawnpointsRatingFunc( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ Assert( file.gameModeDefs[gameModeName].pilotSpawnpointRatingFunc != null, "No respawn func set for " + gameModeName )
+ return file.gameModeDefs[gameModeName].pilotSpawnpointRatingFunc
+}
+
+void functionref( int, array<entity>, int, entity ) function GameMode_GetTitanSpawnpointsRatingFunc( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ Assert( file.gameModeDefs[gameModeName].titanSpawnpointRatingFunc != null, "No respawn func set for " + gameModeName )
+ return file.gameModeDefs[gameModeName].titanSpawnpointRatingFunc
+}
+
+IntFromEntityCompare function GameMode_GetScoreCompareFunc( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].scoreCompareFunc
+}
+
+bool function GameMode_GetSuddenDeathEnabled( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].suddenDeathEnabled
+}
+
+bool function GameMode_GetEvacEnabled( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].evacEnabled
+}
+
+string function GameMode_GetGameEndingWarning( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].gameModeEndingWarning
+}
+
+string function GameMode_GetGameEndingConversation( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].gameModeEndingConversation
+}
+
+array<string> function GameMode_GetScoreboardColumnTitles( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].scoreboardColumnTitles
+}
+
+array<int> function GameMode_GetScoreboardColumnScoreTypes( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].scoreboardColumnScoreTypes
+}
+
+array<int> function GameMode_GetScoreboardColumnNumDigits( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].scoreboardColumnNumDigits
+}
+
+void functionref(entity) function GameMode_GetCustomIntroAnnouncement( string gameModeName )
+{
+ Assert( gameModeName in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+ return file.gameModeDefs[gameModeName].customIntroAnnouncementFunc
+}
+
+/*************************************************************
+
+*************************************************************/
+void function GameMode_RunServerInits()
+{
+ Assert( GAMETYPE in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+
+ foreach ( initFunc in file.gameModeDefs[GAMETYPE].serverInits )
+ {
+ initFunc()
+ }
+}
+
+void function GameMode_RunClientInits()
+{
+ Assert( GAMETYPE in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+
+ foreach ( initFunc in file.gameModeDefs[GAMETYPE].clientInits )
+ {
+ initFunc()
+ }
+}
+
+void function GameMode_RunSharedInits()
+{
+ Assert( GAMETYPE in file.gameModeDefs, "No MP Gametype specified in _settings.nut" )
+
+ foreach ( initFunc in file.gameModeDefs[GAMETYPE].sharedInits )
+ {
+ initFunc()
+ }
+}
+
+void function GameMode_VerifyModes()
+{
+ foreach ( gameModeName, gameModeData in file.gameModeDefs )
+ {
+ int gameModeId = GameMode_GetGameModeId( gameModeName )
+ bool foundGameModeIdString = false
+ foreach ( idString, gameModeEnumId in eGameModes )
+ {
+ if ( gameModeEnumId != gameModeId )
+ continue
+
+ foundGameModeIdString = true
+ break
+ }
+ Assert( foundGameModeIdString, "GAMEMODE not defined properly in eGameModes!" )
+
+ GAMETYPE_TEXT[gameModeName] <- gameModeData.name_localized
+ GAMETYPE_DESC[gameModeName] <- gameModeData.desc_localized
+ GAMETYPE_ICON[gameModeName] <- gameModeData.icon
+ GAMETYPE_COLOR[gameModeName] <- gameModeData.color
+ #if CLIENT
+ PrecacheHUDMaterial( GAMETYPE_ICON[gameModeName] )
+ #endif
+ }
+}
+
+int function GameMode_GetGameModeId( string gameModeName )
+{
+ if ( gameModeName in gameModesStringToIdMap )
+ return gameModesStringToIdMap[gameModeName]
+
+ #if DEVSCRIPTS
+ if ( gameModeName in devGameModesStringToIdMap )
+ return devGameModesStringToIdMap[gameModeName]
+ #endif
+
+ Assert( false, "GAMEMODE " + gameModeName + " not defined in gameModesStringToIdMap" )
+
+ return 0
+}
+
+bool function GameMode_IsDefined( string gameModeName )
+{
+ return (gameModeName in file.gameModeDefs)
+}
+
+float function GameMode_GetLoadoutSelectTime()
+{
+ return GetCurrentPlaylistVarFloat( "pick_loadout_time", 5.0 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/sh_gamemodes_custom.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/sh_gamemodes_custom.gnut
new file mode 100644
index 00000000..51f8bf9e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/sh_gamemodes_custom.gnut
@@ -0,0 +1,20 @@
+untyped
+global function InitCustomGamemodes
+global function AddCallback_OnCustomGamemodesInit
+
+struct {
+ array<void functionref()> onCustomGamemodesInitCallbacks
+} file
+
+void function InitCustomGamemodes()
+{
+ print( "InitCustomGamemodes" )
+
+ foreach ( void functionref() callback in file.onCustomGamemodesInitCallbacks )
+ callback()
+}
+
+void function AddCallback_OnCustomGamemodesInit( void functionref() callback )
+{
+ file.onCustomGamemodesInitCallbacks.append( callback )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/item_inventory/sv_item_inventory.gnut b/Northstar.CustomServers/mod/scripts/vscripts/item_inventory/sv_item_inventory.gnut
new file mode 100644
index 00000000..ff2a4c7c
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/item_inventory/sv_item_inventory.gnut
@@ -0,0 +1,60 @@
+global function Sv_ItemInventory_Init
+global function PIN_Init
+global function SvPlayerInventory_ItemCount
+global function PlayerInventory_StartCriticalSection
+global function PlayerInventory_EndCriticalSectionForWeaponOnEndFrame
+global function PlayerInventory_CountTurrets
+global function PIN_PlayerAbility
+global function PIN_PlayerAbilityReady
+global function PIN_DamageDone
+global function PlayerInventory_RefreshEquippedState
+
+void function Sv_ItemInventory_Init()
+{
+
+}
+
+void function PIN_Init()
+{
+
+}
+
+int function SvPlayerInventory_ItemCount(entity player)
+{
+ return 0
+}
+
+void function PlayerInventory_StartCriticalSection(entity player)
+{
+
+}
+
+void function PlayerInventory_EndCriticalSectionForWeaponOnEndFrame(entity player)
+{
+
+}
+
+int function PlayerInventory_CountTurrets(entity owner)
+{
+ return 0
+}
+
+void function PIN_PlayerAbility(entity player, string name, string action, /*no idea what this type is supposed to be*/ var _0, float duration = 0)
+{
+
+}
+
+void function PIN_PlayerAbilityReady(entity player, string action)
+{
+
+}
+
+void function PIN_DamageDone(entity player, entity victim, var damageInfo)
+{
+
+}
+
+void function PlayerInventory_RefreshEquippedState( entity player )
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut
new file mode 100644
index 00000000..fd877f8c
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut
@@ -0,0 +1,37 @@
+untyped
+global function Lobby_Init
+global function Lobby_OnClientConnectionStarted
+global function Lobby_OnClientConnectionCompleted
+
+void function Lobby_Init()
+{
+ // need to prevent a crash
+ Music_Init()
+
+ if ( IsPrivateMatch() || GetCurrentPlaylistName() == "private_match" ) // IsPrivateMatch() doesn't seem to be reliable on local server start
+ PrivateLobby_Init()
+ else
+ {
+ // non-private lobby clientcommands
+ AddClientCommandCallback( "StartPrivateMatchSearch", ClientCommandCallback_StartPrivateMatchSearch )
+ }
+}
+
+void function Lobby_OnClientConnectionStarted( entity player )
+{
+
+}
+
+void function Lobby_OnClientConnectionCompleted( entity player )
+{
+ FinishClientScriptInitialization( player )
+}
+
+bool function ClientCommandCallback_StartPrivateMatchSearch( entity player, array<string> args )
+{
+ // open lobby in private match mode
+ SetCurrentPlaylist( "private_match" ) // required for private match lobby to start properly
+ ServerCommand( "changelevel mp_lobby" )
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/lobby/_private_lobby.gnut b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_private_lobby.gnut
new file mode 100644
index 00000000..60daa452
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_private_lobby.gnut
@@ -0,0 +1,194 @@
+// TODO: could probably add some checks for whether player setting stuff is player 0 to check for host, might fail in dedicated tho
+
+global function PrivateLobby_Init
+
+struct {
+ int startState
+ string map = "mp_forwardbase_kodai"
+ string mode = "aitdm"
+} file
+
+void function PrivateLobby_Init()
+{
+ print( "PrivateLobby_Init()" )
+ //ClearPlaylistVarOverrides()
+
+ AddClientCommandCallback( "PrivateMatchLaunch", ClientCommandCallback_PrivateMatchLaunch )
+ AddClientCommandCallback( "PrivateMatchSetMode", ClientCommandCallback_PrivateMatchSetMode )
+ AddClientCommandCallback( "SetCustomMap", ClientCommandCallback_SetCustomMap )
+ AddClientCommandCallback( "PrivateMatchSwitchTeams", ClientCommandCallback_PrivateMatchSwitchTeams )
+
+ AddClientCommandCallback( "PrivateMatchSetPlaylistVarOverride", ClientCommandCallback_PrivateMatchSetPlaylistVarOverride )
+ AddClientCommandCallback( "ResetMatchSettingsToDefault", ClientCommandCallback_ResetMatchSettingsToDefault )
+}
+
+bool function ClientCommandCallback_PrivateMatchLaunch( entity player, array<string> args )
+{
+ if ( file.startState == ePrivateMatchStartState.STARTING )
+ {
+ // cancel start if we're already mid-countdown
+ file.startState = ePrivateMatchStartState.READY
+ SetUIVar( level, "privatematch_starting", ePrivateMatchStartState.READY )
+ SetUIVar( level, "gameStartTime", null )
+ }
+ else
+ {
+ // start match
+ file.startState = ePrivateMatchStartState.STARTING
+ thread StartMatch()
+ }
+
+ return true
+}
+
+bool function ClientCommandCallback_PrivateMatchSetMode( entity player, array<string> args )
+{
+ if ( file.startState == ePrivateMatchStartState.STARTING )
+ return true
+
+ if ( args.len() != 1 )
+ return true
+
+ // todo: need to verify this value
+ file.mode = args[0]
+ //GameRules_SetGameMode( args[0] ) // can't do this here due to out of sync errors with new clients
+
+ RefreshPlayerTeams()
+
+ SetUIVar( level, "privatematch_mode", GetPrivateMatchModeIndex( args[0] ) )
+ return true
+}
+
+bool function ClientCommandCallback_SetCustomMap( entity player, array<string> args )
+{
+ if ( file.startState == ePrivateMatchStartState.STARTING )
+ return true
+
+ if ( args.len() != 1 )
+ return true
+
+ // todo: need to verify this value
+ file.map = args[0]
+
+ // todo: this should NOT be necessary, private matches should use an api to register maps in the future rather than hardcoded ids
+ // should be removed whenever possible really
+ SetUIVar( level, "privatematch_map", GetPrivateMatchMapIndex( args[0] ) )
+ return true
+}
+
+bool function ClientCommandCallback_PrivateMatchSwitchTeams( entity player, array<string> args )
+{
+ if ( file.startState == ePrivateMatchStartState.STARTING )
+ return true
+
+ // currently only support 2 teams in private matches
+ SetTeam( player, player.GetTeam() == 2 ? 3 : 2 )
+ return true
+}
+
+void function StartMatch()
+{
+ // set starting uivar
+ SetUIVar( level, "privatematch_starting", ePrivateMatchStartState.STARTING )
+
+ // start countdown
+ SetUIVar( level, "gameStartTime", Time() + 15 )
+ float countdownEndTime = Time() + 15.0
+
+ // can't use start here because we need to check stuff
+ while ( Time() < countdownEndTime )
+ {
+ // stop if the countdown's been cancelled
+ if ( file.startState != ePrivateMatchStartState.STARTING)
+ return
+
+ WaitFrame()
+ }
+
+ if ( file.mode in GAMETYPE_TEXT )
+ GameRules_SetGameMode( file.mode )
+ else
+ GameRules_SetGameMode( GetPlaylistGamemodeByIndex( file.mode, 0 ) )
+
+ try
+ {
+ // todo: not every gamemode uses the same playlist as their name! need some code to resolve these manually
+ // would be nice if the gamemode api got some tweaks to allow for registering private match gamemodes maybe
+ SetCurrentPlaylist( file.mode )
+ }
+ catch ( exception )
+ {
+ // temp
+ if ( file.mode == "speedball" )
+ SetCurrentPlaylist( "lf" )
+
+ print( "couldn't find playlist for gamemode " + file.mode )
+ }
+
+ RefreshPlayerTeams()
+
+ SetConVarBool( "ns_should_return_to_lobby", true ) // potentially temp?
+
+ // TEMP for now: start game
+ ServerCommand( "changelevel " + file.map )
+}
+
+void function RefreshPlayerTeams()
+{
+ int maxTeams = GetGamemodeVarOrUseValue( file.mode, "max_teams", "2" ).tointeger()
+ int maxPlayers = GetGamemodeVarOrUseValue( file.mode, "max_players", "12" ).tointeger()
+
+ // special case for situations where we wrongly assume ffa teams because there's 2 teams/2 players
+ if ( maxPlayers == maxTeams && maxTeams > 2 )
+ {
+ array<entity> players = GetPlayerArray()
+ for ( int i = 0; i < players.len(); i++ )
+ SetTeam( players[ i ], i + 7 ) // 7 is the lowest ffa team
+ }
+ else
+ {
+ bool lastSetMilitia = false
+ foreach ( entity player in GetPlayerArray() )
+ {
+ if ( player.GetTeam() == TEAM_MILITIA || player.GetTeam() == TEAM_IMC )
+ continue
+
+ if ( lastSetMilitia ) // ensure roughly evenish distribution
+ SetTeam( player, TEAM_IMC )
+ else
+ SetTeam( player, TEAM_MILITIA )
+
+ lastSetMilitia = !lastSetMilitia
+ }
+ }
+}
+
+bool function ClientCommandCallback_PrivateMatchSetPlaylistVarOverride( entity player, array<string> args )
+{
+ if ( args.len() < 2 )
+ return true
+
+ bool found = false
+ foreach ( string category in GetPrivateMatchSettingCategories() )
+ {
+ foreach ( CustomMatchSettingContainer setting in GetPrivateMatchCustomSettingsForCategory( category ) )
+ {
+ if ( args[ 0 ] == setting.playlistVar )
+ {
+ found = true
+ break
+ }
+ }
+ }
+
+ if ( found )
+ SetPlaylistVarOverride( args[0], args[1] )
+
+ return true
+}
+
+bool function ClientCommandCallback_ResetMatchSettingsToDefault( entity player, array<string> args )
+{
+ ClearPlaylistVarOverrides()
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_lobby.gnut b/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_lobby.gnut
new file mode 100644
index 00000000..2c02ebdc
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_lobby.gnut
@@ -0,0 +1,495 @@
+globalize_all_functions
+
+const string PRIVATE_MATCH_PLAYLIST = "private_match"
+
+global struct CustomMatchSettingContainer
+{
+ string playlistVar
+ string defaultValue
+ string localizedName
+ bool isEnumSetting
+
+ // enum setting
+ array< string > enumNames
+ array< string > enumValues
+ //table< string, string > enumValuePairs
+}
+
+struct {
+ array<string> modes = [ // default modes in vanilla
+ "aitdm",
+ "tdm",
+ "cp",
+ "at",
+ "ctf",
+ "lts",
+ "ps",
+ "speedball",
+ "mfd",
+ "ttdm",
+ "fd_easy",
+ "fd_normal",
+ "fd_hard",
+ "fd_master",
+ "fd_insane"
+ ]
+
+ array<string> maps = [ // default maps in vanilla
+ "mp_forwardbase_kodai",
+ "mp_grave",
+ "mp_homestead",
+ "mp_thaw",
+ "mp_black_water_canal",
+ "mp_eden",
+ "mp_drydock",
+ "mp_crashsite3",
+ "mp_complex3",
+ "mp_angel_city",
+ "mp_colony02",
+ "mp_glitch",
+ "mp_relic02",
+ "mp_wargames",
+ "mp_rise",
+ "mp_lf_stacks",
+ "mp_lf_deck",
+ "mp_lf_meadow",
+ "mp_lf_traffic",
+ "mp_lf_township",
+ "mp_lf_uma"
+ ]
+
+ table< string, array< CustomMatchSettingContainer > > customMatchSettingsByCategory // we set these up in sh_private_lobby_modes_init
+} file
+
+
+void function AddPrivateMatchMode( string mode )
+{
+ if ( !file.modes.contains( mode ) )
+ file.modes.append( mode )
+
+ #if CLIENT
+ // call this on ui too so the client and ui states are the same
+ RunUIScript( "AddPrivateMatchMode", mode )
+ #endif
+}
+
+void function AddPrivateMatchMap( string map )
+{
+ if ( !file.maps.contains( map ) )
+ file.maps.append( map )
+
+ #if CLIENT
+ // call this on ui too so the client and ui states are the same
+ RunUIScript( "AddPrivateMatchMap", map )
+ #endif
+}
+
+
+void function AddPrivateMatchModeSettingArbitrary( string category, string playlistVar, string defaultValue, string localizedName = "" )
+{
+ if ( localizedName == "" )
+ localizedName = "#" + playlistVar
+
+ if ( !( category in file.customMatchSettingsByCategory ) )
+ file.customMatchSettingsByCategory[ category ] <- []
+
+ bool found = false
+ foreach ( CustomMatchSettingContainer setting in file.customMatchSettingsByCategory[ category ] )
+ {
+ if ( setting.playlistVar == playlistVar )
+ {
+ found = true
+ break
+ }
+ }
+
+ if ( !found )
+ {
+ CustomMatchSettingContainer setting
+ setting.playlistVar = playlistVar
+ setting.defaultValue = defaultValue
+ setting.localizedName = localizedName
+ setting.isEnumSetting = false
+
+ file.customMatchSettingsByCategory[ category ].append( setting )
+ }
+
+ #if CLIENT
+ // call this on ui too so the client and ui states are the same
+ RunUIScript( "AddPrivateMatchModeSettingArbitrary", category, playlistVar, defaultValue, localizedName )
+ #endif
+}
+
+void function AddPrivateMatchModeSettingEnum( string category, string playlistVar, array< string > enums, string defaultValue, string localizedName = "" )
+{
+ table< string, string > pairs
+ for ( int i = 0; i < enums.len(); i++ )
+ pairs[ enums[ i ] ] <- i.tostring()
+
+ AddPrivateMatchModeSettingEnumEx( category, playlistVar, pairs, defaultValue, localizedName )
+}
+
+void function AddPrivateMatchModeSettingEnumUIHack( string category, string playlistVar, string serializedEnumPairs, string defaultValue, string localizedName )
+{
+ // this fucking sucks, but RunUIScript won't take tables, so we serialize to a string
+ // we use \n as a delimeter and basically serialize to an array
+ array< string > serializedArray = split( serializedEnumPairs, "\n" )
+ table< string, string > enumPairs
+
+ for ( int i = 0; i < serializedArray.len(); i += 2 )
+ enumPairs[ serializedArray[ i ] ] <- serializedArray[ i + 1 ]
+
+ AddPrivateMatchModeSettingEnumEx( category, playlistVar, enumPairs, defaultValue, localizedName )
+}
+
+void function AddPrivateMatchModeSettingEnumEx( string category, string playlistVar, table< string, string > enumPairs, string defaultValue, string localizedName = "" )
+{
+ if ( localizedName == "" )
+ localizedName = "#" + playlistVar
+
+ if ( !( category in file.customMatchSettingsByCategory ) )
+ file.customMatchSettingsByCategory[ category ] <- []
+
+ bool found = false
+ foreach ( CustomMatchSettingContainer setting in file.customMatchSettingsByCategory[ category ] )
+ {
+ if ( setting.playlistVar == playlistVar )
+ {
+ found = true
+ break
+ }
+ }
+
+ if ( !found )
+ {
+ CustomMatchSettingContainer setting
+ setting.playlistVar = playlistVar
+ setting.defaultValue = defaultValue
+ setting.localizedName = localizedName
+ setting.isEnumSetting = true
+ //setting.enumValuePairs = enumPairs
+
+ foreach ( string name, string value in enumPairs )
+ {
+ setting.enumNames.append( name )
+ setting.enumValues.append( value )
+ }
+
+ file.customMatchSettingsByCategory[ category ].append( setting )
+ }
+
+ #if CLIENT
+ // call this on ui too so the client and ui states are the same
+ // note: RunUIScript can't take tables, so manually serialize ( sucks, but just how it is ), using \n as a delimeter since i dont believe its ever used in vars
+ string serializedString
+ foreach ( string k, string v in enumPairs )
+ serializedString += k + "\n" + v + "\n"
+
+ RunUIScript( "AddPrivateMatchModeSettingEnumUIHack", category, playlistVar, serializedString, defaultValue, localizedName )
+ #endif
+}
+
+array< string > function GetPrivateMatchSettingCategories()
+{
+ array< string > categories
+ foreach ( string k, v in file.customMatchSettingsByCategory )
+ categories.append( k )
+
+ return categories
+}
+
+array< CustomMatchSettingContainer > function GetPrivateMatchCustomSettingsForCategory( string category )
+{
+ return file.customMatchSettingsByCategory[ category ]
+}
+
+
+array<string> function GetPrivateMatchModes()
+{
+ //array<string> modesArray
+ //
+ //int numModes = GetPlaylistGamemodesCount( PRIVATE_MATCH_PLAYLIST )
+ //for ( int modeIndex = 0; modeIndex < numModes; modeIndex++ )
+ //{
+ // modesArray.append( GetPlaylistGamemodeByIndex( PRIVATE_MATCH_PLAYLIST, modeIndex ) )
+ //}
+
+ //return modesArray
+
+ return file.modes
+}
+
+int function GetPrivateMatchModeIndex( string modeName )
+{
+ //int indexForName = 0
+ //
+ //int numModes = GetPlaylistGamemodesCount( PRIVATE_MATCH_PLAYLIST )
+ //for ( int modeIndex = 0; modeIndex < numModes; modeIndex++ )
+ //{
+ // if ( GetPlaylistGamemodeByIndex( PRIVATE_MATCH_PLAYLIST, modeIndex ) != modeName )
+ // continue
+ //
+ // indexForName = modeIndex;
+ // break
+ //}
+ //
+ //return indexForName
+
+ return file.modes.find( modeName )
+}
+
+
+array<string> function GetPrivateMatchMapsForMode( string modeName )
+{
+ //array<string> mapsArray
+ //
+ //int modeIndex = GetPrivateMatchModeIndex( modeName )
+ //int numMaps = GetPlaylistGamemodeByIndexMapsCount( PRIVATE_MATCH_PLAYLIST, modeIndex )
+ //for ( int mapIndex = 0; mapIndex < numMaps; mapIndex++ )
+ //{
+ // mapsArray.append( GetPlaylistGamemodeByIndexMapByIndex( PRIVATE_MATCH_PLAYLIST, modeIndex, mapIndex ) )
+ //}
+ //
+ //return mapsArray
+
+ array<string> maps
+
+ // use the private match playlist for this if the gamemode is in it already
+ int privatePlaylistModeIndex = GetPrivateMatchModeIndex( modeName )
+ if ( privatePlaylistModeIndex < GetPlaylistGamemodesCount( PRIVATE_MATCH_PLAYLIST ) )
+ {
+ for ( int i = 0; i < GetPlaylistGamemodeByIndexMapsCount( PRIVATE_MATCH_PLAYLIST, privatePlaylistModeIndex ); i++ )
+ maps.append( GetPlaylistGamemodeByIndexMapByIndex( PRIVATE_MATCH_PLAYLIST, privatePlaylistModeIndex, i ) )
+ }
+ else
+ {
+ int numMaps = GetPlaylistGamemodeByIndexMapsCount( modeName, 0 )
+ for ( int i = 0; i < numMaps; i++ )
+ maps.append( GetPlaylistGamemodeByIndexMapByIndex( modeName, 0, i ) )
+ }
+
+ return maps
+}
+
+// never called
+/*array<string> function GetPrivateMatchModesForMap( string mapName )
+{
+ array<string> modesArray
+
+ int numModes = GetPlaylistGamemodesCount( PRIVATE_MATCH_PLAYLIST )
+ for ( int modeIndex = 0; modeIndex < numModes; modeIndex++ )
+ {
+ int numMaps = GetPlaylistGamemodeByIndexMapsCount( PRIVATE_MATCH_PLAYLIST, modeIndex )
+ for ( int mapIndex = 0; mapIndex < numMaps; mapIndex++ )
+ {
+ if ( GetPlaylistGamemodeByIndexMapByIndex( PRIVATE_MATCH_PLAYLIST, modeIndex, mapIndex ) != mapName )
+ continue
+
+ modesArray.append( GetPlaylistGamemodeByIndex( PRIVATE_MATCH_PLAYLIST, modeIndex ) )
+ }
+ }
+
+ return modesArray
+}*/
+
+
+string function GetPrivateMatchMapForIndex( int index )
+{
+ array<string> mapsArray = GetPrivateMatchMaps()
+
+ if ( index >= mapsArray.len() || index < 0 )
+ return ""
+
+ return mapsArray[index]
+}
+
+string function GetPrivateMatchModeForIndex( int index )
+{
+ array<string> modesArray = GetPrivateMatchModes()
+
+ if ( index >= modesArray.len() || index < 0 )
+ return ""
+
+ return modesArray[index]
+}
+
+int function GetPrivateMatchMapIndex( string mapName )
+{
+ array<string> mapsArray = GetPrivateMatchMaps()
+ for ( int index = 0; index < mapsArray.len(); index++ )
+ {
+ if ( mapsArray[index] == mapName )
+ return index
+ }
+
+ return 0
+}
+/*
+int function GetPrivateMatchModeIndex( string modeName )
+{
+ array<string> modesArray = GetPrivateMatchModes()
+ for ( int index = 0; index < modesArray.len(); index++ )
+ {
+ if ( modesArray[index] == modeName )
+ return index
+ }
+
+ return 0
+}
+*/
+
+array<string> function GetPrivateMatchMaps()
+{
+ //array<string> mapsArray
+ //
+ //int numModes = GetPlaylistGamemodesCount( PRIVATE_MATCH_PLAYLIST )
+ //for ( int modeIndex = 0; modeIndex < numModes; modeIndex++ )
+ //{
+ // int numMaps = GetPlaylistGamemodeByIndexMapsCount( PRIVATE_MATCH_PLAYLIST, modeIndex )
+ // for ( int mapIndex = 0; mapIndex < numMaps; mapIndex++ )
+ // {
+ // string mapName = GetPlaylistGamemodeByIndexMapByIndex( PRIVATE_MATCH_PLAYLIST, modeIndex, mapIndex )
+ // if ( mapsArray.contains( mapName ) )
+ // continue
+ //
+ // mapsArray.append( mapName )
+ // }
+ //}
+ //
+ //return mapsArray
+
+ return file.maps
+}
+
+
+
+array<string> function GetPlaylistMaps( string playlistName )
+{
+ array<string> mapsArray
+
+ int numModes = GetPlaylistGamemodesCount( playlistName )
+ for ( int modeIndex = 0; modeIndex < numModes; modeIndex++ )
+ {
+ int numMaps = GetPlaylistGamemodeByIndexMapsCount( playlistName, modeIndex )
+ for ( int mapIndex = 0; mapIndex < numMaps; mapIndex++ )
+ {
+ string mapName = GetPlaylistGamemodeByIndexMapByIndex( playlistName, modeIndex, mapIndex )
+ if ( mapsArray.contains( mapName ) )
+ continue
+
+ mapsArray.append( mapName )
+ }
+ }
+
+ return mapsArray
+}
+
+
+bool function MapSettings_SupportsTitans( string mapName )
+{
+ if ( mapName.find( "mp_lf_") != null )
+ return false
+
+ if ( mapName.find( "coliseum" ) != null )
+ return false;
+
+ return true
+}
+
+bool function MapSettings_SupportsAI( string mapName )
+{
+ if ( mapName.find( "mp_lf_") != null )
+ return false
+
+ if ( mapName.find( "coliseum" ) != null )
+ return false;
+
+ return true
+}
+
+
+bool function ModeSettings_RequiresTitans( string modeName )
+{
+ switch ( modeName )
+ {
+ case "lts":
+ return true
+ }
+
+ return false
+}
+
+bool function ModeSettings_RequiresAI( string modeName )
+{
+ switch ( modeName )
+ {
+ case "aitdm":
+ case "at":
+ return true
+ }
+
+ if ( modeName.find( "fd" ) == 0 ) // bob edit: unsure if this keeps vanilla compatibility, but just make sure fd modes are counted as requiring ai
+ return true
+
+ return false
+}
+
+#if !CLIENT
+string function PrivateMatch_GetSelectedMap()
+{
+ var mapIndex = level.ui.privatematch_map
+ string mapName = GetPrivateMatchMapForIndex( expect int(mapIndex) )
+
+ return mapName
+}
+
+
+string function PrivateMatch_GetSelectedMode()
+{
+ var modeIndex = level.ui.privatematch_mode
+ string modeName = GetPrivateMatchModeForIndex( expect int(modeIndex) )
+
+ return modeName
+}
+#endif
+
+bool function PrivateMatch_IsValidMapModeCombo( string mapName, string modeName )
+{
+ array<string> mapsForMode = GetPrivateMatchMapsForMode( modeName )
+
+ return mapsForMode.contains( mapName )
+}
+
+// end private match stuff
+
+int function Player_GetMaxMatchmakingDelay( entity player )
+{
+ // return GetCurrentPlaylistVarInt( "matchmaking_delay", 0 )
+ return 300
+}
+
+int function Player_GetRemainingMatchmakingDelay( entity player )
+{
+ int lastLeaveTime = player.GetPersistentVarAsInt( PERSISTENCE_LAST_LEAVE_TIME )
+
+ return Player_GetMaxMatchmakingDelay( player ) - (GetCurrentTimeForPersistence() - lastLeaveTime)
+}
+
+int function Player_NextAvailableMatchmakingTime( entity player )
+{
+ #if MP
+ int lastLeaveTime = player.GetPersistentVarAsInt( PERSISTENCE_LAST_LEAVE_TIME )
+ if ( GetCurrentTimeForPersistence() - lastLeaveTime < Player_GetMaxMatchmakingDelay( player ) )
+ {
+ return Player_GetRemainingMatchmakingDelay( player )
+ }
+ #endif
+
+ return 0
+}
+
+int function GetCurrentTimeForPersistence()
+{
+ // Returns the unix timestap offset to the timezone we want to use
+ return GetUnixTimestamp() + DAILY_RESET_TIME_ZONE_OFFSET * SECONDS_PER_HOUR
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_private_lobby_modes_init.gnut b/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_private_lobby_modes_init.gnut
new file mode 100644
index 00000000..41806e16
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_private_lobby_modes_init.gnut
@@ -0,0 +1,55 @@
+global function PrivateMatchModesInit
+
+void function PrivateMatchModesInit()
+{
+ // match settings
+ // super temp: do localisation strings later
+ AddPrivateMatchModeSettingArbitrary( "Match", "scorelimit", "5" ) //, "Score Limit" )
+ AddPrivateMatchModeSettingArbitrary( "Match", "roundscorelimit", "0" ) //, "Score Limit (round-based modes)" )
+ AddPrivateMatchModeSettingArbitrary( "Match", "timelimit", "12" ) //, "Time Limit" )
+ AddPrivateMatchModeSettingArbitrary( "Match", "roundtimelimit", "0" ) //, "Time Limit (round-based modes)" )
+
+ AddPrivateMatchModeSettingArbitrary( "Pilot", "pilot_health_multiplier", "1.0" )
+ AddPrivateMatchModeSettingArbitrary( "Pilot", "respawn_delay", "0.0" )
+ AddPrivateMatchModeSettingEnum( "Pilot", "boosts_enabled", [ "Default", "Disabled" ], "1" )
+ AddPrivateMatchModeSettingEnum( "Pilot", "earn_meter_pilot_overdrive", [ "Disabled", "Enabled", "Only" ], "1" )
+ AddPrivateMatchModeSettingArbitrary( "Pilot", "earn_meter_pilot_multiplier", "1.0" )
+
+ AddPrivateMatchModeSettingArbitrary( "Titan", "earn_meter_titan_multiplier", "1.0" )
+ AddPrivateMatchModeSettingEnum( "Titan", "aegis_upgrades", [ "Disabled", "Enabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Titan", "infinite_doomed_state", [ "Disabled", "Enabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Titan", "titan_shield_regen", [ "Disabled", "Enabled" ], "0" )
+
+ AddPrivateMatchModeSettingEnum( "Riff Settings", "riff_floorislava", [ "Default", "Enabled", "Disabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Riff Settings", "featured_mode_all_holopilot", [ "Disabled", "Enabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Riff Settings", "featured_mode_all_grapple", [ "Disabled", "Enabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Riff Settings", "featured_mode_all_phase", [ "Disabled", "Enabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Riff Settings", "featured_mode_all_ticks", [ "Disabled", "Enabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Riff Settings", "featured_mode_tactikill", [ "Disabled", "Enabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Riff Settings", "featured_mode_amped_tacticals", [ "Disabled", "Enabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Riff Settings", "featured_mode_rocket_arena", [ "Disabled", "Enabled" ], "0" )
+ AddPrivateMatchModeSettingEnum( "Riff Settings", "featured_mode_shotguns_snipers", [ "Disabled", "Enabled" ], "0" )
+
+ // gamemode settings
+ AddPrivateMatchModeSettingEnum( "#GAMEMODE_cp", "amped_capture_points", [ "Disabled", "Enabled" ], "0" )
+
+ AddPrivateMatchModeSettingEnum( "#GAMEMODE_coliseum", "coliseum_loadouts_enabled", [ "Disabled", "Enabled" ], "1" )
+
+
+ // modes
+ AddPrivateMatchMode( "ffa" )
+ AddPrivateMatchMode( "fra" )
+ AddPrivateMatchMode( "coliseum" )
+
+ // playlists
+ AddPrivateMatchMode( "attdm" )
+ AddPrivateMatchMode( "turbo_ttdm" )
+ AddPrivateMatchMode( "alts" )
+ AddPrivateMatchMode( "turbo_lts" )
+ AddPrivateMatchMode( "rocket_lf" )
+ AddPrivateMatchMode( "holopilot_lf" )
+
+ // maps
+ AddPrivateMatchMap( "mp_coliseum" )
+ AddPrivateMatchMap( "mp_coliseum_column" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee.gnut b/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee.gnut
new file mode 100644
index 00000000..035caf9e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee.gnut
@@ -0,0 +1,89 @@
+global function Melee_Init
+
+//global function CodeCallback_NPCMeleeChargedPlayerOrNPC
+global function CodeCallback_OnMeleeKilled
+global function EnablePlantingOnEntity
+
+void function Melee_Init()
+{
+ MeleeShared_Init()
+}
+
+//File is pretty sparse for now. In all honesty a lot of existing functionality in _melee_shared should
+//belong here instead, but we'll wait until we try to do prediction (which requires running the same code
+//on client and server) before we try to split up functionality in the different script files any better.
+
+/*
+void function CodeCallback_NPCMeleeChargedPlayerOrNPC( entity ent, var damageInfo )
+{
+ vector damageForce = DamageInfo_GetDamageForce( damageInfo )
+
+ if ( DamageInfo_GetDamage( damageInfo ) > 0 )
+ {
+ vector dmgVelocity = damageForce
+ dmgVelocity.z *= 0.25
+
+ const float maxAdditionalVelocity = 1200.0
+ if ( LengthSqr( dmgVelocity ) > ( maxAdditionalVelocity * maxAdditionalVelocity ) )
+ {
+ dmgVelocity = Normalize( dmgVelocity )
+ dmgVelocity *= maxAdditionalVelocity
+ }
+
+ ent.SetVelocity( ent.GetVelocity() + dmgVelocity )
+ }
+}
+*/
+
+void function CodeCallback_OnMeleeKilled( entity target )
+{
+ if ( !IsAlive( target ) )
+ return
+
+ target.ClearInvulnerable()
+
+ int damageSourceId
+ if ( target.IsTitan() )
+ {
+ // I don't think this branch ever gets hit. Titan executions do something else.
+ damageSourceId = eDamageSourceId.titan_execution
+ }
+ else
+ {
+ damageSourceId = eDamageSourceId.human_execution
+ }
+
+ entity attacker
+ if ( IsValid( target.e.syncedMeleeAttacker ) )
+ {
+ attacker = target.e.syncedMeleeAttacker
+ }
+ else if ( IsValid( target.e.lastSyncedMeleeAttacker ) )
+ {
+ attacker = target.e.lastSyncedMeleeAttacker
+ }
+ else
+ {
+ attacker = null
+ }
+
+
+ int damageAmount = target.GetMaxHealth() + 1
+ target.TakeDamage( damageAmount , attacker, attacker, { forceKill = true, damageType = DMG_MELEE_EXECUTION, damageSourceId = damageSourceId, scriptType = DF_NO_INDICATOR } )
+}
+
+
+void function EnablePlantingOnEntity( entity titan )
+{
+ entity parentEnt = titan.GetParent()
+
+ if ( parentEnt == null )
+ return
+
+ if ( titan.GetGroundEntity() && titan.GetGroundEntity().HasPusherRootParent() )
+ return
+
+ titan.ClearParent()
+ PutEntityInSafeSpot( titan, parentEnt, null, parentEnt.GetOrigin(), titan.GetOrigin() )
+ titan.Anim_EnablePlanting()
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_rewards.gnut b/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_rewards.gnut
new file mode 100644
index 00000000..46b730d6
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_rewards.gnut
@@ -0,0 +1,74 @@
+untyped
+
+global function MeleeRewards_Init
+
+
+function MeleeRewards_Init()
+{
+ AddSyncedMeleeServerCallback( GetSyncedMeleeChooser( "human", "human" ), GiveMeleeRewards )
+}
+
+void function GiveMeleeRewards( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity enemy )
+{
+ thread GiveMeleeRewards_Internal( player, enemy )
+}
+
+enum eMeleeReward
+{
+ NONE
+ AMMO
+ MAPHACK
+}
+
+function GiveMeleeRewards_Internal( entity player, entity enemy )
+{
+ player.EndSignal( "OnDeath" )
+
+ local reward = eMeleeReward.NONE
+
+ if ( enemy.IsPlayer() )
+ reward = eMeleeReward.MAPHACK
+ else if ( enemy.IsNPC() )
+ reward = eMeleeReward.AMMO
+
+ player.WaitSignal( "SyncedMeleeComplete" )
+
+ switch ( reward )
+ {
+ case eMeleeReward.MAPHACK:
+ ExecutionGivesMapHack( player )
+ break
+ case eMeleeReward.AMMO:
+ ExecutionGivesAmmo( player )
+ break
+ default:
+ break
+ }
+}
+
+function ExecutionGivesMapHack( entity player )
+{
+ printt( "melee gave map hack!" )
+ thread ScanMinimap( player, true )
+}
+
+function ExecutionGivesAmmo( entity player )
+{
+ printt( "melee gave ammo!" )
+ local grenadeWeapon = player.GetOffhandWeapon( 0 )
+
+ if ( !IsValid( grenadeWeapon ) )
+ return
+
+ local maxAmmoClip = player.GetWeaponAmmoMaxLoaded( grenadeWeapon )
+ local remainingAmmo = player.GetWeaponAmmoLoaded( grenadeWeapon )
+
+ if ( remainingAmmo == maxAmmoClip )
+ return
+
+ local ammo = remainingAmmo + 1
+
+ grenadeWeapon.SetWeaponPrimaryClipCount( ammo )
+
+ EmitSoundOnEntity( player, "Coop_AmmoBox_AmmoRefill" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_synced_human.gnut b/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_synced_human.gnut
new file mode 100644
index 00000000..15a8aa3e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_synced_human.gnut
@@ -0,0 +1,588 @@
+untyped
+
+global function MeleeThread_PilotVsEnemy
+global function MeleeSyncedServer_Init
+
+void function MeleeSyncedServer_Init()
+{
+ RegisterSignal( "NpcDealsExecutionDamage" )
+}
+
+bool function MeleeThread_PilotVsEnemy( SyncedMelee action, entity attacker, entity target )
+{
+ // function off for reload scripts
+ return MeleeThread_PilotVsEnemyInternal( action, attacker, target )
+}
+
+bool function MeleeThread_PilotVsEnemyInternal( SyncedMelee action, entity attacker, entity target )
+{
+ Assert( IsHumanSized( target ), target + " is not human sized melee target" )
+ Assert( attacker.IsPlayer() && IsHumanSized( attacker ), attacker + " is not human sized player attacker" )
+ Assert( IsAlive( attacker ) )
+ Assert( IsAlive( target ) )
+
+ bool isAttackerRef = IsAttackerRef( action, target )
+
+ vector attackerOrigin = attacker.GetOrigin()
+ vector targetOrigin = target.GetOrigin()
+
+ attacker.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDestroy" )
+
+ if ( IsSingleplayer() )
+ {
+ if ( attacker.IsPlayer() )
+ {
+ if ( IsCloaked( attacker ) )
+ {
+ UnlockAchievement( attacker, achievements.CLOAK_TAKEDOWN )
+ }
+ }
+ }
+
+ OnThreadEnd(
+ function() : ( attacker, target, attackerOrigin, targetOrigin, action, isAttackerRef )
+ {
+ if ( IsValid( attacker ) )
+ attacker.ClearParent()
+
+ if ( IsValid( target ) )
+ target.ClearParent()
+
+
+ if ( IsValid( attacker ) )
+ {
+ attacker.PlayerMelee_SetState( PLAYER_MELEE_STATE_NONE )
+ }
+
+ // Note that the original attacker/target origins are not guarranteed to be a safe spot now because we have moving geo in the game.
+ // Whoever is the 'ref' will be in a safe position though, so we can always use the origin of the person who has been designated as the 'ref'.
+ if ( IsAlive( attacker ) )
+ {
+ if ( !isAttackerRef && IsValid( target ) )
+ {
+ PutEntityInSafeSpot( attacker, target, null, target.GetOrigin(), attacker.GetOrigin() )
+ }
+ else
+ {
+ PutEntityInSafeSpot( attacker, target, null, attacker.GetOrigin(), attacker.GetOrigin() )
+ }
+
+ }
+
+ if ( IsValid( target ) )
+ {
+ target.ClearParent()
+
+ if ( IsAlive( target ) )
+ {
+ // Note that the original target origin is not guarranteed to be a safe spot now because we have moving geo in the game now.
+ if ( isAttackerRef && IsValid( attacker ) )
+ {
+
+ PutEntityInSafeSpot( target, attacker, null, attacker.GetOrigin(), target.GetOrigin() )
+ }
+ else
+ {
+ PutEntityInSafeSpot( target, attacker, null, target.GetOrigin(), target.GetOrigin() )
+ }
+ }
+ }
+ }
+ )
+
+ thread MeleeThread_PilotVsEnemy_Attacker( action, attacker, target, isAttackerRef )
+ // target's sequence is longer
+ waitthread MeleeThread_PilotVsEnemy_Target( action, attacker, target, isAttackerRef )
+
+ attacker.Signal( "SyncedMeleeComplete" )
+ return true
+}
+
+struct PilotVsEnemyStruct
+{
+ bool clearInvulnerable = false
+ bool wasCloaked = false
+ float cloakEndTime = 0.0
+}
+
+void function DisableCloakBeforeMelee( entity player, PilotVsEnemyStruct dataStruct )
+{
+ if ( IsCloaked( player ) )
+ {
+ dataStruct.wasCloaked = true
+ dataStruct.cloakEndTime = player.GetCloakEndTime()
+ DisableCloak( player, 0.0 )
+ }
+}
+
+void function RestoreCloakAfterMelee( entity player, PilotVsEnemyStruct dataStruct )
+{
+ if ( !IsAlive( player ) )
+ return
+
+ if ( !dataStruct.wasCloaked )
+ return
+
+ float remainingCloakDuration = max( 0.0, dataStruct.cloakEndTime - Time() )
+ if ( remainingCloakDuration > CLOAK_FADE_IN ) //Has to be higher than fade in duration, otherwise will cloak forever
+ EnableCloak( player, remainingCloakDuration, CLOAK_FADE_IN )
+}
+
+void function HandleCloakExecutionWithCloakedAttacker( entity player, PilotVsEnemyStruct dataStruct, SyncedMelee action )
+{
+ if ( !IsCloaked( player ) )
+ return //No need to run DisableCloakBeforeMelee() either
+
+ float attackerSequenceEndTime = Time() + player.GetSequenceDuration( action.attackerAnimation3p )
+ float scheduledCloakEndTime = player.GetCloakEndTime()
+
+ //printt( "attackerSequenceEndTime: " + attackerSequenceEndTime + ", scheduledCloakEndTime: " + scheduledCloakEndTime )
+
+ if ( scheduledCloakEndTime > attackerSequenceEndTime )
+ {
+ //printt( "Cloak ability lasts longer than execution sequence, just doing DisableCloakBeforeMelee" )
+ player.SetCloakFlicker( 0.0, 0.0 ) //Turn off flicker; this is normally not a problem for other executions since cloak is turned off for the entirety of those executions
+ DisableCloakBeforeMelee( player, dataStruct )
+ }
+ else
+ {
+ //Cloak would normally run out during the animation of this execution, which is disruptive to the presentation of cloak animation, so just stop cloak now for good and prevent it from coming back.
+ //printt( "Cloak ability is shorter than execution sequence, DisableCloak now and stop it from coming back" )
+ dataStruct.wasCloaked = true //Have to do this to mark player was cloaked during start of execution, so we can track the stat correctly
+ dataStruct.cloakEndTime = Time()
+ DisableCloak( player, 0.0 )
+ player.Signal( "KillHandleCloakEnd" )
+ }
+}
+
+
+void function MeleeThread_PilotVsEnemy_Attacker( SyncedMelee action, entity attacker, entity target, bool isAttackerRef )
+{
+ attacker.EndSignal( "OnAnimationDone" )
+ attacker.EndSignal( "OnAnimationInterrupted" )
+ attacker.EndSignal( "OnDeath" )
+ attacker.EndSignal( "ScriptAnimStop" )
+
+ attacker.EndSignal( "OnDestroy" )
+ Assert( IsValid( target ) )
+ target.EndSignal( "OnDestroy" )
+
+
+ foreach ( AnimEventData animEventData in action.attacker3pAnimEvents )
+ {
+ AddAnimEvent( attacker, animEventData.eventName, animEventData.callback, animEventData.optionalVar )
+ }
+ AddAnimEvent( attacker, "synced_melee_enable_planting", EnablePlantingOnEntity )
+
+ PilotVsEnemyStruct dataStruct
+ OnThreadEnd(
+ function() : ( attacker, target, action, dataStruct )
+ {
+ if ( IsValid( attacker ) )
+ {
+ DeleteAnimEvent( attacker, "synced_melee_enable_planting" )
+
+ if ( dataStruct.clearInvulnerable )
+ {
+ attacker.ClearInvulnerable()
+ }
+
+ if ( attacker.IsPlayer() )
+ {
+ attacker.PlayerMelee_ExecutionEndAttacker()
+ ClearPlayerAnimViewEntity( attacker )
+ DeployAndEnableWeapons( attacker )
+
+ RestoreCloakAfterMelee( attacker, dataStruct )
+ #if MP
+ IncrementStatForPilotExecutionWhileCloaked( attacker, target, dataStruct )
+ #endif
+ }
+
+ foreach ( AnimEventData animEventData in action.attacker3pAnimEvents )
+ {
+ DeleteAnimEvent( attacker, animEventData.eventName )
+ }
+ }
+
+ if ( !IsAlive( attacker ) )
+ attacker.Anim_Stop()
+ }
+ )
+
+ FirstPersonSequenceStruct attackerSequence
+ attackerSequence.blendTime = 0.4
+ attackerSequence.attachment = "ref"
+ attackerSequence.thirdPersonAnim = action.attackerAnimation3p
+ attackerSequence.firstPersonAnim = action.attackerAnimation1p
+ attackerSequence.thirdPersonCameraAttachments = [action.thirdPersonCameraAttachment]
+ attackerSequence.thirdPersonCameraVisibilityChecks = true
+
+ if ( isAttackerRef )
+ {
+ attackerSequence.noParent = true
+ attackerSequence.playerPushable = true
+ attackerSequence.enablePlanting = true
+ }
+ else
+ {
+ attackerSequence.useAnimatedRefAttachment = true
+ }
+
+ float duration = attacker.GetSequenceDuration( attackerSequence.thirdPersonAnim )
+
+ if ( attacker.IsPlayer() )
+ {
+ float executionEndTime = Time() + duration
+ attacker.PlayerMelee_ExecutionStartAttacker( executionEndTime )
+ attacker.Lunge_ClearTarget()
+ HolsterViewModelAndDisableWeapons( attacker )
+
+ if ( action.ref == "execution_cloak" ) //Special case for cloak execution
+ {
+ HandleCloakExecutionWithCloakedAttacker( attacker, dataStruct, action )
+ }
+ else
+ {
+ DisableCloakBeforeMelee( attacker, dataStruct )
+ }
+
+ if ( IsSingleplayer() )
+ {
+ dataStruct.clearInvulnerable = true
+ attacker.SetInvulnerable()
+ thread LowerEnemyAccuracy( attacker, duration )
+ }
+ }
+
+ if ( isAttackerRef )
+ thread FirstPersonSequence( attackerSequence, attacker )
+ else
+ thread FirstPersonSequence( attackerSequence, attacker, target )
+
+ wait duration
+}
+
+
+void function MeleeThread_PilotVsEnemy_Target( SyncedMelee action, entity attacker, entity target, bool isAttackerRef )
+{
+ attacker.EndSignal( "OnAnimationDone" )
+ attacker.EndSignal( "OnAnimationInterrupted" )
+ attacker.EndSignal( "OnDeath" )
+ attacker.EndSignal( "ScriptAnimStop" )
+
+ attacker.EndSignal( "OnDestroy" )
+ Assert( IsValid( target ) )
+ target.EndSignal( "OnDestroy" )
+
+ foreach ( AnimEventData animEventData in action.target3pAnimEvents )
+ {
+ AddAnimEvent( target, animEventData.eventName, animEventData.callback, animEventData.optionalVar )
+ }
+ AddAnimEvent( target, "synced_melee_enable_planting", EnablePlantingOnEntity )
+
+ PilotVsEnemyStruct dataStruct
+
+ OnThreadEnd(
+ function() : ( attacker, target, action, dataStruct )
+ {
+ if ( IsValid( target ) )
+ {
+ if ( target.IsNPC() && IsMultiplayer() )
+ {
+ SetForceDrawWhileParented( target, false )
+ }
+
+ TargetClearedExecuted( target )
+ DeleteAnimEvent( target, "mark_for_death" )
+ DeleteAnimEvent( target, "phase_gib" )
+
+ foreach ( AnimEventData animEventData in action.target3pAnimEvents )
+ {
+ DeleteAnimEvent( target, animEventData.eventName )
+ }
+ DeleteAnimEvent( target, "synced_melee_enable_planting" )
+
+ bool isAlive = IsAlive( target )
+
+ if ( target.IsPlayer() )
+ {
+ EnableOffhandWeapons( target )
+ if ( isAlive )
+ target.DeployWeapon()
+ }
+
+ if ( isAlive )
+ {
+ if ( target.e.markedForExecutionDeath ) //Kill off target if he already reached blackout part of melee
+ {
+ entity killCreditAttacker = null //If the attacker disconnected, we don't have a player to give credit to, that's fine. Script will not error
+ if ( IsValid( target.e.syncedMeleeAttacker ) )
+ killCreditAttacker = target.e.syncedMeleeAttacker
+ //printt( "Killing off target " + target + " because he already reached blackout part of execution!" )
+
+ int damageAmount = target.GetMaxHealth() + 1
+ target.TakeDamage( damageAmount, killCreditAttacker, killCreditAttacker, { forceKill = true, damageType = DMG_MELEE_EXECUTION, damageSourceId = eDamageSourceId.human_execution } )
+ //markedForExecutionDeath will be cleared in MarkForDeath() which sets it in the first place
+ }
+
+ if ( target.IsPlayer() )
+ RestoreCloakAfterMelee( target, dataStruct )
+ }
+
+ if ( IsValid( target.e.syncedMeleeAttacker ) )
+ {
+ if ( IsValid( target.e.lastSyncedMeleeAttacker ) )
+ {
+ target.e.lastSyncedMeleeAttacker = null
+ }
+
+ target.e.lastSyncedMeleeAttacker = target.e.syncedMeleeAttacker
+ target.e.syncedMeleeAttacker = null
+ }
+ }
+ }
+ )
+
+ TargetSetExecutedBy( target, attacker )
+
+ AddAnimEvent( target, "mark_for_death", MarkForDeath )
+ AddAnimEvent( target, "phase_gib", PhaseGib )
+
+ FirstPersonSequenceStruct targetSequence
+ targetSequence.blendTime = 0.25
+ targetSequence.attachment = "ref"
+ targetSequence.thirdPersonAnim = action.targetAnimation3p
+ targetSequence.thirdPersonCameraAttachments = [action.thirdPersonCameraAttachment]
+ targetSequence.thirdPersonCameraVisibilityChecks = true
+
+ if ( isAttackerRef )
+ {
+ targetSequence.useAnimatedRefAttachment = true
+ if ( target.IsNPC() && IsMultiplayer() )
+ {
+ SetForceDrawWhileParented( target, true )
+ }
+ }
+ else
+ {
+ targetSequence.noParent = true
+ targetSequence.playerPushable = true
+ targetSequence.enablePlanting = true
+ }
+
+
+ if ( target.IsPlayer() )
+ {
+ HolsterViewModelAndDisableWeapons( target )
+ targetSequence.firstPersonAnim = action.targetAnimation1p
+ DisableCloakBeforeMelee( target, dataStruct )
+ }
+
+ if ( attacker.IsPlayer() )
+ {
+ if ( MeleeTargetrequiresDataKnife( target ) )
+ {
+ string tag = GetTagForKnifeMeleeTarget( target )
+ thread AttachPlayerModelForDuration( attacker, DATA_KNIFE_MODEL, tag, 2.2 )
+ }
+ else if ( action.attachTag1p != "" && action.attachModel1p != $"" )
+ {
+ thread AttachPlayerModelForDuration( attacker, action.attachModel1p, action.attachTag1p, 2.2 )
+ }
+ }
+
+ if ( isAttackerRef )
+ waitthread FirstPersonSequence( targetSequence, target, attacker )
+ else
+ waitthread FirstPersonSequence( targetSequence, target )
+}
+
+#if MP
+void function IncrementStatForPilotExecutionWhileCloaked( entity attacker, entity target, PilotVsEnemyStruct dataStruct )
+{
+ if ( !IsAlive( attacker ) )
+ return
+
+ if ( IsAlive( target ) )
+ return
+
+ if ( !target.IsPlayer() )
+ return
+
+ if ( !dataStruct.wasCloaked )
+ return
+
+ IncrementPlayerDidPilotExecutionWhileCloaked( attacker ) //Kinda clumsy we have to do it here instead of where all the other kill stats are incremented. Mainly because we turn cloak off at the start of execution so you can't do it where all the other kill stats are incremented
+}
+#endif
+
+void function TargetClearedExecuted( entity target )
+{
+ target.ClearParent()
+ target.Solid()
+ if ( target.ContextAction_IsMeleeExecution() )
+ target.PlayerMelee_ExecutionEndTarget()
+ if ( target.IsPlayer() )
+ ClearPlayerAnimViewEntity( target )
+}
+
+void function TargetSetExecutedBy( entity target, entity attacker )
+{
+ //Break out of context actions like hacking control panel etc
+ if ( target.ContextAction_IsActive() )
+ target.Anim_Stop()
+
+ target.PlayerMelee_ExecutionStartTarget( attacker )
+ target.e.syncedMeleeAttacker = attacker
+ target.NotSolid()
+}
+
+bool function MeleeTargetrequiresDataKnife( entity target )
+{
+ if ( IsProwler( target ) )
+ return true
+
+ if ( IsPilotElite( target ) )
+ return true
+
+ return false
+}
+
+string function GetTagForKnifeMeleeTarget( entity target )
+{
+ Assert( MeleeTargetrequiresDataKnife( target ) )
+
+ if ( IsProwler( target ) )
+ return "PROPGUN"
+
+ if ( IsPilotElite( target ) )
+ return "KNIFE"
+
+ unreachable
+}
+
+function AttachPlayerModelForDuration( var player, asset modelName, var tag, var time )
+{
+ expect entity( player )
+
+ if ( !IsValid( player ) )
+ return
+
+ Assert( IsValid( tag ), "No tag specified for player" )
+
+ entity viewModel = player.GetFirstPersonProxy() //JFS: Defensive fix for player not having view models sometimes
+ if ( !IsValid( viewModel ) )
+ return
+
+ if ( !EntHasModelSet( viewModel ) )
+ return
+
+ entity model = CreatePropDynamic( modelName )
+ model.SetParent( viewModel, tag, false, 0.0 )
+
+ OnThreadEnd(
+ function() : ( model )
+ {
+ if ( IsValid( model ) )
+ model.Destroy()
+ }
+ )
+
+ player.EndSignal( "OnDeath" )
+
+ wait time
+}
+
+void function MarkForDeath( entity target )
+{
+ if ( target.IsNPC() )
+ {
+ //printt("Killing marked for death npc " + target )
+ //Just kill off NPC now, otherwise it will play pain animations on death
+ CodeCallback_OnMeleeKilled( target )
+ return
+ }
+
+ //printt("marking player " + target + " for death")
+ target.e.markedForExecutionDeath = true //This will kill off the player even if the execution animation is interruped from this point forward
+
+ target.EndSignal( "OnDeath" )
+
+ OnThreadEnd(
+ function() : ( target )
+ {
+ target.e.markedForExecutionDeath = false
+ }
+ )
+
+ WaitForever()
+
+}
+
+void function PhaseGib( entity target )
+{
+ if ( !IsAlive( target ) )
+ return
+
+ target.ClearInvulnerable()
+
+ entity attacker
+ if ( IsValid( target.e.syncedMeleeAttacker ) )
+ {
+ attacker = target.e.syncedMeleeAttacker
+ }
+ else if ( IsValid( target.e.lastSyncedMeleeAttacker ) )
+ {
+ attacker = target.e.lastSyncedMeleeAttacker
+ }
+ else
+ {
+ attacker = null
+ }
+
+ int damageAmount = target.GetMaxHealth() + 1
+ target.TakeDamage( damageAmount , attacker, attacker, { forceKill = true, damageType = DMG_MELEE_EXECUTION, damageSourceId = eDamageSourceId.human_execution, scriptType = DF_NO_INDICATOR | DF_GIB } )
+}
+
+
+entity function CreateSyncedMeleeRef( entity attacker, entity target, SyncedMelee action )
+{
+ entity ref = CreateMeleeScriptMoverBetweenEnts( attacker, target )
+
+ vector angles = target.GetAngles()
+ angles.x = ref.GetAngles().x
+
+ ref.SetAngles( angles )
+ if ( action.animRefPos == "attacker" )
+ ref.SetOrigin( attacker.GetOrigin() )
+ else
+ ref.SetOrigin( target.GetOrigin() )
+ return ref
+}
+
+void function ApplyGruntExecutionDamage( entity ref, entity attacker, entity target, float damageDealt )
+{
+ ref.EndSignal( "OnDestroy" )
+ attacker.EndSignal( "OnDeath" )
+ target.EndSignal( "OnDeath" )
+
+ for ( ;; )
+ {
+ table results = attacker.WaitSignal( "NpcDealsExecutionDamage" )
+ float damage
+ switch ( results.parm )
+ {
+ case "lethal":
+ damage = float( target.GetMaxHealth() )
+ break
+
+ case "nonlethal":
+ damage = min( target.GetHealth() - 10, target.GetMaxHealth() * damageDealt )
+ break
+ }
+
+ target.TakeDamage( damage, attacker, attacker, { damageSourceId=eDamageSourceId.human_execution, scriptType = DF_RAGDOLL } )
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_synced_titan.gnut b/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_synced_titan.gnut
new file mode 100644
index 00000000..5c6285a9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/melee/_melee_synced_titan.gnut
@@ -0,0 +1,1543 @@
+untyped
+
+global function MeleeSyncedTitan_Init
+
+const TITANARMMODEL = $"models/weapons/arms/atlaspov.mdl"
+const TEAM_JUMPJET_DBL = $"P_team_jump_jet_DBL"
+
+enum eTitanExecutionType
+{
+ fistThroughCockpit
+ dummy //not used yet
+}
+
+struct TitanExcutionData
+{
+ string attackerAnimation3p
+ string attackerAnimation3p_vsAutoTitan
+ table<string,string> attackerAnimation3pPilot
+ table<string,string> targetAnimation3p
+ table<string,string> targetAnimation3pPilot
+ string sound_1p
+ string sound_3p
+ array<string> thirdPersonCameraAttachments
+ array<string> linkedExecutions
+}
+
+struct
+{
+ table<string, TitanExcutionData> executionData_3p
+} file
+
+int RAGDOLL_IMPACT_TABLE_IDX = -1
+
+function MeleeSyncedTitan_Init()
+{
+ RAGDOLL_IMPACT_TABLE_IDX = PrecacheImpactEffectTable( "ragdoll_human" )
+ AddSyncedMeleeServerThink( GetSyncedMeleeChooser( "titan", "titan" ), MeleeThread_TitanVsTitan )
+
+ if ( GetBugReproNum() == 129802 )
+ {
+ AddDeathCallback( "npc_titan", OnNPCTitanDeath )
+ }
+
+ PrecacheWeapon( "mp_titanweapon_salvo_rockets" )
+ PrecacheParticleSystem( TEAM_JUMPJET_DBL )
+
+ Init3pExecutions()
+}
+
+void function Init3pExecutions()
+{
+ var dataTable = GetDataTable( $"datatable/titan_executions.rpak" )
+ int numRows = GetDatatableRowCount( dataTable )
+ for ( int row=0; row<numRows; row++ )
+ {
+ TitanExcutionData data = Create_3p_ExecutionData( dataTable, row )
+ string ref = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "ref" ) )
+ file.executionData_3p[ref] <- data
+ }
+}
+
+TitanExcutionData function Create_3p_ExecutionData( var dataTable, int row )
+{
+ string attackerAnimation3p = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "attackerAnim" ) )
+ string attackerAnimation3p_vsAutoTitan = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "attackerAnimVsAutoTitan" ) )
+ string targetAnimation3p_lt = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "victimAnim_lt" ) )
+ string targetAnimation3p_md = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "victimAnim_md" ) )
+ string targetAnimation3p_hv = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "victimAnim_hv" ) )
+ string targetAnimation3pPilot_lt = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "victimAnim_pt_lt" ) )
+ string targetAnimation3pPilot_md = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "victimAnim_pt_md" ) )
+ string targetAnimation3pPilot_hv = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "victimAnim_pt_hv" ) )
+
+ string attackerAnimation3pPilot_lt = ""
+ if ( GetDataTableColumnByName( dataTable, "attackerAnim_pt_lt" ) != -1 )
+ attackerAnimation3pPilot_lt = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "attackerAnim_pt_lt" ) )
+
+ string attackerAnimation3pPilot_md = ""
+ if ( GetDataTableColumnByName( dataTable, "attackerAnim_pt_mt" ) != -1 )
+ attackerAnimation3pPilot_md = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "attackerAnim_pt_mt" ) )
+
+ string attackerAnimation3pPilot_hv = ""
+ if ( GetDataTableColumnByName( dataTable, "attackerAnim_pt_ht" ) != -1 )
+ attackerAnimation3pPilot_hv = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "attackerAnim_pt_ht" ) )
+
+ string sound_1p = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "sound_1p" ) )
+ string sound_3p = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "sound_3p" ) )
+ string camAttach = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "camAttach" ) )
+
+ array<string> camAttachments = split( camAttach, " " )
+
+ array<string> linkedExecutionArray = SplitAndStripStringArray( GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "linkedExecutions" ) ) )
+
+ TitanExcutionData data
+ data.attackerAnimation3p = attackerAnimation3p
+ data.attackerAnimation3p_vsAutoTitan = attackerAnimation3p_vsAutoTitan
+ data.targetAnimation3p[ "stryder" ] <- targetAnimation3p_lt
+ data.targetAnimation3p[ "atlas" ] <- targetAnimation3p_md
+ data.targetAnimation3p[ "ogre" ] <- targetAnimation3p_hv
+ data.targetAnimation3pPilot[ "stryder" ] <- targetAnimation3pPilot_lt
+ data.targetAnimation3pPilot[ "atlas" ] <- targetAnimation3pPilot_md
+ data.targetAnimation3pPilot[ "ogre" ] <- targetAnimation3pPilot_hv
+ data.attackerAnimation3pPilot[ "stryder" ] <- attackerAnimation3pPilot_lt
+ data.attackerAnimation3pPilot[ "atlas" ] <- attackerAnimation3pPilot_md
+ data.attackerAnimation3pPilot[ "ogre" ] <- attackerAnimation3pPilot_hv
+ data.sound_1p = sound_1p
+ data.sound_3p = sound_3p
+ data.thirdPersonCameraAttachments = camAttachments
+ data.linkedExecutions = linkedExecutionArray
+ return data
+}
+
+array<string> function SplitAndStripStringArray( string combinedString )
+{
+ array<string> stringArray = split( combinedString, "," )
+
+ foreach ( i, value in stringArray )
+ {
+ stringArray[ i ] = strip( value )
+ }
+
+ return stringArray
+}
+
+
+struct MeleeThread_TitanVsTitanDataStruct
+{
+ bool setAttackerInvulnerable = false
+ bool setAttackerDemigod = false
+}
+
+bool function MeleeThread_TitanVsTitan( SyncedMelee action, entity attacker, entity target )
+{
+ // function off for reload scripts
+ return MeleeThread_TitanVsTitan_Internal( action, attacker, target )
+}
+
+bool function MeleeThread_TitanVsTitan_Internal( SyncedMelee action, entity attacker, entity target )
+{
+ Assert( target.IsTitan(), target + " is not Titan target" )
+ Assert( attacker.IsPlayer() && attacker.IsTitan(), attacker + " is not Titan attacker" )
+
+ #if SERVER
+ printt( "Player", attacker, "attempting to melee", target, "TitanVsTitanMelee" )
+ #endif
+
+ if ( attacker.ContextAction_IsActive() || target.ContextAction_IsActive() )
+ {
+ printt("Either attacker or target already in ContextAction! Exiting Titan Vs Titan melee attempt")
+ return false
+ }
+
+ if ( !IsAlive( attacker ) )
+ return false
+
+ if ( !IsAlive( target ) )
+ return false
+
+ void functionref( SyncedMelee action, entity attacker, entity target ) func
+ func = GetTitanSyncedMeleeFunc( attacker, target )
+ if ( func == null )
+ return false
+
+ attacker.GetTitanSoul().Signal( "OnSyncedMelee" ) //Need the signal on the soul to clean-up tether traps during synced executions.
+
+ // JFS: signals can kill things mid frame: R2DLC-311 SCRIPT ERROR: PHONE_HOME: [SERVER] Entity is null
+ if ( !IsAlive( attacker ) )
+ return false
+
+ if ( !IsAlive( target ) )
+ return false
+
+ target.GetTitanSoul().Signal( "OnSyncedMelee" )
+
+ // JFS: signals can kill things mid frame: R2DLC-311 SCRIPT ERROR: PHONE_HOME: [SERVER] Entity is null
+ if ( !IsAlive( attacker ) )
+ return false
+
+ if ( !IsAlive( target ) )
+ return false
+ //attacker.Signal( "OnSyncedMelee" )
+ //target.Signal( "OnSyncedMelee" )
+
+ MeleeThread_TitanVsTitanDataStruct dataStruct
+
+ OnThreadEnd(
+ function() : ( attacker, target, dataStruct )
+ {
+ if ( IsValid( attacker ) )
+ {
+ if ( dataStruct.setAttackerInvulnerable )
+ attacker.ClearInvulnerable()
+
+ if ( dataStruct.setAttackerDemigod )
+ DisableDemigod( attacker )
+
+ attacker.PlayerMelee_SetState( PLAYER_MELEE_STATE_NONE )
+ }
+ }
+ )
+
+ string titanSubClass = GetSoulTitanSubClass( attacker.GetTitanSoul() )
+
+ entity burnCardTarget
+ entity bossPlayer = target.GetBossPlayer()
+ if ( target.IsNPC() )
+ {
+ if ( IsValid( bossPlayer ) )
+ burnCardTarget = bossPlayer
+ }
+ else
+ {
+ burnCardTarget = target
+ }
+
+ attacker.PlayerMelee_ExecutionStartAttacker( 0 )
+ target.PlayerMelee_ExecutionStartTarget( attacker )
+
+ attacker.Lunge_ClearTarget()
+
+ ForceTitanSustainedDischargeEnd( target )
+
+ #if TITAN_EXECUTION_ATTACKER_IS_INVULNERABLE
+ dataStruct.setAttackerInvulnerable = true
+ attacker.SetInvulnerable()
+ #else
+ dataStruct.setAttackerDemigod = true
+ EnableDemigod( attacker )
+ #endif
+
+ waitthread func( action, attacker, target )
+
+ if ( !IsValid( attacker ) )
+ return true
+
+ attacker.Signal( "SyncedMeleeComplete" )
+ #if MP
+ if ( attacker.IsPlayer() )
+ AddPlayerScore( attacker, "Execution" )
+ #endif
+ return true
+}
+
+void functionref( SyncedMelee action, entity attacker, entity target ) function GetTitanSyncedMeleeFunc( entity attacker, entity target )
+{
+ if ( GetCurrentPlaylistVarInt( "titan_executions_always_short", 0 ) != 0 )
+ return MeleeThread_AtlasVsTitanShort
+
+ entity soul = attacker.GetTitanSoul()
+ #if SP
+ TitanLoadoutDef loadout = GetTitanLoadoutForCurrentMap()
+ #else
+ TitanLoadoutDef loadout = soul.soul.titanLoadout // GetActiveTitanLoadout( attacker )
+ #endif
+ string executionRef = loadout.titanExecution
+ if ( SoulHasPassive( soul, ePassives.PAS_VANGUARD_COREMETER ) )
+ executionRef = "execution_vanguard_kit"
+
+ if ( executionRef in file.executionData_3p )
+ return TitanVsTitan_3p
+
+ if ( target.IsNPC() )
+ {
+ entity bossPlayer = target.GetBossPlayer()
+ if ( IsValid( bossPlayer ) || !IsVDUTitan( target ) )
+ return MeleeThread_AtlasVsTitanShort
+ }
+
+ string attackerType = GetSoulTitanSubClass( soul )
+
+ switch ( attackerType )
+ {
+ case "stryder":
+ return MeleeThread_StyderVsTitan
+
+ case "ogre":
+ return MeleeThread_OgreVsTitan
+
+ case "atlas":
+ case "buddy":
+ return MeleeThread_AtlasVsTitan
+ }
+
+ return null
+}
+
+void function MeleeThread_AtlasVsTitanShort( SyncedMelee action, entity attacker, entity target )
+{
+ if ( !IsAlive( attacker ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ string attackerAnimation1p = "atpov_melee_sync_frontkill_autotitan"
+ string attackerAnimation3p = "at_melee_sync_frontkill_autotitan"
+ string targetAnimation3p = "at_melee_sync_frontdeath_autotitan"
+
+ target.Signal( "TitanStopsThinking" ) // in future, need to make titan scripted anims co-exist better and not require gotcha stuff like this -Mackey
+
+ local e = {}
+ e.attackerViewBody <- null
+
+ e.attackerStartOrg <- attacker.GetOrigin()
+
+ entity ref = CreateMeleeScriptMoverBetweenEnts( attacker, target )
+
+ FirstPersonSequenceStruct attackerSequence
+ attackerSequence.blendTime = 0.25
+ attackerSequence.attachment = "ref"
+
+ FirstPersonSequenceStruct targetSequence = clone attackerSequence
+
+ attackerSequence.thirdPersonAnim = attackerAnimation3p
+ // attackerSequence.thirdPersonAnimIdle = "at_melee_sync_frontkill_end_idle"
+
+ attackerSequence.firstPersonAnim = attackerAnimation1p
+ targetSequence.thirdPersonAnim = targetAnimation3p
+ targetSequence.blendTime = 0.25
+
+ target.e.syncedMeleeAttacker = attacker
+
+ // attacker.SetInvulnerable()
+ target.SetInvulnerable() //HACK: Have to SetInvulnerable first before attacker holsters weapon, because if the attacker is vortexing, holster will release bullets caught and kill off the victim if low enough health
+
+ //HACK! This function was originally for NPCs only, but now that it is being used for players, we need to holster their weapon
+ if ( target.IsPlayer() )
+ HolsterAndDisableWeapons( target )
+
+ if ( ShouldHolsterWeaponForSyncedMelee( attacker ) )
+ HolsterAndDisableWeapons( attacker )
+
+ local attackerViewBody
+
+ // needs shortened verions
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "Titan_1p_Sync_Melee_vs_AutoTitan", "Titan_3p_Sync_Melee_vs_AutoTitan", attacker, attacker )
+
+ local soul = target.GetTitanSoul()
+ soul.SetInvalidHealthBarEnt( true )
+
+ AddAnimEvent( target, "rider_rodeo_over", ForceTitanRodeoToEnd )
+
+ target.SetInvulnerable() //Setting target of execution as invulnerable to prevent them dying mid-way
+
+ OnThreadEnd(
+ function() : ( ref, attacker, target, e )
+ {
+ if ( IsValid( ref ) )
+ {
+ if ( IsValid( attacker ) )
+ attacker.ClearParent()
+
+ if ( IsValid( target ) )
+ target.ClearParent()
+
+ AssertNoPlayerChildren( ref )
+ ref.Destroy()
+ }
+
+ if ( IsValid( attacker ) )
+ {
+ //attacker.ClearInvulnerable()
+ attacker.UnforceStand()
+ attacker.ClearParent()
+ ClearPlayerAnimViewEntity( attacker )
+ DeployAndEnableWeapons( attacker )
+ attacker.PlayerMelee_ExecutionEndAttacker()
+
+ if ( IsAlive( attacker ) )
+ {
+ // if we got into solid, teleport back to safe place
+ if ( !PutEntityInSafeSpot( attacker, null, null, expect vector( e.attackerStartOrg ), attacker.GetOrigin() ) )
+ {
+ printt( "PutEntityInSafeSpot failed, putting him back at the start origin" )
+ attacker.SetOrigin( expect vector( e.attackerStartOrg ) )
+ }
+
+ }
+ }
+
+ if ( IsValid( target ) )
+ {
+ if ( !target.IsNPC() )
+ {
+ target.PlayerMelee_ExecutionEndTarget()
+ ClearPlayerAnimViewEntity( target )
+ DeployAndEnableWeapons( target )
+ }
+
+
+ if ( IsAlive( target ) )
+ {
+ local attack = attacker
+ if ( !IsValid( attack ) )
+ attack = null
+
+ target.Die( attack, attack, { scriptType = 0, damageSourceId = eDamageSourceId.titan_execution } )
+ }
+
+ target.e.syncedMeleeAttacker = null
+
+ if ( HasAnimEvent( target, "rider_rodeo_over" ) )
+ DeleteAnimEvent( target, "rider_rodeo_over" )
+ }
+ }
+ )
+
+ thread FirstPersonSequence( targetSequence, target, ref )
+ waitthread FirstPersonSequence( attackerSequence, attacker, ref )
+
+ //wait ( 50.0 / 30.0 ) // 37 frames in
+}
+
+
+void function MeleeThread_StyderVsTitan( SyncedMelee action, entity attacker, entity target )
+{
+ table e
+ e.gib <- true
+ e.attackerAnimation1p <- "strypov_melee_sync_frontkill"
+ e.attackerAnimation3p <- "stry_melee_sync_frontkill"
+ e.targetAnimation3p <- "stry_melee_sync_frontdeath"
+ e.targetPilotAnimationForAttacker <- "pt_stry_melee_sync_front_pilotkill_1st"
+ e.targetPilotAnimationForObserver <- "pt_stry_melee_sync_front_pilotkill_3rd"
+ e.targetPilotAnimationForObserver1st <- "ptpov_stry_tvtmelee_targetdeath"
+ e.TitanSpecific1pSyncMeleeSound <- "Stryder_1p_Sync_Melee"
+ e.TitanSpecific3pSyncMeleeSound <- "Stryder_3p_Sync_Melee"
+
+ MeleeThread_TitanRipsPilot( e, action, attacker, target )
+}
+
+void function MeleeThread_AtlasVsTitan( SyncedMelee action, entity attacker, entity target )
+{
+ table e
+ e.gib <- false
+ e.attackerAnimation1p <- "atpov_melee_sync_frontkill"
+ e.attackerAnimation3p <- "at_melee_sync_frontkill"
+ e.targetAnimation3p <- "at_melee_sync_frontdeath"
+ e.targetPilotAnimationForAttacker <- "pt_melee_sync_front_pilotkill_1st"
+ e.targetPilotAnimationForObserver <- "pt_melee_sync_front_pilotkill_3rd"
+ e.targetPilotAnimationForObserver1st <- "ptpov_tvtmelee_targetdeath"
+ e.TitanSpecific1pSyncMeleeSound <- "Atlas_1p_Sync_Melee"
+ e.TitanSpecific3pSyncMeleeSound <- "Atlas_3p_Sync_Melee"
+
+ MeleeThread_TitanRipsPilot( e, action, attacker, target )
+}
+
+function MeleeThread_TitanRipsPilot( table e, SyncedMelee action, entity attacker, entity target )
+{
+ e.attackerViewBody <- null
+ e.attacker <- attacker
+ e.attackerStartOrg <- attacker.GetOrigin()
+
+ entity ref = CreateMeleeScriptMoverBetweenEnts( attacker, target )
+
+ FirstPersonSequenceStruct attackerSequence
+ attackerSequence.blendTime = 0.25
+ attackerSequence.attachment = "ref"
+
+ FirstPersonSequenceStruct targetSequence = clone attackerSequence
+
+ attackerSequence.thirdPersonAnim = expect string ( e.attackerAnimation3p )
+ // attackerSequence.thirdPersonAnimIdle = "at_melee_sync_frontkill_end_idle"
+
+ attackerSequence.firstPersonAnim = expect string( e.attackerAnimation1p )
+ targetSequence.thirdPersonAnim = expect string ( e.targetAnimation3p )
+ targetSequence.blendTime = 0.25
+
+ target.e.syncedMeleeAttacker = attacker
+
+ // attacker.SetInvulnerable()
+ target.SetInvulnerable() //HACK: Have to SetInvulnerable first before attacker holsters weapon, because if the attacker is vortexing, holster will release bullets caught and kill off the victim if low enough health
+ if ( ShouldHolsterWeaponForSyncedMelee( attacker ) )
+ HolsterAndDisableWeapons( attacker )
+
+ if ( !target.IsNPC() )
+ HolsterAndDisableWeapons( target )
+
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( expect string ( e.TitanSpecific1pSyncMeleeSound ), expect string ( e.TitanSpecific3pSyncMeleeSound ), attacker, attacker )
+
+ entity attackerViewBody
+ bool targetIsPlayer = target.IsPlayer()
+
+ if ( targetIsPlayer )
+ {
+ attackerViewBody = Wallrun_CreateCopyOfPilotModel( target ) //attackerViewBody is the model of the pilot getting ripped out of the cockpit
+ }
+ else
+ {
+ attackerViewBody = CreateNpcTitanPilotModel( target )
+ }
+
+ attackerViewBody.SetOrigin( ref.GetOrigin() )
+ e.attackerViewBody = attackerViewBody
+ attackerViewBody.SetOwner( attacker )
+ attackerViewBody.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER
+ attackerViewBody.SetRagdollImpactFX( RAGDOLL_IMPACT_TABLE_IDX )
+ attackerViewBody.SetContinueAnimatingAfterRagdoll( true )
+
+ FirstPersonSequenceStruct attackerBodySequence
+ attackerBodySequence.attachment = "ref"
+ attackerBodySequence.teleport = true
+ attackerBodySequence.thirdPersonAnim = expect string ( e.targetPilotAnimationForAttacker )
+
+ FirstPersonSequenceStruct targetBodySequence
+ targetBodySequence.attachment = "ref"
+ targetBodySequence.blendTime = 0.25
+ targetBodySequence.thirdPersonAnim = expect string ( e.targetPilotAnimationForObserver )
+ targetBodySequence.firstPersonAnim = expect string ( e.targetPilotAnimationForObserver1st )
+
+
+ entity targetSoul = target.GetTitanSoul()
+ targetSoul.SetInvalidHealthBarEnt( true )
+
+ entity targetTitan
+ if ( targetIsPlayer )
+ {
+ e.oldPlayerSettings <- target.s.storedPlayerSettings
+ //target.s.storedPlayerSettings = "pilot_titan_cockpit" // Makes player have titan cockpit temporarily. Turned off to avoid having extra checks all over in script
+ targetTitan = CreateAutoTitanForPlayer_ForTitanBecomesPilot( target ) //TargetTitan is the NPC Titan that is created temporarily during execution
+ DispatchSpawn( targetTitan )
+
+ TitanBecomesPilot( target, targetTitan )
+ DisableTitanRodeo( targetTitan )
+ targetTitan.SetOwner( target )
+ targetTitan.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) //owner cant see
+ targetTitan.PlayerMelee_ExecutionStartTarget( attacker )
+ e.target <- target
+ }
+ else
+ {
+ targetTitan = target
+
+ // target is now a random dude
+ target = CreateSoldier( target.GetTeam(), Vector(0,0,0), Vector(0,0,0) )
+ DispatchSpawn( target )
+ e.target <- target
+ }
+
+
+ AddAnimEvent( targetTitan, "rider_rodeo_over", ForceTitanRodeoToEnd )
+ AddAnimEvent( targetTitan, "melee_killed_ragdoll", MeleeKilledRagdoll, attacker )
+
+ targetTitan.SetInvulnerable() //Setting target of execution as invulnerable to prevent them dying mid-way
+
+ target.SetOwner( attacker )
+ target.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) //owner cant see
+ e.targetTitan <- targetTitan
+
+ if ( GetBugReproNum() == 129802 )
+ thread OnNPCTitanSignalDeath( targetTitan )
+
+ OnThreadEnd(
+ function() : ( ref, attacker, target, targetTitan, e )
+ {
+ if ( IsValid( ref ) )
+ {
+ if ( IsValid( attacker ) )
+ {
+ attacker.ClearParent()
+ }
+ else
+ {
+ TryClearParent( attacker )
+ }
+
+ if ( IsValid( target ) )
+ {
+ target.ClearParent()
+ }
+ else
+ {
+ TryClearParent( target )
+ }
+
+ AssertNoPlayerChildren( ref )
+ ref.Kill_Deprecated_UseDestroyInstead()
+ }
+
+ if ( IsValid( attacker ) )
+ {
+ attacker.UnforceStand()
+ attacker.ClearParent()
+ ClearPlayerAnimViewEntity( attacker )
+ DeployAndEnableWeapons( attacker )
+ attacker.PlayerMelee_ExecutionEndAttacker()
+
+ if ( IsAlive( attacker ) )
+ {
+ // if we got into solid, teleport back to safe place
+ PutEntityInSafeSpot( attacker, null, null, expect vector( e.attackerStartOrg ), attacker.GetOrigin() )
+ }
+ }
+
+ if ( IsValid( target ) )
+ {
+ if ( !target.IsNPC() )
+ {
+ target.PlayerMelee_ExecutionEndTarget()
+ ClearPlayerAnimViewEntity( target )
+ DeployAndEnableWeapons( target )
+ }
+
+ if ( HasAnimEvent( target, "pink_mist" ) )
+ DeleteAnimEvent( target, "pink_mist" )
+
+ if ( IsAlive( expect entity( e.target ) ) )
+ MeleePinkMist( e )
+
+ target.e.syncedMeleeAttacker = null
+ }
+
+ if ( IsValid( e.attackerViewBody ) )
+ e.attackerViewBody.Kill_Deprecated_UseDestroyInstead()
+
+ if ( GetBugReproNum() != 129802 && IsAlive( targetTitan ) )
+ {
+ if ( IsValid( attacker ) )
+ targetTitan.Die( attacker, attacker, { scriptType = DF_MELEE, damageSourceId = eDamageSourceId.titan_execution } )
+ else
+ targetTitan.Die()
+
+ if ( GetBugReproNum() == 129815 )
+ {
+ targetTitan.SetContinueAnimatingAfterRagdoll( true )
+ targetTitan.BecomeRagdoll( Vector(0,0,0), false )
+ }
+ }
+ }
+ )
+
+ target.EndSignal( "OnRespawnPlayer" )
+
+ waitthread TitanSyncedMeleeAnimationsPlay( attackerBodySequence, attackerViewBody, ref, targetBodySequence, target, attackerSequence, attacker, targetSequence, targetTitan, e )
+}
+
+entity function CreateNpcTitanPilotModel( entity titan )
+{
+ asset modelName = GetNpcTitanPilotModel( titan )
+ return CreatePropDynamic( modelName )
+}
+
+
+
+asset function GetNpcTitanPilotModel( entity titan )
+{
+ asset modelName = TEAM_IMC_GRUNT_MODEL
+
+ #if HAS_BOSS_AI
+ if ( IsBossTitan( titan ) )
+ {
+ modelName = GetBossTitanCharacterModel( titan )
+ }
+ #endif
+
+ return modelName
+}
+
+function TitanSyncedMeleeAnimationsPlay( FirstPersonSequenceStruct attackerBodySequence, entity attackerViewBody, entity ref, FirstPersonSequenceStruct targetBodySequence, entity target, FirstPersonSequenceStruct attackerSequence, entity attacker, FirstPersonSequenceStruct targetSequence, entity targetTitan, table e )
+{
+ e.thrown <- false
+ OnThreadEnd (
+ function () : ( targetTitan, target, attacker, e )
+ {
+ // insure visibility
+ if ( IsValid( targetTitan ) )
+ targetTitan.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+
+ if ( !IsAlive( attacker ) )
+ {
+ attacker.Anim_Stop()
+
+ if ( !e.thrown && IsAlive( target ) )
+ {
+ target.Anim_Stop()
+ target.SetOwner( null )
+ target.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ if ( target.IsPlayer() )
+ {
+ ClearPlayerAnimViewEntity( target )
+ target.GetFirstPersonProxy().Anim_Stop()
+ target.SetPlayerSettings( e.oldPlayerSettings )
+ }
+
+ }
+ }
+ }
+ )
+
+ attacker.EndSignal( "OnDeath" )
+ target.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnRespawnPlayer" )
+
+ thread FirstPersonSequence( attackerBodySequence, attackerViewBody, ref )
+ if ( !target.IsPlayer() )
+ {
+ // don't do first person anims if we're not a player
+ targetBodySequence.firstPersonAnim = ""
+ targetBodySequence.firstPersonAnimIdle = ""
+ }
+
+ thread FirstPersonSequence( targetBodySequence, target, ref )
+ thread FirstPersonSequence( attackerSequence, attacker, ref )
+ thread FirstPersonSequence( targetSequence, targetTitan, ref )
+ targetTitan.Anim_AdvanceCycleEveryFrame( true )
+ local duration = attacker.GetSequenceDuration( attackerSequence.thirdPersonAnim )
+
+ if ( e.targetAnimation3p == "at_melee_sync_frontdeath" )
+ {
+ thread MeleeThrowIntoWallSplat( attacker, target, e )
+ }
+ else
+ {
+ AddAnimEvent( target, "pink_mist", MeleePinkMistAnimEvent, e )
+ }
+
+ float timer
+ string titanType = GetSoulTitanSubClass( attacker.GetTitanSoul() )
+ switch ( titanType )
+ {
+ case "stryder":
+ timer = 0.9
+ break
+ case "atlas":
+ case "buddy":
+ timer = 0.45
+ break
+ default:
+ Assert( 0, "Unknown titan type " + titanType )
+ }
+
+ wait timer
+
+ // first the victim cant see his titan, as a pilot, and then he can
+ targetTitan.SetNextThinkNow()
+ targetTitan.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ targetTitan.SetNextThinkNow()
+ wait duration - timer
+}
+
+void function MeleePinkMistAnimEvent( entity target ) //parameter isn't used, but function signature is like this because it's being called from an anim event
+{
+ table e = expect table( GetOptionalAnimEventVar( target, "pink_mist" ) )
+
+ MeleePinkMist( e )
+}
+
+void function MeleePinkMist( table e )
+{
+ entity target = expect entity( e.target )
+
+ if ( !IsAlive( target ) )
+ return
+
+ e.attackerViewBody.Dissolve( ENTITY_DISSOLVE_PINKMIST, Vector( 0, 0, 0 ), 0 )
+ if ( IsValid( e.attacker ) )
+ {
+ target.Die( e.attacker, e.attacker, { damageSourceId = eDamageSourceId.titan_execution, scriptType = DF_GIB } )
+ }
+ else
+ {
+ target.Die( e.target, target, { damageSourceId = eDamageSourceId.titan_execution, scriptType = DF_GIB } )
+ }
+
+ if ( target.IsPlayer() )
+ ClearPlayerAnimViewEntity( target )
+
+ target.ClearInvulnerable()
+}
+
+function MeleeThrowIntoWallSplat( entity attacker, entity target, e )
+{
+ OnThreadEnd(
+ function () : ( target, e )
+ {
+ if ( IsValid( target ) )
+ {
+ target.ClearParent()
+ target.Anim_Stop()
+ target.ClearInvulnerable()
+ }
+ }
+ )
+
+ target.EndSignal( "OnDeath" )
+
+ e.startOrigin <- target.GetOrigin()
+ wait 2.8
+ e.thrown = true
+
+
+ // attacker got killed? saved!
+ if ( !IsAlive( attacker ) )
+ return
+
+ local angles = attacker.GetAngles()
+ angles = AnglesCompose( angles, Vector( -15, 0, 0 ) )
+ local forward = AnglesToForward( angles )
+
+ local endPos
+ for ( ;; )
+ {
+ if ( !target.Anim_IsActive() )
+ break
+
+ local org = target.GetOrigin()
+ if ( IsAlive( attacker ) )
+ {
+ TraceResults titanPilotTrace = TraceLine( attacker.EyePosition(), org, attacker )
+
+ if ( titanPilotTrace.fraction < 1.0 )
+ {
+ endPos = titanPilotTrace.endPos
+ break
+ }
+ }
+
+
+ TraceResults result = TraceLine( org, org + forward * 200 )
+ if ( result.fraction < 1.0 )
+ {
+ wait result.fraction * 0.06
+ break
+ }
+
+ WaitFrame()
+ }
+
+ if ( endPos )
+ {
+ target.SetOrigin( endPos )
+ }
+
+ Assert( IsAlive( target ) )
+
+ target.ClearInvulnerable()
+
+ target.BecomeRagdoll( Vector(0,0,0), false )
+
+ WaitFrame() // ragdoll take hold!
+ EmitSoundOnEntity( target, "Titan_Victim_Wall_Splat" )
+
+ if ( e.gib )
+ {
+ local force = Vector(0,0,0)
+ if ( IsAlive( attacker ) )
+ {
+ local vec = target.GetOrigin() - attacker.GetOrigin()
+ vec.Norm()
+ force = vec
+ }
+ target.Die( attacker, attacker, { scriptType = DF_GIB | DF_KILLSHOT, force = force, damageSourceId = eDamageSourceId.titan_execution } )
+ }
+ else
+ {
+ target.Die( attacker, attacker, { scriptType = DF_KILLSHOT, damageSourceId = eDamageSourceId.titan_execution } )
+ }
+}
+
+
+function MeleeAnimThrow( attacker, target, throwDuration )
+{
+ attacker.EndSignal( "OnDeath" )
+ target.EndSignal( "OnDeath" )
+ wait throwDuration - 0.2
+
+ local angles = attacker.GetAngles()
+ local forward = AnglesToForward( angles )
+ target.ClearParent()
+ target.SetVelocity( forward * 500 )
+
+
+ target.Die( attacker, attacker, { scriptType = DF_KILLSHOT, damageSourceId = eDamageSourceId.titan_execution } )
+}
+
+///////////////////////////////////////
+// OGRE MELEES
+///////////////////////////////////////
+void function MeleeThread_OgreVsTitan( SyncedMelee action, entity attacker, entity target )
+{
+ string attackerAnimation1p = "ogpov_melee_armrip_attacker"
+ string attackerAnimation3p = "og_melee_armrip_attacker"
+ string targetAnimation1p = "ogpov_melee_armrip_victim"
+ string targetAnimation3p = "og_melee_armrip_victim"
+
+ table e = {}
+ e.attackerStartOrg <- attacker.GetOrigin()
+ e.lostArm <- false
+ e.targetStartOrg <- target.GetOrigin()
+
+ entity ref = CreateMeleeScriptMoverBetweenEnts( attacker, target )
+
+ FirstPersonSequenceStruct attackerSequence
+ attackerSequence.blendTime = 0.25
+ attackerSequence.attachment = "ref"
+
+ FirstPersonSequenceStruct targetSequence = clone attackerSequence
+
+ attackerSequence.thirdPersonAnim = attackerAnimation3p
+ attackerSequence.firstPersonAnim = attackerAnimation1p
+
+ if ( target.IsPlayer() )
+ targetSequence.firstPersonAnim = targetAnimation1p
+
+ targetSequence.thirdPersonAnim = targetAnimation3p
+ targetSequence.blendTime = 0.25
+
+ target.e.syncedMeleeAttacker = attacker
+ DisableWeapons( attacker, [] )
+ DisableWeapons( target, [] )
+
+ // attacker.SetInvulnerable()
+ target.SetInvulnerable()
+
+ entity soul = target.GetTitanSoul()
+ soul.SetInvalidHealthBarEnt( true )
+
+ OnThreadEnd(
+ function() : ( ref, attacker, target, e )
+ {
+ if ( IsValid( ref ) )
+ {
+ if ( IsValid( attacker ) )
+ attacker.ClearParent()
+
+ if ( IsValid( target ) )
+ target.ClearParent()
+
+ AssertNoPlayerChildren( ref )
+ ref.Kill_Deprecated_UseDestroyInstead()
+ }
+
+ if ( IsValid( attacker ) )
+ {
+ attacker.UnforceStand()
+ attacker.ClearParent()
+ ClearPlayerAnimViewEntity( attacker )
+ EnableWeapons( attacker, [] )
+ attacker.PlayerMelee_ExecutionEndAttacker()
+
+ if ( IsAlive( attacker ) )
+ {
+ // if we got into solid, teleport back to safe place
+ PutEntityInSafeSpot( attacker, null, null, expect vector( e.attackerStartOrg ), attacker.GetOrigin() )
+ }
+ }
+
+ if ( IsValid( target ) )
+ {
+ DeleteAnimEvent( target, "lost_arm" )
+
+ target.e.syncedMeleeAttacker = null
+
+ target.ClearParent()
+ target.ClearInvulnerable()
+ if ( target.IsPlayer() )
+ {
+ ClearPlayerAnimViewEntity( target )
+ }
+
+ EnableWeapons( target, [] )
+
+ if ( !target.IsNPC() )
+ target.PlayerMelee_ExecutionEndTarget()
+
+ if ( e.lostArm && IsAlive( target ) )
+ {
+ target.Die( attacker, attacker, { scriptType = DF_KILLSHOT, damageSourceId = eDamageSourceId.titan_execution } )
+ return
+ }
+ else if ( target.IsPlayer() )
+ {
+ PutEntityInSafeSpot( target, null, null, expect vector( e.targetStartOrg ), target.GetOrigin() )
+ }
+ }
+ }
+ )
+
+ attacker.EndSignal( "OnDeath" )
+
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "Ogre_1p_Sync_Melee", "Ogre_3p_Sync_Melee", attacker, attacker )
+
+ AddAnimEvent( target, "lost_arm", TitanLostArm, e )
+
+
+ thread FirstPersonSequence( targetSequence, target, ref )
+ waitthread FirstPersonSequence( attackerSequence, attacker, ref )
+}
+
+//Very similar to the above function for now, eventually won't have the 1st person component at all.
+void function TitanVsTitan_3p( SyncedMelee action, entity attacker, entity target )
+{
+ if ( !IsAlive( attacker ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ entity attackerSoul = attacker.GetTitanSoul()
+ #if SP
+ TitanLoadoutDef loadout = GetTitanLoadoutForCurrentMap()
+ string executionRef = loadout.titanExecution
+ TitanExcutionData data = file.executionData_3p[ executionRef ]
+ #else
+ TitanLoadoutDef loadout = attackerSoul.soul.titanLoadout // GetActiveTitanLoadout( attacker )
+ string executionRef = loadout.titanExecution
+ TitanExcutionData data = file.executionData_3p[ executionRef ]
+ if ( data.linkedExecutions.len() > 0 )
+ {
+ array<string> clonedLinkedExecutions = clone data.linkedExecutions
+ for ( int i = clonedLinkedExecutions.len() - 1; i >= 0; i-- )
+ {
+ if ( GetItemRequiresPrime( clonedLinkedExecutions[ i ] ) == true && !HasPrimeToMatchExecutionType( attacker, GetItemType( clonedLinkedExecutions[ i ] ) ) )
+ clonedLinkedExecutions.remove( i )
+ }
+ executionRef = clonedLinkedExecutions.getrandom()
+ data = file.executionData_3p[ executionRef ]
+ }
+ #endif
+ bool shouldApplyBatteryAfterRodeo = false
+ if ( SoulHasPassive( attackerSoul, ePassives.PAS_VANGUARD_COREMETER ) )
+ {
+ executionRef = "execution_vanguard_kit"
+ data = file.executionData_3p[ executionRef ]
+ shouldApplyBatteryAfterRodeo = true
+ }
+
+ string victimType = GetSoulTitanSubClass( target.GetTitanSoul() )
+
+ table e = {}
+ e.attackerStartOrg <- attacker.GetOrigin()
+ e.lostArm <- false
+ e.targetStartOrg <- target.GetOrigin()
+
+ FirstPersonSequenceStruct attackerSequence
+ attackerSequence.blendTime = 0.25
+ attackerSequence.attachment = "ref"
+ attackerSequence.thirdPersonCameraAttachments = clone data.thirdPersonCameraAttachments
+ attackerSequence.thirdPersonCameraVisibilityChecks = true
+ attackerSequence.viewConeFunction = ViewConeZero
+ attackerSequence.noViewLerp = true
+
+ FirstPersonSequenceStruct targetSequence = clone attackerSequence
+
+ attackerSequence.thirdPersonAnim = data.attackerAnimation3p
+ attackerSequence.firstPersonAnim = ""
+
+ if ( target.IsPlayer() )
+ targetSequence.firstPersonAnim = ""
+
+ targetSequence.thirdPersonAnim = data.targetAnimation3p[ victimType ]
+ targetSequence.thirdPersonCameraEntity = target
+
+ target.e.syncedMeleeAttacker = attacker
+
+ // HACK FOR SP!!!
+ e.replacedPrimary <- false
+ string xo16 = "mp_titanweapon_xo16_shorty"
+ if ( IsSingleplayer() && attacker.IsPlayer() && data.attackerAnimation3p == "bt_synced_titan_execute_kickshoot_A" )
+ {
+ array<entity> weapons = attacker.GetMainWeapons()
+ if ( weapons.len() > 0 )
+ {
+ if ( weapons[0].GetWeaponClassName() != xo16 )
+ {
+ e.replacedPrimary = true
+ e.oldPrimary <- weapons[0].GetWeaponClassName()
+ attacker.SetActiveWeaponBySlot( 0 )
+ attacker.ReplaceActiveWeapon( xo16 ) //this assumes the active weapon is the weapon in slot 0 so we need to set active weapon to the one in slot 0
+ }
+ }
+ }
+ // END HACK FOR SP!!!
+
+ if ( !target.IsNPC() )
+ HolsterViewModelAndDisableWeapons( target ) //Melee anims need to use this to stop players from firing weapons but for the weapon to still show up in the 3p anims
+ else
+ DisableWeapons( target, [] )
+
+ if ( attacker.IsPlayer() )
+ {
+ HolsterViewModelAndDisableWeapons( attacker ) //Melee anims need to use this to stop players from firing weapons but for the weapon to still show up in the 3p anims
+ attacker.Anim_StopGesture( DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME )
+ }
+
+ // attacker.SetInvulnerable()
+ target.SetInvulnerable()
+
+ entity targetViewBody
+ FirstPersonSequenceStruct targetBodySequence
+ entity attackerViewBody
+ FirstPersonSequenceStruct attackerBodySequence
+
+ bool titanHasPilot = target.IsPlayer()
+ #if HAS_BOSS_AI
+ titanHasPilot = titanHasPilot || ( IsBossTitan( target ) )
+ #endif
+
+ if ( attacker.IsPlayer() )
+ {
+ Remote_CallFunction_Replay( attacker, "SCB_StopTitanCockpitSounds" )
+ }
+
+ if ( target.IsPlayer() )
+ {
+ Remote_CallFunction_Replay( target, "SCB_StopTitanCockpitSounds" )
+ }
+
+ if ( data.targetAnimation3pPilot[ victimType ] != "" && titanHasPilot )
+ {
+ if ( target.IsNPC() )
+ targetViewBody = CreateNpcTitanPilotModel( target )
+ else
+ targetViewBody = Wallrun_CreateCopyOfPilotModel( target )
+
+ targetViewBody.SetOrigin( target.GetOrigin() )
+ targetViewBody.SetRagdollImpactFX( RAGDOLL_IMPACT_TABLE_IDX )
+ targetViewBody.SetContinueAnimatingAfterRagdoll( true )
+
+ targetBodySequence.attachment = "ref"
+ targetBodySequence.teleport = true
+ targetBodySequence.thirdPersonAnim = data.targetAnimation3pPilot[ victimType ]
+
+ AddAnimEvent( targetViewBody, "pink_mist", MeleePinkMistFakeBody )
+ }
+
+ if ( data.attackerAnimation3pPilot[ victimType ] != "" && attacker.IsPlayer() )
+ {
+ attackerViewBody = Wallrun_CreateCopyOfPilotModel( attacker )
+
+ attackerViewBody.SetOrigin( attacker.GetOrigin() )
+ attackerViewBody.SetRagdollImpactFX( RAGDOLL_IMPACT_TABLE_IDX )
+ attackerViewBody.SetContinueAnimatingAfterRagdoll( true )
+
+ attackerBodySequence.attachment = "ref"
+ attackerBodySequence.teleport = true
+ attackerBodySequence.thirdPersonAnim = data.attackerAnimation3pPilot[ victimType ]
+ }
+
+ if ( !IsValid( targetViewBody ) )
+ {
+ attackerSequence.thirdPersonAnim = data.attackerAnimation3p_vsAutoTitan
+ }
+
+ entity soul = target.GetTitanSoul()
+ soul.SetInvalidHealthBarEnt( true )
+
+ bool isAttackerRef = false
+ if ( GetConVarBool( "melee_titan_execution_attacker_can_be_ref" ) )
+ {
+ isAttackerRef = IsAttackerRef( null, target )
+ }
+
+ OnThreadEnd(
+ function() : ( attacker, target, e, attackerViewBody, targetViewBody, shouldApplyBatteryAfterRodeo, isAttackerRef )
+ {
+ if ( IsValid( attacker ) )
+ {
+ DeleteAnimEvent( attacker, "synced_melee_enable_planting" )
+ DeleteAnimEvent( attacker, "rocket_pod_fire_left" )
+ DeleteAnimEvent( attacker, "rocket_pod_fire_right" )
+
+ attacker.UnforceStand()
+ attacker.ClearParent()
+ ClearPlayerAnimViewEntity( attacker )
+ attacker.PlayerMelee_ExecutionEndAttacker()
+ ForceTitanSustainedDischargeEnd( attacker )
+ DeployViewModelAndEnableWeapons( attacker ) //Melee anims need to use this to stop players from firing weapons but for the weapon to still show up in the 3p anims
+ if ( IsAlive( attacker ) )
+ {
+ if ( !isAttackerRef && IsValid( target ) )
+ {
+ PutEntityInSafeSpot( attacker, target, null, target.GetOrigin(), attacker.GetOrigin() )
+ }
+ else
+ {
+ PutEntityInSafeSpot( attacker, target, null, attacker.GetOrigin(), attacker.GetOrigin() )
+ }
+
+ if ( attacker.IsTitan() )
+ {
+ Remote_CallFunction_Replay( attacker, "SCB_PlayTitanCockpitSounds" )
+ #if TITAN_EXECUTION_GIVES_BATTERY
+ Rodeo_GiveExecutingTitanABattery( attacker )
+ #else
+ if ( shouldApplyBatteryAfterRodeo )
+ Rodeo_GiveExecutingTitanABattery( attacker )
+ #endif
+ }
+
+ if ( IsSingleplayer() )
+ {
+ if ( e.replacedPrimary )
+ {
+ attacker.ReplaceActiveWeapon( e.oldPrimary )
+ }
+ }
+ else
+ {
+ attacker.Anim_Stop() // if you are fighting an NPC, then they can get destroyed early the moment they explode. But sometimes, your animation isn't done playing yet so you can't move
+ }
+ }
+
+ }
+
+ if ( IsValid( target ) )
+ {
+ DeleteAnimEvent( target, "melee_killed_ragdoll" )
+ DeleteAnimEvent( target, "execution_battery_show" )
+ DeleteAnimEvent( target, "execution_battery_hide" )
+
+
+ if ( HasAnimEvent( target, "rider_rodeo_over" ) )
+ DeleteAnimEvent( target, "rider_rodeo_over" )
+
+ target.e.syncedMeleeAttacker = null
+
+ target.ClearParent()
+ target.ClearInvulnerable()
+ if ( target.IsPlayer() )
+ {
+ ClearPlayerAnimViewEntity( target )
+ DeployViewModelAndEnableWeapons( target ) //Melee anims need to use this to stop players from firing weapons but for the weapon to still show up in the 3p anims
+ }
+
+ if ( !target.IsNPC() && target.ContextAction_IsMeleeExecution() )
+ target.PlayerMelee_ExecutionEndTarget()
+
+ if ( IsAlive( target ) ) //Should have no need to PlayTitanCockpitSounds for target because the target is going to die
+ {
+ target.Die( attacker, attacker, { scriptType = DF_KILLSHOT, damageSourceId = eDamageSourceId.titan_execution } )
+ }
+ else if ( target.IsPlayer() )
+ {
+ if ( isAttackerRef && IsValid( attacker ) )
+ {
+ PutEntityInSafeSpot( target, attacker, null, attacker.GetOrigin(), target.GetOrigin() )
+ }
+ else
+ {
+ PutEntityInSafeSpot( target, attacker, null, target.GetOrigin(), target.GetOrigin() )
+ }
+ }
+ }
+
+ if ( IsValid( attackerViewBody ) )
+ {
+ //DeleteAnimEvent( attackerViewBody, "rodeo_battery_rip" )
+ DeleteAnimEvent( attackerViewBody, "execution_battery_pilot" )
+ DeleteAnimEvent( attackerViewBody, "execution_battery_pilot_jump_jets" )
+ attackerViewBody.Hide()
+ attackerViewBody.Destroy()
+ }
+
+ if ( IsValid( targetViewBody ) )
+ {
+ targetViewBody.Hide()
+ targetViewBody.Destroy()
+ }
+ }
+ )
+
+ attacker.EndSignal( "OnDeath" )
+ entity bossPlayer = target.GetBossPlayer()
+ if ( IsValid( bossPlayer ) ) //Executing an auto-Titan, when the pilot disconnects it destroys the auto-titan creating weird circumstances.
+ bossPlayer.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDestroy" )
+
+ if ( isAttackerRef )
+ {
+ thread ClearParentOnDeathOrDestroy( target, attacker )
+ }
+ else
+ {
+ thread ClearParentOnDeathOrDestroy( attacker, target )
+ }
+
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( data.sound_1p, data.sound_3p, attacker, attacker )
+
+ AddAnimEvent( target, "rider_rodeo_over", ForceTitanRodeoToEnd )
+ AddAnimEvent( target, "melee_killed_ragdoll", PredatorMeleeKilledRagdoll )
+ AddAnimEvent( attacker, "synced_melee_enable_planting", EnablePlantingOnEntity )
+ AddAnimEvent( attacker, "rocket_pod_fire_left", Northstar_Rocket_Pod_Left, target )
+ AddAnimEvent( attacker, "rocket_pod_fire_right", Northstar_Rocket_Pod_Right, target )
+ AddAnimEvent( target, "execution_battery_show", Execution_ShowBattery )
+ AddAnimEvent( target, "execution_battery_hide", Execution_HideBattery )
+ if ( attackerViewBody != null )
+ {
+ AddAnimEvent( attackerViewBody, "execution_battery_pilot", Execution_GivePilotBattery )
+ AddAnimEvent( attackerViewBody, "execution_battery_pilot_jump_jets", Execution_BatteryStealJumpJets )
+ }
+
+
+ if ( isAttackerRef )
+ {
+ attackerSequence.enablePlanting = true
+ attackerSequence.playerPushable = true
+ targetSequence.useAnimatedRefAttachment = true
+ }
+ else
+ {
+ targetSequence.enablePlanting = true
+ targetSequence.playerPushable = true
+ attackerSequence.useAnimatedRefAttachment = true
+ }
+
+ array<entity> ignoreEnts = [ attacker, target ]
+
+ vector refAngles = GetRefAnglesBetweenEnts( attacker, target )
+
+ if ( !attacker.IsOnGround() )
+ {
+ refAngles = <0,refAngles.y,0>
+ }
+
+ vector fwd = AnglesToForward( refAngles )
+ fwd *= -1
+ vector targetAngles = VectorToAngles( fwd )
+ if ( !target.IsNPC() )
+ {
+ targetAngles.x = 0
+ target.SetAngles( targetAngles )
+ }
+
+ target.SetAngles( targetAngles )
+
+ if ( attackerViewBody != null )
+ {
+ attackerBodySequence.useAnimatedRefAttachment = true
+ thread FirstPersonSequence( attackerBodySequence, attackerViewBody, attacker )
+ }
+
+ if ( targetViewBody != null )
+ {
+ targetBodySequence.useAnimatedRefAttachment = true
+ thread FirstPersonSequence( targetBodySequence, targetViewBody, target )
+ }
+
+ if ( isAttackerRef )
+ {
+ thread FirstPersonSequence( attackerSequence, attacker )
+ waitthread FirstPersonSequence( targetSequence, target, attacker )
+ }
+ else
+ {
+ thread FirstPersonSequence( targetSequence, target )
+ waitthread FirstPersonSequence( attackerSequence, attacker, target )
+ }
+}
+
+void function Execution_ShowBattery( entity titan )
+{
+ entity titanSoul = titan.GetTitanSoul()
+ if ( !IsValid( titanSoul ) ) //Out of bounds
+ return
+ string titanType = GetSoulTitanSubClass( titanSoul )
+ entity batteryContainer = titanSoul.soul.batteryContainer
+ Assert( IsValid( titanSoul.soul.batteryContainer ), " need to find the repro for this" )
+ if ( !IsValid( titanSoul.soul.batteryContainer ) )
+ return
+
+ batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up_idle" ) )
+}
+
+void function Execution_HideBattery( entity titan )
+{
+ entity titanSoul = titan.GetTitanSoul()
+ if ( !IsValid( titanSoul ) ) //Out of bounds
+ return
+ string titanType = GetSoulTitanSubClass( titanSoul )
+ entity batteryContainer = titanSoul.soul.batteryContainer
+ Assert( IsValid( titanSoul.soul.batteryContainer ), " need to find the repro for this" )
+ if ( !IsValid( titanSoul.soul.batteryContainer ) )
+ return
+
+ batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_down_idle" ) )
+ EmitSoundOnEntity( batteryContainer, GetAudioFromAlias( titanType, "rodeo_battery_steal_3p" ) )
+}
+
+void function Execution_GivePilotBattery( entity fakePilotModel )
+{
+ entity tempBattery3p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS )
+ tempBattery3p.SetParent( fakePilotModel, "R_HAND", false, 0.0 )
+ tempBattery3p.RemoveFromSpatialPartition()
+ tempBattery3p.Show()
+ Battery_StartFX( tempBattery3p )
+}
+
+
+void function Execution_BatteryStealJumpJets( entity fakePilotModel )
+{
+ int attachmentIndex = fakePilotModel.LookupAttachment( "vent_left" )
+ int fxIndex = GetParticleSystemIndex( TEAM_JUMPJET_DBL )
+ StartParticleEffectOnEntity( fakePilotModel, fxIndex, FX_PATTACH_POINT_FOLLOW, attachmentIndex )
+
+ attachmentIndex = fakePilotModel.LookupAttachment( "vent_right" )
+ StartParticleEffectOnEntity( fakePilotModel, fxIndex, FX_PATTACH_POINT_FOLLOW, attachmentIndex )
+}
+
+/*
+void function RodeoBatteryRemoval( entity pilot )
+{
+ entity titan = GetTitanBeingRodeoed( pilot )
+ if ( !IsValid( titan ) )
+ return
+
+ // THROW RODEO RIDER OFF
+ entity soul = titan.GetTitanSoul()
+ string titanType = GetSoulTitanSubClass( soul )
+
+ soul.SetLastRodeoHitTime( Time() )
+
+ RodeoBatteryPackRemovalDamage( pilot, titan, soul )
+
+ if ( !PlayerHasBattery( pilot ) )
+ {
+ AddPlayerScore( pilot, "PilotBatteryStolen" )
+ entity battery = Rodeo_CreateBatteryPack( titan )
+ Rodeo_PilotPicksUpBattery( pilot, battery )
+ thread BatteryThiefHighlight( pilot )
+
+ if ( titan.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, titan, TITAN_GOT_BATTERY_RIPPED_SOUND ) //Consider playing this in world once we get sounds that aren't just notification beeps
+ }
+ }
+
+ vector direction = CalculateDirectionToThrowOffBatteryThief( pilot, titan )
+
+ ThrowRiderOff( pilot, titan, direction ) //This signals RodeoOver
+}
+*/
+
+void function ClearParentOnDeathOrDestroy( entity clearParentEntity, entity onDeathOrDestroyEntity )
+{
+ Assert( IsValid( clearParentEntity ) )
+ Assert( IsAlive( clearParentEntity ) )
+
+ Assert( IsValid( onDeathOrDestroyEntity ) )
+ Assert( IsAlive( onDeathOrDestroyEntity ) )
+
+ OnThreadEnd(
+ function() : ( clearParentEntity, onDeathOrDestroyEntity )
+ {
+ if ( IsValid( clearParentEntity ) )
+ {
+ clearParentEntity.ClearParent()
+
+ if ( IsValid( onDeathOrDestroyEntity ) )
+ {
+ PutEntityInSafeSpot( clearParentEntity, onDeathOrDestroyEntity, null, onDeathOrDestroyEntity.GetOrigin(), clearParentEntity.GetOrigin() )
+ }
+ }
+ }
+ )
+
+ onDeathOrDestroyEntity.EndSignal( "OnDeath" )
+ onDeathOrDestroyEntity.WaitSignal( "OnDestroy" )
+}
+
+void function PredatorMeleeKilledRagdoll( entity titan )
+{
+ titan.e.forceRagdollDeath = true
+}
+
+void function MeleePinkMistFakeBody( entity target )
+{
+ target.Dissolve( ENTITY_DISSOLVE_PINKMIST, < 0, 0, 0 >, 0 )
+}
+
+void function TitanLostArm( entity titan )
+{
+ table e = expect table( GetOptionalAnimEventVar( titan, "lost_arm" ) )
+
+ e.lostArm = true
+}
+
+void function MeleeKilledRagdoll( entity titan )
+{
+ entity attacker = expect entity( GetOptionalAnimEventVar( titan, "melee_killed_ragdoll" ) )
+
+ if ( !IsValid( attacker ) )
+ return
+ titan.Die( attacker, attacker, { scriptType = DF_MELEE, damageSourceId = eDamageSourceId.titan_execution } )
+ titan.SetContinueAnimatingAfterRagdoll( true )
+ titan.BecomeRagdoll( < 0, 0, 0 >, false )
+}
+
+void function OnNPCTitanDeath( entity titan, var damageInfo ) //Debug function, for bug 129802
+{
+ PrintFunc()
+}
+
+void function OnNPCTitanSignalDeath( entity titan ) //Debug function, for bug 129802
+{
+ PrintFunc()
+
+ titan.WaitSignal( "OnDeath" )
+
+ printt( "titan : " + titan + " recieved OnDeath Signal in OnNPCTitanSignalDeath" )
+}
+
+
+void function Northstar_Rocket_Pod_Left( entity guy )
+{
+ entity victim = expect entity( GetOptionalAnimEventVar( guy, "rocket_pod_fire_left" ) )
+ Rocket_Pod( guy, "muzzle_flash", victim )
+}
+
+void function Northstar_Rocket_Pod_Right( entity guy )
+{
+ entity victim = expect entity( GetOptionalAnimEventVar( guy, "rocket_pod_fire_right" ) )
+ Rocket_Pod( guy, "muzzle_flash2", victim )
+}
+
+void function Rocket_Pod( entity guy, string tag, entity victim )
+{
+ entity oldOffhandWeapon = guy.GetOffhandWeapon( 0 )
+ guy.TakeOffhandWeapon( 0 )
+ guy.GiveOffhandWeapon( "mp_titanweapon_salvo_rockets", 0, [ "northstar_prime_execution" ] )
+
+ entity newOffhandWeapon = guy.GetOffhandWeapon( 0 )
+ int attachID = guy.LookupAttachment( tag )
+ vector angles = guy.GetAttachmentAngles( attachID )
+ WeaponPrimaryAttackParams params
+ params.pos = guy.GetAttachmentOrigin( attachID )
+ params.dir = AnglesToForward( angles )
+
+ if ( IsAlive( victim ) && victim.IsTitan() )
+ {
+ vector victimTagPos = victim.GetAttachmentOrigin( victim.LookupAttachment( "CHESTFOCUS" ) ) + RandomVec( 30 )
+ params.dir = Normalize( victimTagPos - params.pos )
+ StartParticleEffectInWorld(GetParticleSystemIndex( $"P_muzzleflash_predator" ), params.pos, VectorToAngles( params.dir ) )
+ }
+
+ // DebugDrawSphere(params.pos, 10, 255,0,0, true, 1.0 )
+ // DebugDrawLine( params.pos, params.pos + params.dir*200, 255,0,0, true, 1.0 )
+
+ thread OnWeaponPrimaryAttack_titanweapon_salvo_rockets( newOffhandWeapon, params )
+
+ guy.TakeOffhandWeapon( 0 )
+
+ if ( oldOffhandWeapon )
+ guy.GiveOffhandWeapon( oldOffhandWeapon.GetWeaponClassName(), 0, oldOffhandWeapon.GetMods() )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_mp.gnut
new file mode 100644
index 00000000..ac0c309b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_mp.gnut
@@ -0,0 +1,41 @@
+global function MpInitAILoadouts
+global function SetProficiency
+global function IsAutoPopulateEnabled
+global function SPMP_UpdateNPCProficiency
+global function SPMP_Callback_ForceAIMissPlayer
+
+void function MpInitAILoadouts()
+{
+
+}
+
+void function SetProficiency( entity soldier )
+{
+
+}
+
+bool function IsAutoPopulateEnabled( var team = null )
+{
+ if ( IsNPCSpawningEnabled() == false )
+ return false
+
+ if ( Flag( "disable_npcs" ) )
+ return false
+
+ if ( team == TEAM_MILITIA && Flag( "Disable_MILITIA" ) )
+ return false
+ if ( team == TEAM_IMC && Flag( "Disable_IMC" ) )
+ return false
+
+ return true
+}
+
+void function SPMP_UpdateNPCProficiency(entity ent)
+{
+
+}
+
+bool function SPMP_Callback_ForceAIMissPlayer(entity npc, entity player)
+{
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_mp.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_mp.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_superspectre.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_superspectre.nut
new file mode 100644
index 00000000..68e888f4
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_superspectre.nut
@@ -0,0 +1,736 @@
+untyped
+
+global function AiSuperspectre_Init
+
+global function SuperSpectre_OnGroundSlamImpact
+global function SuperSpectre_OnGroundLandImpact
+global function SuperSpectreThink
+global function SuperSpectreOnLeeched
+global function SuperSpectre_WarpFall
+global function CreateExplosionInflictor
+global function FragDroneDeplyAnimation
+global function ForceTickLaunch
+
+global function Reaper_LaunchFragDrone_Think
+global function ReaperMinionLauncherThink
+
+//==============================================================
+// AI Super Spectre
+//
+// Super Spectre keeps an array of the minions it spawned.
+// Each of those minions has a reference back to it's "master."
+//==============================================================
+const FRAG_DRONE_BATCH_COUNT = 10
+const FRAG_DRONE_IN_FRONT_COUNT = 2
+const FRAG_DRONE_MIN_LAUNCH_COUNT = 4
+const FRAG_DRONE_LAUNCH_INTIAL_DELAY_MIN = 10
+const FRAG_DRONE_LAUNCH_INTIAL_DELAY_MAX = 20
+const FRAG_DRONE_LAUNCH_INTERVAL = 40
+const SPAWN_ENEMY_TOO_CLOSE_RANGE_SQR = 1048576 // Don't spawn guys if the target enemy is closer than this range (1024^2).
+const SPAWN_HIDDEN_ENEMY_WITHIN_RANGE_SQR = 1048576 // If the enemy can't bee seen, and they are within in this range (1024^2), spawn dudes to find him.
+const SPAWN_ENEMY_ABOVE_HEIGHT = 128 // If the enemy is at least this high up, then spawn dudes to find him.
+const SPAWN_FUSE_TIME = 2.0 // How long after being fired before the spawner explodes and spawns a spectre.
+const SPAWN_PROJECTILE_AIR_TIME = 3.0 // How long the spawn project will be in the air before hitting the ground.
+const SPECTRE_EXPLOSION_DMG_MULTIPLIER = 1.2 // +20%
+const DEV_DEBUG_PRINTS = false
+
+struct
+{
+ int activeMinions_GlobalArrayIdx = -1
+} file
+
+function AiSuperspectre_Init()
+{
+ PrecacheParticleSystem( $"P_sup_spectre_death" )
+ PrecacheParticleSystem( $"P_sup_spectre_death_nuke" )
+ PrecacheParticleSystem( $"P_xo_damage_fire_2" )
+ PrecacheParticleSystem( $"P_sup_spec_dam_vent_1" )
+ PrecacheParticleSystem( $"P_sup_spec_dam_vent_2" )
+ PrecacheParticleSystem( $"P_sup_spectre_dam_1" )
+ PrecacheParticleSystem( $"P_sup_spectre_dam_2" )
+ PrecacheParticleSystem( $"drone_dam_smoke_2" )
+ PrecacheParticleSystem( $"P_wpn_muzzleflash_sspectre" )
+
+ PrecacheImpactEffectTable( "superSpectre_groundSlam_impact" )
+ PrecacheImpactEffectTable( "superSpectre_megajump_land" )
+
+ RegisterSignal( "SuperSpectre_OnGroundSlamImpact" )
+ RegisterSignal( "SuperSpectre_OnGroundLandImpact" )
+ RegisterSignal( "SuperSpectreThinkRunning" )
+ RegisterSignal( "OnNukeBreakingDamage" ) // enough damage to break out or skip nuke
+ RegisterSignal( "death_explosion" )
+ RegisterSignal( "WarpfallComplete" )
+ RegisterSignal( "BeginLaunchAttack" )
+
+ AddDeathCallback( "npc_super_spectre", SuperSpectreDeath )
+ AddDamageCallback( "npc_super_spectre", SuperSpectre_OnDamage )
+ //AddPostDamageCallback( "npc_super_spectre", SuperSpectre_PostDamage )
+
+ file.activeMinions_GlobalArrayIdx = CreateScriptManagedEntArray()
+}
+
+void function SuperSpectre_OnDamage( entity npc, var damageInfo )
+{
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( damageSourceId == eDamageSourceId.suicideSpectreAoE )
+ {
+ // super spectre takes reduced damage from suicide spectres
+ DamageInfo_ScaleDamage( damageInfo, 0.666 )
+ }
+}
+
+void function SuperSpectre_PostDamage( entity npc, var damageInfo )
+{
+ float switchRatio = 0.33
+ float ratio = HealthRatio( npc )
+ if ( ratio < switchRatio )
+ return
+ float newRatio = ( npc.GetHealth() - DamageInfo_GetDamage( damageInfo ) ) / npc.GetMaxHealth()
+ if ( newRatio >= switchRatio )
+ return
+
+ // destroy body groups
+ int bodygroup
+ bodygroup = npc.FindBodyGroup( "lowerbody" )
+ npc.SetBodygroup( bodygroup, 1 )
+ bodygroup = npc.FindBodyGroup( "upperbody" )
+ npc.SetBodygroup( bodygroup, 1 )
+}
+
+void function SuperSpectreDeath( entity npc, var damageInfo )
+{
+ thread DoSuperSpectreDeath( npc, damageInfo )
+}
+
+void function SuperSpectreNukes( entity npc, entity attacker )
+{
+ npc.EndSignal( "OnDestroy" )
+ vector origin = npc.GetWorldSpaceCenter()
+ EmitSoundAtPosition( npc.GetTeam(), origin, "ai_reaper_nukedestruct_explo_3p" )
+ PlayFX( $"P_sup_spectre_death_nuke", origin, npc.GetAngles() )
+
+ thread SuperSpectreNukeDamage( npc.GetTeam(), origin, attacker )
+ WaitFrame() // so effect has time to grow and cover the swap to gibs
+ npc.Gib( <0,0,100> )
+}
+
+void function DoSuperSpectreDeath( entity npc, var damageInfo )
+{
+ // destroyed?
+ if ( !IsValid( npc ) )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ const int SUPER_SPECTRE_NUKE_DEATH_THRESHOLD = 300
+
+ bool giveBattery = ( npc.ai.shouldDropBattery && IsSingleplayer() )
+
+ if ( !ShouldNukeOnDeath( npc ) || !npc.IsOnGround() || !npc.IsInterruptable() || DamageInfo_GetDamage( damageInfo ) > SUPER_SPECTRE_NUKE_DEATH_THRESHOLD || ( IsValid( attacker ) && attacker.IsTitan() ) )
+ {
+ // just boom
+ vector origin = npc.GetWorldSpaceCenter()
+ EmitSoundAtPosition( npc.GetTeam(), origin, "ai_reaper_explo_3p" )
+ npc.Gib( DamageInfo_GetDamageForce( damageInfo ) )
+ if ( giveBattery )
+ SpawnTitanBatteryOnDeath( npc, null )
+
+ return
+ }
+
+ npc.ai.killShotSound = false
+ npc.EndSignal( "OnDestroy" )
+
+ entity nukeFXInfoTarget = CreateEntity( "info_target" )
+ nukeFXInfoTarget.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ DispatchSpawn( nukeFXInfoTarget )
+
+ nukeFXInfoTarget.SetParent( npc, "HIJACK" )
+
+ EmitSoundOnEntity( nukeFXInfoTarget, "ai_reaper_nukedestruct_warmup_3p" )
+
+ AI_CreateDangerousArea_DamageDef( damagedef_reaper_nuke, nukeFXInfoTarget, TEAM_INVALID, true, true )
+
+ OnThreadEnd(
+ function() : ( nukeFXInfoTarget, npc, attacker, giveBattery )
+ {
+ if ( IsValid( nukeFXInfoTarget ) )
+ {
+ StopSoundOnEntity( nukeFXInfoTarget, "ai_reaper_nukedestruct_warmup_3p" )
+ nukeFXInfoTarget.Destroy()
+ }
+
+
+ if ( IsValid( npc ) )
+ {
+ thread SuperSpectreNukes( npc, attacker )
+ if ( giveBattery )
+ {
+ SpawnTitanBatteryOnDeath( npc, null )
+ }
+ }
+ }
+ )
+
+ //int bodygroup = npc.FindBodyGroup( "upperbody" )
+ //npc.SetBodygroup( bodygroup, 1 )
+
+ // TODO: Add death sound
+
+ WaitSignalOnDeadEnt( npc, "death_explosion" )
+}
+
+entity function CreateExplosionInflictor( vector origin )
+{
+ entity inflictor = CreateEntity( "script_ref" )
+ inflictor.SetOrigin( origin )
+ inflictor.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ DispatchSpawn( inflictor )
+ return inflictor
+}
+
+void function SuperSpectreNukeDamage( int team, vector origin, entity attacker )
+{
+ // all damage must have an inflictor currently
+ entity inflictor = CreateExplosionInflictor( origin )
+
+ OnThreadEnd(
+ function() : ( inflictor )
+ {
+ if ( IsValid( inflictor ) )
+ inflictor.Destroy()
+ }
+ )
+
+ int explosions = 8
+ float time = 1.0
+
+ for ( int i = 0; i < explosions; i++ )
+ {
+ entity explosionOwner
+ if ( IsValid( attacker ) )
+ explosionOwner = attacker
+ else
+ explosionOwner = GetTeamEnt( team )
+
+ RadiusDamage_DamageDefSimple(
+ damagedef_reaper_nuke,
+ origin, // origin
+ explosionOwner, // owner
+ inflictor, // inflictor
+ 0 ) // dist from attacker
+
+ wait RandomFloatRange( 0.01, 0.21 )
+ }
+}
+
+void function SuperSpectre_OnGroundLandImpact( entity npc )
+{
+ PlayImpactFXTable( npc.GetOrigin(), npc, "superSpectre_megajump_land", SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+}
+
+
+void function SuperSpectre_OnGroundSlamImpact( entity npc )
+{
+ PlayGroundSlamFX( npc )
+}
+
+
+function PlayGroundSlamFX( entity npc )
+{
+ int attachment = npc.LookupAttachment( "muzzle_flash" )
+ vector origin = npc.GetAttachmentOrigin( attachment )
+ PlayImpactFXTable( origin, npc, "superSpectre_groundSlam_impact", SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+}
+
+
+bool function EnemyWithinRangeSqr( entity npc, entity enemy, float range )
+{
+ vector pos = npc.GetOrigin()
+ vector enemyPos = enemy.GetOrigin()
+ float distance = DistanceSqr( pos, enemyPos )
+
+ return distance <= range
+}
+
+bool function ShouldLaunchFragDrones( entity npc, int activeMinions_EntArrayID )
+{
+// printt( "active " + GetScriptManagedEntArrayLen( activeMinions_EntArrayID ) )
+ if ( !npc.ai.superSpectreEnableFragDrones )
+ return false
+
+ // check global minions
+ if ( GetScriptManagedEntArrayLen( file.activeMinions_GlobalArrayIdx ) > 5 )
+ return false
+
+ // only launch if all minions are dead
+ if ( GetScriptManagedEntArrayLen( activeMinions_EntArrayID ) > 5 )
+ return false
+
+ entity enemy = npc.GetEnemy()
+
+ // Only spawn dudes if we have an enemy
+ if ( !IsValid( enemy ) )
+ return false
+
+ vector ornull lkp = npc.LastKnownPosition( enemy )
+ if ( lkp == null )
+ return false
+
+ expect vector( lkp )
+
+ // Don't spawn if the enemy is too far away
+ if ( Distance( npc.GetOrigin(), lkp ) > 1500 )
+ return false
+
+ return true
+}
+
+function SuperSpectreOnLeeched( npc, player )
+{
+ local maxHealth = npc.GetMaxHealth()
+ npc.SetHealth( maxHealth * 0.5 ) // refill to half health
+}
+
+function SuperSpectreThink( entity npc )
+{
+ npc.EndSignal( "OnDeath" )
+
+ int team = npc.GetTeam()
+
+ int activeMinions_EntArrayID = CreateScriptManagedEntArray()
+ if ( npc.kv.squadname == "" )
+ SetSquad( npc, UniqueString( "super_spec_squad" ) )
+
+ npc.ai.superSpectreEnableFragDrones = expect int( npc.Dev_GetAISettingByKeyField( "enable_frag_drones" ) ) == 1
+
+ OnThreadEnd (
+ function() : ( activeMinions_EntArrayID, npc, team )
+ {
+ entity owner
+ if ( IsValid( npc ) )
+ owner = npc
+
+ foreach ( minion in GetScriptManagedEntArray( activeMinions_EntArrayID ) )
+ {
+ // Self destruct the suicide spectres if applicable
+ if ( minion.GetClassName() != "npc_frag_drone" )
+ continue
+
+ if ( minion.ai.suicideSpectreExplodingAttacker == null )
+ minion.TakeDamage( minion.GetHealth(), owner, owner, { scriptType = DF_DOOMED_HEALTH_LOSS, damageSourceId = eDamageSourceId.mp_weapon_super_spectre } )
+ }
+ }
+ )
+
+ wait RandomFloatRange( FRAG_DRONE_LAUNCH_INTIAL_DELAY_MIN, FRAG_DRONE_LAUNCH_INTIAL_DELAY_MAX )
+
+ npc.kv.doScheduleChangeSignal = true
+
+ while ( 1 )
+ {
+ if ( ShouldLaunchFragDrones( npc, activeMinions_EntArrayID ) )
+ waitthread SuperSpectre_LaunchFragDrone_Think( npc, activeMinions_EntArrayID )
+
+ wait FRAG_DRONE_LAUNCH_INTERVAL
+ }
+}
+
+void function SuperSpectre_LaunchFragDrone_Think( entity npc, int activeMinions_EntArrayID )
+{
+ array<vector> targetOrigins = GetFragDroneTargetOrigins( npc, npc.GetOrigin(), 200, 2000, 64, FRAG_DRONE_BATCH_COUNT )
+
+ if ( targetOrigins.len() < FRAG_DRONE_MIN_LAUNCH_COUNT )
+ return
+
+ npc.RequestSpecialRangeAttack( targetOrigins.len() + FRAG_DRONE_IN_FRONT_COUNT )
+
+ // wait for first attack signal
+ npc.WaitSignal( "OnSpecialAttack" )
+ npc.EndSignal( "OnDeath" )
+ npc.EndSignal( "OnScheduleChange" ) // kv.doScheduleChangeSignal = true
+
+ // drop a few in front of enemy view
+ entity enemy = npc.GetEnemy()
+ if ( enemy )
+ {
+ vector searchOrigin = enemy.GetOrigin() + ( enemy.GetForwardVector() * 400 )
+ array<vector> frontOfEnemyOrigins = GetFragDroneTargetOrigins( npc, searchOrigin, 0, 500, 16, FRAG_DRONE_IN_FRONT_COUNT )
+
+ foreach ( targetOrigin in frontOfEnemyOrigins )
+ {
+ thread LaunchSpawnerProjectile( npc, targetOrigin, activeMinions_EntArrayID )
+ //DebugDrawBox( targetOrigin, Vector(-10, -10, 0), Vector(10, 10, 10), 255, 0, 0, 255, 5 )
+ npc.WaitSignal( "OnSpecialAttack" )
+ }
+ }
+
+ // drop rest in pre-searched spots
+ foreach ( targetOrigin in targetOrigins )
+ {
+ thread LaunchSpawnerProjectile( npc, targetOrigin, activeMinions_EntArrayID )
+ npc.WaitSignal( "OnSpecialAttack" )
+ }
+}
+
+void function ReaperMinionLauncherThink( entity reaper )
+{
+ if ( GetBugReproNum() != 221936 )
+ reaper.kv.squadname = ""
+
+ StationaryAIPosition launchPos = GetClosestAvailableStationaryPosition( reaper.GetOrigin(), 8000, eStationaryAIPositionTypes.LAUNCHER_REAPER )
+ launchPos.inUse = true
+
+ OnThreadEnd(
+ function () : ( launchPos )
+ {
+ launchPos.inUse = false
+ }
+ )
+
+ reaper.EndSignal( "OnDeath" )
+ reaper.AssaultSetFightRadius( 96 )
+ reaper.AssaultSetGoalRadius( reaper.GetMinGoalRadius() )
+
+ while ( true )
+ {
+ WaitFrame()
+
+ if ( Distance( reaper.GetOrigin(), launchPos.origin ) > 96 )
+ {
+ printt( reaper," ASSAULT:", launchPos.origin, Distance( reaper.GetOrigin(), launchPos.origin ) )
+ reaper.AssaultPoint( launchPos.origin )
+ table signalData = WaitSignal( reaper, "OnFinishedAssault", "OnEnterGoalRadius", "OnFailedToPath" )
+ printt( reaper," END ASSAULT:", launchPos.origin, signalData.signal )
+ if ( signalData.signal == "OnFailedToPath" )
+ continue
+ }
+
+ printt( reaper," LAUNCH:", launchPos.origin )
+ waitthread Reaper_LaunchFragDrone_Think( reaper, "npc_frag_drone_fd" )
+ printt( reaper," END LAUNCH:", launchPos.origin )
+ while ( GetScriptManagedEntArrayLen( reaper.ai.activeMinionEntArrayID ) > 2 )
+ WaitFrame()
+ }
+}
+
+void function Reaper_LaunchFragDrone_Think( entity reaper, string fragDroneSettings = "" )
+{
+ if ( reaper.ai.activeMinionEntArrayID < 0 )
+ reaper.ai.activeMinionEntArrayID = CreateScriptManagedEntArray()
+
+ int activeMinions_EntArrayID = reaper.ai.activeMinionEntArrayID
+
+ const int MAX_TICKS = 4
+
+ int currentMinions = GetScriptManagedEntArray( reaper.ai.activeMinionEntArrayID ).len()
+ int minionsToSpawn = MAX_TICKS - currentMinions
+
+ if ( minionsToSpawn <= 0 )
+ return
+
+ array<vector> targetOrigins = GetFragDroneTargetOrigins( reaper, reaper.GetOrigin(), 200, 2000, 64, MAX_TICKS )
+
+ if ( targetOrigins.len() < minionsToSpawn )
+ return
+
+ if ( IsAlive( reaper.GetEnemy() ) && ( reaper.GetEnemy().IsPlayer() || reaper.GetEnemy().IsNPC() ) && reaper.CanSee( reaper.GetEnemy() ) )
+ return
+
+ OnThreadEnd(
+ function() : ( reaper )
+ {
+ if ( IsValid( reaper ) )
+ {
+ reaper.Anim_Stop()
+ }
+ }
+ )
+
+ printt( reaper, " BEGIN LAUNCHING: ", minionsToSpawn, reaper.GetCurScheduleName() )
+
+ reaper.EndSignal( "OnDeath" )
+
+ while ( !reaper.IsInterruptable() )
+ WaitFrame()
+
+ waitthread PlayAnim( reaper, "sspec_idle_to_speclaunch" )
+
+ while ( minionsToSpawn > 0 )
+ {
+ // drop rest in pre-searched spots
+ foreach ( targetOrigin in targetOrigins )
+ {
+ if ( minionsToSpawn <= 0 )
+ break
+
+ printt( reaper, " LAUNCHING: ", minionsToSpawn )
+ thread LaunchSpawnerProjectile( reaper, targetOrigin, activeMinions_EntArrayID, fragDroneSettings )
+ minionsToSpawn--
+
+ if ( minionsToSpawn <= 0 )
+ break
+
+ waitthread PlayAnim( reaper, "sspec_speclaunch_fire" )
+ }
+ }
+
+ waitthread PlayAnim( reaper, "sspec_speclaunch_to_idle" )
+}
+
+
+
+array<vector> function GetFragDroneTargetOrigins( entity npc, vector origin, float minRadius, float maxRadius, int randomCount, int desiredCount )
+{
+ array<vector> targetOrigins
+/*
+ vector angles = npc.GetAngles()
+ angles.x = 0
+ angles.z = 0
+
+ vector origin = npc.GetOrigin() + Vector( 0, 0, 1 )
+ float arc = 0
+ float dist = 200
+
+ for ( ;; )
+ {
+ if ( dist > 2000 || targetOrigins.len() >= 12 )
+ break
+
+ angles = AnglesCompose( angles, <0,arc,0> )
+ arc += 35
+ arc %= 360
+ dist += 200
+
+ vector ornull tryOrigin = TryCreateFragDroneLaunchTrajectory( npc, origin, angles, dist )
+ if ( tryOrigin == null )
+ continue
+ expect vector( tryOrigin )
+ targetOrigins.append( tryOrigin )
+ }
+*/
+ float traceFrac = TraceLineSimple( origin, origin + <0, 0, 200>, npc )
+ if ( traceFrac < 1 )
+ return targetOrigins;
+
+ array< vector > randomSpots = NavMesh_RandomPositions_LargeArea( origin, HULL_HUMAN, randomCount, minRadius, maxRadius )
+
+ int numFragDrones = 0
+ foreach( spot in randomSpots )
+ {
+ targetOrigins.append( spot )
+ numFragDrones++
+ if ( numFragDrones == desiredCount )
+ break
+ }
+
+ return targetOrigins
+}
+
+vector ornull function TryCreateFragDroneLaunchTrajectory( entity npc, vector origin, vector angles, float dist )
+{
+ vector forward = AnglesToForward( angles )
+ vector targetOrigin = origin + forward * dist
+
+ vector ornull clampedPos = NavMesh_ClampPointForHullWithExtents( targetOrigin, HULL_HUMAN, < 300, 300, 100 > )
+
+ if ( clampedPos == null )
+ return null
+
+ vector vel = GetVelocityForDestOverTime( origin, expect vector( clampedPos ), SPAWN_PROJECTILE_AIR_TIME )
+ float traceFrac = TraceLineSimple( origin, origin + vel, npc )
+ //DebugDrawLine( origin, origin + vel, 255, 0, 0, true, 5.0 )
+ if ( traceFrac >= 0.5 )
+ return clampedPos
+ return null
+}
+
+void function FragDroneDeplyAnimation( entity drone, float minDelay = 0.5, float maxDelay = 2.5 )
+{
+ Assert( !drone.ai.fragDroneArmed, "Armed drone was told to play can animation. Spawn drone with CreateFragDroneCan()" )
+ drone.EndSignal( "OnDeath" )
+
+ drone.SetInvulnerable()
+ OnThreadEnd(
+ function() : ( drone )
+ {
+ drone.ClearInvulnerable()
+ }
+ )
+
+ drone.Anim_ScriptedPlay( "sd_closed_idle" )
+ wait RandomFloatRange( minDelay, maxDelay )
+
+ #if MP
+ while ( !drone.IsInterruptable() )
+ {
+ WaitFrame()
+ }
+ #endif
+
+ drone.Anim_ScriptedPlay( "sd_closed_to_open" )
+
+ // Wait for P_drone_frag_open_flicker FX to play inside sd_closed_to_open
+ wait 0.6
+}
+
+void function LaunchSpawnerProjectile( entity npc, vector targetOrigin, int activeMinions_EntArrayID, string droneSettings = "" )
+{
+ //npc.EndSignal( "OnDeath" )
+
+ entity weapon = npc.GetOffhandWeapon( 0 )
+
+ if ( !IsValid( weapon ) )
+ return
+
+ int id = npc.LookupAttachment( "launch" )
+ vector launchPos = npc.GetAttachmentOrigin( id )
+ int team = npc.GetTeam()
+ vector launchAngles = npc.GetAngles()
+ string squadname = expect string( npc.kv.squadname )
+ vector vel = GetVelocityForDestOverTime( launchPos, targetOrigin, SPAWN_PROJECTILE_AIR_TIME )
+
+// DebugDrawLine( npc.GetOrigin() + <3,3,3>, launchPos + <3,3,3>, 255, 0, 0, true, 5.0 )
+ float armTime = SPAWN_PROJECTILE_AIR_TIME + RandomFloatRange( 1.0, 2.5 )
+ entity nade = weapon.FireWeaponGrenade( launchPos, vel, <200,0,0>, armTime, damageTypes.dissolve, damageTypes.explosive, PROJECTILE_NOT_PREDICTED, true, true )
+
+ AddToScriptManagedEntArray( activeMinions_EntArrayID, nade )
+ AddToScriptManagedEntArray( file.activeMinions_GlobalArrayIdx, nade )
+
+ nade.SetOwner( npc )
+ nade.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( nade, team, activeMinions_EntArrayID, squadname, droneSettings )
+ {
+ vector origin = nade.GetOrigin()
+ vector angles = nade.GetAngles()
+
+ vector ornull clampedPos = NavMesh_ClampPointForHullWithExtents( origin, HULL_HUMAN, < 100, 100, 100 > )
+ if ( clampedPos == null )
+ return
+
+ entity drone = CreateFragDroneCan( team, expect vector( clampedPos ), < 0, angles.y, 0 > )
+ SetSpawnOption_SquadName( drone, squadname )
+ if ( droneSettings != "" )
+ {
+ SetSpawnOption_AISettings( drone, droneSettings )
+ }
+ drone.kv.spawnflags = SF_NPC_ALLOW_SPAWN_SOLID // clamped to navmesh no need to check solid
+ DispatchSpawn( drone )
+
+ thread FragDroneDeplyAnimation( drone )
+
+ AddToScriptManagedEntArray( activeMinions_EntArrayID, drone )
+ AddToScriptManagedEntArray( file.activeMinions_GlobalArrayIdx, drone )
+ }
+ )
+
+ Grenade_Init( nade, weapon )
+
+ EmitSoundOnEntity( npc, "SpectreLauncher_AI_WpnFire" )
+ WaitForever()
+
+// wait SPAWN_PROJECTILE_AIR_TIME + SPAWN_FUSE_TIME
+}
+
+
+// Seriously don't use this unless absolutely necessary! Used for scripted moment in Reapertown.
+// Bypasses all of the tick launch rules and sends a request for launching ticks to code immediately.
+void function ForceTickLaunch( entity npc )
+{
+ SuperSpectre_LaunchFragDrone_Think( npc, file.activeMinions_GlobalArrayIdx )
+}
+
+
+/************************************************************************************************\
+######## ######## ####### ######## ####### ######## ## ## ######## ########
+## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## #### ## ## ##
+######## ######## ## ## ## ## ## ## ## ######## ######
+## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ####### ## ####### ## ## ## ########
+\************************************************************************************************/
+
+
+function SuperSpectre_WarpFall( entity ai )
+{
+ ai.EndSignal( "OnDestroy" )
+
+ vector origin = ai.GetOrigin()
+ entity mover = CreateOwnedScriptMover( ai )
+ ai.SetParent( mover, "", false, 0 )
+ ai.Hide()
+ ai.SetEfficientMode( true )
+ ai.SetInvulnerable()
+
+ WaitFrame() // give AI time to hide before moving
+
+ vector warpPos = origin + < 0, 0, 1000 >
+ mover.SetOrigin( warpPos )
+
+ #if GRUNTCHATTER_ENABLED
+ GruntChatter_TryIncomingSpawn( ai, origin )
+ #endif
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, "Titan_1P_Warpfall_Start" )
+
+ local e = {}
+ e.warpfx <- PlayFX( TURBO_WARP_FX, warpPos + < 0, 0, -104 >, mover.GetAngles() )
+ e.smokeFx <- null
+
+ OnThreadEnd(
+ function() : ( e, mover, ai )
+ {
+ if ( IsAlive( ai ) )
+ {
+ ai.ClearParent()
+ ai.SetVelocity( <0,0,0> )
+ ai.Signal( "WarpfallComplete" )
+ }
+ if ( IsValid( e.warpfx ) )
+ e.warpfx.Destroy()
+ if ( IsValid( e.smokeFx ) )
+ e.smokeFx.Destroy()
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ }
+ )
+ wait 0.5
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, "Titan_3P_Warpfall_WarpToLanding" )
+
+ wait 0.4
+
+ ai.Show()
+
+ e.smokeFx = PlayFXOnEntity( TURBO_WARP_COMPANY, ai, "", <0.0, 0.0, 152.0> )
+
+ local time = 0.2
+ mover.MoveTo( origin, time, 0, 0 )
+ wait time
+
+ ai.SetEfficientMode( false )
+ ai.ClearInvulnerable()
+
+ e.smokeFx.Destroy()
+ PlayFX( $"droppod_impact", origin )
+
+ Explosion_DamageDefSimple(
+ damagedef_reaper_fall,
+ origin,
+ ai, // attacker
+ ai, // inflictor
+ origin )
+
+ wait 0.1
+}
+
+bool function ShouldNukeOnDeath( entity ent )
+{
+ if ( IsMultiplayer() )
+ return false
+
+ return ent.Dev_GetAISettingByKeyField( "nuke_on_death" ) == 1
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut
new file mode 100644
index 00000000..a4c6e187
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut
@@ -0,0 +1,2179 @@
+untyped
+
+globalize_all_functions
+
+//********************************************************************************************
+// Base Gametype
+//********************************************************************************************
+const DEATH_CHAT_DELAY = 0.3
+
+global struct OutOfBoundsDataStruct //Have to globalize it because all functions are globalized in this file :/
+{
+ int outOfBoundsTriggersTouched = 0
+ float timeBackInBound = 0
+ float timeLeftBeforeDyingFromOutOfBounds = OUT_OF_BOUNDS_TIME_LIMIT
+}
+
+struct
+{
+ PilotLoadoutDef& playbackBotLoadout
+ array<entity> outOfBoundsTriggers = []
+ array<entity> hurtTriggers = []
+ bool functionref( entity, entity, var ) isProtectedFromFriendlyFire
+ table< entity, OutOfBoundsDataStruct > outOfBoundsTable
+} file
+
+function BaseGametype_Init()
+{
+ FlagInit( "APlayerHasSpawned" )
+ FlagInit( "PilotBot" )
+
+ if ( !reloadingScripts )
+ {
+ level.gameTypeText <- null
+ level.classTypeText <- null
+
+ level.titanAlwaysAvailableForTeam <- [ 0, 0, 0, 0 ]
+
+ level.missingPlayersTimeout <- null
+
+ CreateTeamColorControlPoints()
+
+ AddClientCommandCallback( "CC_SelectRespawn", ClientCommand_SelectRespawn )
+ AddClientCommandCallback( "CC_RespawnPlayer", ClientCommand_RespawnPlayer )
+
+ AddCallback_NPCLeeched( OnNPCLeeched )
+
+ MarkTeamsAsBalanced_Off()
+ }
+
+ if ( IsSingleplayer() )
+ {
+ file.isProtectedFromFriendlyFire = IsProtectedFromFriendlyFire_SP
+ }
+ else
+ {
+ file.isProtectedFromFriendlyFire = IsProtectedFromFriendlyFire_MP
+ }
+
+ RegisterSignal( "OnDamageNotify" )
+ RegisterSignal( "OnRespawned" )
+ RegisterSignal( "ChoseToSpawnAsTitan" )
+ RegisterSignal( "OutOfBounds" )
+ RegisterSignal( "BackInBounds" )
+ RegisterSignal( "PlayerKilled" )
+ RegisterSignal( "RespawnMe" )
+ RegisterSignal( "SimulateGameScore" )
+ RegisterSignal( "ObserverThread" )
+ RegisterSignal( "CE_FLAGS_CHANGED" )
+
+ RegisterSignal( "Stop_OnStartTouch_EntityOutOfBounds" )
+ RegisterSignal( "Stop_OnEndTouch_EntityBackInBounds" )
+
+ RegisterSignal( "OnRespawnSelect" )
+
+ AddCallback_EntitiesDidLoad( BaseGametypeEntitiesDidLoad )
+
+ BaseGametype_Init_MPSP()
+
+ AddCallback_OnTitanBecomesPilot( OnTitanBecomesPilot_OutOfBoundsCheck )
+}
+
+void function BaseGametypeEntitiesDidLoad()
+{
+ OutOfBoundsSetup()
+ TriggerHurtSetup()
+}
+
+function CreateTeamColorControlPoints()
+{
+ Assert( !( "fx_CP_color_enemy" in level ) )
+ Assert( !( "fx_CP_color_friendly" in level ) )
+
+ entity enemy = CreateEntity( "info_placement_helper" )
+ SetTargetName( enemy, UniqueString( "teamColorControlPoint_enemy" ) )
+ enemy.kv.start_active = 1
+ DispatchSpawn( enemy )
+
+ enemy.SetOrigin( ENEMY_COLOR_FX )
+ svGlobal.fx_CP_color_enemy = enemy
+
+ entity friendly = CreateEntity( "info_placement_helper" )
+ SetTargetName( friendly, UniqueString( "teamColorControlPoint_friendly" ) )
+ friendly.kv.start_active = 1
+ DispatchSpawn( friendly )
+
+ friendly.SetOrigin( FRIENDLY_COLOR_FX )
+ svGlobal.fx_CP_color_friendly = friendly
+
+ entity neutral = CreateEntity( "info_placement_helper" )
+ SetTargetName( neutral, UniqueString( "teamColorControlPoint_neutral" ) )
+ neutral.kv.start_active = 1
+ DispatchSpawn( neutral )
+
+ neutral.SetOrigin( NEUTRAL_COLOR_FX )
+ svGlobal.fx_CP_color_neutral = neutral
+}
+
+const SOLDIER_SOUND_PAIN = "npc_grunt_pain"
+
+void function CodeCallback_OnPrecache()
+{
+ if ( IsLobby() )
+ return
+
+ Assert( IsSingleplayer() || GAMETYPE in GAMETYPE_TEXT )
+
+ // these should be level specific in SP
+ PrecacheEntity( "npc_soldier" )
+ PrecacheEntity( "turret" )
+
+ PrecacheEntity( "npc_dropship", DROPSHIP_MODEL )
+
+ //Scavenger ore models. Need to precache here instead of in gamemode scripts for vpk builds
+ //Removing for build
+ /*level.scavengerSmallRocks <- [
+ $"models/rocks/rock_01_sandstone.mdl"
+ //$"models/rocks/rock_02_sandstone.mdl"
+ //$"models/rocks/rock_03_sandstone.mdl"
+ //$"models/rocks/single_rock_01.mdl"
+ //$"models/rocks/single_rock_02.mdl"
+ //$"models/rocks/single_rock_03.mdl"
+ //$"models/rocks/single_rock_04.mdl"
+ ]
+
+ level.scavengerLargeRocks <- [
+ $"models/rocks/rock_boulder_large_01.mdl"
+ //$"models/rocks/sandstone_rock01.mdl"
+ //$"models/rocks/sandstone_rock02.mdl"
+ //$"models/rocks/sandstone_rock03.mdl"
+ //$"models/rocks/sandstone_rock04.mdl"
+ //$"models/rocks/sandstone_rock05.mdl"
+ ]
+
+ foreach ( model in level.scavengerSmallRocks )
+ {
+ PrecacheModel( model )
+ }
+
+ foreach ( model in level.scavengerLargeRocks )
+ {
+ PrecacheModel( model )
+ }*/
+
+ if ( !IsMenuLevel() )
+ {
+ InitGameState()
+ SetGameState( eGameState.WaitingForPlayers )
+ }
+
+ level.ui.disableDev = IsMatchmakingServer()
+}
+
+function AddFlinch( entity attackedEnt, damageInfo )
+{
+ Assert( IsValid_ThisFrame( attackedEnt ) )
+
+ //if ( !( "nextFlinchTime" in attackedEnt.s ) )
+ // attackedEnt.s.nextFlinchTime <- 0
+ //if ( Time() < attackedEnt.s.nextFlinchTime )
+ // return
+ //attackedEnt.s.nextFlinchTime = Time() + RandomFloatRange( 2.0, 4.0 )
+
+ vector damageAngles = VectorToAngles( DamageInfo_GetDamageForce( damageInfo ) )
+ vector entAngles = attackedEnt.EyeAngles()
+
+ float damageYaw = (damageAngles.y + 180) - entAngles.y
+
+ damageYaw = AngleNormalize( damageYaw )
+
+ if ( damageYaw < 0 )
+ damageYaw += 360
+
+ if ( damageYaw < 45 )
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_BACKWARDS );
+ else if ( damageYaw < 135 )
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_RIGHT );
+ else if ( damageYaw < 225 )
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_FORWARDS );
+ else if ( damageYaw < 315 )
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_LEFT );
+ else
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_BACKWARDS );
+}
+
+
+bool function IsProtectedFromFriendlyFire_MP( entity attacker, entity ent, var damageInfo )
+{
+ // no suicide protection
+ if ( attacker == ent )
+ return false
+
+ if ( attacker.GetTeam() != ent.GetTeam() )
+ return false
+
+ if ( DamageIgnoresFriendlyFire( damageInfo ) )
+ return false
+
+ if ( ent.GetOwner() != attacker && ent.GetBossPlayer() != attacker )
+ return true
+
+ if ( ent.e.noOwnerFriendlyFire == true )
+ return true
+
+ if ( ent.IsNPC() && ent.ai.preventOwnerDamage )
+ return true
+
+ return false
+}
+
+bool function IsProtectedFromNPCFire( entity attacker, entity ent )
+{
+ if ( attacker == ent )
+ return false
+ if ( attacker.IsNPC() && ent.IsNPC() && ent.ai.invulnerableToNPC == true )
+ return true
+ return false
+}
+
+
+bool function IsProtectedFromFriendlyFire_SP( entity attacker, entity ent, var damageInfo )
+{
+ // no suicide protection
+ if ( attacker == ent )
+ return false
+
+ if ( attacker.GetTeam() == ent.GetTeam() )
+ {
+ if ( attacker.IsNPC() )
+ {
+ // dont titanfall me!
+ if ( ent.IsPlayer() )
+ return true
+
+ // bullets dont damage same team of npcs
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_BULLET )
+ return true
+ }
+ else if ( attacker.IsPlayer() )
+ {
+ if ( ent.IsNPC() )
+ {
+ if ( ent.IsTitan() )
+ return true
+
+ return !ent.AISetting_ShootableByFriendlyPlayer()
+ }
+ if ( ent.IsProjectile() )
+ return false
+ return true
+ }
+
+ if ( DamageIgnoresFriendlyFire( damageInfo ) )
+ return false
+
+ if ( ent.IsNPC() && ent.ai.preventOwnerDamage )
+ {
+ if ( attacker == ent.GetOwner() || attacker == ent.GetBossPlayer() )
+ return true
+ }
+ }
+
+ return false
+}
+
+bool function DamageIgnoresFriendlyFire( damageInfo )
+{
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return true
+
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ switch ( damageSourceID )
+ {
+ case eDamageSourceId.switchback_trap:
+ case eDamageSourceId.suicideSpectreAoE:
+ case eDamageSourceId.mp_titanweapon_stun_laser: // for energy transfer functionality. Preventing FF damage in the callback.
+ case eDamageSourceId.mp_titanability_smoke: // For FD Vanguard Shield Upgrades. Preventing FF damage in the callback.
+ return true
+ }
+
+ return false
+}
+
+bool function ScriptCallback_ShouldEntTakeDamage( entity ent, damageInfo )
+{
+ if ( ent.IsInvulnerable() )
+ return false
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ bool entIsPlayer = ent.IsPlayer()
+
+ if ( !attacker )
+ return false
+
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+
+ if ( attacker == ent || IsValid( inflictor ) && inflictor == ent )
+ {
+ if ( (damageType & DF_NO_SELF_DAMAGE) > 0 )
+ return false
+ }
+
+ if ( file.isProtectedFromFriendlyFire( attacker, ent, damageInfo ) )
+ return false
+
+ if ( IsProtectedFromNPCFire( attacker, ent ) )
+ return false
+
+ if ( !ShouldEntTakeDamage_SPMP( ent, damageInfo ) )
+ return false
+
+ if ( ent.IsTitan() )
+ {
+ const int BULLET_VORTEX_FLAGS = (DF_VORTEX_REFIRE | DF_BULLET)
+ if ( ((damageType & BULLET_VORTEX_FLAGS) == BULLET_VORTEX_FLAGS) && (ent == attacker) )
+ return false // don't let vortex-refiring titan hit themselves with bullet or bullet splash damage
+
+ if ( IsTitanWithinBubbleShield( ent ) && TitanHasBubbleShieldWeapon( ent ) && !(damageType & DF_DOOMED_HEALTH_LOSS) )
+ return false
+ }
+
+ if ( IsTitanCrushDamage( damageInfo ) )
+ {
+ if ( attacker.IsPhaseShifted() )
+ return false
+ }
+
+ if ( (inflictor != null) )
+ {
+ if ( inflictor.IsProjectile() )
+ {
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( attacker == ent )
+ {
+ bool shouldDamageOwner = inflictor.GetProjectileWeaponSettingBool( eWeaponVar.explosion_damages_owner )
+ if ( !shouldDamageOwner )
+ return false
+
+ if ( entIsPlayer )
+ {
+ array<string> mods = inflictor.ProjectileGetMods()
+ foreach ( mod in mods )
+ {
+ if ( mod == "jump_kit" )
+ {
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ damageAmount *= 0.75
+ DamageInfo_SetDamage( damageInfo, damageAmount )
+ // DamageInfo_SetDamageForce( damageInfo, DamageInfo_GetDamageForce( damageInfo ) * 2.0 )
+ }
+ }
+ }
+ }
+ }
+
+ if ( inflictor.e.onlyDamageEntitiesOnce == true && inflictor.e.damagedEntities.contains( ent ) )
+ return false
+
+ if ( inflictor.e.onlyDamageEntitiesOncePerTick == true )
+ {
+ float currentTime = Time()
+ if ( currentTime != inflictor.e.lastDamageTickTime )
+ {
+ inflictor.e.damagedEntities.clear()
+ inflictor.e.lastDamageTickTime = currentTime
+ }
+ else if ( inflictor.e.damagedEntities.contains( ent ) )
+ {
+ return false
+ }
+ }
+ }
+
+ if ( ent.IsPlayer() )
+ {
+ return ShouldPlayerTakeDamage( ent, damageInfo )
+ }
+
+ return true
+}
+
+bool function ShouldPlayerTakeDamage( entity player, damageInfo )
+{
+ if ( player.IsGodMode() )
+ return false
+
+ if ( player.IsPhaseShifted()
+ && !(DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS)
+ && !IsDamageFromDamageTrigger( damageInfo ) )
+ return false
+
+ if ( player.IsInvulnerable() )
+ return false
+
+ if ( player.IsTitan() )
+ {
+ return true
+ }
+ else
+ {
+ //Rodeo cases
+ entity titanSoul = player.GetTitanSoulBeingRodeoed()
+ if ( IsValid( titanSoul ) )
+ {
+ entity titan = titanSoul.GetTitan()
+ //Stop being stepped on by the guy you are rodeoing
+ if ( IsTitanCrushDamage( damageInfo ) && ( titan == DamageInfo_GetAttacker( damageInfo ) ) )
+ return false
+ else
+ return true
+ }
+ else
+ {
+ return true
+ }
+ }
+
+ unreachable
+}
+
+
+void function HandlePainSounds( entity ent, var damageInfo )
+{
+ //exit if the thing is dead
+ if ( ent.GetHealth() < DamageInfo_GetDamage( damageInfo ) )
+ return
+
+ PlayPainSounds( ent, damageInfo )
+}
+
+float function GetHeadshotDamageMultiplierFromDamageInfo( var damageInfo )
+{
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ if ( weapon )
+ {
+ float result = weapon.GetWeaponSettingFloat( eWeaponVar.damage_headshot_scale )
+ return result
+ }
+
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ if ( inflictor && inflictor.IsProjectile() )
+ {
+ float result = inflictor.GetProjectileWeaponSettingFloat( eWeaponVar.damage_headshot_scale )
+ return result
+ }
+
+ return 1.0
+}
+
+function HandleLocationBasedDamage( entity ent, var damageInfo )
+{
+ // Don't allow non-players to get headshots or any other location bonuses
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsValid( attacker ) || !attacker.IsPlayer() )
+ return
+
+ bool debugPrints = false
+ int hitGroup = DamageInfo_GetHitGroup( damageInfo )
+
+ if ( debugPrints )
+ {
+ printt( "---------------------" )
+ printt( "LOCATION BASED DAMAGE" )
+ printt( "HIDGROUP ID:", hitGroup )
+ if ( hitGroup == HITGROUP_GENERIC )
+ printt( "HITGROUP: HITGROUP_GENERIC" )
+ else if ( hitGroup == HITGROUP_HEAD )
+ printt( "HITGROUP: HITGROUP_HEAD" )
+ else if ( hitGroup == HITGROUP_CHEST )
+ printt( "HITGROUP: HITGROUP_CHEST" )
+ else if ( hitGroup == HITGROUP_STOMACH )
+ printt( "HITGROUP: HITGROUP_STOMACH" )
+ else if ( hitGroup == HITGROUP_LEFTARM )
+ printt( "HITGROUP: HITGROUP_LEFTARM" )
+ else if ( hitGroup == HITGROUP_RIGHTARM )
+ printt( "HITGROUP: HITGROUP_RIGHTARM" )
+ else if ( hitGroup == HITGROUP_LEFTLEG )
+ printt( "HITGROUP: HITGROUP_LEFTLEG" )
+ else if ( hitGroup == HITGROUP_RIGHTLEG )
+ printt( "HITGROUP: HITGROUP_RIGHTLEG" )
+ else if ( hitGroup == HITGROUP_GEAR )
+ printt( "HITGROUP: HITGROUP_GEAR" )
+ else
+ printt( "HITGROUP: UNKNOWN" )
+ }
+
+ bool isValidHeadShot = IsValidHeadShot( damageInfo, ent )
+ if ( isValidHeadShot )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_HEADSHOT )
+
+ float damageMult_location = 1.0
+
+ var weaponName // TODO: If set to type string, will cause errors because weaponName can be ""
+ if ( DamageInfo_GetWeapon( damageInfo ) )
+ weaponName = DamageInfo_GetWeapon( damageInfo ).GetWeaponClassName()
+ else if ( DamageInfo_GetInflictor( damageInfo ) && (DamageInfo_GetInflictor( damageInfo ) instanceof CProjectile ) )
+ weaponName = DamageInfo_GetInflictor( damageInfo ).ProjectileGetWeaponClassName()
+
+ if ( ent.IsTitan() )
+ {
+ damageMult_location = GetCriticalScaler( ent, damageInfo )
+ }
+ else if ( IsSuperSpectre( ent ) )
+ {
+ if ( CritWeaponInDamageInfo( damageInfo ) && IsCriticalHit( attacker, ent, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) ) )
+ {
+ damageMult_location = GetCriticalScaler( ent, damageInfo )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+ }
+ }
+ else if ( IsStalker( ent ) )
+ {
+ // note: stalker location based damage is done in _ai_stalker.gnut.
+ switch ( hitGroup )
+ {
+ case HITGROUP_GEAR:
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+ break
+ }
+ }
+ else if ( isValidHeadShot )
+ {
+ damageMult_location = GetHeadshotDamageMultiplierFromDamageInfo( damageInfo )
+ }
+
+ // modify damage value based on where we hit
+ if ( damageMult_location != 1.0 )
+ {
+ if ( debugPrints )
+ {
+ printt( "Multiplier:", damageMult_location )
+ printt( "---------------------" )
+ }
+
+ DamageInfo_ScaleDamage( damageInfo, damageMult_location )
+ }
+}
+
+function PlayerDamageFeedback( entity ent, damageInfo )
+{
+// printt( "player damage feedback for " + ent )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ Assert( attacker.IsPlayer() )
+
+ int customDamageType = DamageInfo_GetCustomDamageType( damageInfo )
+
+ if ( IsMaxRangeShot( damageInfo ) )
+ customDamageType = customDamageType | DF_MAX_RANGE
+
+ if ( ent.GetHealth() - DamageInfo_GetDamage( damageInfo ) <= 0 )
+ {
+ if ( !ent.IsNPC() || ent.ai.killShotSound )
+ customDamageType = customDamageType | DF_KILLSHOT
+ }
+
+ attacker.NotifyDidDamage( ent, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamagePosition( damageInfo ), customDamageType, DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageFlags( damageInfo ), DamageInfo_GetHitGroup( damageInfo ), DamageInfo_GetWeapon( damageInfo ), DamageInfo_GetDistFromAttackOrigin( damageInfo ) )
+}
+
+void function UpdateLastDamageTime( entity ent )
+{
+ if ( !ent.IsPlayer() )
+ return
+
+ ent.p.lastDamageTime = Time()
+}
+
+void function PlayerDealtTitanDamage( entity attacker, entity victim, float savedDamage, var damageInfo )
+{
+ if ( attacker != victim )
+ {
+ attacker.p.titanDamageDealt += savedDamage
+
+#if MP
+ UpdateTitanWeaponDamageStat( attacker, savedDamage, damageInfo )
+
+ if ( attacker.IsTitan() )
+ {
+ attacker.p.titanDamageDealt_Stat += savedDamage
+ if ( attacker.p.titanDamageDealt_Stat >= 500 ) // buffer the titan stat damage so that we don't spam damage callbacks
+ {
+ UpdateTitanDamageStat( attacker, attacker.p.titanDamageDealt_Stat, damageInfo )
+ attacker.p.titanDamageDealt_Stat = 0
+ }
+ }
+#endif
+ }
+}
+
+function UpdateAttackerInfo( entity ent, entity attacker, damage )
+{
+ entity attackerPlayer = GetPlayerFromEntity( attacker )
+ if ( !attackerPlayer )
+ return
+
+ // cannot be your own last attacker
+ if ( attackerPlayer == ent )
+ return
+
+ if ( !damage || damage <= 0 )
+ return
+
+ if ( !("attackerInfo" in ent.s) )
+ ent.s.attackerInfo <- {}
+ else if ( ent.GetHealth() == ent.GetMaxHealth() )
+ ent.s.attackerInfo.clear()
+
+ if ( !(attackerPlayer.weakref() in ent.s.attackerInfo ) )
+ ent.s.attackerInfo[attackerPlayer.weakref()] <- 0
+
+ ent.s.attackerInfo[attackerPlayer.weakref()] += damage
+
+ ent.e.lastAttacker = attackerPlayer
+}
+
+entity function GetAttackerPlayerOrBossPlayer( entity attacker )
+{
+ if ( !IsValid( attacker ) )
+ return null
+
+ if ( attacker.IsPlayer() )
+ return attacker
+
+ entity bossPlayer = attacker.GetBossPlayer()
+ if ( !IsValid( bossPlayer ) )
+ return null
+
+ return bossPlayer
+}
+
+entity function GetAttackerOrLastAttacker( entity ent, damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( ShouldGetLastAttacker( ent, attacker ) == false )
+ return attacker
+
+ entity lastAttacker = GetLastAttacker( ent ) //Attacker doesn't work, get last attacker
+
+ if ( IsValid( lastAttacker ) == true )
+ return lastAttacker
+
+ //last attacker doesn't work, get latestAssistingPlayerInfo
+ AssistingPlayerStruct attackerInfo = GetLatestAssistingPlayerInfo( ent )
+ if ( IsValid( attackerInfo.player ) )
+ return attackerInfo.player
+
+ if ( IsValid( attacker ) ) //No Last Attacker and No Lastest Assisting Player, e.g. when you suicide before taking damage. Just return the attacker if valid
+ return attacker
+
+ return null
+}
+
+bool function ShouldGetLastAttacker( entity ent, entity attacker )
+{
+ if ( IsValid( attacker ) == false )
+ return true
+
+ if ( attacker == ent ) //suicide
+ return true
+
+ if ( attacker.IsPlayer() == false && attacker.IsNPC() == false ) //Environmental damage
+ return true
+
+ return false
+}
+
+function ClearLastAttacker( entity ent )
+{
+ ent.e.lastAttacker = null
+}
+
+entity function GetLastAttacker( entity ent )
+{
+ if ( ent.IsTitan() && IsValid( ent.GetTitanSoul() ) ) // JFS: second check is defensive
+ {
+ entity soul = ent.GetTitanSoul()
+ if ( soul.lastAttackInfo && "attacker" in soul.lastAttackInfo && IsValid( soul.lastAttackInfo.attacker ) )
+ return expect entity( soul.lastAttackInfo.attacker )
+ }
+
+ if ( !IsValid( ent.e.lastAttacker ) )
+ return null
+
+ return ent.e.lastAttacker
+}
+
+bool function PlayerOrNPCKilled( entity ent, var damageInfo )
+{
+ bool gamePlayingOrSuddenDeath = GamePlayingOrSuddenDeath() // Storing this off here, the game state can change in the callbacks below which may cause kills to not count
+
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ if ( damageSourceID == eDamageSourceId.round_end )
+ return false
+
+ entity attacker = GetAttackerOrLastAttacker( ent, damageInfo )
+ if ( !IsValid( attacker ) )
+ return false
+
+ if ( ent.IsPlayer() )
+ {
+ LogPlayerMatchStat_Death( ent )
+
+ if ( attacker.IsPlayer() && (attacker != ent) )
+ LogPlayerMatchStat_KilledAPilot( attacker )
+ }
+
+ if ( ent.IsNPC() && !IsValidNPCTarget( ent ) )
+ return false
+
+ if ( !attacker.IsPlayer() )
+ {
+ entity newAttacker = GetPlayerFromEntity( attacker )
+ if ( IsValid( newAttacker ) )
+ attacker = newAttacker
+ }
+
+ if ( ent.IsPlayer() )
+ {
+ //Do callbacks. Main reason we call this here as opposed to CodeCallback_OnPlayerKilled() is legacy script compatibility reasons.
+ //For example: In script immediately above this we change the attacker to get the player behind the kill, e.g. owner of a pet titan, etc. Bunch of registered callbacks depends on this.
+ foreach( callbackFunc in svGlobal.onPlayerKilledCallbacks )
+ callbackFunc( ent, attacker, damageInfo )
+ }
+ else if ( ent.IsNPC() )
+ {
+ //Do callbacks. Main reason we call this here as opposed to CodeCallback_OnNPCKilled() is legacy script compatibility reasons.
+ //For example: In script immediately above this we change the attacker to get the player behind the kill, e.g. owner of a pet titan, etc. Bunch of registered callbacks depends on this.
+ foreach( callbackFunc in svGlobal.onNPCKilledCallbacks )
+ {
+ callbackFunc( ent, attacker, damageInfo )
+ }
+ }
+
+ if ( ent.IsTitan() )
+ {
+ thread TitanVO_DelayedTitanDown( ent )
+ }
+
+ if ( !attacker.IsPlayer() )
+ {
+ // This gets the last player that did damage to the entity so that we can give him the kill
+ AssistingPlayerStruct attackerInfo = GetLatestAssistingPlayerInfo( ent )
+ attacker = attackerInfo.player
+
+ if ( !IsValid( attacker ) )
+ return true
+
+ // Hack - attacker history isn't on client to calculate if a player should get credit for a kill when AI steals the final killing shot while a player is damaging them.
+ array<entity> playerArray = GetPlayerArray()
+ foreach ( player in playerArray )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_SetAssistInformation", attackerInfo.damageSourceId, attacker.GetEncodedEHandle(), ent.GetEncodedEHandle(), attackerInfo.assistTime )
+ }
+ }
+
+ // player attacker only from here down
+
+ PreScoreEventUpdateStats( attacker, ent )
+ if ( ent.GetTeam() != attacker.GetTeam() )
+ {
+ if ( ent.IsPlayer() )
+ ScoreEvent_PlayerKilled( ent, attacker, damageInfo )
+ else if ( ent.IsTitan() && ent.IsNPC() )
+ ScoreEvent_TitanKilled( ent, attacker, damageInfo )
+ else
+ ScoreEvent_NPCKilled( ent, attacker, damageInfo )
+ }
+ PostScoreEventUpdateStats( attacker, ent )
+
+ if ( ent.GetTeam() == attacker.GetTeam() )
+ {
+ return false
+ }
+
+ // Respawn Kill INFECTION!!
+ if ( ent.IsPlayer() && attacker.IsPlayer() )
+ {
+ if ( ent.GetPersistentVar( "respawnKillInfected" ) && !attacker.GetPersistentVar( "respawnKillInfected" ) )
+ attacker.SetPersistentVar( "respawnKillInfected", true )
+ }
+
+ if ( gamePlayingOrSuddenDeath )
+ {
+ if ( ent.IsPlayer() )
+ {
+ if ( ent.IsTitan() )
+ {
+ //if we killed a player in a titan count two kills (one for the pilot, one for the titan )
+ attacker.AddToPlayerGameStat( PGS_KILLS, 2 )
+ attacker.AddToPlayerGameStat( PGS_TITAN_KILLS, 1 )
+ attacker.AddToPlayerGameStat( PGS_PILOT_KILLS, 1 )
+ }
+ else
+ {
+ attacker.AddToPlayerGameStat( PGS_KILLS, 1 )
+ attacker.AddToPlayerGameStat( PGS_PILOT_KILLS, 1 )
+ }
+ }
+ else
+ {
+ if ( ent.IsTitan() )
+ attacker.AddToPlayerGameStat( PGS_TITAN_KILLS, 1 )
+
+ if( !IsMarvin( ent ) && !ent.IsTitan() )
+ attacker.AddToPlayerGameStat( PGS_NPC_KILLS, 1 )
+ }
+ }
+
+ return true
+}
+
+// used to calculate build time credit in special cases. Cloak Drones and Suicide Spectres use it for now.
+float function CalculateBuildTimeCredit( entity attacker, entity target, float damage, int health, int maxHealth, string playlistVarStr, float defaultCredit )
+{
+ float titanSpawnDelay = GetTitanBuildTime( attacker )
+ float timerCredit = 0
+
+ health = maxint( 0, health ) // health should never be less then 0
+ if ( titanSpawnDelay && IsAlive( target ) )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( playlistVarStr, defaultCredit )
+
+ float dealtDamage = min( health, damage )
+ timerCredit = timerCredit * (dealtDamage / maxHealth )
+ }
+
+ return timerCredit
+}
+
+function UpdateNextRespawnTime( entity player, float time )
+{
+ player.nv.nextRespawnTime = time
+}
+
+bool function ShouldSetObserverTarget( entity attacker )
+{
+ if ( !IsAlive( attacker ) )
+ return false
+
+ if ( attacker.IsPlayer() && attacker.IsObserver() )
+ return false
+
+ return true
+}
+
+float function CalculateLengthOfKillReplay( entity player, int methodOfDeath ) //Meant to be called on the same frame player dies
+{
+ return GetDeathCamLength( player ) + GetKillReplayBeforeTime( player, methodOfDeath ) + GetKillReplayAfterTime( player )
+}
+
+float function GetKillReplayBeforeTime( entity player, int methodOfDeath )
+{
+ switch ( methodOfDeath )
+ {
+ case eDamageSourceId.damagedef_titan_fall:
+ case eDamageSourceId.damagedef_titan_hotdrop:
+ case eDamageSourceId.damagedef_reaper_fall:
+ case eDamageSourceId.droppod_impact:
+ return KILL_REPLAY_BEFORE_KILL_TIME_DROPPOD
+ }
+
+ if ( !GamePlayingOrSuddenDeath() )
+ return KILL_REPLAY_BEFORE_KILL_TIME_SHORT
+
+ float titanKillReplayTime = KILL_REPLAY_BEFORE_KILL_TIME_TITAN
+ float pilotKillReplayTime = KILL_REPLAY_BEFORE_KILL_TIME_PILOT
+ switch ( methodOfDeath )
+ {
+ case eDamageSourceId.titan_execution:
+ return titanKillReplayTime + 3.0
+
+ case eDamageSourceId.switchback_trap:
+ if ( player.IsTitan() )
+ return titanKillReplayTime + 6.0
+ else
+ return pilotKillReplayTime + 8.0
+ }
+
+ if ( player.IsTitan() )
+ return titanKillReplayTime
+
+ // titan recently?
+ if ( Time() - player.lastTitanTime < 5.0 )
+ return titanKillReplayTime
+
+ return pilotKillReplayTime
+}
+
+function TrackDestroyTimeForReplay( entity attacker, table replayTracker )
+{
+ float startTime = Time()
+ // tracks the time until the attacker becomes invalid
+ EndSignal( replayTracker, "OnDestroy" )
+
+ OnThreadEnd(
+ function () : ( replayTracker, startTime )
+ {
+ replayTracker.validTime = Time() - startTime
+ }
+ )
+
+ string signal = "OnDestroy"
+
+ if ( IsAlive( attacker ) )
+ attacker.WaitSignal( signal )
+ else
+ WaitSignalOnDeadEnt( attacker, signal )
+}
+
+#if MP
+function PlayerWatchesKillReplay( entity player, int inflictorEHandle, int attackerViewIndex, float timeSinceAttackerSpawned, float timeOfDeath, float beforeTime, table replayTracker )
+{
+ OnThreadEnd(
+ function () : ( player, replayTracker )
+ {
+ Signal( replayTracker, "OnDestroy" )
+ }
+ )
+
+ player.EndSignal( "RespawnMe" )
+
+ float timeBeforeKill = beforeTime
+ float timeAfterKill = GetKillReplayAfterTime( player )
+
+ if ( timeBeforeKill > timeSinceAttackerSpawned )
+ timeBeforeKill = timeSinceAttackerSpawned
+
+ float replayDelay = timeBeforeKill + ( Time() - timeOfDeath )
+ if ( replayDelay < 0 )
+ {
+ print( "PlayerWatchesKillReplay(): replayDelay is < 0 (" + replayDelay + "). Aborting kill replay.\n" )
+ return
+ }
+
+ player.SetKillReplayDelay( replayDelay, THIRD_PERSON_KILL_REPLAY_ALWAYS )
+ player.SetKillReplayInflictorEHandle( inflictorEHandle )
+ player.SetKillReplayVictim( player )
+ player.SetViewIndex( attackerViewIndex )
+
+ wait timeBeforeKill
+
+ if ( replayTracker.validTime != null && replayTracker.validTime < timeAfterKill )
+ {
+ float waitTime = expect float( replayTracker.validTime ) - 0.1 // cut off just before ent becomes invalid in the past
+ if ( waitTime > 0 )
+ wait waitTime
+ }
+ else
+ {
+ wait timeAfterKill
+ }
+}
+#endif // #if MP
+
+bool function ClientCommand_SelectRespawn( entity player, array<string> args )
+{
+ if ( IsAlive( player ) )
+ return true
+
+ if ( args.len() == 0 )
+ return true
+
+ int index = args[ 0 ].tointeger()
+
+ switch ( index )
+ {
+ case 1:
+ player.SetPersistentVar( "spawnAsTitan", true )
+ break
+ case 2:
+ player.SetPersistentVar( "spawnAsTitan", false )
+ break
+ }
+
+ return true
+}
+
+
+bool function ClientCommand_RespawnPlayer( entity player, array<string>args )
+{
+ if ( IsSingleplayer() )
+ return true
+
+ if ( IsAlive( player ) )
+ return true
+
+ if ( args.len() != 1 )
+ return true
+
+ string opParm = args[ 0 ]
+
+ if ( opParm.find( "burncard" ) != null )
+ {
+ //int burnCard = opParm.tointeger()
+ //SetPlayerBurnCardSlotToActivate( player, burnCard )
+ return true
+ }
+ else if ( opParm == "Titan" )
+ {
+ player.SetPersistentVar( "spawnAsTitan", true )
+ }
+ else if ( opParm == "Pilot" )
+ {
+ player.SetPersistentVar( "spawnAsTitan", false )
+ }
+
+ float deathCamLength = GetDeathCamLength( player )
+ float skipBufferTime = 0.5
+ if ( Time() > (player.p.postDeathThreadStartTime + deathCamLength) - skipBufferTime )
+ {
+ player.s.respawnSelectionDone = true
+ player.Signal( "RespawnMe" )
+ }
+
+ return true
+}
+
+function AIChatter( string alias, int team, vector origin )
+{
+ array<entity> ai = GetNearbyFriendlyGrunts( origin, team )
+
+ if ( ai.len() > 0 )
+ {
+ PlaySquadConversationToAll( alias, ai[0] )
+ }
+}
+
+const MAX_ACTIVITY_DISABLED = 0
+const MAX_ACTIVITY_PILOTS = 1
+const MAX_ACTIVITY_TITANS = 2
+const MAX_ACTIVITY_PILOTS_AND_TITANS = 3
+const MAX_ACTIVITY_CONGER_MODE = 4
+
+bool function GetPilotBotFlag()
+{
+ // IMPORTANT: Please call this consistently instead of Flag( "PilotBot" )
+ // Force titan or pilot bots according to max activity mode if it is enabled.
+ // Otherwise, leave the "pilotBot" flag alone and do what the game mode wants.
+ int max_activity_mode = GetConVarInt( "max_activity_mode" )
+ if ( max_activity_mode == MAX_ACTIVITY_PILOTS || max_activity_mode == MAX_ACTIVITY_PILOTS_AND_TITANS )
+ return true
+ else if ( max_activity_mode == MAX_ACTIVITY_TITANS )
+ return false
+ else if ( max_activity_mode == MAX_ACTIVITY_CONGER_MODE )
+ return rand() % 2 != 0 // conger mode: 50/50 pilot and titan bots!
+ else
+ return Flag( "PilotBot" )
+
+ unreachable
+}
+
+
+function DoRespawnPlayer( entity player, entity spawnPoint )
+{
+ player.p.lastSpawnPoint = spawnPoint
+ player.RespawnPlayer( spawnPoint ) //This will send "OnRespawned" signal, killing the thread if started from PostDeathThread
+}
+
+function SetupPilotSpawnOnRematch( entity player )
+{
+ // clear respawn countdown message
+ if ( GetWaveSpawnType() == eWaveSpawnType.DROPSHIP )
+ MessageToPlayer( player, eEventNotifications.Clear )
+
+ player.SetOrigin( player.p.rematchOrigin )
+
+ if ( GetWaveSpawnType() == eWaveSpawnType.DISABLED )
+ wait 0.9
+
+ if ( IsAlive( player ) )//HACK: This seems terrible, we shouldn't have to do this
+ {
+ printt( "This happened one time, in retail." )
+ return
+ }
+
+ if ( ShouldGivePlayerInfoOnSpawn() )
+ thread GivePlayerInfoOnSpawn( player )
+
+ return
+}
+
+bool function ShouldGivePlayerInfoOnSpawn()
+{
+ return GetCurrentPlaylistVarInt( "minimap_sonar_pulse_on_respawn", 0 ) > 0
+}
+
+function GivePlayerInfoOnSpawn( entity player )
+{
+ player.EndSignal( "OnDeath" )
+
+ //PrintFunc()
+
+ while( player.IsWatchingKillReplay() )
+ WaitFrame()
+
+ //printt( " GivePlayerInfoOnSpawn Player isn't watching kill replay anymore!" )
+
+ wait 0.2 //Hack: Have to wait even though player should not be watching kill replay anymore...
+
+ //This needed a wait, probably because at this time we haven't given them loadouts yet, so when we do give them loadouts it strips out the passive?
+ thread ScanMinimap( player, true, 0.5 ) //x second minimap pulse
+}
+
+bool function ShouldStartSpawn( entity player )
+{
+ if ( Riff_FloorIsLava() )
+ return false
+
+ if ( Flag( "ForceStartSpawn" ) )
+ return true
+
+ if ( Flag( "IgnoreStartSpawn" ) )
+ return false
+
+ if ( GetGameState() <= eGameState.Prematch )
+ return true
+
+ if ( player.s.respawnCount )
+ return false
+
+ return GameTime_PlayingTime() < START_SPAWN_GRACE_PERIOD
+}
+
+void function PlayerSpawnsIntoPetTitan( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ entity titan = player.GetPetTitan()
+
+ vector origin = titan.GetOrigin() + Vector( 0, 0, 600 )
+ vector angles = titan.GetAngles()
+
+ entity camera = CreateTitanDropCamera( origin, Vector(90,angles.y,0) )
+ player.SetViewEntity( camera, false )
+
+ player.isSpawning = true // set this to prevent .isSpawning checks from returning false
+
+ angles.x = 70
+
+ player.SetOrigin( origin )
+ player.SnapEyeAngles( angles )
+ player.SetVelocity( Vector( 0.0, 0.0, 0.0 ) )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ {
+ player.ClearViewEntity()
+ player.ClearSpawnPoint()
+ player.isSpawning = null
+ }
+ }
+ )
+
+ wait 0.2
+
+ local criteria = {
+ embark = "above_close",
+ titanCanStandRequired = true
+ }
+
+ local embarkAction
+ embarkAction = FindEmbarkActionForCriteria( criteria )
+ if ( embarkAction == null )
+ embarkAction = GetRandomEmbarkAction()
+
+ if ( IsValid( camera ) )
+ {
+ // camera can be invalid for a moment when server shuts down
+ // camera.FireNow( "Disable", "!activator", null, player )
+ camera.Destroy()
+ }
+
+ DoRespawnPlayer( player, null )
+
+ if ( PlayerCanSpawnIntoTitan( player ) )
+ {
+ table action = expect table( GenerateEmbarkActionTable( player, titan, embarkAction ) )
+ PlayerEmbarksTitan( player, titan, action )
+ }
+}
+
+entity function CreateTitanDropCamera( origin, angles )
+{
+ entity viewControl = CreateEntity( "point_viewcontrol" )
+ viewControl.kv.spawnflags = 56 // infinite hold time, snap to goal angles, make player non-solid
+
+ viewControl.SetOrigin( origin )
+ viewControl.SetAngles( angles )
+ DispatchSpawn( viewControl )
+
+ return viewControl
+}
+
+entity function CreateDropPodViewController( entity pod )
+{
+ entity viewControl = CreateEntity( "point_viewcontrol" )
+ viewControl.kv.spawnflags = 56 // infinite hold time, snap to goal angles, make player non-solid
+
+ viewControl.SetOrigin( pod.GetOrigin() + Vector( 44, -64, 520 ) )
+ float yaw = pod.GetAngles().y
+ viewControl.SetAngles( Vector( 90, yaw + 10, 0 ) )
+ DispatchSpawn( viewControl )
+
+ viewControl.SetParent( pod )
+
+ return viewControl
+}
+
+
+function ClearEntInUseOnDestroy( dropPoint, dropPod )
+{
+ dropPod.WaitSignal( "OnDestroy" )
+ dropPoint.e.spawnPointInUse = false
+}
+
+float function GetPlayerLastRespawnTime( entity player )
+{
+ return expect float( player.s.respawnTime )
+}
+
+entity function GetEmbarkPlayer( entity titan )
+{
+ if ( "embarkingPlayer" in titan.s )
+ return expect entity( titan.s.embarkingPlayer )
+
+ return null
+}
+
+entity function GetDisembarkPlayer( entity titan )
+{
+ if ( "disembarkingPlayer" in titan.s )
+ return expect entity( titan.s.disembarkingPlayer )
+
+ return null
+}
+
+entity function GetEmbarkDisembarkPlayer( entity titan )
+{
+ entity result = GetEmbarkPlayer( titan )
+
+ if ( IsValid( result ) )
+ return result
+
+ result = GetDisembarkPlayer( titan )
+ if ( IsValid( result ) )
+ return result
+
+ return null
+}
+
+void function CodeCallback_OnNPCKilled( entity npc, var damageInfo )
+{
+ if ( IsSingleplayer() )
+ {
+ OnNPCKilled_SP( npc, damageInfo )
+ return
+ }
+
+ HandleDeathPackage( npc, damageInfo )
+
+ if ( npc.IsTitan() )
+ {
+ // if a player is getting in, kill him too
+ entity player = GetEmbarkPlayer( npc )
+ if ( IsAlive( player ) )
+ {
+ // kill the embarking player
+ //printt( "Killed embarking player" )
+ KillFromInfo( player, damageInfo )
+ }
+
+ if ( !GetDoomedState( npc ) )
+ {
+ // Added via AddCallback_OnTitanDoomed
+ foreach ( callbackFunc in svGlobal.onTitanDoomedCallbacks )
+ {
+ callbackFunc( npc, damageInfo )
+ }
+ }
+ }
+
+ PlayerOrNPCKilled( npc, damageInfo )
+}
+
+void function OnNPCKilled_SP( entity npc, var damageInfo )
+{
+ HandleDeathPackage( npc, damageInfo )
+
+ if ( npc.IsTitan() )
+ {
+ // if a player is getting in, kill him too
+ entity player = GetEmbarkPlayer( npc )
+ if ( IsAlive( player ) )
+ {
+ // kill the embarking player
+ //printt( "Killed embarking player" )
+ KillFromInfo( player, damageInfo )
+ }
+
+ if ( !GetDoomedState( npc ) )
+ {
+ // Added via AddCallback_OnTitanDoomed
+ foreach ( callbackFunc in svGlobal.onTitanDoomedCallbacks )
+ {
+ callbackFunc( npc, damageInfo )
+ }
+ }
+ }
+
+ entity attacker = GetAttackerOrLastAttacker( npc, damageInfo )
+ if ( !IsValid( attacker ) )
+ return
+
+ if ( !attacker.IsPlayer() )
+ {
+ entity newAttacker = GetPlayerFromEntity( attacker )
+ if ( IsValid( newAttacker ) )
+ attacker = newAttacker
+ }
+
+ foreach( callbackFunc in svGlobal.onNPCKilledCallbacks )
+ {
+ callbackFunc( npc, attacker, damageInfo )
+ }
+
+ if ( npc.IsTitan() )
+ thread TitanVO_DelayedTitanDown( npc )
+}
+
+void function CodeCallback_OnEntityDestroyed( entity ent )
+{
+ // Must do ent.SetDoDestroyCallback( true ) to get this callback
+// print( "OnEntityDestroyed " + ent.entindex() + "\n" )
+
+ if ( "onEntityDestroyedCallbacks" in ent.s )
+ {
+ foreach ( callbackFunc in ent.s.onEntityDestroyedCallbacks )
+ {
+ callbackFunc( ent )
+ }
+ }
+}
+
+function AddEntityDestroyedCallback( ent, callbackFunc )
+{
+ AssertParameters( callbackFunc, 1, "entity" )
+
+ if ( !( "onEntityDestroyedCallbacks" in ent.s ) )
+ ent.s.onEntityDestroyedCallbacks <- []
+
+ ent.s.onEntityDestroyedCallbacks.append( callbackFunc )
+
+ // set this or else the ent won't run CodeCallback_OnEntityDestroyed at all
+ ent.SetDoDestroyCallback( true )
+}
+
+bool function WeaponInterruptsCloak( entity weapon )
+{
+ if ( !IsValid( weapon ) )
+ return false
+
+ return weapon.GetWeaponInfoFileKeyField( "does_not_interrupt_cloak" ) != 1
+}
+
+void function CodeCallback_WeaponFireInCloak( entity player )
+{
+ if ( !WeaponInterruptsCloak( player.GetActiveWeapon() ) )
+ return
+
+ if ( player.IsTitan() ) // Fix timing issue with auto-eject cloak and firing your weapon as a Titan cancelling it. This assumes we never want cloaked titans!
+ return
+
+ // if ( player.cloakedForever )
+ // {
+ // player.SetCloakFlicker( 1.0, 2.0 )
+ // return
+ // }
+
+ // // Check if we are allowed some cloaked shots based on ability selection
+ // if ( player.s.cloakedShotsAllowed > 0 )
+ // {
+ // player.s.cloakedShotsAllowed--
+ // return
+ // }
+
+ if ( IsMultiplayer() )
+ {
+ //player.SetCloakFlicker( 1.0, 2.0 )
+
+ DisableCloak( player, 0.5 )
+ entity weapon = player.GetOffhandWeapon( OFFHAND_LEFT )
+ //printt( "weapon", weapon.GetWeaponClassName() )
+ // JFS; need code feature to properly reset next attack time/cooldown stuff
+ if ( IsValid( weapon ) && weapon.GetWeaponClassName() == "mp_ability_cloak" )
+ {
+ player.TakeOffhandWeapon( OFFHAND_LEFT )
+ player.GiveOffhandWeapon( "mp_ability_cloak", OFFHAND_LEFT )
+ weapon = player.GetOffhandWeapon( OFFHAND_LEFT )
+ weapon.SetWeaponPrimaryClipCountAbsolute( 0 )
+ }
+ }
+ else
+ {
+ DisableCloak( player, 0.5 )
+ }
+}
+
+// need "you will change class next time" message
+function OnPlayerCloseClassMenu( entity player )
+{
+ if ( GetGameState() <= eGameState.Prematch )
+ return
+
+ if ( player.IsEntAlive() )
+ return
+
+ if ( player.s.inPostDeath )
+ return
+
+ if ( IsValid( player.isSpawning ) )
+ return
+
+ thread DecideRespawnPlayer( player ) // there is a wait that happens later when using rematch burncard in Frontier Defense.
+}
+
+// playerconnected Reload
+void function CodeCallback_OnClientReloadConnectionCompleted( entity player )
+{
+ FinishClientScriptInitialization( player )
+}
+
+
+bool function ShouldPlayerHaveLossProtection( entity player )
+{
+ if ( level.nv.matchProgress < GetCurrentPlaylistVarInt( "matchLossProtectionThreshold", 10 ) )
+ return false
+
+ if ( IsPrivateMatch() )
+ return false
+
+ if ( IsFFAGame() )
+ return true
+
+ int team = player.GetTeam()
+ int otherTeam = GetOtherTeam( team )
+ int teamScore = IsRoundBased() ? GameRules_GetTeamScore2( team ) : GameRules_GetTeamScore( team )
+ int otherTeamScore = IsRoundBased() ? GameRules_GetTeamScore2( otherTeam ) : GameRules_GetTeamScore( otherTeam )
+
+ if ( teamScore < otherTeamScore )
+ return true
+
+ return false
+}
+
+// This server will recieve this command from the client once they have loaded/run all of their scripts
+// Any client hud initialization should be done here
+function FinishClientScriptInitialization( entity player )
+{
+ printt( "Player client script initialization complete: " + player );
+
+ player.p.clientScriptInitialized = true
+
+ SyncServerVars( player )
+ SyncEntityVars( player )
+ SyncUIVars( player )
+
+ Remote_CallFunction_Replay( player, "ServerCallback_ClientInitComplete" )
+}
+
+function NotifyClientsOfConnection( entity player, state )
+{
+ int playerEHandle = player.GetEncodedEHandle()
+ array<entity> players = GetPlayerArray()
+ foreach ( ent in players )
+ {
+ if ( ent != player )
+ Remote_CallFunction_Replay( ent, "ServerCallback_PlayerConnectedOrDisconnected", playerEHandle, state )
+ }
+}
+
+function NotifyClientsOfTeamChange( entity player, int oldTeam, int newTeam )
+{
+ int playerEHandle = player.GetEncodedEHandle()
+ array<entity> players = GetPlayerArray()
+ foreach ( ent in players )
+ {
+ //if ( ent != player )
+ Remote_CallFunction_Replay( ent, "ServerCallback_PlayerChangedTeams", playerEHandle, oldTeam, newTeam )
+ }
+}
+
+
+bool function IsValidNPCTarget( entity ent )
+{
+ switch ( ent.GetClassName() )
+ {
+ case "npc_marvin":
+ case "npc_soldier":
+ case "npc_spectre":
+ case "npc_stalker":
+ case "npc_super_spectre":
+ case "npc_prowler":
+ case "npc_drone":
+ case "npc_titan":
+ case "npc_turret_sentry":
+ case "npc_turret_mega":
+ case "npc_dropship":
+ return true
+ }
+
+ return false
+}
+
+int function CodeCallback_GetWeaponDamageSourceId( entity weapon )
+{
+ string classname = weapon.GetWeaponClassName()
+
+ #if DEV
+ if ( ("devWeapons" in level) && classname in level.devWeapons )
+ return 0
+
+ #endif
+ //Filter out abilities for now
+ if ( !(classname in eDamageSourceId) )
+ return damagedef_unknown
+
+ //Assert( classname in getconsttable().eDamageSourceId, classname + " not added to eDamageSourceId enum" )
+ int damageSourceInt = eDamageSourceId[ classname ]
+ return damageSourceInt
+}
+
+
+
+
+function TriggerHurtSetup()
+{
+ file.hurtTriggers.extend( GetEntArrayByClass_Expensive( "trigger_hurt" ) )
+ foreach( trigger in file.hurtTriggers )
+ {
+ trigger.ConnectOutput( "OnStartTouch", TriggerHurtEnter )
+ }
+}
+
+void function TriggerHurtEnter( entity trigger, entity ent, entity caller, var value )
+{
+ if ( ent.e.destroyTriggerHurt )
+ ent.Destroy()
+}
+
+#if MP
+table< entity, table< entity, bool > > oob_triggerEntPairs
+
+void function SetupOutOfBoundsTrigger( entity trigger )
+{
+ if ( !(trigger in oob_triggerEntPairs) )
+ oob_triggerEntPairs[trigger] <- {}
+}
+#endif
+
+function OutOfBoundsSetup()
+{
+ file.outOfBoundsTriggers.extend( GetEntArrayByClass_Expensive( "trigger_out_of_bounds" ) )
+ foreach( trigger in file.outOfBoundsTriggers )
+ {
+ #if MP
+ SetupOutOfBoundsTrigger( trigger )
+ trigger.ConnectOutput( "OnStartTouch", EntityEnterOutOfBoundsTrig )
+ trigger.ConnectOutput( "OnEndTouch", EntityLeaveOutOfBoundsTrig )
+ #else
+ trigger.ConnectOutput( "OnStartTouch", EntityOutOfBounds )
+ trigger.ConnectOutput( "OnEndTouch", EntityBackInBounds )
+ #endif
+ }
+
+ AddCallback_GameStateEnter( eGameState.Postmatch, OutOfBoundsDisable )
+}
+
+void function OutOfBoundsDisable()
+{
+ foreach( trigger in file.outOfBoundsTriggers )
+ {
+ #if MP
+ foreach ( ent, val in oob_triggerEntPairs[trigger] )
+ oob_triggerEntPairs[trigger][ent] = false
+ trigger.DisconnectOutput( "OnStartTouch", EntityEnterOutOfBoundsTrig )
+ trigger.DisconnectOutput( "OnEndTouch", EntityLeaveOutOfBoundsTrig )
+ #else
+ trigger.DisconnectOutput( "OnStartTouch", EntityOutOfBounds )
+ trigger.DisconnectOutput( "OnEndTouch", EntityBackInBounds )
+ #endif
+ }
+}
+
+bool function IsPointOutOfBounds( vector point )
+{
+ foreach ( trigger in file.outOfBoundsTriggers )
+ {
+ if ( trigger.ContainsPoint( point ) )
+ return true
+ }
+ return false
+}
+
+#if MP
+void function EntityEnterOutOfBoundsTrig( entity trigger, entity ent, entity caller, var value )
+{
+ if ( !IsValid( ent ) || !ent.IsPlayer() )
+ {
+ EntityOutOfBounds( trigger, ent, null, null )
+ return
+ }
+
+ if ( !(ent in oob_triggerEntPairs[trigger]) )
+ {
+ oob_triggerEntPairs[trigger][ent] <- true
+ thread EntityCheckOutOfBoundsThread( trigger, ent )
+ }
+ else
+ {
+ oob_triggerEntPairs[trigger][ent] = true
+ // thread is already running
+ }
+}
+
+void function EntityLeaveOutOfBoundsTrig( entity trigger, entity ent, entity caller, var value )
+{
+ if ( !(ent in oob_triggerEntPairs[trigger]) )
+ {
+ EntityBackInBounds( trigger, ent, null, null )
+ return
+ }
+
+ oob_triggerEntPairs[trigger][ent] = false // tell thread to stop
+}
+
+bool function TriggerIsTouchingPlayerHullAtPoint( entity player, entity trigger, float triggerminz, vector pos, float radius )
+{
+ if ( trigger.GetClassName() == "trigger_cylinder" )
+ {
+ array<entity> touchingEnts = trigger.GetTouchingEntities()
+ return touchingEnts.contains( player )
+ }
+ else
+ {
+ return BrushTriggerIsTouchingPlayerHullAtPoint( trigger, triggerminz, pos, radius )
+ }
+
+ unreachable
+}
+
+bool function BrushTriggerIsTouchingPlayerHullAtPoint( entity trigger, float triggerminz, vector pos, float radius )
+{
+ if ( pos.z < triggerminz )
+ return false
+
+ radius *= 1.0824 // expand by 1/cos(22.5) so that an octagon circumscribes the circle
+
+ if ( trigger.ContainsPoint( pos ) ||
+ trigger.ContainsPoint( pos + <radius,0,0> ) ||
+ trigger.ContainsPoint( pos + < -radius,0,0> ) ||
+ trigger.ContainsPoint( pos + <0,radius,0> ) ||
+ trigger.ContainsPoint( pos + <0,-radius,0> ) )
+ return true
+
+ float radius45 = radius * 0.7071
+
+ if ( trigger.ContainsPoint( pos + <radius45,radius45,0> ) ||
+ trigger.ContainsPoint( pos + < -radius45,-radius45,0> ) ||
+ trigger.ContainsPoint( pos + <radius45,-radius45,0> ) ||
+ trigger.ContainsPoint( pos + < -radius45,radius45,0> ) )
+ return true
+
+ return false
+}
+
+void function EntityCheckOutOfBoundsThread( entity trigger, entity ent )
+{
+ float minz = trigger.GetOrigin().z + trigger.GetBoundingMins().z
+ float radius = ent.GetBoundingMaxs().x
+
+ bool wasTouching = false
+ for ( ;; )
+ {
+ wait 0.099
+
+ if ( !IsValid( ent ) )
+ break
+
+ if ( !oob_triggerEntPairs[trigger][ent] )
+ break
+
+ bool isTouching
+ if ( ent.IsOnGround() )
+ {
+ if ( ent.IsWallRunning() && !ent.IsWallHanging() )
+ {
+ isTouching = TriggerIsTouchingPlayerHullAtPoint( ent, trigger, minz, ent.GetOrigin() + <0,0,10>, radius )
+ }
+ else
+ {
+ isTouching = true
+ }
+ }
+ else
+ {
+ vector startpos = ent.GetOrigin()
+ vector endpos = startpos
+ endpos.z -= 2048
+
+ TraceResults result = TraceHull( startpos, endpos, ent.GetBoundingMins(), ent.GetBoundingMaxs(), ent, TRACE_MASK_PLAYERSOLID, TRACE_COLLISION_GROUP_PLAYER )
+ if ( result.startSolid || result.fraction >= 1 || TriggerIsTouchingPlayerHullAtPoint( ent, trigger, minz, result.endPos + <0,0,40>, radius ) )
+ {
+ //DebugDrawLine( startpos, result.endPos, 255,255,255, true, 3.0 )
+ isTouching = true
+ }
+ else
+ {
+ //DebugDrawLine( startpos, result.endPos, 255,0,0, true, 3.0 )
+ isTouching = false
+ }
+ }
+
+ if ( isTouching == wasTouching )
+ continue
+
+ wasTouching = isTouching
+ if ( isTouching )
+ {
+ EntityOutOfBounds( trigger, ent, null, null )
+ }
+ else
+ {
+ EntityBackInBounds( trigger, ent, null, null )
+ }
+ }
+
+ if ( wasTouching )
+ {
+ EntityBackInBounds( trigger, ent, null, null )
+ }
+
+ delete oob_triggerEntPairs[trigger][ent]
+}
+#endif
+
+void function EntityOutOfBounds( entity trigger, entity ent, entity caller, var value )
+{
+ //printt( "ENTITY", ent, "IS OUT OF BOUNDS ON TRIGGER", trigger )
+
+ if ( ent.e.destroyOutOfBounds )
+ ent.Destroy()
+
+ if ( !IsValidOutOfBoundsEntity( ent, trigger ) )
+ return
+
+ //printt( "Valid Out OfBounds Entity, EntityOutOfBounds" )
+
+ if ( !(ent in file.outOfBoundsTable) ) //Note that we never remove the ent from the table after adding it
+ {
+ OutOfBoundsDataStruct initialDataStruct
+ initialDataStruct.timeBackInBound = max( 0, Time() - OUT_OF_BOUNDS_DECAY_TIME )
+
+ ManageAddEntToOutOfBoundsTable( ent, initialDataStruct )
+ }
+
+ OutOfBoundsDataStruct dataStruct = file.outOfBoundsTable[ ent ]
+
+ dataStruct.outOfBoundsTriggersTouched++
+
+ Assert( dataStruct.outOfBoundsTriggersTouched > 0 )
+
+ // Not already touching another trigger
+ if ( dataStruct.outOfBoundsTriggersTouched == 1 )
+ {
+ float decayTime = max( 0, Time() - dataStruct.timeBackInBound - OUT_OF_BOUNDS_DECAY_DELAY )
+ float outOfBoundsTimeRegained = decayTime * ( OUT_OF_BOUNDS_TIME_LIMIT / OUT_OF_BOUNDS_DECAY_TIME )
+ float deadTime = clamp( dataStruct.timeLeftBeforeDyingFromOutOfBounds + outOfBoundsTimeRegained, 0.0, OUT_OF_BOUNDS_TIME_LIMIT )
+
+ //printt( "Decay Time: " + decayTime + ", outOfBoundsTimeRegained:" + outOfBoundsTimeRegained + ", timeLeftBeforeDyingFromOutOfBounds: " + dataStruct.timeLeftBeforeDyingFromOutOfBounds + ", deadTime: " + deadTime )
+
+ dataStruct.timeLeftBeforeDyingFromOutOfBounds = deadTime
+
+ ent.SetOutOfBoundsDeadTime( Time() + deadTime )
+
+ thread KillEntityOutOfBounds( ent, trigger )
+ }
+
+ //printt( "ent.GetOutOfBoundsDeadTime():", ent.GetOutOfBoundsDeadTime() )
+}
+
+bool function EntityIsOutOfBounds( entity ent )
+{
+ if ( !( ent in file.outOfBoundsTable ) )
+ return false
+ return file.outOfBoundsTable[ ent ].outOfBoundsTriggersTouched > 0
+}
+
+void function EntityBackInBounds( entity trigger, entity ent, entity caller, var value )
+{
+ //printt( "ENTITY", ent, "IS BACK IN BOUNDS OF TRIGGER", trigger )
+
+ if ( !IsValidOutOfBoundsEntity( ent, trigger ) )
+ return
+
+ //printt( "Valid Out OfBounds Entity, EntityBackInBounds" )
+
+ if ( !(ent in file.outOfBoundsTable) ) //Can go back in bounds even though we went out of bounds as an invalid ent, e.g. in a dropship
+ {
+ OutOfBoundsDataStruct initialDataStruct
+ ManageAddEntToOutOfBoundsTable( ent, initialDataStruct )
+
+ ent.SetOutOfBoundsDeadTime( 0.0 )
+ ent.Signal( "BackInBounds" )
+
+ return
+ }
+ else
+ {
+ OutOfBoundsDataStruct dataStruct = file.outOfBoundsTable[ ent ]
+
+ dataStruct.outOfBoundsTriggersTouched--
+ if ( dataStruct.outOfBoundsTriggersTouched < 0 ) //You can exit from bounds while being an invalid ent from out of bounds on the way in, e.g. during dropship anims, etc
+ dataStruct.outOfBoundsTriggersTouched = 0
+
+ if ( dataStruct.outOfBoundsTriggersTouched == 0 )
+ {
+ dataStruct.timeBackInBound = Time()
+ dataStruct.timeLeftBeforeDyingFromOutOfBounds = max( 0, ent.GetOutOfBoundsDeadTime() - Time() )
+ ent.SetOutOfBoundsDeadTime( 0.0 )
+ ent.Signal( "BackInBounds" )
+ return
+ }
+ }
+}
+
+void function KillEntityOutOfBounds( entity ent, entity trigger )
+{
+ if ( GetGameState() < eGameState.Playing )
+ return
+
+ Assert( ent.GetOutOfBoundsDeadTime() != 0 )
+ Assert( Time() <= ent.GetOutOfBoundsDeadTime() )
+
+ ent.EndSignal( "OnDeath" )
+ ent.Signal( "OutOfBounds" )
+ ent.EndSignal( "OutOfBounds" )
+ ent.EndSignal( "BackInBounds" )
+
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ if ( IsValid( ent ) && !IsAlive( ent ) )
+ {
+ file.outOfBoundsTable[ ent ].outOfBoundsTriggersTouched = 0
+ ent.SetOutOfBoundsDeadTime( 0 )
+ }
+ }
+ )
+
+ wait ent.GetOutOfBoundsDeadTime() - Time()
+
+ if ( !IsValidOutOfBoundsEntity( ent, trigger ) )
+ return
+
+ if ( ent.GetOutOfBoundsDeadTime() == 0 )
+ return
+
+ ent.Die( svGlobal.worldspawn, svGlobal.worldspawn, { scriptType = DF_INSTANT, damageSourceId = eDamageSourceId.outOfBounds } )
+}
+
+bool function IsValidOutOfBoundsEntity( entity ent, entity trigger )
+{
+ if ( !IsValid( ent ) )
+ return false
+
+ if ( !IsAlive( ent ) )
+ return false
+
+ int triggerTeam = expect int( trigger.kv.teamnumber.tointeger() )
+
+ Assert( triggerTeam >= 0 )
+
+ if ( triggerTeam != 0 && ent.GetTeam() != triggerTeam )
+ return false
+
+ // Temp hack for tday intro, might not keep this
+ if ( "disableOutOfBounds" in level && level.disableOutOfBounds == true )
+ return false
+
+ if ( ent.IsPlayer() )
+ {
+ if ( ent.IsNoclipping() && !ent.Anim_IsActive() ) //Need to check for Anim_IsActive because PlayAnim() calls will set IsNoclipping() to true. This caused a bug with ejecting out of a OutOfBounds trigger
+ return false
+
+ entity parentEnt = ent.GetParent()
+ if ( IsValid( parentEnt ) && IsDropship( parentEnt ) )
+ return false
+
+ return true
+ }
+
+ if ( ent.IsNPC() && ent.IsTitan() )
+ return true
+
+ return false
+}
+
+void function OnTitanBecomesPilot_OutOfBoundsCheck( entity pilot, entity npc_titan )
+{
+ if ( pilot.GetOutOfBoundsDeadTime() == 0 )
+ return
+
+ npc_titan.SetOrigin( npc_titan.GetOrigin() ) //Kinda a hack to force redetection of the Titan touching the out of bounds trigger
+}
+
+void function ManageAddEntToOutOfBoundsTable( entity ent, OutOfBoundsDataStruct dataStruct ) //Might be overkill, but: suggested by Haggerty to avoid leak of constantly adding ents to the file table without removing them
+{
+ //First clean up dead references in table
+ table< entity, OutOfBoundsDataStruct> tempTable = clone file.outOfBoundsTable
+
+ foreach( ent, dataStruct in tempTable )
+ {
+ if ( !IsValid( ent ) )
+ {
+ delete file.outOfBoundsTable[ ent ]
+ }
+ }
+
+ //Now add the new ent
+
+ file.outOfBoundsTable[ ent ] <- dataStruct
+}
+
+bool function PlayerCanSpawn( entity player )
+{
+ if ( IsAlive( player ) )
+ return false
+
+ if ( player.isSpawning )
+ return false
+
+ return true
+}
+
+function SetTitanAvailable( entity player )
+{
+ Assert( player.entindex() < 32 )
+ int shiftIndex = player.entindex() - 1
+ int elimMask = (1 << shiftIndex)
+
+ level.nv.titanAvailableBits = level.nv.titanAvailableBits | elimMask
+
+ #if MP
+ PIN_PlayerAbilityReady( player, "titanfall" )
+ #endif
+}
+
+function ClearTitanAvailable( entity player )
+{
+ Assert( player.entindex() < 32 )
+ int shiftIndex = player.entindex() - 1
+ int elimMask = (1 << shiftIndex)
+
+ level.nv.titanAvailableBits = level.nv.titanAvailableBits & (~elimMask)
+}
+
+
+
+function SetRespawnAvailable( entity player )
+{
+ Assert( player.entindex() < 32 )
+ int shiftIndex = player.entindex() - 1
+ int elimMask = (1 << shiftIndex)
+
+ level.nv.respawnAvailableBits = level.nv.respawnAvailableBits | elimMask
+}
+
+
+function ClearRespawnAvailable( entity player )
+{
+ Assert( player.entindex() < 32 )
+ int shiftIndex = player.entindex() - 1
+ int elimMask = (1 << shiftIndex)
+
+ level.nv.respawnAvailableBits = level.nv.respawnAvailableBits & (~elimMask)
+}
+
+
+void function SetPlayerEliminated( entity player )
+{
+ player.SetPlayerGameStat( PGS_ELIMINATED, 1 )
+}
+
+void function ClearPlayerEliminated( entity player )
+{
+ player.SetPlayerGameStat( PGS_ELIMINATED, 0 )
+}
+
+bool function IsPlayerEliminated( entity player )
+{
+ return (player.GetPlayerGameStat( PGS_ELIMINATED ) > 0)
+}
+
+bool function IsTeamEliminated( int team )
+{
+ array<entity> players = GetPlayerArrayOfTeam( team )
+
+ foreach ( player in players )
+ {
+ if ( IsPlayerEliminated( player ) != true )
+ return false
+ }
+
+ return true
+}
+
+// Clears all scoreboard data for the player to make sure we never use old data
+void function ClearPostGameScoreboardData( entity player )
+{
+ if ( !IsValid( player ) || !player.IsPlayer() )
+ return
+
+ player.SetPersistentVar( "isPostGameScoreboardValid", false )
+ player.SetPersistentVar( "isFDPostGameScoreboardValid", false )
+}
+
+bool function ShouldShowLossProtectionOnEOG( entity player )
+{
+ if ( player.p.hasMatchLossProtection != true )
+ return false
+
+ if ( player.GetTeam() == GetWinningTeam() )
+ return false
+
+ if ( IsPrivateMatch() )
+ return false
+
+ return true
+}
+
+bool function GameModeRemove( entity ent )
+{
+ string gameMode = GameRules_GetGameMode()
+ switch ( gameMode )
+ {
+ // These game modes have checkboxes in leveled
+ case LAST_TITAN_STANDING:
+ case TEAM_DEATHMATCH:
+ case ATTRITION:
+ case CAPTURE_POINT:
+ case CAPTURE_THE_FLAG:
+ case FORT_WAR:
+ case FFA:
+ case FD:
+ break
+
+ // These game modes use tdm spawns
+ case PILOT_SKIRMISH:
+ case WINGMAN_PILOT_SKIRMISH:
+ case MARKED_FOR_DEATH_PRO:
+ case MARKED_FOR_DEATH:
+ case T_DAY:
+ case AI_TDM:
+ case BOMB:
+ case HARDCORE_TDM:
+ case COLISEUM:
+ case HUNTED:
+ case DON:
+ case TITAN_BRAWL:
+ case SPEEDBALL:
+ gameMode = TEAM_DEATHMATCH
+ break
+
+ case RAID:
+ case ATCOOP:
+ case CONQUEST:
+ case PVE_SANDBOX:
+ gameMode = ATTRITION
+ break
+
+ case LTS_BOMB:
+ case WINGMAN_LAST_TITAN_STANDING:
+ gameMode = LAST_TITAN_STANDING
+ break
+
+ case FREE_AGENCY:
+ gameMode = FFA
+ break
+
+ default:
+ // If a game mode is not handled in here, spawnpoints won't have checkboxes that correspond to it, so all spawnpoints will be used in that mode, which is probably bad.
+ Assert( false, "Game mode " + gameMode + " not handled in GameModeRemove()" )
+ }
+
+ AT_CollisionCleanup( ent )
+
+ string gamemodeKey = "gamemode_" + gameMode
+ if ( ent.HasKey( gamemodeKey ) && (ent.kv[gamemodeKey] == "0" || ent.kv[gamemodeKey] == "") )
+ {
+ // printt( "Removing ent " + ent.GetClassName() + " with " + gamemodeKey + " = \"" + ent.kv[gamemodeKey] + "\" at " + ent.GetOrigin() )
+ ent.Destroy()
+ return true
+ }
+ //printt( "keeping ent", ent.GetClassName() )
+
+ return false
+}
+
+void function AT_CollisionCleanup( entity spawnPoint )
+{
+ if ( spawnPoint.GetScriptName() == "at_mega_turret" )
+ {
+ if ( spawnPoint.GetLinkEnt() != null ) // assuming this is func_brush_navmesh_separator
+ {
+ entity brush = spawnPoint.GetLinkEnt()
+ brush.NotSolid()
+ }
+ }
+}
+
+
+void function EntityFire( entity ent, string fire )
+{
+ ent.Fire( fire )
+}
+
+void function EntityFireDelayed( entity ent, string fire, string parm, float delay )
+{
+ ent.Fire( fire, parm, delay )
+}
+
+#if MP
+void function AddOutOfBoundsTriggerWithParams( vector org, float radius = 250.0, float height = 250.0 )
+{
+ entity trigger = CreateEntity( "trigger_cylinder" )
+ trigger.SetRadius( radius )
+ trigger.SetAboveHeight( height ) //Still not quite a sphere, will see if close enough
+ trigger.SetBelowHeight( height )
+ trigger.SetOrigin( org )
+ DispatchSpawn( trigger )
+ SetupOutOfBoundsTrigger( trigger )
+ trigger.SetEnterCallback( OnOOBTriggerEnter )
+ trigger.SetLeaveCallback( OnOOBTriggerLeave )
+}
+
+void function OnOOBTriggerEnter( entity trigger, entity ent )
+{
+ EntityEnterOutOfBoundsTrig( trigger, ent, null, 0 )
+}
+
+void function OnOOBTriggerLeave( entity trigger, entity ent )
+{
+ EntityLeaveOutOfBoundsTrig( trigger, ent, null, 0 )
+}
+#endif \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut
new file mode 100644
index 00000000..d7db601b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut
@@ -0,0 +1,613 @@
+untyped
+global function BaseGametype_Init_MPSP
+global function CodeCallback_OnClientConnectionStarted
+global function CodeCallback_OnClientConnectionCompleted
+global function CodeCallback_OnClientDisconnected
+global function CodeCallback_OnPlayerRespawned
+global function CodeCallback_OnPlayerKilled
+global function DecideRespawnPlayer
+global function RespawnAsPilot
+global function RespawnAsTitan
+global function TryGameModeAnnouncement
+
+global function SetKillcamsEnabled
+global function KillcamsEnabled
+global function SetPlayerDeathsHidden
+global function TrackTitanDamageInPlayerGameStat
+
+global function ShouldEntTakeDamage_SPMP
+global function GetTitanBuildTime
+global function TitanPlayerHotDropsIntoLevel
+
+struct {
+ bool killcamsEnabled = true
+ bool playerDeathsHidden = false
+ int titanDamageGameStat = -1
+
+ entity intermissionCamera
+ array<entity> specCams
+} file
+
+void function BaseGametype_Init_MPSP()
+{
+ AddSpawnCallback( "info_intermission", SetIntermissionCamera )
+ AddCallback_EntitiesDidLoad( SetSpecCams )
+
+ RegisterSignal( "ObserverTargetChanged" )
+ AddClientCommandCallback( "spec_next", ClientCommandCallback_spec_next )
+ AddClientCommandCallback( "spec_prev", ClientCommandCallback_spec_prev )
+ AddClientCommandCallback( "spec_mode", ClientCommandCallback_spec_mode )
+
+ AddDamageCallback( "player", AddToTitanDamageStat )
+ AddDamageCallback( "npc_titan", AddToTitanDamageStat )
+}
+
+void function SetIntermissionCamera( entity camera )
+{
+ file.intermissionCamera = camera
+}
+
+void function SetSpecCams()
+{
+ // spec cams are called spec_cam1,2,3 etc by default, so this is the easiest way to get them imo
+ int camNum = 1
+ entity lastCam = null
+ do {
+ lastCam = GetEnt( "spec_cam" + camNum++ )
+
+ if ( lastCam != null )
+ file.specCams.append( lastCam )
+ } while ( lastCam != null )
+}
+
+void function CodeCallback_OnClientConnectionStarted( entity player )
+{
+ // not a real player?
+ #if DEV
+ if ( player.GetPlayerName() == "Replay" )
+ return
+ #endif
+
+ if ( IsLobby() )
+ {
+ Lobby_OnClientConnectionStarted( player )
+ return
+ }
+
+// ScreenFade( player, 0, 0, 0, 255, 2.0, 0.5, FFADE_IN | FFADE_PURGE )
+
+ SetTargetName( player, "player" + player.entindex() )
+
+ player.p.controllableProjectiles_scriptManagedID = CreateScriptManagedEntArray()
+ player.p.npcFollowersArrayID = CreateScriptManagedEntArray()
+
+ player.s = {}
+ player.s.attackerInfo <- {}
+ player.p.clientScriptInitialized = player.IsBot()
+ player.s.inPostDeath <- null
+ player.s.respawnCount <- 0
+ player.s.respawnTime <- 0
+ player.s.lostTitanTime <- 0
+ player.s.cloakedShotsAllowed <- 0
+ player.s.startDashMeleeTime <- 0
+ player.s.respawnSelectionDone <- true // this gets set to false in postdeaththread but we need it to be true when connecting
+ player.s.waveSpawnProtection <- false
+
+ player.s.nextStatUpdateFunc <- null
+
+ player.s.activeTrapArrayId <- CreateScriptManagedEntArray()
+
+ player.s.restartBurnCardEffectOnSpawn <- false
+ player.s.replacementDropInProgress <- false
+
+ player.s.inGracePeriod <- true
+
+ // should I just add these when playing coop?
+ player.s.usedLoadoutCrate <- false
+ player.s.restockAmmoTime <- 0
+ player.s.restockAmmoCrate <- null
+
+ player.s.autoTitanLastEngageCalloutTime <- 0
+ player.s.autoTitanLastEngageCallout <- null
+ player.s.lastAIConversationTime <- {} // when was a conversation last played?
+
+ player.s.updatedPersistenceOnDisconnect <- false
+
+ player.s.lastFriendlySpawnedOn <- null
+ player.s.nextWaveSpawnTime <- 0.0
+
+ player.s.meleeSlowMoEndTime <- 0.0
+
+ player.p.connectTime = Time()
+
+ Assert( !player._entityVars )
+ InitEntityVars( player )
+
+ // Added via AddCallback_OnClientConnecting
+ foreach ( callbackFunc in svGlobal.onClientConnectingCallbacks )
+ {
+ callbackFunc( player )
+ }
+
+ printl( "Player connect started: " + player )
+
+ InitPassives( player )
+}
+
+// playerconnected
+void function CodeCallback_OnClientConnectionCompleted( entity player )
+{
+ if ( IsLobby() )
+ {
+ Lobby_OnClientConnectionCompleted( player )
+ return
+ }
+
+ player.hasConnected = true
+
+ InitMeleeAnimEventCallbacks( player )
+ ZiplineInit( player )
+
+ UpdateMinimapStatus( player )
+ UpdateMinimapStatusToOtherPlayers( player )
+ MinimapPlayerConnected( player )
+ NotifyClientsOfConnection( player, 1 )
+ PlayCurrentTeamMusicEventsOnPlayer( player )
+ SetCurrentTeamObjectiveForPlayer( player )
+
+ entity skycam = GetEnt( "skybox_cam_level" )
+ if ( skycam != null )
+ player.SetSkyCamera( skycam )
+
+ FinishClientScriptInitialization( player )
+
+ // Added via AddCallback_OnClientConnected
+ foreach ( callbackFunc in svGlobal.onClientConnectedCallbacks )
+ {
+ callbackFunc( player )
+ }
+
+ if ( !Flag( "PlayerDidSpawn") )
+ __PlayerDidSpawn( player )
+
+ svGlobal.levelEnt.Signal( "PlayerDidSpawn", { player = player } )
+
+ // handle spawning late joiners
+ if ( GetGameState() == eGameState.Playing )
+ {
+ if ( RespawnsEnabled() )
+ {
+ // likely temp, deffo needs some work
+ if ( Riff_SpawnAsTitan() == 1 ) // spawn as titan
+ thread RespawnAsTitan( player )
+ else // spawn as pilot
+ RespawnAsPilot( player )
+ }
+ else
+ thread PlayerBecomesSpectator( player )
+ }
+}
+
+void function CodeCallback_OnClientDisconnected( entity player, string reason )
+{
+ if ( IsLobby() )
+ {
+ player.Signal( "_disconnectedInternal" )
+ UpdateBadRepPresent()
+ return
+ }
+
+ if ( !player.hasConnected )
+ return
+
+ // Added via AddCallback_OnClientDisconnected
+ foreach ( callbackFunc in svGlobal.onClientDisconnectedCallbacks )
+ {
+ callbackFunc( player )
+ }
+
+ player.Disconnected()
+ player.p.isDisconnected = true
+ player.CleanupMPClasses()
+}
+
+void function CodeCallback_OnPlayerRespawned( entity player )
+{
+ player.Signal( "OnRespawned" ) // kill any postdeaththreads that could be running
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_YouRespawned" )
+ player.s.respawnTime = Time()
+
+ Loadouts_TryGivePilotLoadout( player )
+
+ foreach ( void functionref( entity ) callback in svGlobal.onPlayerRespawnedCallbacks )
+ callback( player )
+}
+
+void function CodeCallback_OnPlayerKilled( entity player, var damageInfo )
+{
+ PlayerOrNPCKilled( player, damageInfo )
+ thread PostDeathThread_MP( player, damageInfo )
+}
+
+void function PostDeathThread_MP( entity player, var damageInfo ) // based on gametype_sp: postdeaththread_sp
+{
+ if ( player.s.inPostDeath )
+ return
+
+ float timeOfDeath = Time()
+ player.p.postDeathThreadStartTime = Time()
+
+ Assert( IsValid( player ), "Not a valid player" )
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "OnRespawned" )
+
+ player.p.deathOrigin = player.GetOrigin()
+ player.p.deathAngles = player.GetAngles()
+
+ player.s.inPostDeath = true
+ player.s.respawnSelectionDone = false
+
+ player.cloakedForever = false
+ player.stimmedForever = false
+ player.SetNoTarget( false )
+ player.SetNoTargetSmartAmmo( false )
+ player.ClearExtraWeaponMods()
+
+ player.AddToPlayerGameStat( PGS_DEATHS, 1 )
+
+ if ( player.IsTitan() )
+ SoulDies( player.GetTitanSoul(), damageInfo ) // cleanup some titan stuff, no idea where else to put this
+
+ ClearRespawnAvailable( player )
+
+ OnThreadEnd( function() : ( player )
+ {
+ if ( !IsValid( player ) )
+ return
+
+ player.SetPredictionEnabled( true )
+ player.s.inPostDeath = false
+ })
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ int methodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ player.p.rematchOrigin = player.p.deathOrigin
+ if ( IsValid( attacker ) && methodOfDeath == eDamageSourceId.titan_execution )
+ {
+ // execution can throw you out of the map
+ player.p.rematchOrigin = attacker.GetOrigin()
+ }
+
+ player.Signal( "RodeoOver" )
+ player.ClearParent()
+
+ // do some pre-replay stuff if we're gonna do a replay
+ float replayLength = CalculateLengthOfKillReplay( player, methodOfDeath )
+ bool shouldDoReplay = Replay_IsEnabled() && KillcamsEnabled() && ShouldDoReplay( player, attacker, replayLength, methodOfDeath )
+ table replayTracker = { validTime = null }
+ if ( shouldDoReplay )
+ thread TrackDestroyTimeForReplay( attacker, replayTracker )
+
+ int damageSource = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ //if ( damageSource == eDamageSourceId.fall )
+ //{
+ // // this is straight up just incorrect lol, based off tf1 stuff
+ //
+ // player.SetObserverModeStaticPosition( player.GetOrigin() )
+ // player.SetObserverModeStaticAngles( player.GetVelocity() * -1 )
+ //
+ // player.StartObserverMode( OBS_MODE_STATIC_LOCKED )
+ // player.SetObserverTarget( null )
+ //}
+ //else
+ //{
+ player.StartObserverMode( OBS_MODE_DEATHCAM )
+ if ( ShouldSetObserverTarget( attacker ) )
+ player.SetObserverTarget( attacker )
+ else
+ player.SetObserverTarget( null )
+ //}
+
+ if ( !file.playerDeathsHidden )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_YouDied", attacker.GetEncodedEHandle(), GetHealthFrac( attacker ), methodOfDeath )
+
+ float deathcamLength = GetDeathCamLength( player )
+ wait deathcamLength
+
+ // use info_intermission camera after deathcam, if it exists
+ if ( file.intermissionCamera != null )
+ {
+ player.SetObserverModeStaticPosition( file.intermissionCamera.GetOrigin() )
+ player.SetObserverModeStaticAngles( file.intermissionCamera.GetAngles() )
+ player.StartObserverMode( OBS_MODE_STATIC_LOCKED )
+ player.SetObserverTarget( null )
+ }
+
+ // quick note: in cases where player.Die() is called: e.g. for round ends, player == attacker
+ if ( shouldDoReplay )
+ {
+ player.SetPredictionEnabled( false )
+
+ player.watchingKillreplayEndTime = Time() + replayLength
+ float beforeTime = GetKillReplayBeforeTime( player, methodOfDeath )
+
+ replayTracker.validTime <- null
+
+ float respawnTime = Time() - 2 // seems to get the killreplay to end around the actual kill
+ if ( "respawnTime" in attacker.s )
+ respawnTime = Time() - expect float ( attacker.s.respawnTime )
+
+ thread PlayerWatchesKillReplayWrapper( player, attacker, respawnTime, timeOfDeath, beforeTime, replayTracker )
+ }
+
+ player.SetPlayerSettings( "spectator" ) // prevent a crash with going from titan => pilot on respawn
+
+ if ( RespawnsEnabled() )
+ {
+ // is it a good idea to do respawn code in postdeaththread? fuck if i know lol
+ float respawnDelay = max( 0, GetCurrentPlaylistVarFloat( "respawn_delay", 0.0 ) - deathcamLength )
+
+ print( "respawn delay " + respawnDelay )
+
+ UpdateNextRespawnTime( player, Time() + respawnDelay )
+ SetRespawnAvailable( player )
+
+ wait respawnDelay
+
+ player.WaitSignal( "RespawnMe" ) // set in base_gametype: ClientCommand_RespawnPlayer
+
+ player.SetPredictionEnabled( true )
+ ClearRespawnAvailable( player ) // need so the respawn icon doesn't show for like a frame on next death
+
+ if ( ( expect bool( player.GetPersistentVar( "spawnAsTitan" ) ) && IsTitanAvailable( player ) ) || ( Riff_SpawnAsTitan() > 0 && Riff_ShouldSpawnAsTitan( player ) ) ) // spawn as titan
+ thread RespawnAsTitan( player )
+ else // spawn as pilot
+ RespawnAsPilot( player )
+ }
+ else
+ {
+ thread PlayerBecomesSpectator( player )
+ }
+}
+
+void function PlayerWatchesKillReplayWrapper( entity player, entity attacker, float timeSinceAttackerSpawned, float timeOfDeath, float beforeTime, table replayTracker )
+{
+ PlayerWatchesKillReplay( player, attacker.GetEncodedEHandle(), attacker.GetIndexForEntity(), timeSinceAttackerSpawned, timeOfDeath, beforeTime, replayTracker )
+ player.ClearReplayDelay()
+ player.ClearViewEntity()
+ player.SetPredictionEnabled( true )
+}
+
+void function EndReplayOnTime( entity player, float replayLength )
+{
+ player.EndSignal( "RespawnMe" )
+ player.EndSignal( "OnRespawned" )
+
+ wait replayLength
+ if ( IsValid( player ) && KillcamsEnabled() )
+ {
+ print( "fucking how" )
+
+ player.ClearReplayDelay()
+ player.ClearViewEntity()
+ player.SetPredictionEnabled( true )
+
+ player.SetObserverTarget( null )
+ }
+}
+
+void function DecideRespawnPlayer( entity player )
+{
+ // this isn't even used atm, could likely be removed if some vanilla code didn't rely on it
+}
+
+void function RespawnAsPilot( entity player, bool manualPosition = false )
+{
+ player.RespawnPlayer( FindSpawnPoint( player, false, ShouldStartSpawn( player ) && !IsFFAGame() ) )
+}
+
+void function RespawnAsTitan( entity player, bool manualPosition = false )
+{
+ player.isSpawning = true
+
+ entity spawnpoint = FindSpawnPoint( player, true, ShouldStartSpawn( player ) && !IsFFAGame() )
+
+ TitanLoadoutDef titanLoadout = GetTitanLoadoutForPlayer( player )
+
+ asset model = GetPlayerSettingsAssetForClassName( titanLoadout.setFile, "bodymodel" )
+ Attachment warpAttach = GetAttachmentAtTimeFromModel( model, "at_hotdrop_01", "offset", spawnpoint.GetOrigin(), spawnpoint.GetAngles(), 0 )
+ PlayFX( TURBO_WARP_FX, warpAttach.position, warpAttach.angle )
+
+ player.RespawnPlayer( null ) // spawn player as pilot so they get their pilot loadout on embark
+
+ entity titan = CreateAutoTitanForPlayer_FromTitanLoadout( player, titanLoadout, spawnpoint.GetOrigin(), spawnpoint.GetAngles() )
+ DispatchSpawn( titan )
+ player.SetPetTitan( null ) // prevent embark prompt from showing up
+
+ AddCinematicFlag( player, CE_FLAG_HIDE_MAIN_HUD ) // hide hud
+ player.HolsterWeapon() // hide crosshair
+
+ // do titanfall scoreevent
+ AddPlayerScore( player, "Titanfall", player )
+
+ entity camera = CreateTitanDropCamera( spawnpoint.GetAngles(), < 90, titan.GetAngles().y, 0 > )
+ camera.SetParent( titan )
+
+ // calc offset for spawnpoint angle
+ // todo this seems bad but too lazy to figure it out rn
+ //vector xyOffset = RotateAroundOrigin2D( < 44, 0, 0 >, < 0, 0, 0>, spawnpoint.GetAngles().y )
+ //xyOffset.z = 520 // < 44, 0, 520 > at 0,0,0, seems to be the offset used in tf2
+ //print( xyOffset )
+
+ vector xyOffset = RotateAroundOrigin2D( < 44, 0, 520 >, < 0, 0, 0 >, spawnpoint.GetAngles().y )
+
+ camera.SetLocalOrigin( xyOffset )
+ camera.SetLocalAngles( < camera.GetAngles().x, spawnpoint.GetAngles().y, camera.GetAngles().z > ) // this straight up just does not work lol
+ camera.Fire( "Enable", "!activator", 0, player )
+
+ waitthread TitanHotDrop( titan, "at_hotdrop_01", spawnpoint.GetOrigin(), spawnpoint.GetAngles(), player, camera ) // do hotdrop anim
+
+ camera.Fire( "Disable", "!activator", 0, player ) // stop using the camera
+ camera.Destroy()
+ RemoveCinematicFlag( player, CE_FLAG_HIDE_MAIN_HUD ) // show hud
+ player.DeployWeapon() // let them use weapons again
+ player.isSpawning = false
+
+ PilotBecomesTitan( player, titan ) // make player titan
+ titan.Destroy() // pilotbecomestitan leaves an npc titan that we need to delete
+}
+
+
+// spectator stuff
+
+void function PlayerBecomesSpectator( entity player )
+{
+ player.StartObserverMode( OBS_MODE_CHASE )
+
+ player.EndSignal( "OnRespawned" )
+ player.EndSignal( "OnDestroy" )
+
+ int targetIndex = 0
+
+ while ( true )
+ {
+ table result = player.WaitSignal( "ObserverTargetChanged" )
+
+ array<entity> targets
+
+ targets.append( file.intermissionCamera )
+ foreach( entity cam in file.specCams )
+ targets.append( cam )
+
+ array<entity> targetPlayers
+ if ( IsFFAGame() )
+ targetPlayers = GetPlayerArray_Alive()
+ else
+ targetPlayers = GetPlayerArrayOfTeam_Alive( player.GetTeam() )
+
+ foreach( entity player in targetPlayers )
+ targets.append( player )
+
+ if ( result.next )
+ targetIndex = ( targetIndex + 1 ) % targets.len()
+ else
+ {
+ if ( targetIndex == 0 )
+ targetIndex = ( targets.len() - 1 )
+ else
+ targetIndex--
+ }
+
+ entity target = targets[ targetIndex ]
+
+ player.StopObserverMode()
+ player.SetSpecReplayDelay( 0.0 ) // clear spectator replay
+
+ if ( target.IsPlayer() )
+ {
+ player.SetObserverTarget( target )
+ player.StartObserverMode( OBS_MODE_CHASE )
+ }
+ else
+ {
+ player.SetObserverModeStaticPosition( target.GetOrigin() )
+ player.SetObserverModeStaticAngles( target.GetAngles() )
+ player.StartObserverMode( OBS_MODE_STATIC )
+ }
+ }
+}
+
+bool function ClientCommandCallback_spec_next( entity player, array<string> args )
+{
+ if ( player.GetObserverMode() == OBS_MODE_CHASE || player.GetObserverMode() == OBS_MODE_STATIC || player.GetObserverMode() == OBS_MODE_IN_EYE )
+ player.Signal( "ObserverTargetChanged", { next = true } )
+
+ return true
+}
+
+bool function ClientCommandCallback_spec_prev( entity player, array<string> args )
+{
+ if ( player.GetObserverMode() == OBS_MODE_CHASE || player.GetObserverMode() == OBS_MODE_STATIC || player.GetObserverMode() == OBS_MODE_IN_EYE )
+ player.Signal( "ObserverTargetChanged", { next = false } )
+
+ return true
+}
+
+bool function ClientCommandCallback_spec_mode( entity player, array<string> args )
+{
+ // currently unsure how this actually gets called on client, works through console and has references in client.dll tho
+ if ( player.GetObserverMode() == OBS_MODE_CHASE )
+ {
+ // set to first person spectate
+ player.SetSpecReplayDelay( FIRST_PERSON_SPECTATOR_DELAY )
+ player.SetViewEntity( player.GetObserverTarget(), true )
+ player.StartObserverMode( OBS_MODE_IN_EYE )
+ }
+ else if ( player.GetObserverMode() == OBS_MODE_IN_EYE )
+ {
+ // set to third person spectate
+ player.SetSpecReplayDelay( 0.0 )
+ player.StartObserverMode( OBS_MODE_CHASE )
+ }
+
+ return true
+}
+
+
+void function TryGameModeAnnouncement( entity player ) // only putting this here because it's here in gametype_sp lol
+{
+ Remote_CallFunction_NonReplay( player, "ServerCallback_GameModeAnnouncement" )
+ PlayFactionDialogueToPlayer( GameMode_GetGameModeAnnouncement( GAMETYPE ), player )
+}
+
+void function SetKillcamsEnabled( bool enabled )
+{
+ file.killcamsEnabled = enabled
+}
+
+bool function KillcamsEnabled()
+{
+ return file.killcamsEnabled
+}
+
+void function SetPlayerDeathsHidden( bool hidden )
+{
+ file.playerDeathsHidden = hidden
+}
+
+void function TrackTitanDamageInPlayerGameStat( int playerGameStat )
+{
+ file.titanDamageGameStat = playerGameStat
+}
+
+void function AddToTitanDamageStat( entity victim, var damageInfo )
+{
+ if ( !victim.IsTitan() || file.titanDamageGameStat == -1 )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ float amount = DamageInfo_GetDamage( damageInfo )
+
+ if ( attacker.IsPlayer() && attacker != victim )
+ attacker.AddToPlayerGameStat( file.titanDamageGameStat, amount ) // titan damage on
+}
+
+
+// stuff to change later
+
+bool function ShouldEntTakeDamage_SPMP( entity ent, var damageInfo )
+{
+ return true
+}
+
+float function GetTitanBuildTime(entity player)
+{
+ return 100.0
+}
+
+void function TitanPlayerHotDropsIntoLevel( entity player )
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_battery_port.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_battery_port.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_battery_port.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_bleedout.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_bleedout.gnut
new file mode 100644
index 00000000..2192b4b1
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_bleedout.gnut
@@ -0,0 +1,403 @@
+//Bleed Out Mechanic Shared by several game modes.
+global function Bleedout_Init
+global function Bleedout_StartPlayerBleedout
+global function Bleedout_SetCallback_OnPlayerStartBleedout
+global function Bleedout_SetCallback_OnPlayerGiveFirstAid
+global function Bleedout_ShouldAIMissBleedingPlayer
+
+const asset FX_BLOODTRAIL = $"skit_blood_decal_LG"
+const float BLEEDOUT_MAX_USE_DIST2_MOD = 64 * 64
+
+struct
+{
+ table<entity,bool> isBleeding
+ table<entity, entity> IsGettingFirstAidFrom
+ table<entity,entity> lastAttacker
+ void functionref(entity) Callback_OnPlayerStartBleedout
+ void functionref(entity) Callback_OnPlayerGiveFirstAid
+ int firstAidAttemptID = 0 //The ID that identifies the first aid attempt. Used to distinguish between simultainous healing attempts on the client
+} file
+
+void function Bleedout_Init()
+{
+ RegisterSignal( "BleedOut_StopBleeding" )
+ RegisterSignal( "BleedOut_OnRevive" )
+ RegisterSignal( "BleedOut_OnStartDying" )
+ RegisterSignal( "OnContinousUseStopped" )
+
+ AddCallback_OnClientConnected( Bleedout_OnClientConnected )
+ AddCallback_OnClientDisconnected( Bleedout_OnClientDisconnected )
+
+ PrecacheParticleSystem( FX_BLOODTRAIL )
+}
+
+void function Bleedout_OnClientConnected( entity player )
+{
+ file.isBleeding[ player ] <- false
+ file.IsGettingFirstAidFrom[ player ] <- null
+ file.lastAttacker[ player ] <- svGlobal.worldspawn
+}
+
+void function Bleedout_OnClientDisconnected( entity player )
+{
+ delete file.isBleeding[ player ]
+ delete file.IsGettingFirstAidFrom[ player ]
+ delete file.lastAttacker[ player ]
+}
+
+void function Bleedout_SetCallback_OnPlayerStartBleedout( void functionref(entity) callback )
+{
+ file.Callback_OnPlayerStartBleedout = callback
+}
+
+void function Bleedout_SetCallback_OnPlayerGiveFirstAid( void functionref(entity) callback )
+{
+ file.Callback_OnPlayerGiveFirstAid = callback
+}
+
+void function Bleedout_StartPlayerBleedout( entity player, entity attacker )
+{
+ //if the player is already bleeding don't restart bleeding logic.
+ if ( file.isBleeding[ player ] )
+ return
+
+ player.Signal( "BleedOut_StopBleeding" )
+ player.Signal( "BleedOut_OnStartDying" )
+
+ file.lastAttacker[ player ] = attacker
+
+ if ( IsValid( file.Callback_OnPlayerStartBleedout ) && !file.isBleeding[ player ] )
+ file.Callback_OnPlayerStartBleedout( player )
+
+ thread BloodTrail( player )
+ thread PlayerDying( player )
+ thread EnablePlayerRes( player )
+
+ //Start selfhealing thread if enabled.
+ if ( Bleedout_GetSelfResEnabled() )
+ thread EnablePlayerSelfRes( player )
+
+ if ( Bleedout_GetDeathOnTeamBleedout() )
+ CheckForTeamBleedout( player.GetTeam() )
+}
+
+void function PlayerDying( entity player )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "BleedOut_OnRevive" )
+ player.EndSignal( "BleedOut_OnStartDying" )
+
+ float bleedoutTime = Bleedout_GetBleedoutTime()
+ bool forceHolster = Bleedout_GetForceWeaponHolster()
+
+ array<int> ids = []
+ ids.append( StatusEffect_AddEndless( player, eStatusEffect.move_slow, 0.25 ) )
+ ids.append( StatusEffect_AddEndless( player, eStatusEffect.turn_slow, 0.3 ) )
+
+ if ( bleedoutTime > 0 )
+ ids.append( StatusEffect_AddEndless( player, eStatusEffect.bleedoutDOF, 1.0 ) )
+
+ file.isBleeding[ player ] = true
+
+ player.ForceCrouch()
+ player.SetOneHandedWeaponUsageOn()
+
+ if ( forceHolster )
+ HolsterAndDisableWeapons( player )
+
+ OnThreadEnd(
+ function() : ( player, ids, forceHolster )
+ {
+ if ( IsValid( player ) )
+ {
+ foreach ( id in ids )
+ StatusEffect_Stop( player, id )
+
+ file.isBleeding[ player ] = false
+ file.lastAttacker[ player ] = svGlobal.worldspawn
+
+ player.UnforceCrouch()
+ player.SetOneHandedWeaponUsageOff()
+ //Remote_CallFunction_NonReplay( player, "ServerCallback_BLEEDOUT_PlayerRevivedDOF" )
+
+ if ( forceHolster )
+ DeployAndEnableWeapons( player )
+
+ //Hide wounded icon for wounded player's allies
+ int woundedPlayerEHandle = player.GetEncodedEHandle()
+ array<entity> teamPlayers = GetPlayerArrayOfTeam( player.GetTeam() )
+ foreach ( entity teamPlayer in teamPlayers )
+ {
+ if ( teamPlayer == player )
+ continue
+ Remote_CallFunction_NonReplay( teamPlayer, "ServerCallback_BLEEDOUT_HideWoundedMarker", woundedPlayerEHandle )
+ }
+ }
+ }
+ )
+
+ //if ( bleedoutTime > 0 )
+ // StatusEffect_AddTimed( player, eStatusEffect.bleedoutDOF, 1.0, bleedoutTime, 0.0 )
+ //Remote_CallFunction_NonReplay( player, "ServerCallback_BLEEDOUT_StartDyingDOF", bleedoutTime )
+
+ //Show wounded icon for wounded player's allies
+ int woundedPlayerEHandle = player.GetEncodedEHandle()
+ array<entity> teamPlayers = GetPlayerArrayOfTeam( player.GetTeam() )
+ foreach ( entity teamPlayer in teamPlayers )
+ {
+ if ( teamPlayer == player )
+ continue
+
+ Remote_CallFunction_NonReplay( teamPlayer, "ServerCallback_BLEEDOUT_ShowWoundedMarker", woundedPlayerEHandle, Time(), Time() + bleedoutTime )
+ }
+
+ if ( bleedoutTime > 0 )
+ wait bleedoutTime
+ else
+ WaitForever()
+
+ PlayerDiesFromBleedout( player, file.lastAttacker[ player ] )
+}
+
+void function EnablePlayerRes( entity player )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "BleedOut_OnStartDying" )
+ player.EndSignal( "BleedOut_OnRevive" )
+
+ Highlight_SetFriendlyHighlight( player, "interact_object_los_line" )
+
+ if ( IsPilotEliminationBased() )
+ SetPlayerEliminated( player )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ {
+ player.UnsetUsable()
+ Highlight_ClearFriendlyHighlight( player )
+ }
+ }
+ )
+
+ while ( true )
+ {
+ //If the player is not currently being treated or is self healing. (Team healing should always override self-healing)
+ if ( !IsPlayerGettingFirstAid( player ) || IsPlayerSelfHealing( player ) )
+ {
+ player.SetUsableByGroup( "friendlies pilot" )
+ player.SetUsePrompts( "#BLEEDOUT_USE_TEAMMATE_RES", "#BLEEDOUT_USE_TEAMMATE_RES_PC" )
+
+ entity playerHealer = expect entity ( player.WaitSignal( "OnPlayerUse" ).player )
+ player.UnsetUsable()
+
+ //Player can only res other players if they are not bleeding out themselves.
+ if ( !file.isBleeding[ playerHealer ] && ( !IsPlayerGettingFirstAid( player ) || IsPlayerSelfHealing( player ) ) )
+ waitthread PlayerAttemptRes( playerHealer, player )
+ }
+ else
+ {
+ WaitFrame()
+ }
+ }
+}
+
+void function EnablePlayerSelfRes( entity player )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "BleedOut_OnStartDying" )
+ player.EndSignal( "BleedOut_OnRevive" )
+
+ while ( true )
+ {
+ if ( !IsPlayerGettingFirstAid( player ) )
+ MessageToPlayer( player, eEventNotifications.BLEEDOUT_SelfHealPrompt )
+
+ if ( player.UseButtonPressed() && !IsPlayerGettingFirstAid( player ) )
+ {
+ MessageToPlayer( player, eEventNotifications.Clear )
+ waitthread PlayerAttemptRes( player, player )
+ }
+
+ WaitFrame()
+ }
+}
+
+void function PlayerAttemptRes( entity playerHealer, entity playerToRes )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ playerToRes.EndSignal( "OnDeath" )
+ playerHealer.EndSignal( "OnDeath" )
+ playerHealer.EndSignal( "OnContinousUseStopped" )
+
+ HolsterAndDisableWeapons( playerHealer )
+
+ playerHealer.MovementDisable()
+ playerToRes.MovementDisable()
+
+ float firstAidTime = playerHealer == playerToRes ? Bleedout_GetFirstAidTimeSelf() : Bleedout_GetFirstAidTime()
+ float firstAidHealPercent = Bleedout_GetFirstAidHealPercent()
+
+ float endTime = Time() + firstAidTime
+
+ int playerEHandle = playerToRes.GetEncodedEHandle()
+ int healerEHandle = playerHealer.GetEncodedEHandle()
+ int attemptID = GetNewFirstAidAttemptID()
+
+ Remote_CallFunction_NonReplay( playerToRes, "ServerCallback_BLEEDOUT_StartFirstAidProgressBar", endTime, playerEHandle, healerEHandle, attemptID )
+ Remote_CallFunction_NonReplay( playerHealer, "ServerCallback_BLEEDOUT_StartFirstAidProgressBar", endTime, playerEHandle, healerEHandle, attemptID )
+ file.IsGettingFirstAidFrom[ playerToRes ] = playerHealer
+
+ OnThreadEnd(
+ function() : ( playerHealer, playerToRes, attemptID )
+ {
+ if ( IsValid( playerHealer ) )
+ {
+ DeployAndEnableWeapons( playerHealer )
+ playerHealer.MovementEnable()
+ Remote_CallFunction_NonReplay( playerHealer, "ServerCallback_BLEEDOUT_StopFirstAidProgressBar", attemptID )
+ }
+
+ if ( IsValid( playerToRes ) )
+ {
+ file.IsGettingFirstAidFrom[ playerToRes ] = null
+ playerToRes.MovementEnable()
+ Remote_CallFunction_NonReplay( playerToRes, "ServerCallback_BLEEDOUT_StopFirstAidProgressBar", attemptID )
+ }
+ }
+ )
+
+ waitthread TrackContinuousUse( playerHealer, playerToRes, firstAidTime, true )
+
+ //Heal player health
+ playerToRes.SetHealth( playerToRes.GetMaxHealth() * firstAidHealPercent )
+ file.isBleeding[ playerToRes ] = false
+ file.lastAttacker[ playerToRes ] = svGlobal.worldspawn
+ if ( IsPilotEliminationBased() )
+ ClearPlayerEliminated( playerToRes )
+
+ if ( IsValid( file.Callback_OnPlayerGiveFirstAid ) )
+ {
+ //Do not run this callback if player is self healing.
+ if ( playerHealer != playerToRes )
+ file.Callback_OnPlayerGiveFirstAid( playerHealer )
+ }
+
+ playerToRes.Signal( "BleedOut_OnRevive" )
+
+}
+
+void function BloodTrail( entity player )
+{
+ player.EndSignal( "BleedOut_StopBleeding" )
+ player.EndSignal( "BleedOut_OnRevive" )
+ player.EndSignal( "OnDeath")
+
+ while ( true )
+ {
+ float interval = RandomFloatRange( 0.25, 0.5 )
+ PlayFXOnEntity( FX_BLOODTRAIL, player )
+ wait interval
+ }
+}
+
+void function PlayerDiesFromBleedout( entity player, entity attacker )
+{
+ if ( IsValid( attacker ) )
+ {
+ player.Die( attacker, attacker, { damageSourceId = eDamageSourceId.bleedout } )
+ //player.BecomeRagdoll( Vector(0,0,0), false )
+ }
+ else
+ {
+ player.Die( svGlobal.worldspawn, svGlobal.worldspawn, { damageSourceId = eDamageSourceId.bleedout } )
+ //player.BecomeRagdoll( Vector(0,0,0), false )
+ }
+
+
+}
+
+//This function checks to see if all players on a team are dead or bleeding out.
+//If all the players are dead/bleeding out, it kills the surviving team players.
+void function CheckForTeamBleedout( int team )
+{
+ array<entity> teamPlayers = GetPlayerArrayOfTeam( team )
+ foreach ( entity teamPlayer in teamPlayers )
+ {
+ if ( IsAlive( teamPlayer ) && !file.isBleeding[ teamPlayer ] )
+ return
+ }
+
+ //All players on team are bleeding out
+ foreach ( entity teamPlayer in teamPlayers )
+ {
+ if ( IsAlive( teamPlayer ) )
+ PlayerDiesFromBleedout( teamPlayer, file.lastAttacker[ teamPlayer ] )
+ }
+}
+
+bool function Bleedout_ShouldAIMissBleedingPlayer( entity player )
+{
+ //If the player is not bleeding
+ if ( !file.isBleeding[ player ] )
+ return false
+
+ //If the bleedout settings don't affect AI accuracy.
+ if ( !Bleedout_ShouldAIMissPlayer() )
+ return false
+
+ return true
+}
+
+bool function IsPlayerGettingFirstAid( entity player )
+{
+ return file.IsGettingFirstAidFrom[ player ] != null
+}
+
+bool function IsPlayerSelfHealing( entity player )
+{
+ return file.IsGettingFirstAidFrom[ player ] == player
+}
+
+//////////////
+//Utilities
+//////////////
+void function TrackContinuousUse( entity player, entity useTarget, float useTime, bool doRequireUseButtonHeld )
+{
+ player.EndSignal( "OnDeath" )
+ useTarget.EndSignal( "OnDeath" )
+ useTarget.EndSignal( "OnDestroy" )
+
+ table result = {}
+ result.success <- false
+
+ float maxDist2 = DistanceSqr( player.GetOrigin(), useTarget.GetOrigin() ) + BLEEDOUT_MAX_USE_DIST2_MOD
+
+ OnThreadEnd
+ (
+ function() : ( player, result )
+ {
+ if ( !result.success )
+ {
+ player.Signal( "OnContinousUseStopped" )
+ }
+ }
+ )
+
+ float startTime = Time()
+ while ( Time() < startTime + useTime && (!doRequireUseButtonHeld || player.UseButtonPressed()) && !player.IsPhaseShifted() && DistanceSqr( player.GetOrigin(), useTarget.GetOrigin() ) <= maxDist2 )
+ WaitFrame()
+
+ if ( ( !doRequireUseButtonHeld || player.UseButtonPressed() ) && DistanceSqr( player.GetOrigin(), useTarget.GetOrigin() ) <= maxDist2 )
+ result.success = true
+}
+
+int function GetNewFirstAidAttemptID()
+{
+ file.firstAidAttemptID += 1
+ return file.firstAidAttemptID
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_challenges.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_challenges.gnut
new file mode 100644
index 00000000..466a5042
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_challenges.gnut
@@ -0,0 +1,6 @@
+global function InitChallenges
+
+void function InitChallenges()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_changemap.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_changemap.nut
new file mode 100644
index 00000000..95d7492e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_changemap.nut
@@ -0,0 +1,24 @@
+global function CodeCallback_MatchIsOver
+
+void function CodeCallback_MatchIsOver()
+{
+ if ( !IsPrivateMatch() && IsMatchmakingServer() )
+ SetUIVar( level, "putPlayerInMatchmakingAfterDelay", true )
+ else
+ SetUIVar( level, "putPlayerInMatchmakingAfterDelay", false )
+
+ if ( ShouldReturnToLobby() )
+ {
+ SetCurrentPlaylist( "private_match" ) // needed for private lobby to load
+
+ if ( IsSingleplayer() )
+ GameRules_ChangeMap( "mp_lobby", "tdm" ) // need to change back to mp playlist or loadouts will break in lobby
+ else
+ GameRules_ChangeMap( "mp_lobby", GAMETYPE )
+ }
+
+#if DEV
+ if ( !IsMatchmakingServer() )
+ GameRules_ChangeMap( "mp_lobby", GAMETYPE )
+#endif // #if DEV
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp.nut
new file mode 100644
index 00000000..ac8a397f
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp.nut
@@ -0,0 +1,67 @@
+untyped
+global function ClassicMp_Init
+global function ClassicMP_TryDefaultIntroSetup // called in mp_sh_init
+global function ClassicMP_SetCustomIntro
+global function ClassicMP_OnIntroStarted
+global function ClassicMP_OnIntroFinished
+global function ClassicMP_GetIntroLength
+global function GetClassicMPMode
+
+struct {
+ void functionref() introSetupFunc
+ float introLength
+} file
+
+void function ClassicMp_Init()
+{
+ // literally nothing to do here atm lol
+}
+
+void function ClassicMP_TryDefaultIntroSetup()
+{
+ if ( file.introSetupFunc == null )
+ {
+ if ( IsFFAGame() )
+ ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, ClassicMP_DefaultNoIntro_GetLength() )
+ else
+ ClassicMP_SetCustomIntro( ClassicMP_DefaultDropshipIntro_Setup, DROPSHIP_INTRO_LENGTH )
+ }
+
+ thread DelayedDoDefaultIntroSetup()
+}
+
+void function DelayedDoDefaultIntroSetup()
+{
+ // wait a frame for CodeCallback_MapInit to run which generally sets custom intros
+ WaitFrame()
+ file.introSetupFunc()
+}
+
+void function ClassicMP_SetCustomIntro( void functionref() setupFunc, float introLength )
+{
+ file.introSetupFunc = setupFunc
+ file.introLength = introLength
+}
+
+void function ClassicMP_OnIntroStarted()
+{
+ print( "started intro!" )
+ SetServerVar( "gameStartTime", Time() + file.introLength )
+ SetServerVar( "roundStartTime", Time() + file.introLength )
+}
+
+void function ClassicMP_OnIntroFinished()
+{
+ print( "intro finished!" )
+ SetGameState( eGameState.Playing )
+}
+
+float function ClassicMP_GetIntroLength()
+{
+ return file.introLength
+}
+
+bool function GetClassicMPMode()
+{
+ return GetCurrentPlaylistVarInt( "classic_mp", 1 ) == 1
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut
new file mode 100644
index 00000000..02c312be
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut
@@ -0,0 +1,239 @@
+untyped
+global function ClassicMP_DefaultDropshipIntro_Setup
+
+const array<string> DROPSHIP_IDLE_ANIMS = [ "Classic_MP_flyin_exit_playerA_idle",
+ "Classic_MP_flyin_exit_playerB_idle",
+ "Classic_MP_flyin_exit_playerC_idle",
+ "Classic_MP_flyin_exit_playerD_idle" ]
+
+const array<string> DROPSHIP_IDLE_ANIMS_POV = [ "Classic_MP_flyin_exit_povA_idle",
+ "Classic_MP_flyin_exit_povB_idle",
+ "Classic_MP_flyin_exit_povC_idle",
+ "Classic_MP_flyin_exit_povD_idle" ]
+
+const array<string> DROPSHIP_JUMP_ANIMS = [ "Classic_MP_flyin_exit_playerA_jump",
+ "Classic_MP_flyin_exit_playerB_jump",
+ "Classic_MP_flyin_exit_playerC_jump",
+ "Classic_MP_flyin_exit_playerD_jump" ]
+
+const array<string> DROPSHIP_JUMP_ANIMS_POV = [ "Classic_MP_flyin_exit_povA_jump",
+ "Classic_MP_flyin_exit_povB_jump",
+ "Classic_MP_flyin_exit_povC_jump",
+ "Classic_MP_flyin_exit_povD_jump" ]
+
+const array<int> DROPSHIP_ANIMS_YAW = [ -18, 8, 8, -16 ]
+
+global const float DROPSHIP_INTRO_LENGTH = 15.0 // TODO tweak this
+
+struct IntroDropship
+{
+ entity dropship
+
+ int playersInDropship
+ entity[4] players
+}
+
+struct {
+ IntroDropship[2] militiaDropships
+ IntroDropship[2] imcDropships
+
+ float introStartTime
+ int numPlayersInIntro
+} file
+
+
+void function ClassicMP_DefaultDropshipIntro_Setup()
+{
+ AddCallback_OnClientConnected( DropshipIntro_OnClientConnected )
+ AddCallback_OnClientDisconnected( DropshipIntro_OnClientDisconnected )
+
+ AddCallback_GameStateEnter( eGameState.Prematch, OnPrematchStart )
+}
+
+void function DropshipIntro_OnClientConnected( entity player )
+{
+ // find the player's team's dropships
+ IntroDropship[2] teamDropships = player.GetTeam() == TEAM_MILITIA ? file.militiaDropships : file.imcDropships
+
+ // find a dropship with an empty slot
+ foreach ( IntroDropship dropship in teamDropships )
+ if ( dropship.playersInDropship < 4 )
+ // we've found a valid dropship
+ // find an empty player slot
+ for ( int i = 0; i < dropship.players.len(); i++ )
+ if ( dropship.players[ i ] == null ) // empty slot
+ {
+ dropship.players[ i ] = player
+ dropship.playersInDropship++
+
+ // spawn player into intro if we're already doing intro
+ if ( GetGameState() == eGameState.Prematch )
+ thread SpawnPlayerIntoDropship( player )
+
+ return
+ }
+}
+
+void function DropshipIntro_OnClientDisconnected( entity player )
+{
+ // find the player's dropship
+ IntroDropship[2] teamDropships = player.GetTeam() == TEAM_MILITIA ? file.militiaDropships : file.imcDropships
+
+ // find the player
+ foreach ( IntroDropship dropship in teamDropships )
+ for ( int i = 0; i < dropship.players.len(); i++ )
+ if ( dropship.players[ i ] == player )
+ {
+ // we've found the player, remove them
+ dropship.players[ i ] = null
+ dropship.playersInDropship--
+
+ return
+ }
+}
+
+void function OnPrematchStart()
+{
+ ClassicMP_OnIntroStarted()
+
+ print( "starting dropship intro!" )
+ file.introStartTime = Time()
+
+ // spawn dropships
+ array<entity> dropshipSpawns = GetEntArrayByClass_Expensive( "info_spawnpoint_dropship_start" )
+ foreach ( entity dropshipSpawn in dropshipSpawns )
+ {
+ if ( GameModeRemove( dropshipSpawn ) || ( GetSpawnpointGamemodeOverride() != GAMETYPE && dropshipSpawn.HasKey( "gamemode_" + GetSpawnpointGamemodeOverride() ) && dropshipSpawn.kv[ "gamemode_" + GetSpawnpointGamemodeOverride() ] == "0" ) )
+ continue
+
+ // todo: possibly make this only spawn dropships if we've got enough players to need them
+ int createTeam = GetServerVar( "switchedSides" ) != 1 ? dropshipSpawn.GetTeam() : GetOtherTeam( dropshipSpawn.GetTeam() )
+ IntroDropship[2] teamDropships = createTeam == TEAM_MILITIA ? file.militiaDropships : file.imcDropships
+ int dropshipIndex = !IsValid( teamDropships[ 0 ].dropship ) ? 0 : 1
+
+ // create entity
+ entity dropship = CreateDropship( createTeam, dropshipSpawn.GetOrigin(), dropshipSpawn.GetAngles() )
+
+ teamDropships[ dropshipIndex ].dropship = dropship
+ AddAnimEvent( dropship, "dropship_warpout", WarpoutEffect )
+
+ DispatchSpawn( dropship )
+
+ // have to do this after dispatch otherwise it won't work for some reason
+ dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" )
+ // could also use $"models/vehicle/goblin_dropship/goblin_dropship_hero.mdl", unsure which
+
+ thread PlayAnim( dropship, "dropship_classic_mp_flyin" )
+ }
+
+ foreach ( entity player in GetPlayerArray() )
+ thread SpawnPlayerIntoDropship( player )
+}
+
+void function SpawnPlayerIntoDropship( entity player )
+{
+ if ( IsAlive( player ) )
+ player.Die() // kill them so we don't have any issues respawning them later
+
+ WaitFrame()
+
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "Disconnected" )
+
+ file.numPlayersInIntro++
+
+ // find the player's dropship and seat
+ IntroDropship[2] teamDropships = player.GetTeam() == TEAM_MILITIA ? file.militiaDropships : file.imcDropships
+ IntroDropship playerDropship
+ int playerDropshipIndex
+ foreach ( IntroDropship dropship in teamDropships )
+ for ( int i = 0; i < dropship.players.len(); i++ )
+ if ( dropship.players[ i ] == player )
+ {
+ playerDropship = dropship
+ playerDropshipIndex = i
+
+ break
+ }
+
+ if ( playerDropship.dropship == null )
+ {
+ // if we're at this point, we have more players than we do dropships, oh dear
+ ScreenFadeFromBlack( player, 0.0 )
+ RespawnAsPilot( player )
+
+ file.numPlayersInIntro--
+ return
+ }
+
+ // figure out what anims we're using for idle
+ string idleAnim = DROPSHIP_IDLE_ANIMS[ playerDropshipIndex ]
+ string idleAnimPov = DROPSHIP_IDLE_ANIMS_POV[ playerDropshipIndex ]
+
+ FirstPersonSequenceStruct idleSequence
+ idleSequence.firstPersonAnim = idleAnimPov
+ idleSequence.thirdPersonAnim = idleAnim
+ idleSequence.attachment = "ORIGIN"
+ idleSequence.teleport = true
+ idleSequence.viewConeFunction = ViewConeRampFree
+ idleSequence.hideProxy = true
+ idleSequence.setInitialTime = Time() - file.introStartTime
+
+ // respawn player and holster their weapons so they aren't out
+ player.RespawnPlayer( null )
+ player.DisableWeaponViewModel()
+
+ // hide hud and fade screen out from black
+ AddCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+ ScreenFadeFromBlack( player, 0.5, 0.5 )
+ // faction leaders are done clientside, spawn them here
+ Remote_CallFunction_NonReplay( player, "ServerCallback_SpawnFactionCommanderInDropship", playerDropship.dropship.GetEncodedEHandle(), file.introStartTime )
+ thread FirstPersonSequence( idleSequence, player, playerDropship.dropship )
+
+ // wait until the anim is done
+ WaittillAnimDone( player ) // unsure if this is the best way to do this
+ // todo: possibly rework this to actually get the time the idle anim takes and start the starttime of the jump sequence for very late joiners using that
+
+ // honestly go rewrite alot of this too it's messy
+
+ // figure out what anims we're using for jump
+ string jumpAnim = DROPSHIP_JUMP_ANIMS[ playerDropshipIndex ]
+ string jumpAnimPov = DROPSHIP_JUMP_ANIMS_POV[ playerDropshipIndex ]
+
+ FirstPersonSequenceStruct jumpSequence
+ jumpSequence.firstPersonAnim = jumpAnimPov
+ jumpSequence.thirdPersonAnim = jumpAnim
+ jumpSequence.attachment = "ORIGIN"
+ //jumpSequence.setInitialTime = Time() - ( file.introStartTime + player.GetSequenceDuration( idleAnim ) )
+ jumpSequence.setInitialTime = Time() - ( file.introStartTime + 10.9 ) // pretty sure you should do this with GetScriptedAnimEventCycleFrac?
+ // idk unsure how to use that, all i know is getsequenceduration > the length it actually should be
+
+ thread FirstPersonSequence( jumpSequence, player, playerDropship.dropship )
+ WaittillAnimDone( player )
+
+ // unparent player and their camera from the dropship
+ player.ClearParent()
+ ClearPlayerAnimViewEntity( player )
+
+ file.numPlayersInIntro--
+ if ( file.numPlayersInIntro == 0 )
+ ClassicMP_OnIntroFinished() // set intro as finished
+
+ // wait for intro timer to be fully done
+ wait( Time() - ( file.introStartTime + DROPSHIP_INTRO_LENGTH ) )
+ player.MovementDisable() // disable all movement but let them look around still
+ player.ConsumeDoubleJump() // movementdisable doesn't prevent double jumps
+
+ // wait for player to hit the ground
+ while ( !player.IsOnGround() && !player.IsWallRunning() && !player.IsWallHanging() ) // todo this needs tweaking
+ WaitFrame()
+
+ // show weapon viewmodel and hud and let them move again
+ player.MovementEnable()
+ player.EnableWeaponViewModel()
+ RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+
+ if ( GetServerVar( "switchedSides" ) != 1 )
+ TryGameModeAnnouncement( player )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_no_intro.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_no_intro.gnut
new file mode 100644
index 00000000..106f867b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_no_intro.gnut
@@ -0,0 +1,80 @@
+untyped
+
+global function ClassicMP_DefaultNoIntro_Setup
+global function ClassicMP_DefaultNoIntro_GetLength
+
+global const float NOINTRO_INTRO_PILOT_LENGTH = 10.0
+global const float TITAN_DROP_SPAWN_INTRO_LENGTH = 0.0 // this intro shouldn't have a countdown visually, so we have to set the length of this intro to 0
+global const float TITAN_DROP_SPAWN_INTRO_REAL_LENGTH = 2.0 // we wait roughly this long during the intro, even when it's technically over
+
+void function ClassicMP_DefaultNoIntro_Setup()
+{
+ AddCallback_OnClientConnected( ClassicMP_DefaultNoIntro_SpawnPlayer )
+ AddCallback_GameStateEnter( eGameState.Prematch, ClassicMP_DefaultNoIntro_Start )
+}
+
+float function ClassicMP_DefaultNoIntro_GetLength()
+{
+ if ( ShouldIntroSpawnAsTitan() )
+ return TITAN_DROP_SPAWN_INTRO_LENGTH
+ else
+ return NOINTRO_INTRO_PILOT_LENGTH
+
+ unreachable
+}
+
+void function ClassicMP_DefaultNoIntro_Start()
+{
+ ClassicMP_OnIntroStarted()
+
+ foreach ( entity player in GetPlayerArray() )
+ ClassicMP_DefaultNoIntro_SpawnPlayer( player )
+
+ if ( ShouldIntroSpawnAsTitan() )
+ wait TITAN_DROP_SPAWN_INTRO_REAL_LENGTH
+ else
+ {
+ wait NOINTRO_INTRO_PILOT_LENGTH
+
+ foreach ( entity player in GetPlayerArray() )
+ {
+ player.UnfreezeControlsOnServer()
+ RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+ TryGameModeAnnouncement( player )
+ }
+ }
+
+ ClassicMP_OnIntroFinished()
+}
+
+void function ClassicMP_DefaultNoIntro_SpawnPlayer( entity player )
+{
+ if ( GetGameState() != eGameState.Prematch )
+ return
+
+ if ( IsAlive( player ) )
+ player.Die()
+
+ if ( ShouldIntroSpawnAsTitan() )
+ thread ClassicMP_DefaultNoIntro_TitanSpawnPlayer( player )
+ else
+ thread ClassicMP_DefaultNoIntro_PilotSpawnPlayer( player )
+}
+
+
+// spawn as pilot for intro
+void function ClassicMP_DefaultNoIntro_PilotSpawnPlayer( entity player )
+{
+ RespawnAsPilot( player )
+ player.FreezeControlsOnServer()
+ AddCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+ ScreenFadeFromBlack( player, 0.5, 0.5 )
+}
+
+// spawn as titan for intro
+void function ClassicMP_DefaultNoIntro_TitanSpawnPlayer( entity player )
+{
+ // blocking call
+ RespawnAsTitan( player, false )
+ TryGameModeAnnouncement( player )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut
new file mode 100644
index 00000000..2e565142
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut
@@ -0,0 +1,999 @@
+untyped
+
+
+global function CodeCallback_Init
+global function CodeCallback_DamagePlayerOrNPC
+global function GameModeRulesShouldGiveTimerCredit
+global function SetGameModeRulesShouldGiveTimerCredit
+global function SetGameModeRulesEarnMeterOnDamage
+global function GetDamageOrigin
+global function CodeCallBack_ShouldTriggerSniperCam
+global function CodeCallback_ForceAIMissPlayer
+global function CodeCallback_OnTouchHealthKit
+global function CodeCallback_OnPlayerGrappled
+global function CodeCallback_OnProjectileGrappled
+global function DamageInfo_ScaleDamage
+global function CodeCallback_CheckPassThroughAddsMods
+global function SetTitanMeterGainScale
+
+#if MP
+global function CodeCallback_OnServerAnimEvent
+#endif
+
+struct AccumulatedDamageData
+{
+ float accumulatedDamage
+ float lastDamageTime
+}
+
+struct
+{
+ float titanMeterGainScale = 0.0001
+ bool functionref( entity, entity, var ) ShouldGiveTimerCreditGameModeRules
+ void functionref( entity, entity, TitanDamage, float ) earnMeterOnDamageGameModeRulesCallback
+
+ table<entity, AccumulatedDamageData> playerAccumulatedDamageData
+} file
+
+void function CodeCallback_Init()
+{
+ file.ShouldGiveTimerCreditGameModeRules = ShouldGiveTimerCredit_Default
+ file.earnMeterOnDamageGameModeRulesCallback = GameModeRulesEarnMeterOnDamage_Default
+ RegisterSignal( "DamagedPlayerOrNPC" )
+ RegisterSignal( "UpdateAccumulatedDamageAfterDelay" )
+
+ AddCallback_OnClientConnected( OnClientConnected )
+}
+
+void function OnClientConnected( entity player )
+{
+ AccumulatedDamageData damageData
+ file.playerAccumulatedDamageData[player] <- damageData
+}
+
+// TODO: Get an equivalent callback happening on the client, so we can stop using ServerCallback_PlayerTookDamage which is always out of date to some degree.
+void function CodeCallback_DamagePlayerOrNPC( entity ent, var damageInfo )
+{
+ bool entIsPlayer = ent.IsPlayer()
+ bool entIsTitan = ent.IsTitan()
+ bool entIsNPC = ent.IsNPC()
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+
+ bool attackerIsPlayer = false
+ bool attackerIsTitan = false
+ bool attackerIsNPC = false
+
+ if ( IsValid( attacker ) )
+ {
+ attackerIsPlayer = attacker.IsPlayer()
+ attackerIsTitan = attacker.IsTitan()
+ attackerIsNPC = attacker.IsNPC()
+ }
+
+ // Set damage source correctly when npc grunts or titans try to melee us
+ if ( attackerIsNPC && DamageInfo_GetCustomDamageType( damageInfo ) & DF_MELEE )
+ {
+ if ( IsValid( attacker ) )
+ {
+ if ( attackerIsTitan )
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.auto_titan_melee )
+ }
+ else if ( IsSpectre( attacker ) )
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.spectre_melee )
+ }
+ else if ( IsProwler( attacker ) )
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.prowler_melee )
+ }
+ else if ( IsSuperSpectre( attacker ) )
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.super_spectre_melee )
+ }
+ else
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.grunt_melee )
+ }
+ }
+ }
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( "CodeCallback_DamagePlayerOrNPC ent:", ent )
+ printt( " Attacker:", DamageInfo_GetAttacker( damageInfo ) )
+ printt( " Inflictor:", DamageInfo_GetInflictor( damageInfo ) )
+ printt( " Distance:", DamageInfo_GetDistFromAttackOrigin( damageInfo ) )
+ printt( " Original damage:", DamageInfo_GetDamage( damageInfo ) )
+ printt( " Hitbox:", DamageInfo_GetHitBox( damageInfo ) )
+ int sourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ printt( " SourceID:", sourceID )
+ if ( sourceID == -1 )
+ printt( " SourceID: From Code (npc melee, etc)" )
+ else
+ printt( " SourceID:", GetObitFromDamageSourceID( sourceID ) )
+
+ PrintDamageFlags( DamageInfo_GetCustomDamageType( damageInfo ) )
+ #endif
+
+ if ( !ScriptCallback_ShouldEntTakeDamage( ent, damageInfo ) )
+ {
+ // EMP triggers on damage, but in some cases players are invlunerable (embark, disembark, etc...)
+ if ( entIsPlayer && DamageInfo_GetDamageSourceIdentifier( damageInfo ) in level._empForcedCallbacks )
+ {
+ if ( ShouldPlayEMPEffectEvenWhenDamageIsZero( ent, attacker ) )
+ EMP_DamagedPlayerOrNPC( ent, damageInfo )
+ }
+
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ if ( ( IsAirDrone( ent ) ) && ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) in level._empForcedCallbacks ) )
+ {
+ EMP_DamagedPlayerOrNPC( ent, damageInfo )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == damagedef_titan_step )
+ HandleFootstepDamage( ent, damageInfo )
+
+ // HACK helps trap/grenade weapons do damage to the correct entities (player who deployed it as well as the team opposite his)
+ if ( IsValid( inflictor ) && "originalOwner" in inflictor.s )
+ {
+ local ogOwner = inflictor.s.originalOwner
+ if ( IsValid( ogOwner ) )
+ {
+ // if the victim is the guy who damaged the trap, and he is not the ogOwner...
+ if ( ent == attacker && ent != ogOwner )
+ {
+ // HACK to do this legit we need DamageInfo_SetAttacker( damageInfo )
+ // victim should take damage from the original owner instead of the satchel attacker so he gets a kill credit
+ ent.TakeDamage( DamageInfo_GetDamage( damageInfo ), ogOwner, inflictor, { weapon = DamageInfo_GetWeapon( damageInfo ), origin = DamageInfo_GetDamagePosition( damageInfo ), force = DamageInfo_GetDamageForce( damageInfo ), scriptType = DamageInfo_GetCustomDamageType( damageInfo ), damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo ) } )
+
+ // now zero out the normal damage and return
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ }
+ }
+
+ if ( IsValid( inflictor ) )
+ {
+ if ( inflictor.IsProjectile() && entIsPlayer )
+ {
+ if ( inflictor.proj.damageScale != 1.0 )
+ {
+ DamageInfo_ScaleDamage( damageInfo, inflictor.proj.damageScale )
+ }
+
+ // Don't take damage from projectiles created before you where spawned.
+ if ( inflictor.GetProjectileCreationTime() < ent.s.respawnTime && ( Time() - ent.s.respawnTime ) < 2.0 )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ }
+
+ if ( inflictor.e.onlyDamageEntitiesOnce == true || inflictor.e.onlyDamageEntitiesOncePerTick == true )
+ {
+ Assert( !inflictor.e.damagedEntities.contains(ent) )
+ inflictor.e.damagedEntities.append( ent )
+ }
+ }
+
+ // Round damage to nearest full value
+ DamageInfo_SetDamage( damageInfo, floor( DamageInfo_GetDamage( damageInfo ) + 0.5 ) )
+ if ( DamageInfo_GetDamage( damageInfo ) <= 0 )
+ return
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " rounded damage amount:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ HandleLocationBasedDamage( ent, damageInfo )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after location based damage:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ //PROTO Defensive AI Chip. Ideally less invisible gameplay, but something that can combo with other chips.
+ if ( ent.IsTitan() && entIsNPC )
+ {
+ entity soul = ent.GetTitanSoul()
+ if ( IsValid( soul ) && SoulHasPassive( soul, ePassives.PAS_GUARDIAN_CHIP ) )
+ {
+ DamageInfo_SetDamage( damageInfo, DamageInfo_GetDamage( damageInfo ) * 0.8 )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( "After guardian chip :", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+ }
+ }
+
+ RunClassDamageCallbacks( ent, damageInfo )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after class damage callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+
+ // use AddDamageByCallback( "classname", function ) to registed functions
+ if ( IsValid( attacker ) )
+ {
+ if ( attackerIsTitan )
+ {
+ entity soul = attacker.GetTitanSoul()
+ if ( IsValid( soul ) )
+ {
+ float damageAmpScale = 1.0 + StatusEffect_Get( soul, eStatusEffect.titan_damage_amp )
+ if ( damageAmpScale != 1.0 )
+ DamageInfo_ScaleDamage( damageInfo, damageAmpScale )
+ }
+ }
+
+ string attackerClassName = attacker.GetClassName()
+ if ( attackerClassName in svGlobal.damageByCallbacks )
+ {
+ foreach ( callbackFunc in svGlobal.damageByCallbacks[attackerClassName] )
+ {
+ callbackFunc( ent, damageInfo )
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+ }
+ }
+ }
+
+ float damageMultiplier = 1.0 + StatusEffect_Get( ent, eStatusEffect.damage_received_multiplier )
+ if ( damageMultiplier != 1.0 )
+ DamageInfo_ScaleDamage( damageInfo, damageMultiplier )
+
+ // Added via AddEntityCallback_OnDamaged
+ foreach ( callbackFunc in ent.e.entDamageCallbacks )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after AddEntityCallback_OnDamaged callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ // use AddDamageCallbackSourceID( "classname", function ) to registed functions
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( damageSourceId in shGlobal.damageSourceIdCallbacks )
+ {
+ foreach ( callbackFunc in shGlobal.damageSourceIdCallbacks[ damageSourceId ] )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+ }
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after damageSourceID callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+
+ RunClassDamageFinalCallbacks( ent, damageInfo )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after class damage final callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ DamageInfo_AddDamageFlags( damageInfo, DAMAGEFLAG_NOPAIN )
+
+ float savedDamage = DamageInfo_GetDamage( damageInfo )
+
+ TitanDamage titanDamage
+ if ( entIsPlayer )
+ {
+ PlayerTookDamage( ent, damageInfo, attacker, inflictor, damageSourceId, titanDamage )
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 && entIsTitan )
+ {
+ EarnMeterDamageConversion( damageInfo, attacker, ent, 0, titanDamage )
+ return
+ }
+
+ if ( attackerIsPlayer )
+ PlayerDamageFeedback( ent, damageInfo )
+ savedDamage = DamageInfo_GetDamage( damageInfo )
+
+ if ( !entIsTitan )
+ ent.SetCloakFlicker( 0.5, 0.65 )
+ }
+ else
+ {
+ Assert( entIsNPC )
+ bool clearedDamage
+ if ( ent.ai.buddhaMode )
+ {
+ float currentDamage = DamageInfo_GetDamage( damageInfo )
+ int remainingHealth = ent.GetHealth()
+
+ if ( currentDamage >= remainingHealth - ( DOOMED_MIN_HEALTH + 1 ) )
+ {
+ currentDamage = max( remainingHealth - ( DOOMED_MIN_HEALTH + 1 ), 0 )
+ DamageInfo_SetDamage( damageInfo, currentDamage )
+ clearedDamage = currentDamage == 0
+ }
+ }
+
+ if ( !clearedDamage )
+ {
+ if ( entIsTitan )
+ {
+ Titan_NPCTookDamage( ent, damageInfo, titanDamage )
+ savedDamage = DamageInfo_GetDamage( damageInfo )
+ }
+ else
+ {
+ Generic_NPCTookDamage( ent, damageInfo, titanDamage )
+ }
+ }
+
+ if ( attackerIsPlayer )
+ PlayerDamageFeedback( ent, damageInfo )
+ }
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " After player damage mod:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ if ( titanDamage.shieldDamage > 0 )
+ printt( " Shield Damage:", titanDamage.shieldDamage )
+ #endif
+
+ // Added via AddEntityCallback_OnPostDamaged
+ foreach ( callbackFunc in ent.e.entPostDamageCallbacks )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+
+ UpdateLastDamageTime( ent )
+
+ //pain sounds _base_gametype.nut, death sounds in _death_package.nut
+ UpdateDamageState( ent, damageInfo )
+ HandlePainSounds( ent, damageInfo )
+
+ UpdateAttackerInfo( ent, attacker, savedDamage )
+
+ if ( !(DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS) )
+ {
+ if ( attackerIsPlayer )
+ {
+ if ( entIsTitan )
+ {
+ PlayerDealtTitanDamage( attacker, ent, savedDamage, damageInfo )
+
+ entity entSoul = ent.GetTitanSoul()
+ if ( attacker.p.currentTargetPlayerOrSoul_Ent != entSoul )
+ {
+ attacker.p.currentTargetPlayerOrSoul_Ent = ent.GetTitanSoul()
+
+ TitanVO_TellPlayersThatAreAlsoFightingThisTarget( attacker, entSoul )
+ }
+ attacker.p.currentTargetPlayerOrSoul_LastHitTime = Time()
+ }
+ else if ( entIsPlayer )
+ {
+ attacker.p.currentTargetPlayerOrSoul_Ent = ent
+ attacker.p.currentTargetPlayerOrSoul_LastHitTime = Time()
+ }
+ }
+ }
+
+ EarnMeterDamageConversion( damageInfo, attacker, ent, savedDamage, titanDamage )
+
+ if ( entIsTitan )
+ {
+ TitanDamageFlinch( ent, damageInfo )
+
+ if ( TitanDamageRewardsTitanCoreTime() && entIsPlayer && attacker.GetTeam() != ent.GetTeam() )
+ AddCreditToTitanCoreBuilderForTitanDamageReceived( ent, savedDamage )
+ }
+
+ if ( entIsPlayer && !entIsTitan )
+ PilotDamageFlinch( ent, damageInfo )
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " final damage done:", DamageInfo_GetDamage( damageInfo ) )
+ printt( " health: " + ent.GetHealth() )
+ #endif
+
+ RunClassPostDamageCallbacks( ent, damageInfo )
+
+ #if SERVER && MP
+ Stats_OnPlayerDidDamage( ent, damageInfo )
+ PIN_DamageDone( attacker, ent, DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ attacker.Signal( "DamagedPlayerOrNPC" )
+}
+
+void function EarnMeterDamageConversion( var damageInfo, entity attacker, entity ent, float savedDamage, TitanDamage titanDamage )
+{
+ if ( !(DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS) )
+ {
+ bool shouldGiveTimerCredit = file.ShouldGiveTimerCreditGameModeRules( attacker, ent, damageInfo )
+ if ( attacker.IsPlayer() )
+ {
+ float titanSpawnDelay = GetTitanBuildTime( attacker )
+ float timerCredit = 0.0
+
+ if ( shouldGiveTimerCredit )
+ {
+ file.earnMeterOnDamageGameModeRulesCallback( attacker, ent, titanDamage, savedDamage )
+
+ // Timer Credit seems unused. Need to investigate if all DecrementBuildTimer functions are worthless.
+ if ( titanSpawnDelay && IsAlive( ent ) && GetCurrentPlaylistVarInt( "titan_build_credit_enabled", 1 ) == 1 )
+ {
+ if ( ent.IsTitan() )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "titan_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( attacker, SFLAG_HUNTER_TITAN ) )
+ timerCredit *= 2.0
+ }
+ else
+ {
+ if ( ent.IsPlayer() )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "player_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( attacker, SFLAG_HUNTER_PILOT ) )
+ timerCredit *= 2.5
+ }
+ else
+ {
+ if ( IsGrunt( ent ) )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "ai_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( attacker, SFLAG_HUNTER_GRUNT ) )
+ timerCredit *= 2.5
+ }
+ else
+ if ( IsSpectre( ent ) )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( attacker, SFLAG_HUNTER_SPECTRE ) )
+ timerCredit *= 2.5
+ }
+ else
+ if ( IsTurret( ent ) )
+ {
+
+ timerCredit = GetCurrentPlaylistVarFloat( "megaturret_kill_credit", 0.5 )
+ //No 2x burn card for shooting mega turret
+ }
+ #if HAS_EVAC
+ else
+ if ( IsEvacDropship( ent ) )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "evac_dropship_kill_credit", 0.5 )
+ }
+ #endif
+ }
+ }
+
+ float dealtDamage = min( ent.GetHealth(), (savedDamage + titanDamage.shieldDamage) )
+ timerCredit = timerCredit * (dealtDamage / ent.GetMaxHealth().tofloat())
+ }
+
+ if ( IsPilot( attacker ) && PlayerHasPassive( attacker, ePassives.PAS_AT_HUNTER ) )
+ timerCredit *= 1.1
+
+ if ( timerCredit && (!TitanDamageRewardsTitanCoreTime() || !attacker.IsTitan() ) )
+ DecrementBuildTimer( attacker, timerCredit )
+ }
+ }
+
+ if ( shouldGiveTimerCredit //Primary Check
+ && TitanDamageRewardsTitanCoreTime() //Playlist var check
+ && ent.IsTitan()
+ && attacker.IsTitan()
+ && attacker.GetTeam() != ent.GetTeam()
+ && !attacker.ContextAction_IsMeleeExecution() // Some melee executions deal A LOT of damage
+ )
+ AddCreditToTitanCoreBuilderForTitanDamageInflicted( attacker, savedDamage + titanDamage.shieldDamage )
+ }
+}
+
+
+bool function ShouldUseNonTitanHeavyArmorDamageScale( entity victim )
+{
+ if ( (victim.GetArmorType() != ARMOR_TYPE_HEAVY) )
+ return false
+
+ if ( victim.IsTitan() )
+ return false
+
+ if ( IsDropship( victim ) )
+ return false
+
+ return true
+}
+
+void function GameModeRulesEarnMeterOnDamage_Default( entity attacker, entity victim, TitanDamage titanDamage, float savedDamage )
+{
+ #if MP
+ if ( victim.IsTitan() && !attacker.IsTitan() && !IsValid( attacker.GetPetTitan() ) )
+ {
+ float damage = min( victim.GetHealth(), (savedDamage + titanDamage.shieldDamage) )
+ float meterAmount = damage * file.titanMeterGainScale
+ if ( PlayerHasPassive( attacker, ePassives.PAS_AT_HUNTER ) )
+ meterAmount *= 1.1
+ PlayerEarnMeter_AddOwnedFrac( attacker, meterAmount )
+
+ AccumulatedDamageData damageData = file.playerAccumulatedDamageData[attacker]
+ damageData.lastDamageTime = Time()
+ damageData.accumulatedDamage += meterAmount
+
+ if ( damageData.accumulatedDamage >= 0.01 )
+ {
+ attacker.Signal( "UpdateAccumulatedDamageAfterDelay" )
+ AddPlayerScore( attacker, "DamageTitan", null, "", int( damageData.accumulatedDamage * 100 ) )
+ damageData.accumulatedDamage = 0
+ }
+ else
+ {
+ thread UpdateAccumulatedDamageAfterDelay( attacker )
+ }
+ }
+ #endif
+}
+
+void function SetTitanMeterGainScale( float scalar )
+{
+ file.titanMeterGainScale = scalar
+}
+
+#if MP
+void function UpdateAccumulatedDamageAfterDelay( entity attacker )
+{
+ attacker.EndSignal( "OnDeath" )
+ attacker.Signal( "UpdateAccumulatedDamageAfterDelay" )
+ attacker.EndSignal( "UpdateAccumulatedDamageAfterDelay" )
+
+ wait 0.25
+
+ AccumulatedDamageData damageData = file.playerAccumulatedDamageData[attacker]
+
+ if ( damageData.accumulatedDamage == 0 )
+ return
+
+ AddPlayerScore( attacker, "DamageTitan", null, "", int( max( damageData.accumulatedDamage * 100, 1 ) ) )
+ damageData.accumulatedDamage = 0
+}
+#endif
+
+void function SetGameModeRulesEarnMeterOnDamage( void functionref( entity, entity, TitanDamage, float ) rules )
+{
+ file.earnMeterOnDamageGameModeRulesCallback = rules
+}
+
+bool function ShouldGiveTimerCredit_Default( entity player, entity victim, var damageInfo )
+{
+ if ( player == victim )
+ return false
+
+ if ( player.IsTitan() && !IsCoreAvailable( player ) )
+ return false
+
+ if ( GAMETYPE == FREE_AGENCY && !player.IsTitan() )
+ return false
+
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ switch ( damageSourceID )
+ {
+ case eDamageSourceId.mp_titancore_flame_wave:
+ case eDamageSourceId.mp_titancore_flame_wave_secondary:
+ case eDamageSourceId.mp_titancore_salvo_core:
+ case damagedef_titan_fall:
+ case damagedef_nuclear_core:
+ return false
+ }
+
+ return true
+}
+
+bool function GameModeRulesShouldGiveTimerCredit( entity player, entity victim, var damageInfo )
+{
+ return file.ShouldGiveTimerCreditGameModeRules( player, victim, damageInfo )
+}
+
+void function SetGameModeRulesShouldGiveTimerCredit( bool functionref( entity, entity, var ) rules )
+{
+ file.ShouldGiveTimerCreditGameModeRules = rules
+}
+
+function TitanDamageFlinch( entity ent, damageInfo )
+{
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ if ( TitanStagger( ent, damageInfo ) )
+ return
+
+ if ( DamageInfo_GetDamage( damageInfo ) >= TITAN_ADDITIVE_FLINCH_DAMAGE_THRESHOLD )
+ AddFlinch( ent, damageInfo )
+}
+
+function PilotDamageFlinch( entity ent, damageInfo )
+{
+ //if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ // return
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+ if ( damage >= 5 )
+ AddFlinch( ent, damageInfo )
+}
+
+vector function GetDamageOrigin( damageInfo, entity victim = null )
+{
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+
+ if ( inflictor == svGlobal.worldspawn )
+ return DamageInfo_GetDamagePosition( damageInfo )
+
+ vector damageOrigin = IsValid( inflictor ) ? inflictor.GetOrigin() : DamageInfo_GetDamagePosition( damageInfo )
+
+ switch ( damageSourceId )
+ {
+ case eDamageSourceId.mp_weapon_satchel:
+ case eDamageSourceId.mp_weapon_proximity_mine:
+ case eDamageSourceId.mp_titanweapon_arc_pylon:
+ break
+
+ case damagedef_nuclear_core:
+ case eDamageSourceId.mp_titanability_smoke:
+ //if ( IsValid( victim ) && victim.IsPlayer() && IsValid( victim.GetTitanSoulBeingRodeoed() ) )
+ {
+ damageOrigin += (RandomVecInDome( Vector( 0, 0, -1 ) ) * 300.0)
+ damageOrigin += Vector( 0, 0, 128 )
+ }
+ break
+
+ case eDamageSourceId.switchback_trap:
+ if ( IsValid( victim ) && victim.IsPlayer() )
+ damageOrigin = victim.EyePosition() + (RandomVecInDome( Vector( 0, 0, -1 ) ) * 300.0)
+ break
+
+ default:
+ if ( DamageInfo_GetAttacker( damageInfo ) )
+ {
+ inflictor = DamageInfo_GetAttacker( damageInfo )
+ damageOrigin = inflictor.GetWorldSpaceCenter()
+ }
+ break
+ }
+
+ return damageOrigin
+}
+
+/*
+function TrackDPS( ent )
+{
+ ent.s.dpsTracking <- {}
+ ent.s.dpsTracking.damage <- 0
+
+ local startTime = Time()
+
+ ent.WaitSignal( "Doomed" )
+
+ local duration = Time() - startTime
+
+ printt( "DPS:", ent.s.dpsTracking.damage / duration, duration )
+
+ delete ent.s.dpsTracking
+}
+
+function UpdateDPS( ent, damageInfo )
+{
+ if ( GetDoomedState( ent ) )
+ return
+
+ if ( !( "dpsTracking" in ent.s ) )
+ thread TrackDPS( ent )
+
+ ent.s.dpsTracking.damage += DamageInfo_GetDamage( damageInfo )
+}
+*/
+
+
+void function PlayerTookDamage( entity player, var damageInfo, entity attacker, entity inflictor, int damageSourceId, TitanDamage titanDamage )
+{
+ int hitBox = DamageInfo_GetHitBox( damageInfo )
+
+ bool critHit = false
+
+ if ( CritWeaponInDamageInfo( damageInfo ) )
+ critHit = IsCriticalHit( attacker, player, hitBox, DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) )
+
+ if ( critHit )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+
+ array<string> weaponMods = GetWeaponModsFromDamageInfo( damageInfo )
+
+ local eModSourceID = null
+ foreach ( mod in weaponMods )
+ {
+ local modSourceID = GetModSourceID( mod )
+ if ( modSourceID != null && modSourceID in modNameStrings )
+ eModSourceID = modSourceID
+ }
+
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == eDamageSourceId.fall )
+ DamageInfo_SetForceKill( damageInfo, true )
+
+ bool isTitan = player.IsTitan()
+
+ if ( isTitan )
+ Titan_PlayerTookDamage( player, damageInfo, attacker, critHit, titanDamage )
+ else
+ Wallrun_PlayerTookDamage( player, damageInfo, attacker )
+
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ bool isKillShot = (damageAmount >= player.GetHealth())
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ if ( isKillShot )
+ damageType = (damageType | DF_KILLSHOT)
+
+ if ( isTitan && (DamageInfo_GetDamage( damageInfo ) == 0) )
+ {
+ if ( titanDamage.doomedNow ) // to make kill card come up for you even if you have auto-eject. In Titan_PlayerTookDamage we set damage to 0 if you have Auto-Eject and are doomed
+ TellClientPlayerTookDamage( player, damageInfo, attacker, eModSourceID, damageType, damageSourceId, titanDamage )
+ }
+
+ vector attackerOrigin = Vector( 0, 0, 0 )
+ if ( IsValid( attacker ) )
+ attackerOrigin = attacker.GetOrigin()
+
+ if ( IsAlive( player ) )
+ {
+ float storeTime = MAX_DAMAGE_HISTORY_TIME
+ entity storeEnt
+ if ( isTitan )
+ {
+ storeEnt = player.GetTitanSoul()
+ }
+ else
+ {
+ storeEnt = player
+ if ( IsSingleplayer() )
+ storeTime = 30.0
+ }
+
+ StoreDamageHistoryAndUpdate( storeEnt, storeTime, DamageInfo_GetDamage( damageInfo ), attackerOrigin, damageType, damageSourceId, attacker, weaponMods )
+ }
+
+ if ( !(DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS) )
+ TellClientPlayerTookDamage( player, damageInfo, attacker, eModSourceID, damageType, damageSourceId, titanDamage )
+}
+
+function TellClientPlayerTookDamage( entity player, damageInfo, entity attacker, eModSourceID, int damageType, int damageSourceId, TitanDamage titanDamage )
+{
+ if ( !player.hasConnected )
+ return
+
+ local attackerEHandle = IsValid( attacker ) ? attacker.GetEncodedEHandle() : null
+ local weaponEHandle = IsValid( DamageInfo_GetWeapon( damageInfo ) ) ? DamageInfo_GetWeapon( damageInfo ).GetEncodedEHandle() : null
+ local damageOrigin = GetDamageOrigin( damageInfo, player )
+
+ if ( player.IsTitan() )
+ Remote_CallFunction_Replay( player, "ServerCallback_TitanTookDamage", DamageInfo_GetDamage( damageInfo ), damageOrigin.x, damageOrigin.y, damageOrigin.z, damageType, damageSourceId, attackerEHandle, eModSourceID, titanDamage.doomedNow, titanDamage.doomedDamage )
+ else
+ Remote_CallFunction_Replay( player, "ServerCallback_PilotTookDamage", DamageInfo_GetDamage( damageInfo ), damageOrigin.x, damageOrigin.y, damageOrigin.z, damageType, damageSourceId, attackerEHandle, eModSourceID )
+}
+
+// This only handles damage events. Whizbys can still cause snipercam to trigger without passing through this check.
+function CodeCallBack_ShouldTriggerSniperCam( damageInfo )
+{
+ switch ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) )
+ {
+ case damagedef_titan_step:
+ case eDamageSourceId.super_electric_smoke_screen:
+ return false
+ }
+
+ return true
+}
+
+bool function CodeCallback_ForceAIMissPlayer( entity npc, entity player )
+{
+ return SPMP_Callback_ForceAIMissPlayer( npc, player )
+}
+
+bool function CodeCallback_OnTouchHealthKit( entity player, entity ent )
+{
+ string entityClassName = ent.GetClassName()
+
+ Assert( entityClassName in svGlobal.onTouchHealthKitCallbacks )
+
+ array<bool functionref( entity player, entity healthpack )> callbackFuncs = svGlobal.onTouchHealthKitCallbacks[ entityClassName ]
+ foreach ( callbackFunc in callbackFuncs )
+ {
+ bool result = callbackFunc( player, ent )
+ if ( result )
+ return result
+ }
+
+ return false
+}
+
+bool function ShouldPlayEMPEffectEvenWhenDamageIsZero( entity ent, entity attacker )
+{
+ if ( ent.IsTitan() && IsTitanWithinBubbleShield( ent ) )
+ return false
+
+ if ( !IsValid( attacker ) )
+ return true
+
+ if ( attacker.GetTeam() != ent.GetTeam() )
+ return true
+
+ return false
+}
+
+void function CodeCallback_OnPlayerGrappled( entity player, entity victim )
+{
+ if ( victim.GetTeam() != player.GetTeam() )
+ {
+ if ( victim.p.lastGrappledTime + TITAN_GRAPPLE_DEBOUNCE_TIME < Time() )
+ {
+ if ( player.IsTitan() )
+ {
+ victim.TakeDamage( TITAN_GRAPPLE_DAMAGE, player, player, { origin = victim.EyePosition(), scriptType = DF_GIB, damageSourceId = eDamageSourceId.titan_grapple } )
+
+ if ( victim.IsTitan() )
+ {
+ entity soul = victim.GetTitanSoul()
+ if ( soul == null )
+ soul = victim
+
+ float fadeTime = 0.5
+ StatusEffect_AddTimed( soul, eStatusEffect.dodge_speed_slow, 0.75, 0.9 + fadeTime, fadeTime )
+ StatusEffect_AddTimed( soul, eStatusEffect.move_slow, 0.75, 0.9 + fadeTime, fadeTime )
+ }
+ }
+
+ if ( victim.IsPlayer() )
+ {
+ if ( player.IsTitan() )
+ MessageToPlayer( victim, eEventNotifications.Grapple_WasGrappled_ByTitan )
+ else
+ MessageToPlayer( victim, eEventNotifications.Grapple_WasGrappled_ByPilot )
+ }
+ }
+
+ victim.p.lastGrappledTime = Time()
+ }
+}
+
+void function CodeCallback_OnProjectileGrappled( entity player, entity projectile )
+{
+
+}
+
+void function DamageInfo_ScaleDamage( var damageInfo, float scalar )
+{
+ DamageInfo_SetDamage( damageInfo, DamageInfo_GetDamage( damageInfo ) * scalar )
+}
+
+string function CodeCallback_CheckPassThroughAddsMods( entity player, entity hitEnt, string currWeaponName )
+{
+ if ( !IsValid( player ) )
+ return ""
+
+ if ( StatusEffect_Get( hitEnt, eStatusEffect.pass_through_amps_weapon ) > 0 )
+ {
+ array<string> mods = GetWeaponBurnMods( currWeaponName )
+ if ( mods.len() > 0 )
+ return mods[0]
+ }
+ return ""
+}
+
+void function Generic_NPCTookDamage( entity npc, damageInfo, TitanDamage titanDamage )
+{
+ Assert( !npc.IsTitan() )
+ Assert( DamageInfo_GetDamage( damageInfo ) > 0 )
+ Assert( IsAlive( npc ) )
+
+ bool critHit = false
+ if ( CritWeaponInDamageInfo( damageInfo ) )
+ critHit = IsCriticalHit( DamageInfo_GetAttacker( damageInfo ), npc, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) )
+
+ if ( critHit )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+
+ titanDamage.shieldDamage = NPCShieldHealthUpdate( npc, damageInfo, critHit )
+}
+
+
+int function NPCShieldHealthUpdate( entity npc, damageInfo, bool critHit )
+{
+ if ( npc.GetShieldHealth() <= 0 )
+ return 0
+
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == damagedef_suicide )
+ return 0
+
+ if ( DamageInfo_GetForceKill( damageInfo ) )
+ {
+ npc.SetShieldHealth( 0 )
+ return 0
+ }
+ else if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_BYPASS_SHIELD )
+ {
+ return 0
+ }
+
+ DamageInfo_AddCustomDamageType( damageInfo, DF_SHIELD_DAMAGE )
+ return int( ShieldModifyDamage( npc, damageInfo ) )
+}
+
+#if MP
+// taken from sp/sh_sp_dialogue.gnut, with some sp-exclusive stuff removed
+
+// called by code when an animation does { event AE_SV_VSCRIPT_CALLBACK FrameNumber "some string" }
+// and by a script function OnFootstep, apparently.
+void function CodeCallback_OnServerAnimEvent( entity ent, string eventName )
+{
+ PerfStart( PerfIndexServer.CB_OnServerAnimEvent )
+ if ( HasAnimEvent( ent, eventName ) )
+ thread RunAnimEventCallbacks( ent, eventName )
+
+ if ( eventName in svGlobal.globalAnimEventCallbacks )
+ {
+ thread svGlobal.globalAnimEventCallbacks[ eventName ]( ent )
+ PerfEnd( PerfIndexServer.CB_OnServerAnimEvent )
+ return
+ }
+
+
+ // couldn't find this eventName on the ent or the global anim events,
+ // so try breaking it down. If we didn't find it, it means
+ // script needs to handle the event, even if it is just to
+ // do nothing with it
+
+ array<string> tokens = split( eventName, ":" )
+ string tokenName = tokens[0]
+
+ switch ( tokenName )
+ {
+ case "worldsound":
+ GlobalAnimEventWithStringParameter_WorldSound( ent, tokens[1] )
+ break
+
+ case "signal":
+ SendSignalFromTokens( ent, tokens )
+ break
+
+ case "flagset":
+ GlobalAnimEventWithStringParameter_FlagSet( ent, tokens[1] )
+ break
+
+ //case "dialogue":
+ // // Make sure that animation triggered dialogue uses the correct priority and skips the queue
+ // string name = tokens[1]
+ // Assert( file.registeredDialogIDs.find( name ) >= 0, "Dialogue line " + name + " is not registered" )
+ // int aliasID = file.registeredDialogIDs.find( name )
+ // DialogueData data = file.registeredDialog[ aliasID ]
+ // Assert( data.priority == PRIORITY_NO_QUEUE, "Dialogue " + name + " triggered via qc must use PRIORITY_NO_QUEUE" )
+ // thread PlayDialogue( name, ent )
+ // break
+
+ case "fireViperSalvo":
+ int value = tokens[1].tointeger()
+ ent.Signal( "fireSalvo", { num = value } )
+ break
+
+ //case "conversation":
+ // thread PlayerConversation( tokens[1], GetPlayerArray()[0], ent )
+ // break
+ }
+
+ PerfEnd( PerfIndexServer.CB_OnServerAnimEvent )
+}
+#endif // #if MP \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_dropship_spawn_common.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_dropship_spawn_common.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_dropship_spawn_common.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate.nut
new file mode 100644
index 00000000..603c38fa
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate.nut
@@ -0,0 +1,144 @@
+untyped
+
+global function GameState_Init
+global function InitGameState
+
+global function SetRoundBased
+global function SetCustomIntroLength
+
+global function SetGetDifficultyFunc
+global function GetDifficultyLevel
+
+//********************************************************************************************
+// Game State
+//********************************************************************************************
+global const PREMATCH_TIMER_INTRO_DEFAULT = 46
+global const PREMATCH_TIMER_NO_INTRO = 7 //shows 5 when fade from black
+global const CLEAR_PLAYERS_BUFFER = 2.0
+
+global const ENDROUND_FREEZE = 0
+global const ENDROUND_MOVEONLY = 1
+global const ENDROUND_FREE = 3
+
+global const NO_DETERMINED_WINNING_TEAM_YET = -1
+
+struct
+{
+ int functionref() difficultyFunc
+} file
+
+global enum eWinReason
+{
+ DEFAULT,
+ SCORE_LIMIT,
+ TIME_LIMIT,
+ ELIMINATION
+}
+
+
+function GameState_Init()
+{
+ FlagInit( "GamePlaying" )
+ FlagInit( "DisableTimeLimit" )
+ FlagInit( "DisableScoreLimit" )
+ FlagInit( "AnnounceWinnerEnabled", true )
+ FlagInit( "AnnounceProgressEnabled", true )
+ FlagInit( "DefendersWinDraw" )
+
+ RegisterSignal( "RoundEnd" )
+ RegisterSignal( "GameEnd" )
+ RegisterSignal( "GameStateChanged" )
+ RegisterSignal( "CatchUpFallBehindVO" )
+ RegisterSignal( "ClearedPlayers" )
+
+
+ level.devForcedWin <- false //For dev purposes only. Used to check if we forced a win through dev command
+ level.devForcedTimeLimit <- false
+
+ level.lastTimeLeftSeconds <- null
+
+ level.lastScoreSwapVOTime <- null
+
+ level.nextMatchProgressAnnouncementLevel <- MATCH_PROGRESS_EARLY //When we make a matchProgressAnnouncement, this variable is set
+
+ level.endOfRoundPlayerState <- ENDROUND_FREEZE
+
+ level._swapGameStateOnNextFrame <- false
+ level.clearedPlayers <- false
+
+ level.customEpilogueDuration <- null
+
+ level.lastTeamTitans <- {}
+ level.lastTeamTitans[TEAM_IMC] <- null
+ level.lastTeamTitans[TEAM_MILITIA] <- null
+ level.lastTeamPilots <- {}
+ level.lastTeamPilots[TEAM_IMC] <- null
+ level.lastTeamPilots[TEAM_MILITIA] <- null
+
+ level.firstTitanfall <- false
+
+ level.lastPlayingEmptyTeamCheck <- 0
+
+ level.doneWaitingForPlayersTimeout <- 0
+
+ level.attackDefendBased <- false
+
+ level.roundBasedUsingTeamScore <- false
+
+ level.roundBasedTeamScoreNoReset <- false
+
+ level.customIntroLength <- null
+
+ level.sendingPlayersAway <- false
+
+ level.forceNoMoreRounds <- false
+
+ // prevents ties... need an option to disable in the future
+ level.firstToScoreLimit <- TEAM_UNASSIGNED
+ level.allowPointsOverLimit <- false
+
+ file.difficultyFunc = DefaultDifficultyFunc
+
+ #if MP
+ AddCallback_EntitiesDidLoad( GameState_EntitiesDidLoad )
+ #endif
+}
+
+
+int function DefaultDifficultyFunc()
+{
+ return 0
+}
+
+void function SetGetDifficultyFunc( int functionref() difficultyFunc )
+{
+ Assert( file.difficultyFunc == DefaultDifficultyFunc )
+
+ file.difficultyFunc = difficultyFunc
+}
+
+
+// This function is meant to init stuff that _gamestate uses, as opposed
+// to stuff that any particular gamestate like Playing uses
+function InitGameState()
+{
+ #if MP
+ PIN_GameStart()
+ #endif
+}
+
+function SetRoundBased( state )
+{
+ level.nv.roundBased = state
+}
+
+function SetCustomIntroLength( time )
+{
+ level.customIntroLength = time
+}
+
+int function GetDifficultyLevel()
+{
+ return file.difficultyFunc()
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut
new file mode 100644
index 00000000..197ac5e9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut
@@ -0,0 +1,734 @@
+untyped
+
+global function PIN_GameStart
+global function SetGameState
+global function GameState_EntitiesDidLoad
+global function WaittillGameStateOrHigher
+global function AddCallback_OnRoundEndCleanup
+
+global function SetShouldUsePickLoadoutScreen
+global function SetSwitchSidesBased
+global function SetSuddenDeathBased
+global function SetShouldUseRoundWinningKillReplay
+global function SetRoundWinningKillReplayKillClasses
+global function SetRoundWinningKillReplayAttacker
+global function SetWinner
+global function SetTimeoutWinnerDecisionFunc
+global function AddTeamScore
+
+global function GameState_GetTimeLimitOverride
+global function IsRoundBasedGameOver
+global function ShouldRunEvac
+global function GiveTitanToPlayer
+global function GetTimeLimit_ForGameMode
+
+struct {
+ // used for togglable parts of gamestate
+ bool usePickLoadoutScreen
+ bool switchSidesBased
+ bool suddenDeathBased
+ int functionref() timeoutWinnerDecisionFunc
+
+ // for waitingforplayers
+ int numPlayersFullyConnected
+
+ bool hasSwitchedSides
+
+ int announceRoundWinnerWinningSubstr
+ int announceRoundWinnerLosingSubstr
+
+ bool roundWinningKillReplayTrackPilotKills = true
+ bool roundWinningKillReplayTrackTitanKills = false
+
+ float roundWinningKillReplayTime
+ entity roundWinningKillReplayVictim
+ entity roundWinningKillReplayAttacker
+ int roundWinningKillReplayMethodOfDeath
+ float roundWinningKillReplayTimeOfDeath
+ float roundWinningKillReplayHealthFrac
+
+ array<void functionref()> roundEndCleanupCallbacks
+} file
+
+void function PIN_GameStart()
+{
+ // todo: using the pin telemetry function here, weird and was done veeery early on before i knew how this all worked, should use a different one
+
+ // called from InitGameState
+ //FlagInit( "ReadyToStartMatch" )
+
+ SetServerVar( "switchedSides", 0 )
+ SetServerVar( "winningTeam", -1 )
+
+ AddCallback_GameStateEnter( eGameState.WaitingForCustomStart, GameStateEnter_WaitingForCustomStart )
+ AddCallback_GameStateEnter( eGameState.WaitingForPlayers, GameStateEnter_WaitingForPlayers )
+ AddCallback_OnClientConnected( WaitingForPlayers_ClientConnected )
+ AddCallback_OnClientDisconnected( WaitingForPlayers_ClientDisconnected )
+
+ AddCallback_GameStateEnter( eGameState.PickLoadout, GameStateEnter_PickLoadout )
+ AddCallback_GameStateEnter( eGameState.Prematch, GameStateEnter_Prematch )
+ AddCallback_GameStateEnter( eGameState.Playing, GameStateEnter_Playing )
+ AddCallback_GameStateEnter( eGameState.WinnerDetermined, GameStateEnter_WinnerDetermined )
+ AddCallback_GameStateEnter( eGameState.SwitchingSides, GameStateEnter_SwitchingSides )
+ AddCallback_GameStateEnter( eGameState.SuddenDeath, GameStateEnter_SuddenDeath )
+ AddCallback_GameStateEnter( eGameState.Postmatch, GameStateEnter_Postmatch )
+
+ AddCallback_OnPlayerKilled( OnPlayerKilled )
+ AddDeathCallback( "npc_titan", OnTitanKilled )
+
+ RegisterSignal( "CleanUpEntitiesForRoundEnd" )
+}
+
+void function SetGameState( int newState )
+{
+ SetServerVar( "gameStateChangeTime", Time() )
+ SetServerVar( "gameState", newState )
+ svGlobal.levelEnt.Signal( "GameStateChanged" )
+
+ // added in AddCallback_GameStateEnter
+ foreach ( callbackFunc in svGlobal.gameStateEnterCallbacks[ newState ] )
+ callbackFunc()
+}
+
+void function GameState_EntitiesDidLoad()
+{
+ // nothing of importance to put here, this is referenced in _gamestate though so need it
+}
+
+void function WaittillGameStateOrHigher( int gameState )
+{
+ while ( GetGameState() < gameState )
+ svGlobal.levelEnt.WaitSignal( "GameStateChanged" )
+}
+
+
+// logic for individual gamestates:
+
+
+// eGameState.WaitingForCustomStart
+void function GameStateEnter_WaitingForCustomStart()
+{
+ // unused in release, comments indicate this was supposed to be used for an e3 demo
+ // perhaps games in this demo were manually started by an employee? no clue really
+}
+
+
+// eGameState.WaitingForPlayers
+void function GameStateEnter_WaitingForPlayers()
+{
+ foreach ( entity player in GetPlayerArray() )
+ WaitingForPlayers_ClientConnected( player )
+
+ thread WaitForPlayers( GetPendingClientsCount() + file.numPlayersFullyConnected ) // like 90% sure there should be a way to get number of loading clients on server but idk it
+}
+
+void function WaitForPlayers( int wantedNum )
+{
+ // note: atm if someone disconnects as this happens the game will just wait forever
+ print( "WaitForPlayers(): " + wantedNum + " players" )
+ float endTime = Time() + 120.0
+
+ while ( endTime > Time() )
+ {
+ if ( file.numPlayersFullyConnected >= wantedNum )
+ break
+
+ WaitFrame()
+ }
+
+ print( "done waiting!" )
+
+ wait 1.0 // bit nicer
+
+ if ( file.usePickLoadoutScreen )
+ SetGameState( eGameState.PickLoadout )
+ else
+ SetGameState( eGameState.Prematch )
+}
+
+void function WaitingForPlayers_ClientConnected( entity player )
+{
+ if ( GetGameState() == eGameState.WaitingForPlayers )
+ ScreenFadeToBlackForever( player, 0.0 )
+
+ file.numPlayersFullyConnected++
+}
+
+void function WaitingForPlayers_ClientDisconnected( entity player )
+{
+ file.numPlayersFullyConnected--
+}
+
+
+// eGameState.PickLoadout
+void function GameStateEnter_PickLoadout()
+{
+ thread GameStateEnter_PickLoadout_Threaded()
+}
+
+void function GameStateEnter_PickLoadout_Threaded()
+{
+ float pickloadoutLength = 20.0 // may need tweaking
+ SetServerVar( "minPickLoadOutTime", Time() + pickloadoutLength )
+
+ // titan selection menu can change minPickLoadOutTime so we need to wait manually until we hit the time
+ while ( Time() < GetServerVar( "minPickLoadOutTime" ) )
+ WaitFrame()
+
+ SetGameState( eGameState.Prematch )
+}
+
+
+// eGameState.Prematch
+void function GameStateEnter_Prematch()
+{
+ int timeLimit = GameMode_GetTimeLimit( GAMETYPE ) * 60
+ if ( file.switchSidesBased )
+ timeLimit /= 2 // endtime is half of total per side
+
+ SetServerVar( "gameEndTime", Time() + timeLimit + ClassicMP_GetIntroLength() )
+ SetServerVar( "roundEndTime", Time() + ClassicMP_GetIntroLength() + GameMode_GetRoundTimeLimit( GAMETYPE ) * 60 )
+}
+
+
+// eGameState.Playing
+void function GameStateEnter_Playing()
+{
+ thread GameStateEnter_Playing_Threaded()
+}
+
+void function GameStateEnter_Playing_Threaded()
+{
+ WaitFrame() // ensure timelimits are all properly set
+
+ while ( GetGameState() == eGameState.Playing )
+ {
+ // could cache these, but what if we update it midgame?
+ float endTime
+ if ( IsRoundBased() )
+ endTime = expect float( GetServerVar( "roundEndTime" ) )
+ else
+ endTime = expect float( GetServerVar( "gameEndTime" ) )
+
+ // time's up!
+ if ( Time() >= endTime )
+ {
+ int winningTeam
+ if ( file.timeoutWinnerDecisionFunc != null )
+ winningTeam = file.timeoutWinnerDecisionFunc()
+ else
+ winningTeam = GameScore_GetWinningTeam()
+
+ if ( file.switchSidesBased && !file.hasSwitchedSides && !IsRoundBased() ) // in roundbased modes, we handle this in setwinner
+ SetGameState( eGameState.SwitchingSides )
+ else if ( file.suddenDeathBased && winningTeam == TEAM_UNASSIGNED ) // suddendeath if we draw and suddendeath is enabled and haven't switched sides
+ SetGameState( eGameState.SuddenDeath )
+ else
+ SetWinner( winningTeam )
+ }
+
+ WaitFrame()
+ }
+}
+
+
+// eGameState.WinnerDetermined
+// these are likely innacurate
+const float ROUND_END_FADE_KILLREPLAY = 1.0
+const float ROUND_END_DELAY_KILLREPLAY = 3.0
+const float ROUND_END_FADE_NOKILLREPLAY = 8.0
+const float ROUND_END_DELAY_NOKILLREPLAY = 10.0
+
+void function GameStateEnter_WinnerDetermined()
+{
+ thread GameStateEnter_WinnerDetermined_Threaded()
+}
+
+void function GameStateEnter_WinnerDetermined_Threaded()
+{
+ bool killcamsWereEnabled = KillcamsEnabled()
+ if ( killcamsWereEnabled ) // dont want killcams to interrupt stuff
+ SetKillcamsEnabled( false )
+
+ WaitFrame() // wait a frame so other scripts can setup killreplay stuff
+
+ entity replayAttacker = file.roundWinningKillReplayAttacker
+ bool doReplay = Replay_IsEnabled() && !( !IsRoundBased() && Evac_IsEnabled() ) && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker )
+ && Time() - file.roundWinningKillReplayTime <= ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY
+
+ float replayLength = 2.0 // extra delay if no replay
+ if ( doReplay )
+ {
+ replayLength = ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY
+ if ( "respawnTime" in replayAttacker.s && Time() - replayAttacker.s.respawnTime < replayLength )
+ replayLength += Time() - expect float ( replayAttacker.s.respawnTime )
+
+ SetServerVar( "roundWinningKillReplayEntHealthFrac", file.roundWinningKillReplayHealthFrac )
+ }
+
+ foreach ( entity player in GetPlayerArray() )
+ thread PlayerWatchesRoundWinningKillReplay( player, doReplay, replayLength )
+
+ wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
+ CleanUpEntitiesForRoundEnd() // fade should be done by this point, so cleanup stuff now when people won't see
+ wait replayLength
+
+ WaitFrame() // prevent a race condition with PlayerWatchesRoundWinningKillReplay
+ file.roundWinningKillReplayAttacker = null // clear this
+
+ if ( killcamsWereEnabled )
+ SetKillcamsEnabled( true )
+
+ if ( IsRoundBased() )
+ {
+ svGlobal.levelEnt.Signal( "RoundEnd" )
+ int roundsPlayed = expect int ( GetServerVar( "roundsPlayed" ) )
+ SetServerVar( "roundsPlayed", roundsPlayed + 1 )
+
+ float highestScore = max( GameRules_GetTeamScore( TEAM_IMC ), GameRules_GetTeamScore( TEAM_MILITIA ) )
+ int roundScoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE )
+
+ if ( highestScore >= roundScoreLimit )
+ SetGameState( eGameState.Postmatch )
+ else if ( file.switchSidesBased && !file.hasSwitchedSides && highestScore >= ( roundScoreLimit.tofloat() / 2.0 ) ) // round up
+ SetGameState( eGameState.SwitchingSides ) // note: switchingsides will handle setting to pickloadout and prematch by itself
+ else if ( file.usePickLoadoutScreen )
+ SetGameState( eGameState.PickLoadout )
+ else
+ SetGameState ( eGameState.Prematch )
+ }
+ else
+ {
+ if ( Evac_IsEnabled() )
+ SetGameState( eGameState.Epilogue )
+ else
+ SetGameState( eGameState.Postmatch )
+ }
+}
+
+void function PlayerWatchesRoundWinningKillReplay( entity player, bool doReplay, float replayLength )
+{
+ player.FreezeControlsOnServer()
+
+ int winningTeam = GetWinningTeam()
+ int announcementSubstr
+ if ( winningTeam != TEAM_UNASSIGNED )
+ announcementSubstr = player.GetTeam() == winningTeam ? file.announceRoundWinnerWinningSubstr : file.announceRoundWinnerLosingSubstr
+
+ if ( IsRoundBased() )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_AnnounceRoundWinner", winningTeam, announcementSubstr, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME, GameRules_GetTeamScore2( TEAM_MILITIA ), GameRules_GetTeamScore2( TEAM_IMC ) )
+ else
+ Remote_CallFunction_NonReplay( player, "ServerCallback_AnnounceWinner", winningTeam, announcementSubstr, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME )
+
+ if ( IsRoundBased() || !Evac_IsEnabled() ) // if we're doing evac, then no fades or killreplay
+ {
+ ScreenFadeToBlackForever( player, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME )
+ wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
+
+ if ( doReplay )
+ {
+ player.SetPredictionEnabled( false ) // prediction fucks with replays
+
+ entity attacker = file.roundWinningKillReplayAttacker
+ player.SetKillReplayDelay( Time() - replayLength, THIRD_PERSON_KILL_REPLAY_ALWAYS )
+ player.SetKillReplayInflictorEHandle( attacker.GetEncodedEHandle() )
+ player.SetKillReplayVictim( file.roundWinningKillReplayVictim )
+ player.SetViewIndex( attacker.GetIndexForEntity() )
+ player.SetIsReplayRoundWinning( true )
+
+ if ( replayLength >= ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY - 0.5 ) // only do fade if close to full length replay
+ {
+ // this doesn't work because fades don't work on players that are in a replay, unsure how official servers do this
+ wait replayLength - 2.0
+ ScreenFadeToBlackForever( player, 2.0 )
+
+ wait 2.0
+ }
+ else
+ wait replayLength
+ }
+ else
+ wait replayLength // this will just be extra delay if no replay
+
+ player.SetPredictionEnabled( true )
+ player.ClearReplayDelay()
+ player.ClearViewEntity()
+ player.UnfreezeControlsOnServer()
+ }
+}
+
+
+// eGameState.SwitchingSides
+void function GameStateEnter_SwitchingSides()
+{
+ thread GameStateEnter_SwitchingSides_Threaded()
+}
+
+void function GameStateEnter_SwitchingSides_Threaded()
+{
+ bool killcamsWereEnabled = KillcamsEnabled()
+ if ( killcamsWereEnabled ) // dont want killcams to interrupt stuff
+ SetKillcamsEnabled( false )
+
+ WaitFrame() // wait a frame so callbacks can set killreplay info
+
+ entity replayAttacker = file.roundWinningKillReplayAttacker
+ bool doReplay = Replay_IsEnabled() && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker ) && !IsRoundBased() // for roundbased modes, we've already done the replay
+ && Time() - file.roundWinningKillReplayTime <= SWITCHING_SIDES_DELAY
+
+ float replayLength = SWITCHING_SIDES_DELAY_REPLAY // extra delay if no replay
+ if ( doReplay )
+ {
+ replayLength = SWITCHING_SIDES_DELAY
+ if ( "respawnTime" in replayAttacker.s && Time() - replayAttacker.s.respawnTime < replayLength )
+ replayLength += Time() - expect float ( replayAttacker.s.respawnTime )
+
+ SetServerVar( "roundWinningKillReplayEntHealthFrac", file.roundWinningKillReplayHealthFrac )
+ }
+
+ foreach ( entity player in GetPlayerArray() )
+ thread PlayerWatchesSwitchingSidesKillReplay( player, doReplay, replayLength )
+
+ wait SWITCHING_SIDES_DELAY_REPLAY
+ CleanUpEntitiesForRoundEnd() // fade should be done by this point, so cleanup stuff now when people won't see
+ wait replayLength
+
+ if ( killcamsWereEnabled )
+ SetKillcamsEnabled( true )
+
+ file.hasSwitchedSides = true
+ svGlobal.levelEnt.Signal( "RoundEnd" ) // might be good to get a new signal for this? not 100% necessary tho i think
+ SetServerVar( "switchedSides", 1 )
+ file.roundWinningKillReplayAttacker = null // reset this after replay
+
+ if ( file.usePickLoadoutScreen )
+ SetGameState( eGameState.PickLoadout )
+ else
+ SetGameState ( eGameState.Prematch )
+}
+
+void function PlayerWatchesSwitchingSidesKillReplay( entity player, bool doReplay, float replayLength )
+{
+ player.FreezeControlsOnServer()
+
+ ScreenFadeToBlackForever( player, SWITCHING_SIDES_DELAY_REPLAY ) // automatically cleared
+ wait SWITCHING_SIDES_DELAY_REPLAY
+
+ if ( doReplay )
+ {
+ player.SetPredictionEnabled( false ) // prediction fucks with replays
+
+ // delay seems weird for switchingsides? ends literally the frame the flag is collected
+
+ entity attacker = file.roundWinningKillReplayAttacker
+ player.SetKillReplayDelay( Time() - replayLength, THIRD_PERSON_KILL_REPLAY_ALWAYS )
+ player.SetKillReplayInflictorEHandle( attacker.GetEncodedEHandle() )
+ player.SetKillReplayVictim( file.roundWinningKillReplayVictim )
+ player.SetViewIndex( attacker.GetIndexForEntity() )
+ player.SetIsReplayRoundWinning( true )
+
+ if ( replayLength >= SWITCHING_SIDES_DELAY - 0.5 ) // only do fade if close to full length replay
+ {
+ // this doesn't work because fades don't work on players that are in a replay, unsure how official servers do this
+ wait replayLength - ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
+ ScreenFadeToBlackForever( player, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME )
+
+ wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
+ }
+ else
+ wait replayLength
+ }
+ else
+ wait SWITCHING_SIDES_DELAY_REPLAY // extra delay if no replay
+
+ player.SetPredictionEnabled( true )
+ player.ClearReplayDelay()
+ player.ClearViewEntity()
+ player.UnfreezeControlsOnServer()
+}
+
+
+// eGameState.SuddenDeath
+void function GameStateEnter_SuddenDeath()
+{
+ // disable respawns, suddendeath calling is done on a kill callback
+ SetRespawnsEnabled( false )
+}
+
+
+// eGameState.Postmatch
+void function GameStateEnter_Postmatch()
+{
+ foreach ( entity player in GetPlayerArray() )
+ {
+ player.FreezeControlsOnServer()
+ thread ForceFadeToBlack( player )
+ }
+
+ thread GameStateEnter_Postmatch_Threaded()
+}
+
+void function GameStateEnter_Postmatch_Threaded()
+{
+ wait GAME_POSTMATCH_LENGTH
+
+ GameRules_EndMatch()
+}
+
+void function ForceFadeToBlack( entity player )
+{
+ // todo: check if this is still necessary
+ player.EndSignal( "OnDestroy" )
+
+ // hack until i figure out what deathcam stuff is causing fadetoblacks to be cleared
+ while ( true )
+ {
+ WaitFrame()
+ ScreenFadeToBlackForever( player, 0.0 )
+ }
+}
+
+
+// shared across multiple gamestates
+
+void function OnPlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ if ( !GamePlayingOrSuddenDeath() )
+ return
+
+ // set round winning killreplay info here if we're tracking pilot kills
+ // todo: make this not count environmental deaths like falls, unsure how to prevent this
+ if ( file.roundWinningKillReplayTrackPilotKills && victim != attacker && attacker != svGlobal.worldspawn && IsValid( attacker ) )
+ {
+ file.roundWinningKillReplayTime = Time()
+ file.roundWinningKillReplayVictim = victim
+ file.roundWinningKillReplayAttacker = attacker
+ file.roundWinningKillReplayMethodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ file.roundWinningKillReplayTimeOfDeath = Time()
+ file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker )
+ }
+
+ // note: pilotstitans is just win if enemy team runs out of either pilots or titans
+ if ( IsPilotEliminationBased() || GetGameState() == eGameState.SuddenDeath )
+ {
+ if ( GetPlayerArrayOfTeam_Alive( victim.GetTeam() ).len() == 0 )
+ {
+ // for ffa we need to manually get the last team alive
+ if ( IsFFAGame() )
+ {
+ array<int> teamsWithLivingPlayers
+ foreach ( entity player in GetPlayerArray_Alive() )
+ {
+ if ( !teamsWithLivingPlayers.contains( player.GetTeam() ) )
+ teamsWithLivingPlayers.append( player.GetTeam() )
+ }
+
+ if ( teamsWithLivingPlayers.len() == 1 )
+ SetWinner( teamsWithLivingPlayers[ 0 ], "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" )
+ else if ( teamsWithLivingPlayers.len() == 0 ) // failsafe: only team was the dead one
+ SetWinner( TEAM_UNASSIGNED, "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" ) // this is fine in ffa
+ }
+ else
+ SetWinner( GetOtherTeam( victim.GetTeam() ), "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" )
+ }
+ }
+
+ if ( ( Riff_EliminationMode() == eEliminationMode.Titans || Riff_EliminationMode() == eEliminationMode.PilotsTitans ) && victim.IsTitan() ) // need an extra check for this
+ OnTitanKilled( victim, damageInfo )
+}
+
+void function OnTitanKilled( entity victim, var damageInfo )
+{
+ if ( !GamePlayingOrSuddenDeath() )
+ return
+
+ // set round winning killreplay info here if we're tracking titan kills
+ // todo: make this not count environmental deaths like falls, unsure how to prevent this
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( file.roundWinningKillReplayTrackTitanKills && victim != attacker && attacker != svGlobal.worldspawn && IsValid( attacker ) )
+ {
+ file.roundWinningKillReplayTime = Time()
+ file.roundWinningKillReplayVictim = victim
+ file.roundWinningKillReplayAttacker = attacker
+ file.roundWinningKillReplayMethodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ file.roundWinningKillReplayTimeOfDeath = Time()
+ file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker )
+ }
+
+ // note: pilotstitans is just win if enemy team runs out of either pilots or titans
+ if ( IsTitanEliminationBased() )
+ {
+ int livingTitans
+ foreach ( entity titan in GetTitanArrayOfTeam( victim.GetTeam() ) )
+ livingTitans++
+
+ if ( livingTitans == 0 )
+ {
+ // for ffa we need to manually get the last team alive
+ if ( IsFFAGame() )
+ {
+ array<int> teamsWithLivingTitans
+ foreach ( entity titan in GetTitanArray() )
+ {
+ if ( !teamsWithLivingTitans.contains( titan.GetTeam() ) )
+ teamsWithLivingTitans.append( titan.GetTeam() )
+ }
+
+ if ( teamsWithLivingTitans.len() == 1 )
+ SetWinner( teamsWithLivingTitans[ 0 ], "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" )
+ else if ( teamsWithLivingTitans.len() == 0 ) // failsafe: only team was the dead one
+ SetWinner( TEAM_UNASSIGNED, "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" ) // this is fine in ffa
+ }
+ else
+ SetWinner( GetOtherTeam( victim.GetTeam() ), "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" )
+ }
+ }
+}
+
+void function AddCallback_OnRoundEndCleanup( void functionref() callback )
+{
+ file.roundEndCleanupCallbacks.append( callback )
+}
+
+void function CleanUpEntitiesForRoundEnd()
+{
+ // this function should clean up any and all entities that need to be removed between rounds, ideally at a point where it isn't noticable to players
+ SetPlayerDeathsHidden( true ) // hide death sounds and such so people won't notice they're dying
+
+ foreach ( entity player in GetPlayerArray() )
+ {
+ ClearTitanAvailable( player )
+
+ if ( IsAlive( player ) )
+ player.Die()
+
+ if ( IsAlive( player.GetPetTitan() ) )
+ player.GetPetTitan().Destroy()
+ }
+
+ foreach ( entity npc in GetNPCArray() )
+ if ( IsAlive( npc ) )
+ npc.Die() // need this because getnpcarray includes the pettitans we just killed at this point
+
+ // allow other scripts to clean stuff up too
+ svGlobal.levelEnt.Signal( "CleanUpEntitiesForRoundEnd" )
+ foreach ( void functionref() callback in file.roundEndCleanupCallbacks )
+ callback()
+
+ SetPlayerDeathsHidden( false )
+}
+
+
+
+// stuff for gamemodes to call
+
+void function SetShouldUsePickLoadoutScreen( bool shouldUse )
+{
+ file.usePickLoadoutScreen = shouldUse
+}
+
+void function SetSwitchSidesBased( bool switchSides )
+{
+ file.switchSidesBased = switchSides
+}
+
+void function SetSuddenDeathBased( bool suddenDeathBased )
+{
+ file.suddenDeathBased = suddenDeathBased
+}
+
+void function SetShouldUseRoundWinningKillReplay( bool shouldUse )
+{
+ SetServerVar( "roundWinningKillReplayEnabled", shouldUse )
+}
+
+void function SetRoundWinningKillReplayKillClasses( bool pilot, bool titan )
+{
+ file.roundWinningKillReplayTrackPilotKills = pilot
+ file.roundWinningKillReplayTrackTitanKills = titan // player kills in titans should get tracked anyway, might be worth renaming this
+}
+
+void function SetRoundWinningKillReplayAttacker( entity attacker )
+{
+ file.roundWinningKillReplayTime = Time()
+ file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker )
+ file.roundWinningKillReplayAttacker = attacker
+}
+
+void function SetWinner( int team, string winningReason = "", string losingReason = "" )
+{
+ SetServerVar( "winningTeam", team )
+
+ if ( winningReason.len() == 0 )
+ file.announceRoundWinnerWinningSubstr = 0
+ else
+ file.announceRoundWinnerWinningSubstr = GetStringID( winningReason )
+
+ if ( losingReason.len() == 0 )
+ file.announceRoundWinnerLosingSubstr = 0
+ else
+ file.announceRoundWinnerLosingSubstr = GetStringID( losingReason )
+
+ if ( IsRoundBased() )
+ {
+ if ( team != TEAM_UNASSIGNED )
+ {
+ GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + 1 )
+ GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + 1 )
+ }
+
+ SetGameState( eGameState.WinnerDetermined )
+ }
+ else
+ SetGameState( eGameState.WinnerDetermined )
+}
+
+void function AddTeamScore( int team, int amount )
+{
+ GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + amount )
+ GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + amount )
+
+ int scoreLimit
+ if ( IsRoundBased() )
+ scoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE )
+ else
+ scoreLimit = GameMode_GetScoreLimit( GAMETYPE )
+
+ int score = GameRules_GetTeamScore( team )
+ if ( score >= scoreLimit || GetGameState() == eGameState.SuddenDeath )
+ SetWinner( team )
+ else if ( ( file.switchSidesBased && !file.hasSwitchedSides ) && score >= ( scoreLimit.tofloat() / 2.0 ) )
+ SetGameState( eGameState.SwitchingSides )
+}
+
+void function SetTimeoutWinnerDecisionFunc( int functionref() callback )
+{
+ file.timeoutWinnerDecisionFunc = callback
+}
+
+// idk
+
+float function GameState_GetTimeLimitOverride()
+{
+ return 100
+}
+
+bool function IsRoundBasedGameOver()
+{
+ return false
+}
+
+bool function ShouldRunEvac()
+{
+ return true
+}
+
+void function GiveTitanToPlayer( entity player )
+{
+
+}
+
+float function GetTimeLimit_ForGameMode()
+{
+ return 100.0
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_goblin_dropship.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_goblin_dropship.nut
new file mode 100644
index 00000000..fe36e668
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_goblin_dropship.nut
@@ -0,0 +1,784 @@
+untyped
+
+global function GoblinDropship_Init
+
+#if MP
+ global function GetZiplineDropshipSpawns
+#endif //MP
+global function RunDropshipDropoff
+global function DropshipFindDropNodes
+global function AnaylsisFuncDropshipFindDropNodes
+global function AddTurret
+global function SetDropTableSpawnFuncs
+
+const LINEGEN_DEBUG = 0
+global const bool FLIGHT_PATH_DEBUG = false
+const LINEGEN_TIME = 600.0
+
+const OPTIMAL_ZIPNODE_DIST_SQRD = 16384 //128 sqrd
+// 4096 64 sqrd
+// 65536 256 sqrd
+
+struct
+{
+ array<entity> ziplineDropshipSpawns
+
+ table < var, var > dropshipSound = {
+ [ TEAM_IMC ] = {
+ [ DROPSHIP_STRAFE ] = "Goblin_IMC_TroopDeploy_Flyin",
+ [ DROPSHIP_VERTICAL ] = "Goblin_Dropship_Flyer_Attack_Vertical_Succesful",
+ [ DROPSHIP_FLYER_ATTACK_ANIM_VERTICAL ] = "Goblin_Flyer_Dropshipattack_Vertical",
+ [ DROPSHIP_FLYER_ATTACK_ANIM ] = "Goblin_Flyer_Dropshipattack"
+ },
+ [ TEAM_MILITIA ] = {
+ [ DROPSHIP_STRAFE ] = "Crow_MCOR_TroopDeploy_Flyin",
+ [ DROPSHIP_VERTICAL ] = "Crow_Dropship_Flyer_Attack_Vertical_Succesful",
+ [ DROPSHIP_FLYER_ATTACK_ANIM_VERTICAL ] = "Crow_Flyer_Dropshipattack_Vertical",
+ [ DROPSHIP_FLYER_ATTACK_ANIM ] = "Crow_Flyer_Dropshipattack"
+ }
+ }
+
+} file
+
+function GoblinDropship_Init()
+{
+ RegisterSignal( "OnDropoff" )
+ RegisterSignal( "embark" )
+ RegisterSignal( "WarpedIn" )
+ PrecacheImpactEffectTable( "dropship_dust" )
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+}
+
+void function EntitiesDidLoad()
+{
+ //Generate a list of valid zipline dropship drop off points.
+ #if MP
+ BuildZiplineDropshipSpawnPoints()
+ #endif //MP
+}
+
+#if MP
+void function BuildZiplineDropshipSpawnPoints()
+{
+ array<entity> spawnPoints = SpawnPoints_GetDropPod()
+ file.ziplineDropshipSpawns = []
+
+ foreach ( entity spawnPoint in spawnPoints )
+ {
+ if ( !DropshipCanZiplineDropAtSpawnPoint( spawnPoint ) )
+ continue
+
+ file.ziplineDropshipSpawns.append( spawnPoint )
+ }
+
+ //Assert( file.dropshipSpawns.len() > 0, "No valid zipline dropship spawns exist in this map." )
+}
+
+//Function returns an array of level droppod spawns that have been pretested to ensure they have the space for zipline deployments.
+array<entity> function GetZiplineDropshipSpawns()
+{
+ return clone file.ziplineDropshipSpawns
+}
+#endif //MP
+
+bool function AnaylsisFuncDropshipFindDropNodes( FlightPath flightPath, vector origin, float yaw )
+{
+ return DropshipFindDropNodes( flightPath, origin, yaw, "both", false, IsLegalFlightPath ).len() != 0
+}
+
+// run from TryAnalysisAtOrigin
+table<string,table<string,NodeFP> > function DropshipFindDropNodes( FlightPath flightPath, vector origin, float yaw, string side = "both", ignoreCollision = false, bool functionref( FlightPath, vector, vector, vector, bool = 0 ) legalFlightFunc = null, bool amortize = false )
+{
+ // find nodes to deploy to
+ table<string,table<string,NodeFP> > foundNodes
+
+ vector angles = Vector( 0, yaw, 0 )
+ vector forward = AnglesToForward( angles )
+ vector right = AnglesToRight( angles )
+ Point start = GetWarpinPosition( flightPath.model, flightPath.anim, origin, angles )
+ if ( fabs( start.origin.x ) > MAX_WORLD_COORD )
+ return {}
+ if ( fabs( start.origin.y ) > MAX_WORLD_COORD )
+ return {}
+ if ( fabs( start.origin.z ) > MAX_WORLD_COORD )
+ return {}
+
+ if ( !ignoreCollision )
+ {
+ if ( !legalFlightFunc( flightPath, origin, forward, right, FLIGHT_PATH_DEBUG && !IsActiveNodeAnalysis() ) )
+ return {}
+ }
+
+ Point deployPoint = GetPreviewPoint( flightPath )
+ vector deployOrigin = GetOriginFromPoint( deployPoint, origin, forward, right )
+ vector deployAngles = GetAnglesFromPoint( deployPoint, angles )
+ float deployYaw = deployAngles.y
+
+ // flatten it
+ deployAngles.x = 0
+ deployAngles.z = 0
+
+ float pitch = 50
+ vector deployRightAngles = AnglesCompose( deployAngles, Vector( 0, -90, 0 ) )
+ deployRightAngles = AnglesCompose( deployRightAngles, Vector( pitch, 0, 0 ) )
+
+ vector deployLeftAngles = AnglesCompose( deployAngles, Vector( 0, 90, 0 ) )
+ deployLeftAngles = AnglesCompose( deployLeftAngles, Vector( pitch, 0, 0 ) )
+
+ table<int,NodeFP> nodeTable
+ bool foundRightNodes = false
+ bool foundLeftNodes = false
+
+ if ( side == "right" || side == "both" || side == "either" )
+ {
+ nodeTable = FindDropshipDeployNodes( deployOrigin, deployRightAngles, amortize )
+ if ( LINEGEN_DEBUG )
+ {
+ foreach( node in nodeTable )
+ DebugDrawLine( deployOrigin, node.origin, 200, 200, 200, true, 30.0 )
+ }
+
+ if ( nodeTable.len() )
+ {
+ if ( amortize )
+ WaitFrame()
+ foundRightNodes = FindBestDropshipNodesForSide( foundNodes, nodeTable, "right", flightPath, origin, forward, right, angles, deployOrigin, deployRightAngles, amortize )
+ }
+
+ if ( !foundRightNodes && side != "either" )
+ return {}
+
+ if ( amortize )
+ WaitFrame()
+ }
+
+ if ( side == "left" || side == "both" || side == "either" )
+ {
+ nodeTable = FindDropshipDeployNodes( deployOrigin, deployLeftAngles, amortize )
+ if ( nodeTable.len() )
+ {
+ if ( amortize )
+ WaitFrame()
+ foundLeftNodes = FindBestDropshipNodesForSide( foundNodes, nodeTable, "left", flightPath, origin, forward, right, angles, deployOrigin, deployLeftAngles, amortize )
+ }
+
+ if ( !foundLeftNodes && side != "either" )
+ return {}
+ }
+
+ if ( !foundRightNodes && !foundLeftNodes )
+ return {}
+
+ if ( LINEGEN_DEBUG || FLIGHT_PATH_DEBUG )
+ {
+ DrawArrow( origin, angles, 15.0, 250 )
+ float time = 500.0
+ foreach ( side, nodes in foundNodes )
+ {
+ //DebugDrawText( nodes.centerNode.origin + Vector(0,0,55), nodes.centerNode.fraction + "", true, time )
+ //DebugDrawText( nodes.centerNode.origin, "" + nodes.centerNode.dot, true, time )
+ DebugDrawLine( nodes.centerNode.origin, nodes.centerNode.attachOrigin, 120, 255, 120, true, time )
+ DebugDrawCircle( nodes.centerNode.origin, Vector( 0,0,0 ), 15, 120, 255, 120, true, time )
+
+ //DebugDrawText( nodes.leftNode.origin + Vector(0,0,55), nodes.leftNode.fraction + "", true, time )
+ //DebugDrawText( nodes.leftNode.origin, "" + nodes.leftNode.dot, true, time )
+ DebugDrawLine( nodes.leftNode.origin, nodes.leftNode.attachOrigin, 255, 120, 120, true, time )
+ DebugDrawCircle( nodes.leftNode.origin, Vector( 0,0,0 ), 15, 255, 120, 120, true, time )
+
+ //DebugDrawText( nodes.rightNode.origin + Vector(0,0,55), nodes.rightNode.fraction + "", true, time )
+ //DebugDrawText( nodes.rightNode.origin, "" + nodes.rightNode.dot, true, time )
+ DebugDrawLine( nodes.rightNode.origin, nodes.rightNode.attachOrigin, 120, 120, 255, true, time )
+ DebugDrawCircle( nodes.rightNode.origin, Vector( 0,0,0 ), 15, 120, 120, 255, true, time )
+
+ //DebugDrawLine( nodes.rightNode.origin, nodes.centerNode.origin, 200, 200, 200, true, time )
+ //DebugDrawText( nodes.rightNode.origin + Vector(0,0,20), "dist: " + Distance( nodes.rightNode.origin, nodes.centerNode.origin ), true, time )
+ //DebugDrawLine( nodes.leftNode.origin, nodes.centerNode.origin, 200, 200, 200, true, time )
+ //DebugDrawText( nodes.leftNode.origin + Vector(0,0,20), "dist: " + Distance( nodes.leftNode.origin, nodes.centerNode.origin ), true, time )
+
+ //DebugDrawLine( origin, origin + deployForward * 200, 50, 255, 50, true, time )
+
+ // foreach ( node in nodes.rightNodes )
+ // {
+ // DebugDrawText( node.origin + Vector(0,0,25), "R", true, 15 )
+ // }
+ //
+ // foreach ( node in nodes.leftNodes )
+ // {
+ // DebugDrawText( node.origin + Vector(0,0,25), "L", true, 15 )
+ // }
+ }
+
+// IsLegalFlightPath( flightPath, origin, forward, right, true )
+ }
+
+ return foundNodes
+}
+
+
+table<int,NodeFP> function FindDropshipDeployNodes( vector deployOrigin, vector deployAngles, bool amortize = false )
+{
+ vector deployForward = AnglesToForward( deployAngles )
+
+ vector end = deployOrigin + deployForward * 3000
+ TraceResults result = TraceLine( deployOrigin, end, null, TRACE_MASK_NPCWORLDSTATIC )
+
+ if ( LINEGEN_DEBUG )
+ {
+ DebugDrawLine( deployOrigin, result.endPos, 255, 255, 255, true, LINEGEN_TIME )
+ DebugDrawText( result.endPos + Vector( 0,0,10 ), "test", true, LINEGEN_TIME )
+ DebugDrawCircle( result.endPos, Vector( 0,0,0 ), 35, 255, 255, 255, true, LINEGEN_TIME )
+ }
+ // no hit?
+ if ( result.fraction >= 1.0 )
+ return {}
+
+ int node = GetNearestNodeToPos( result.endPos )
+ if ( node == -1 )
+ return {}
+
+ if ( LINEGEN_DEBUG )
+ {
+ DebugDrawText( GetNodePos( node ) + Vector(0,0,10), "nearest node", true, 15.0 )
+ DebugDrawCircle( GetNodePos( node ), Vector( 0,0,0 ), 20, 60, 60, 255, true, LINEGEN_TIME )
+ }
+
+ array<vector> neighborPositions = NavMesh_GetNeighborPositions( GetNodePos( node ), HULL_HUMAN, 20 )
+
+ if ( amortize )
+ WaitFrame()
+
+ table<int,NodeFP> nodeTable = {}
+ int uniqueID = -2
+ foreach ( pos in neighborPositions )
+ {
+ NodeFP attachPoint
+ attachPoint.origin = pos
+ attachPoint.uniqueID = uniqueID
+
+ nodeTable[ uniqueID ] <- attachPoint
+ uniqueID--
+ }
+
+ return nodeTable
+}
+
+void function AddDirectionVec( array<NodeFP> nodeArray, vector origin )
+{
+ // different direction vecs because we want a node to the left, center, and straight
+ foreach ( node, tab in nodeArray )
+ {
+ vector vec
+ vec = tab.origin - origin
+ vec.Norm()
+ tab.vec = vec
+ }
+}
+
+void function AddDirectionVecFromDir( array<NodeFP> nodeArray, vector origin, vector dir )
+{
+ // different direction vecs because we want a node to the left, center, and straight
+ foreach ( node, tab in nodeArray )
+ {
+ vector vec
+ vec = ( tab.origin + dir * 50 ) - origin
+ vec.Norm()
+ tab.vec = vec
+ }
+}
+
+bool function FindBestDropshipNodesForSide( table<string,table<string,NodeFP> > foundNodes, table<int,NodeFP> nodeTable, string side, FlightPath flightPath, vector origin, vector forward, vector right, vector angles, vector deployOrigin, vector deployAngles, bool amortize )
+{
+ vector deployForward = AnglesToForward( deployAngles )
+ vector deployRight = AnglesToRight( deployAngles )
+
+ float RatioForLeftRight = 0.2
+ vector RightDeployForward = ( ( deployForward * ( 1.0 - RatioForLeftRight ) ) + ( deployRight * RatioForLeftRight * -1 ) )
+ RightDeployForward.Norm()
+ vector LeftDeployForward = ( ( deployForward * ( 1.0 - RatioForLeftRight ) ) + ( deployRight * RatioForLeftRight ) )
+ LeftDeployForward.Norm()
+
+ if ( amortize )
+ WaitFrame()
+
+ foundNodes[ side ] <- {}
+ array<AttachPoint> attachPoints = GetAttachPoints( flightPath, side )
+
+ array<NodeFP> centerNodes = GetNodeArrayFromTable( nodeTable )
+ AddDirectionVec( centerNodes, deployOrigin )
+ NodeFP centerNode = GetBestDropshipNode( attachPoints[2], centerNodes, origin, deployForward, forward, right, angles, NullNodeFP )
+ if ( centerNode == NullNodeFP )
+ return false
+ delete nodeTable[ centerNode.uniqueID ]
+
+ if ( amortize )
+ WaitFrame()
+
+ array<NodeFP> leftNodes = GetCulledNodes( nodeTable, deployRight * -1 )
+ AddDirectionVecFromDir( leftNodes, deployOrigin, deployRight * -1 )
+ NodeFP leftNode = GetBestDropshipNode( attachPoints[1], leftNodes, origin, RightDeployForward, forward, right, angles, centerNode )
+ if ( leftNode == NullNodeFP )
+ return false
+ delete nodeTable[ leftNode.uniqueID ]
+
+ if ( amortize )
+ WaitFrame()
+
+ array<NodeFP> rightNodes = GetCulledNodes( nodeTable, deployRight )
+ AddDirectionVecFromDir( rightNodes, deployOrigin, deployRight )
+ NodeFP rightNode = GetBestDropshipNode( attachPoints[0], rightNodes, origin, LeftDeployForward, forward, right, angles, centerNode )
+ if ( rightNode == NullNodeFP )
+ return false
+
+ table<string,NodeFP> Table
+ Table.centerNode <- centerNode
+ Table.leftNode <- leftNode
+ Table.rightNode <- rightNode
+
+ //Table.rightNodes <- rightNodes // for debug
+ //Table.leftNodes <- leftNodes // for debug
+
+ foundNodes[ side ] = Table
+ return true
+}
+
+array<NodeFP> function GetNodeArrayFromTable( table<int,NodeFP> nodeTable )
+{
+ array<NodeFP> Array
+ foreach ( Table in nodeTable )
+ {
+ Array.append( Table )
+ }
+
+ return Array
+}
+
+array<NodeFP> function GetCulledNodes( table<int,NodeFP> nodeTable, vector right )
+{
+ table<int,NodeFP> leftNodes
+ // get the nodes on the left
+ foreach ( nod, tab in nodeTable )
+ {
+ float dot = DotProduct( tab.vec, right )
+ if ( dot >= 0.0 )
+ {
+ leftNodes[ nod ] <- tab
+ }
+ }
+
+ return GetNodeArrayFromTable( leftNodes )
+}
+
+NodeFP function GetBestDropshipNode( AttachPoint attachPoint, array<NodeFP> nodeArray, vector origin, vector deployForward, vector forward, vector right, vector angles, NodeFP centerNode, bool showdebug = false )
+{
+ foreach ( node in nodeArray )
+ {
+ node.dot = DotProduct( node.vec, deployForward )
+ if ( showdebug )
+ {
+ DebugDrawText( node.origin, "dot: " + node.dot, true, 15.0 )
+ int green = 0
+ int red = 255
+ if ( node.dot > 0.9 )
+ {
+ float frac = ( 1.0 - node.dot ) / 0.1
+ frac = 1.0 - frac
+
+ green = int( frac * 255 )
+ red -= green
+ }
+
+ DebugDrawLine( node.origin, node.origin + ( node.vec * -1000 ), red, green, 0, true, 15.0 )
+ DebugDrawCircle( node.origin, Vector( 0,0,0 ), 25, red, green, 0, true, 15.0 )
+ }
+ }
+
+ if ( !nodeArray.len() )
+ return NullNodeFP
+
+ vector attachOrigin = GetOriginFromAttachPoint( attachPoint, origin, forward, right )
+ vector attachAngles = GetAnglesFromAttachPoint( attachPoint, angles )
+ vector attachForward = AnglesToForward( attachAngles )
+ vector attachRight = AnglesToRight( attachAngles )
+
+ FlightPath offsetAnalysis = GetAnalysisForModel( TEAM_IMC_GRUNT_MODEL, ZIPLINE_IDLE_ANIM )
+ Point offsetPoint = GetPreviewPoint( offsetAnalysis )
+ vector offsetOrigin = GetOriginFromPoint( offsetPoint, attachOrigin, attachForward, attachRight )
+// DebugDrawLine( offsetOrigin, attachOrigin, 255, 255, 0, true, 15 )
+
+ nodeArray.sort( SortHighestDot )
+
+ vector mins = GetBoundsMin( HULL_HUMAN )
+ vector maxs = GetBoundsMax( HULL_HUMAN )
+
+ array<NodeFP> passedNodes
+
+ for ( int i = 0; i < nodeArray.len(); i++ )
+ {
+ NodeFP node = nodeArray[i]
+
+ // beyond the allowed dot
+ if ( node.dot < 0.3 )
+ return NullNodeFP
+
+ // trace to see if the ai could drop to the node from here
+ TraceResults result = TraceHull( offsetOrigin, node.origin, mins, maxs, null, TRACE_MASK_NPCWORLDSTATIC, TRACE_COLLISION_GROUP_NONE )
+ if ( result.fraction < 1.0 )
+ continue //return
+
+ // trace to insure that there will be a good place to hook the zipline
+ if ( !GetHookOriginFromNode( offsetOrigin, node.origin, attachOrigin ) )
+ continue
+
+ node.fraction = result.fraction
+ node.attachOrigin = offsetOrigin
+ node.attachName = attachPoint.name
+
+ if ( centerNode != NullNodeFP )
+ {
+ //test for distance, not too close, not too far
+ local distSqr = DistanceSqr( centerNode.origin, node.origin )
+ node.rating = fabs( OPTIMAL_ZIPNODE_DIST_SQRD - distSqr )
+ passedNodes.append( node )
+ continue
+ }
+
+ return node
+ }
+
+ if ( centerNode != NullNodeFP && passedNodes.len() )
+ {
+ passedNodes.sort( SortLowestRating )
+ return passedNodes[ 0 ]
+ }
+
+ return NullNodeFP
+}
+
+int function SortHighestDot( NodeFP a, NodeFP b )
+{
+ if ( a.dot > b.dot )
+ return -1
+
+ if ( a.dot < b.dot )
+ return 1
+
+ return 0
+}
+
+int function SortLowestRating( NodeFP a, NodeFP b )
+{
+ if ( a.rating > b.rating )
+ return 1
+
+ if ( a.rating < b.rating )
+ return -1
+
+ return 0
+}
+
+void function SetDropTableSpawnFuncs( CallinData drop, entity functionref( int, vector, vector ) spawnFunc, int count )
+{
+ array<entity functionref( int, vector, vector )> spawnFuncArray
+ //spawnFuncArray.resize( count, spawnFunc )
+ for ( int i = 0; i < count; i++ )
+ {
+ spawnFuncArray.append( spawnFunc )
+ }
+ drop.npcSpawnFuncs = spawnFuncArray
+}
+
+asset function GetTeamDropshipModel( int team, bool hero = false )
+{
+ if ( hero )
+ {
+ if ( team == TEAM_IMC )
+ return GetFlightPathModel( "fp_dropship_hero_model" )
+ else
+ return GetFlightPathModel( "fp_crow_hero_model" )
+ }
+ else
+ {
+ if ( team == TEAM_IMC )
+ return GetFlightPathModel( "fp_dropship_model" )
+ else
+ return GetFlightPathModel( "fp_crow_model" )
+ }
+
+ unreachable
+}
+
+//This function tests to see if the given spawn point has enough clearance for a dropship to deploy zipline grunts.
+bool function DropshipCanZiplineDropAtSpawnPoint( entity spawnPoint )
+{
+ CallinData drop
+ drop.origin = spawnPoint.GetOrigin()
+ drop.yaw = spawnPoint.GetAngles().y
+ drop.dist = 768
+ SetCallinStyle( drop, eDropStyle.ZIPLINE_NPC )
+ int style = drop.style
+
+ bool validSpawn = false
+ array<string> anims = GetRandomDropshipDropoffAnims()
+
+ string animation
+ FlightPath flightPath
+
+ foreach ( anim in anims )
+ {
+ animation = anim
+ flightPath = GetAnalysisForModel( DROPSHIP_MODEL, anim )
+
+ if ( style == eDropStyle.NONE )
+ {
+ if ( !drop.yawSet )
+ {
+ style = eDropStyle.NEAREST
+ }
+ else
+ {
+ style = eDropStyle.NEAREST_YAW
+ }
+ }
+
+ validSpawn = TestSpawnPointForStyle( flightPath, drop )
+
+ if ( validSpawn )
+ return true
+ }
+
+ return false
+}
+
+function RunDropshipDropoff( CallinData Table )
+{
+ vector origin = Table.origin
+ float yaw = Table.yaw
+ int team = Table.team
+ entity owner = Table.owner
+ string squadname = Table.squadname
+ string side = Table.side
+ array<entity functionref( int, vector, vector )> npcSpawnFuncs = Table.npcSpawnFuncs
+ int style = Table.style
+ int health = 7800
+
+ if ( Table.dropshipHealth != 0 )
+ health = Table.dropshipHealth
+ Table.success = false
+
+ if ( Flag( "DisableDropships" ) )
+ return
+
+ if ( team == 0 )
+ {
+ if ( owner )
+ team = owner.GetTeam()
+ else
+ team = 0
+ }
+
+ SpawnPointFP spawnPoint
+ array<string> anims = GetRandomDropshipDropoffAnims()
+
+ // Override anim, level scripter takes responsibility for it working in this location or not
+ if ( Table.anim != "" )
+ {
+ anims.clear()
+ anims.append( Table.anim )
+ }
+
+ string animation
+ FlightPath flightPath
+ bool wasPlayerOwned = IsValid( owner ) && IsValidPlayer( owner )
+
+ foreach ( anim in anims )
+ {
+ animation = anim
+ flightPath = GetAnalysisForModel( DROPSHIP_MODEL, anim )
+
+ if ( style == eDropStyle.NONE )
+ {
+ if ( !Table.yawSet )
+ {
+ style = eDropStyle.NEAREST
+ }
+ else
+ {
+ style = eDropStyle.NEAREST_YAW
+ }
+ }
+
+ spawnPoint = GetSpawnPointForStyle( flightPath, Table )
+
+ if ( spawnPoint.valid )
+ break
+ }
+
+ if ( !spawnPoint.valid )
+ {
+ printt( "Couldn't find good spawn location for dropship" )
+ return
+ }
+
+ Table.success = true
+
+ entity ref = CreateScriptRef()
+ if ( Table.forcedPosition )
+ {
+ ref.SetOrigin( Table.origin )
+ ref.SetAngles( Vector( 0, Table.yaw, 0 ) )
+ }
+ else
+ {
+ ref.SetOrigin( spawnPoint.origin )
+ ref.SetAngles( spawnPoint.angles )
+ }
+
+ // used for when flyers attack dropships
+ if ( "nextDropshipAttackedByFlyers" in level && level.nextDropshipAttackedByFlyers )
+ animation = FlyersAttackDropship( ref, animation )
+
+ Assert( IsNewThread(), "Must be threaded off" )
+
+ DropTable dropTable
+
+ if ( Table.dropTable.valid )
+ {
+ dropTable = Table.dropTable
+ }
+ else
+ {
+ bool ignoreCollision = true // = style == eDropStyle.FORCED
+ thread FindDropshipZiplineNodes( dropTable, flightPath, ref.GetOrigin(), ref.GetAngles(), side, ignoreCollision, true )
+ }
+
+ asset model = GetTeamDropshipModel( team )
+ waitthread WarpinEffect( model, animation, ref.GetOrigin(), ref.GetAngles() )
+ entity dropship = CreateDropship( team, ref.GetOrigin(), ref.GetAngles() )
+ SetSpawnOption_SquadName( dropship, squadname )
+ dropship.kv.solid = SOLID_VPHYSICS
+ DispatchSpawn( dropship )
+ Table.dropship = dropship
+ //dropship.SetPusher( true )
+ dropship.SetHealth( health )
+ dropship.SetMaxHealth( health )
+ Table.dropship = dropship
+ dropship.EndSignal( "OnDeath" )
+ dropship.Signal( "WarpedIn" )
+ ref.Signal( "WarpedIn" )
+ Signal( Table, "WarpedIn" )
+
+ AddDropshipDropTable( dropship, dropTable ) // this is where the ai will drop to
+
+ if ( IsValid( owner ) )
+ {
+ dropship.SetCanCloak( false )
+ dropship.SetOwner( owner )
+ if ( owner.IsPlayer() )
+ dropship.SetBossPlayer( owner )
+ }
+
+ local dropshipSound = GetTeamDropshipSound( team, animation )
+ if ( Table.customSnd != "" )
+ dropshipSound = Table.customSnd
+
+ OnThreadEnd(
+ function() : ( dropship, ref, Table, dropshipSound )
+ {
+ ref.Destroy()
+ if ( IsValid( dropship ) )
+ StopSoundOnEntity( dropship, dropshipSound )
+ if ( IsAlive( dropship ) )
+ {
+ dropship.Destroy()
+ }
+
+ Signal( Table, "OnDropoff", { guys = null } )
+ }
+ )
+
+ array<entity> guys
+ if ( !wasPlayerOwned || IsValidPlayer( owner ) )
+ {
+ guys = CreateNPCSForDropship( dropship, Table.npcSpawnFuncs, side )
+
+ foreach ( guy in guys )
+ {
+ if ( IsAlive( guy ) )
+ {
+ if ( IsValidPlayer( owner ) )
+ {
+ NPCFollowsPlayer( guy, owner )
+ }
+ }
+ }
+ }
+
+ //thread DropshipMissiles( dropship )
+ dropship.Hide()
+ EmitSoundOnEntity( dropship, dropshipSound ) //HACK: Note that the anims can play sounds too! For R3 just make it consistent so it's all played in script or all played in anims
+ thread ShowDropship( dropship )
+ thread PlayAnimTeleport( dropship, animation, ref, 0 )
+
+ ArrayRemoveDead( guys )
+
+ Signal( Table, "OnDropoff", { guys = guys } )
+
+ WaittillAnimDone( dropship )
+ wait 2.0
+}
+
+void function FindDropshipZiplineNodes( DropTable dropTable, FlightPath flightPath, vector origin, vector angles, string side = "both", bool ignoreCollision = false, bool amortize = false )
+{
+ dropTable.nodes = DropshipFindDropNodes( flightPath, origin, angles.y, side, ignoreCollision, IsLegalFlightPath_OverTime, amortize )
+ dropTable.valid = true
+}
+
+function ShowDropship( dropship )
+{
+ dropship.EndSignal( "OnDestroy" )
+ wait 0.16
+ dropship.Show()
+}
+
+entity function AddTurret( entity dropship, int team, string turretWeapon, string attachment, int health = 700 )
+{
+ entity turret = CreateEntity( "npc_turret_sentry" )
+ turret.kv.TurretRange = 1500
+ turret.kv.AccuracyMultiplier = 1.0
+ turret.kv.FieldOfView = 0.4
+ turret.kv.FieldOfViewAlert = 0.4
+ SetSpawnOption_Weapon( turret, turretWeapon )
+ turret.SetOrigin( Vector(0,0,0) )
+ turret.SetTitle( "#NPC_DROPSHIP" )
+ turret.s.skipTurretFX <- true
+ DispatchSpawn( turret )
+
+ SetTargetName( turret, "DropshipTurret" )
+ turret.SetHealth( health)
+ turret.SetMaxHealth( health )
+ turret.Hide()
+ //turret.Show()
+ entity weapon = turret.GetActiveWeapon()
+ weapon.Hide()
+ SetTeam( turret, team )
+ turret.SetParent( dropship, attachment, false )
+ turret.EnableTurret()
+ turret.SetOwner( dropship.GetOwner() )
+ turret.SetAimAssistAllowed( false )
+
+ entity bossPlayer = dropship.GetBossPlayer()
+ if ( IsValidPlayer( bossPlayer ) )
+ turret.SetBossPlayer( dropship.GetBossPlayer() )
+
+ HideName( turret )
+ return turret
+}
+
+function GetTeamDropshipSound( team, animation )
+{
+ Assert( team in file.dropshipSound )
+ Assert( animation in file.dropshipSound[ team ] )
+
+ return file.dropshipSound[ team ][ animation ]
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_lasermesh.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_lasermesh.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_lasermesh.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_loadout_crate.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_loadout_crate.nut
new file mode 100644
index 00000000..d987c774
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_loadout_crate.nut
@@ -0,0 +1,183 @@
+untyped
+
+global function LoadoutCrate_Init
+
+const LOADOUT_CRATE_MODEL = $"models/containers/pelican_case_ammobox.mdl"
+global function AddLoadoutCrate
+global function DestroyAllLoadoutCrates
+
+function LoadoutCrate_Init()
+{
+ level.loadoutCrateManagedEntArrayID <- CreateScriptManagedEntArray()
+ PrecacheModel( LOADOUT_CRATE_MODEL )
+
+ AddSpawnCallback( "prop_dynamic", LoadoutCreatePrePlaced )
+}
+
+function AddLoadoutCrate( team, vector origin, vector angles, bool showOnMinimap = true, entity crate = null )
+{
+ expect int( team )
+
+ local crateCount = GetScriptManagedEntArray( level.loadoutCrateManagedEntArrayID ).len()
+ Assert( crateCount < MAX_LOADOUT_CRATE_COUNT, "Can't have more than " + MAX_LOADOUT_CRATE_COUNT + " Loadout Crates" )
+
+ angles += Vector( 0, -90, 0 )
+
+ if ( !IsValid( crate ) )
+ {
+ crate = CreatePropScript( LOADOUT_CRATE_MODEL, origin, angles, 6 )
+ SetTargetName( crate, "loadoutCrate" )
+ }
+
+ SetTeam( crate, team )
+ crate.SetUsable()
+ if ( team == TEAM_MILITIA || team == TEAM_IMC )
+ crate.SetUsableByGroup( "friendlies pilot" )
+ else
+ crate.SetUsableByGroup( "pilot" )
+
+ crate.SetUsePrompts( "#LOADOUT_CRATE_HOLD_USE", "#LOADOUT_CRATE_PRESS_USE" )
+ crate.SetForceVisibleInPhaseShift( true )
+
+ if ( showOnMinimap )
+ {
+ #if R1_VGUI_MINIMAP
+ crate.Minimap_SetDefaultMaterial( $"vgui/hud/coop/coop_ammo_locker_icon" )
+ #endif
+ crate.Minimap_SetObjectScale( MINIMAP_LOADOUT_CRATE_SCALE )
+ crate.Minimap_SetAlignUpright( true )
+ crate.Minimap_AlwaysShow( TEAM_IMC, null )
+ crate.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ crate.Minimap_SetHeightTracking( true )
+ crate.Minimap_SetZOrder( MINIMAP_Z_OBJECT )
+ crate.Minimap_SetCustomState( eMinimapObject_prop_script.FD_LOADOUT_CHEST )
+ }
+
+ AddToScriptManagedEntArray( level.loadoutCrateManagedEntArrayID, crate )
+
+ //thread LoadoutCrateMarkerThink( "LoadoutCrateMarker" + string( crateCount ), crate )
+ thread LoadoutCrateThink( crate )
+ thread LoadoutCrateRestockAmmoThink( crate )
+
+ Highlight_SetNeutralHighlight( crate, "interact_object_los" )
+}
+
+void function LoadoutCreatePrePlaced( entity ent )
+{
+ if ( ent.GetTargetName().find( "loot_crate" ) == 0 )
+ {
+ ent.Destroy()
+ return
+ }
+
+ if ( ent.GetTargetName().find( "loadout_crate" ) != 0 )
+ return
+
+ if ( IsSingleplayer() )
+ return
+
+ vector angles = ent.GetAngles() + Vector( 0, 90, 0 )
+ AddLoadoutCrate( TEAM_BOTH, ent.GetOrigin(), angles, false, ent )
+}
+
+function LoadoutCrateMarkerThink( marker, crate )
+{
+ crate.EndSignal( "OnDestroy" )
+ crate.EndSignal( "OnDeath" )
+
+ OnThreadEnd(
+ function() : ( marker )
+ {
+ ClearMarker( marker )
+ }
+ )
+
+ while ( 1 )
+ {
+ if ( GetGameState() <= eGameState.Prematch )
+ ClearMarker( marker )
+ else
+ SetMarker( marker, crate )
+
+ svGlobal.levelEnt.WaitSignal( "GameStateChanged" )
+ }
+}
+
+function LoadoutCrateThink( crate )
+{
+ crate.EndSignal( "OnDestroy" )
+ while ( true )
+ {
+ var player = crate.WaitSignal( "OnPlayerUse" ).player
+
+ if ( player.IsPlayer() )
+ {
+ thread UsingLoadoutCrate( crate, player )
+ wait 1 // debounce on using the crate to minimize the risk of using it twice before the menu opens.
+ }
+ }
+}
+
+function LoadoutCrateRestockAmmoThink( crate )
+{
+ crate.EndSignal( "OnDestroy" )
+ local distSqr
+ local crateOrigin = crate.GetOrigin()
+ local triggerDistSqr = 96 * 96
+ local resetDistSqr = 384 * 384
+
+ while ( true )
+ {
+ wait 1 // check every second
+ array<entity> playerArray = GetPlayerArray_Alive()
+ foreach( player in playerArray )
+ {
+ if ( player.IsTitan() )
+ continue
+
+ if ( player.ContextAction_IsBusy() )
+ continue
+
+ distSqr = DistanceSqr( crateOrigin, player.GetOrigin() )
+ if ( distSqr <= triggerDistSqr && player.s.restockAmmoTime < Time() )
+ {
+ if ( TraceLineSimple( player.EyePosition(), crate.GetOrigin() + Vector( 0.0, 0.0, 24.0 ), crate ) == 1.0 )
+ {
+ player.s.restockAmmoCrate = crate
+ player.s.restockAmmoTime = Time() + 10 // debounce time before you can get new ammo again if you stay next to the crate.
+ //MessageToPlayer( player, eEventNotifications.CoopAmmoRefilled, null, null )
+ RestockPlayerAmmo( player )
+ }
+ }
+
+ if ( distSqr > resetDistSqr && player.s.restockAmmoTime > 0 && player.s.restockAmmoCrate == crate )
+ {
+ player.s.restockAmmoCrate = null
+ player.s.restockAmmoTime = 0
+ }
+ }
+ }
+}
+
+function UsingLoadoutCrate( crate, player )
+{
+ expect entity( player )
+
+ player.p.usingLoadoutCrate = true
+ player.s.usedLoadoutCrate = true
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Coop_AmmoBox_Open" )
+ Remote_CallFunction_UI( player, "ServerCallback_OpenPilotLoadoutMenu" )
+}
+
+// should be called if we enter an epilogue ... maybe?
+function DestroyAllLoadoutCrates()
+{
+ local crateArray = GetScriptManagedEntArray( level.loadoutCrateManagedEntArrayID )
+ foreach( crate in crateArray )
+ crate.Destroy()
+
+ //dissolve didn't work
+ //Dissolve( ENTITY_DISSOLVE_CHAR, Vector( 0, 0, 0 ), 0 )
+ //ENTITY_DISSOLVE_CORE
+ //ENTITY_DISSOLVE_NORMAL
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_mp_mapspawn.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_mp_mapspawn.gnut
new file mode 100644
index 00000000..6860d817
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_mp_mapspawn.gnut
@@ -0,0 +1,65 @@
+//todo change this to be map-based
+global function SPMP_MapSpawn_Init
+global struct SvSpawnGlobals
+{
+ array<entity> allNormalSpawnpoints
+}
+
+global SvSpawnGlobals svSpawnGlobals
+
+void function SPMP_MapSpawn_Init()
+{
+ printl( "Code Script: _mp_mapspawn" )
+
+ svGlobal.npcsSpawnedThisFrame_scriptManagedArray[ TEAM_IMC ] <- CreateScriptManagedEntArray()
+ svGlobal.npcsSpawnedThisFrame_scriptManagedArray[ TEAM_MILITIA ] <- CreateScriptManagedEntArray()
+
+ level.spawnActions <- {} // these run after all initial spawn functions have run
+ svGlobal.levelEnt = CreateEntity( "info_landmark" )
+ SetTargetName( svGlobal.levelEnt, "Level Ent" )
+ DispatchSpawn( svGlobal.levelEnt )
+ level.isTestmap <- false
+
+ FlagInit( "EntitiesDidLoad" )
+ FlagInit( "PlayerDidSpawn" )
+
+ level.privateMatchForcedEnd <- null
+ level.defenseTeam <- TEAM_IMC
+
+ level.onRodeoStartedCallbacks <- [] // runs when a player starts rodeoing a titan
+ level.onRodeoEndedCallbacks <- [] // runs when a player stops rodeoing a titan
+
+ FlagInit( "FireteamAutoSpawn" )
+ FlagInit( "DebugFoundEnemy" )
+ FlagInit( "OldAnimRefStyle" )
+ FlagInit( "EarlyCatch" )
+ FlagInit( "ForceStartSpawn" )
+ FlagInit( "IgnoreStartSpawn" )
+ FlagInit( "ReadyToStartMatch" ) // past waiting for players, in prematch
+
+ RegisterSignal( "OnChangedPlayerClass" )
+ RegisterSignal( "Disconnected" )
+ RegisterSignal( "_disconnectedInternal" )
+ RegisterSignal( "TeamChange" )
+ RegisterSignal( "LeftClass" )
+ RegisterSignal( "forever" )
+ RegisterSignal( "waitOver" )
+ RegisterSignal( "HitSky" )
+
+ AddSpawnCallback( "trigger_hurt", InitDamageTriggers )
+
+ AddSpawnCallbackEditorClass( "func_brush", "func_brush_navmesh_separator", NavmeshSeparatorThink )
+
+ //AddCallback_EntitiesDidLoad( ActivateSkyBox )
+
+ AddSpawnCallback( "player", MP_PlayerPostInit )
+
+ // unsure if this should be done here, but it's required for mp to load
+ PrecacheModel( $"models/menu/default_environment.mdl" )
+
+ //if ( IsMultiplayer() && GetClassicMPMode() && !IsLobby() )
+ // ClassicMP_TryDefaultIntroSetup()
+
+ //InitDefaultLoadouts()
+ SPMP_Shared_Init()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_music.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_music.gnut
new file mode 100644
index 00000000..44320336
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_music.gnut
@@ -0,0 +1,107 @@
+global function Music_Init
+global function CreateTeamMusicEvent
+global function PlayCurrentTeamMusicEventsOnPlayer
+global function CreateLevelIntroMusicEvent
+global function PlayMusicToCompletion
+global function PlayMusicToAll
+global function CreateLevelWinnerDeterminedMusicEvent
+
+const int MUSIC_EVENT_UNINITIALIZED = -1
+
+
+struct MusicEvent
+{
+ int musicPieceID = MUSIC_EVENT_UNINITIALIZED
+ float timeMusicStarted
+ bool shouldSeek
+}
+
+struct
+{
+ table< int, MusicEvent > musicEvents
+} file
+
+
+void function Music_Init()
+{
+ MusicEvent imcMusicEvent
+ MusicEvent militiaMusicEvent
+ file.musicEvents[ TEAM_IMC ] <- imcMusicEvent
+ file.musicEvents[ TEAM_MILITIA ] <- militiaMusicEvent
+
+ AddCallback_GameStateEnter( eGameState.Prematch, CreateLevelIntroMusicEvent )
+}
+
+void function CreateTeamMusicEvent( int team, int musicPieceID, float timeMusicStarted, bool shouldSeek = true )
+{
+ Assert( !( shouldSeek == false && timeMusicStarted > 0 ), "Don't pass in timeMusicStarted when creating a TeamMusicEvent with shouldSeek set to false!" )
+
+ MusicEvent musicEvent
+ musicEvent.musicPieceID = musicPieceID
+ musicEvent.timeMusicStarted = timeMusicStarted
+ musicEvent.shouldSeek = shouldSeek
+
+ file.musicEvents[ team ] = musicEvent
+}
+
+void function PlayCurrentTeamMusicEventsOnPlayer( entity player )
+{
+ int team = player.GetTeam()
+ MusicEvent musicEvent
+
+ if ( team in file.musicEvents )
+ musicEvent = file.musicEvents[ team ]
+ else
+ musicEvent = file.musicEvents[ TEAM_MILITIA ] //This normally means we're in FFA. Fine to failsafe to use any music event
+
+ if ( musicEvent.musicPieceID == MUSIC_EVENT_UNINITIALIZED ) //No current music event
+ return
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlayTeamMusicEvent", musicEvent.musicPieceID, musicEvent.timeMusicStarted, musicEvent.shouldSeek )
+}
+
+void function CreateLevelIntroMusicEvent()
+{
+ //printt( "Creating LevelIntroMusicEvent" )
+ CreateTeamMusicEvent( TEAM_IMC, eMusicPieceID.LEVEL_INTRO, Time() )
+ CreateTeamMusicEvent( TEAM_MILITIA, eMusicPieceID.LEVEL_INTRO, Time() )
+}
+
+void function PlayMusicToCompletion( int musicID )
+{
+ array<entity> players = GetPlayerArray()
+ foreach ( entity player in players )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlayMusicToCompletion", musicID )
+ }
+}
+
+void function PlayMusicToAll( int musicID )
+{
+ array<entity> players = GetPlayerArray()
+ foreach ( entity player in players )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlayMusic", musicID )
+ }
+}
+
+void function CreateLevelWinnerDeterminedMusicEvent()
+{
+ //printt( "Creating CreateLevelWinnerDeterminedMusicEvent" )
+ if ( IsFFAGame() )
+ return
+
+ int winningTeam = GetWinningTeam()
+
+ if ( winningTeam )
+ {
+ int losingTeam = GetOtherTeam( winningTeam )
+ CreateTeamMusicEvent( winningTeam, eMusicPieceID.LEVEL_WIN, Time() )
+ CreateTeamMusicEvent( losingTeam, eMusicPieceID.LEVEL_LOSS, Time() )
+ }
+ else
+ {
+ CreateTeamMusicEvent( TEAM_MILITIA, eMusicPieceID.LEVEL_DRAW, Time() )
+ CreateTeamMusicEvent( TEAM_IMC, eMusicPieceID.LEVEL_DRAW, Time() )
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_pickups.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_pickups.gnut
new file mode 100644
index 00000000..ecf9b3e5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_pickups.gnut
@@ -0,0 +1,1195 @@
+untyped
+
+global function Pickups_Init
+global function AddCollectible
+global function UpdateCollectiblesAfterLoadSaveGame
+global function CreateWeaponPickup
+global function CreatePickup
+global function WaitUntilPlayerPicksUp
+global function AddClipToWeapon
+global function AddRoundsToWeapon
+global function AddRoundToWeapon
+global function AddClipToMainWeapons
+global function AddTwoClipToMainWeapons
+global function AddRoundToOrdnance
+global function AddRoundsToTactical
+global function CreateScriptWeapon
+global function GetAllLeveledScriptWeapons
+global function TitanLoadoutWaitsForPickup // Needed only to be global to spawn pickups from dev menu.
+
+#if DEV
+ global function Dev_ResetCollectiblesProgress_Level
+#endif
+
+const HEALTH_PICKUP_AMOUNT = 3000
+global const PICKUP_GLOW_FX = $"P_ar_titan_droppoint"
+//const HEALTH_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam_collision.mdl"
+//const HEALTH_MODEL = $"models/domestic/trash_can_green_closed.mdl"
+//const HEALTH_MODEL = $"models/weapons/bullets/projectile_rocket_large.mdl"
+const HEALTH_MODEL = $"models/gameplay/health_pickup_small.mdl"
+const HEALTH_MODEL_LARGE = $"models/gameplay/health_pickup_large.mdl"
+
+const GRENADE_AMMO_MODEL = $"models/Weapons/ammoboxes/ammobox_01.mdl"
+const LION_MODEL = $"models/statues/lion_statue_bronze_green_small.mdl"
+const HELMET_COLLECTIBLE_MODEL = $"models/humans/heroes/mlt_hero_jack_helmet_static.mdl"
+const COLLECTIBLE_GLOW_EFFECT = $"P_item_bluelion"
+
+
+global struct LeveledScriptedWeapons
+{
+ table<string, bool> foundScriptWeapons
+ array<entity> infoTargets
+}
+
+struct HealthPickup
+{
+ float healAmount
+ float healTime
+ string pickupSound
+ string healSound
+ string endSound
+ asset model
+}
+
+struct Collectible
+{
+ entity ent
+ int id
+ vector pos
+}
+
+struct
+{
+ int nextHealthDropSmall
+ int nextHealthDropLarge
+ float lastHealthDropTime
+ table<string, HealthPickup> healthPickups
+ array<Collectible> collectibles
+ int testMapCollectibleValue
+} file
+
+function Pickups_Init()
+{
+ HealthPickup small
+ small.healAmount = 0.4
+ small.healTime = 1.0
+ small.pickupSound = "Pilot_HealthPack_Small_Pickup"
+ small.healSound = "Pilot_HealthPack_Small_Healing"
+ small.endSound = "Pilot_HealthPack_Small_Healing_End"
+ small.model = HEALTH_MODEL
+ file.healthPickups[ "health_pickup_small" ] <- small
+
+ HealthPickup large
+ large.healAmount = 0.8
+ large.healTime = 2.0
+ large.pickupSound = "Pilot_HealthPack_Large_Pickup"
+ large.healSound = "Pilot_HealthPack_Large_Healing"
+ large.endSound = "Pilot_HealthPack_Large_Healing_End"
+ large.model = HEALTH_MODEL_LARGE
+ file.healthPickups[ "health_pickup_large" ] <- large
+
+
+ //AddSpawnCallbackEditorClass( "script_ref", "script_pickup_health", HealthPickup_OnSpawned )
+ //AddSpawnCallbackEditorClass( "script_ref", "script_pickup_health_large", HealthPickupLarge_OnSpawned )
+ AddSpawnCallbackEditorClass( "script_mover_lightweight", "script_collectible", AddCollectible )
+ AddSpawnCallbackEditorClass( "script_ref", "script_pickup_weapon", CreateWeaponPickup )
+ //AddSpawnCallbackEditorClass( "script_ref", "script_pickup_grenades", CreateGrenadeAmmoPickup )
+ //AddSpawnCallbackEditorClass( "script_ref", "script_pickup_ammo", CreateGrenadeAmmoPickup )
+ AddSpawnCallbackEditorClass( "script_ref", "script_pickup_titan", CreateTitanPickup )
+
+ PrecacheModel( HEALTH_MODEL )
+ PrecacheModel( HEALTH_MODEL_LARGE )
+ PrecacheModel( GRENADE_AMMO_MODEL )
+ PrecacheModel( LION_MODEL )
+ PrecacheModel( HELMET_COLLECTIBLE_MODEL )
+ PrecacheParticleSystem( COLLECTIBLE_PICKUP_EFFECT )
+ PrecacheParticleSystem( COLLECTIBLE_GLOW_EFFECT )
+
+ RegisterSignal( "NewHealthPickup" )
+ RegisterSignal( "CollectibleEndThink" )
+
+ SetNextHealthDropSmall()
+ SetNextHealthDropLarge()
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad_Pickups )
+}
+
+void function CreateScriptWeapon( entity point )
+{
+ CreateWeaponPickup( point )
+ array<entity> linkParents = point.GetLinkParentArray()
+ foreach ( entity linkParent in linkParents )
+ {
+ linkParent.UnlinkFromEnt( point )
+ }
+ //point.Destroy()
+}
+
+void function EntitiesDidLoad_Pickups()
+{
+ if ( shGlobal.proto_pilotHealthPickupsEnabled )
+ {
+ AddDeathCallback( "npc_soldier", OnNPCKilled_DropHealth )
+ AddDeathCallback( "npc_spectre", OnNPCKilled_DropHealth )
+ }
+
+ SetupCollectibles()
+}
+
+void function SetNextHealthDropSmall()
+{
+ if ( shGlobal.proto_pilotHealthRegenDisabled )
+ file.nextHealthDropSmall = RandomInt( 6 ) + 6
+ else
+ file.nextHealthDropSmall = RandomInt( 4 ) + 4
+
+ file.lastHealthDropTime = Time()
+}
+
+void function SetNextHealthDropLarge()
+{
+ file.nextHealthDropLarge = RandomIntRange( 3, 6 )
+}
+
+void function OnNPCKilled_DropHealth( entity npc, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsValid( attacker ) )
+ return
+
+ switch ( npc.GetClassName() )
+ {
+ case "npc_soldier":
+ case "npc_spectre":
+ case "npc_stalker":
+ break
+
+ default:
+ return
+ }
+
+ OnNPCKilled_DropHealth_Internal( npc, attacker, damageInfo )
+}
+
+void function OnNPCKilled_DropHealth_Internal( entity npc, entity attacker, var damageInfo )
+{
+ if ( npc.GetTeam() != TEAM_IMC )
+ return
+
+ file.nextHealthDropSmall--
+
+ if ( !DropHealthFromDeath( npc, attacker ) )
+ return
+
+ SetNextHealthDropSmall()
+
+ vector angles = npc.GetAngles()
+ angles.x = 0
+
+ vector forward = AnglesToForward( angles )
+
+ vector origin = npc.GetWorldSpaceCenter() + forward * 16
+
+ entity health
+ file.nextHealthDropLarge--
+ if ( file.nextHealthDropLarge < 0 && shGlobal.proto_pilotHealthRegenDisabled )
+ {
+ SetNextHealthDropLarge()
+ health = CreateHealthPickupSized( origin, angles, "health_pickup_large" )
+ }
+ else
+ {
+ health = CreateHealthPickupSized( origin, angles, "health_pickup_small" )
+ }
+
+ EmitSoundOnEntity( health, "Pilot_HealthPack_Drop" )
+ health.Fire( "Kill", "", 180 )
+}
+
+bool function DropHealthFromDeath( entity npc, entity attacker )
+{
+ if ( !attacker.IsPlayer() )
+ return file.nextHealthDropSmall < 0
+
+ if ( Time() - file.lastHealthDropTime < 3.0 )
+ return false
+
+ float healthRatio = float( attacker.GetHealth() ) / attacker.GetMaxHealth()
+
+ float chanceFromNextHealth = GraphCapped( file.nextHealthDropSmall, 0, 5, 0.4, 1.1 )
+ float chanceFromLowHealth = GraphCapped( healthRatio, 0.25, 0.80, 0.4, 1.1 )
+ float dropChance = chanceFromNextHealth * chanceFromLowHealth
+
+ return RandomFloat( 1.0 ) > dropChance
+
+ /*
+ if ( file.nextHealthDropSmall <= 1 )
+ {
+ float ratio = float( attacker.GetHealth() ) / attacker.GetMaxHealth() - 0.15
+ float random = RandomFloat( 1.0 )
+ if ( random > ratio )
+ return true
+ }
+
+ if ( Time() - file.lastHealthDropTime < 5.0 )
+ return false
+
+ if ( float( attacker.GetHealth() ) / attacker.GetMaxHealth() > 0.25 )
+ return false
+
+ return Distance( attacker.GetOrigin(), npc.GetOrigin() ) < 2000
+ */
+}
+
+void function CreateTitanPickup( entity ent )
+{
+ entity mover = CreatePickup( ent, LION_MODEL, DropTitanPickedUp )
+
+ mover.EndSignal( "OnDestroy" )
+ wait 0.2 // for some buggy reason!?
+ EmitSoundOnEntity( mover, "health_pickup_loopsound_far" )
+ EmitSoundOnEntity( mover, "health_pickup_loopsound_near" )
+ return
+}
+
+bool function DropTitanPickedUp( entity player )
+{
+ if ( player.IsTitan() )
+ return false
+
+ AddPlayerScore( player, "PilotHealthPickup" )
+ EmitSoundOnEntity( player, "titan_energyshield_up" )
+ player.SetNextTitanRespawnAvailable( 0 )
+
+ return true
+}
+
+function DisplayTempNameText( entity ent, string text )
+{
+ ent.EndSignal( "OnDestroy" )
+ for ( ;; )
+ {
+ array<entity> players = GetPlayerArray()
+ if ( players.len() )
+ {
+ entity nearestPlayer = GetClosest( players, ent.GetOrigin(), 2300 )
+ if ( nearestPlayer != null )
+ DebugDrawText( ent.GetWorldSpaceCenter(), text, true, 1 )
+ }
+ wait 0.9
+ }
+}
+
+entity function CreateHealthPickupSized( vector origin, vector angles, string healthType )
+{
+ HealthPickup pickup = file.healthPickups[ healthType ]
+ entity ent = CreatePropPhysics( pickup.model, origin, angles )
+ ent.NotSolid()
+
+
+ angles = AnglesCompose( angles, < -45,0,0> )
+ vector forward = AnglesToForward( angles )
+ ent.SetVelocity( forward * 200 )
+
+ ent.SetAngularVelocity( RandomFloatRange( 300, 500 ), RandomFloatRange( -100, 100 ), 0 )
+
+ thread HealthPickupWaitsForPickup( ent, pickup )
+ Highlight_SetNeutralHighlight( ent, "health_pickup" )
+ return ent
+}
+
+void function HealthPickup_OnSpawned( entity ent )
+{
+ //if ( shGlobal.proto_pilotHealthRegenDisabled )
+ {
+ CreateHealthPickupSized( ent.GetOrigin(), ent.GetAngles(), "health_pickup_small" )
+ ent.Destroy()
+ return
+ }
+
+}
+
+void function HealthPickupLarge_OnSpawned( entity ent )
+{
+ if ( shGlobal.proto_pilotHealthRegenDisabled )
+ {
+ CreateHealthPickupSized( ent.GetOrigin(), ent.GetAngles(), "health_pickup_small" )
+ ent.Destroy()
+ //HealthPickup_OnSpawned( ent )
+ return
+ }
+
+ CreateHealthPickupSized( ent.GetOrigin(), ent.GetAngles(), "health_pickup_large" )
+ ent.Destroy()
+}
+
+void function CreateHealthRegenField( entity ent )
+{
+ ent.EndSignal( "OnDestroy" )
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+ )
+
+ PickupGlow pickupGlow = CreatePickupGlow( ent, 0, 255, 0 )
+
+ float healthRemainingMax = 1000
+ float healthRemainingCurrent = healthRemainingMax
+ float useIncrement = 9
+ float refillRate = 0
+ float nextRegenTime = 0
+
+ bool available
+ entity player
+ entity lastPlayer
+
+ for ( ;; )
+ {
+ WaitFrame()
+
+ float ratio = healthRemainingCurrent / healthRemainingMax
+ int green = int( Graph( ratio, 0, 1, 0, 255 ) )
+ PickupGlow_SetColor( pickupGlow, 0, green, 0 )
+
+ if ( Time() > nextRegenTime )
+ {
+ healthRemainingCurrent = min( healthRemainingCurrent + refillRate, healthRemainingMax )
+ nextRegenTime = Time() + 1.2
+ }
+
+ if ( healthRemainingCurrent < useIncrement )
+ continue
+
+ player = GetHealthPickupPlayer( ent )
+
+ if ( lastPlayer != player )
+ {
+ if ( IsValid( lastPlayer ) )
+ {
+ //EmitSoundOnEntity( lastPlayer, "Pilot_Stimpack_Deactivate" )
+ StopSoundOnEntity( lastPlayer, "Pilot_Stimpack_Loop" )
+ }
+
+ if ( player != null )
+ {
+ // new player powers up
+ EmitSoundOnEntity( player, "Pilot_Stimpack_Activate" )
+ EmitSoundOnEntity( player, "Pilot_Stimpack_Loop" )
+ }
+
+ lastPlayer = player
+ }
+
+ if ( player == null )
+ continue
+
+ // recent damage reduces healing effect, so you cant abuse it
+ float recentDamage = TotalDamageOverTime_BlendedOut( player, 0.5, 5.0 )
+
+ // damage is ramped down based on how much damage was taken recently
+ float damageMod = GraphCapped( recentDamage, 0, 40, 1.0, 0.35 )
+ float healthGain = useIncrement * damageMod
+
+ healthRemainingCurrent -= healthGain
+ int newHealth = int( min( player.GetHealth() + healthGain, player.GetMaxHealth() ) )
+ player.SetHealth( newHealth )
+
+ nextRegenTime = Time() + 10
+ }
+}
+
+entity function GetHealthPickupPlayer( entity ent )
+{
+ // try to heal the player
+ entity player = GetPickupPlayer( ent )
+ if ( player == null )
+ return null
+ if ( !IsPilot( player ) )
+ return null
+ if ( player.GetHealth() >= player.GetMaxHealth() )
+ return null
+
+ return player
+}
+
+void function CreateGrenadeAmmoPickup( entity ent )
+{
+ thread DisplayTempNameText( ent, "Ammo" )
+ CreatePickup( ent, GRENADE_AMMO_MODEL, GenericAmmoPickup )
+ CreatePickupGlow( ent, 13, 104, 255 )
+}
+
+entity function CreatePickup( entity ent, asset model, bool functionref( entity ) pickupFunc )
+{
+ entity mover = CreateEntity( "script_mover" )
+ mover.kv.solid = 0
+ mover.SetValueForModelKey( model )
+ mover.SetFadeDistance( 5000 )
+ mover.kv.SpawnAsPhysicsMover = 0
+ mover.SetOrigin( ent.GetOrigin() )
+ mover.SetAngles( ent.GetAngles() )
+ DispatchSpawn( mover )
+
+ ent.EndSignal( "OnDestroy" )
+
+ mover.SetOwner( ent )
+
+ ent.SetParent( mover )
+ thread PickupWaitsForPickup( ent, mover, pickupFunc )
+
+ return mover
+}
+
+void function PickupWaitsForPickup( entity ent, entity mover, bool functionref( entity ) pickupFunc )
+{
+ ent.EndSignal( "OnDestroy" )
+ OnThreadEnd(
+ function() : ( mover, ent )
+ {
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+ )
+
+// local colorVec = Vector(r,g,b)
+// local cpoint = CreateEntity( "info_placement_helper" )
+// SetTargetName( cpoint, UniqueString( "pickup_controlpoint" ) )
+// DispatchSpawn( cpoint )
+// cpoint.SetOrigin( colorVec )
+// local glowFX = PlayFXWithControlPoint( PICKUP_GLOW_FX, mover.GetOrigin(), cpoint, null, null, null, C_PLAYFX_LOOP )
+//
+// OnThreadEnd(
+// function() : ( ent, mover, glowFX, cpoint )
+// {
+// cpoint.Fire( "Kill", "", 1.0 )
+// if ( IsValid(glowFX) )
+// {
+// glowFX.Fire( "StopPlayEndCap" )
+// glowFX.Fire( "Kill", "", 1.0 )
+// }
+// mover.Destroy()
+// ent.Destroy()
+// }
+// )
+
+ for ( ;; )
+ {
+ entity player = WaitUntilPlayerPicksUp( ent )
+ if ( pickupFunc( player ) )
+ return
+ WaitFrame()
+ }
+}
+
+void function HealthPickupWaitsForPickup( entity ent, HealthPickup pickup )
+{
+ ent.EndSignal( "OnDestroy" )
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+ )
+
+ for ( ;; )
+ {
+ WaitFrame()
+
+ entity player = WaitUntilPlayerPicksUp( ent )
+ if ( player.IsTitan() )
+ continue
+ if ( player.GetHealth() >= player.GetMaxHealth() )
+ continue
+
+ thread HealPlayerOverTime( player, pickup )
+ return
+ }
+}
+
+
+void function TitanLoadoutWaitsForPickup( entity ent, bool functionref( entity, entity ) pickupFunc )
+{
+ ent.EndSignal( "OnDestroy" )
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+ )
+
+ for ( ;; )
+ {
+ entity player = WaitUntilPlayerPicksUp( ent )
+ if ( pickupFunc( player, ent ) )
+ return
+ WaitFrame()
+ }
+}
+
+
+entity function GetPickupPlayer( entity ent )
+{
+ array<entity> players = GetPlayerArray()
+
+ vector entOrigin = ent.GetCenter()
+
+ foreach ( player in players )
+ {
+ if ( !IsAlive( player ) )
+ continue
+
+ int pickupDist
+ if ( player.IsTitan() )
+ pickupDist = 256 * 256
+ else
+ pickupDist = 96 * 96
+
+ if ( GetEditorClass( ent ) == "script_collectible" )
+ pickupDist = 72 * 72
+
+ vector playerOrigin = player.GetOrigin()
+ if ( DistanceSqr( playerOrigin, entOrigin ) < pickupDist )
+ {
+ TraceResults trace
+ trace = TraceLine( entOrigin, playerOrigin, [ player, ent ], TRACE_MASK_SOLID, TRACE_COLLISION_GROUP_NONE )
+ if ( trace.fraction >= 0.99 || trace.hitEnt == ent )
+ return player
+ }
+ }
+
+ return null
+}
+
+entity function WaitUntilPlayerPicksUp( entity ent )
+{
+ while ( true )
+ {
+ entity player = GetPickupPlayer( ent )
+ if ( player != null )
+ return player
+ WaitFrame()
+ }
+
+ unreachable
+}
+
+function PickupHover( mover )
+{
+ mover.EndSignal( "OnDestroy" )
+
+ int direction = 1
+
+ while ( 1 )
+ {
+ mover.MoveTo( mover.GetOrigin() + Vector( 0, 0, 20*direction ), 1, 0.4, 0.4 )
+ mover.RotateTo( mover.GetAngles() + Vector( 0, 90, 0 ), 1, 0, 0 )
+ direction *= -1
+ wait 1
+ }
+}
+
+int function AddClipToMainWeapons( entity player )
+{
+ int gainedAmmo
+ foreach ( weapon in player.GetMainWeapons() )
+ {
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ }
+ return gainedAmmo
+}
+
+int function AddTwoClipToMainWeapons( entity player )
+{
+ int gainedAmmo
+ for ( int i = 0; i < 2; i++ )
+ {
+ foreach ( weapon in player.GetMainWeapons() )
+ {
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ }
+ }
+ return gainedAmmo
+}
+
+int function AddRoundToOrdnance( entity player )
+{
+ int gainedAmmo
+ entity ordnance = player.GetOffhandWeapon( OFFHAND_ORDNANCE )
+ if ( IsValid( ordnance ) )
+ gainedAmmo += AddRoundToWeapon( player, ordnance )
+ return gainedAmmo
+}
+
+int function AddRoundsToTactical( entity player, int count = 1 )
+{
+ int gainedAmmo
+ entity ordnance = player.GetOffhandWeapon( OFFHAND_SPECIAL )
+ if ( IsValid( ordnance ) )
+ gainedAmmo += AddRoundsToWeapon( player, ordnance, count )
+ return gainedAmmo
+}
+
+int function AddClipToWeapon( entity player, entity weapon )
+{
+ int ammoPerClip = weapon.GetWeaponPrimaryClipCountMax()
+ int gainedAmmo = 0
+
+ switch ( weapon.GetWeaponInfoFileKeyField( "fire_mode" ) )
+ {
+ case "offhand_hybrid":
+ case "offhand":
+ case "offhand_instant":
+
+ // offhand weapons typically cant store ammo, so refill the current clip
+ if ( ammoPerClip > 0 )
+ {
+ int primaryClipCount = weapon.GetWeaponPrimaryClipCount()
+ weapon.SetWeaponPrimaryClipCount( ammoPerClip )
+ gainedAmmo = weapon.GetWeaponPrimaryClipCount() - primaryClipCount
+ }
+ break
+
+ default:
+ int primaryAmmoCount = weapon.GetWeaponPrimaryAmmoCount()
+ // this weapon has off-clip ammo storage, so add ammo to storage
+ int stockpile = player.GetWeaponAmmoStockpile( weapon )
+ weapon.SetWeaponPrimaryAmmoCount( primaryAmmoCount + ammoPerClip )
+ gainedAmmo = player.GetWeaponAmmoStockpile( weapon ) - stockpile
+ break
+ }
+
+ return gainedAmmo
+}
+
+int function AddRoundToWeapon( entity player, entity weapon )
+{
+ return AddRoundsToWeapon( player, weapon, 1 )
+}
+
+int function AddRoundsToWeapon( entity player, entity weapon, int rounds )
+{
+ int ammoPerClip = weapon.GetWeaponPrimaryClipCountMax()
+ int gainedAmmo = 0
+
+ switch ( weapon.GetWeaponInfoFileKeyField( "fire_mode" ) )
+ {
+ case "offhand_hybrid":
+ case "offhand":
+ case "offhand_instant":
+
+ // offhand weapons typically cant store ammo, so refill the current clip
+ if ( ammoPerClip > 0 )
+ {
+ int primaryAmmoInClipCount = weapon.GetWeaponPrimaryClipCount()
+ int newAmmo = minint( ammoPerClip, primaryAmmoInClipCount + rounds )
+ if ( newAmmo > primaryAmmoInClipCount )
+ {
+ weapon.SetWeaponPrimaryClipCount( newAmmo )
+ gainedAmmo = weapon.GetWeaponPrimaryClipCount() - primaryAmmoInClipCount
+ }
+ }
+ break
+
+
+ default:
+ int primaryAmmoCount = weapon.GetWeaponPrimaryAmmoCount()
+ // this weapon has off-clip ammo storage, so add ammo to storage
+ int stockpile = player.GetWeaponAmmoStockpile( weapon )
+ weapon.SetWeaponPrimaryAmmoCount( primaryAmmoCount + rounds )
+ gainedAmmo = player.GetWeaponAmmoStockpile( weapon ) - stockpile
+ break
+ }
+
+ return gainedAmmo
+}
+
+bool function GenericAmmoPickup( entity player )
+{
+ if ( player.IsTitan() )
+ return false
+
+ Assert( player.IsPlayer() )
+
+ int gainedAmmo = 0
+ foreach ( weapon in player.GetMainWeapons() )
+ {
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ }
+ foreach ( weapon in player.GetOffhandWeapons() )
+ {
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ }
+
+ if ( gainedAmmo > 0 )
+ {
+ AddPlayerScore( player, "PilotAmmoPickup" )
+ EmitSoundOnEntity( player, "BurnCard_GrenadeRefill_Refill" )
+ EmitSoundOnEntity( player, "titan_energyshield_up" )
+ return true
+ }
+
+ return false
+}
+
+
+bool function GrenadeAmmoPickedUp( entity player )
+{
+ if ( player.IsTitan() )
+ return false
+
+ Assert( player.IsPlayer() )
+
+ entity weapon = player.GetOffhandWeapon( 0 )
+ if ( !IsValid( weapon ) )
+ return false
+
+ int ammo = weapon.GetWeaponPrimaryClipCount()
+ int newAmmo = minint( player.GetWeaponAmmoMaxLoaded( weapon ), ammo + 2 )
+ weapon.SetWeaponPrimaryClipCount( newAmmo )
+
+ bool pickup = newAmmo > ammo
+
+ if ( pickup )
+ {
+ AddPlayerScore( player, "PilotAmmoPickup" )
+ EmitSoundOnEntity( player, "BurnCard_GrenadeRefill_Refill" )
+ EmitSoundOnEntity( player, "titan_energyshield_up" )
+ return true
+ }
+
+ return false
+}
+
+void function HealPlayerOverTime( entity player, HealthPickup pickup )
+{
+ //StimPlayer( player, pickup.healTime * 2.0 )
+
+ Assert( IsNewThread(), "Must be threaded off" )
+ Assert( player.IsPlayer() )
+ //AddPlayerScore( player, "PilotHealthPickup" )
+ //EmitSoundOnEntity( player, "titan_energyshield_up" )
+
+ // cycle old effects
+ //player.Signal( "NewHealthPickup" )
+ //player.EndSignal( "NewHealthPickup" )
+
+ Assert( IsAlive( player ) )
+ player.EndSignal( "OnDeath" )
+
+ EmitSoundOnEntity( player, pickup.pickupSound )
+ EmitSoundOnEntity( player, pickup.healSound )
+
+
+ int frames = 10 * int( pickup.healTime )
+ float amount = player.GetMaxHealth() * pickup.healAmount
+ float healthPerFrame = amount / frames
+ float midTime = Time() + pickup.healTime * 0.5
+ float healthRemainder = 0
+
+ for ( int i = 0; i < frames; i++ )
+ {
+ WaitFrame()
+
+ float healthThisFrame
+ if ( Time() < midTime )
+ healthThisFrame = healthPerFrame * 0.6
+ else
+ healthThisFrame = healthPerFrame * 1.4
+
+ healthRemainder += healthThisFrame % 1
+ healthThisFrame -= healthThisFrame % 1
+ healthThisFrame += ( healthRemainder - healthRemainder % 1 )
+ healthRemainder %= 1
+// printt( "healththisframe is " + healthThisFrame + " healthRemainder is " + healthRemainder )
+
+ float newHealth = min( player.GetHealth() + healthThisFrame, player.GetMaxHealth() )
+ player.SetHealth( newHealth )
+ }
+
+ EmitSoundOnEntity( player, pickup.endSound )
+}
+
+LeveledScriptedWeapons function GetAllLeveledScriptWeapons()
+{
+ LeveledScriptedWeapons leveledScriptedWeapons
+
+ table<string, bool> tableAllWeapons
+
+ foreach ( weaponName in GetAllSPWeapons() )
+ {
+ tableAllWeapons[ weaponName ] <- true
+ }
+
+ foreach ( ent in GetEntArrayByClass_Expensive( "info_target" ) )
+ {
+ if ( !ent.HasKey( "editorclass" ) )
+ continue
+
+ string editorclass = expect string( ent.kv.editorclass )
+ if ( !( editorclass in tableAllWeapons ) )
+ continue
+
+ leveledScriptedWeapons.infoTargets.append( ent )
+ leveledScriptedWeapons.foundScriptWeapons[ editorclass ] <- true
+ }
+
+ // legacy support
+ foreach ( ent in GetEntArrayByClass_Expensive( "script_ref" ) )
+ {
+ if ( !ent.HasKey( "editorclass" ) )
+ continue
+ if ( ent.kv.editorclass != "script_pickup_weapon" )
+ continue
+
+ Assert( ent.HasKey( "script_weapon" ) )
+ string weapon = expect string( ent.kv.script_weapon )
+ if ( !( weapon in tableAllWeapons ) )
+ continue
+
+ //leveledScriptedWeapons.infoTargets.append( ent )
+ leveledScriptedWeapons.foundScriptWeapons[ weapon ] <- true
+ }
+
+ AddSpawnCallbackEditorClass( "script_ref", "script_pickup_weapon", CreateWeaponPickup )
+
+ return leveledScriptedWeapons
+}
+
+void function CreateWeaponPickup( entity ent )
+{
+ Assert( ent.HasKey( "script_weapon" ) )
+ string weaponClass = ent.GetValueForKey( "script_weapon" )
+
+ #if DEV
+ if ( !IsTestMap() )
+ {
+ if ( !WeaponIsPrecached( weaponClass ) )
+ {
+ CodeWarning( "Weapon " + weaponClass + " is not precached, re-export auto precache script" )
+ return
+ }
+
+ if ( !GetWeaponInfoFileKeyField_Global( weaponClass, "leveled_pickup" ) )
+ {
+ CodeWarning( "Tried to place illegal " + weaponClass + " in leveled at " + ent.GetOrigin() )
+ return
+ }
+ }
+
+ VerifyWeaponPickupModel( ent, weaponClass )
+
+ if ( GetWeaponInfoFileKeyField_Global( weaponClass, "offhand_default_inventory_slot" ) == OFFHAND_LEFT )
+ {
+ CodeWarning( "Illegal pickup " + weaponClass + " at " + ent.GetOrigin() )
+ return
+ }
+
+ #endif
+
+
+ bool doMarkAsLoadoutPickup = false
+ int loadoutIndex = GetSPTitanLoadoutIndexForWeapon( weaponClass )
+ if ( loadoutIndex >= 0 )
+ {
+ if ( IsBTLoadoutUnlocked( loadoutIndex ) )
+ return
+ else
+ doMarkAsLoadoutPickup = true
+ }
+
+ bool constrain = !ent.HasKey( "start_constrained" ) || ( ent.HasKey( "start_constrained" ) && ent.GetValueForKey( "start_constrained" ) == "1" )
+ entity weapon
+
+ if ( constrain ) // make all weapons constrained for now
+ {
+ weapon = CreateWeaponEntityByNameConstrained( weaponClass, ent.GetOrigin(), ent.GetAngles() )
+ }
+ else
+ {
+ weapon = CreateWeaponEntityByNameWithPhysics( weaponClass, ent.GetOrigin(), ent.GetAngles() )
+ weapon.SetVelocity( <0,0,0> )
+ }
+
+ SetTargetName( weapon, "leveled_" + weaponClass )
+ if ( ent.HasKey( "fadedist" ) )
+ {
+ weapon.kv.fadedist = ent.kv.fadedist
+ }
+ else
+ {
+ weapon.kv.fadedist = -1
+ }
+
+ ApplyWeaponModsFromEnt( ent, weapon )
+
+ if ( ent.HasKey( "script_name" ) )
+ {
+ weapon.kv.script_name = ent.kv.script_name
+ }
+
+ if ( doMarkAsLoadoutPickup )
+ {
+ weapon.MarkAsLoadoutPickup()
+ thread CreateTitanWeaponPickupHintTrigger( weapon )
+ thread TitanLoadoutWaitsForPickup( weapon, SPTitanLoadoutPickup )
+ }
+
+ HighlightWeapon( weapon )
+
+ // for s2s -mo
+ // for sp_training (pickups travel with moving gun racks) -sean
+ if ( ent.GetParent() )
+ {
+ weapon.SetParent( ent.GetParent(), "", true )
+ }
+
+ // for sp_training, to replenish the weapon when it's picked up -sean
+ ent.e.attachedEnts.append( weapon )
+}
+
+void function VerifyWeaponPickupModel( entity ent, string weaponClass )
+{
+ var playermodel
+ var playermodel1 = GetWeaponInfoFileKeyFieldAsset_Global( weaponClass, "droppedmodel" )
+ var playermodel2 = GetWeaponInfoFileKeyFieldAsset_Global( weaponClass, "playermodel" )
+ if ( playermodel1 != $"" )
+ playermodel = playermodel1
+ else
+ playermodel = playermodel2
+ playermodel = playermodel.tolower()
+ expect asset( playermodel )
+
+ asset modelName = ent.GetModelName().tolower()
+ if ( modelName != $"" && playermodel != modelName )
+ CodeWarning( "Incorrect Model on weapon " + weaponClass + " at " + ent.GetOrigin() + ", replace with real weapon for auto fix. ( " + modelName + " != " + playermodel + ")" )
+}
+
+void function ApplyWeaponModsFromEnt( entity ent, entity weapon )
+{
+ if ( ent.HasKey( "script_mods" ) )
+ {
+ array<string> mods = split( ent.GetValueForKey( "script_mods" ), " " )
+ if ( mods.len() > 0 )
+ {
+ weapon.SetMods( mods )
+ return
+ }
+ }
+
+ array<string> mods = GetWeaponModsForCurrentLevel( weapon.GetWeaponClassName() )
+ if ( mods.len() )
+ {
+ weapon.SetMods( [ mods.getrandom() ] )
+ }
+}
+
+
+void function AddCollectible( entity ent )
+{
+ Assert( ent.GetClassName() == "script_mover_lightweight" )
+
+ if ( ent.GetModelName() != HELMET_COLLECTIBLE_MODEL )
+ ent.SetModel( HELMET_COLLECTIBLE_MODEL )
+
+ ent.DisableFastPathRendering() // Workaround for glow effect not drawing (bug #177177)
+
+ Collectible collectible
+ collectible.ent = ent
+ collectible.pos = ent.GetOrigin()
+ file.collectibles.append( collectible )
+
+ // Drop to ground
+ if ( !collectible.ent.HasKey( "hover" ) || collectible.ent.kv.hover == "0" )
+ {
+ vector groundPos = OriginToGround( collectible.ent.GetOrigin() + <0,0,1> )
+ collectible.ent.SetOrigin( groundPos + < 0, 0, 32 > )
+ collectible.pos = collectible.ent.GetOrigin()
+ }
+
+ // Effect and not solid
+ collectible.ent.DisableHibernation()
+ collectible.ent.NotSolid()
+ collectible.ent.EnableRenderAlways()
+ collectible.ent.kv.fadedist = 100000
+}
+
+void function SetupCollectibles()
+{
+ // Make sure that the number of collectibles in the level matches the hardcoded value in sh_consts so that SP menus know total number per level.
+ string mapName = GetMapName()
+ int saveIndex = GetCollectibleLevelIndex( mapName )
+ Assert( saveIndex < 0 || file.collectibles.len() == GetMaxLionsInLevel( mapName ), "Collectibles count mismatch. Update LEVEL_UNLOCKS_COUNT in sh_consts.gnut to " + file.collectibles.len() )
+
+ // Index the collectibles so each is unique and it's status can be stored in a cvar. They are sorted by distance from map center to keep consistent on each map load
+ file.collectibles.sort( SortCollectiblesFunc )
+
+ foreach ( int i, Collectible collectible in file.collectibles )
+ {
+ collectible.id = 1 << i
+ thread CollectibleThink( collectible )
+ }
+}
+
+int function SortCollectiblesFunc( Collectible a, Collectible b )
+{
+ float distA = DistanceSqr( a.ent.GetOrigin(), <0,0,0> )
+ float distB = DistanceSqr( b.ent.GetOrigin(), <0,0,0> )
+ if ( distA > distB )
+ return 1
+ else if ( distA < distB )
+ return -1
+ return 0
+}
+
+void function UpdateCollectiblesAfterLoadSaveGame()
+{
+ // This has to run when a save game is loaded because the collectible may be there in the save game, but it was picked up after the save, so we need to delete the ones the player picked up
+ foreach ( Collectible collectible in file.collectibles )
+ {
+ if ( HasCollectible( collectible ) )
+ {
+ //DebugDrawSphere( collectible.pos, 40.0, 255, 0, 0, true, 600.0 )
+ if ( IsValid( collectible.ent ) )
+ collectible.ent.Destroy()
+ Signal( collectible, "CollectibleEndThink" )
+ }
+ }
+
+ // Delete all the weapons already unlocked in the level
+ array<string> unlockedLoadouts = GetSPTitanLoadoutsUnlocked()
+ SPTitanLoadout_RemoveOwnedLoadoutPickupsInLevel( unlockedLoadouts )
+}
+
+#if DEV
+ void function Dev_ResetCollectiblesProgress_Level()
+ {
+ printt( "RESETTING COLLECTIBLE PROGRESS (LEVEL)" )
+ string mapName = GetMapName()
+ int saveIndex = GetCollectibleLevelIndex( mapName )
+ if ( saveIndex == -1 )
+ return
+ printt( " sp_unlocks_level_" + saveIndex, 0 )
+ SetConVarInt( "sp_unlocks_level_" + saveIndex, 0 )
+ }
+#endif
+
+void function CollectibleThink( Collectible collectible )
+{
+ EndSignal( collectible, "CollectibleEndThink" )
+
+ if ( HasCollectible( collectible ) )
+ {
+ //printt( "Player already has collectible", collectible.id )
+ //DebugDrawSphere( collectible.ent.GetOrigin(), 25.0, 150, 0, 0, true, 600.0 )
+ collectible.ent.Destroy()
+ return
+ }
+
+ entity glowEffect = StartParticleEffectOnEntity_ReturnEntity( collectible.ent, GetParticleSystemIndex( COLLECTIBLE_GLOW_EFFECT ), FX_PATTACH_ABSORIGIN_FOLLOW, 0 )
+
+ // Rotate the collectible
+ collectible.ent.NonPhysicsRotate( < 0, 0, 1 >, 35.0 )
+
+ WaitFrame() // emit sound doesn't work on first frame so we have to wait a frame so sound will play. Player can't pickup collectible in frame 1 anyways
+
+ EmitSoundOnEntity( collectible.ent, "Emit_PilotHelmet_Collectible" )
+
+ //wait 1.0
+ //DebugDrawText( collectible.ent.GetOrigin(), string(collectible.id), true, 600.0 )
+ //DebugDrawSphere( collectible.ent.GetOrigin(), 25.0, 255, 0, 0, true, 600.0 )
+
+ // Wait until it's touched by a player
+ entity player = WaitUntilPlayerPicksUp( collectible.ent )
+
+ // Remove collectible
+ EmitSoundOnEntity( player, "Pilot_Collectible_Pickup" )
+ if ( IsValid( glowEffect ) )
+ EffectStop( glowEffect )
+ collectible.ent.Destroy()
+
+ // Save to player profile
+ string mapName = GetMapName()
+ int saveIndex = GetCollectibleLevelIndex( mapName )
+ int bitMask
+ if ( saveIndex >= 0 )
+ {
+ // If it's a real map we store it to player profile
+ string unlockVar = "sp_unlocks_level_" + saveIndex
+ bitMask = GetConVarInt( unlockVar )
+ bitMask = bitMask | collectible.id
+ //printt( "Saving collectible state", unlockVar, bitMask )
+ SetConVarInt( unlockVar, bitMask )
+ }
+ else
+ {
+ // Not a real map, we store it to a file var that wont persist, just so we can pick them up and have kind of working collectibles in test maps
+ CodeWarning( "Collectible state not being saved because this map is not shipping" )
+ file.testMapCollectibleValue = file.testMapCollectibleValue | collectible.id
+ bitMask = file.testMapCollectibleValue
+ }
+
+ // See how many collectibles are found now to pass to the RUI
+ int numCollectiblesFound = GetCollectiblesFoundForLevel( mapName )
+ int maxCollectibles = GetMaxLionsInLevel( mapName )
+
+ // Show message on HUD
+ Remote_CallFunction_NonReplay( player, "ServerCallback_CollectibleFoundMessage", numCollectiblesFound, maxCollectibles )
+
+ CollectiblePickupRumble( player )
+
+ UpdateHeroStatsForPlayer( player )
+
+ int totalLionsCollectedForGame = GetTotalLionsCollected()
+
+ if ( totalLionsCollectedForGame >= GetTotalLionsInGame() )
+ UnlockAchievement( player, achievements.COLLECTIBLES_3 )
+
+ if ( totalLionsCollectedForGame >= ACHIEVEMENT_COLLECTIBLES_2_COUNT )
+ UnlockAchievement( player, achievements.COLLECTIBLES_2 )
+
+ if ( totalLionsCollectedForGame >= ACHIEVEMENT_COLLECTIBLES_1_COUNT )
+ UnlockAchievement( player, achievements.COLLECTIBLES_1 )
+}
+
+void function CollectiblePickupRumble( entity player )
+{
+ float rumbleAmplitude = 200.0
+ float rumbleFrequency = 90.0
+ float rumbleDuration = 2.2
+
+ CreateAirShakeRumbleOnly( player.GetOrigin(), rumbleAmplitude, rumbleFrequency, rumbleDuration )
+}
+
+bool function HasCollectible( Collectible collectible )
+{
+ string mapName = GetMapName()
+ int saveIndex = GetCollectibleLevelIndex( mapName )
+
+ // Not a shipping map, so there is no saved var for this level. Just always make it available
+ if ( saveIndex == -1 )
+ return false
+
+ string unlockVar = "sp_unlocks_level_" + saveIndex
+ int bitMask = GetConVarInt( unlockVar )
+
+ return bool(bitMask & collectible.id)
+}
+
+
+
+
+
+
+
+
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_pickups_glow.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_pickups_glow.gnut
new file mode 100644
index 00000000..f1fe4ecc
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_pickups_glow.gnut
@@ -0,0 +1,53 @@
+global function CreatePickupGlow
+global function PickupGlow_SetColor
+
+global struct PickupGlow
+{
+ entity cpoint
+ entity glowFX
+}
+
+PickupGlow function CreatePickupGlow( entity ent, int r, int g, int b )
+{
+ vector origin = ent.GetOrigin()
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "pickup_controlpoint" ) )
+ DispatchSpawn( cpoint )
+ cpoint.SetOrigin( Vector(r,g,b) )
+ entity glowFX = PlayFXWithControlPoint( COLLECTIBLE_PICKUP_EFFECT, origin, cpoint, -1, null, null, C_PLAYFX_LOOP )
+
+ PickupGlow pickupGlow
+ pickupGlow.cpoint = cpoint
+ pickupGlow.glowFX = glowFX
+ thread PickupGlowCleanup( ent, pickupGlow )
+ return pickupGlow
+}
+
+void function PickupGlowCleanup( entity ent, PickupGlow pickupGlow )
+{
+ OnThreadEnd(
+ function() : ( pickupGlow )
+ {
+ StopPickupGlow( pickupGlow )
+ }
+ )
+
+ ent.WaitSignal( "OnDestroy" )
+}
+
+void function StopPickupGlow( PickupGlow pickupGlow )
+{
+ if ( IsValid( pickupGlow.cpoint ) )
+ pickupGlow.cpoint.Destroy()
+
+ if ( IsValid(pickupGlow.glowFX) )
+ {
+ EntityFire( pickupGlow.glowFX, "StopPlayEndCap" )
+ pickupGlow.glowFX.Destroy()
+ }
+}
+
+void function PickupGlow_SetColor( PickupGlow pickupGlow, int r, int g, int b )
+{
+ pickupGlow.cpoint.SetOrigin( Vector( r, g, b ) )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_playlist.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_playlist.gnut
new file mode 100644
index 00000000..dfceab41
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_playlist.gnut
@@ -0,0 +1,6 @@
+global function Playlist_Init
+
+void function Playlist_Init()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_revive.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_revive.gnut
new file mode 100644
index 00000000..b2f5c467
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_revive.gnut
@@ -0,0 +1,352 @@
+untyped
+
+
+global function Revive_Init
+
+global function PlayerRevivesOrBleedsOut
+global function DeathPackage_PlayerRevive
+global function ShouldRevivePlayer
+
+const float REVIVE_BLEED_OUT_TIME = 15.0
+global const float REVIVE_DEATH_TIME = 2.0
+const float REVIVE_DIST_OUTER = 135.0
+const float REVIVE_DIST_INNER = 120.0
+
+struct
+{
+ table fakePlayers
+} file
+
+function Revive_Init()
+{
+ if ( !ReviveEnabled() )
+ return
+
+ RegisterSignal( "KillReviveNag" )
+ RegisterSignal( "DoneBleedingOut" )
+ RegisterSignal( "ReviveSucceeded" )
+ RegisterSignal( "ReviveFailed" )
+ RegisterSignal( "ForceBleedOut" )
+
+ AddCallback_OnClientDisconnected( ReviveOnClientDisconnect )
+}
+
+void function PlayerRevivesOrBleedsOut( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "ForceBleedOut" )
+ svGlobal.levelEnt.EndSignal( "RoundEnd" )
+
+ table e = { revived = false }
+ //thread PlayerReviveVONag( player, 0.5 )
+
+ OnThreadEnd(
+ function() : ( player, e )
+ {
+ if ( !IsValid( player ) )
+ return
+
+ player.Signal( "KillReviveNag" )
+ player.Signal( "DoneBleedingOut" )
+ player.nv.reviveBleedingOut = -1.0 //-1 means off
+
+ if ( e.revived )
+ {
+ player.Signal( "ReviveSucceeded")
+ thread PlayerStandsBackUp( player )
+ }
+ else
+ {
+ file.fakePlayers[ player ].Destroy()
+ player.Signal( "ReviveFailed" )
+ DecideRespawnPlayer( player )
+ }
+ }
+ )
+
+ wait( REVIVE_DEATH_TIME )
+ player.StartObserverMode( OBS_MODE_DEATHCAM )
+
+ ForceRespawnIfEntireTeamIsDead( player )
+
+ float endTime = Time() + REVIVE_BLEED_OUT_TIME
+ player.nv.reviveBleedingOut = endTime
+
+ bool reviving = false
+ float doneReviveTime = Time() + 100
+
+ float distOuterSqr = pow( REVIVE_DIST_OUTER, 2 )
+ float distInnerSqr = pow( REVIVE_DIST_INNER, 2 )
+
+ while ( true )
+ {
+ array<entity> healers = Revive_GetAvailablePlayerHealers( player )
+
+ //we were reviving but aren't anymore - set revive to false.
+ if ( reviving && !FriendlyIsReviving( healers, player, distOuterSqr ) )
+ {
+ //thread PlayerReviveVONag( player )
+ reviving = false
+ player.nv.reviveHealedTime = -1.0 //-1 means off
+ }
+ //we were not reviving but now we are? set the new revive done time.
+ else if ( !reviving && FriendlyIsReviving( healers, player, distInnerSqr ) )
+ {
+ player.Signal( "KillReviveNag" )
+ doneReviveTime = Time() + REVIVE_TIME_TO_REVIVE
+ player.nv.reviveHealedTime = doneReviveTime
+ reviving = true
+ }
+
+ //are we done reviving? then set the value and return
+ if ( reviving && Time() > doneReviveTime )
+ {
+ e.revived = true
+ return
+ }
+
+ //we didn't make it
+ if ( !reviving && Time() > endTime )
+ return
+
+ wait 0.2
+ }
+}
+
+void function ForceRespawnIfEntireTeamIsDead( entity player )
+{
+ int playerTeam = player.GetTeam()
+ array<entity> victimTeamMembers = GetPlayerArrayOfTeam( playerTeam )
+ foreach ( member in victimTeamMembers )
+ {
+ if ( member.p.isReviving || IsAlive( member ) )
+ return
+ }
+ foreach ( member in victimTeamMembers )
+ {
+ if ( player != member && member.p.isReviving == false )
+ member.Signal( "ForceBleedOut" )
+ }
+ MessageToTeam( GetOtherTeam( playerTeam ), eEventNotifications.EnemyTeamEliminated )
+ player.Signal( "ForceBleedOut" )
+}
+
+void function PlayerReviveVONag( entity player, float delay = 0.5 )
+{
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "KillReviveNag" )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ StopSoundOnEntity( player, "diag_coop_bleedout_help" )
+ }
+ )
+
+ if ( delay > 0 )
+ wait delay
+
+ while ( true )
+ {
+ float time = EmitSoundOnEntity( player, "diag_coop_bleedout_help" )
+ wait time
+
+ wait RandomFloatRange( 10, 15 )
+ }
+}
+
+bool function FriendlyIsReviving( array<entity> healers, entity player, float distSqr )
+{
+ vector origin = player.GetOrigin()
+
+ foreach ( friend in healers )
+ {
+ if ( !IsAlive( friend ) )
+ continue
+
+ if ( DistanceSqr( friend.GetOrigin(), origin ) < distSqr )
+ return true
+ }
+
+ return false
+}
+
+array<entity> function Revive_GetAvailablePlayerHealers( entity player )
+{
+ int team = player.GetTeam()
+ array<entity> players = GetPlayerArrayOfTeam( team )
+ array<entity> playersCanRevive = []
+ foreach ( player in players )
+ {
+ if ( !IsAlive( player ) )
+ continue
+
+ playersCanRevive.append( player )
+ }
+
+ return playersCanRevive
+}
+
+bool function ShouldRevivePlayer( entity player, var damageInfo )
+{
+ if ( !ReviveEnabled() )
+ return false
+
+ if ( !GamePlaying() )
+ return false
+
+ if ( player.ContextAction_IsMeleeExecution() )
+ return false
+
+ if ( player.IsTitan() )
+ return false
+
+ int source = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ if ( source == eDamageSourceId.fall ||
+ source == eDamageSourceId.submerged ||
+ source == eDamageSourceId.outOfBounds ||
+ source == eDamageSourceId.indoor_inferno )
+ return false
+
+ return true
+}
+
+entity function SpawnFakePlayer( entity player, int team, vector origin, vector angles, asset weaponModel, asset model )
+{
+ float fadeDist = 10000.0
+ int solidType = 0// 0 = no collision, 2 = bounding box, 6 = use vPhysics, 8 = hitboxes only
+
+ entity fakePlayer = CreatePropDynamic( model, origin, angles, solidType, fadeDist )
+ if ( !( player in file.fakePlayers ) )
+ {
+ file.fakePlayers[ player ] <- null
+ }
+ file.fakePlayers[ player ] = fakePlayer
+
+ thread FakePlayerTrack( fakePlayer, player )
+
+ if ( weaponModel != $"" )
+ {
+ entity gun = CreatePropDynamic( weaponModel, origin, angles, 0, fadeDist )
+ gun.SetParent( fakePlayer, "PROPGUN" )
+ }
+
+ return fakePlayer
+}
+
+void function FakePlayerTrack( entity fakePlayer, entity player )
+{
+ fakePlayer.EndSignal( "OnDestroy" )
+ player.EndSignal( "OnDestroy" )
+ vector lastPlayerOrg = Vector( 0.0, 0.0, 0.0 )
+
+ while ( true )
+ {
+ if ( player.GetOrigin() == lastPlayerOrg )
+ player.SetVelocity( Vector( 0.0, 0.0, 0.0 ) )
+ lastPlayerOrg = player.GetOrigin()
+
+ fakePlayer.SetOrigin( player.GetOrigin() )
+ WaitFrame()
+ }
+}
+
+void function DeathPackage_PlayerRevive( entity player )
+{
+ player.kv.VisibilityFlags = ENTITY_VISIBLE_TO_NOBODY
+
+ vector deathOrg = player.GetOrigin()
+
+ vector mins = Vector( -16.0, -16.0, 0.0 )
+ vector maxs = Vector( 16.0, 16.0, 72.0 )
+ TraceResults result = TraceHull( deathOrg + Vector( 0.0, 0.0, 8.0 ), deathOrg + Vector( 0.0, 0.0, -16000.0 ), mins, maxs, player, ( TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS ), TRACE_COLLISION_GROUP_NONE )
+
+ player.SetVelocity( Vector( 0.0, 0.0, 0.0 ) )
+ thread ReviveLerpToOrigin( player, deathOrg, result.endPos )
+
+ entity activeWeapon = player.GetActiveWeapon()
+
+ asset weaponModel = activeWeapon == null ? $"" : activeWeapon.GetModelName()
+
+ entity fakePlayer = SpawnFakePlayer( player, player.GetTeam(), deathOrg, player.GetAngles(), weaponModel, player.GetModelName() )
+ fakePlayer.Anim_Play( "pt_wounded_drag_zinger_A_idle" )
+ player.Anim_Play( "pt_wounded_drag_zinger_A_idle" )
+}
+
+void function ReviveLerpToOrigin( entity player, vector deathOrg, vector endPos )
+{
+ player.EndSignal( "DoneBleedingOut" )
+ player.EndSignal( "OnDestroy" )
+
+ entity mover = CreateScriptMover()
+
+ OnThreadEnd(
+ function() : ( player, mover )
+ {
+ if ( IsValid( player ) )
+ player.ClearParent()
+
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ }
+ )
+
+ mover.SetOrigin( deathOrg )
+ player.SetOrigin( deathOrg )
+ player.SetParent( mover )
+
+ float moveTime = GraphCapped( deathOrg.z - endPos.z, 0.0, 768.0, 0.1, 2.0 )
+
+ mover.NonPhysicsMoveTo( endPos, moveTime, moveTime, 0.0 )
+ wait( moveTime )
+ player.ClearParent()
+
+ while ( true )
+ {
+ player.SetOrigin( endPos )
+ WaitFrame()
+ }
+}
+
+void function PlayerStandsBackUp( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+ svGlobal.levelEnt.EndSignal( "RoundEnd" )
+
+ entity fakePlayer = expect entity( file.fakePlayers[ player ] )
+ file.fakePlayers[ player ] = null
+
+ player.p.isReviving = true
+
+ OnThreadEnd(
+ function() : ( player, fakePlayer )
+ {
+ if ( IsValid( fakePlayer ) )
+ fakePlayer.Destroy()
+
+ if ( IsValid( player ) )
+ player.p.isReviving = false
+ }
+ )
+
+ fakePlayer.Anim_Play( "CQB_knockback_pain_react" )
+ fakePlayer.Anim_SetInitialTime( 2.0 )
+ wait( 1.5 )
+
+ var settings = player.GetPlayerSettings()
+ player.SetPlayerSettings( "spectator" )
+ player.SetPlayerSettings( settings )
+ player.RespawnPlayer( null )
+}
+
+void function ReviveOnClientDisconnect( entity player )
+{
+ if ( player in file.fakePlayers )
+ {
+ if ( IsValid( file.fakePlayers[ player ] ) )
+ file.fakePlayers[ player ].Destroy()
+ delete file.fakePlayers[ player ]
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut
new file mode 100644
index 00000000..238eab1d
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut
@@ -0,0 +1,224 @@
+untyped
+
+global function Score_Init
+
+global function AddPlayerScore
+global function ScoreEvent_PlayerKilled
+global function ScoreEvent_TitanDoomed
+global function ScoreEvent_TitanKilled
+global function ScoreEvent_NPCKilled
+
+global function ScoreEvent_SetEarnMeterValues
+global function ScoreEvent_SetupEarnMeterValuesForMixedModes
+
+struct {
+ bool firstStrikeDone = false
+} file
+
+void function Score_Init()
+{
+ AddCallback_OnClientConnected( InitPlayerForScoreEvents )
+}
+
+void function InitPlayerForScoreEvents( entity player )
+{
+ player.s.currentKillstreak <- 0
+ player.s.lastKillTime <- 0.0
+ player.s.currentTimedKillstreak <- 0
+}
+
+void function AddPlayerScore( entity targetPlayer, string scoreEventName, entity associatedEnt = null, string noideawhatthisis = "", int pointValueOverride = -1 )
+{
+ ScoreEvent event = GetScoreEvent( scoreEventName )
+
+ if ( !event.enabled || !IsValid( targetPlayer ) || !targetPlayer.IsPlayer() )
+ return
+
+ var associatedHandle = 0
+ if ( associatedEnt != null )
+ associatedHandle = associatedEnt.GetEncodedEHandle()
+
+ if ( pointValueOverride != -1 )
+ event.pointValue = pointValueOverride
+
+ float scale = targetPlayer.IsTitan() ? event.coreMeterScalar : 1.0
+
+ float earnValue = event.earnMeterEarnValue * scale
+ float ownValue = event.earnMeterOwnValue * scale
+
+ PlayerEarnMeter_AddEarnedAndOwned( targetPlayer, earnValue * scale, ownValue * scale )
+
+ // PlayerEarnMeter_AddEarnedAndOwned handles this scaling by itself, we just need to do this for the visual stuff
+ float pilotScaleVar = ( expect string ( GetCurrentPlaylistVarOrUseValue( "earn_meter_pilot_multiplier", "1" ) ) ).tofloat()
+ float titanScaleVar = ( expect string ( GetCurrentPlaylistVarOrUseValue( "earn_meter_titan_multiplier", "1" ) ) ).tofloat()
+
+ if ( targetPlayer.IsTitan() )
+ {
+ earnValue *= titanScaleVar
+ ownValue *= titanScaleVar
+ }
+ else
+ {
+ earnValue *= pilotScaleVar
+ ownValue *= pilotScaleVar
+ }
+
+ Remote_CallFunction_NonReplay( targetPlayer, "ServerCallback_ScoreEvent", event.eventId, event.pointValue, event.displayType, associatedHandle, ownValue, earnValue )
+
+ if ( event.displayType & eEventDisplayType.CALLINGCARD ) // callingcardevents are shown to all players
+ {
+ foreach ( entity player in GetPlayerArray() )
+ {
+ if ( player == targetPlayer ) // targetplayer already gets this in the scorevent callback
+ continue
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_CallingCardEvent", event.eventId, associatedHandle )
+ }
+ }
+
+ if ( ScoreEvent_HasConversation( event ) )
+ PlayFactionDialogueToPlayer( event.conversation, targetPlayer )
+}
+
+void function ScoreEvent_PlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ // reset killstreaks and stuff
+ victim.s.currentKillstreak = 0
+ victim.s.lastKillTime = 0.0
+ victim.s.currentTimedKillstreak = 0
+
+ victim.p.numberOfDeathsSinceLastKill++ // this is reset on kill
+
+ // have to do this early before we reset victim's player killstreaks
+ // nemesis when you kill a player that is dominating you
+ if ( attacker.IsPlayer() && attacker in victim.p.playerKillStreaks && victim.p.playerKillStreaks[ attacker ] == NEMESIS_KILL_REQUIREMENT )
+ AddPlayerScore( attacker, "Nemesis" )
+
+ // reset killstreaks on specific players
+ foreach ( entity killstreakPlayer, int numKills in victim.p.playerKillStreaks )
+ delete victim.p.playerKillStreaks[ killstreakPlayer ]
+
+ if ( victim.IsTitan() )
+ ScoreEvent_TitanKilled( victim, attacker, damageInfo )
+
+ if ( !attacker.IsPlayer() )
+ return
+
+
+ // pilot kill
+ AddPlayerScore( attacker, "KillPilot", victim )
+
+ // headshot
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_HEADSHOT )
+ AddPlayerScore( attacker, "Headshot", victim )
+
+ // first strike
+ if ( !file.firstStrikeDone )
+ {
+ file.firstStrikeDone = true
+ AddPlayerScore( attacker, "FirstStrike", attacker )
+ }
+
+ // comeback
+ if ( attacker.p.numberOfDeathsSinceLastKill >= COMEBACK_DEATHS_REQUIREMENT )
+ {
+ AddPlayerScore( attacker, "Comeback" )
+ attacker.p.numberOfDeathsSinceLastKill = 0
+ }
+
+
+ // untimed killstreaks
+ attacker.s.currentKillstreak++
+ if ( attacker.s.currentKillstreak == 3 )
+ AddPlayerScore( attacker, "KillingSpree" )
+ else if ( attacker.s.currentKillstreak == 5 )
+ AddPlayerScore( attacker, "Rampage" )
+
+ // increment untimed killstreaks against specific players
+ if ( !( victim in attacker.p.playerKillStreaks ) )
+ attacker.p.playerKillStreaks[ victim ] <- 1
+ else
+ attacker.p.playerKillStreaks[ victim ]++
+
+ // dominating
+ if ( attacker.p.playerKillStreaks[ victim ] == DOMINATING_KILL_REQUIREMENT )
+ AddPlayerScore( attacker, "Dominating" )
+
+
+ // timed killstreaks
+ if ( Time() - attacker.s.lastKillTime <= CASCADINGKILL_REQUIREMENT_TIME )
+ {
+ attacker.s.currentTimedKillstreak++
+
+ if ( attacker.s.currentTimedKillstreak == DOUBLEKILL_REQUIREMENT_KILLS )
+ AddPlayerScore( attacker, "DoubleKill" )
+ else if ( attacker.s.currentTimedKillstreak == TRIPLEKILL_REQUIREMENT_KILLS )
+ AddPlayerScore( attacker, "TripleKill" )
+ else if ( attacker.s.currentTimedKillstreak == MEGAKILL_REQUIREMENT_KILLS )
+ AddPlayerScore( attacker, "MegaKill" )
+ }
+ else
+ attacker.s.currentTimedKillstreak = 0 // reset if a kill took too long
+
+ attacker.s.lastKillTime = Time()
+}
+
+void function ScoreEvent_TitanDoomed( entity titan, entity attacker, var damageInfo )
+{
+ // will this handle npc titans with no owners well? i have literally no idea
+
+ if ( titan.IsNPC() )
+ AddPlayerScore( attacker, "DoomAutoTitan", titan )
+ else
+ AddPlayerScore( attacker, "DoomTitan", titan )
+}
+
+void function ScoreEvent_TitanKilled( entity victim, entity attacker, var damageInfo )
+{
+ // will this handle npc titans with no owners well? i have literally no idea
+ if ( !attacker.IsPlayer() )
+ return
+
+ if ( attacker.IsTitan() )
+ AddPlayerScore( attacker, "TitanKillTitan", victim.GetTitanSoul().GetOwner() )
+ else
+ AddPlayerScore( attacker, "KillTitan", victim.GetTitanSoul().GetOwner() )
+}
+
+void function ScoreEvent_NPCKilled( entity victim, entity attacker, var damageInfo )
+{
+ AddPlayerScore( attacker, ScoreEventForNPCKilled( victim, damageInfo ), victim )
+}
+
+
+
+void function ScoreEvent_SetEarnMeterValues( string eventName, float earned, float owned, float coreScale = 1.0 )
+{
+ ScoreEvent event = GetScoreEvent( eventName )
+ event.earnMeterEarnValue = earned
+ event.earnMeterOwnValue = owned
+ event.coreMeterScalar = coreScale
+}
+
+void function ScoreEvent_SetupEarnMeterValuesForMixedModes() // mixed modes in this case means modes with both pilots and titans
+{
+ // todo needs earn/overdrive values
+ // player-controlled stuff
+ ScoreEvent_SetEarnMeterValues( "KillPilot", 0.0, 0.05 )
+ ScoreEvent_SetEarnMeterValues( "KillTitan", 0.0, 0.15 )
+ ScoreEvent_SetEarnMeterValues( "TitanKillTitan", 0.0, 0.0 ) // unsure
+ ScoreEvent_SetEarnMeterValues( "PilotBatteryStolen", 0.0, 0.35 )
+
+ // ai
+ ScoreEvent_SetEarnMeterValues( "KillGrunt", 0.0, 0.02, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "KillSpectre", 0.0, 0.02, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "LeechSpectre", 0.0, 0.02 )
+ ScoreEvent_SetEarnMeterValues( "KillStalker", 0.0, 0.02, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "KillSuperSpectre", 0.0, 0.1, 0.5 )
+}
+
+void function ScoreEvent_SetupEarnMeterValuesForTitanModes()
+{
+ // todo needs earn/overdrive values
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_serverflags.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_serverflags.nut
new file mode 100644
index 00000000..a665463d
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_serverflags.nut
@@ -0,0 +1,35 @@
+untyped
+
+globalize_all_functions
+
+function GiveServerFlag( player, passive )
+{
+ if ( !( player.serverFlags & passive ) )
+ {
+ player.serverFlags = player.serverFlags | passive
+ }
+
+ // enter/exit functions for specific passives
+ switch ( passive )
+ {
+ }
+}
+
+function TakeServerFlag( player, passive )
+{
+ if ( !PlayerHasServerFlag( player, passive ) )
+ return
+
+ player.serverFlags = player.serverFlags & ( ~passive )
+
+ // enter/exit functions for specific passives
+ switch ( passive )
+ {
+ }
+
+}
+
+bool function PlayerHasServerFlag( player, passive )
+{
+ return bool( player.serverFlags & passive )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_sniper_spectres.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_sniper_spectres.nut
new file mode 100644
index 00000000..ce513259
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_sniper_spectres.nut
@@ -0,0 +1,485 @@
+untyped
+
+const MAXNODES_PER_SNIPERSPOT = 4
+const MAX_SNIPERSPOTS = 30 // for speed of iterating through the array
+const SNIPERSPOT_RADIUSCHECK = 200
+const SNIPERSPOT_HEIGHTCHECK = 160
+const SNIPERNODE_TOOCLOSE_SQR = 2500//50x50
+
+global function SniperSpectres_Init
+global function TowerDefense_AddSniperLocation
+global function Dev_AddSniperLocation
+global function DebugDrawSniperLocations
+global function Sniper_MoveToNewLocation
+global function Sniper_FreeSniperNodeOnDeath
+global function SniperCloak
+global function SniperDeCloak
+
+
+function SniperSpectres_Init()
+{
+ FlagInit( "TD_SniperLocationsInit" )
+
+ level.TowerDefense_SniperNodes <- []
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+}
+
+void function EntitiesDidLoad()
+{
+ thread SniperLocationsInit()
+}
+
+/************************************************************************************************\
+
+######## ####### ####### ## ######
+ ## ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ######
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ## ##
+ ## ####### ####### ######## ######
+
+\************************************************************************************************/
+function TowerDefense_AddSniperLocation( origin, yaw, heightCheck = SNIPERSPOT_HEIGHTCHECK )
+{
+ Assert( !Flag( "TD_SniperLocationsInit" ), "sniper locations added too late" )
+ Assert( ( level.TowerDefense_SniperNodes.len() < MAX_SNIPERSPOTS ), "adding too many snper locations, max is " + MAX_SNIPERSPOTS )
+
+ local loc = CreateSniperLocation( origin, yaw, heightCheck )
+
+ level.TowerDefense_SniperNodes.append( loc )
+}
+
+function Dev_AddSniperLocation( origin, yaw, heightCheck = SNIPERSPOT_HEIGHTCHECK )
+{
+ thread __AddSniperLocationInternal( origin, yaw, heightCheck )
+}
+
+function __AddSniperLocationInternal( origin, yaw, heightCheck )
+{
+ local loc = CreateSniperLocation( origin, yaw, heightCheck )
+ SniperLocationSetup( loc )
+ DebugDrawSingleSniperLocation( loc, 4.0 )
+}
+
+function DebugDrawSniperLocations()
+{
+ foreach ( loc in level.TowerDefense_SniperNodes )
+ DebugDrawSingleSniperLocation( loc, 600.0 )
+}
+
+function DebugDrawSingleSniperLocation( loc, float time )
+{
+ if ( !loc.maxGuys )
+ {
+ DebugDrawSniperSpot( expect vector( loc.pos ), [ 32.0, 40.0, 48.0 ], 255, 0, 0, time, loc.yaw )
+ return
+ }
+
+ DebugDrawSniperSpot( expect vector( loc.pos ), [ 28.0 ], 20, 20, 20, time, loc.yaw )
+
+ foreach ( node in loc.coverNodes )
+ DebugDrawSniperSpot( expect vector( node.pos ), [ 16.0, 24.0, 32.0 ], 50, 50, 255, time, null, loc.pos )
+
+ foreach ( node in loc.extraNodes )
+ DebugDrawSniperSpot( expect vector( node.pos ), [ 14.0, 22.0, 30.0 ], 255, 0, 255, time, null, loc.pos )
+}
+
+function DebugDrawSniperSpot( vector pos, array<float> radii, int r, int g, int b, float time, yaw = null, pos2 = null )
+{
+ foreach ( radius in radii )
+ DebugDrawCircle( pos, Vector( 0, 0, 0 ), radius, r, g, b, true, time )
+
+ if ( yaw != null )
+ {
+ local angles = Vector( 0, yaw, 0 )
+ local forward = AnglesToForward( angles )
+ local right = AnglesToRight( angles )
+ local length = radii[ radii.len() - 1 ]
+ local endPos = pos + ( forward * ( length * 1.75 ) )
+ local rightPos = pos + ( right * length )
+ local leftPos = pos + ( right * -length )
+ DebugDrawLine( pos, endPos, r, g, b, true, time )
+ DebugDrawLine( rightPos, endPos, r, g, b, true, time )
+ DebugDrawLine( leftPos, endPos, r, g, b, true, time )
+
+ local ring = GetDesirableRing( pos, yaw )
+ DebugDrawCircle( expect vector( ring.pos ), Vector( 0, 0, 0 ), expect float( ring.radius ), r, g, b, true, time )
+ }
+
+ if ( pos2 != null )
+ DebugDrawLine( pos, pos2, r, g, b, true, time )
+}
+
+/************************************************************************************************\
+
+######## ### ######## ## ## #### ## ## ######
+## ## ## ## ## ## ## ## ### ## ## ##
+## ## ## ## ## ## ## ## #### ## ##
+######## ## ## ## ######### ## ## ## ## ## ####
+## ######### ## ## ## ## ## #### ## ##
+## ## ## ## ## ## ## ## ### ## ##
+## ## ## ## ## ## #### ## ## ######
+
+\************************************************************************************************/
+//HACK -> this should probably move into code
+function Sniper_MoveToNewLocation( entity sniper )
+{
+ sniper.EndSignal( "OnDeath" )
+ sniper.EndSignal( "OnDestroy" )
+
+ delaythread( 2 ) SniperCloak( sniper )
+
+ //go searching for nodes that are up somewhere
+ local sniperNode = GetRandomSniperNodeWithin( sniper, 3000 )
+
+ Sniper_FreeSniperNode( sniper )//free his current node
+ Sniper_TakeSniperNode( sniper, sniperNode )
+ Sniper_AssaultLocation( sniper, sniperNode )
+
+ WaitSignal( sniper, "OnFinishedAssault", "OnDeath", "OnDestroy", "AssaultTimeOut" )
+
+ SniperDeCloak( sniper )
+}
+
+function Sniper_TakeSniperNode( sniper, sniperNode )
+{
+ Assert( sniper.s.sniperNode == null ) // didn't free the last one
+ sniper.s.sniperNode = sniperNode
+
+ Assert( sniperNode.locked == false )// someone else already has it?
+ sniperNode.locked = true
+
+ local loc = sniperNode.loc
+ loc.numGuys++
+}
+
+function Sniper_FreeSniperNode( sniper )
+{
+ local sniperNode = sniper.s.sniperNode
+ if ( sniperNode == null )
+ return
+
+ sniper.s.sniperNode = null
+
+ local loc = sniperNode.loc
+ loc.numGuys--
+ sniperNode.locked = false
+}
+
+function Sniper_FreeSniperNodeOnDeath( entity sniper )
+{
+ sniper.WaitSignal( "OnDeath" )
+ Sniper_FreeSniperNode( sniper )
+}
+
+void function SniperCloak( entity sniper )
+{
+ if ( !IsAlive( sniper ) )
+ return
+
+ if ( !sniper.CanCloak() )
+ return
+
+ sniper.kv.allowshoot = 0
+ sniper.SetCloakDuration( 3.0, -1, 0 )
+ sniper.Minimap_Hide( TEAM_IMC, null )
+ sniper.Minimap_Hide( TEAM_MILITIA, null )
+}
+
+void function SniperDeCloak( entity sniper )
+{
+ if ( !IsAlive( sniper ) )
+ return
+
+ sniper.kv.allowshoot = 1
+ sniper.SetCloakDuration( 0, 0, 1.5 )
+ sniper.Minimap_AlwaysShow( TEAM_IMC, null )
+ sniper.Minimap_AlwaysShow( TEAM_MILITIA, null )
+}
+
+function Sniper_AssaultLocation( sniper, sniperNode )
+{
+ Assert( sniper.s.sniperNode == sniperNode ) // didn't get the right one
+
+ local origin = sniperNode.pos
+ local loc = sniperNode.loc
+ local angles = Vector( 0, loc.yaw, 0 )
+
+ Assert( "assaultPoint" in sniper.s )
+ sniper.AssaultPoint( origin )
+ sniper.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+}
+
+function GetRandomSniperNodeWithin( sniper, maxDist )
+{
+ Assert( level.TowerDefense_SniperNodes.len() >= 2 )
+
+ local origin = sniper.GetOrigin()
+ local locations = SniperNodeWithin( level.TowerDefense_SniperNodes, origin, maxDist )
+
+ if ( locations.len() )
+ locations.randomize()
+
+ local goalNode = FindFreeSniperNode( locations )
+ if ( goalNode != null )
+ return goalNode
+
+ //if we get here it's because there are no free nodes within the maxDist
+ locations = SniperNodeClosest( level.TowerDefense_SniperNodes, origin )
+
+ goalNode = FindFreeSniperNode( locations )
+ Assert ( goalNode != null )
+
+ return goalNode
+}
+
+function FindFreeSniperNode( locations )
+{
+ foreach( loc in locations )
+ {
+ //is if filled up?
+ if ( loc.numGuys >= loc.maxGuys )
+ continue
+
+ //grab the first unlocked cover node
+ foreach ( node in loc.coverNodes )
+ {
+ if ( node.locked )
+ continue
+
+ return node
+ }
+
+ //ok then grab the first unlocked extra node
+ foreach ( node in loc.extraNodes )
+ {
+ if ( node.locked )
+ continue
+
+ return node
+ }
+ }
+
+ return null
+}
+
+//ArrayWithin() copy
+function SniperNodeWithin( Array, origin, maxDist )
+{
+ maxDist *= maxDist
+
+ local resultArray = []
+ foreach( loc in Array )
+ {
+ local testspot = null
+ testspot = loc.pos
+
+ local dist = DistanceSqr( origin, testspot )
+ if ( dist <= maxDist )
+ resultArray.append( loc )
+ }
+ return resultArray
+}
+
+//ArrayClosest() copy
+function SniperNodeClosest( Array, origin )
+{
+ Assert( type( Array ) == "array" )
+ local allResults = SniperArrayDistanceResults( Array, origin )
+
+ allResults.sort( SniperArrayDistanceCompare )
+
+ local returnEntities = []
+
+ foreach ( index, result in allResults )
+ {
+ returnEntities.insert( index, result.loc )
+ }
+
+ // the actual distances aren't returned
+ return returnEntities
+}
+
+function SniperArrayDistanceResults( Array, origin )
+{
+ Assert( type( Array ) == "array" )
+ local allResults = []
+
+ foreach ( loc in Array )
+ {
+ local results = {}
+ local testspot = null
+
+ testspot = loc.pos
+
+ results.distanceSqr <- LengthSqr( testspot - origin )
+ results.loc <- loc
+ allResults.append( results )
+ }
+
+ return allResults
+}
+
+function SniperArrayDistanceCompare( a, b )
+{
+ if ( a.distanceSqr > b.distanceSqr )
+ return 1
+ else if ( a.distanceSqr < b.distanceSqr )
+ return -1
+
+ return 0;
+}
+
+
+
+/************************************************************************************************\
+
+######## ######## ######## ###### ### ## ######
+## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ##
+######## ######## ###### ####### ## ## ## ## ##
+## ## ## ## ## ######### ## ##
+## ## ## ## ## ## ## ## ## ## ##
+## ## ## ######## ###### ## ## ######## ######
+
+\************************************************************************************************/
+function CreateSniperLocation( origin, yaw, heightCheck )
+{
+ local loc = {}
+ loc.pos <- origin
+ loc.yaw <- yaw
+ loc.heightCheck <- heightCheck
+ loc.numGuys <- 0
+ loc.maxGuys <- 0
+ loc.coverNodes <- []
+ loc.extraNodes <- []
+
+ return loc
+}
+
+function CreateSniperNode( location, origin )
+{
+ local node = {}
+ node.locked <- false
+ node.loc <- location
+ node.pos <- origin
+
+ return node
+}
+
+function SniperLocationsInit()
+{
+ FlagSet( "TD_SniperLocationsInit" )
+ local time = Time()
+
+ foreach ( loc in level.TowerDefense_SniperNodes )
+ {
+ SniperLocationSetup( loc )
+ wait 0.1 //space out all the slow stuff so it doesn't happen on the same frame
+ }
+
+ printt( "<<<<<***********************************************************>>>>>" )
+ printt( "SniperLocationsInit() took ", Time() - time, " seconds to complete" )
+ printt( "<<<<<***********************************************************>>>>>" )
+}
+
+function SniperLocationSetup( loc )
+{
+ array<vector> extraPos = GetNeighborPositionsAroundSniperLocation( expect vector( loc.pos ), expect float( loc.yaw ), expect float( loc.heightCheck ), MAXNODES_PER_SNIPERSPOT )
+ foreach ( origin in extraPos )
+ {
+ local node = CreateSniperNode( loc, origin )
+ loc.extraNodes.append( node )
+ }
+
+ loc.maxGuys = loc.coverNodes.len() + loc.extraNodes.len()
+ if ( loc.maxGuys == 0 )
+ printt( "sniper spot at [ " + loc.pos + " ] has no nodes around it within " + SNIPERSPOT_RADIUSCHECK + " units." )
+ Assert( loc.maxGuys <= MAXNODES_PER_SNIPERSPOT )
+}
+
+array<vector> function GetNeighborPositionsAroundSniperLocation( vector pos, float yaw, float heightCheck, int max )
+{
+ local height = pos.z
+ local isSpectre = true
+ local radius = SNIPERSPOT_RADIUSCHECK
+ array<vector> goalPos = []
+
+ array<vector> neighborPos = NavMesh_GetNeighborPositions( pos, HULL_HUMAN, MAXNODES_PER_SNIPERSPOT )
+ neighborPos = SortPositionsByClosestToPos( neighborPos, pos, yaw )
+ foreach ( origin in neighborPos )
+ {
+ if ( fabs( origin.z - height ) > heightCheck )
+ continue
+
+ if ( !IsMostDesireablePos( origin, pos, yaw ) )
+ continue
+
+ if ( IsPosTooCloseToOtherPositions( origin, goalPos ) )
+ continue
+
+ goalPos.append( origin )
+ if ( goalPos.len() == max )
+ break
+ }
+
+ return goalPos
+}
+
+array<vector> function SortPositionsByClosestToPos( array<vector> neighborPos, vector pos, float yaw )
+{
+ table ring = GetDesirableRing( pos, yaw )
+ vector testPos = expect vector( ring.pos )
+
+ array<vector> returnOrigins = ArrayClosestVector( neighborPos, testPos )
+ return returnOrigins
+}
+
+bool function IsPosTooCloseToOtherPositions( vector testPos, array<vector> positions )
+{
+ foreach ( pos in positions )
+ {
+ if ( DistanceSqr( pos, testPos ) <= SNIPERNODE_TOOCLOSE_SQR )
+ return true
+ }
+ return false
+}
+
+function IsMostDesireablePos( testPos, sniperPos, yaw )
+{
+ /*
+ what this function does is actually draw a circle out infront of the position based on the yaw.
+ then it checks to see if the node is within that circle.
+ Since most sniper positions are on EDGES of buildings, windows, etc, this techinique helps grab more nodes along the edge
+ */
+
+ table ring = GetDesirableRing( sniperPos, yaw )
+ local radiusSqr = ring.radius * ring.radius
+
+ if ( Distance2DSqr( testPos, ring.pos ) <= radiusSqr )
+ return true
+
+ return false
+}
+
+table function GetDesirableRing( pos, yaw )
+{
+ local dist = 200
+ local radius = 300
+
+ local vec = AnglesToForward( Vector( 0, yaw, 0 ) ) * dist
+ local testPos = pos + vec
+
+ table ring = {}
+ ring.pos <- testPos
+ ring.radius <- radius
+ return ring
+}
+
+
+
+
+
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_spawn_functions.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_spawn_functions.nut
new file mode 100644
index 00000000..3d9b84f3
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_spawn_functions.nut
@@ -0,0 +1,60 @@
+untyped
+
+global function SpawnFunctions_Init
+
+function SpawnFunctions_Init()
+{
+ if ( IsLobby() )
+ return
+
+ // shared OnSpawned callbacks
+ AddSpawnCallback( "script_mover", SpawnScriptMover )
+ AddSpawnCallback( "path_track", SpawnPathTrack )
+ AddSpawnCallback( "info_hint", SpawnInfoHint )
+ AddDeathCallback( "npc_titan", EmptyDeathCallback ) // so death info gets sent to client
+
+ // Arc Cannon Targets
+ foreach ( classname, val in ArcCannonTargetClassnames )
+ {
+ AddSpawnCallback( classname, AddToArcCannonTargets )
+ }
+
+ foreach ( classname, val in ProximityTargetClassnames )
+ {
+ AddSpawnCallback( classname, AddToProximityTargets )
+ }
+}
+
+void function EmptyDeathCallback( entity _1, var _2 )
+{
+}
+
+
+void function SpawnPathTrack( entity node )
+{
+ if ( node.HasKey( "WaitSignal" ) )
+ RegisterSignal( node.kv.WaitSignal )
+
+ if ( node.HasKey( "SendSignal" ) )
+ RegisterSignal( node.kv.SendSignal )
+
+ if ( node.HasKey( "WaitFlag" ) )
+ FlagInit( expect string( node.kv.WaitFlag ) )
+
+ if ( node.HasKey( "SetFlag" ) )
+ FlagInit( expect string( node.kv.SetFlag ) )
+}
+
+void function SpawnScriptMover( entity ent )
+{
+ if ( ent.HasKey( "custom_health" ) )
+ {
+ //printt( "setting health on " + ent + " to " + ent.kv.custom_health.tointeger() )
+ ent.SetHealth( ent.kv.custom_health.tointeger() )
+ }
+}
+
+void function SpawnInfoHint( entity ent )
+{
+ Assert( !ent.HasKey( "hotspot" ) || ent.kv.hotspot.tolower() in level.hotspotHints, "info_hint at " + ent.GetOrigin() + " has unknown hotspot hint: " + ent.kv.hotspot.tolower() )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_spectre_rack.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_spectre_rack.nut
new file mode 100644
index 00000000..a76c0fc9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_spectre_rack.nut
@@ -0,0 +1,395 @@
+
+global function SpectreRack_Init
+global function IsStalkerRack
+global function SpawnFromStalkerRack
+global function AddSpectreRackCallback
+global function GetSpectreRackFromEnt
+global function SetupSpectreRack
+global function SpectreRackActivationEffects
+global function TrackFriendlySpectre
+
+const FX_GREEN_GLOW = $"P_spectre_rack_glow_idle"
+const WARNING_LIGHT_BLINK = $"warning_light_orange_blink"
+const SPECTRE_RACK_ACHIEVEMENT_COUNT = 6
+
+global struct SpectreRackSpectre
+{
+ string attachName
+ entity dummyModel
+ entity glowFX
+ entity spawner
+}
+
+global struct SpectreRack
+{
+ entity rackEnt
+ array<SpectreRackSpectre> spectreRackSpectres
+}
+
+struct
+{
+ int playersSpectreArrayIdx
+ array<string> spectreRackTypes
+ array<SpectreRack> spectreRacks
+ array<void functionref( entity, entity )> callbackFuncs
+} file
+
+void function AddSpectreRackCallback( void functionref( entity, entity ) func )
+{
+ Assert( !file.callbackFuncs.contains( func ) )
+ file.callbackFuncs.append( func )
+}
+
+void function SpectreRack_Init()
+{
+ if ( reloadingScripts )
+ return
+
+ file.spectreRackTypes.append( "npc_spectre_rack_wall" )
+ file.spectreRackTypes.append( "npc_spectre_rack_multi" )
+ file.spectreRackTypes.append( "npc_spectre_rack_triple" )
+ //file.spectreRackTypes.append( "npc_spectre_rack_portable" )
+ //file.spectreRackTypes.append( "npc_spectre_rack_palette" )
+
+ PrecacheParticleSystem( FX_GREEN_GLOW )
+ PrecacheParticleSystem( WARNING_LIGHT_BLINK )
+
+ foreach ( string rackType in file.spectreRackTypes )
+ {
+ AddSpawnCallbackEditorClass( "prop_dynamic", rackType, SetupSpectreRack )
+ }
+
+ if ( IsSingleplayer() )
+ {
+ file.playersSpectreArrayIdx = CreateScriptManagedEntArray()
+ AddSpectreRackCallback( TrySpectreAchievement )
+ }
+}
+
+bool function IsStalkerRack( entity ent )
+{
+ if ( !ent.HasKey( "editorclass" ) )
+ return false
+ string editorclass = ent.GetValueForKey( "editorclass" )
+ return file.spectreRackTypes.contains( editorclass )
+}
+
+void function SetupSpectreRack( entity rack )
+{
+ SpectreRack spectreRack
+ spectreRack.rackEnt = rack
+
+ // Get attach point info from the model being used
+ while( true )
+ {
+ int attachIndex = spectreRack.spectreRackSpectres.len() + 1
+ string attachment = "spectre_attach_" + attachIndex
+
+ int id = rack.LookupAttachment( attachment )
+ if ( id == 0 )
+ break
+
+ SpectreRackSpectre spectreRackSpectre
+ spectreRackSpectre.attachName = attachment
+ spectreRack.spectreRackSpectres.append( spectreRackSpectre )
+ }
+
+ // Get linked spawner
+ array<entity> linkedEnts = rack.GetLinkEntArray()
+ int spawnerCount = 0
+ foreach ( index, ent in linkedEnts )
+ {
+ if ( IsSpawner( ent ) )
+ {
+ spectreRack.spectreRackSpectres[index].spawner = ent
+ spawnerCount++
+ }
+ }
+ Assert( spawnerCount == spectreRack.spectreRackSpectres.len(), "Spectre rack " + rack + " at: " + rack.GetOrigin() + " " + rack.GetValueForKey( "editorclass" ) + " must link to exactly " + spectreRack.spectreRackSpectres.len() + " spawner" )
+
+ // Create dummy spectre models to idle on the rack
+ foreach ( spectreRackSpectre in spectreRack.spectreRackSpectres )
+ {
+ int attachID = rack.LookupAttachment( spectreRackSpectre.attachName )
+ vector origin = rack.GetAttachmentOrigin( attachID )
+ vector angles = rack.GetAttachmentAngles( attachID )
+
+ var spawnerKeyValues = spectreRackSpectre.spawner.GetSpawnEntityKeyValues()
+ expect table( spawnerKeyValues )
+ asset model = spectreRackSpectre.spawner.GetSpawnerModelName()
+ int skin
+ if ( "skin" in spawnerKeyValues )
+ {
+ skin = int( spawnerKeyValues.skin )
+ }
+
+ string idleAnim = GetIdleAnimForSpawner( spectreRackSpectre.spawner )
+ entity dummySpectre = CreatePropDynamic( model, origin, angles )
+ dummySpectre.SetSkin( skin )
+ dummySpectre.SetParent( rack, spectreRackSpectre.attachName )
+ thread PlayAnimTeleport( dummySpectre, idleAnim, rack, spectreRackSpectre.attachName )
+
+ spectreRackSpectre.dummyModel = dummySpectre
+ }
+
+ // Create effects on the rack
+ if ( !rack.HasKey( "DisableStatusLights" ) || rack.GetValueForKey( "DisableStatusLights" ) == "0" )
+ SpectreRackCreateFx( spectreRack, FX_GREEN_GLOW )
+
+ file.spectreRacks.append( spectreRack )
+}
+
+void function SpawnFromStalkerRack( entity rack, entity activator = null )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ SpectreRack spectreRack = GetSpectreRackFromEnt( rack )
+ Assert( IsValid( spectreRack.rackEnt ) )
+
+ thread SpectreRackActivationEffects( spectreRack )
+ thread SpectreRackActivationSpawners( spectreRack, activator )
+
+ if ( IsValid( activator ) && activator.IsPlayer() )
+ UnlockAchievement( activator, achievements.HACK_STALKERS )
+}
+
+void function SpectreRackActivationEffects( SpectreRack spectreRack )
+{
+ EndSignal( spectreRack, "OnDestroy" )
+ EndSignal( spectreRack.rackEnt, "OnDestroy" )
+
+ OnThreadEnd
+ (
+ function() : ( spectreRack )
+ {
+ if ( IsValid( spectreRack ) )
+ SpectreRackDestroyFx( spectreRack )
+ }
+ )
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, spectreRack.rackEnt.GetOrigin() + Vector( 0, 0, 72), "colony_spectre_initialize_beep" )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, spectreRack.rackEnt.GetOrigin() + Vector( 0, 0, 72), "corporate_spectrerack_activate" )
+
+ SpectreRackDestroyFx( spectreRack )
+ SpectreRackCreateFx( spectreRack, WARNING_LIGHT_BLINK )
+
+ // Let the flash FX linger a bit longer, then the thread end will kill the fx
+ wait 6
+}
+
+void function SpectreRackActivationSpawners( SpectreRack spectreRack, entity activator = null )
+{
+ EndSignal( spectreRack, "OnDestroy" )
+ EndSignal( spectreRack.rackEnt, "OnDestroy" )
+
+ array<int> spawnOrder
+ for ( int i = 0 ; i < spectreRack.spectreRackSpectres.len() ; i++ )
+ {
+ spawnOrder.append(i)
+ }
+
+ spawnOrder.randomize()
+
+ foreach ( int index in spawnOrder )
+ {
+ thread SpectreRackReleaseSpectre( spectreRack, index, activator )
+
+ wait RandomFloatRange( 0.0, 0.25 )
+ }
+}
+
+void function SpectreRackReleaseSpectre( SpectreRack spectreRack, int index, entity activator = null )
+{
+ SpectreRackSpectre spectreRackSpectre = spectreRack.spectreRackSpectres[ index ]
+ if ( !IsValid( spectreRackSpectre.dummyModel ) )
+ return
+
+ entity rackEnt = spectreRack.rackEnt
+
+ entity dummy = spectreRackSpectre.dummyModel
+ Assert( IsValid ( dummy ) )
+
+ entity spawner = spectreRackSpectre.spawner
+ Assert( IsValid ( spawner ) )
+
+ EndSignal( spectreRackSpectre, "OnDestroy" )
+ EndSignal( rackEnt, "OnDestroy" )
+
+ var spawnerKeyValues = spawner.GetSpawnEntityKeyValues()
+ expect table( spawnerKeyValues )
+ if ( "script_delay" in spawnerKeyValues )
+ {
+ float delay = float( spawnerKeyValues.script_delay )
+ wait delay
+ }
+
+ if ( IsValid( dummy ) )
+ dummy.Destroy()
+ entity spectre = spawner.SpawnEntity()
+ DispatchSpawn( spectre )
+ spectre.ContextAction_SetBusy()
+
+ if ( IsValid( activator ) )
+ {
+ SetTeam( spectre, activator.GetTeam() )
+ //spectre.DisableBehavior( "Assault" )
+ /*
+ if ( activator.IsPlayer() )
+ {
+ NPCFollowsPlayer( spectre, activator )
+ }
+ else if ( activator.IsNPC() )
+ {
+ NPCFollowsNPC( spectre, activator )
+ }
+ */
+ }
+
+ string deployAnim = GetDeployAnimForSpawner( spectreRackSpectre.spawner )
+ string idleAnim = GetIdleAnimForSpawner( spectreRackSpectre.spawner )
+
+ EndSignal( spectre, "OnDeath" )
+
+ string attachment = spectreRackSpectre.attachName
+ spectre.SetParent( rackEnt, attachment )
+ thread PlayAnimTeleport( spectre, idleAnim, rackEnt, attachment )
+
+ if ( CoinFlip() )
+ EmitSoundOnEntity( spectre, "diag_stalker_generic" )
+
+ spectre.SetNoTarget( true )
+ waitthread PlayAnim( spectre, deployAnim, rackEnt, attachment )
+ spectre.ClearParent()
+ float yaw = spectre.GetAngles().y
+ spectre.SetAngles( <0,yaw,0> )//spectres released on moving platforms angle correctly
+
+ foreach ( func in file.callbackFuncs )
+ {
+ thread func( spectre, activator )
+ }
+
+ spectre.SetTitle( spectre.GetSettingTitle() )
+ Highlight_SetFriendlyHighlight( spectre, "sp_friendly_pilot" )
+ ShowName( spectre )
+ wait 1
+ spectre.SetNoTarget( false )
+ spectre.ContextAction_ClearBusy()
+}
+
+void function SpectreRackCreateFx( SpectreRack spectreRack, asset fxName )
+{
+ for ( int i = 0 ; i < spectreRack.spectreRackSpectres.len() ; i++ )
+ {
+ string attachment = "glow_" + i
+ int id = spectreRack.rackEnt.LookupAttachment( attachment )
+ Assert( id != 0, "Missing attachment \"" + attachment + "\" in model " + spectreRack.rackEnt.GetModelName() )
+
+ entity fx = PlayLoopFXOnEntity( fxName, spectreRack.rackEnt, attachment )
+ Assert( !IsValid( spectreRack.spectreRackSpectres[i].glowFX ) )
+ spectreRack.spectreRackSpectres[i].glowFX = fx
+ }
+}
+
+void function SpectreRackDestroyFx( SpectreRack spectreRack )
+{
+ foreach ( spectreRackSpectre in spectreRack.spectreRackSpectres )
+ {
+ entity fx = spectreRackSpectre.glowFX
+ if ( !IsValid_ThisFrame( fx ) )
+ continue
+ fx.ClearParent()
+ fx.Destroy()
+ }
+}
+
+SpectreRack function GetSpectreRackFromEnt( entity rack )
+{
+ // Get the spectre rack struct from the placed entity
+ foreach ( SpectreRack rackStruct in file.spectreRacks )
+ {
+ if ( rackStruct.rackEnt == rack )
+ return rackStruct
+ }
+ SpectreRack rackStruct
+ return rackStruct
+}
+
+string function GetIdleAnimForSpawner( entity spawner )
+{
+ string idleAnim
+ string spawnClassName = GetEditorClass( spawner )
+ if ( spawnClassName == "" )
+ spawnClassName = spawner.GetSpawnEntityClassName()
+
+ switch( spawnClassName )
+ {
+ case "npc_stalker":
+ case "npc_stalker_zombie":
+ case "npc_stalker_zombie_mossy":
+ idleAnim = "st_medbay_idle_armed"
+ break
+ case "npc_spectre":
+ idleAnim = "sp_med_bay_dropidle_A"
+ break
+ default:
+ idleAnim = "st_medbay_idle_armed"
+ break
+ }
+
+ return idleAnim
+}
+
+string function GetDeployAnimForSpawner( entity spawner )
+{
+ string deployAnim
+ string spawnClassName = GetEditorClass( spawner )
+ if ( spawnClassName == "" )
+ spawnClassName = spawner.GetSpawnEntityClassName()
+
+ switch( spawnClassName )
+ {
+ case "npc_stalker":
+ case "npc_stalker_zombie":
+ case "npc_stalker_zombie_mossy":
+ deployAnim = "st_medbay_drop_armed"
+ break
+ case "npc_spectre_suicide":
+ deployAnim = "sp_med_bay_drop_unarmed"
+ break
+ case "npc_spectre":
+ deployAnim = "sp_med_bay_drop_A"
+ break
+ default:
+ deployAnim = "st_medbay_drop_armed"
+ break
+ }
+
+ return deployAnim
+}
+
+void function TrySpectreAchievement( entity npc, entity activator )
+{
+ if ( !IsValid( activator ) )
+ return
+
+ if ( !activator.IsPlayer() )
+ return
+
+ TrackFriendlySpectre( npc, activator )
+}
+
+void function TrackFriendlySpectre( entity npc, entity player )
+{
+ if ( player.GetTeam() != npc.GetTeam() )
+ return
+
+ if ( IsStalker( npc ) )
+ AddToScriptManagedEntArray( file.playersSpectreArrayIdx, npc )
+ else
+ return
+
+ printt( "Achievment tracking stalker: " + GetScriptManagedEntArrayLen( file.playersSpectreArrayIdx ) )
+ if ( GetScriptManagedEntArrayLen( file.playersSpectreArrayIdx ) >= SPECTRE_RACK_ACHIEVEMENT_COUNT )
+ {
+ UnlockAchievement( player, achievements.HACK_ROBOTS )
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut
new file mode 100644
index 00000000..0e8b58f4
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut
@@ -0,0 +1,78 @@
+global function Stats_Init
+global function AddStatCallback
+global function Stats_SaveStatDelayed
+global function PlayerStat_GetCurrentInt
+global function PlayerStat_GetCurrentFloat
+global function UpdatePlayerStat
+global function IncrementPlayerDidPilotExecutionWhileCloaked
+global function UpdateTitanDamageStat
+global function UpdateTitanWeaponDamageStat
+global function UpdateTitanCoreEarnedStat
+global function PreScoreEventUpdateStats
+global function PostScoreEventUpdateStats
+global function Stats_OnPlayerDidDamage
+
+void function Stats_Init()
+{
+
+}
+
+void function AddStatCallback(string statCategory, string statAlias, string statSubAlias, void functionref(entity, float, string) callback, string subRef)
+{
+
+}
+
+void function Stats_SaveStatDelayed(entity player, string statCategory, string statAlias, string statSubAlias)
+{
+
+}
+
+int function PlayerStat_GetCurrentInt(entity player, string statCategory, string statAlias, string statSubAlias)
+{
+ return 0
+}
+
+float function PlayerStat_GetCurrentFloat(entity player, string statCategory, string statAlias, string statSubAlias)
+{
+ return 0
+}
+
+void function UpdatePlayerStat(entity player, string statCategory, string subStat, int count = 0)
+{
+
+}
+
+void function IncrementPlayerDidPilotExecutionWhileCloaked(entity player)
+{
+
+}
+
+void function UpdateTitanDamageStat(entity attacker, float savedDamage, var damageInfo)
+{
+
+}
+
+void function UpdateTitanWeaponDamageStat(entity attacker, float savedDamage, var damageInfo)
+{
+
+}
+
+void function UpdateTitanCoreEarnedStat( entity player, entity titan )
+{
+
+}
+
+void function PreScoreEventUpdateStats(entity attacker, entity ent)
+{
+
+}
+
+void function PostScoreEventUpdateStats(entity attacker, entity ent)
+{
+
+}
+
+void function Stats_OnPlayerDidDamage(entity player, var damageInfo)
+{
+
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_npc.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_npc.nut
new file mode 100644
index 00000000..58285087
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_npc.nut
@@ -0,0 +1,818 @@
+untyped
+
+global function TitanNPC_Init
+
+global function CodeCallback_PlayerRequestClimbInNPCTitan
+global function ResetTitanLoadoutFromPrimary
+
+global function NPCTitanNextMode
+global function NPCTitanInitModeOnPlayerRespawn
+global function SetupAutoTitan
+global function SetupNPC_TitanTitle
+global function SetPlayerPetTitan
+global function AutoTitanChangedEnemy
+global function PlayAutoTitanConversation
+global function CreateTitanModelAndSkinSetup
+global function SetWeaponCooldowns
+
+global function ResetTitanBuildTime
+
+global function CreateNPCTitanFromSettings
+
+global function FreeAutoTitan
+
+global function GetRandomTitanWeapon
+
+global function SpawnTitanBatteryOnDeath
+global function CreateTitanBattery
+
+global function WaitForHotdropToEnd
+global function ResetCoreKillCounter
+
+const TITAN_USE_HOLD_PROMPT = "Hold [USE] to Pilot||Hold [USE] to Rodeo"
+const TITAN_USE_PRESS_PROMPT = "Press [USE] to Pilot||Press [USE] to Rodeo"
+
+const int BATTERY_DROP_BOSS = 4
+
+const float BATTERY_DROP_HEALTH_FRAC_SURE = 0.2
+const float BATTERY_DROP_HEALTH_FRAC_MID = 0.5
+
+const int BATTERY_DROP_MID_CHANCE = 70
+const int BATTERY_DROP_LOW_CHANCE = 40
+
+struct
+{
+ int coreKillCounter = 0
+} file
+
+function TitanNPC_Init()
+{
+ RegisterSignal( "ChangedTitanMode" )
+ RegisterSignal( "PROTO_WeaponPickup" )
+
+ AddSoulDeathCallback( AutoTitanDestroyedCheck )
+
+ #if R1_VGUI_MINIMAP
+ Minimap_PrecacheMaterial( $"vgui/HUD/threathud_titan_friendlyself" )
+ Minimap_PrecacheMaterial( $"vgui/HUD/threathud_titan_friendlyself_guard" )
+ #endif
+
+ if ( IsSingleplayer() )
+ {
+ AddSpawnCallbackEditorClass( "script_ref", "script_titan_battery", SpawnTitanBattery )
+ AddDeathCallback( "npc_titan", SpawnTitanBatteryOnDeath )
+ AddDeathCallback( "npc_titan", TitanAchievementTracking_SP )
+ }
+}
+
+void function AutoTitanDestroyedCheck( entity soul, var damageInfo )
+{
+ entity titan = soul.GetTitan()
+ if ( !IsValid( titan ) )
+ return
+
+ entity player = soul.GetBossPlayer()
+ if ( !IsValid( player ) )
+ return
+
+ SetActiveTitanLoadoutIndex( player, -1 )
+
+ if ( player.GetPetTitan() == titan )
+ player.SetPetTitan( null )
+
+ if ( soul.IsEjecting() )
+ return
+
+ // has another titan?
+ if ( GetPlayerTitanInMap( player ) )
+ return
+
+ switch ( Riff_TitanAvailability() )
+ {
+ case eTitanAvailability.Default:
+ break
+
+ default:
+ if ( !Riff_IsTitanAvailable( player ) )
+ return
+ }
+
+ if ( GAMETYPE == SST )
+ return
+
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == eDamageSourceId.round_end )
+ return
+
+ thread PlayConversationToPlayer( "AutoTitanDestroyed", player )
+}
+
+
+
+//////////////////////////////////////////////////////////
+function SetupNPC_TitanTitle( npcTitan, player )
+{
+ npcTitan.SetBossPlayer( player )
+
+ #if R1_VGUI_MINIMAP
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ npcTitan.Minimap_SetBossPlayerMaterial( $"vgui/HUD/threathud_titan_friendlyself" )
+ break;
+
+ //case eNPCTitanMode.ROAM:
+ // break;
+
+ case eNPCTitanMode.STAY:
+ npcTitan.Minimap_SetBossPlayerMaterial( $"vgui/HUD/threathud_titan_friendlyself_guard" )
+ break;
+ }
+ #endif
+}
+
+//////////////////////////////////////////////////////////
+void function NPCTitanNextMode( entity npcTitan, entity player )
+{
+ entity soul = npcTitan.GetTitanSoul()
+ if ( !SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) && PROTO_AutoTitansDisabled() )
+ return
+
+ NPCTitanDisableCurrentMode( npcTitan, player )
+
+ local mode = player.GetPetTitanMode() + 1
+ if ( mode == eNPCTitanMode.MODE_COUNT )
+ mode = eNPCTitanMode.FOLLOW
+
+ player.SetPetTitanMode( mode )
+ npcTitan.Signal( "ChangedTitanMode" )
+
+ SetupNPC_TitanTitle( npcTitan, player )
+ NPCTitanEnableCurrentMode( npcTitan, player )
+}
+
+//////////////////////////////////////////////////////////
+function NPCTitanSetBehaviorForMode( entity npcTitan, entity player )
+{
+ entity soul = npcTitan.GetTitanSoul()
+ if ( soul == null)
+ soul = player.GetTitanSoul()
+
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ if ( soul && SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ npcTitan.SetBehaviorSelector( "behavior_mp_auto_titan_enhanced" )
+ else
+ npcTitan.SetBehaviorSelector( "behavior_mp_auto_titan" )
+ break;
+
+ //case eNPCTitanMode.ROAM:
+ // break;
+
+ case eNPCTitanMode.STAY:
+ if ( soul && SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ npcTitan.SetBehaviorSelector( "behavior_mp_auto_titan_enhanced_guard" )
+ else
+ npcTitan.SetBehaviorSelector( "behavior_mp_auto_titan_guard" )
+ break;
+ }
+}
+
+//////////////////////////////////////////////////////////
+function NPCTitanDisableCurrentMode( entity npcTitan, entity player )
+{
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ npcTitan.DisableBehavior( "Follow" )
+ break;
+
+ //case eNPCTitanMode.ROAM:
+ // break;
+
+ case eNPCTitanMode.STAY:
+ npcTitan.DisableBehavior( "Assault" )
+ break;
+ }
+}
+
+
+//////////////////////////////////////////////////////////
+function NPCTitanEnableCurrentMode( entity npcTitan, entity player )
+{
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ NPCTitanFollowPilotInit( npcTitan, player )
+ break;
+
+ //case eNPCTitanMode.ROAM:
+ // break;
+
+ case eNPCTitanMode.STAY:
+ {
+
+ local traceStart = player.EyePosition()
+ local forward = AnglesToForward( player.EyeAngles() )
+ local traceEnd = traceStart + ( forward * 12000 )
+
+ TraceResults result = TraceLine( traceStart, traceEnd, player, TRACE_MASK_BLOCKLOS, TRACE_COLLISION_GROUP_NONE )
+
+ local dir = result.endPos - npcTitan.EyePosition()
+
+ // DebugDrawLine( result.endPos, npcTitan.EyePosition(), 255, 0, 0, true, 5 )
+
+ local titanAngles;
+ if ( LengthSqr( dir ) > 100 )
+ titanAngles = VectorToAngles( dir )
+ else
+ titanAngles = player.GetAngles()
+
+ titanAngles.z = 0;
+
+ npcTitan.AssaultPointClamped( npcTitan.GetOrigin() )
+ npcTitan.AssaultSetAngles( titanAngles, true )
+ break;
+ }
+ }
+
+ NPCTitanSetBehaviorForMode( npcTitan, player )
+}
+
+
+void function AutoTitanChangedEnemy( entity titan )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ entity enemy = titan.GetEnemy()
+
+ if ( !IsAlive( enemy ) )
+ return
+
+ if ( !titan.CanSee( enemy ) )
+ return
+
+ string aliasSuffix
+ if ( enemy.IsTitan() )
+ aliasSuffix = "autoEngageTitan"
+ else if ( IsGrunt( enemy ) )
+ aliasSuffix = "autoEngageGrunt"
+ else if ( enemy.IsHuman() && enemy.IsPlayer() )
+ aliasSuffix = "autoEngagePilot"
+
+ if ( aliasSuffix == "" )
+ return
+
+ PlayAutoTitanConversation( titan, aliasSuffix )
+}
+
+function AutoTitanShouldSpeak( entity titan, entity owner, aliasSuffix )
+{
+ if ( IsForcedDialogueOnly( owner ) )
+ return false
+
+ if ( "disableAutoTitanConversation" in titan.s )
+ {
+ return false
+ }
+ //Shut Auto Titans up when game isn't active anymore
+ if ( GetGameState() >= eGameState.Postmatch )
+ {
+ return false
+ }
+
+ entity owner
+
+ if ( titan.IsPlayer() )
+ {
+ owner = titan
+ }
+ else
+ {
+ owner = GetPetTitanOwner( titan )
+ if ( !IsValid( owner ) )
+ return
+ }
+
+ if ( owner.s.autoTitanLastEngageCallout == aliasSuffix )
+ {
+ // just did this line, so significant time has to pass before we will use it again
+ return Time() > owner.s.autoTitanLastEngageCalloutTime + 28
+ }
+
+ // this is a new line, so just make sure we haven't spoken too recently
+ return Time() > owner.s.autoTitanLastEngageCalloutTime + 7
+}
+
+void function PlayAutoTitanConversation( entity titan, string aliasSuffix )
+{
+ entity owner
+
+ if ( titan.IsPlayer() )
+ {
+ owner = titan
+ }
+ else
+ {
+ owner = GetPetTitanOwner( titan )
+ if ( !IsValid( owner ) )
+ return
+ }
+
+ if ( !AutoTitanShouldSpeak( titan, owner, aliasSuffix ) ) //Only use the suffix since that's the distinguishing part of the alias, i.e. "engage_titans"
+ return
+
+ owner.s.autoTitanLastEngageCalloutTime = Time()
+ owner.s.autoTitanLastEngageCallout = aliasSuffix //Only use the suffix since that's the distinguishing part of the alias, i.e. "engage_titans"
+
+ int conversationID = GetConversationIndex( aliasSuffix )
+ Remote_CallFunction_Replay( owner, "ServerCallback_PlayTitanConversation", conversationID )
+}
+
+
+void function FreeAutoTitan( entity npcTitan )
+{
+ //npcTitan.SetEnemyChangeCallback( "" )
+
+ local bossPlayer = npcTitan.GetBossPlayer()
+
+ if ( !IsValid( bossPlayer ) )
+ return
+
+ bossPlayer.SetPetTitan( null )
+
+ local soul = npcTitan.GetTitanSoul()
+
+ npcTitan.ClearBossPlayer()
+ soul.ClearBossPlayer()
+
+ npcTitan.SetTitle( "" )
+
+ npcTitan.Signal( "TitanStopsThinking" )
+ npcTitan.UnsetUsable()
+
+ thread TitanKneel( npcTitan )
+}
+
+
+//////////////////////////////////////////////////////////
+function SetupAutoTitan( entity npcTitan, entity player )
+{
+ #if SP
+ npcTitan.SetUsePrompts( "#HOLD_TO_EMBARK_SP", "#PRESS_TO_EMBARK_SP" )
+ #endif
+
+ #if MP
+ npcTitan.SetUsePrompts( "#HOLD_TO_EMBARK", "#PRESS_TO_EMBARK" )
+ #endif
+
+ npcTitan.SetUsableByGroup( "owner pilot" )
+
+ NPCTitanFollowPilotInit( npcTitan, player )
+
+ NPCTitanGuardModeInit( npcTitan )
+
+ npcTitan.SetEnemyChangeCallback( AutoTitanChangedEnemy )
+
+ NPCTitanEnableCurrentMode( npcTitan, player )
+
+ npcTitan.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+ npcTitan.EnableNPCFlag( NPC_NEW_ENEMY_FROM_SOUND )
+ UpdateEnemyMemoryFromTeammates( npcTitan )
+
+ SetPlayerPetTitan( player, npcTitan )
+
+ SetupNPC_TitanTitle( npcTitan, player )
+
+ ShowName( npcTitan )
+
+ SPMP_UpdateNPCProficiency( npcTitan )
+}
+
+function SetPlayerPetTitan( entity player, entity npcTitan )
+{
+ if ( npcTitan == player.GetPetTitan() )
+ return
+
+ entity previousOwner = GetPetTitanOwner( npcTitan )
+ if ( IsValid( previousOwner ) )
+ {
+ previousOwner.SetPetTitan( null )
+ }
+
+ if ( IsAlive( player.GetPetTitan() ) )
+ {
+ Assert( !player.s.replacementDropInProgress, "Tried to give us a titan when we were executing a Titanfall" )
+ // kill old pet titan
+ player.GetPetTitan().Die( null, null, { scriptType = DF_INSTANT, damageSourceId = damagedef_suicide } )
+ }
+
+ // HACK: not really a hack, but this could be optimized to only render always for a given client
+ npcTitan.EnableRenderAlways()
+ player.SetPetTitan( npcTitan )
+ #if HAS_TITAN_EARNING
+ ClearTitanAvailable( player )
+ #endif
+ SetTeam( npcTitan, player.GetTeam() )
+ entity soul = npcTitan.GetTitanSoul()
+ if ( soul == null )
+ soul = player.GetTitanSoul()
+
+ string settings = GetSoulPlayerSettings( soul )
+ var maintainTitle = Dev_GetPlayerSettingByKeyField_Global( settings, "keep_title_on_autotitan" )
+ if ( maintainTitle != null && maintainTitle == 1 )
+ {
+ string title = expect string( GetPlayerSettingsFieldForClassName( settings, "printname" ) )
+ npcTitan.SetTitle( title )
+ }
+ else if ( SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ {
+ npcTitan.SetTitle( "#NPC_AUTO_TITAN_ENHANCED" )
+ }
+ else
+ {
+ npcTitan.SetTitle( "#NPC_AUTO_TITAN" )
+ }
+
+ npcTitan.DisableHibernation()
+}
+
+
+//////////////////////////////////////////////////////////
+function NPCTitanFollowPilotInit( npcTitan, player )
+{
+ int followBehavior = GetDefaultNPCFollowBehavior( npcTitan )
+ npcTitan.InitFollowBehavior( player, followBehavior )
+
+ if ( IsMultiplayer() )
+ {
+ npcTitan.SetFollowGoalTolerance( 700 )
+ npcTitan.SetFollowGoalCombatTolerance( 700 )
+ npcTitan.SetFollowTargetMoveTolerance( 200 )
+ }
+ else
+ {
+ npcTitan.SetFollowGoalTolerance( 500 )
+ npcTitan.SetFollowGoalCombatTolerance( 1200 )
+ npcTitan.SetFollowTargetMoveTolerance( 150 )
+ }
+
+ npcTitan.EnableBehavior( "Follow" )
+ npcTitan.DisableBehavior( "Assault" )
+}
+
+//////////////////////////////////////////////////////////
+function NPCTitanGuardModeInit( npcTitan )
+{
+#if DEV // Bug 110047
+ Assert( IsValid( npcTitan ) )
+ if ( !npcTitan.IsTitan() && !npcTitan.IsNPC() )
+ printl( "npcTitan is " + npcTitan.GetClassName() )
+#endif
+
+ npcTitan.AssaultSetFightRadius( 0 )
+
+ if ( IsSingleplayer() )
+ {
+ npcTitan.AssaultSetGoalRadius( 512 )
+ npcTitan.AssaultSetArrivalTolerance( 300 )
+ }
+ else
+ {
+ npcTitan.AssaultSetGoalRadius( 400 )
+ npcTitan.AssaultSetArrivalTolerance( 200 )
+ }
+}
+
+//////////////////////////////////////////////////////////
+function NPCTitanInitModeOnPlayerRespawn( player )
+{
+ if ( IsValid( player.GetPetTitan() ) )
+ {
+ local titan = player.GetPetTitan()
+
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ NPCTitanFollowPilotInit( titan, player )
+ break;
+
+ default:
+ // nothing to do for other modes
+ break;
+ }
+ }
+}
+
+//////////////////////////////////////////////////////////
+function CodeCallback_PlayerRequestClimbInNPCTitan( npcTitan, player )
+{
+}
+
+
+
+
+//////////////////////////////////////////////////////////
+entity function CreateNPCTitanFromSettings( string settings, int team, vector origin, vector angles )
+{
+ entity npc = CreateNPCTitan( settings, team, origin, angles )
+ DispatchSpawn( npc )
+ return npc
+}
+
+function CreateTitanModelAndSkinSetup( entity npc )
+{
+ asset currentModel = npc.GetModelName()
+
+ if ( IsSingleplayer() )
+ {
+ switch ( currentModel )
+ {
+ case $"":
+ case $"models/titans/buddy/titan_buddy.mdl":
+ case $"models/titans/light/sp_titan_light_locust.mdl":
+ case $"models/titans/light/sp_titan_light_raptor.mdl":
+ case $"models/titans/heavy/sp_titan_heavy_deadbolt.mdl":
+ case $"models/titans/heavy/sp_titan_heavy_ogre.mdl":
+ case $"models/titans/medium/sp_titan_medium_ajax.mdl":
+ case $"models/titans/medium/sp_titan_medium_wraith.mdl":
+ break
+
+ default:
+ CodeWarning( "NPC titan at " + npc.GetOrigin() + " had non-sp titan model " + currentModel )
+ break
+ }
+ }
+
+ string settings = npc.ai.titanSettings.titanSetFile
+ asset model = GetPlayerSettingsAssetForClassName( settings, "bodymodel" )
+ npc.SetValueForModelKey( model )
+}
+
+// NEW TITAN STUFF BROUGHT OVER FROM TOWER DEFENSE R1
+string function GetRandomTitanWeapon()
+{
+ TitanLoadoutDef loadout = GetAllowedTitanLoadouts().getrandom()
+ return loadout.primary
+}
+
+void function ResetTitanBuildTime( entity player )
+{
+ if ( player.IsTitan() )
+ {
+ player.SetTitanBuildTime( GetCoreBuildTime( player ) )
+ return
+ }
+
+ player.SetTitanBuildTime( GetTitanBuildTime( player ) )
+}
+
+
+/* SP */
+
+void function SpawnTitanBattery( entity batteryRef )
+{
+ vector origin = batteryRef.GetOrigin()
+ entity battery = CreateTitanBattery( origin )
+ batteryRef.Destroy()
+}
+
+void function SpawnTitanBatteryOnDeath( entity titan, var damageInfo )
+{
+ if ( !titan.ai.shouldDropBattery || titan.GetTeam() == TEAM_MILITIA )
+ return
+ // if ( RandomFloatRange( 0, 100 ) < 50 )
+ // return
+ int attachID = titan.LookupAttachment( "CHESTFOCUS" )
+ vector origin = titan.GetAttachmentOrigin( attachID )
+
+ int numBatt = 0
+
+ if ( titan.IsTitan() && titan.ai.bossTitanType == TITAN_MERC )
+ {
+ numBatt = BATTERY_DROP_BOSS
+ }
+ else
+ {
+ if ( Flag( "PlayerDidSpawn" ) )
+ {
+ entity player = GetPlayerArray()[0]
+ entity playerTitan = GetTitanFromPlayer( player )
+
+ if ( IsValid( playerTitan ) &&
+ (
+ GetDoomedState( playerTitan ) ||
+ RandomDropBatteryBasedOnHealth( playerTitan )
+ )
+ )
+ {
+ numBatt = 1
+ }
+ }
+ }
+
+ for ( int i=0; i<numBatt; i++ )
+ {
+ vector vec = RandomVec( 150 )
+ if ( numBatt == 1 )
+ vec = < 0,0,0 >
+ entity battery = CreateTitanBattery( origin )
+ battery.SetVelocity( < vec.x, vec.y, 400 > )
+ }
+}
+
+entity function CreateTitanBattery( vector origin )
+{
+ entity battery = Rodeo_CreateBatteryPack()
+ battery.SetOrigin( origin )
+ //Highlight_SetNeutralHighlight( battery, "power_up" )
+ // if ( IsValid( battery ) )
+ // {
+ // PickupGlow glow = CreatePickupGlow( battery, 0, 255, 0 )
+ // glow.glowFX.SetParent( battery, "", true, 0 )
+ // }
+ return battery
+}
+
+void function SetWeaponCooldowns( entity player, array<entity> weapons, float cooldown )
+{
+ foreach ( weapon in weapons )
+ {
+ int max = weapon.GetWeaponPrimaryClipCountMax()
+ if ( max <= 0 )
+ continue
+ int current = int( max * cooldown )
+ weapon.SetWeaponPrimaryClipCountAbsolute( current )
+
+ if ( weapon.IsChargeWeapon() )
+ {
+ float chargeCooldownTime = weapon.GetWeaponSettingFloat( eWeaponVar.charge_cooldown_time )
+ if ( chargeCooldownTime > 1.0 )
+ {
+ weapon.SetWeaponPrimaryClipCountAbsolute( max )
+ weapon.SetWeaponChargeFractionForced( 1.0 - cooldown )
+ }
+ }
+ }
+}
+
+void function ResetTitanLoadoutFromPrimary( entity titan )
+{
+ Assert( titan.IsTitan() )
+ Assert( IsAlive( titan ) )
+
+// EmitSoundOnEntity( player, "Coop_AmmoBox_AmmoRefill" )
+ entity soul = titan.GetTitanSoul()
+ // not a real titan, swapping in/out of titan etc
+ if ( soul == null )
+ return
+
+ array<entity> weapons = GetPrimaryWeapons( titan )
+
+ foreach ( weapon in weapons )
+ {
+ TitanLoadoutDef ornull titanLoadout = GetTitanLoadoutForPrimary( weapon.GetWeaponClassName() )
+ if ( titanLoadout == null )
+ continue
+ expect TitanLoadoutDef( titanLoadout )
+
+ float coreValue = SoulTitanCore_GetNextAvailableTime( soul )
+
+ ReplaceTitanLoadoutWhereDifferent( titan, titanLoadout )
+
+ SoulTitanCore_SetNextAvailableTime( soul, coreValue )
+
+ if ( titan.IsPlayer() )
+ {
+// Remote_CallFunction_Replay( titan, "ServerCallback_NotifyLoadout", titan.GetEncodedEHandle() )
+ Remote_CallFunction_Replay( titan, "ServerCallback_UpdateTitanModeHUD" )
+ }
+ break
+ }
+}
+
+void function WaitForHotdropToEnd( entity titan )
+{
+ // Wait until player sees the boss titan
+ while ( titan.e.isHotDropping )
+ {
+ WaitFrame()
+ }
+}
+
+bool function RandomDropBatteryBasedOnHealth( entity playerTitan )
+{
+ float healthFrac = GetHealthFrac( playerTitan )
+ int randomPercent = RandomIntRange( 0, 100 )
+
+ if ( healthFrac <= BATTERY_DROP_HEALTH_FRAC_SURE )
+ {
+ return true
+ }
+ else if ( healthFrac <= BATTERY_DROP_HEALTH_FRAC_MID )
+ {
+ return randomPercent <= BATTERY_DROP_MID_CHANCE
+ }
+ else
+ {
+ return randomPercent <= BATTERY_DROP_LOW_CHANCE
+ }
+
+ return false
+}
+
+void function TitanAchievementTracking_SP( entity titan, var damageInfo )
+{
+ entity player = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !titan.IsTitan() )
+ return
+
+ if ( !IsValid( player ) )
+ return
+
+ if ( !player.IsPlayer() )
+ return
+
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ switch ( damageSourceId )
+ {
+ case eDamageSourceId.mp_titancore_salvo_core:
+ case eDamageSourceId.mp_titancore_laser_cannon:
+ case eDamageSourceId.mp_titancore_flame_wave:
+ case eDamageSourceId.mp_titancore_flame_wave_secondary:
+ case eDamageSourceId.mp_titancore_shift_core:
+ case eDamageSourceId.mp_titanweapon_flightcore_rockets:
+ case eDamageSourceId.mp_titancore_amp_core:
+ file.coreKillCounter++
+ break
+ case eDamageSourceId.mp_titanweapon_predator_cannon:
+ array<string> weaponMods = GetWeaponModsFromDamageInfo( damageInfo )
+ if ( weaponMods.contains( "Smart_Core" ) )
+ {
+ file.coreKillCounter++
+ }
+ break
+ #if HAS_BOSS_AI
+ case eDamageSourceId.titan_execution:
+ if ( IsMercTitan( titan ) )
+ {
+ UnlockAchievement( player, achievements.EXECUTE_BOSS )
+ }
+ break
+ #endif
+ }
+
+ if ( file.coreKillCounter >= 3 )
+ {
+ UnlockAchievement( player, achievements.CORE_MULTIKILL )
+ }
+
+ if ( !player.IsTitan() )
+ {
+ UnlockAchievement( player, achievements.PILOT_TITANKILL )
+ }
+
+ // don't count vortex refire for core kills
+ int scriptDamageType = DamageInfo_GetCustomDamageType( damageInfo )
+ if ( scriptDamageType & DF_VORTEX_REFIRE )
+ return
+
+ switch ( damageSourceId )
+ {
+ case eDamageSourceId.mp_titancore_salvo_core:
+ UnlockAchievement( player, achievements.CORE_SALVO )
+ break
+ case eDamageSourceId.mp_titancore_laser_cannon:
+ UnlockAchievement( player, achievements.CORE_LASER )
+ break
+ case eDamageSourceId.mp_titancore_flame_wave:
+ case eDamageSourceId.mp_titancore_flame_wave_secondary:
+ UnlockAchievement( player, achievements.CORE_FLAME )
+ break
+ case eDamageSourceId.mp_titancore_shift_core:
+ UnlockAchievement( player, achievements.CORE_SWORD )
+ break
+ case eDamageSourceId.mp_titanweapon_flightcore_rockets:
+ UnlockAchievement( player, achievements.CORE_FLIGHT )
+ break
+ case eDamageSourceId.mp_titancore_amp_core:
+ UnlockAchievement( player, achievements.CORE_BURST )
+ break
+ case eDamageSourceId.mp_titanweapon_predator_cannon:
+ array<string> weaponMods = GetWeaponModsFromDamageInfo( damageInfo )
+ if ( weaponMods.contains( "Smart_Core" ) )
+ {
+ UnlockAchievement( player, achievements.CORE_SMART )
+ }
+ break
+ }
+}
+
+// this gets called whenever a core is started
+void function ResetCoreKillCounter()
+{
+ file.coreKillCounter = 0
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_tether.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_tether.gnut
new file mode 100644
index 00000000..b088651a
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_tether.gnut
@@ -0,0 +1,307 @@
+global function TitanTether_Init
+global function AddTitanTether
+global function TetherFlyIn
+global function PROTO_GetActiveTethers
+global function CodeCallback_OnTetherRemove
+global function CodeCallback_OnTetherDamageMilestone
+global function AddOnTetherCallback
+
+struct TetherData
+{
+ entity owner
+ entity[2] endpointEnts
+ array<entity> tetherEnts = []
+ entity anchor
+ entity endEntForPlayer
+ entity endEntForOthers
+ int teamNum
+ int codeTetherID
+}
+
+
+struct
+{
+ array<TetherData> activeTitanTethers = []
+ array< void functionref( entity, entity ) > onTetherCallbacks = []
+} file
+
+void function TitanTether_Init()
+{
+ PrecacheImpactEffectTable( "exp_tether_trap" ) //Needs to match damagedef_fd_tether_trap
+}
+
+void function AddOnTetherCallback( void functionref( entity, entity ) callback )
+{
+ file.onTetherCallbacks.append( callback )
+}
+
+void function AddTitanTether( entity owner, entity startEnt, entity endEnt, array<entity> tetherEnts, entity anchor, entity tetherEndEntForPlayer, entity tetherEndEntForOthers, bool isExplosiveTether )
+{
+ //Run callbacks for tether trap activation.
+ foreach ( callback in file.onTetherCallbacks )
+ {
+ callback( owner, endEnt )
+ }
+
+ TetherData tetherData
+ tetherData.owner = owner
+
+ tetherData.teamNum = owner.GetTeam()
+
+ Assert( !startEnt.IsTitan() )
+ Assert( endEnt.IsTitan()|| IsSuperSpectre( endEnt ) )
+
+ if ( endEnt.IsTitan() || IsSuperSpectre( endEnt ) )
+ {
+ tetherData.codeTetherID = endEnt.AddTether( startEnt.GetOrigin() )
+ if ( owner.IsPlayer() )
+ EmitSoundOnEntityExceptToPlayer( startEnt, owner, "Wpn_TetherTrap_PopOpen_3p" )//Spring Sound
+ else
+ EmitSoundOnEntity( startEnt, "Wpn_TetherTrap_PopOpen_3p" )//Spring Sound
+
+ if ( endEnt.IsTitan() )
+ endEnt = endEnt.GetTitanSoul()
+ }
+
+ tetherData.endpointEnts[0] = startEnt
+ tetherData.endpointEnts[1] = endEnt
+
+ tetherData.tetherEnts = tetherEnts
+
+ tetherData.anchor = anchor
+ tetherData.endEntForPlayer = tetherEndEntForPlayer
+ tetherData.endEntForOthers = tetherEndEntForOthers
+
+ file.activeTitanTethers.append( tetherData )
+
+ thread TetherCleanup( owner, startEnt, endEnt, tetherData, isExplosiveTether )
+}
+
+void function TetherCleanup( entity owner, entity startEnt, entity endEnt, TetherData tetherData, bool isExplosiveTether )
+{
+ startEnt.EndSignal( "OnDestroy" )
+ endEnt.EndSignal( "OnDestroy" )
+ endEnt.EndSignal( "OnSyncedMelee" )
+
+ int tetherID = tetherData.codeTetherID
+// int statusEffectId = StatusEffect_AddEndless( endEnt, eStatusEffect.tethered, 1.0 )
+ int statusEffectId = StatusEffect_AddTimed( endEnt, eStatusEffect.tethered, 1.0, 5.0, 0.0 )
+
+ vector anchorOrigin = tetherData.anchor.GetOrigin()
+
+ OnThreadEnd(
+ function() : ( owner, anchorOrigin, endEnt, tetherID, statusEffectId, isExplosiveTether )
+ {
+ if ( isExplosiveTether && IsValid( owner ) && IsValid( endEnt ) )
+ {
+ Explosion_DamageDefSimple( damagedef_fd_tether_trap, anchorOrigin,owner, owner, anchorOrigin + < 0, 0, 32 > )
+ }
+
+ foreach ( index, tetherData in file.activeTitanTethers )
+ {
+ if ( tetherData.codeTetherID == tetherID )
+ {
+ thread TitanTether_Remove( tetherData )
+ break
+ }
+ }
+
+ if ( IsValid( endEnt ) )
+ StatusEffect_Stop( endEnt, statusEffectId )
+ }
+ )
+
+ WaitForever()
+}
+
+void function TetherFlyIn( entity flyFrom, entity flyTo, entity rope, entity owner )
+{
+ flyTo.EndSignal( "OnDestroy" )
+ vector destLocal = flyTo.GetLocalOrigin()
+ flyTo.SetAbsOrigin( flyFrom.GetOrigin() )
+ flyTo.NonPhysicsMoveInWorldSpaceToLocalPos( destLocal, 0.3, 0, 0 )
+ wait 0.3
+ if ( IsValid( owner ) && owner.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( flyTo, owner, "Weapon_TetherGun_Attach_1P_VS_3P" )
+ EmitSoundOnEntityExceptToPlayer( flyTo, owner, "Weapon_TetherGun_Attach_3P_VS_3P" )
+ }
+ else
+ {
+ EmitSoundOnEntity( flyTo, "Weapon_TetherGun_Attach_3P_VS_3P" )
+ }
+}
+
+
+void function TitanTether_Remove( TetherData tetherData )
+{
+ entity endEnt = tetherData.endpointEnts[1]
+ if ( IsValid( endEnt ) )
+ {
+ if ( IsSoul( endEnt ) )
+ endEnt = endEnt.GetTitan()
+
+ if ( endEnt.IsValidTetherID( tetherData.codeTetherID ) )
+ endEnt.RemoveTether( tetherData.codeTetherID )
+ }
+
+ vector angvel = < RandomFloatRange( 50, 1000 ), RandomFloatRange( -200, 200 ), RandomFloatRange( -200, 200 )>
+
+ vector velForPlayer
+ vector velForOthers
+ vector rotaxis
+ float rotspeed
+ if ( IsValid( endEnt ) )
+ {
+ vector forward = endEnt.GetPlayerOrNPCViewVector()
+ velForPlayer = forward * 200
+
+ rotaxis = endEnt.GetPlayerOrNPCViewRight()
+ rotaxis += forward * RandomFloatRange( -0.4, 0.4 )
+ rotaxis += endEnt.GetPlayerOrNPCViewUp() * RandomFloatRange( -0.4, 0.4 )
+ rotspeed = RandomFloatRange( -2000, -1000 )
+ }
+ else
+ {
+ rotaxis = RandomVec( 1 )
+ rotspeed = RandomFloatRange( -2000, 2000 )
+ }
+
+ vector pullDirForPlayer
+ vector pullDirForOthers
+
+ bool endEntForPlayerIsValid = IsValid( tetherData.endEntForPlayer )
+ bool endEntForOthersIsValid = IsValid( tetherData.endEntForOthers )
+
+ if ( IsValid( tetherData.anchor ) )
+ {
+ if ( endEntForPlayerIsValid )
+ pullDirForPlayer = Normalize( tetherData.anchor.GetOrigin() - tetherData.endEntForPlayer.GetOrigin() )
+ if ( endEntForOthersIsValid )
+ pullDirForOthers = Normalize( tetherData.anchor.GetOrigin() - tetherData.endEntForOthers.GetOrigin() )
+ }
+
+ velForPlayer += < RandomFloatRange(-100,100), RandomFloatRange(-100,100), 0> + pullDirForPlayer * 100
+ float pullDirForOthersZ = pullDirForOthers.z
+ pullDirForOthers.z = 0
+ pullDirForOthers += < RandomFloatRange(-1,1), RandomFloatRange(-1,1), 0>
+ velForOthers = Normalize( pullDirForOthers )
+ velForOthers.x *= RandomFloatRange( 100, 200 )
+ velForOthers.y *= RandomFloatRange( 100, 200 )
+ velForOthers.z = pullDirForOthersZ * 200
+
+ rotaxis = Normalize( rotaxis )
+
+ // Since we're unparenting the tethers, we need to change how they control who they're visible to
+ if ( IsValid( endEnt ) )
+ {
+ if ( endEntForPlayerIsValid )
+ {
+ tetherData.endEntForPlayer.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER
+ tetherData.endEntForPlayer.SetOwner( endEnt )
+ }
+ if ( endEntForOthersIsValid )
+ {
+ tetherData.endEntForOthers.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY
+ tetherData.endEntForOthers.SetOwner( endEnt )
+ }
+ }
+ else
+ {
+ if ( endEntForPlayerIsValid )
+ tetherData.endEntForPlayer.kv.VisibilityFlags = ENTITY_VISIBLE_TO_NOBODY
+ if ( endEntForOthersIsValid )
+ tetherData.endEntForOthers.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ }
+
+ if ( endEntForPlayerIsValid )
+ {
+ tetherData.endEntForPlayer.ClearParent()
+ tetherData.endEntForPlayer.NonPhysicsRotate( rotaxis, rotspeed )
+ tetherData.endEntForPlayer.NonPhysicsMoveWithGravity( velForPlayer, < 0, 0, -750> )
+ tetherData.endEntForPlayer.RenderWithViewModels( false )
+ tetherData.endEntForPlayer.Dissolve( ENTITY_DISSOLVE_NORMAL, <0,0,0>, 100 )
+ }
+
+ if ( endEntForOthersIsValid )
+ {
+ tetherData.endEntForOthers.ClearParent()
+ tetherData.endEntForOthers.NonPhysicsRotate( rotaxis, rotspeed )
+ tetherData.endEntForOthers.NonPhysicsMoveWithGravity( velForOthers, < 0, 0, -750> )
+ tetherData.endEntForOthers.Dissolve( ENTITY_DISSOLVE_NORMAL, <0,0,0>, 100 )
+ }
+
+ wait 0.5
+
+ foreach ( index, ent in tetherData.endpointEnts )
+ {
+ if ( !IsValid( ent ) )
+ continue
+
+ if ( ent instanceof CBaseGrenade )
+ ent.Destroy()
+ }
+
+ foreach ( entity ent in tetherData.tetherEnts )
+ {
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+}
+
+
+int function PROTO_GetActiveTethers( entity owner )
+{
+// _PruneActiveTitanTethers()
+
+ int activeTethers = 0
+ foreach ( TetherData tetherData in file.activeTitanTethers )
+ {
+ if ( tetherData.owner == owner )
+ activeTethers++
+ }
+
+ return activeTethers
+}
+
+
+bool removingTether
+
+void function CodeCallback_OnTetherRemove( entity guy, int tetherID )
+{
+ Assert( !removingTether )
+ removingTether = true
+
+ foreach ( index, tetherData in file.activeTitanTethers )
+ {
+ if ( tetherData.codeTetherID == tetherID )
+ {
+ thread TitanTether_Remove( tetherData )
+ file.activeTitanTethers.fastremove( index )
+ break
+ }
+ }
+
+ removingTether = false
+}
+
+TetherData function GetTetherDataForCodeID( int codeTetherID )
+{
+ foreach ( index, tetherData in file.activeTitanTethers )
+ {
+ if ( tetherData.codeTetherID == codeTetherID )
+ return tetherData
+ }
+
+ unreachable
+}
+
+void function CodeCallback_OnTetherDamageMilestone( entity guy, int tetherID, int damageMilestoneIndex, float health )
+{
+ float healthFrac = 1.0 - health / 1000.0
+
+ TetherData tetherData = GetTetherDataForCodeID( tetherID )
+
+ vector ang = tetherData.endEntForPlayer.GetLocalAngles()
+ tetherData.endEntForPlayer.SetLocalAngles( < healthFrac * 45, ang.y, ang.z> )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_transfer.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_transfer.nut
new file mode 100644
index 00000000..7b126cd0
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_titan_transfer.nut
@@ -0,0 +1,641 @@
+untyped
+
+global function TitanTransfer_Init
+
+global function PilotBecomesTitan
+global function TitanBecomesPilot
+global function CreateAutoTitanForPlayer_ForTitanBecomesPilot
+global function CreateAutoTitanForPlayer_FromTitanLoadout
+global function CopyWeapons
+global function StorePilotWeapons
+global function RetrievePilotWeapons
+global function SetTitanSettings
+
+global function StoreWeapons
+global function GiveWeaponsFromStoredArray
+
+global function TitanCoreEffectTransfer_threaded
+global function ForceTitanSustainedDischargeEnd
+
+function TitanTransfer_Init()
+{
+ // these vars transfer from player titan to npc titan and vice versa
+
+ RegisterSignal( "PlayerEmbarkedTitan" )
+ RegisterSignal( "PlayerDisembarkedTitan" )
+
+ AddSoulTransferFunc( TitanCoreEffectTransfer )
+ AddCallback_OnTitanBecomesPilot( OnClassChangeBecomePilot )
+ AddCallback_OnPilotBecomesTitan( OnClassChangeBecomeTitan )
+}
+
+void function TitanCoreEffectTransfer( entity soul, entity titan, entity oldTitan )
+{
+ thread TitanCoreEffectTransfer_threaded( soul, titan, oldTitan )
+}
+function TitanCoreEffectTransfer_threaded( entity soul, entity titan, entity oldTitan )
+{
+ WaitEndFrame() // because the titan aint a titan yet
+
+ if ( !IsValid( soul ) || !IsValid( titan ) )
+ return
+
+ if ( !( "coreEffect" in soul.s ) )
+ return
+
+ soul.s.coreEffect.ent.Kill_Deprecated_UseDestroyInstead()
+ soul.s.coreEffect.ent = soul.s.coreEffect.func( titan, soul.s.coreEffect.parameter )
+}
+
+void function OnClassChangeBecomePilot( entity player, entity titan ) //Stuff here used to be in old CPlayer:OnChangedPlayerClass, for turning into Pilot class
+{
+ player.ClearDoomed()
+ player.UnsetUsable()
+ player.lastTitanTime = Time()
+
+ player.Minimap_SetHeightTracking( true )
+ ResetTitanBuildTime( player )
+ RandomizeHead( player )
+}
+
+void function OnClassChangeBecomeTitan( entity player, entity titan ) //Stuff here used to be in old CPlayer:OnChangedPlayerClass, for turning into Titan class
+{
+ CodeCallback_PlayerInTitanCockpit( player, player )
+ player.Minimap_SetHeightTracking( false )
+ ResetTitanBuildTime( player )
+}
+
+function CopyWeapons( entity fromEnt, entity toEnt )
+{
+ entity activeWeapon = fromEnt.GetActiveWeapon()
+ if ( IsValid( activeWeapon ) )
+ {
+ if ( activeWeapon.IsWeaponOffhand() )
+ fromEnt.ClearOffhand()
+ }
+
+ array<entity> weapons = fromEnt.GetMainWeapons()
+ foreach ( weapon in weapons )
+ {
+ entity giveWeapon = fromEnt.TakeWeapon_NoDelete( weapon.GetWeaponClassName() )
+ toEnt.GiveExistingWeapon( giveWeapon )
+ }
+
+ for ( int i = 0; i < OFFHAND_COUNT; i++ )
+ {
+ entity offhandWeapon
+ offhandWeapon = fromEnt.TakeOffhandWeapon_NoDelete( i )
+
+ // maintain offhand index
+ if ( offhandWeapon )
+ toEnt.GiveExistingOffhandWeapon( offhandWeapon, i )
+ }
+
+ if ( activeWeapon )
+ {
+ string name = activeWeapon.GetWeaponClassName()
+ toEnt.SetActiveWeaponByName( name )
+ }
+}
+
+array<StoredWeapon> function StoreWeapons( entity player )
+{
+ array<StoredWeapon> storedWeapons
+
+ entity activeWeapon = player.GetActiveWeapon()
+
+ array<entity> mainWeapons = player.GetMainWeapons()
+
+ foreach ( i, weapon in mainWeapons )
+ {
+ StoredWeapon sw
+
+ if ( weapon.GetScriptFlags0() & WEAPONFLAG_AMPED )
+ {
+ weapon.Signal( "StopAmpedWeapons" )
+ }
+
+ sw.name = weapon.GetWeaponClassName()
+ sw.weaponType = eStoredWeaponType.main
+ sw.activeWeapon = ( weapon == activeWeapon )
+ sw.inventoryIndex = i
+ sw.mods = weapon.GetMods()
+ sw.modBitfield = weapon.GetModBitField()
+ sw.ammoCount = weapon.GetWeaponPrimaryAmmoCount()
+ sw.clipCount = weapon.GetWeaponPrimaryClipCount()
+ sw.nextAttackTime = weapon.GetNextAttackAllowedTime()
+ sw.skinIndex = weapon.GetSkin()
+ sw.camoIndex = weapon.GetCamo()
+ sw.isProScreenOwner = weapon.GetProScreenOwner() == player
+
+ storedWeapons.append( sw )
+ }
+
+ array<entity> offhandWeapons = player.GetOffhandWeapons()
+
+ foreach ( weapon in offhandWeapons )
+ {
+ StoredWeapon sw
+
+ sw.name = weapon.GetWeaponClassName()
+ sw.weaponType = eStoredWeaponType.offhand
+ sw.activeWeapon = ( weapon == activeWeapon )
+ sw.inventoryIndex = weapon.GetInventoryIndex()
+ sw.mods = weapon.GetMods()
+ sw.ammoCount = weapon.GetWeaponPrimaryAmmoCount()
+ sw.clipCount = weapon.GetWeaponPrimaryClipCount()
+ sw.nextAttackTime = weapon.GetNextAttackAllowedTime()
+ #if MP
+ sw.burnReward = weapon.e.burnReward
+ #endif
+
+ if ( sw.activeWeapon )
+ storedWeapons.insert( 0, sw )
+ else
+ storedWeapons.append( sw )
+ }
+
+ return storedWeapons
+}
+
+void function GiveWeaponsFromStoredArray( entity player, array<StoredWeapon> storedWeapons )
+{
+ int activeWeaponSlot = 0
+ foreach ( i, storedWeapon in storedWeapons )
+ {
+ entity weapon
+
+ switch ( storedWeapon.weaponType )
+ {
+ case eStoredWeaponType.main:
+ weapon = player.GiveWeapon( storedWeapon.name, storedWeapon.mods )
+ weapon.SetWeaponSkin( storedWeapon.skinIndex )
+ weapon.SetWeaponCamo( storedWeapon.camoIndex )
+ #if MP
+ if ( storedWeapon.isProScreenOwner )
+ {
+ weapon.SetProScreenOwner( player )
+ UpdateProScreen( player, weapon )
+ }
+
+ string weaponCategory = GetWeaponInfoFileKeyField_GlobalString( weapon.GetWeaponClassName(), "menu_category" )
+ if ( weaponCategory == "at" || weaponCategory == "special" ) // refill AT/grenadier ammo stockpile
+ {
+ int defaultTotal = weapon.GetWeaponSettingInt( eWeaponVar.ammo_default_total )
+ int clipSize = weapon.GetWeaponSettingInt( eWeaponVar.ammo_clip_size )
+
+ weapon.SetWeaponPrimaryAmmoCount( defaultTotal - clipSize )
+
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( storedWeapon.clipCount )
+ }
+ else
+ {
+ weapon.SetWeaponPrimaryAmmoCount( storedWeapon.ammoCount )
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( storedWeapon.clipCount )
+ }
+ #else
+ weapon.SetWeaponPrimaryAmmoCount( storedWeapon.ammoCount )
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( storedWeapon.clipCount )
+ #endif
+
+
+ if ( storedWeapon.activeWeapon )
+ activeWeaponSlot = i
+
+ break
+
+ case eStoredWeaponType.offhand:
+ player.GiveOffhandWeapon( storedWeapon.name, storedWeapon.inventoryIndex, storedWeapon.mods )
+
+ weapon = player.GetOffhandWeapon( storedWeapon.inventoryIndex )
+ weapon.SetNextAttackAllowedTime( storedWeapon.nextAttackTime )
+ weapon.SetWeaponPrimaryAmmoCount( storedWeapon.ammoCount )
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( storedWeapon.clipCount )
+ #if MP
+ weapon.e.burnReward = storedWeapon.burnReward
+ #endif
+
+ break
+
+ default:
+ unreachable
+ }
+ }
+
+ #if MP
+ PlayerInventory_RefreshEquippedState( player )
+ #endif
+
+ player.SetActiveWeaponBySlot( activeWeaponSlot )
+}
+
+void function RetrievePilotWeapons( entity player )
+{
+ TakeAllWeapons( player )
+ GiveWeaponsFromStoredArray( player, player.p.storedWeapons )
+ SetPlayerCooldowns( player )
+ player.p.storedWeapons.clear()
+}
+
+function StorePilotWeapons( entity player )
+{
+ player.p.storedWeapons = StoreWeapons( player )
+ StoreOffhandData( player, false )
+ TakeAllWeapons( player )
+}
+
+function TransferHealth( srcEnt, destEnt )
+{
+ destEnt.SetMaxHealth( srcEnt.GetMaxHealth() )
+ destEnt.SetHealth( srcEnt.GetHealth() )
+ //destEnt.SetHealthPerSegment( srcEnt.GetHealthPerSegment() )
+}
+
+entity function CreateAutoTitanForPlayer_FromTitanLoadout( entity player, TitanLoadoutDef loadout, vector origin, vector angles )
+{
+ int team = player.GetTeam()
+
+ player.titansBuilt++
+ ResetTitanBuildTime( player )
+
+ entity npcTitan = CreateNPCTitan( loadout.setFile, team, origin, angles, loadout.setFileMods )
+ SetTitanSpawnOptionsFromLoadout( npcTitan, loadout )
+ SetSpawnOption_OwnerPlayer( npcTitan, player )
+
+ if ( IsSingleplayer() )
+ {
+ npcTitan.EnableNPCFlag( NPC_IGNORE_FRIENDLY_SOUND )
+ }
+ #if MP
+ string titanRef = GetTitanCharacterNameFromSetFile( loadout.setFile )
+ npcTitan.SetTargetInfoIcon( GetTitanCoreIcon( titanRef ) )
+ #endif
+
+ return npcTitan
+}
+
+entity function CreateAutoTitanForPlayer_ForTitanBecomesPilot( entity player, bool hidden = false )
+{
+ vector origin = player.GetOrigin()
+ vector angles = player.GetAngles()
+ TitanLoadoutDef loadout = GetTitanLoadoutFromPlayerInventory( player )
+
+ int team = player.GetTeam()
+ entity npcTitan = CreateNPCTitan( loadout.setFile, team, origin, angles, loadout.setFileMods )
+ npcTitan.s.spawnWithoutSoul <- true
+ SetTitanSpawnOptionsFromLoadout( npcTitan, loadout )
+ SetSpawnOption_OwnerPlayer( npcTitan, player )
+ npcTitan.SetSkin( player.GetSkin() )
+ npcTitan.SetCamo( player.GetCamo() )
+ npcTitan.SetDecal( player.GetDecal() )
+
+ if ( IsSingleplayer() )
+ npcTitan.EnableNPCFlag( NPC_IGNORE_FRIENDLY_SOUND )
+
+ return npcTitan
+}
+
+void function SetTitanSpawnOptionsFromLoadout( entity titan, TitanLoadoutDef loadout )
+{
+ titan.ai.titanSpawnLoadout = loadout
+}
+
+TitanLoadoutDef function GetTitanLoadoutFromPlayerInventory( entity player ) //TODO: Examine necessity for this? Was needed in R1 where TItans could pick up weapons off the ground, but may not be needed in R2 anymore. Might just be fine to call GetTitanLoadoutForPlayer()
+{
+ TitanLoadoutDef loadout = GetTitanLoadoutForPlayer( player )
+ loadout.setFile = player.GetPlayerSettings()
+ loadout.setFileMods = UntypedArrayToStringArray( player.GetPlayerSettingsMods() )
+
+ array mainWeapons = player.GetMainWeapons()
+ if ( mainWeapons.len() )
+ {
+ entity wep = player.GetMainWeapons()[0]
+ loadout.primary = wep.GetWeaponClassName()
+ loadout.primaryMods = wep.GetMods()
+ }
+
+ entity ord = player.GetOffhandWeapon(OFFHAND_ORDNANCE)
+ if ( ord )
+ {
+ loadout.ordnance = ord.GetWeaponClassName()
+ loadout.ordnanceMods = ord.GetMods()
+ }
+
+ entity tac = player.GetOffhandWeapon(OFFHAND_SPECIAL)
+ if ( tac )
+ {
+ loadout.special = tac.GetWeaponClassName()
+ loadout.specialMods = tac.GetMods()
+ }
+
+ entity antirodeo = player.GetOffhandWeapon(OFFHAND_ANTIRODEO)
+ if ( antirodeo )
+ {
+ loadout.antirodeo = antirodeo.GetWeaponClassName()
+ loadout.antirodeoMods = antirodeo.GetMods()
+ }
+
+ entity melee = player.GetMeleeWeapon()
+ if ( melee )
+ loadout.melee = melee.GetWeaponClassName()
+
+ return loadout
+}
+
+void function ForceTitanSustainedDischargeEnd( entity player )
+{
+ // To disable core's while disembarking
+ local weapons = player.GetOffhandWeapons()
+ foreach ( weapon in weapons )
+ {
+ if ( weapon.IsChargeWeapon() )
+ weapon.ForceChargeEndNoAttack()
+
+ if ( weapon.IsSustainedDischargeWeapon() && weapon.IsDischarging() )
+ weapon.ForceSustainedDischargeEnd();
+ }
+}
+
+function TitanBecomesPilot( entity player, entity titan )
+{
+ Assert( IsAlive( player ), player + ": Player is not alive" )
+ Assert( player.IsTitan(), player + " is not a titan" )
+
+ Assert( IsAlive( titan ), titan + " is not alive." )
+ Assert( titan.IsTitan(), titan + " is not alive." )
+
+ asset model = player.GetModelName()
+ int skin = player.GetSkin()
+ int camo = player.GetCamo()
+ int decal = player.GetDecal()
+ titan.SetModel( model )
+ titan.SetSkin( skin )
+ titan.SetCamo( camo )
+ titan.SetDecal( decal )
+ titan.SetPoseParametersSameAs( player )
+ titan.SequenceTransitionFromEntity( player )
+
+ ForceTitanSustainedDischargeEnd( player )
+
+ TransferHealth( player, titan )
+ //Transfer children before player becomes pilot model
+ player.TransferChildrenTo( titan )
+ player.TransferTethersToEntity( titan )
+ entity soul = player.GetTitanSoul()
+ SetSoulOwner( soul, titan )
+ Assert( player.GetTitanSoul() == null )
+
+ // this must happen before changing the players settings
+ TransferDamageStates( player, titan )
+
+ // cant have a titan passive when you're not a titan
+ TakeAllTitanPassives( player )
+
+ player.SetPlayerSettingsWithMods( player.s.storedPlayerSettings, player.s.storedPlayerSettingsMods )
+ player.SetPlayerSettingPosMods( PLAYERPOSE_STANDING, player.s.storedPlayerStandMods )
+ player.SetPlayerSettingPosMods( PLAYERPOSE_CROUCHING, player.s.storedPlayerCrouchMods )
+ player.SetSkin( player.s.storedPlayerSkinIndex )
+ player.SetCamo( player.s.storedPlayerCamoIndex )
+
+ delete player.s.storedPlayerSettings
+ delete player.s.storedPlayerSettingsMods
+ delete player.s.storedPlayerStandMods
+ delete player.s.storedPlayerCrouchMods
+ delete player.s.storedPlayerSkinIndex
+ delete player.s.storedPlayerCamoIndex
+
+ TakeAllWeapons( titan )
+ CopyWeapons( player, titan )
+
+ player.SetTitle( "" )
+
+ RetrievePilotWeapons( player )
+
+ if ( Riff_AmmoLimit() != eAmmoLimit.Default )
+ {
+ switch ( Riff_AmmoLimit() )
+ {
+ case eAmmoLimit.Limited:
+ local weapons = player.GetMainWeapons()
+ foreach ( weapon in weapons )
+ {
+ local clipAmmo = player.GetWeaponAmmoMaxLoaded( weapon )
+
+ if ( clipAmmo > 0 )
+ weapon.SetWeaponPrimaryAmmoCount( clipAmmo * 2 )
+ }
+
+ local offhand = player.GetOffhandWeapon( 0 )
+ if ( offhand )
+ {
+ local ammoLoaded = player.GetWeaponAmmoMaxLoaded( offhand )
+ offhand.SetWeaponPrimaryClipCount( max( 1, ammoLoaded - 2 ) )
+ }
+ break
+ }
+ }
+
+ // Added via AddCallback_OnTitanBecomesPilot
+ foreach ( callbackFunc in svGlobal.onTitanBecomesPilotCallbacks )
+ {
+ callbackFunc( player, titan )
+ }
+
+ // Ensure rodeo doesn't happen straight away, if a nearby Titan runs by
+ Rodeo_SetCooldown( player )
+
+ if ( player.cloakedForever )
+ {
+ // infinite cloak active
+ EnableCloakForever( player )
+ }
+ if ( player.stimmedForever )
+ {
+ StimPlayer( player, USE_TIME_INFINITE )
+ }
+
+ soul.Signal( "PlayerDisembarkedTitan", { player = player } )
+
+ // no longer owned
+ if ( soul.capturable )
+ {
+ soul.ClearBossPlayer()
+ titan.ClearBossPlayer()
+ titan.SetUsableByGroup( "friendlies pilot" )
+ titan.DisableBehavior( "Follow" )
+ player.SetPetTitan( null )
+ if ( !player.s.savedTitanBuildTimer )
+ ResetTitanBuildTime( player )
+ else
+ player.SetNextTitanRespawnAvailable( player.s.savedTitanBuildTimer )
+ return
+ }
+ return titan
+}
+
+function PilotBecomesTitan( entity player, entity titan, bool fullCopy = true )
+{
+ player.SetPetTitan( null )
+
+ // puts the weapons into a table
+ StorePilotWeapons( player )
+
+ #if HAS_TITAN_WEAPON_SWAPPING
+ {
+ foreach ( weapon in titan.GetMainWeapons() )
+ {
+ // the pilot's weapons will recent entirely in sp if this doesn't match
+ player.p.lastPrimaryWeaponEnt = weapon
+ break
+ }
+ }
+ #endif
+
+ if ( fullCopy )
+ {
+ CopyWeapons( titan, player )
+ }
+
+ //Should only be the first time a player embarks into a Titan that Titan's life.
+ //Check with differ if there is a better way than .e.var on the soul.
+ //TitanLoadoutDef loadout = GetTitanLoadoutForPlayer( player )
+ //PROTO_DisplayTitanLoadouts( player, titan, loadout )
+
+ entity soul = titan.GetTitanSoul()
+ soul.soul.lastOwner = player
+
+ player.s.storedPlayerSettings <- player.GetPlayerSettings()
+ player.s.storedPlayerSettingsMods <- player.GetPlayerSettingsMods()
+ player.s.storedPlayerStandMods <- player.GetPlayerModsForPos( PLAYERPOSE_STANDING )
+ player.s.storedPlayerCrouchMods <- player.GetPlayerModsForPos( PLAYERPOSE_CROUCHING )
+ player.s.storedPlayerSkinIndex <- player.GetSkin()
+ player.s.storedPlayerCamoIndex <- player.GetCamo()
+ printt( player.GetSkin(), player.GetCamo() )
+
+ string settings = GetSoulPlayerSettings( soul )
+ var titanTint = Dev_GetPlayerSettingByKeyField_Global( settings, "titan_tint" )
+
+ if ( titanTint != null )
+ {
+ expect string( titanTint )
+ Highlight_SetEnemyHighlight( player, titanTint )
+ }
+ else
+ {
+ Highlight_ClearEnemyHighlight( player )
+ }
+
+ if ( !player.GetParent() )
+ {
+ player.SetOrigin( titan.GetOrigin() )
+ player.SetAngles( titan.GetAngles() )
+ player.SetVelocity( Vector( 0,0,0 ) )
+ }
+
+ if ( soul.capturable )
+ {
+ printt( player.GetPetTitan(), player.GetNextTitanRespawnAvailable() )
+ if ( IsValid( player.GetPetTitan() ) || player.s.replacementDropInProgress )
+ player.s.savedTitanBuildTimer <- null
+ else
+ player.s.savedTitanBuildTimer <- player.GetNextTitanRespawnAvailable()
+
+ if ( GameRules_GetGameMode() == "ctt" )
+ {
+ titan.Minimap_AlwaysShow( 0, null )
+ player.Minimap_AlwaysShow( TEAM_IMC, null )
+ player.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ player.SetHudInfoVisibilityTestAlwaysPasses( true )
+ }
+ }
+
+ SetSoulOwner( soul, player )
+
+ if ( soul.GetBossPlayer() != player )
+ SoulBecomesOwnedByPlayer( soul, player )
+
+ foreach ( int passive, _ in level.titanPassives )
+ {
+ if ( SoulHasPassive( soul, passive ) )
+ {
+ GiveTitanPassiveLifeLong( player, passive )
+ }
+ }
+
+ asset model = titan.GetModelName()
+ int skin = titan.GetSkin()
+ int camo = titan.GetCamo()
+ int decal = titan.GetDecal()
+ TitanSettings titanSettings = titan.ai.titanSettings
+ array<string> mods = titanSettings.titanSetFileMods
+
+ player.SetPlayerSettingsFromDataTable( { playerSetFile = titanSettings.titanSetFile, playerSetFileMods = mods } )
+ var title = GetPlayerSettingsFieldForClassName( settings, "printname" )
+
+ if ( title != null )
+ {
+ player.SetTitle( expect string( title ) )
+ }
+
+ if ( IsAlive( player ) )
+ TransferHealth( titan, player )
+
+ player.SetModel( model )
+ player.SetSkin( skin )
+ player.SetCamo( camo )
+ player.SetDecal( decal )
+ player.SetPoseParametersSameAs( titan )
+ player.SequenceTransitionFromEntity( titan )
+
+ // no cloak titan
+ player.SetCloakDuration( 0, 0, 0 )
+
+ // this must happen after changing the players settings
+ TransferDamageStates( titan, player )
+
+ titan.TransferTethersToEntity( player )
+
+ //We parent the player to the titan in the process of embarking
+ //Must clear parent when transfering children to avoid parenting the player to himself
+ if ( player.GetParent() == titan )
+ player.ClearParent()
+ //Transfer children after player has become titan model.
+ titan.TransferChildrenTo( player )
+
+ player.SetOrigin( titan.GetOrigin() )
+ player.SetAngles( titan.GetAngles() )
+ player.SetVelocity( Vector( 0,0,0 ) )
+
+ soul.e.embarkCount++
+ soul.Signal( "PlayerEmbarkedTitan", { player = player } )
+ player.Signal( "PlayerEmbarkedTitan", { titan = titan } )
+ titan.Signal( "TitanStopsThinking" )
+
+ // Added via AddCallback_OnPilotBecomesTitan
+ foreach ( callbackFunc in svGlobal.onPilotBecomesTitanCallbacks )
+ {
+ callbackFunc( player, titan )
+ }
+
+ #if DEV
+ thread Dev_CheckTitanIsDeletedAtEndOfPilotBecomesTitan( titan )
+ #endif
+}
+
+void function SetTitanSettings( TitanSettings titanSettings, string titanSetFile, array<string> mods = [] )
+{
+ Assert( titanSettings.titanSetFile == "", "Tried to set titan settings to " + titanSetFile + ", but its already set to " + titanSettings.titanSetFile )
+ titanSettings.titanSetFile = titanSetFile
+ titanSettings.titanSetFileMods = mods
+}
+
+void function Dev_CheckTitanIsDeletedAtEndOfPilotBecomesTitan( entity titan )
+{
+ WaitEndFrame()
+
+ Assert( !IsValid( titan ), "Titan should be deleted at end of PilotBecomesTitan" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_tonecontroller.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_tonecontroller.nut
new file mode 100644
index 00000000..786eda23
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_tonecontroller.nut
@@ -0,0 +1,189 @@
+untyped
+
+global function ToneController_Init
+
+global function UpdateToneSettings
+
+global function SetAutoExposureMin
+global function SetAutoExposureMax
+global function SetAutoExposureCompensation
+global function SetAutoExposureCompensationBias
+global function SetAutoExposureRate
+global function UseDefaultAutoExposure
+global function SetBloomScale
+
+function ToneController_Init()
+{
+ level.toneController <- CreateEntity( "env_tonemap_controller" )
+ DispatchSpawn( level.toneController )
+
+ AddCallback_EntitiesDidLoad( UpdateToneSettings )
+}
+
+void function UpdateToneSettings()
+{
+ string mapName = GetMapName()
+
+ UseDefaultAutoExposure()
+
+ switch ( mapName )
+ {
+ case "mp_angel_city":
+ SetAutoExposureMin( 1.11 )
+ SetAutoExposureMax( 1.5 )
+ break;
+
+ case "mp_boneyard":
+ SetAutoExposureMin( 1.3 )
+ SetAutoExposureMax( 2.3 )
+ break;
+
+ case "mp_lagoon":
+ SetAutoExposureMin( 0.8 )
+ SetAutoExposureMax( 2.0 )
+ break;
+
+ case "mp_o2":
+ SetAutoExposureMin( 1.0 )
+ SetAutoExposureMax( 2.0 )
+ break;
+
+ case "mp_fracture":
+ SetAutoExposureMin( 1.25 )
+ SetAutoExposureMax( 4.0 )
+ break;
+
+ case "mp_training_ground":
+ SetAutoExposureMin( 0.6 )
+ SetAutoExposureMax( 2.5 )
+ break;
+
+ case "mp_relic":
+ SetAutoExposureMin( 0.9 )
+ SetAutoExposureMax( 2.0 )
+ break;
+
+ case "mp_smugglers_cove":
+ SetAutoExposureMin( 0.5 )
+ SetAutoExposureMax( 0.7 )
+ break;
+
+ case "mp_swampland":
+ SetAutoExposureMin( 0.5 )
+ SetAutoExposureMax( 0.8 )
+ break;
+
+ case "mp_runoff":
+ SetAutoExposureMin( 0.5 )
+ SetAutoExposureMax( 1.0 )
+ break;
+
+ case "mp_wargames":
+ SetAutoExposureMin( 1.0 )
+ SetAutoExposureMax( 1.75 )
+ break;
+
+ case "mp_harmony_mines":
+ SetAutoExposureMin( 1.0 )
+ SetAutoExposureMax( 1.75 )
+ break;
+
+ case "mp_switchback":
+ SetAutoExposureMin( 1.0 )
+ SetAutoExposureMax( 1.75 )
+ break;
+
+ case "mp_sandtrap":
+ SetAutoExposureMin( 0.5 )
+ SetAutoExposureMax( 1.15 )
+ break;
+
+ case "mp_taube_rock_photo_test":
+ SetAutoExposureMin( 1.2 )
+ SetAutoExposureMax( 2.0 )
+ SetBloomScale (1.0)
+ break;
+
+ case "mp_taube_forest_test":
+ SetAutoExposureMin( 1.2 )
+ SetAutoExposureMax( 2.0 )
+ SetBloomScale (1.0)
+ break;
+
+ case "mp_pbr_ball_test":
+ SetAutoExposureMin( 1.2 )
+ SetAutoExposureMax( 2.0 )
+ break;
+
+ case "mp_mendoko_taube_style":
+ SetBloomScale (1.0)
+ break;
+
+ case "mp_kodai_josh_style_01":
+ SetBloomScale (1.0)
+ break;
+
+ case "mp_fake_sky_taube_01":
+ SetBloomScale (1.0)
+ break;
+
+ case "sp_beacon_taube_style":
+ SetBloomScale (1.0)
+ break;
+
+ case "sp_trainer":
+ SetBloomScale( 0.2 )
+ SetAutoExposureMin( 0.8 )
+ SetAutoExposureMax( 0.8 )
+ break
+
+ case "sp_beacon":
+ SetAutoExposureMax( 5.0 )
+ break;
+
+ case "sp_beacon_spoke0":
+ SetAutoExposureMax( 5.0 )
+ break;
+
+ default:
+ UseDefaultAutoExposure()
+ break
+ }
+}
+
+
+
+function SetAutoExposureMin( float value )
+{
+ level.toneController.Fire( "SetAutoExposureMin", value )
+}
+
+function SetAutoExposureMax( float value )
+{
+ level.toneController.Fire( "SetAutoExposureMax", value )
+}
+
+function SetAutoExposureCompensation( float value )
+{
+ level.toneController.Fire( "SetAutoExposureCompensation", value )
+}
+
+function SetAutoExposureCompensationBias( float value )
+{
+ level.toneController.Fire( "SetAutoExposureCompensationBias", value )
+}
+
+function SetAutoExposureRate( float value )
+{
+ level.toneController.Fire( "SetAutoExposureRate", value )
+}
+
+function UseDefaultAutoExposure()
+{
+ level.toneController.Fire( "UseDefaultAutoExposure" )
+}
+
+function SetBloomScale( float value )
+{
+ level.toneController.Fire( "SetBloomScale", value )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_utility_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_utility_mp.gnut
new file mode 100644
index 00000000..ea7d9d44
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_utility_mp.gnut
@@ -0,0 +1,18 @@
+global function Utility_MP_Init
+global function ClientCommand_OnDevnetBugScreenshot
+global function SafeForTitanFall
+
+void function Utility_MP_Init()
+{
+
+}
+
+bool function ClientCommand_OnDevnetBugScreenshot( entity player, array<string> args )
+{
+ return true
+}
+
+bool function SafeForTitanFall( vector dropPoint )
+{
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_vr.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_vr.nut
new file mode 100644
index 00000000..b9759ddf
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_vr.nut
@@ -0,0 +1,66 @@
+untyped
+
+global function VR_Init
+global function VR_GroundTroopsDeathCallback
+
+struct {
+ string vr_settings = ""
+} file
+
+function VR_Init( string settings = "", bool enableDropships = false )
+{
+ if ( reloadingScripts )
+ return
+
+ if ( !enableDropships )
+ FlagSet( "DisableDropships" )
+
+ file.vr_settings = settings
+
+ //AddDeathCallback( "npc_soldier", VR_GroundTroopsDeathCallback )
+ //AddDeathCallback( "npc_spectre", VR_GroundTroopsDeathCallback )
+ //AddDeathCallback( "npc_marvin", VR_GroundTroopsDeathCallback )
+ //AddDeathCallback( "player", VR_GroundTroopsDeathCallback )
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+}
+
+void function EntitiesDidLoad()
+{
+ if ( file.vr_settings.find( "no_evac" ) != null )
+ svGlobal.evacEnabled = false
+
+ if ( file.vr_settings.find( "no_npc" ) != null )
+ {
+ disable_npcs()
+ }
+
+ if ( file.vr_settings.find( "no_titan" ) != null )
+ {
+ Riff_ForceTitanAvailability( eTitanAvailability.Never )
+ FlagSet( "PilotBot" )
+ }
+}
+
+void function VR_GroundTroopsDeathCallback( entity guy, var damageInfo )
+{
+ EmitSoundAtPosition( TEAM_UNASSIGNED, guy.GetOrigin(), "Object_Dissolve" )
+
+ if ( ShouldDoDissolveDeath( guy, damageInfo ) )
+ guy.Dissolve( ENTITY_DISSOLVE_CHAR, Vector( 0, 0, 0 ), 0 )
+}
+
+function ShouldDoDissolveDeath( guy, damageInfo )
+{
+ if ( !guy.IsPlayer() )
+ return true
+
+ // can't dissolve players when they're not playing the game, otherwise when the game starts again they're invisible
+ local gs = GetGameState()
+ if ( gs != eGameState.Playing && gs != eGameState.SuddenDeath && gs != eGameState.Epilogue )
+ {
+ printt( "Skipping player dissolve death because game is not active ( player:", guy, ")" )
+ return false
+ }
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/_lf_maps_shared.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/_lf_maps_shared.gnut
new file mode 100644
index 00000000..d61d6baa
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/_lf_maps_shared.gnut
@@ -0,0 +1,8 @@
+global function SetupLiveFireMaps
+
+// live fire maps don't support alot of things like intros and titans, this makes sure those things are disabled
+void function SetupLiveFireMaps()
+{
+ Riff_ForceTitanAvailability( eTitanAvailability.Never )
+ ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, ClassicMP_DefaultNoIntro_GetLength() )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_angel_city.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_angel_city.nut
new file mode 100644
index 00000000..68b49ad5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_angel_city.nut
@@ -0,0 +1,27 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ Evac_AddLocation( < 2527.889893, -2865.360107, 753.002991 >, < 0, -80.54, 0 > )
+ Evac_AddLocation( < 1253.530029, -554.075012, 811.125 >, < 0, 180, 0 > )
+ Evac_AddLocation( < 2446.989990, 809.364014, 576.0 >, < 0, 90.253, 0 > )
+ Evac_AddLocation( < -2027.430054, 960.395020, 609.007996 >, < 0, 179.604, 0 > )
+
+ Evac_SetSpacePosition( < -1700, -5500, -7600 >, < -3.620642, 270.307129, 0 > )
+
+ // todo: also we need to change the powerup spawns on this map, they use a version from an older patch
+
+ // there are some really busted titan startspawns that are on the fucking other side of the map from where they should be, so we remove them
+ AddSpawnCallback( "info_spawnpoint_titan_start", TrimBadTitanStartSpawns )
+}
+
+void function TrimBadTitanStartSpawns( entity spawn )
+{
+ if ( spawn.GetTeam() == TEAM_MILITIA )
+ return // mil spawns are fine on this map
+
+ vector comparisonOrigin = < 2281.39, -3333.06, 200.031 >
+
+ if ( Distance2D( spawn.GetOrigin(), comparisonOrigin ) >= 2000.0 )
+ spawn.Destroy()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_angel_city_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_angel_city_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_angel_city_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_black_water_canal.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_black_water_canal.nut
new file mode 100644
index 00000000..2e35417f
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_black_water_canal.nut
@@ -0,0 +1,19 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ // there are some really busted titan startspawns that are on the fucking other side of the map from where they should be, so we remove them
+ AddSpawnCallback( "info_spawnpoint_titan_start", TrimBadTitanStartSpawns )
+}
+
+void function TrimBadTitanStartSpawns( entity spawn )
+{
+ vector comparisonOrigin
+ if ( spawn.GetTeam() == TEAM_IMC )
+ comparisonOrigin = < 160.625, 4748.13, -251.447 >
+ else
+ comparisonOrigin = < 1087.13, -4914.88, -199.969 >
+
+ if ( Distance2D( spawn.GetOrigin(), comparisonOrigin ) >= 1000.0)
+ spawn.Destroy()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_black_water_canal_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_black_water_canal_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_black_water_canal_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_coliseum.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_coliseum.nut
new file mode 100644
index 00000000..398b2fc5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_coliseum.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_coliseum_column.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_coliseum_column.nut
new file mode 100644
index 00000000..398b2fc5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_coliseum_column.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_complex3.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_complex3.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_complex3.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_crashsite3.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_crashsite3.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_crashsite3.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_drydock.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_drydock.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_drydock.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_drydock_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_drydock_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_drydock_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_eden.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_eden.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_eden.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_forwardbase_kodai.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_forwardbase_kodai.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_forwardbase_kodai.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_forwardbase_kodai_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_forwardbase_kodai_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_forwardbase_kodai_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_glitch.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_glitch.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_glitch.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_glitch_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_glitch_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_glitch_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_grave.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_grave.nut
new file mode 100644
index 00000000..f4b48f6d
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_grave.nut
@@ -0,0 +1,19 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ // there are some really busted titan startspawns that are on the fucking other side of the map from where they should be, so we remove them
+ AddSpawnCallback( "info_spawnpoint_titan_start", TrimBadTitanStartSpawns )
+}
+
+void function TrimBadTitanStartSpawns( entity spawn )
+{
+ vector comparisonOrigin
+ if ( spawn.GetTeam() == TEAM_IMC )
+ comparisonOrigin = < -2144, -4944, 1999.7 >
+ else
+ comparisonOrigin = < 11026.8, -5163.18, 1885.64 >
+
+ if ( Distance2D( spawn.GetOrigin(), comparisonOrigin ) >= 2000.0 )
+ spawn.Destroy()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_grave_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_grave_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_grave_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_homestead.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_homestead.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_homestead.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_homestead_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_homestead_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_homestead_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut
new file mode 100644
index 00000000..398b2fc5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_meadow.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_meadow.nut
new file mode 100644
index 00000000..398b2fc5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_meadow.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_stacks.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_stacks.nut
new file mode 100644
index 00000000..398b2fc5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_stacks.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_township.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_township.nut
new file mode 100644
index 00000000..398b2fc5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_township.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_traffic.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_traffic.nut
new file mode 100644
index 00000000..398b2fc5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_traffic.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_uma.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_uma.nut
new file mode 100644
index 00000000..398b2fc5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_uma.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_relic02.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_relic02.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_relic02.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_relic02_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_relic02_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_relic02_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_rise.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_rise.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_rise.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_rise_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_rise_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_rise_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_thaw.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_thaw.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_thaw.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_thaw_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_thaw_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_thaw_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut
new file mode 100644
index 00000000..b6c8cfc2
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut
@@ -0,0 +1,7 @@
+untyped
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames_fd.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames_fd.nut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/pintelemetry.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/pintelemetry.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/pintelemetry.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/player_cloak.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/player_cloak.nut
new file mode 100644
index 00000000..8ef7dcd9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/player_cloak.nut
@@ -0,0 +1,184 @@
+untyped //TODO: get rid of player.s.cloakedShotsAllowed. (Referenced in base_gametype_sp, so remove for R5)
+
+global function PlayerCloak_Init
+
+global const CLOAK_FADE_IN = 1.0
+global const CLOAK_FADE_OUT = 1.0
+
+global function EnableCloak
+global function DisableCloak
+global function EnableCloakForever
+global function DisableCloakForever
+
+//=========================================================
+// player_cloak
+//
+//=========================================================
+
+void function PlayerCloak_Init()
+{
+ RegisterSignal( "OnStartCloak" )
+ RegisterSignal( "KillHandleCloakEnd" ) //Somewhat awkward, mainly to smooth out weird interactions with cloak ability and cloak execution
+
+ AddCallback_OnPlayerKilled( AbilityCloak_OnDeath )
+ AddSpawnCallback( "npc_titan", SetCannotCloak )
+}
+
+void function SetCannotCloak( entity ent )
+{
+ ent.SetCanCloak( false )
+}
+
+void function PlayCloakSounds( entity player )
+{
+ EmitSoundOnEntityOnlyToPlayer( player, player, "cloak_on_1P" )
+ EmitSoundOnEntityExceptToPlayer( player, player, "cloak_on_3P" )
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "cloak_sustain_loop_1P" )
+ EmitSoundOnEntityExceptToPlayer( player, player, "cloak_sustain_loop_3P" )
+}
+
+void function EnableCloak( entity player, float duration, float fadeIn = CLOAK_FADE_IN )
+{
+ if ( player.cloakedForever )
+ return
+
+ thread AICalloutCloak( player )
+
+ PlayCloakSounds( player )
+
+ float cloakDuration = duration - fadeIn
+
+ Assert( cloakDuration > 0.0, "Not valid cloak duration. Check that duration is larger than the fadeinTime. When this is not true it will cause the player to be cloaked forever. If you want to do that use EnableCloakForever instead" )
+
+ player.SetCloakDuration( fadeIn, cloakDuration, CLOAK_FADE_OUT )
+
+ player.s.cloakedShotsAllowed = 0
+
+ Battery_StopFXAndHideIconForPlayer( player )
+
+ thread HandleCloakEnd( player )
+}
+
+void function AICalloutCloak( entity player )
+{
+ player.EndSignal( "OnDeath" )
+
+ wait CLOAK_FADE_IN //Give it a beat after cloak has finishing cloaking in
+
+ array<entity> nearbySoldiers = GetNPCArrayEx( "npc_soldier", TEAM_ANY, player.GetTeam(), player.GetOrigin(), 1000 ) //-1 for distance parameter means all spectres in map
+ foreach ( entity grunt in nearbySoldiers )
+ {
+ if ( !IsAlive( grunt ) )
+ continue
+
+ if ( grunt.GetEnemy() == player )
+ {
+ ScriptDialog_PilotCloaked( grunt, player )
+ return //Only need one guy to say this instead of multiple guys
+ }
+ }
+}
+
+void function EnableCloakForever( entity player )
+{
+ player.SetCloakDuration( CLOAK_FADE_IN, -1, CLOAK_FADE_OUT )
+
+ player.cloakedForever = true
+
+ thread HandleCloakEnd( player )
+ PlayCloakSounds( player )
+}
+
+
+void function DisableCloak( entity player, float fadeOut = CLOAK_FADE_OUT )
+{
+ StopSoundOnEntity( player, "cloak_sustain_loop_1P" )
+ StopSoundOnEntity( player, "cloak_sustain_loop_3P" )
+
+ bool wasCloaked = player.IsCloaked( CLOAK_INCLUDE_FADE_IN_TIME )
+
+ if ( fadeOut < CLOAK_FADE_OUT && wasCloaked )
+ {
+ EmitSoundOnEntityOnlyToPlayer( player, player, "cloak_interruptend_1P" )
+ EmitSoundOnEntityExceptToPlayer( player, player, "cloak_interruptend_3P" )
+
+ StopSoundOnEntity( player, "cloak_warningtoend_1P" )
+ StopSoundOnEntity( player, "cloak_warningtoend_3P" )
+ }
+
+ player.SetCloakDuration( 0, 0, fadeOut )
+}
+
+void function DisableCloakForever( entity player, float fadeOut = CLOAK_FADE_OUT )
+{
+ DisableCloak( player, fadeOut )
+ player.cloakedForever = false
+}
+
+
+void function HandleCloakEnd( entity player )
+{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnEMPPilotHit" )
+ player.EndSignal( "OnChangedPlayerClass" )
+ player.Signal( "OnStartCloak" )
+ player.EndSignal( "OnStartCloak" )
+ player.EndSignal( "KillHandleCloakEnd" ) //Calling DisableCloak() after EnableCloak() doesn't kill this thread by design (to allow attacking through cloak etc), so this signal is for when you want to kill this thread
+
+ float duration = player.GetCloakEndTime() - Time()
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( !IsValid( player ) )
+ return
+
+ if ( PlayerHasBattery( player ) )
+ Battery_StartFX( GetBatteryOnBack( player ) )
+
+ StopSoundOnEntity( player, "cloak_sustain_loop_1P" )
+ StopSoundOnEntity( player, "cloak_sustain_loop_3P" )
+ if ( !IsCloaked( player ) )
+ return
+
+ if ( !IsAlive( player ) || !player.IsHuman() )
+ {
+ DisableCloak( player )
+ return
+ }
+
+ float duration = player.GetCloakEndTime() - Time()
+ if ( duration <= 0 )
+ {
+ DisableCloak( player )
+ }
+ }
+ )
+
+ float soundBufferTime = 3.45
+
+ if ( duration > soundBufferTime )
+ {
+ wait ( duration - soundBufferTime )
+ if ( !IsCloaked( player ) )
+ return
+ EmitSoundOnEntityOnlyToPlayer( player, player, "cloak_warningtoend_1P" )
+ EmitSoundOnEntityExceptToPlayer( player, player, "cloak_warningtoend_3P" )
+
+ wait soundBufferTime
+ }
+ else
+ {
+ wait duration
+ }
+}
+
+
+void function AbilityCloak_OnDeath( entity player, entity attacker, var damageInfo )
+{
+ if ( !IsCloaked( player ) )
+ return
+
+ DisableCloak( player, 0 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut
new file mode 100644
index 00000000..26e4c713
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut
@@ -0,0 +1,439 @@
+untyped
+
+global function InitRatings // temp for testing
+
+global function Spawn_Init
+global function SetSpawnsUseFrontline
+global function SetRespawnsEnabled
+global function RespawnsEnabled
+global function SetSpawnpointGamemodeOverride
+global function GetSpawnpointGamemodeOverride
+global function CreateNoSpawnArea
+global function DeleteNoSpawnArea
+
+global function GetCurrentFrontline
+global function FindSpawnPoint
+
+global function RateSpawnpoints_Generic
+
+struct NoSpawnArea
+{
+ string id
+ int blockedTeam
+ int blockOtherTeams
+ vector position
+ float lifetime
+ float radius
+}
+
+struct {
+ bool respawnsEnabled = true
+ string spawnpointGamemodeOverride
+
+ array<vector> preferSpawnNodes
+ table<string, NoSpawnArea> noSpawnAreas
+ bool sidesSwitched = false
+
+ bool frontlineBased = false
+ float lastImcFrontlineRatingTime
+ float lastMilitiaFrontlineRatingTime
+ Frontline& lastImcFrontline
+ Frontline& lastMilitiaFrontline
+} file
+
+void function Spawn_Init()
+{
+ AddCallback_GameStateEnter( eGameState.SwitchingSides, OnSwitchingSides )
+ AddCallback_EntitiesDidLoad( InitPreferSpawnNodes )
+
+ AddSpawnCallback( "info_spawnpoint_human", InitSpawnpoint )
+ AddSpawnCallback( "info_spawnpoint_human_start", InitSpawnpoint )
+ AddSpawnCallback( "info_spawnpoint_titan", InitSpawnpoint )
+ AddSpawnCallback( "info_spawnpoint_titan_start", InitSpawnpoint )
+}
+
+void function InitPreferSpawnNodes()
+{
+ foreach ( entity hardpoint in GetEntArrayByClass_Expensive( "info_hardpoint" ) )
+ {
+ if ( !hardpoint.HasKey( "hardpointGroup" ) )
+ continue
+
+ if ( hardpoint.kv.hardpointGroup != "A" && hardpoint.kv.hardpointGroup != "B" && hardpoint.kv.hardpointGroup != "C" )
+ continue
+
+ file.preferSpawnNodes.append( hardpoint.GetOrigin() )
+ }
+
+ //foreach ( entity frontline in GetEntArrayByClass_Expensive( "info_frontline" ) )
+ // file.preferSpawnNodes.append( frontline.GetOrigin() )
+}
+
+void function InitSpawnpoint( entity spawnpoint )
+{
+ spawnpoint.s.lastUsedTime <- -999
+}
+
+void function SetRespawnsEnabled( bool enabled )
+{
+ file.respawnsEnabled = enabled
+}
+
+bool function RespawnsEnabled()
+{
+ return file.respawnsEnabled
+}
+
+string function CreateNoSpawnArea( int blockSpecificTeam, int blockEnemiesOfTeam, vector position, float lifetime, float radius )
+{
+ NoSpawnArea noSpawnArea
+ noSpawnArea.blockedTeam = blockSpecificTeam
+ noSpawnArea.blockOtherTeams = blockEnemiesOfTeam
+ noSpawnArea.position = position
+ noSpawnArea.lifetime = lifetime
+ noSpawnArea.radius = radius
+
+ // generate an id
+ noSpawnArea.id = UniqueString( "noSpawnArea" )
+
+ thread NoSpawnAreaLifetime( noSpawnArea )
+
+ return noSpawnArea.id
+}
+
+void function NoSpawnAreaLifetime( NoSpawnArea noSpawnArea )
+{
+ wait noSpawnArea.lifetime
+ DeleteNoSpawnArea( noSpawnArea.id )
+}
+
+void function DeleteNoSpawnArea( string noSpawnIdx )
+{
+ try // unsure if the trycatch is necessary but better safe than sorry
+ {
+ delete file.noSpawnAreas[ noSpawnIdx ]
+ }
+ catch ( exception )
+ {}
+}
+
+void function SetSpawnpointGamemodeOverride( string gamemode )
+{
+ file.spawnpointGamemodeOverride = gamemode
+}
+
+string function GetSpawnpointGamemodeOverride()
+{
+ if ( file.spawnpointGamemodeOverride != "" )
+ return file.spawnpointGamemodeOverride
+ else
+ return GAMETYPE
+
+ unreachable
+}
+
+void function SetSpawnsUseFrontline( bool useFrontline )
+{
+ file.frontlineBased = useFrontline
+}
+
+bool function InitRatings( entity player, int team )
+{
+ Frontline frontline = GetCurrentFrontline( team )
+ print( team )
+ print( frontline.friendlyCenter )
+
+ vector offsetOrigin = frontline.friendlyCenter + frontline.combatDir * 256
+ SpawnPoints_InitFrontlineData( offsetOrigin, frontline.combatDir, frontline.line, frontline.friendlyCenter, 2.0 ) // temp
+
+ if ( player != null )
+ SpawnPoints_InitRatings( player, team ) // no idea what the second arg supposed to be lol
+
+ return frontline.friendlyCenter == < 0, 0, 0 > && file.frontlineBased // if true, use startspawns
+}
+
+Frontline function GetCurrentFrontline( int team )
+{
+ float lastFrontlineRatingTime
+ Frontline lastFrontline
+ if ( team == TEAM_IMC )
+ {
+ lastFrontline = file.lastImcFrontline
+ lastFrontlineRatingTime = file.lastImcFrontlineRatingTime
+ }
+ else
+ {
+ lastFrontline = file.lastMilitiaFrontline
+ lastFrontlineRatingTime = file.lastMilitiaFrontlineRatingTime
+ }
+
+ // current frontline is old, get a new one
+ if ( lastFrontlineRatingTime + 20.0 < Time() || lastFrontline.friendlyCenter == < 0, 0, 0 > )
+ {
+ print( "rerating frontline..." )
+ Frontline frontline = GetFrontline( team )
+
+ // this doesn't work lol
+ /*if ( frontline.friendlyCenter == < 0, 0, 0 > )
+ {
+ // recalculate to startspawnpoint positions
+ array<entity> startSpawns = SpawnPoints_GetPilotStart( team )
+
+ vector averagePos
+ vector averageDir
+ foreach ( entity spawnpoint in startSpawns )
+ {
+ averagePos.x += spawnpoint.GetOrigin().x
+ averagePos.y += spawnpoint.GetOrigin().y
+ averagePos.z += spawnpoint.GetOrigin().z
+
+ averageDir.x += spawnpoint.GetAngles().x
+ averageDir.y += spawnpoint.GetAngles().y
+ averageDir.z += spawnpoint.GetAngles().z
+ }
+
+ averagePos.x /= startSpawns.len()
+ averagePos.y /= startSpawns.len()
+ averagePos.z /= startSpawns.len()
+
+ averageDir.x /= startSpawns.len()
+ averageDir.y /= startSpawns.len()
+ averageDir.z /= startSpawns.len()
+
+ print( "average " + averagePos )
+
+ frontline.friendlyCenter = averagePos
+ frontline.origin = averagePos
+ frontline.combatDir = averageDir * -1
+ }*/
+
+ if ( team == TEAM_IMC )
+ {
+ file.lastImcFrontlineRatingTime = Time()
+ file.lastImcFrontline = frontline
+ }
+ else
+ {
+ file.lastMilitiaFrontlineRatingTime = Time()
+ file.lastMilitiaFrontline = frontline
+ }
+
+ lastFrontline = frontline
+ }
+
+ return lastFrontline
+}
+
+entity function FindSpawnPoint( entity player, bool isTitan, bool useStartSpawnpoint )
+{
+ int team = player.GetTeam()
+ if ( file.sidesSwitched )
+ team = GetOtherTeam( team )
+
+ useStartSpawnpoint = InitRatings( player, player.GetTeam() ) || useStartSpawnpoint // force startspawns if no frontline
+ print( "useStartSpawnpoint: " + useStartSpawnpoint )
+
+ array<entity> spawnpoints
+ if ( useStartSpawnpoint )
+ spawnpoints = isTitan ? SpawnPoints_GetTitanStart( team ) : SpawnPoints_GetPilotStart( team )
+ else
+ spawnpoints = isTitan ? SpawnPoints_GetTitan() : SpawnPoints_GetPilot()
+
+ void functionref( int, array<entity>, int, entity ) ratingFunc = isTitan ? GameMode_GetTitanSpawnpointsRatingFunc( GAMETYPE ) : GameMode_GetPilotSpawnpointsRatingFunc( GAMETYPE )
+ ratingFunc( isTitan ? TD_TITAN : TD_PILOT, spawnpoints, team, player )
+
+ if ( isTitan )
+ {
+ if ( useStartSpawnpoint )
+ SpawnPoints_SortTitanStart()
+ else
+ SpawnPoints_SortTitan()
+
+ spawnpoints = useStartSpawnpoint ? SpawnPoints_GetTitanStart( team ) : SpawnPoints_GetTitan()
+ }
+ else
+ {
+ if ( useStartSpawnpoint )
+ SpawnPoints_SortPilotStart()
+ else
+ SpawnPoints_SortPilot()
+
+ spawnpoints = useStartSpawnpoint ? SpawnPoints_GetPilotStart( team ) : SpawnPoints_GetPilot()
+ }
+
+ entity spawnpoint = GetBestSpawnpoint( player, spawnpoints )
+
+ spawnpoint.s.lastUsedTime = Time()
+ player.SetLastSpawnPoint( spawnpoint )
+
+ return spawnpoint
+}
+
+entity function GetBestSpawnpoint( entity player, array<entity> spawnpoints )
+{
+ // not really 100% sure on this randomisation, needs some thought
+ array<entity> validSpawns
+ foreach ( entity spawnpoint in spawnpoints )
+ {
+ if ( IsSpawnpointValid( spawnpoint, player.GetTeam() ) )
+ {
+ validSpawns.append( spawnpoint )
+
+ if ( validSpawns.len() == 3 ) // arbitrary small sample size
+ break
+ }
+ }
+
+ if ( validSpawns.len() == 0 )
+ {
+ // no valid spawns, very bad, so dont care about spawns being valid anymore
+ print( "found no valid spawns! spawns may be subpar!" )
+ foreach ( entity spawnpoint in spawnpoints )
+ {
+ validSpawns.append( spawnpoint )
+
+ if ( validSpawns.len() == 3 ) // arbitrary small sample size
+ break
+ }
+ }
+
+ return validSpawns[ RandomInt( validSpawns.len() ) ] // slightly randomize it
+}
+
+bool function IsSpawnpointValid( entity spawnpoint, int team )
+{
+ //if ( !spawnpoint.HasKey( "ignoreGamemode" ) || ( spawnpoint.HasKey( "ignoreGamemode" ) && spawnpoint.kv.ignoreGamemode == "0" ) ) // used by script-spawned spawnpoints
+ //{
+ // if ( file.spawnpointGamemodeOverride != "" )
+ // {
+ // string gamemodeKey = "gamemode_" + file.spawnpointGamemodeOverride
+ // if ( spawnpoint.HasKey( gamemodeKey ) && ( spawnpoint.kv[ gamemodeKey ] == "0" || spawnpoint.kv[ gamemodeKey ] == "" ) )
+ // return false
+ // }
+ // else if ( GameModeRemove( spawnpoint ) )
+ // return false
+ //}
+
+ if ( Riff_FloorIsLava() && spawnpoint.GetOrigin().z < GetLethalFogTop() )
+ return false
+
+ int compareTeam = spawnpoint.GetTeam()
+ if ( file.sidesSwitched && ( compareTeam == TEAM_MILITIA || compareTeam == TEAM_IMC ) )
+ compareTeam = GetOtherTeam( compareTeam )
+
+ if ( spawnpoint.GetTeam() > 0 && compareTeam != team && !IsFFAGame() )
+ return false
+
+ if ( spawnpoint.IsOccupied() )
+ return false
+
+ foreach ( k, NoSpawnArea noSpawnArea in file.noSpawnAreas )
+ {
+ if ( Distance( noSpawnArea.position, spawnpoint.GetOrigin() ) > noSpawnArea.radius )
+ continue
+
+ if ( noSpawnArea.blockedTeam != TEAM_INVALID && noSpawnArea.blockedTeam == team )
+ return false
+
+ if ( noSpawnArea.blockOtherTeams != TEAM_INVALID && noSpawnArea.blockOtherTeams != team )
+ return false
+ }
+
+ array<entity> projectiles = GetProjectileArrayEx( "any", TEAM_ANY, TEAM_ANY, spawnpoint.GetOrigin(), 400 )
+ foreach ( entity projectile in projectiles )
+ if ( projectile.GetTeam() != team )
+ return false
+
+ if ( Time() - spawnpoint.s.lastUsedTime <= 1.0 )
+ return false
+
+ return true
+}
+
+void function RateSpawnpoints_Generic( int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ // calculate ratings for preferred nodes
+ // this tries to prefer nodes with more teammates, then activity on them
+ // todo: in the future it might be good to have this prefer nodes with enemies up to a limit of some sort
+ // especially in ffa modes i could deffo see this falling apart a bit rn
+ // perhaps dead players could be used to calculate some sort of activity rating? so high-activity points with an even balance of friendly/unfriendly players are preferred
+ array<float> preferSpawnNodeRatings
+ foreach ( vector preferSpawnNode in file.preferSpawnNodes )
+ {
+ float currentRating
+
+ // this seems weird, not using rn
+ //Frontline currentFrontline = GetCurrentFrontline( team )
+ //if ( !IsFFAGame() || currentFrontline.friendlyCenter != < 0, 0, 0 > )
+ // currentRating += max( 0.0, ( 1000.0 - Distance2D( currentFrontline.origin, preferSpawnNode ) ) / 200 )
+
+ foreach ( entity nodePlayer in GetPlayerArray() )
+ {
+ float currentChange = 0.0
+
+ // the closer a player is to a node the more they matter
+ float dist = Distance2D( preferSpawnNode, nodePlayer.GetOrigin() )
+ if ( dist > 600.0 )
+ continue
+
+ currentChange = ( 600.0 - dist ) / 5
+ if ( player == nodePlayer )
+ currentChange *= -3 // always try to stay away from places we've already spawned
+ else if ( !IsAlive( nodePlayer ) ) // dead players mean activity which is good, but they're also dead so they don't matter as much as living ones
+ currentChange *= 0.6
+ if ( nodePlayer.GetTeam() != player.GetTeam() ) // if someone isn't on our team and alive they're probably bad
+ {
+ if ( IsFFAGame() ) // in ffa everyone is on different teams, so this isn't such a big deal
+ currentChange *= -0.2
+ else
+ currentChange *= -0.6
+ }
+
+ currentRating += currentChange
+ }
+
+ preferSpawnNodeRatings.append( currentRating )
+ }
+
+ foreach ( entity spawnpoint in spawnpoints )
+ {
+ float currentRating
+ float petTitanModifier
+ // scale how much a given spawnpoint matters to us based on how far it is from each node
+ bool spawnHasRecievedInitialBonus = false
+ for ( int i = 0; i < file.preferSpawnNodes.len(); i++ )
+ {
+ // bonus if autotitan is nearish
+ if ( IsAlive( player.GetPetTitan() ) && Distance( player.GetPetTitan().GetOrigin(), file.preferSpawnNodes[ i ] ) < 1200.0 )
+ petTitanModifier += 10.0
+
+ float dist = Distance2D( spawnpoint.GetOrigin(), file.preferSpawnNodes[ i ] )
+ if ( dist > 750.0 )
+ continue
+
+ if ( dist < 600.0 && !spawnHasRecievedInitialBonus )
+ {
+ currentRating += 10.0
+ spawnHasRecievedInitialBonus = true // should only get a bonus for simply being by a node once to avoid over-rating
+ }
+
+ currentRating += ( preferSpawnNodeRatings[ i ] * ( ( 750.0 - dist ) / 75 ) ) + max( RandomFloat( 1.25 ), 0.9 )
+ if ( dist < 250.0 ) // shouldn't get TOO close to an active node
+ currentRating *= 0.7
+
+ if ( spawnpoint.s.lastUsedTime < 10.0 )
+ currentRating *= 0.7
+ }
+
+ float rating = spawnpoint.CalculateRating( checkClass, team, currentRating, currentRating + petTitanModifier )
+ //print( "spawnpoint at " + spawnpoint.GetOrigin() + " has rating: " + )
+
+ if ( rating != 0.0 || currentRating != 0.0 )
+ print( "rating = " + rating + ", internal rating = " + currentRating )
+ }
+}
+
+void function OnSwitchingSides()
+{
+ file.sidesSwitched = true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_debug.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_debug.gnut
new file mode 100644
index 00000000..75ec8cf2
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_debug.gnut
@@ -0,0 +1,6 @@
+global function SpawnDebug_Init
+
+void function SpawnDebug_Init()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_on_friendly.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_on_friendly.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_on_friendly.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_wave.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_wave.gnut
new file mode 100644
index 00000000..b8895c55
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_wave.gnut
@@ -0,0 +1,6 @@
+global function SpawnWave_Init
+
+void function SpawnWave_Init()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_wave_dropship.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_wave_dropship.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn_wave_dropship.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/_leeching.gnut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_leeching.gnut
new file mode 100644
index 00000000..c9d1f9dd
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_leeching.gnut
@@ -0,0 +1,493 @@
+global function Leeching_Init
+
+global function DoLeechAnimEvent
+global function DoLeech
+global function TryLeechAbortCallback
+global function StartLeechingProgress
+global function StopLeechingProgress
+global function EnableLeeching
+global function DisableLeeching
+global function MarvinWeaponsFree
+global function GetLeechedEnts
+global function DataKnifeSuccessSounds
+global function DataKnifeCanceledSounds
+
+global function GetTeamLeechedEnts
+
+// _leeching.nut
+// sets up global stuff for leeching
+global const MARVIN_EMOTE_SOUND_HAPPY = "diag_spectre_gs_LeechEnd_01_1"
+global const MARVIN_EMOTE_SOUND_SAD = "diag_spectre_gs_LeechAborted_01_1"
+global const MARVIN_EMOTE_SOUND_PAIN = "diag_spectre_gs_LeechStart_01_1"
+
+#if MP
+global const bool WIFI_HACK_OVERFLOW_DIES = false // Kill a random leeched ent from within the team, exluding the current target, to create a new team member when hacked
+#elseif SP
+global const bool WIFI_HACK_OVERFLOW_DIES = true
+#endif
+
+struct LeechFuncInfo
+{
+ string classname
+ void functionref(entity,entity) DoLeech
+ void functionref(entity,entity) LeechStart
+ void functionref(entity,entity) LeechAbort
+}
+
+struct
+{
+ table<string, LeechFuncInfo> leechFuncs
+} file
+
+void function Leeching_Init()
+{
+ RegisterSignal( "OnLeeched" )
+ RegisterSignal( "OnStartLeech" )
+ RegisterSignal( "OnStopLeeched" )
+ RegisterSignal( "EnableLeeching" )
+
+ // Spectre leech
+ LeechFuncInfo spectre
+ spectre.classname = "npc_spectre"
+ spectre.DoLeech = LeechGeneric
+ spectre.LeechStart = LeechStartGeneric
+ spectre.LeechAbort = LeechAbortGeneric
+ file.leechFuncs[spectre.classname] <- spectre
+
+ // Reaper leech
+ LeechFuncInfo reaper
+ reaper.classname = "npc_super_spectre"
+ reaper.DoLeech = LeechGeneric
+ reaper.LeechStart = LeechStartGeneric
+ reaper.LeechAbort = LeechAbortGeneric
+ file.leechFuncs[reaper.classname] <- reaper
+
+ // Drone leech
+ LeechFuncInfo drone
+ drone.classname = "npc_drone"
+ drone.DoLeech = LeechGeneric
+ drone.LeechStart = LeechStartGeneric
+ drone.LeechAbort = LeechAbortGeneric
+ file.leechFuncs[drone.classname] <- drone
+
+ // Gunship leech
+ LeechFuncInfo gunship
+ gunship.classname = "npc_gunship"
+ gunship.DoLeech = LeechGeneric
+ gunship.LeechStart = LeechStartGeneric
+ gunship.LeechAbort = LeechAbortGeneric
+ file.leechFuncs[gunship.classname] <- gunship
+
+ LeechFuncInfo relay
+ relay.classname = "logic_relay"
+ relay.DoLeech = Leech_LogicRelay
+ file.leechFuncs[relay.classname] <- relay
+
+ LeechFuncInfo physbox
+ physbox.classname = "func_physbox"
+ physbox.DoLeech = Leech_FuncPhysbox
+ file.leechFuncs[physbox.classname] <- physbox
+}
+
+void function EnableLeeching( entity self )
+{
+ self.SetUsePrompts( "#DEFAULT_HACK_HOLD_PROMPT", "#DEFAULT_HACK_HOLD_PROMPT" )
+
+ Leech_SetLeechable( self )
+}
+
+void function DisableLeeching( entity self )
+{
+ if ( !IsValid_ThisFrame( self ) )
+ return
+
+ self.SetUsePrompts( " ", " " )
+
+ Leech_ClearLeechable( self )
+}
+
+void function StartLeechingProgress( entity self, entity leecher )
+{
+ self.Signal( "OnStartLeech" )
+ leecher.Signal( "OnStartLeech" )
+ self.ai.leechInProgress = true
+ self.ai.leechStartTime = Time()
+
+ TryLeechStartCallback( self, leecher )
+}
+
+void function StopLeechingProgress( entity self )
+{
+ self.ai.leechInProgress = false
+ self.ai.leechStartTime = -1
+}
+
+// called when any entity gets leeched
+void function DoLeechAnimEvent( entity self )
+{
+ entity leecher = expect entity( GetOptionalAnimEventVar( self, "leech_switchteam" ) )
+
+ DoLeech( self, leecher )
+}
+
+void function DoLeech( entity self, entity leecher )
+{
+ if ( !IsLeechable( self ) )
+ EnableLeeching( self )
+
+ Assert( "s" in self, "Self " + self + " has no .s" )
+ Assert( leecher )
+
+ // DEPRECATED- no scripts are currently using the results.player functionality- slayback
+ // logic_relays get Triggered when the object is leeched
+ //local results = {}
+ //results.player <- leecher
+ //self.Signal( "OnLeeched", results )
+ //leecher.Signal( "OnLeeched", results )
+
+ Signal( self, "OnLeeched" )
+ Signal( leecher, "OnLeeched" )
+
+ //DisableLeeching( self )
+
+ //_EnableLeechedPointMessage()
+
+ if ( leecher.IsPlayer() )
+ {
+ if ( self.IsNPC() )
+ self.SetBossPlayer( leecher )
+
+ TableRemoveDeadByKey( leecher.p.leechedEnts )
+
+ leecher.p.leechedEnts[ self ] <- self
+
+ // this will kill a random leeched ent from within the team, exluding the current target.
+ if ( WIFI_HACK_OVERFLOW_DIES )
+ ReleaseLeechOverflow( leecher, self )
+ }
+
+ if ( self.IsNPC() )
+ {
+ SetTeam( self, leecher.GetTeam() )
+ SetSquad( self, "" )
+ self.SetAutoSquad()
+ self.ClearPotentialThreatPos()
+ self.DisableBehavior( "Assault" )
+
+ foreach ( trigger in self.e.sonarTriggers )
+ {
+ OnSonarTriggerLeaveInternal( trigger, self )
+ }
+
+ #if DEV
+ // if crosshair spawned, switch squad so he isn't mixed in a squad with opposing team spectres
+ string squadname = expect string( self.kv.squadname )
+ if ( squadname.find( "crosshairSpawnSquad" ) != null )
+ self.SetSquad( "crosshairSpawnSquad_team_" + self.GetTeam() + "_" + self.GetClassName() )
+ #endif
+ }
+
+ // call a class specific leeching function for custom behavior
+ string targetCN = self.GetClassName()
+ if ( targetCN in file.leechFuncs )
+ {
+ LeechFuncInfo info = file.leechFuncs[ targetCN ]
+
+ // Assert( "DoLeech" in file.leechFuncs[ targetCN ] ) // not sure how to check legit functionref -slayback
+ void functionref(entity,entity) classLeechingFunc = file.leechFuncs[ targetCN ].DoLeech
+ thread classLeechingFunc( self, leecher )
+ }
+}
+
+array<entity> function GetTeamLeechedEnts( int team )
+{
+ array<entity> players = GetPlayerArrayOfTeam( team )
+ int totalCount = 0
+
+ array<entity> leechedArray
+ foreach ( player in players )
+ {
+ if ( IsValid( player ) && !player.IsBot() )
+ leechedArray.extend( GetLeechedEnts( player ) )
+ }
+
+ return leechedArray
+}
+
+array<entity> function GetLeechedEnts( entity leecher = null )
+{
+ array<entity> ents
+
+ foreach ( entity ent in leecher.p.leechedEnts )
+ {
+ if ( IsAlive( ent ) )
+ ents.append( ent )
+ }
+
+ return ents
+}
+
+void function TryLeechStartCallback( entity self, entity leecher )
+{
+ string leechtargetCN = self.GetClassName()
+ if ( leechtargetCN in file.leechFuncs )
+ {
+ if ( "LeechStart" in file.leechFuncs[ leechtargetCN ] )
+ {
+ void functionref(entity,entity) leechStartFunc = file.leechFuncs[ leechtargetCN ].LeechStart
+ thread leechStartFunc( self, leecher )
+ }
+ }
+}
+
+void function TryLeechAbortCallback( entity self, entity leecher )
+{
+ string leechtargetCN = self.GetClassName()
+ if ( leechtargetCN in file.leechFuncs )
+ {
+ if ( "LeechAbort" in file.leechFuncs[ leechtargetCN ] )
+ {
+ void functionref(entity,entity) leechAbortFunc = file.leechFuncs[ leechtargetCN ].LeechAbort
+ thread leechAbortFunc( self, leecher )
+ }
+ }
+}
+
+void function DataKnifeSuccessSounds( entity player )
+{
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "dataknife_complete", "dataknife_complete_3p", player, player )
+}
+
+void function DataKnifeCanceledSounds( entity player )
+{
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "dataknife_aborted", "dataknife_aborted_3p", player, player )
+}
+
+
+// --- CLASS SPECIFIC LEECH FUNCTIONS ---
+void function Leech_LogicRelay( entity self, entity leecher )
+{
+ Assert( self.GetClassName() == "logic_relay" )
+
+ // logic_relays get Triggered when the object is leeched
+ EntFire( self, "Trigger" )
+}
+
+void function Leech_FuncPhysbox( entity self, entity leecher )
+{
+ Assert( self.GetClassName() == "func_physbox" )
+
+ EntFire( self, "FireUser1" )
+}
+
+
+void function MarvinWeaponsFree( entity self )
+{
+ Assert( IsAlive( self ), self + " is dead, not alive!" )
+
+ // already have a weapon
+ if ( !self.GetActiveWeapon() )
+ return
+
+ self.EndSignal( "OnStopLeeched" )
+ self.EndSignal( "OnDeath" )
+ self.EndSignal( "OnTakeWeapon" )
+
+ OnThreadEnd(
+ function () : ( self )
+ {
+ if ( !IsAlive( self ) )
+ return
+ }
+ )
+
+ // its combat, time to get the hate on
+ EntFire( self, "UnholsterWeapon" )
+
+ WaitForever()
+}
+
+
+void function LeechStart_Marvin( entity self, entity leecher )
+{
+ //self.SetSkin( 4 )
+ EmitSoundOnEntity( self, MARVIN_EMOTE_SOUND_PAIN )
+}
+
+void function LeechAbort_Marvin( entity self, entity leecher )
+{
+ //self.SetSkin( 1 ) // happy
+ EmitSoundOnEntity( self, MARVIN_EMOTE_SOUND_SAD )
+}
+
+
+// Spectre leech
+
+void function Leech_Spectre( entity self, entity leecher )
+{
+ thread Leech_SpectreThread( self, leecher )
+}
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+void function Leech_SpectreThread( entity self, entity leecher )
+{
+ Assert( self.GetClassName() == "npc_spectre" )
+
+ self.EndSignal( "OnDestroy" )
+ self.EndSignal( "OnDeath" )
+ self.EndSignal( "OnLeeched" )
+
+ EmitSoundOnEntity( self, MARVIN_EMOTE_SOUND_HAPPY )
+
+ Assert( leecher.IsPlayer() )
+
+ leecher.EndSignal( "OnDestroy" )
+
+ AddPlayerScore( leecher, "LeechSpectre" )
+
+ float timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( leecher, SFLAG_HUNTER_SPECTRE ) )
+ timerCredit *= 2.5
+ DecrementBuildTimer( leecher, timerCredit )
+
+ NPCFollowsPlayer( self, leecher )
+
+ OnThreadEnd(
+ function() : ( self, leecher )
+ {
+ // leecher is still connected so don't kill the spectre
+ if ( IsValid( leecher ) && !IsDisconnected( leecher ) )
+ return
+
+ // leecher is disconnected so kill the spectre
+ if ( IsAlive( self ) )
+ self.Die()
+ }
+ )
+
+ foreach ( callbackFunc in svGlobal.onLeechedCustomCallbackFunc )
+ {
+ callbackFunc( self, leecher )
+ }
+
+ WaitForever()
+}
+///////////////////////////////////////////////////////////////////////////////////////////////////////////
+void function LeechGeneric( entity self, entity leecher )
+{
+ thread LeechGenericThread( self, leecher )
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Run after the npc is successfully leeched
+// HACK: Should make this used by all leeched ents to avoid copy/pasted duplication. Will switch Spectre over to use this soon
+void function LeechGenericThread( entity self, entity leecher )
+{
+ Assert( IsValid( self ) )
+ self.EndSignal( "OnDestroy" )
+ self.EndSignal( "OnDeath" )
+ self.EndSignal( "OnLeeched" )
+
+ Assert( leecher.IsPlayer() )
+ leecher.EndSignal( "OnDestroy" )
+
+ string leechSound
+ float timerCredit
+
+ //--------------------------------------------
+ // Handle class-specific stuff
+ //---------------------------------------------
+ switch ( self.GetClassName() )
+ {
+ case "npc_spectre":
+ leechSound = MARVIN_EMOTE_SOUND_HAPPY
+ AddPlayerScore( leecher, "LeechSpectre" )
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( leecher, SFLAG_HUNTER_SPECTRE ) )
+ timerCredit *= 2.5
+ break
+ case "npc_super_spectre":
+ leechSound = MARVIN_EMOTE_SOUND_HAPPY
+ AddPlayerScore( leecher, "LeechSuperSpectre" )
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ break
+ case "npc_drone":
+ leechSound = MARVIN_EMOTE_SOUND_HAPPY
+ AddPlayerScore( leecher, "LeechDrone" )
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ break
+ case "npc_gunship":
+ leechSound = MARVIN_EMOTE_SOUND_HAPPY
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ break
+ default:
+ Assert( 0, "Unhandled hacked entity: " + self.GetClassName() )
+ }
+
+ EmitSoundOnEntity( self, leechSound )
+ DecrementBuildTimer( leecher, timerCredit )
+
+ // Multiplayer the leeched NPCs still follow the player, but in SP we don't want them to
+ if ( IsMultiplayer() )
+ NPCFollowsPlayer( self, leecher )
+
+ //--------------------------------------------
+ // Any leech custom callback funcs?
+ //---------------------------------------------
+ foreach ( callbackFunc in svGlobal.onLeechedCustomCallbackFunc )
+ {
+ callbackFunc( self, leecher )
+ }
+
+}
+
+/////////////////////////////////////////////////////////////////////////////////////
+void function LeechStartGeneric( entity self, entity leecher )
+{
+ string leechStartSound
+
+ switch( self.GetClassName() )
+ {
+ case "npc_spectre":
+ leechStartSound = MARVIN_EMOTE_SOUND_PAIN
+ break
+ case "npc_super_spectre":
+ leechStartSound = MARVIN_EMOTE_SOUND_PAIN
+ break
+ case "npc_drone":
+ leechStartSound = MARVIN_EMOTE_SOUND_PAIN
+ break
+ case "npc_gunship":
+ leechStartSound = MARVIN_EMOTE_SOUND_PAIN
+ break
+ }
+ Assert( leechStartSound != "", "Couldn't find leechStartSound for: " + self )
+
+ EmitSoundOnEntity( self, leechStartSound )
+}
+/////////////////////////////////////////////////////////////////////////////////////
+void function LeechAbortGeneric( entity self, entity leecher )
+{
+ string leechAbortSound
+
+ switch( self.GetClassName() )
+ {
+ case "npc_spectre":
+ leechAbortSound = MARVIN_EMOTE_SOUND_SAD
+ break
+ case "npc_super_spectre":
+ leechAbortSound = MARVIN_EMOTE_SOUND_SAD
+ break
+ case "npc_drone":
+ leechAbortSound = MARVIN_EMOTE_SOUND_SAD
+ break
+ case "npc_gunship":
+ leechAbortSound = MARVIN_EMOTE_SOUND_SAD
+ break
+ }
+ Assert( leechAbortSound != "", "Couldn't find leechAbortSound for: " + self )
+
+ EmitSoundOnEntity( self, leechAbortSound )
+
+}
+
+// --- END CLASS SPECIFIC LEECH FUNCTIONS --- \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/_pilot_leeching.gnut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_pilot_leeching.gnut
new file mode 100644
index 00000000..596ca711
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_pilot_leeching.gnut
@@ -0,0 +1,610 @@
+global function PlayerLeeching_Init
+
+global function LeechSurroundingSpectres
+global function CodeCallback_LeechStart
+global function LeechPropagate
+global function ReleaseLeechOverflow
+global function IsBeingLeeched
+
+// 384 ~ 32 feet
+// 256 ~ 21.3 feet
+// 192 ~ 16 feet
+// 128 ~ 10.6 feet
+const float SPECTRE_LEECH_SURROUNDING_RANGE = 384.0
+
+#if MP
+const int GLOBAL_LEECH_LIMIT = 100 // per team
+const int MAX_LEECHABLE = 100 // per player
+const bool PROPAGATE_ON_LEECH = true
+#elseif SP
+const int GLOBAL_LEECH_LIMIT = 8
+const int MAX_LEECHABLE = 4
+const bool PROPAGATE_ON_LEECH = false
+#endif
+
+void function PlayerLeeching_Init()
+{
+ #if SERVER
+ PrecacheModel( DATA_KNIFE_MODEL )
+ #endif
+}
+
+void function PlayerStopLeeching( entity player, entity target )
+{
+ Assert( target != null )
+ Assert( player.p.leechTarget == target )
+
+ StopLeechingProgress( player.p.leechTarget )
+
+ player.p.leechTarget = null
+}
+
+void function CodeCallback_LeechStart( entity player, entity target )
+{
+ thread LeechStartThread( player, target )
+}
+
+void function LeechStartThread( entity player, entity target )
+{
+ if ( !IsAlive( target ) )
+ return
+
+ if ( !IsAlive( player ) )
+ return
+
+ LeechActionInfo action = FindLeechAction( player, target )
+ if ( !action.isValid )
+ return
+
+/*
+ if ( player.ContextAction_IsActive()
+ || player.ContextAction_IsActive()
+ || target.ContextAction_IsActive() )
+ {
+ return
+ }
+*/
+
+ player.EndSignal( "ScriptAnimStop" )
+ player.EndSignal( "OnDeath" )
+ target.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDeath" )
+ target.EndSignal( "ScriptAnimStop" )
+
+ StartLeechingProgress( target, player )
+
+ LeechData e
+ e.playerStartOrg = player.GetOrigin()
+ e.targetStartPos = target.GetOrigin()
+
+ OnThreadEnd
+ (
+ function() : ( e, player, target )
+ {
+ if ( IsValid( player ) )
+ {
+ player.SetSyncedEntity( null )
+ if ( player.ContextAction_IsLeeching() )
+ player.Event_LeechEnd()
+
+ // reset to start position in case animation moves us at all
+ //player.SetOrigin( e.playerStartOrg )
+ player.Anim_Stop()
+ player.UnforceStand()
+
+ // done with first person anims
+ ClearPlayerAnimViewEntity( player )
+ DeployAndEnableWeapons( player )
+ }
+
+ if ( IsValid( target ) )
+ {
+ if ( !e.success )
+ {
+ if ( IsValid( player ) )
+ {
+ TryLeechAbortCallback( target, player ) //Make "failed leech" sounds play here after exiting leech animation
+ }
+ }
+
+ #if MP
+ target.SetUsable()
+ #endif
+ target.SetNoTarget( false )
+ target.SetNoTargetSmartAmmo( false )
+ target.Anim_Stop()
+ target.ClearParent()
+ if ( IsAlive( target ) )
+ {
+ // Note that e.targetStartPos is not guarranteed to be a safe spot since we can have moving geo in the game now
+ PutEntityInSafeSpot( target, null, null, target.GetOrigin(), target.GetOrigin() )
+ }
+
+ if ( target.ContextAction_IsLeeching() )
+ target.Event_LeechEnd()
+
+ }
+
+ foreach ( knife in e.knives )
+ {
+ if ( IsValid( knife ) )
+ {
+ knife.Destroy()
+ }
+
+ }
+
+ if ( IsValid( player ) && player.p.leechTarget )
+ {
+ PlayerStopLeeching( player, player.p.leechTarget )
+ }
+
+ if ( IsValid( e.ref ) )
+ {
+ if ( IsValid( player ) )
+ player.ClearParent()
+
+ if ( IsValid( target ) )
+ target.ClearParent()
+
+ //printt( "kill the ref" )
+ if ( IsValid( e.ref ) && !e.ref.IsPlayer() )
+ e.ref.Destroy()
+ }
+ }
+ )
+
+ Assert( player.p.leechTarget == null )
+ player.p.leechTarget = target
+ player.Event_LeechStart()
+ target.Event_LeechStart()
+ player.ForceStand()
+ HolsterAndDisableWeapons( player )
+
+ float leechTime = svGlobal.defaultPilotLeechTime
+ if ( PlayerHasPassive( player, ePassives.PAS_FAST_HACK ) )
+ leechTime *= 0.85
+
+ e.leechTime = leechTime
+
+ #if MP
+ target.UnsetUsable()
+ #endif
+ target.SetNoTarget( true )
+ target.SetNoTargetSmartAmmo( true )
+
+ if ( IsSpectre( target ) )
+ TellSquadmatesSpectreIsGettingLeeched( target, player )
+
+ waitthread PlayerLeechTargetAnimation( player, target, action, e )
+
+ e.leechStartTime = Time()
+ Remote_CallFunction_Replay( player, "ServerCallback_DataKnifeStartLeech", e.leechTime )
+ waitthread WaittillFinishedLeeching( player, target, e )
+
+ if ( e.success )
+ {
+ thread DataKnifeSuccessSounds( player )
+
+ DoLeech( target, player )
+ PlayerStopLeeching( player, target )
+
+ // this will kill a random leeched ent from within the team, exluding the current target. When it's not done elsewhere
+ if ( !WIFI_HACK_OVERFLOW_DIES )
+ ReleaseLeechOverflow( player, target )
+
+ //this is called when the player leeches - not when the system is leeching other spectres
+ if ( PROPAGATE_ON_LEECH && IsSpectre( target ) )
+ LeechSurroundingSpectres( target.GetOrigin(), player )
+ }
+ else
+ {
+ DataKnifeCanceledSounds( player )
+ Remote_CallFunction_Replay( player, "ServerCallback_DataKnifeCancelLeech" )
+ PlayerStopLeeching( player, player.p.leechTarget )
+ }
+
+ waitthread PlayerExitLeechingAnim( player, target, action, e )
+}
+
+void function TellSquadmatesSpectreIsGettingLeeched( entity spectre, entity player )
+{
+ string squadName = expect string( spectre.kv.squadname )
+ if ( squadName == "" )
+ return
+
+ array<entity> squad = GetNPCArrayBySquad( squadName )
+ squad.removebyvalue( spectre )
+
+ foreach ( squadMate in squad )
+ {
+ //printt( "Setting enemy of " + squadMate + " to player: " + player )
+ squadMate.SetEnemyLKP( player, player.GetOrigin() )
+ }
+}
+
+void function ReleaseLeechOverflow( entity player, entity lastLeeched )
+{
+ array<entity> teamLeechedEnts = GetTeamLeechedEnts( player.GetTeam() )
+ array<entity> leechedEnts = GetLeechedEnts( player )
+ int globalOverflow = GLOBAL_LEECH_LIMIT - teamLeechedEnts.len()
+ int playerOverflow = MAX_LEECHABLE - leechedEnts.len()
+
+ int overflow = minint( globalOverflow, playerOverflow )
+
+ if ( overflow >= 0 )
+ return
+
+ overflow = abs( overflow )
+
+ teamLeechedEnts.randomize()
+ foreach ( ent in teamLeechedEnts )
+ {
+ if ( lastLeeched == ent )
+ continue
+
+ entity owner = ent.GetBossPlayer()
+ Assert( owner.IsPlayer() )
+
+
+ // I think it's better to kill the overflow then have it become an enemy again.
+ ent.Die()
+
+ delete owner.p.leechedEnts[ ent ]
+ overflow--
+
+ if ( overflow == 0 )
+ break
+ }
+
+ Assert( overflow == 0 )
+}
+
+
+int function GetMaxNumberOfLeechedEnts( entity player )
+{
+ int teamLeechedCount = GetTeamLeechedEnts( player.GetTeam() ).len()
+ int leechedEntsCount = GetLeechedEnts( player ).len()
+ int teamLimit = maxint( 0, GLOBAL_LEECH_LIMIT - teamLeechedCount )
+ int maxSize = maxint( 0, MAX_LEECHABLE - leechedEntsCount )
+ maxSize = minint( teamLimit, maxSize )
+
+ return maxSize
+}
+
+void function LeechSurroundingSpectres( vector origin, entity player )
+{
+ array<entity> enemySpectreArray = GetNPCArrayEx( "npc_spectre", TEAM_ANY, player.GetTeam(), player.GetOrigin(), SPECTRE_LEECH_SURROUNDING_RANGE )
+
+ if ( !enemySpectreArray.len() )
+ return
+
+ // don't resize the array if we should kill the overflow instead
+ if ( !WIFI_HACK_OVERFLOW_DIES )
+ {
+ int maxSize = GetMaxNumberOfLeechedEnts( player )
+ int newSize = minint( enemySpectreArray.len(), maxSize )
+
+ enemySpectreArray.resize( newSize, null )
+ }
+
+ foreach ( spectre in enemySpectreArray )
+ {
+ thread LeechPropagate( spectre, player )
+ }
+
+ if ( enemySpectreArray.len() )
+ {
+ if ( PlayerHasPassive( player, ePassives.PAS_WIFI_SPECTRE ) )
+ {
+ EmitSoundOnEntity( player, "BurnCard_WiFiVirus_TurnSpectre" )
+ printt( "play BurnCard_WiFiVirus_TurnSpectre" )
+ }
+ }
+}
+
+void function LeechPropagate( entity spectre, entity player )
+{
+ if ( spectre.ContextAction_IsActive() )
+ return
+
+ if ( !spectre.IsInterruptable() )
+ return
+
+ if ( spectre.GetParent() )
+ return
+
+ if ( !Leech_IsLeechable( spectre ) )
+ return
+
+ player.EndSignal( "OnDestroy" )
+ spectre.EndSignal( "OnDestroy" )
+ spectre.EndSignal( "OnDeath" )
+
+ spectre.Event_LeechStart()
+
+ AddAnimEvent( spectre, "leech_switchteam", DoLeechAnimEvent, player )
+
+ OnThreadEnd(
+ function() : ( spectre )
+ {
+ if ( IsValid( spectre ) )
+ {
+ DeleteAnimEvent( spectre, "leech_switchteam" )
+
+ if ( spectre.ContextAction_IsLeeching() )
+ spectre.Event_LeechEnd()
+ }
+ }
+ )
+
+ spectre.Anim_Stop()
+ waitthread PlayAnim( spectre, "sp_reboot" )
+ spectre.SetVelocity( Vector(0,0,0) )
+}
+
+void function WaittillFinishedLeeching( entity player, entity target, LeechData e )
+{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "ScriptAnimStop" )
+ target.EndSignal( "OnDeath" )
+
+ if ( !player.UseButtonPressed() )
+ return
+
+ float waitTime = e.leechTime
+ float timePassed = Time() - e.leechStartTime
+ waitTime -= timePassed
+ if ( waitTime > 0 )
+ {
+ float startTime = Time()
+ while ( Time() < startTime + waitTime && player.UseButtonPressed() )
+ {
+ WaitFrame()
+ }
+ }
+
+ if ( player.UseButtonPressed() )
+ e.success = true
+}
+
+/////////////////////////////////////////////////////////////
+bool function IsLeechTargetUsedAsAnimNode( entity target )
+{
+ return target.AISetting_LeechAnimTag() != ""
+}
+
+/////////////////////////////////////////////////////////////
+void function PlayerLeechTargetAnimation( entity player, entity target, LeechActionInfo action, LeechData e )
+{
+ Assert( action.isValid )
+ vector targetStartOrg = target.GetOrigin()
+ vector targetStartAng = target.GetAngles()
+
+ vector initialPlayerPosition = player.GetOrigin()
+ vector initialTargetPosition = target.GetOrigin()
+
+ vector endOrigin = target.GetOrigin()
+ vector startOrigin = player.GetOrigin()
+ vector refVec = endOrigin - startOrigin
+ string animTag
+
+ FirstPersonSequenceStruct playerSequence
+
+ //---------------------------------------------------------
+ // Leech anims played on the leech target, or at player position?
+ //---------------------------------------------------------
+ if ( IsLeechTargetUsedAsAnimNode( target ) )
+ {
+ e.ref = CreateLeechingScriptMoverBetweenEnts( player, target )
+ animTag = target.AISetting_LeechAnimTag()
+ Assert( animTag != "" )
+ e.ref.SetOrigin( target.GetOrigin() )
+ e.ref.SetParent( target, animTag )
+ }
+ else
+ {
+ e.ref = player
+ e.ref.SetOrigin( e.playerStartOrg )
+ playerSequence.playerPushable = true
+ }
+
+ e.ref.EndSignal( "OnDestroy" )
+
+ //-----------------------------------------------------------------
+ // Player FirstPersonSequence for the leeching
+ //-----------------------------------------------------------------
+ playerSequence.blendTime = 0.25
+ playerSequence.attachment = "ref"
+
+ //-----------------------------------------------------------------
+ // Only create FirstPersonSequence for leech target if anims exist
+ //-----------------------------------------------------------------
+ bool haveTargetSequence = false
+ FirstPersonSequenceStruct targetSequence
+
+ if ( action.targetAnimation3pStart != "" )
+ {
+ targetSequence = clone playerSequence
+ haveTargetSequence = true
+ }
+
+ playerSequence.thirdPersonAnim = action.playerAnimation3pStart
+ playerSequence.thirdPersonAnimIdle = action.playerAnimation3pIdle
+ playerSequence.firstPersonAnim = action.playerAnimation1pStart
+ playerSequence.firstPersonAnimIdle = action.playerAnimation1pIdle
+
+ entity viewmodel = player.GetFirstPersonProxy()
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt1" ) )
+ AddAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt1", PlaySound_DataKnife_Hack_Spectre_Pt1 )
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt2" ) )
+ AddAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt2", PlaySound_DataKnife_Hack_Spectre_Pt2 )
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt3" ) )
+ AddAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt3", PlaySound_DataKnife_Hack_Spectre_Pt3 )
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_Spectre_Servo_Heavy_Short" ) )
+ AddAnimEvent( viewmodel, "PlaySound_Spectre_Servo_Heavy_Short", PlaySound_Spectre_Servo_Heavy_Short )
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_ArmorRattle" ) )
+ AddAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_ArmorRattle", PlaySound_DataKnife_Hack_Spectre_ArmorRattle )
+
+ if ( haveTargetSequence )
+ {
+ targetSequence.thirdPersonAnim = action.targetAnimation3pStart
+ targetSequence.thirdPersonAnimIdle = action.targetAnimation3pIdle
+ }
+
+ playerSequence.noParent = true
+
+ //-----------------------------------
+ // Data knife
+ //-----------------------------------
+ asset model = DATA_KNIFE_MODEL
+
+ string knifeTag = GetTagForDataknife( target )
+ entity thirdPersonKnife = CreatePropDynamic( model )
+ SetTargetName( thirdPersonKnife, "thirdPersonKnife" )
+ thirdPersonKnife.SetParent( player, knifeTag, false, 0.0 )
+ e.knives.append( thirdPersonKnife )
+
+ SetForceDrawWhileParented( target, true )
+
+ //------------------------------------------------------------------------------
+ // Play leech anim sequence for player, but only for target if leech anims exist
+ //-------------------------------------------------------------------------------
+ player.SetSyncedEntity( target )
+ entity ref = e.ref
+ if ( haveTargetSequence )
+ thread Animate_PlayerLeechTarget( targetSequence, target, ref )
+
+ waitthread FirstPersonSequence( playerSequence, player, null )
+}
+
+
+//Basically copy pasted from CreateMeleeScriptMoverBetweenEnts
+entity function CreateLeechingScriptMoverBetweenEnts( entity attacker, entity target )
+{
+ vector endOrigin = target.GetOrigin()
+ vector startOrigin = attacker.GetOrigin()
+ vector refVec = endOrigin - startOrigin
+
+ vector refAng = VectorToAngles( refVec )
+ float pitch = refAng.x
+ if ( pitch > 180 )
+ pitch -= 360
+ if ( fabs( pitch ) > 35 ) //If pitch is too much, use angles from target
+ refAng = target.GetAngles() // Leech does it from behind target, so use target's angles.
+
+ vector refPos = endOrigin - refVec * 0.5
+
+ entity ref = CreateOwnedScriptMover( attacker )
+ ref.SetOrigin( refPos )
+ ref.SetAngles( refAng )
+
+ return ref
+}
+
+void function Animate_PlayerLeechTarget( FirstPersonSequenceStruct targetSequence, entity target, entity ref )
+{
+ ref.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDestroy" )
+ waitthread FirstPersonSequence( targetSequence, target, ref )
+}
+
+void function PlayerExitLeechingAnim( entity player, entity target, LeechActionInfo action, LeechData e )
+{
+ FirstPersonSequenceStruct playerSequence
+ playerSequence.blendTime = 0.3
+ playerSequence.attachment = "ref"
+ playerSequence.teleport = false
+ playerSequence.noParent = true
+ playerSequence.playerPushable = true
+
+ //--------------------------------------
+ // Target animates only if he has anims
+ //---------------------------------------
+ bool hasTargetSequence = false
+ FirstPersonSequenceStruct targetSequence
+ if ( action.targetAnimation3pEnd != "" )
+ {
+ targetSequence = clone playerSequence
+ hasTargetSequence = true
+ }
+
+ playerSequence.thirdPersonAnim = action.playerAnimation3pEnd
+ playerSequence.firstPersonAnim = action.playerAnimation1pEnd
+ playerSequence.snapPlayerFeetToEyes = false
+
+ entity ref = e.ref
+
+ if ( hasTargetSequence )
+ {
+ targetSequence.thirdPersonAnim = action.targetAnimation3pEnd
+ thread FirstPersonSequence( targetSequence, target, ref )
+ }
+ waitthread FirstPersonSequence( playerSequence, player, null )
+
+ //-------------------------------------------------------------
+ // Detach from rodeo if applicable (drones, superspectres, etc)
+ //-------------------------------------------------------------
+ if ( Rodeo_IsAttached( player ) )
+ player.Signal( "RodeoOver" )
+}
+
+bool function IsBeingLeeched( entity npc )
+{
+ return npc.ai.leechInProgress
+}
+
+void function PlaySound_DataKnife_Hack_Spectre_Pt1( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "DataKnife_Hack_Spectre_Pt1" )
+
+}
+
+void function PlaySound_DataKnife_Hack_Spectre_Pt2( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "DataKnife_Hack_Spectre_Pt2" )
+
+}
+
+void function PlaySound_DataKnife_Hack_Spectre_Pt3( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "DataKnife_Hack_Spectre_Pt3" )
+
+}
+
+void function PlaySound_Spectre_Servo_Heavy_Short( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Spectre.Servo.Heavy.Short" )
+
+}
+
+void function PlaySound_DataKnife_Hack_Spectre_ArmorRattle( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "DataKnife_Hack_Spectre_ArmorRattle" )
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/_slamzoom.nut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_slamzoom.nut
new file mode 100644
index 00000000..83ee3916
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_slamzoom.nut
@@ -0,0 +1,85 @@
+untyped
+
+global function ShouldSlamzoomSpawn
+global function SlammzoomSpawn
+
+const SLAMZOOM_WHOOSH_SOUND = "UI_InGame_MarkedForDeath_PlayerUnmarked"
+const SLAMZOOM_COLOR_CORRECTION = "materials/correction/player_electric_damage.raw"
+
+function ShouldSlamzoomSpawn()
+{
+ return ( GetCurrentPlaylistVarInt( "enable_slamzoom_spawn", 0 ) == 1 )
+}
+
+function SlammzoomSpawn( entity player, vector origin, vector angles, entity friendlyPilot = null )
+{
+ player.EndSignal( "OnDestroy" )
+ player.SetOrigin( origin )
+ player.SnapEyeAngles( angles )
+
+ vector landorigin = player.EyePosition()
+
+ // slamzoom
+ int slamzoom_height = 4000
+ float slamzoom_time1 = 0.5
+ float slamzoom_time2 = 0.7
+ float slamzoom_rotate_time = 0.3
+ int slamzoom_angle = 90
+ int enter_angle = 90 - slamzoom_angle
+
+ vector start_angles = Vector( -90-enter_angle, angles.y, 0 )
+ vector start_vector = AnglesToForward( start_angles )
+
+ // origin = origin + Vector(0,0,48)
+
+ entity camera = CreateTitanDropCamera( origin + start_vector * slamzoom_height, Vector( slamzoom_angle, angles.y, 0.0) )
+ camera.Fire( "Enable", "!activator", 0, player )
+
+ entity mover = CreateScriptMover()
+ if ( IsValid( camera ) )
+ {
+ // camera can be invalid for a moment when server shuts down
+ mover.SetOrigin( camera.GetOrigin() )
+ mover.SetAngles( camera.GetAngles() )
+ camera.SetParent( mover )
+ }
+
+ OnThreadEnd(
+ function() : ( mover, camera )
+ {
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ if ( IsValid( camera ) )
+ camera.Destroy()
+ }
+ )
+
+ player.isSpawning = mover
+ mover.MoveTo( mover.GetOrigin() + (start_vector * 100), slamzoom_time1, slamzoom_time1*0.4, slamzoom_time1*0.4 )
+ wait slamzoom_time1
+ EmitSoundOnEntityOnlyToPlayer( player, player, SLAMZOOM_WHOOSH_SOUND )
+ mover.MoveTo( landorigin, slamzoom_time2, slamzoom_time2*0.5, slamzoom_time2*0.2 )
+ wait slamzoom_time2 - slamzoom_rotate_time
+ mover.RotateTo( angles, slamzoom_rotate_time, slamzoom_rotate_time*0.2, slamzoom_rotate_time*0.2 )
+ wait slamzoom_rotate_time
+ player.isSpawning = null
+ wait 0.1
+ if ( IsValid( camera ) )
+ {
+ // camera can be invalid for a moment when server shuts down
+ camera.FireNow( "Disable", "!activator", null, player )
+ }
+
+ if ( IsValid( friendlyPilot ) )
+ {
+ MessageToPlayer( friendlyPilot, eEventNotifications.FriendlyPlayerSpawnedOnYou, player )
+ MessageToPlayer( player, eEventNotifications.SpawnedOnFriendlyPlayer, friendlyPilot )
+ }
+
+ if ( ShouldGivePlayerInfoOnSpawn() )
+ thread GivePlayerInfoOnSpawn( player )
+
+ player.SetOrigin( origin )
+ player.SnapEyeAngles( angles )
+ player.RespawnPlayer( null )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/_zipline.gnut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_zipline.gnut
new file mode 100644
index 00000000..a129c479
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_zipline.gnut
@@ -0,0 +1,838 @@
+untyped
+
+global function Zipline_Init
+
+global function GuyZiplinesToGround
+global function GetZiplineSpawns
+global function GetHookOriginFromNode
+global function ZiplineInit
+
+global function CodeCallback_ZiplineMount
+global function CodeCallback_ZiplineStart
+global function CodeCallback_ZiplineMove
+global function CodeCallback_ZiplineStop
+
+global function AddCallback_ZiplineStart
+global function AddCallback_ZiplineStop
+
+global function TrackMoverDirection
+global function CreateRopeEntities
+global function SpawnZiplineEntities
+global function GetZiplineLandingAnims
+global function AnimDoneStuckInSolidFailSafe
+
+struct {
+ array<string> zipLineLandingAnimations = [
+ "pt_zipline_dismount_standF",
+ "pt_zipline_dismount_crouchF",
+ "pt_zipline_dismount_crouch180",
+ "pt_zipline_dismount_breakright",
+ "pt_zipline_land"
+ "pt_zipline_land"
+ "pt_zipline_land"
+ "pt_zipline_land"
+ "pt_zipline_land"
+ "pt_zipline_land"
+ ]
+
+ array<string> zipLinePlayerLandingAnimations = [
+ "pt_zipline_dismount_standF"
+ ]
+
+ array<string> zipLineReadyAnimations = [
+ "pt_zipline_ready_idleA",
+ "pt_zipline_ready_idleB"
+ ]
+} file
+
+//typedef EntitiesDidLoadCallbackType void functionref(entity)
+array<void functionref(entity,entity)> _ZiplineStartCallbacks
+array<void functionref(entity)> _ZiplineStopCallbacks
+
+function Zipline_Init()
+{
+ if ( reloadingScripts )
+ return
+
+ RegisterSignal( "deploy" )
+ RegisterSignal( "stop_deploy" )
+ RegisterSignal( "npc_deployed" )
+
+ VehicleDropshipNew_Init()
+
+ level.MIN_ZIPLINE_LAND_DIST_SQRD <- 128 * 128
+ level.MIN_ZIPLINE_HOOK_DIST_SQRD <- 256 * 256
+ level._info_spawnpoint_dropships <- {}
+ AddSpawnCallback( "info_spawnpoint_dropship", AddToSpawnpointDropships )
+
+ PrecacheParticleSystem( $"hmn_mcorps_jump_jet_wallrun_full" )
+ PrecacheParticleSystem( $"hmn_imc_jump_jet_wallrun_full" )
+ PrecacheParticleSystem( $"P_Zipline_hld_1" )
+
+}
+
+void function AddToSpawnpointDropships( entity self )
+{
+ level._info_spawnpoint_dropships[ self ] <- self
+}
+
+function GetZiplineSpawns()
+{
+ local targets = []
+ foreach ( ent in clone level._info_spawnpoint_dropships )
+ {
+ if ( IsValid( ent ) )
+ {
+ targets.append( ent )
+ continue
+ }
+
+ delete level._info_spawnpoint_dropships[ ent ]
+ }
+
+ return targets
+}
+
+
+function GuyZiplinesToGround( guy, Table )
+{
+ expect entity( guy )
+
+ OnThreadEnd(
+ function() : ( guy )
+ {
+ if ( IsValid( guy ) )
+ guy.SetEfficientMode( false )
+ }
+ )
+
+ local ship = Table.ship
+ local dropPos = GetDropPos( Table )
+
+ // ship didn't find a drop spot
+ if ( dropPos == null )
+ WaitForever()
+
+ //DebugDrawLine( guy.GetOrigin(), dropPos, 255, 0, 0, true, 8.0 )
+
+ local attachOrigin = ship.GetAttachmentOrigin( Table.attachIndex )
+ local nodeOrigin = dropPos
+ local hookOrigin = GetHookOriginFromNode( guy.GetOrigin(), nodeOrigin, attachOrigin )
+
+ // couldn't find a place to hook it? This needs to be tested on precompile
+ if ( !hookOrigin )
+ {
+ printt( "WARNING! Bad zipline dropship position!" )
+ WaitForever()
+ }
+
+ Table.hookOrigin <- hookOrigin
+
+ // Track the movement of the script mover that moves the guy to the ground
+ local e = {}
+
+ waitthread GuyRidesZiplineToGround( guy, Table, e, dropPos )
+
+ //DebugDrawLine( guy.GetOrigin(), dropPos, 255, 0, 135, true, 5.0 )
+
+ if ( !( "forward" in Table ) )
+ {
+ // the sequence ended before the guy reached the ground
+ local start = guy.GetOrigin()
+ // this needs functionification
+ local end = Table.hookOrigin + Vector( 0,0,-80 )
+ TraceResults result = TraceLine( start, end, guy )
+ local angles = guy.GetAngles()
+ Table.forward <- AnglesToForward( angles )
+ Table.origin <- result.endPos
+ }
+
+ // the guy detaches and falls to the ground
+ string landingAnim = file.zipLineLandingAnimations.getrandom()
+ //DrawArrow( guy.GetOrigin(), guy.GetAngles(), 5.0, 80 )
+
+ if ( !guy.IsInterruptable() )
+ return
+
+ guy.Anim_ScriptedPlay( landingAnim )
+ guy.Anim_EnablePlanting()
+
+ ShowName( guy )
+
+ local vec = e.currentOrigin - e.oldOrigin
+
+ guy.SetVelocity( vec * 15 )
+
+ thread AnimDoneStuckInSolidFailSafe( guy )
+}
+
+function AnimDoneStuckInSolidFailSafe( entity guy )
+{
+ guy.EndSignal( "OnDeath" )
+ guy.WaitSignal( "OnAnimationDone" )
+
+ if ( EntityInSolid( guy ) )
+ {
+ vector ornull clampedPos
+ clampedPos = NavMesh_ClampPointForAIWithExtents( guy.GetOrigin(), guy, < 400, 400, 400 > )
+
+ if ( clampedPos != null )
+ {
+ guy.SetOrigin( expect vector( clampedPos ) )
+ printt( guy + " was in solid, teleported" )
+ }
+ }
+}
+
+function TrackMoverDirection( mover, e )
+{
+ mover.EndSignal( "OnDestroy" )
+ // track the way the mover movers, so we can do the
+ // correct velocity on the falling guy
+ local origin = mover.GetOrigin()
+ e.oldOrigin <- origin
+ e.currentOrigin <- origin
+
+ for ( ;; )
+ {
+ WaitFrame()
+ e.oldOrigin = e.currentOrigin
+ e.currentOrigin = mover.GetOrigin()
+ }
+}
+
+function GuyRidesZiplineToGround( entity guy, zipline, e, dropPos )
+{
+ entity mover = CreateOwnedScriptMover( guy )
+ mover.EndSignal( "OnDestroy" )
+
+ thread TrackMoverDirection( mover, e )
+
+ OnThreadEnd(
+ function() : ( mover, zipline, guy )
+ {
+ thread ZiplineRetracts( zipline )
+
+ if ( IsValid( guy ) )
+ {
+ guy.ClearParent()
+ StopSoundOnEntity( guy, "3p_zipline_loop" )
+ EmitSoundOnEntity( guy, "3p_zipline_detach" )
+ }
+
+ if ( IsValid( mover ) )
+ mover.Kill_Deprecated_UseDestroyInstead()
+ }
+ )
+
+
+ local rideDist = Distance( guy.GetOrigin(), zipline.hookOrigin )
+
+ // how long it takes the zipline to travel 1000 units
+ zipline.pinTime <- Graph( rideDist, 0, 1000, 0, 0.4 )
+
+ // how long it takes the zipline to retract,
+ zipline.retractTime <- Graph( rideDist, 0, 1000, 0, 0.5 )
+
+ // how long it takes the rider to ride 1000 units
+ float rideTime = Graph( rideDist, 0, 1000, 0, 2.5 )
+
+
+ // orient the script_mover in the direction its going
+ local angles = guy.GetAngles()
+ local forward = AnglesToForward( angles )
+ local right = AnglesToRight( angles )
+
+ CreateRopeEntities( zipline )
+
+ local zipAttachOrigin = zipline.ship.GetAttachmentOrigin( zipline.attachIndex )
+ zipline.end.SetOrigin( zipAttachOrigin )
+
+ zipline.start.SetParent( zipline.ship, zipline.shipAttach )
+ zipline.mid.SetParent( zipline.ship, zipline.shipAttach )
+
+ // now that the origin is set we can spawn the zipline, otherwise we
+ // see the zipline lerp in
+ SpawnZiplineEntities( zipline )
+
+
+ // the zipline shoots out
+ ZiplineMover( expect entity( zipline.end ), zipline.hookOrigin, zipline.pinTime )
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, zipAttachOrigin, "dropship_zipline_zipfire" )
+ delaythread( zipline.pinTime ) ZiplineMoveCleanup( zipline )
+
+// wait zipline.pinTime * 0.37
+ wait zipline.pinTime
+ EmitSoundAtPosition( TEAM_UNASSIGNED, zipline.hookOrigin, "dropship_zipline_impact" )
+
+ zipline.mid.SetParent( mover, "ref", false )
+ thread MoverMovesToGround( zipline, mover, rideTime )
+
+ if ( !IsAlive( guy ) || !guy.IsInterruptable() )
+ return
+
+ guy.SetParent( mover, "ref", false, 0.0 )
+
+ EmitSoundOnEntity( guy, "3p_zipline_attach" )
+ waitthread PlayAnim( guy, "pt_zipline_ready2slide", mover )
+ EmitSoundOnEntity( guy, "3p_zipline_loop" )
+
+ if ( !IsAlive( guy ) || !guy.IsInterruptable() || guy.GetParent() != mover )
+ return
+
+ // Anim_PlayWithRefPoint requires that the guy be parented to the ref point.
+ thread PlayAnim( guy, ZIPLINE_IDLE_ANIM, mover, "ref" )
+
+ //thread ZiplineAutoClipsToGeo( zipline, mover )
+
+ //wait 0.4 // some time to clear the lip
+
+ local nodeOrigin = dropPos
+ //DebugDrawLine( guy.GetOrigin(), nodeOrigin, 200, 255, 50, true, 8.0 )
+
+ rideDist = Distance( guy.GetOrigin(), nodeOrigin )
+ rideDist -= 100 // for animation at end
+ if ( rideDist < 0 )
+ rideDist = 0
+ rideTime = Graph( rideDist, 0, 100, 0, 0.15 )
+/*
+ printt( "ride time " + rideTime )
+ local endTime = Time() + rideTime
+ for ( ;; )
+ {
+ if ( Time() >= endTime )
+ return
+
+ DebugDrawLine( guy.GetOrigin(), nodeOrigin, 255, 0, 0, true, 0.15 )
+ DebugDrawText( nodeOrigin, ( endTime - Time() ) + "", true, 0.15 )
+ WaitFrame()
+ }
+*/
+ wait rideTime
+
+ thread ZiplineStuckFailsafe( guy, nodeOrigin )
+}
+
+function ZiplineStuckFailsafe( guy, nodeOrigin )
+{
+ TimeOut( 15.0 )
+
+ guy.EndSignal( "OnDeath" )
+
+ guy.WaitSignal( "OnFailedToPath" )
+
+ guy.SetOrigin( nodeOrigin )
+ printt( "Warning: AI Path failsafe at " + nodeOrigin )
+}
+
+function ZiplineMoveCleanup( zipline )
+{
+ // work around for moveto bug
+ if ( IsValid( zipline.end ) )
+ {
+ zipline.end.SetOrigin( zipline.hookOrigin )
+ }
+}
+
+function MoverMovesToGround( zipline, mover, timeTotal )
+{
+ // this handles the start point moving.
+ mover.EndSignal( "OnDestroy" )
+ zipline.ship.EndSignal( "OnDestroy" )
+
+ local origin = zipline.ship.GetAttachmentOrigin( zipline.attachIndex )
+ local angles = zipline.ship.GetAttachmentAngles( zipline.attachIndex )
+ mover.SetOrigin( origin )
+ mover.SetAngles( angles )
+
+ local start = zipline.start.GetOrigin()
+ local end = zipline.hookOrigin + Vector( 0,0,-180 )
+
+ local blendTime = 0.5
+ if ( timeTotal <= blendTime )
+ blendTime = 0
+
+ angles = VectorToAngles( end - start )
+ angles.x = 0
+ angles.z = 0
+
+ mover.MoveTo( end, timeTotal, blendTime, 0 )
+ mover.RotateTo( angles, 0.2 )
+}
+
+
+function WaitUntilZiplinerNearsGround( guy, zipline )
+{
+ local start, end, frac
+ local angles = guy.GetAngles()
+ local forward = AnglesToForward( angles )
+
+ local zipAngles, zipForward, dropDist
+
+ if ( guy.IsNPC() )
+ dropDist = 150
+ else
+ dropDist = 10 //much closer for player
+
+ local mins = guy.GetBoundingMins()
+ local maxs = guy.GetBoundingMaxs()
+
+ TraceResults result
+
+ for ( ;; )
+ {
+ start = guy.GetOrigin()
+ end = start + Vector(0,0,-dropDist)
+ end += forward * dropDist
+// TraceResults result = TraceLine( start, end, guy )
+ result = TraceHull( start, end, mins, maxs, guy, TRACE_MASK_NPCSOLID_BRUSHONLY, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( start, end, 255, 0, 0, true, 0.2 )
+
+ if ( result.fraction < 1.0 )
+ break
+
+ start = guy.GetOrigin()
+ end = zipline.hookOrigin + Vector( 0,0,-80 )
+
+ zipForward = ( end - start )
+ zipForward.Norm()
+ zipForward *= 250
+
+ end = start + zipForward
+ //DebugDrawLine( start, end, 255, 0, 0, true, 0.1 )
+
+// result = TraceLine( start, end, guy )
+ //DebugDrawLine( start, end, 255, 150, 0, true, 0.2 )
+ result = TraceHull( start, end, mins, maxs, guy, TRACE_MASK_NPCSOLID_BRUSHONLY, TRACE_COLLISION_GROUP_NONE )
+
+ if ( result.fraction < 1.0 )
+ break
+
+ WaitFrame()
+ }
+
+ zipline.origin <- result.endPos
+ zipline.forward <- forward
+}
+
+
+function ZiplineRetracts( zipline )
+{
+ if ( !IsValid( zipline.start ) )
+ return
+ if ( !IsValid( zipline.mid ) )
+ return
+ if ( !IsValid( zipline.end ) )
+ return
+
+ OnThreadEnd(
+ function() : ( zipline )
+ {
+ if ( IsValid( zipline.start ) )
+ zipline.start.Kill_Deprecated_UseDestroyInstead()
+
+ if ( IsValid( zipline.mid ) )
+ zipline.mid.Kill_Deprecated_UseDestroyInstead()
+
+ // is the only one that's not parented and only gets deleted here
+ zipline.end.Kill_Deprecated_UseDestroyInstead()
+ }
+ )
+
+ // IsValid check succeeds, even if a delete brought us here.
+ // IsValid should've failed.
+ if ( !IsAlive( expect entity( zipline.ship ) ) )
+ return
+
+ zipline.ship.EndSignal( "OnDestroy" )
+
+ zipline.start.EndSignal( "OnDestroy" )
+ zipline.mid.EndSignal( "OnDestroy" )
+ zipline.end.EndSignal( "OnDestroy" )
+
+ local start, end, mid
+ local startDist
+ local endDist
+ local totalDist
+ local progress
+ local newMidPoint
+ local midRetractProgress
+
+ local startTime = Time()
+ local endTime = startTime + 0.3
+
+ zipline.mid.ClearParent()
+
+ start = zipline.start.GetOrigin()
+ end = zipline.end.GetOrigin()
+ mid = zipline.mid.GetOrigin()
+
+ startDist = Distance( mid, start )
+ endDist = Distance( mid, end )
+ totalDist = startDist + endDist
+
+ if ( totalDist <= 0 )
+ return
+
+ progress = startDist / totalDist
+// newMidPoint = end * progress + start * ( 1 - progress )
+//
+// // how far from the midpoint we are, vertically
+// local mid_z_offset = newMidPoint.z - mid.z
+// local addOffset
+
+ for ( ;; )
+ {
+ start = zipline.start.GetOrigin()
+ end = zipline.end.GetOrigin()
+
+ newMidPoint = end * progress + start * ( 1 - progress )
+
+ midRetractProgress = GraphCapped( Time(), startTime, endTime, 0, 1 )
+ if ( midRetractProgress >= 1.0 )
+ break
+
+ newMidPoint = mid * ( 1 - midRetractProgress ) + newMidPoint * midRetractProgress
+ //addOffset = mid_z_offset * ( 1 - midRetractProgress )
+ //newMidPoint.z -= addOffset
+ //DebugDrawLine( zipline.mid.GetOrigin(), newMidPoint, 255, 0, 0, true, 0.2 )
+
+ if ( !IsValid( zipline.mid ) )
+ {
+ printt( "Invalid zipline mid! Impossible!" )
+ }
+ else
+ {
+ zipline.mid.SetOrigin( newMidPoint )
+ }
+
+
+// startDist = Distance( mid, start )
+// endDist = Distance( mid, end )
+// totalDist = startDist + endDist
+// progress = startDist / totalDist
+ WaitFrame()
+ }
+
+// DebugDrawLine( zipline.start.GetOrigin(), zipline.mid.GetOrigin(), 255, 100, 50, true, 5.0 )
+// DebugDrawLine( zipline.end.GetOrigin(), zipline.mid.GetOrigin(), 60, 100, 244, true, 5.0 )
+ local moveTime = 0.4
+ ZiplineMover( expect entity( zipline.start ), zipline.end.GetOrigin(), moveTime )
+ ZiplineMover( expect entity( zipline.mid ), zipline.end.GetOrigin(), moveTime )
+
+ wait moveTime
+/*
+ startTime = Time()
+ endTime = startTime + zipline.retractTime
+ end = zipline.end.GetOrigin()
+
+ if ( !IsValid( zipline.mid ) )
+ return
+ mid = zipline.mid.GetOrigin()
+
+ local org
+
+ for ( ;; )
+ {
+ start = zipline.start.GetOrigin()
+
+ progress = Graph( Time(), startTime, endTime )
+ if ( progress >= 1.0 )
+ break
+
+ org = end * ( 1 - progress ) + start * progress
+ zipline.end.SetOrigin( org )
+
+ org = mid * ( 1 - progress ) + start * progress
+
+ if ( !IsValid( zipline.mid ) )
+ return
+ zipline.mid.SetOrigin( org )
+
+ WaitFrame()
+ }
+*/
+}
+
+function CreateRopeEntities( Table )
+{
+ local subdivisions = 8 // 25
+ local slack = 100 // 25
+ string midpointName = UniqueString( "rope_midpoint" )
+ string endpointName = UniqueString( "rope_endpoint" )
+
+ entity rope_start = CreateEntity( "move_rope" )
+ rope_start.kv.NextKey = midpointName
+ rope_start.kv.MoveSpeed = 64
+ rope_start.kv.Slack = slack
+ rope_start.kv.Subdiv = subdivisions
+ rope_start.kv.Width = "2"
+ rope_start.kv.TextureScale = "1"
+ rope_start.kv.RopeMaterial = "cable/cable.vmt"
+ rope_start.kv.PositionInterpolator = 2
+
+ entity rope_mid = CreateEntity( "keyframe_rope" )
+ SetTargetName( rope_mid, midpointName )
+ rope_mid.kv.NextKey = endpointName
+ rope_mid.kv.MoveSpeed = 64
+ rope_mid.kv.Slack = slack
+ rope_mid.kv.Subdiv = subdivisions
+ rope_mid.kv.Width = "2"
+ rope_mid.kv.TextureScale = "1"
+ rope_mid.kv.RopeMaterial = "cable/cable.vmt"
+
+ entity rope_end = CreateEntity( "keyframe_rope" )
+ SetTargetName( rope_end, endpointName )
+ rope_end.kv.MoveSpeed = 64
+ rope_end.kv.Slack = slack
+ rope_end.kv.Subdiv = subdivisions
+ rope_end.kv.Width = "2"
+ rope_end.kv.TextureScale = "1"
+ rope_end.kv.RopeMaterial = "cable/cable.vmt"
+
+ Table.start <- rope_start
+ Table.mid <- rope_mid
+ Table.end <- rope_end
+
+ return Table
+}
+
+function SpawnZiplineEntities( Table )
+{
+ // after origins are set
+ DispatchSpawn( Table.start )
+ DispatchSpawn( Table.mid )
+ DispatchSpawn( Table.end )
+ return Table
+}
+
+function GetDropPos( zipline )
+{
+ entity ship = expect entity( zipline.ship )
+ if ( !HasDropshipDropTable( ship ) )
+ return null
+
+ DropTable dropTable = GetDropshipDropTable( ship )
+
+ foreach ( side, nodeTables in dropTable.nodes )
+ {
+ foreach ( nodeTable in nodeTables )
+ {
+ if ( nodeTable.attachName == zipline.shipAttach )
+ return nodeTable.origin
+ }
+ }
+
+ return null
+}
+
+function GetHookOriginFromNode( origin, nodeOrigin, attachOrigin )
+{
+ // need to use the slope of guy to node to get the slope for the zipline, then launch it from the attachment origin
+ local dropVec = nodeOrigin - origin
+ local dropDist = Length( dropVec )
+ dropVec.Norm()
+
+// DrawArrow( nodeOrigin, Vector(0,0,0), 5, 100 )
+ local attachEnd = attachOrigin + dropVec * ( dropDist + 1500 ) // some buffer
+ TraceResults zipTrace = TraceLine( attachOrigin, attachEnd, null, TRACE_MASK_NPCWORLDSTATIC )
+
+// DebugDrawLine( attachOrigin, zipTrace.endPos, 0, 255, 0, true, 5.0 )
+// DebugDrawLine( zipTrace.endPos, attachEnd, 255, 0, 0, true, 5.0 )
+
+ // zipline didn't connect with anything
+ if ( zipTrace.fraction == 1.0 )
+ {
+// DebugDrawLine( attachOrigin, attachEnd, 255, 255, 0, true, 5.0 )
+ return null
+ }
+
+ if ( Distance( zipTrace.endPos, attachOrigin ) < 300 )
+ return null
+
+ return zipTrace.endPos
+}
+
+function ZiplineInit( entity player )
+{
+ player.s.ziplineEffects <- []
+}
+
+function CreateZiplineJetEffects( entity player )
+{
+ asset jumpJetEffectFriendlyName = $"hmn_imc_jump_jet_wallrun_full"
+ asset jumpJetEffectEnemyName = $"hmn_mcorps_jump_jet_wallrun_full"
+ int playerTeam = player.GetTeam()
+
+ //HACK!
+ //Create 2 sets of jump jet effects, 1 visible to friendly, 1 visible to enemy
+ //Doing this for a myriad of reasons on the server as opposed to on the client like the rest
+ //of the jump jet effects. Since ziplining isn't all that common an action it should be fine
+
+ //create left jump jetfriendly
+ entity leftJumpJetFriendly = CreateEntity( "info_particle_system" )
+ leftJumpJetFriendly.kv.start_active = 1
+ leftJumpJetFriendly.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY
+ leftJumpJetFriendly.SetValueForEffectNameKey( jumpJetEffectFriendlyName )
+ SetTargetName( leftJumpJetFriendly, UniqueString() )
+ leftJumpJetFriendly.SetParent( player, "vent_left_back", false, 0 )
+ SetTeam( leftJumpJetFriendly, playerTeam )
+ leftJumpJetFriendly.SetOwner( player)
+ DispatchSpawn( leftJumpJetFriendly )
+
+ //now create right jump jet for friendly
+ entity rightJumpJetFriendly = CreateEntity( "info_particle_system" )
+ rightJumpJetFriendly.kv.start_active = 1
+ rightJumpJetFriendly.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY
+ rightJumpJetFriendly.SetValueForEffectNameKey( jumpJetEffectFriendlyName )
+ SetTargetName( rightJumpJetFriendly, UniqueString() )
+ rightJumpJetFriendly.SetParent( player, "vent_right_back", false, 0 )
+ SetTeam( rightJumpJetFriendly, playerTeam )
+ rightJumpJetFriendly.SetOwner( player)
+ DispatchSpawn( rightJumpJetFriendly )
+
+ //create left jump jet for enemy
+ entity leftJumpJetEnemy = CreateEntity( "info_particle_system" )
+ leftJumpJetEnemy.kv.start_active = 1
+ leftJumpJetEnemy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_ENEMY
+ leftJumpJetEnemy.SetValueForEffectNameKey( jumpJetEffectEnemyName )
+ SetTargetName( leftJumpJetEnemy, UniqueString() )
+ leftJumpJetEnemy.SetParent( player, "vent_left_back", false, 0 )
+ SetTeam( leftJumpJetEnemy, playerTeam )
+ leftJumpJetEnemy.SetOwner( player)
+ DispatchSpawn( leftJumpJetEnemy )
+
+ //now create right jump jet for enemy
+ entity rightJumpJetEnemy = CreateEntity( "info_particle_system" )
+ rightJumpJetEnemy.kv.start_active = 1
+ rightJumpJetEnemy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_ENEMY
+ rightJumpJetEnemy.SetValueForEffectNameKey( jumpJetEffectEnemyName )
+ SetTargetName( rightJumpJetEnemy, UniqueString() )
+ rightJumpJetEnemy.SetParent( player, "vent_right_back", false, 0 )
+ SetTeam( rightJumpJetEnemy, playerTeam )
+ rightJumpJetEnemy.SetOwner( player)
+ DispatchSpawn( rightJumpJetEnemy )
+
+ //sparks from the hand
+ entity handSparks = CreateEntity( "info_particle_system" )
+ handSparks.kv.start_active = 1
+ handSparks.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY
+ handSparks.SetValueForEffectNameKey( $"P_Zipline_hld_1" )
+ SetTargetName( handSparks, UniqueString() )
+ handSparks.SetParent( player, "L_HAND", false, 0 )
+ handSparks.SetOwner( player)
+ DispatchSpawn( handSparks )
+
+ //Do it again for greater intensity!
+ entity handSparks2 = CreateEntity( "info_particle_system" )
+ handSparks2.kv.start_active = 1
+ handSparks2.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY
+ handSparks2.SetValueForEffectNameKey( $"P_Zipline_hld_1" )
+ SetTargetName( handSparks2, UniqueString() )
+ handSparks2.SetParent( player, "L_HAND", false, 0 )
+ handSparks2.SetOwner( player)
+ DispatchSpawn( handSparks2 )
+
+ player.s.ziplineEffects.append( leftJumpJetFriendly )
+ player.s.ziplineEffects.append( rightJumpJetFriendly )
+ player.s.ziplineEffects.append( leftJumpJetEnemy )
+ player.s.ziplineEffects.append( rightJumpJetEnemy )
+
+ player.s.ziplineEffects.append( handSparks )
+ player.s.ziplineEffects.append( handSparks2 )
+}
+
+void function CodeCallback_ZiplineMount( entity player, entity zipline )
+{
+ // printl( "Mounting zipline")
+ #if SERVER
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "player_zipline_attach", "3p_zipline_attach", player, player )
+ #endif
+
+}
+
+void function CodeCallback_ZiplineStart( entity player, entity zipline )
+{
+ #if SERVER
+ CreateZiplineJetEffects( player )
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "player_zipline_loop", "3p_zipline_loop", player, player )
+ foreach ( callback in _ZiplineStartCallbacks )
+ thread callback( player, zipline )
+ #endif
+}
+
+void function CodeCallback_ZiplineMove( entity player, entity zipline )
+{
+ #if SERVER
+ if ( player.IsPhaseShifted() )
+ {
+ foreach( effect in player.s.ziplineEffects )
+ {
+ IsValid( effect )
+ effect.Destroy()
+ }
+ player.s.ziplineEffects.clear()
+ }
+ else if ( player.s.ziplineEffects.len() <= 0 )
+ {
+ CreateZiplineJetEffects( player );
+ }
+ #endif
+}
+
+void function CodeCallback_ZiplineStop( entity player )
+{
+ #if SERVER
+ foreach( effect in player.s.ziplineEffects )
+ {
+ IsValid( effect )
+ effect.Destroy()
+ }
+ player.s.ziplineEffects.clear()
+
+ StopSoundOnEntity( player, "player_zipline_loop" )
+ StopSoundOnEntity( player, "3p_zipline_loop" )
+
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "player_zipline_detach", "3p_zipline_detach", player, player )
+
+ foreach ( callback in _ZiplineStopCallbacks )
+ thread callback( player )
+ #endif
+}
+
+void function AddCallback_ZiplineStart( void functionref(entity,entity) callback )
+{
+ _ZiplineStartCallbacks.append( callback )
+}
+
+void function AddCallback_ZiplineStop( void functionref(entity) callback )
+{
+ _ZiplineStopCallbacks.append( callback )
+}
+
+function ZiplineMover( entity ent, end, timeTotal, blendIn = 0, blendOut = 0 )
+{
+ Assert( !IsThreadTop(), "This should not be waitthreaded off, it creates timing issues." )
+ entity mover = CreateOwnedScriptMover( ent )
+ ent.SetParent( mover )
+
+ OnThreadEnd(
+ function() : ( ent, mover )
+ {
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ }
+ )
+
+ mover.MoveTo( end, timeTotal, blendIn, blendOut )
+ wait timeTotal
+
+ if ( IsValid( ent ) )
+ ent.ClearParent()
+}
+
+array<string> function GetZiplineLandingAnims()
+{
+ return file.zipLineLandingAnimations
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/class_wallrun.gnut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/class_wallrun.gnut
new file mode 100644
index 00000000..58de59c1
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/class_wallrun.gnut
@@ -0,0 +1,224 @@
+untyped
+
+global function ClassWallrun_Init
+
+global function Wallrun_AddPlayer
+global function Wallrun_OnPlayerSpawn
+global function Wallrun_OnPlayerDeath
+global function Wallrun_PlayerTookDamage
+global function Wallrun_EnforceWeaponDefaults
+global function Wallrun_CreateCopyOfPilotModel
+
+function ClassWallrun_Init()
+{
+
+ // Make weapons less effective when playing at higher difficulty.
+ level.lethalityMods <- {}
+}
+
+function Wallrun_AddPlayer( player )
+{
+ player.playerClassData[level.pilotClass] <- {}
+}
+
+
+function Wallrun_EnforceWeaponDefaults( player )
+{
+ if ( player.playerClassData[ level.pilotClass ].primaryWeapon )
+ {
+ // settings already exist
+ return
+ }
+
+ player.playerClassData[ level.pilotClass ].primaryWeapon = "mp_weapon_r97"
+ player.playerClassData[ level.pilotClass ].secondaryWeapon = "mp_weapon_sniper"
+
+ local offhandWeaponTable = {}
+ offhandWeaponTable[0] <- { weapon = "mp_weapon_frag_grenade", mods = [] }
+ offhandWeaponTable[1] <- { weapon = "mp_ability_heal", mods = [] }
+
+ player.playerClassData[ level.pilotClass ].offhandWeapons = offhandWeaponTable
+ player.playerClassData[ level.pilotClass ].playerSetFile = DEFAULT_PILOT_SETTINGS
+}
+
+
+function Wallrun_OnPlayerSpawn( player )
+{
+}
+
+
+function Wallrun_PlayerTookDamage( entity player, damageInfo, entity attacker )
+{
+ if ( IsDemigod( player ) )
+ {
+ EntityDemigod_TryAdjustDamageInfo( player, damageInfo )
+ return
+ }
+
+ AdjustDamageForRodeoPlayers( player, damageInfo, attacker )
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " After Wallrun_PlayerTookDamage:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ if ( player.GetShieldHealthMax() > 0 )
+ {
+ local shieldDamage = PilotShieldHealthUpdate( player, damageInfo )
+ return shieldDamage
+ }
+
+ return
+}
+
+function AdjustDamageForRodeoPlayers( entity player, var damageInfo, entity attacker )
+{
+ if ( player == attacker )
+ return
+
+ local titanSoulRodeoed = player.GetTitanSoulBeingRodeoed()
+ if ( !IsValid( titanSoulRodeoed ) )
+ return
+
+ local playerParent = titanSoulRodeoed.GetTitan()
+
+ // dont let npcs hurt rodeo player
+ if ( attacker.IsNPC() && attacker != playerParent && DamageInfo_GetDamageSourceIdentifier( damageInfo ) != eDamageSourceId.mp_titanability_smoke )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ local damage = DamageInfo_GetDamage( damageInfo )
+
+ if ( !ShouldAdjustDamageForRodeoPlayer( damageInfo ) )
+ return
+
+ local maxPer500ms
+
+ if ( attacker == playerParent )
+ {
+ // rodeo'd player can't damage quite as much
+ maxPer500ms = 56
+ }
+ else
+ if ( playerParent.GetTeam() == player.GetTeam() )
+ {
+ // riding same team titan protects you a bit from random fire on that titan
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_EXPLOSION )
+ {
+ maxPer500ms = 75
+ }
+ else if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_MELEE ) //If melee, players still die in one hit
+ {
+ maxPer500ms = player.GetMaxHealth() + 1
+ }
+ else
+ {
+ maxPer500ms = 175
+ }
+ }
+ else
+ {
+ return
+ }
+
+ //Set a cap on how much damage the playerParent can do.
+ local damageTaken = GetTotalDamageTakenInTime( player, 0.5 )
+
+ local allowedDamage = maxPer500ms - damageTaken
+ if ( damage < allowedDamage )
+ return
+
+ damage = allowedDamage
+ if ( damage <= 0 )
+ damage = 0
+
+ DamageInfo_SetDamage( damageInfo, damage )
+}
+
+
+function ShouldAdjustDamageForRodeoPlayer( damageInfo )
+{
+ int sourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ switch( sourceID )
+ {
+ case eDamageSourceId.rodeo_trap:
+ case eDamageSourceId.mp_titanweapon_vortex_shield:
+ case eDamageSourceId.mp_titanweapon_vortex_shield_ion:
+ case eDamageSourceId.mp_titanability_smoke:
+ case eDamageSourceId.mp_weapon_satchel: //added so that rodeoing players are no longer invulnerable to their satchels when detonated by Titan's smoke
+ return false
+
+ default:
+ return true
+ }
+}
+
+
+function Wallrun_OnPlayerDeath( entity player, damageInfo )
+{
+ if ( IsValidHeadShot( damageInfo, player ) )
+ {
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ local soundAlias
+ if ( damageType & DF_SHOTGUN )
+ {
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Flesh.Shotgun.BulletImpact_Headshot_3P_vs_1P" )
+ soundAlias = "Flesh.Shotgun.BulletImpact_Headshot_3P_vs_3P"
+ }
+ else if ( damageType & damageTypes.bullet || damageType & DF_BULLET )
+ {
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Flesh.Light.BulletImpact_Headshot_3P_vs_1P" )
+ soundAlias = "Flesh.Light.BulletImpact_Headshot_3P_vs_3P"
+ }
+ else if ( damageType & damageTypes.largeCaliber || damageType & DF_GIB )
+ {
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Flesh.Heavy.BulletImpact_Headshot_3P_vs_1P" )
+ soundAlias = "Flesh.Heavy.BulletImpact_Headshot_3P_vs_3P"
+ }
+
+ if ( soundAlias )
+ {
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ array<entity> pilotArray = GetPlayerArray()
+ //Iterating because we need to not play this sound on 2 pilots and the function only allows for 1. Performance difference is negligible according to Eric M between this and adding a specific code function.
+ foreach ( pilot in pilotArray )
+ {
+ if ( !IsValid( pilot ) )
+ continue
+
+ if ( pilot == player || pilot == attacker )
+ continue
+
+ EmitSoundOnEntityOnlyToPlayer( player, pilot, soundAlias )
+ }
+ }
+ }
+}
+
+
+entity function Wallrun_CreateCopyOfPilotModel( entity player )
+{
+ const string PLAYER_SETTINGS_FIELD = "bodymodel"
+
+ asset modelName
+ if ( player.IsTitan() )
+ {
+ modelName = GetPlayerSettingsAssetForClassName( player.s.storedPlayerSettings, PLAYER_SETTINGS_FIELD )
+ }
+ else
+ {
+ modelName = player.GetPlayerSettingsAsset( PLAYER_SETTINGS_FIELD )
+ }
+
+ entity model = CreatePropDynamic( modelName )
+
+ SetTeam( model, player.GetTeam() )
+
+ //model.SetSkin( 0 )
+
+ RandomizeHead( model )
+
+ return model
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/rodeo/_rodeo.gnut b/Northstar.CustomServers/mod/scripts/vscripts/rodeo/_rodeo.gnut
new file mode 100644
index 00000000..72ff58b7
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/rodeo/_rodeo.gnut
@@ -0,0 +1,545 @@
+untyped
+
+global function Rodeo_Init
+
+global function CodeCallback_StartRodeo
+global function CodeCallback_ForceEndRodeo
+global function CodeCallback_EmbarkTitan
+global function CodeCallback_EmbarkTitanFromGrapple
+
+global function PlayerBeginsRodeo
+global function WatchForPlayerJumpingOffRodeo
+global function PlayerJumpsOffRodeoTarget
+global function PlayerClimbsIntoRodeoPosition
+global function rodeodebug
+
+//-----------------------------------------------------------------------------
+// _rodeo.nut
+//
+// The central location for rodeo, mostly a place to put code callbacks that
+// then call other things in other files based on the thing being rodeod.
+//
+//-----------------------------------------------------------------------------
+//
+// HOW TO ADD A NEW RODEO TYPE
+//
+// Create a new file for the rodeo type like _rodeo_prowler.nut and:
+// Implement "IsValid_NEWTYPE_RodeoTarget()"
+// Implement "GetRodeoPackage_RIDER_to_NEWTYPE_()"
+// Implement "_RIDER_Begins_NEWTYPE_Rodeo()"
+// Implement "_RIDER_LerpsInto_NEWTYPE_Rodeo()"
+//
+// _RIDER_ is the rodeo rider type like "Player" or "Prowler"
+// _NEWTYPE_ is the new kind of rodeo target like "SuperSpectre" or "Drone"
+//
+// In _rodeo_shared.nut:
+// IncludeFile() the NEWTYPE file
+// Add a hook for NEWTYPE in CodeCallback_OnRodeoAttach()
+// Add a hook for NEWTYPE in CodeCallback_IsValidRodeoTarget()
+// Add a hook for NEWTYPE in GetRodeoPackage() if needed
+//
+//-----------------------------------------------------------------------------
+
+function Rodeo_Init()
+{
+ RodeoShared_Init()
+ RodeoTitan_Init()
+ RegisterSignal( "RodeoPointOfNoReturn" )
+ AddCallback_OnTitanDoomed( OnTitanDoomed_Rodeo )
+}
+
+
+void function CodeCallback_EmbarkTitan( entity player, entity titan )
+{
+ if ( player.Lunge_IsActive() && (titan == player.Lunge_GetTargetEntity()) )
+ {
+ if ( PlayerCanImmediatelyEmbarkTitan( player, titan ) )
+ {
+ table embarkDirection = expect table( FindBestEmbark( player, titan ) )
+ thread PlayerEmbarksTitan( player, titan, embarkDirection )
+ }
+ }
+}
+
+bool function CodeCallback_EmbarkTitanFromGrapple( entity player, entity titan )
+{
+ Assert( player.IsHuman() )
+ Assert( titan.IsTitan() )
+
+ if ( !PlayerCanEmbarkIntoTitan( player, titan ) )
+ return false
+
+ table ornull embarkDirection = expect table ornull( FindBestEmbark( player, titan, false ) )
+ if ( !embarkDirection )
+ return false
+
+ expect table( embarkDirection )
+ thread PlayerEmbarksTitan( player, titan, embarkDirection )
+
+ return true
+}
+
+
+void function CodeCallback_StartRodeo( entity player, entity rodeoTarget )
+{
+ if ( IsMenuLevel() )
+ return
+
+ // Review: Good to remove?
+ if ( GetBugReproNum() == 7205 )
+ {
+ thread RodeoTest( player, rodeoTarget )
+ return
+ }
+
+ thread PlayerBeginsRodeo( player, player.p.rodeoPackage, rodeoTarget )
+}
+
+
+void function CodeCallback_ForceEndRodeo( entity player )
+{
+ ForceEndRodeo( player )
+}
+
+void function ForceEndRodeo( entity player )
+{
+ player.Signal( "RodeoOver" )
+}
+
+
+function RodeoTest( player, rodeoTarget )
+{
+ player.SetParent( rodeoTarget, "RODEO", false, 1 )
+ wait 5
+ player.ClearParent()
+ Rodeo_Detach( player )
+}
+
+function PlayerBeginsRodeo( entity player, RodeoPackageStruct rodeoPackage, entity rodeoTarget )
+{
+ Assert( player.GetParent() == null )
+ player.Lunge_ClearTarget()
+
+ Assert( IsValid( player ) )
+ Assert( IsValid( rodeoTarget ) )
+ Assert( !player.IsTitan() )
+
+ if ( rodeoTarget.IsTitan() )
+ PlayerBeginsTitanRodeo( player, rodeoPackage, rodeoTarget )
+ else
+ PlayerBeginsNPCRodeo( player, rodeoPackage, rodeoTarget ) //Not tested very well since non-Titan Rodeo never really became a thing. Should work thought
+}
+
+function PlayerBeginsNPCRodeo( entity player, RodeoPackageStruct rodeoPackage, entity rodeoTarget )
+{
+ bool sameTeam = player.GetTeam() == rodeoTarget.GetTeam()
+ bool playerWasEjecting = player.p.pilotEjecting // have to store this off here because the "RodeoStarted" signal below ends eject, so it will be too late to check it in actual rodeo function
+
+ player.Signal( "RodeoStarted" )
+
+ OnThreadEnd(
+ function () : ( player, rodeoTarget )
+ {
+ RodeoPackageStruct rodeoPackage = player.p.rodeoPackage
+
+ entity newRodeoTarget = rodeoTarget
+ if ( IsValid( player ) )
+ {
+ player.Signal( "RodeoOver" )
+
+ // Added via AddCallback_OnRodeoEnded
+ foreach ( callbackFunc in level.onRodeoEndedCallbacks ) //TODO: Remove this!
+ {
+ callbackFunc( player )
+ }
+
+ // show name of the pilot again
+ player.SetNameVisibleToFriendly( true )
+ player.SetNameVisibleToEnemy( true )
+
+ ClearPlayerAnimViewEntity( player )
+
+ // blend out the clear anim view entity
+ player.AnimViewEntity_SetLerpOutTime( 0.4 )
+
+ player.ClearParent()
+ player.Anim_Stop()
+ player.SetOneHandedWeaponUsageOff()
+ player.SetTitanSoulBeingRodeoed( null )
+ player.UnforceStand()
+ player.kv.PassDamageToParent = false
+ player.TouchGround() // so you can double jump off
+
+ StopSoundOnEntity( player, rodeoPackage.cockpitSound )
+ StopSoundOnEntity( player, rodeoPackage.worldSound )
+
+ if ( Rodeo_IsAttached( player ) )
+ {
+ Rodeo_Detach( player )
+ }
+
+ if ( IsAlive( player ) )
+ {
+ int attachIndex = newRodeoTarget.LookupAttachment( rodeoPackage.attachPoint )
+ vector startPos = newRodeoTarget.GetAttachmentOrigin( attachIndex )
+
+ if ( !PlayerCanTeleportHere( player, startPos, newRodeoTarget ) )
+ {
+ startPos = newRodeoTarget.GetOrigin()
+ if ( !PlayerCanTeleportHere( player, startPos, newRodeoTarget ) )
+ startPos = player.GetOrigin()
+ }
+
+ thread PlayerJumpsOffRodeoTarget( player, newRodeoTarget, startPos )
+ }
+ }
+ }
+ )
+
+ rodeoTarget.EndSignal( "OnDestroy" )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "RodeoOver" )
+
+ string rodeoTargetType = rodeoPackage.rodeoTargetType
+
+ thread WatchForPlayerJumpingOffRodeo( player )
+
+ player.SetNameVisibleToFriendly( false ) // hide name of the pilot while he is rodeoing
+ player.SetNameVisibleToEnemy( false )
+ player.ForceStand()
+ HolsterAndDisableWeapons( player )
+ player.SetOneHandedWeaponUsageOn()
+ player.TouchGround() // so you can double jump off
+
+ waitthread PlayerClimbsIntoRodeoPosition( player, rodeoTarget, rodeoPackage, playerWasEjecting )
+
+ #if FACTION_DIALOGUE_ENABLED
+ if ( !sameTeam )
+ PlayFactionDialogueToPlayer( "kc_rodeo", player )
+ #endif
+
+ // Go straight into idle animations
+ FirstPersonSequenceStruct sequence
+ sequence.thirdPersonAnimIdle = GetAnimFromAlias( rodeoTargetType, "pt_rodeo_panel_aim_idle" )
+ sequence.firstPersonAnimIdle = GetAnimFromAlias( rodeoTargetType, "ptpov_rodeo_panel_aim_idle" )
+
+ if ( !rodeoPackage.useAttachAngles )
+ player.Anim_IgnoreParentRotation( true )
+
+ sequence.useAnimatedRefAttachment = true
+
+ thread FirstPersonSequence( sequence, player, rodeoTarget )
+
+ if ( sameTeam )
+ {
+ player.GetFirstPersonProxy().HideFirstPersonProxy()
+ OpenViewCone( player )
+ }
+ else
+ {
+ PlayerRodeoViewCone( player, rodeoTargetType ) // TODO: Add air_drone and make enum in this func()
+ }
+
+ // look! he rodeoed!
+ thread AIChatter( "aichat_rodeo_cheer", player.GetTeam(), player.GetOrigin() )
+
+ Rodeo_OnFinishClimbOnAnimation( player ) // This is to let code know the player has finished climbing on the rodeo and ready to fire
+
+ if ( sameTeam )
+ {
+ player.PlayerCone_Disable()
+ player.EnableWorldSpacePlayerEyeAngles()
+ }
+
+ DeployAndEnableWeapons( player )
+
+ WaitForever()
+}
+
+void function PlayerClimbsIntoRodeoPosition( entity player, entity rodeoTarget, RodeoPackageStruct rodeoPackage, bool playerWasEjecting = false ) //TODO: Rename this function since new style rodeo anims have climbing as part of the anim
+{
+ player.EndSignal( "OnDeath" )
+
+
+ // The only thing that should have a soul is titans now. Legacy. Can't remove without major code feature work.
+ entity soul
+ if ( rodeoTarget.IsTitan() )
+ {
+ soul = rodeoTarget.GetTitanSoul()
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "OnDestroy" )
+ }
+ else
+ {
+ rodeoTarget.EndSignal( "OnTitanDeath" )
+ rodeoTarget.EndSignal( "OnDestroy" )
+ }
+
+ FirstPersonSequenceStruct sequence
+ sequence.attachment = rodeoPackage.attachPoint
+ SetRodeoAnimsFromPackage( sequence, rodeoPackage )
+
+ switch ( rodeoPackage.method )
+ {
+ case RODEO_APPROACH_FALLING_FROM_ABOVE:
+ table animStartPos = player.Anim_GetStartForRefEntity_Old( sequence.thirdPersonAnim, rodeoTarget, rodeoPackage.attachPoint )
+ float dist = Distance( player.GetOrigin(), animStartPos.origin )
+ float speed = Length( player.GetVelocity() )
+ float fallTime = dist / speed
+ fallTime *= 0.95
+
+ sequence.blendTime = clamp( fallTime, 0.4, 1 )
+
+ break
+
+ case RODEO_APPROACH_JUMP_ON:
+ sequence.blendTime = 0.6
+ break
+
+ default:
+ Assert( 0, "Unhandled rodeo method " + rodeoPackage.method )
+ }
+
+ if ( !PlayerHasPassive( player, ePassives.PAS_STEALTH_MOVEMENT ) )
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( rodeoPackage.cockpitSound, rodeoPackage.worldSound, player, rodeoTarget )
+
+ string titanType
+
+ // Titans only
+ if ( IsValid( soul ) )
+ {
+ if ( !( player in soul.rodeoRiderTracker ) )
+ {
+ soul.rodeoRiderTracker[ player ] <- true
+ if ( rodeoTarget.GetTeam() == player.GetTeam() )
+ {
+ AddPlayerScore( player, "HitchRide" )
+ AddPlayerScore( rodeoTarget, "GiveRide" )
+ }
+ else
+ {
+ AddPlayerScore( player, "RodeoEnemyTitan" )
+
+ #if HAS_STATS
+ UpdatePlayerStat( player, "misc_stats", "rodeos" )
+
+ if ( playerWasEjecting )
+ UpdatePlayerStat( player, "misc_stats", "rodeosFromEject" )
+ #endif
+
+ #if SERVER && MP
+ PIN_AddToPlayerCountStat( player, "rodeos" )
+ if ( rodeoTarget.IsPlayer() )
+ PIN_AddToPlayerCountStat( rodeoTarget, "rodeo_receives" )
+ #endif
+ }
+ }
+
+ titanType = GetSoulTitanSubClass( soul )
+ }
+
+ MessageToPlayer( player, eEventNotifications.Rodeo_HideBatteryHint )
+
+ float time = player.GetSequenceDuration( sequence.thirdPersonAnim )
+
+ if ( !rodeoPackage.useAttachAngles )
+ player.Anim_IgnoreParentRotation( true )
+
+ thread FirstPersonSequence( sequence, player, rodeoTarget )
+ wait time
+}
+
+void function WatchForPlayerJumpingOffRodeo( entity player )
+{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "RodeoOver" )
+ player.EndSignal( "RodeoPointOfNoReturn" )
+
+ wait 0.6 // debounce so you dont accihop
+
+ AddButtonPressedPlayerInputCallback( player, IN_JUMP, ForceEndRodeo )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ //RodeoOver is signalled at the end of PlayerBeginsRodeo, so even if Rodeo ends via the Titan disconnecting etc this will run
+ RemoveButtonPressedPlayerInputCallback( player, IN_JUMP, ForceEndRodeo )
+ }
+ )
+
+ WaitForever()
+}
+
+
+void function PlayerJumpsOffRodeoTarget( entity player, entity rodeoTarget, vector startPos )
+{
+ #if DEV
+ if ( GetDebugRodeoPrint() )
+ printt( "PlayerJumpsOffRodeoTarget, playerPos: " + player.GetOrigin() + " playerAngles: " + player.GetAngles() + " rodeoTargetPos: " + rodeoTarget.GetOrigin() + " rodeoTargetAngles: " + rodeoTarget.GetAngles() + ", startPos: " + startPos )
+ #endif
+
+ // ejected, or rip off battery, etc. Those adjust velocity for the rodeo player anyway, so don't do any more adjustments for them.
+ if ( player.p.rodeoShouldAdjustJumpOffVelocity == false )
+ return
+
+ if ( !IsValid( rodeoTarget ) )
+ {
+ PutEntityInSafeSpot( player, null, null, startPos, player.GetOrigin() )
+
+ #if DEV
+ if ( GetDebugRodeoPrint() )
+ printt( "PlayerJumpsOffRodeoTarget, playerPos after PutEntityInSafeSpot, !ISValid(rodeoTarget): " + player.GetOrigin() )
+ #endif
+ return
+ }
+
+ PutEntityInSafeSpot( player, rodeoTarget, null, startPos, player.GetOrigin() )
+ #if DEV
+ if ( GetDebugRodeoPrint() )
+ printt( "PlayerJumpsOffRodeoTarget, playerPos after PutEntityInSafeSpot, rodeoTarget valid: " + player.GetOrigin() )
+ #endif
+
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "Rodeo_Jump_Off_Interior", "Rodeo_Jump_Off", player, rodeoTarget )
+
+ vector forward = player.GetViewForward()
+ vector right = player.GetViewRight()
+
+ forward.z = 0
+ right.z = 0
+
+ // map the player's controls to his angles, and add that velocity
+ float xAxis = player.GetInputAxisRight()
+ float yAxis = player.GetInputAxisForward()
+
+ vector velocity
+ if ( fabs( xAxis ) < 0.2 && fabs( yAxis ) < 0.2 )
+ {
+ // no press = back press
+ velocity = Vector(0,0,0)
+ }
+ else
+ {
+ vector forwardVec = forward * yAxis
+ vector rightVec = right * xAxis
+ vector directionVec = ( rightVec + forwardVec )
+
+ //printt( "ForwardVec: " + forwardVec + ", rightVec: " + rightVec + ", directionVec :" + directionVec + ", directionVec scaled: " + (directionVec * 350 ) ) // for bug 123013
+
+ float speed = 350
+ velocity = directionVec * speed
+ }
+
+ // IMPORTANT: Don't give boost pilots too much vertical or they go sky high
+ if ( player.GetPlayerSettingsField( "boostEnabled" ).tointeger() > 0 )
+ velocity += Vector(0,0,200)
+ else
+ velocity += Vector(0,0,390 )
+
+ //printt( "Setting velocity to: " + velocity ) // for bug 123013
+
+ player.SetVelocity( velocity )
+ player.JumpedOffRodeo()
+}
+
+void function rodeodebug()
+{
+ // console command for forcing rodeo amongst 2 players
+ thread makerodeothread()
+}
+
+void function makerodeothread()
+{
+ array<entity> players = GetPlayerArray()
+ vector titanOrg
+ bool titanOrgSet = false
+ entity titan, pilot
+
+ for ( int i = players.len() - 1; i >= 0; i-- )
+ {
+ entity player = players[i]
+
+ if ( player.IsTitan() )
+ {
+ titan = player
+ }
+ else
+ {
+ pilot = player
+ }
+ }
+
+ if ( !titan )
+ {
+ for ( int i = players.len() - 1; i >= 0; i-- )
+ {
+ entity player = players[i]
+
+ if ( !player.IsTitan() )
+ {
+ player.SetPlayerSettings( "titan_atlas" )
+ titan = player
+ break
+ }
+ }
+ }
+ else
+ if ( !pilot )
+ {
+ for ( int i = players.len() - 1; i >= 0; i-- )
+ {
+ entity player = players[i]
+
+ if ( player.IsTitan() )
+ {
+ thread TitanEjectPlayer( player )
+ wait 1.5
+ pilot = player
+ break
+ }
+ }
+ }
+
+ for ( int i = players.len() - 1; i >= 0; i-- )
+ {
+ entity player = players[i]
+
+ if ( player.IsTitan() )
+ {
+ titanOrg = player.GetOrigin()
+ titanOrgSet = true
+ }
+ }
+
+ if ( !titanOrgSet )
+ return
+
+ for ( int i = players.len() - 1; i >= 0; i-- )
+ {
+ entity player = players[i]
+
+ if ( !player.IsTitan() )
+ {
+ vector angles = titan.GetAngles()
+ vector forward = AnglesToForward( angles )
+ titanOrg += forward * -100
+ titanOrg.z += 500
+ angles.x = 30
+ player.SetAngles( angles )
+ player.SetOrigin( titanOrg )
+ player.SetVelocity( Vector(0,0,0) )
+ break
+ }
+ }
+}
+
+void function OnTitanDoomed_Rodeo( entity titan, var damageInfo )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ entity rodeoPilot = GetRodeoPilot( titan )
+ if ( !IsValid( rodeoPilot ) )
+ return
+
+ Remote_CallFunction_NonReplay( rodeoPilot, "ServerCallback_UpdateRodeoRiderHud" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut b/Northstar.CustomServers/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut
new file mode 100644
index 00000000..9f05a0cd
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut
@@ -0,0 +1,2456 @@
+untyped //Panel.s stuff needs to be typed
+
+global function RodeoTitan_Init
+
+global function EnableTitanRodeo
+global function DisableTitanRodeo
+global function DebugRodeoTimes
+global function PlayerBeginsTitanRodeo
+global function ForceTitanRodeoToEnd
+global function PlayerRodeoViewCone
+global function OpenViewCone
+global function RodeoPanelIsOpen
+global function PlayerRemovesBatteryPack
+global function Rodeo_PilotAddsBatteryToFriendlyTitan
+global function GiveFriendlyRodeoPlayerProtection
+global function TakeAwayFriendlyRodeoPlayerProtection
+global function Rodeo_GiveBatteryToPlayer
+global function Rodeo_PilotThrowsBattery
+global function Rodeo_RemoveBatteryOffPlayer
+global function Rodeo_RemoveAllBatteriesOffPlayer
+global function Rodeo_GiveExecutingTitanABattery
+global function Rodeo_CreateBatteryPack
+global function SetSoulBatteryCount
+global function GetPlayerBatteryCount
+global function PlayerHasMaxBatteryCount
+global function Rodeo_PilotPicksUpBattery_Silent
+
+global function AddOnRodeoStartedCallback
+global function AddOnRodeoEndedCallback
+
+global function PilotBattery_SetMaxCount
+global function ThrowRiderOff
+
+global function Burnmeter_EmergencyBattery
+global function Burnmeter_AmpedBattery
+
+global function Battery_StartFX
+global function Battery_StopFX
+global function Battery_StopFXAndHideIconForPlayer
+
+global function RemovePlayerAirControl //This function should really be in a server only SP & MP utility script file. No such file exists as of right now.
+global function RestorePlayerAirControl //This function should really be in a server only SP & MP utility script file. No such file exists as of right now.
+
+#if DEV
+global function SetDebugRodeoPrint
+global function GetDebugRodeoPrint
+#endif
+
+#if MP
+global function SetApplyBatteryCallback
+#endif
+
+const float BATTERY_PICKUP_IGNORE_FRAC = 0.98
+const RODEO_EXPLOSION_DAMAGEFRAC = 0.3
+global const RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS = $"models/props/titan_battery_static/titan_battery_static.mdl" //Need a separate one for rodeo anims, instead of manually rotating the existing one
+const RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_VERTICAL_HEIGHT = 80
+const RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_HORIZONTAL_SPEED = 450
+const RODEO_THROW_BATTERY_BUTTON_HOLD_TIME = 0.5
+const RODEO_CLAMBER_FAILED_SOUND_DEBOUNCE_TIME = 2.0
+global const BATTERY_FX_FRIENDLY = $"P_xo_battery"
+global const BATTERY_FX_AMPED = $"P_xo_battery_amped"
+
+const HAS_BATTERY_THIEF_ICON = false
+
+const string PILOT_PICKS_UP_BATTERY_SOUND = "UI_TitanBattery_Pilot_PickUp"
+const string PILOT_APPLIES_BATTERY_TO_TITAN_HEALTH_RESTORED_SOUND = "UI_TitanBattery_Pilot_Give_TitanBattery"
+
+
+global int RODEO_BATTERY_EXPLOSION_EFFECT
+
+const string TITAN_GOT_BATTERY_RIPPED_SOUND = "UI_TitanBattery_Pilot_Take_TitanBattery"
+
+global float ANTI_RODEO_DEFAULT_START_DELAY = 0.5
+global float ANTI_RODEO_DEFAULT_DRAIN_DURATION = 1.25
+global float ANTI_RODEO_DEFAULT_WINDOW_DURATION = 0.1
+global float ANTI_RODEO_DEFAULT_WINDOW_START = 0.55
+
+struct AntiRodeoPlayerData
+{
+ bool antiRodeoPressed
+ float startTime
+ float windowStartFrac
+ float windowEndFrac
+ entity antiRodeoPlayer
+ bool wasCrouched
+}
+
+struct
+{
+ array<void functionref(entity,entity)> onRodeoEndedCallbacks
+ array<void functionref(entity,entity)> onRodeoStartedCallbacks
+
+ table<entity, AntiRodeoPlayerData> antiRodeoPlayerData
+
+ int maxPilotBatteryCount = 1
+ bool debugRodeoPrint = false
+
+ table<entity, bool> playersThatWantToUseRodeoGrenade
+
+ void functionref(entity,entity,entity) applyBatteryCallback
+} file
+
+//-----------------------------------------------------------------------------
+// _rodeo_titan.nut
+//
+// Script for a player (pilot) rodoeing a titan.
+//
+//-----------------------------------------------------------------------------
+
+void function RodeoTitan_Init()
+{
+ PrecacheParticleSystem( $"P_impact_rodeo_damage" ) //Rodeo hit spark
+ PrecacheParticleSystem( $"P_rodeo_damage_1" ) //DamageState1
+ PrecacheParticleSystem( $"P_rodeo_damage_2" ) //DamageState2
+ PrecacheParticleSystem( $"P_rodeo_damage_3" ) //DamageState3
+ PrecacheParticleSystem( BATTERY_FX_FRIENDLY )
+ PrecacheParticleSystem( BATTERY_FX_AMPED )
+
+ RegisterSignal( "CancelAirControlLoss" )
+ RegisterSignal( "FriendlyRodeoDeployWeapon" )
+ RegisterSignal( "MonitorRodeoPastPointOfNoReturn" )
+ RegisterSignal( "PostRodeoAirControl" )
+ PrecacheModel( RODEO_BATTERY_MODEL )
+ PrecacheModel( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS )
+
+ AddSoulDeathCallback( SoulRodeoEnds )
+ AddSoulTransferFunc( RecreateRodeoPanelDamageFX )
+
+ RODEO_BATTERY_EXPLOSION_EFFECT = PrecacheParticleSystem( $"P_impact_exp_FRAG_metal" )
+
+ AddCallback_OnPlayerKilled( Rodeo_DropAllBatteriesOnDeath )
+ AddCallback_OnTouchHealthKit( "item_titan_battery", Rodeo_OnTouchBatteryPack )
+ AddCallback_OnPilotBecomesTitan( Rodeo_ApplyAllBatteriesOnEmbark )
+
+ //AddSoulInitFunc( Rodeo_HealthDecayThink )
+
+ if ( IsMultiplayer() )
+ {
+ //AddDeathCallback( "player", TitanDropsBatteryOnDeath ) //SP has its own functions. Maybe we should just copy SP's stuff? They have the green highlight FX for it too
+ //AddDeathCallback( "npc_titan", TitanDropsBatteryOnDeath ) //SP has its own functions. Maybe we should just copy SP's stuff? They have the green highlight FX for it too
+
+ AddDamageCallback( "player", ShowRequestRodeoBatteryHint_OnDamage )
+ AddCallback_OnPilotBecomesTitan( ShowRequestRodeoBatteryHint_OnPilotBecomesTitan )
+
+ AddClientCommandCallback( "OfferRodeoBattery", ClientCommand_OfferRodeoBattery )
+ AddClientCommandCallback( "RequestRodeoBattery", ClientCommand_RequestRodeoBattery )
+
+ #if MP
+ AddClientCommandCallback( "TryNukeGrenade", ClientCommand_TryNukeGrenade )
+ RegisterSignal( "TryNukeGrenade" )
+ RegisterSignal( "RodeoNukeWindowEnded" )
+ #endif
+ }
+ else
+ {
+ AddSoulInitFunc( DisableBTRodeo )
+ }
+}
+
+void function Rodeo_HealthDecayThink( entity soul ) //Remove if we don't want rodeo battery to drain health
+{
+ thread Rodeo_HealthDecayThinkInternal( soul )
+}
+
+void function Rodeo_HealthDecayThinkInternal( entity soul ) //Remove if we don't want rodeo battery to drain health
+{
+ soul.EndSignal( "OnDestroy" ) //This needs to be OnDestroy instead of OnDeath because souls don't have a death animation
+ soul.EndSignal( "OnTitanDeath" )
+
+ bool draining = false
+
+ while ( 1 )
+ {
+ entity titan = soul.GetTitan()
+
+ if ( Rodeo_ShouldDrainHealth( soul ) )
+ {
+ if ( !draining )
+ {
+ draining = true
+ EmitSoundOnEntity( titan, "titan_energyshield_down" )
+ }
+
+ int damageAmout = Rodeo_GetDrainAmount( soul )
+
+ titan.TakeDamage( damageAmout, soul.e.lastRodeoAttacker, soul.e.lastRodeoAttacker, { scriptType = damageTypes.rodeoBatteryRemoval | DF_NO_INDICATOR, damageSourceId = eDamageSourceId.rodeo_battery_removal, hitbox = 2 } )
+ }
+ else
+ {
+ if ( draining )
+ {
+ draining = false
+ StopSoundOnEntity( titan, "titan_energyshield_down" )
+ }
+ }
+ WaitFrame()
+ }
+}
+
+bool function Rodeo_ShouldDrainHealth( entity soul ) //Remove if we don't want rodeo battery to drain health
+{
+ entity titan = soul.GetTitan()
+ if ( !IsAlive( titan ) )
+ return false
+
+ if ( GetDoomedState( titan ) )
+ return false
+
+ int batt = GetSoulBatteryCount( soul )
+ int maxBattHealth = GetSegmentHealthForTitan( titan ) * batt
+ int health = titan.GetHealth()
+ return ( health > maxBattHealth )
+}
+
+int function Rodeo_GetDrainAmount( entity soul ) //Remove if we don't want rodeo battery to drain health
+{
+ entity titan = soul.GetTitan()
+
+ float damagePerSec = GetSegmentHealthForTitan( titan ) / RODEO_DRAIN_TIME
+ float damagePerFrame = ceil( GetSegmentHealthForTitan( titan ) / RODEO_DRAIN_TIME ) * 0.1
+ int damageAmout = int( damagePerFrame )
+
+ int batt = GetSoulBatteryCount( soul )
+ int maxBattHealth = GetSegmentHealthForTitan( titan ) * batt
+ int health = titan.GetHealth()
+ if ( health - maxBattHealth < damageAmout )
+ damageAmout = health - maxBattHealth
+
+ return damageAmout
+}
+
+void function GiveFriendlyRodeoPlayerProtection( entity titan )
+{
+ entity friendlyRider = GetFriendlyRodeoPilot( titan )
+ if ( IsValid( friendlyRider ) )
+ {
+ //printt( "Set friendlyRider PassDamageToParent true" )
+ friendlyRider.kv.PassDamageToParent = true //rodeo player now passes damage to titan
+ }
+}
+
+void function TakeAwayFriendlyRodeoPlayerProtection( entity titan )
+{
+ entity friendlyRider = GetFriendlyRodeoPilot( titan )
+ if ( IsValid( friendlyRider ) )
+ {
+ //printt( "Set friendlyRider PassDamageToParent false" )
+ friendlyRider.kv.PassDamageToParent = false //rodeo player now takes full damage
+ }
+}
+
+void function CreateSparksInsideTitanPanel( panel )
+{
+ entity impactSpark = CreateEntity( "info_particle_system" )
+ impactSpark.kv.start_active = 1
+ impactSpark.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ impactSpark.SetValueForEffectNameKey( $"P_impact_rodeo_damage" )
+ SetTargetName( impactSpark, UniqueString() )
+ impactSpark.SetParent( panel, "hatch", false, 0 )
+ DispatchSpawn( impactSpark )
+ impactSpark.Kill_Deprecated_UseDestroyInstead( 1.5 )
+}
+
+
+void function CreateDamageStateParticlesForPanel( var panel, asset particleSystem = $"P_impact_rodeo_damage" )
+{
+ entity impactSpark = CreateEntity( "info_particle_system" )
+ impactSpark.kv.start_active = 1
+ impactSpark.SetOwner( panel.GetParent() )
+ impactSpark.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // not visible to owner
+ impactSpark.SetValueForEffectNameKey( particleSystem )
+ SetTargetName( impactSpark, UniqueString() )
+ impactSpark.SetParent( panel, "hatch", false, 0 )
+ DispatchSpawn( impactSpark )
+ if ( IsValid( panel.s.lastDamageStateParticleSystem ) )
+ {
+ //printt("Killing particle system: " + panel.s.lastDamageStateParticleSystem)
+ panel.s.lastDamageStateParticleSystem.Kill_Deprecated_UseDestroyInstead()
+ }
+
+ panel.s.lastDamageStateParticleSystem = impactSpark
+}
+
+
+void function RecreateRodeoPanelDamageFX( entity soul, entity titan, entity oldTitan )
+{
+ thread RecreateRodeoPanelDamageFX_threaded( soul )
+}
+
+
+void function RecreateRodeoPanelDamageFX_threaded( entity soul )
+{
+ WaitEndFrame()
+ entity panel = soul.soul.batteryContainer
+
+ if (! IsValid( panel ) )
+ return
+
+ entity lastDamageStateParticleSystem = expect entity ( panel.s.lastDamageStateParticleSystem )
+
+ if ( IsValid( lastDamageStateParticleSystem ) )
+ {
+ CreateDamageStateParticlesForPanel( panel, lastDamageStateParticleSystem.GetValueForEffectNameKey() ) //This kills the last particle system too
+ }
+}
+
+void function RodeoPanelIsOpen( entity panel )
+{
+ panel.s.opened = true
+
+ entity titan = panel.GetParent()
+ Assert( titan.IsTitan() )
+
+ entity soul = titan.GetTitanSoul()
+ Assert( IsValid( soul ) )
+
+ soul.SetLastRodeoHitTime( Time() ) //Make warning always trigger now when panel is ripped
+ soul.soul.batteryContainerBeingUsed = false
+}
+
+void function RodeoBatteryRemoval( entity pilot )
+{
+ entity titan = GetTitanBeingRodeoed( pilot )
+ if ( !IsValid( titan ) )
+ return
+
+ // THROW RODEO RIDER OFF
+ entity soul = titan.GetTitanSoul()
+ string titanType = GetSoulTitanSubClass( soul )
+
+ soul.SetLastRodeoHitTime( Time() )
+
+ RodeoBatteryPackRemovalDamage( pilot, titan, soul )
+
+ bool playerHadBattery = PlayerHasBattery( pilot )
+
+ if ( !playerHadBattery )
+ {
+ AddPlayerScore( pilot, "PilotBatteryStolen" )
+ entity battery = Rodeo_CreateBatteryPack( titan )
+ Rodeo_PilotPicksUpBattery( pilot, battery )
+ thread BatteryThiefHighlight( pilot )
+
+ if ( titan.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, titan, TITAN_GOT_BATTERY_RIPPED_SOUND ) //Consider playing this in world once we get sounds that aren't just notification beeps
+ }
+ }
+
+ vector direction = CalculateDirectionToThrowOffBatteryThief( pilot, titan )
+
+ ThrowRiderOff( pilot, titan, direction ) //This signals RodeoOver
+
+ #if MP
+ PIN_PlayerRodeoedEnemyTitanToCompletion( pilot, titan, playerHadBattery )
+ #endif
+}
+
+void function RodeoBatteryGrenadeShow( entity pilot )
+{
+ entity titanSoul = pilot.GetTitanSoulBeingRodeoed()
+ Assert( IsValid( titanSoul ) )
+
+ foreach( tempProp in pilot.p.rodeoAnimTempProps )
+ {
+ tempProp.Show()
+ }
+}
+
+void function RodeoBatteryRemoval_ShowBattery( entity pilot )
+{
+ foreach( tempProp in pilot.p.rodeoAnimTempProps )
+ {
+ tempProp.Show()
+ }
+
+ entity titanSoul = pilot.GetTitanSoulBeingRodeoed()
+
+ string titanType = GetSoulTitanSubClass( titanSoul )
+
+ entity batteryContainer = titanSoul.soul.batteryContainer
+ batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_down_idle" ) )
+
+}
+
+
+void function RodeoBatteryStealthMovementWarning( entity pilot )
+{
+ if ( !PlayerHasPassive( pilot, ePassives.PAS_STEALTH_MOVEMENT ) )
+ return
+
+ entity titanSoul = pilot.GetTitanSoulBeingRodeoed()
+
+ titanSoul.SetLastRodeoHitTime( Time() ) //This shows the warning icon on the Titan's hud
+}
+
+vector function CalculateDirectionToThrowOffBatteryThief( entity batteryThief, entity titan )
+{
+ vector backward
+ vector right
+
+ if ( titan.IsPlayer() )
+ {
+ backward = titan.GetViewForward() * -1.0
+ right = titan.GetViewRight()
+ }
+ else
+ {
+ backward = titan.GetForwardVector() * -1.0
+ right = titan.GetRightVector()
+ }
+
+ backward.z = 0
+ right.z = 0
+
+ backward = Normalize( backward )
+ right = Normalize( right )
+
+ // map the player's controls to his angles, and add that velocity
+ float xAxis = batteryThief.GetInputAxisRight()
+ float yAxis = batteryThief.GetInputAxisForward()
+
+ xAxis = GraphCapped( xAxis, -1.0, 1.0, -0.4, 0.4 )
+ yAxis = GraphCapped( yAxis, -1.0, 1.0, 1.0, 0.75 ) //Cap it so you don't actually let the players jump forwards
+
+ vector direction
+ if ( fabs( xAxis ) < 0.2 && fabs( yAxis ) < 0.2 )
+ {
+ // no significant controller deflection, just push forward by 0.75 as default
+ direction = backward * 0.75
+ }
+ else
+ {
+ vector forwardVec = backward * yAxis
+ vector rightVec = right * xAxis
+ direction = rightVec + forwardVec
+ }
+
+ direction *= RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_HORIZONTAL_SPEED
+ direction.z = RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_VERTICAL_HEIGHT
+
+ // JFS: R2DLC-310 SCRIPT ERROR: PHONE_HOME: [SERVER] vecAbsVelocity isn't valid
+ if ( LengthSqr( direction ) < 0.0 )
+ return <0, 0, RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_VERTICAL_HEIGHT>
+
+ return direction
+}
+
+void function CancelAirControlLossAfterTouchGround( entity player )
+{
+ player.Signal( "CancelAirControlLoss" )
+}
+
+
+void function BatteryThiefHighlight( entity player )
+{
+ Highlight_SetEnemyHighlight( player, "battery_thief" )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( !IsValid( player ) )
+ return
+
+ if ( Hightlight_HasEnemyHighlight( player, "battery_thief" ) )
+ Highlight_ClearEnemyHighlight( player )
+ }
+ )
+
+ wait RODEO_BATTERY_THIEF_ICON_DURATION
+}
+
+void function ForceTitanRodeoToEnd( entity titan ) //TODO: Not typed since it is added via anim event
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) )
+ return
+
+ SoulRodeoEnds( soul, null )
+}
+
+
+void function SoulRodeoEnds( entity soul, var damageInfo )
+{
+ entity titan = soul.GetTitan()
+
+ if( !IsValid( titan ) )
+ return
+
+ entity rider = GetRodeoPilot( titan )
+
+ if ( !IsValid( rider ) )
+ return
+
+ rider.Signal( "RodeoOver" )
+ rider.ClearParent()
+}
+
+
+void function EnableTitanRodeo( entity titan )
+{
+ Assert( titan.IsTitan(), "tried calling EnableTitanRodeo on non-titan" )
+
+ entity titanSoul = titan.GetTitanSoul()
+
+ Assert( IsValid( titanSoul ) )
+
+ titanSoul.SetIsValidRodeoTarget( true ) //Lets rodeo happen on them.
+}
+
+
+void function DisableTitanRodeo( entity titan )
+{
+ Assert( titan.IsTitan(), "tried calling DisableTitanRodeo( on non-titan" )
+
+ entity titanSoul = titan.GetTitanSoul()
+
+ Assert( IsValid( titanSoul ) )
+
+ titanSoul.SetIsValidRodeoTarget( false ) //Stops rodeo from happening on them.
+}
+
+void function AddOnRodeoStartedCallback( void functionref(entity,entity) callbackFunc )
+{
+ Assert (!( file.onRodeoStartedCallbacks.contains( callbackFunc ) ))
+ file.onRodeoStartedCallbacks.append( callbackFunc )
+}
+
+void function AddOnRodeoEndedCallback( void functionref(entity,entity) callbackFunc )
+{
+ Assert (!( file.onRodeoEndedCallbacks.contains( callbackFunc ) ))
+ file.onRodeoEndedCallbacks.append( callbackFunc )
+}
+
+function PlayerBeginsTitanRodeo( entity player, RodeoPackageStruct rodeoPackage, entity rodeoTitan )
+{
+ entity soul = rodeoTitan.GetTitanSoul()
+ Assert( IsValid( soul ) )
+
+ bool sameTeam = player.GetTeam() == rodeoTitan.GetTeam()
+ bool playerWasEjecting = player.p.pilotEjecting // have to store this off here because the "RodeoStarted" signal below ends eject, so it will be too late to check it in actual rodeo function. Used to check for eject -> rodeo
+
+ player.p.rodeoShouldAdjustJumpOffVelocity = true
+
+ player.Signal( "RodeoStarted" )
+
+ bool playerHadBatteryAtStartOfRodeo = PlayerHasBattery( player )
+
+ OnThreadEnd(
+ function () : ( player, soul, sameTeam, rodeoTitan, playerHadBatteryAtStartOfRodeo )
+ {
+ RodeoPackageStruct rodeoPackage = player.p.rodeoPackage
+ entity newRodeoTitan = rodeoTitan
+
+ //Clear the rodeo alert and update the newRodeoTitan to be the soul's titan
+ if ( IsValid( soul ) )
+ {
+ soul.SetLastRodeoHitTime( 0 ) //Clear rodeo warning for next time a player jumps on
+ newRodeoTitan = soul.GetTitan() //rodeoTitan might have changed because a player embarked/disembarked etc
+
+ foreach ( callbackFunc in file.onRodeoEndedCallbacks )
+ {
+ callbackFunc( player, newRodeoTitan )
+ }
+
+ for( int i = 0; i < soul.rodeoReservedSlots.len(); ++i )
+ {
+ if ( soul.rodeoReservedSlots[ i ] == player )
+ {
+ soul.rodeoReservedSlots[ i ] = null
+ break
+ }
+ }
+
+ if ( soul.soul.batteryContainerBeingUsed && playerHadBatteryAtStartOfRodeo ) //i.e. rodeo got interruped early
+ {
+ string titanType = GetSoulTitanSubClass( soul )
+ entity batteryContainer = soul.soul.batteryContainer
+ batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up" ) )
+ soul.soul.batteryContainerBeingUsed = false
+
+ }
+
+ // if the player is invalid, we still need to enable rodeo on the titan
+ // normally this would happen in Rodeo_Detach(), but that only works if the player is valid
+ if ( !IsValid( player ) )
+ EnableTitanRodeo( newRodeoTitan )
+ }
+
+ if ( IsValid( player ) )
+ {
+ player.Signal( "RodeoOver" )
+ player.SetNameVisibleToFriendly( true ) // show name of the pilot again
+ player.SetNameVisibleToEnemy( true )
+ ClearPlayerAnimViewEntity( player )
+ player.AnimViewEntity_SetLerpOutTime( 0.4 ) // blend out the clear anim view entity
+ player.ClearParent()
+ player.Anim_Stop()
+ player.SetOneHandedWeaponUsageOff()
+ player.SetTitanSoulBeingRodeoed( null )
+ player.UnforceStand()
+ player.kv.PassDamageToParent = false
+ player.TouchGround() // so you can double jump off
+ StopSoundOnEntity( player, rodeoPackage.cockpitSound )
+ StopSoundOnEntity( player, rodeoPackage.worldSound )
+ if ( Rodeo_IsAttached( player ) )
+ Rodeo_Detach( player )
+
+ if ( IsAlive( player ) )
+ {
+ int attachIndex = newRodeoTitan.LookupAttachment( rodeoPackage.attachPoint )
+ vector startPos = newRodeoTitan.GetAttachmentOrigin( attachIndex )
+
+ if ( !PlayerCanTeleportHere( player, startPos, newRodeoTitan ) )
+ {
+ startPos = newRodeoTitan.GetOrigin()
+ if ( !PlayerCanTeleportHere( player, startPos, newRodeoTitan ) )
+ startPos = player.GetOrigin()
+ }
+
+ thread PlayerJumpsOffRodeoTarget( player, newRodeoTitan, startPos )
+ }
+
+ #if MP
+ player.Signal( "RodeoNukeWindowEnded" )
+ if ( player in file.playersThatWantToUseRodeoGrenade )
+ delete file.playersThatWantToUseRodeoGrenade[ player ]
+ #endif
+ }
+ }
+ )
+
+
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "OnDestroy" )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "RodeoOver" )
+
+ string rodeoTitanType = rodeoPackage.rodeoTargetType
+
+ #if MP
+ thread OpenRodeoNukeWindow( player, rodeoTitan )
+ #endif
+
+ thread WatchForPlayerJumpingOffRodeo( player )
+
+ // hide name of the pilot while he is rodeoing
+ player.SetNameVisibleToFriendly( false )
+ player.SetNameVisibleToEnemy( false )
+ player.ForceStand()
+ thread ManagePlayerWeaponDeployment( player, soul ) //Spin this off in its own thread since there are multiple ways for weapon to be deployed
+ player.SetOneHandedWeaponUsageOn()
+ player.TouchGround() // so you can double jump off
+ player.SetTitanSoulBeingRodeoed( soul )
+
+ if ( soul.GetShieldHealth() > 0.0 ) // This was not evaluating properly with 0 being an int, so make it a float which works
+ GiveFriendlyRodeoPlayerProtection( rodeoTitan )
+
+ foreach ( callbackFunc in file.onRodeoStartedCallbacks )
+ callbackFunc( player, rodeoTitan )
+
+ if ( player.GetTeam() != rodeoTitan.GetTeam() && !PlayerHasPassive( player, ePassives.PAS_STEALTH_MOVEMENT ) )
+ soul.SetLastRodeoHitTime( Time() ) // Alert Titan immediately if you don't have passive
+
+ soul.soul.batteryMovedDown = false
+ if ( ShouldThrowGrenadeInHatch( player ) ) //Either player is going to apply a battery, or it's going to throw a grenade. In either case, we want the battery to move
+ {
+ Rodeo_MoveBatteryDown( soul )
+ }
+
+ soul.soul.batteryContainerBeingUsed = true //All rodeo points mark batteryContainer as being true, various exit points mark it as being false when they are done cleaning it up (e.g. playing the appropriate battery going up/down anims)
+
+ if ( !sameTeam )
+ {
+ #if FACTION_DIALOGUE_ENABLED
+ thread PlayRodeoFactionDialogueAfterDelay( player, 0.5 )
+ #endif
+ TitanVO_AlertTitansTargetingThisTitanOfRodeo( player, soul )
+ }
+
+ waitthread PlayerClimbsIntoRodeoPosition( player, rodeoTitan, rodeoPackage, playerWasEjecting )
+
+ #if MP
+ player.Signal( "RodeoNukeWindowEnded" )
+ #endif
+
+ // There has been a wait, verify things are still valid.
+
+ if ( !IsValid( soul ) )
+ return
+
+ entity rodeoTitan = soul.GetTitan()
+
+ if ( !IsAlive( rodeoTitan ) )
+ return
+
+ TryBatteryStyleRodeo( player, rodeoTitan, soul, rodeoPackage )
+}
+
+#if FACTION_DIALOGUE_ENABLED
+void function PlayRodeoFactionDialogueAfterDelay( entity player, float delay = 0.5 )
+{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "RodeoOver" )
+
+ wait delay
+ PlayFactionDialogueToPlayer( "kc_rodeo", player )
+}
+#endif
+
+void function Rodeo_MoveBatteryDown( entity soul )
+{
+ if ( soul.soul.batteryMovedDown )
+ return
+
+ string titanType = GetSoulTitanSubClass( soul )
+ entity batteryContainer = soul.soul.batteryContainer
+ batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_down" ) )
+ soul.soul.batteryMovedDown = true
+}
+
+void function ManagePlayerWeaponDeployment( entity player, entity titanSoul )
+{
+ HolsterAndDisableWeapons( player )
+
+ titanSoul.EndSignal( "OnTitanDeath" )
+ titanSoul.EndSignal( "OnDestroy" )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "RodeoOver" )
+ player.EndSignal( "FriendlyRodeoDeployWeapon" )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ DeployAndEnableWeapons( player )
+ }
+ )
+
+ WaitForever()
+}
+
+
+
+
+vector function GetAntiRodeoThrowOffDirection( entity rodeoRider, entity titan )
+{
+ vector backward
+ vector right
+
+ if ( titan.IsPlayer() )
+ {
+ backward = titan.GetViewForward() * -1.0
+ right = titan.GetViewRight()
+ }
+ else
+ {
+ backward = titan.GetForwardVector() * -1.0
+ right = titan.GetRightVector()
+ }
+
+ backward.z = 0
+ right.z = 0
+
+ // map the player's controls to his angles, and add that velocity
+ float xAxis = rodeoRider.GetInputAxisRight()
+ float yAxis = rodeoRider.GetInputAxisForward()
+
+ xAxis = GraphCapped( xAxis, -1.0, 1.0, -0.4, 0.4 )
+ yAxis = GraphCapped( yAxis, -1.0, 1.0, 1.0, 0.75 ) //Cap it so you don't actually let the players jump forwards
+
+ vector direction
+ if ( fabs( xAxis ) < 0.2 && fabs( yAxis ) < 0.2 )
+ {
+ // no significant controller deflection, just push forward by 0.75 as default
+ direction = backward * 0.75
+ }
+ else
+ {
+ vector forwardVec = backward * yAxis
+ vector rightVec = right * xAxis
+ direction = rightVec + forwardVec
+ }
+
+ direction *= 600
+ direction.z = 25
+
+ return direction
+}
+
+
+void function RodeoPilotPullsOutWeapon( entity rodeoPilot, entity rodeoTitan, string rodeoTitanType )
+{
+ PlayerRodeoViewCone( rodeoPilot, rodeoTitanType )
+
+ Rodeo_OnFinishClimbOnAnimation( rodeoPilot ) // This is to let code know the rodeoPilot has finished climbing on the rodeo and ready to fire
+ rodeoPilot.Signal( "FriendlyRodeoDeployWeapon" )
+}
+
+void function TryBatteryStyleRodeo( entity rodeoPilot, entity rodeoTitan, entity titanSoul, RodeoPackageStruct rodeoPackage )
+{
+ titanSoul.EndSignal( "OnTitanDeath" )
+
+ string rodeoTitanType = rodeoPackage.rodeoTargetType
+ if ( rodeoPilot.GetTeam() == rodeoTitan.GetTeam() )
+ {
+ if ( PilotCanApplyBattery( rodeoPilot, rodeoTitan ) )
+ waitthread PlayerAppliesBatteryPack( rodeoPilot, rodeoTitan, titanSoul, rodeoPackage )
+
+ rodeoTitan = titanSoul.GetTitan()
+ Assert( IsAlive( rodeoTitan ) )
+
+ //printt( "After applying battery" )
+
+ //This is default R1 style rodeo, with the panel ripped and ready to be shot at
+ FirstPersonSequenceStruct sequence
+ sequence.attachment = "hijack"
+ sequence.thirdPersonAnimIdle = GetAnimFromAlias( rodeoTitanType, "pt_rodeo_back_right_idle" )
+ sequence.firstPersonAnimIdle = GetAnimFromAlias( rodeoTitanType, "ptpov_rodeo_back_right_idle" )
+ sequence.useAnimatedRefAttachment = true
+ thread FirstPersonSequence( sequence, rodeoPilot, rodeoTitan )
+
+ RodeoPilotPullsOutWeapon( rodeoPilot, rodeoTitan, rodeoTitanType )
+ WaitForever()
+
+ }
+
+ #if MP
+ if ( PlayerWantsToThrowNukeGrenade( rodeoPilot ) )
+ {
+ waitthread PlayerAppliesBatteryPack( rodeoPilot, rodeoTitan, titanSoul, rodeoPackage )
+ return
+ }
+ #endif
+
+ if ( ShouldThrowGrenadeInHatch( rodeoPilot ) )
+ {
+ waitthread PlayerThrowsGrenadeInHatch( rodeoPilot, rodeoTitan, titanSoul, rodeoPackage ) //This ends rodeo at the end of the sequence
+ }
+ else
+ {
+ waitthread PlayerRemovesBatteryPack( rodeoPilot, rodeoTitan, titanSoul, rodeoPackage ) //This ends rodeo at the end of the sequence
+ }
+
+}
+
+struct RodeoRiderSequenceStruct
+{
+ bool wasCloaked = false
+ float cloakEndTime = 0.0
+ string interiorSound = ""
+ string exteriorSound = ""
+}
+
+void function DisableCloakBeforeRodeoSequence( entity rodeoPilot, RodeoRiderSequenceStruct dataStruct )
+{
+ if ( !IsCloaked( rodeoPilot ) )
+ return
+
+ dataStruct.wasCloaked = true
+ dataStruct.cloakEndTime = rodeoPilot.GetCloakEndTime()
+ DisableCloak( rodeoPilot, 0.0 )
+
+}
+
+void function RestoreCloakAfterRodeoSequence( entity rodeoPilot, RodeoRiderSequenceStruct dataStruct )
+{
+ if ( !IsAlive( rodeoPilot ) )
+ return
+
+ if ( !dataStruct.wasCloaked )
+ return
+
+ Assert( dataStruct.cloakEndTime > 0.0 )
+
+ float remainingCloakDuration = max( 0.0, dataStruct.cloakEndTime - Time() )
+ if ( remainingCloakDuration > CLOAK_FADE_IN ) //Has to be greater than 1.0 fade in duration, otherwise will cloak forever
+ EnableCloak( rodeoPilot, remainingCloakDuration, CLOAK_FADE_IN )
+}
+
+void function PlayerRemovesBatteryPack( entity rodeoPilot, entity rodeoTitan, entity titanSoul, RodeoPackageStruct rodeoPackage )
+{
+ string titanType = GetSoulTitanSubClass( titanSoul )
+
+ RodeoRiderSequenceStruct dataStruct
+ dataStruct.interiorSound = GetAudioFromAlias( titanType, "rodeo_battery_steal_1p" )
+ dataStruct.exteriorSound = GetAudioFromAlias( titanType, "rodeo_battery_steal_3p" )
+ DisableCloakBeforeRodeoSequence( rodeoPilot, dataStruct )
+
+ entity tempBattery3p
+ tempBattery3p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS )
+ tempBattery3p.SetParent( rodeoPilot, "R_HAND", false, 0.0 )
+ tempBattery3p.RemoveFromSpatialPartition()
+ tempBattery3p.Hide()
+
+ entity pilotFirstPersonProxy = rodeoPilot.GetFirstPersonProxy()
+ entity tempBattery1p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS )
+ tempBattery1p.SetParent( pilotFirstPersonProxy, "R_HAND", false, 0.0 )
+ tempBattery1p.RemoveFromSpatialPartition()
+ tempBattery1p.Hide()
+
+ rodeoPilot.p.rodeoAnimTempProps.append( tempBattery1p )
+ rodeoPilot.p.rodeoAnimTempProps.append( tempBattery3p )
+
+ AddAnimEvent( rodeoPilot, "rodeo_battery_show", RodeoBatteryRemoval_ShowBattery ) //Consider adding this in add player
+ AddAnimEvent( rodeoPilot, "rodeo_battery_rip", RodeoBatteryRemoval )
+ AddAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning", RodeoBatteryStealthMovementWarning )
+ thread MonitorRodeoPastPointOfNoReturn( rodeoPilot, titanSoul )
+
+ OnThreadEnd(
+ function() : ( rodeoPilot, titanSoul, titanType, dataStruct )
+ {
+ if ( IsValid( titanSoul ) )
+ {
+ entity rodeoPanel = titanSoul.soul.batteryContainer
+ if ( IsValid( rodeoPanel ) )
+ {
+ titanSoul.soul.batteryContainerBeingUsed = false
+
+ if ( titanSoul.soul.batteryContainerPastPointOfNoReturn )
+ {
+ rodeoPanel.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up" ) )
+
+ }
+ else
+ {
+ rodeoPanel.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up_idle" ) )
+ rodeoPanel.Anim_DisableSequenceTransition() //Snap into place instead of blending
+ }
+
+ titanSoul.soul.batteryContainerPastPointOfNoReturn = false
+ }
+ }
+
+
+ if ( !IsValid( rodeoPilot ) )
+ return
+
+ if ( HasAnimEvent( rodeoPilot, "rodeo_battery_rip" ) )
+ DeleteAnimEvent( rodeoPilot, "rodeo_battery_rip" )
+
+ if ( HasAnimEvent( rodeoPilot, "rodeo_battery_show" ) )
+ DeleteAnimEvent( rodeoPilot, "rodeo_battery_show" )
+
+ if ( HasAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning" ) )
+ DeleteAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning" )
+
+ ClearRodeoAnimTempProps( rodeoPilot )
+
+ StopSoundOnEntity( rodeoPilot, dataStruct.interiorSound )
+ StopSoundOnEntity( rodeoPilot, dataStruct.exteriorSound )
+
+ RestoreCloakAfterRodeoSequence( rodeoPilot, dataStruct )
+ }
+ )
+
+ FirstPersonSequenceStruct sequence
+ sequence.attachment = "hijack"
+ string batteryRipAnim = GetAnimFromAlias( titanType, "pt_rodeo_back_right_hijack_battery" ) // default, old style
+ //printt( "Battery Rip Anim: " + batteryRipAnim )
+ sequence.thirdPersonAnim = batteryRipAnim
+ sequence.firstPersonAnim = GetAnimFromAlias( titanType, "ptpov_rodeo_back_right_hijack_battery" )
+
+ if ( GetBugReproNum() == 112023 )
+ rodeoTitan.SnapEyeAngles( < 89, 100.02, 0 > )
+
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( dataStruct.interiorSound, dataStruct.exteriorSound, rodeoPilot, rodeoTitan ) //Play sound on rodeoPilot instead of panel
+ FirstPersonSequence( sequence, rodeoPilot, rodeoTitan )
+}
+
+void function MonitorRodeoPastPointOfNoReturn( entity rodeoPilot, entity titanSoul )
+{
+ titanSoul.Signal( "MonitorRodeoPastPointOfNoReturn" )
+ titanSoul.EndSignal( "MonitorRodeoPastPointOfNoReturn" )
+ rodeoPilot.EndSignal( "RodeoOver" )
+
+ titanSoul.soul.batteryContainerPastPointOfNoReturn = false
+
+ rodeoPilot.WaitSignal( "RodeoPointOfNoReturn" )
+ titanSoul.soul.batteryContainerPastPointOfNoReturn = true
+}
+
+void function PlayerThrowsGrenadeInHatch( entity rodeoPilot, entity rodeoTitan, entity titanSoul, RodeoPackageStruct rodeoPackage )
+{
+ AddAnimEvent( rodeoPilot, "rodeo_battery_grenade_damage", RodeoBatteryRemoval )
+ AddAnimEvent( rodeoPilot, "rodeo_battery_grenade_show", RodeoBatteryGrenadeShow )
+ AddAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning", RodeoBatteryStealthMovementWarning )
+
+ entity grenade3p = CreatePropDynamic( GRENADE_MODEL )
+ grenade3p.SetParent( rodeoPilot, "PROPGUN", false, 0.0 )
+ grenade3p.RemoveFromSpatialPartition()
+ grenade3p.Hide()
+
+ entity grenade1p = CreatePropDynamic( GRENADE_MODEL )
+ grenade1p.SetParent( rodeoPilot.GetFirstPersonProxy(), "PROPGUN", false, 0.0 )
+ grenade1p.RemoveFromSpatialPartition()
+ grenade1p.Hide()
+
+ rodeoPilot.p.rodeoAnimTempProps.append( grenade3p )
+ rodeoPilot.p.rodeoAnimTempProps.append( grenade1p )
+
+ string titanType = GetSoulTitanSubClass( titanSoul )
+ RodeoRiderSequenceStruct dataStruct
+ dataStruct.interiorSound = GetAudioFromAlias( titanType, "rodeo_grenade_1p" )
+ dataStruct.exteriorSound = GetAudioFromAlias( titanType, "rodeo_grenade_3p" )
+ DisableCloakBeforeRodeoSequence( rodeoPilot, dataStruct )
+
+ OnThreadEnd(
+ function() : ( rodeoPilot, titanSoul, titanType, dataStruct )
+ {
+ if ( IsValid( titanSoul ) )
+ {
+ titanSoul.soul.batteryContainerBeingUsed = false
+ titanSoul.soul.batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up" ) )
+ }
+
+ if ( !IsValid( rodeoPilot ) )
+ return
+
+ if ( HasAnimEvent( rodeoPilot, "rodeo_battery_grenade_damage" ) )
+ DeleteAnimEvent( rodeoPilot, "rodeo_battery_grenade_damage" )
+
+ if ( HasAnimEvent( rodeoPilot, "rodeo_battery_grenade_show" ) )
+ DeleteAnimEvent( rodeoPilot, "rodeo_battery_grenade_show" )
+
+ if ( HasAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning" ) )
+ DeleteAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning" )
+
+ ClearRodeoAnimTempProps( rodeoPilot )
+
+ StopSoundOnEntity( rodeoPilot, dataStruct.interiorSound )
+ StopSoundOnEntity( rodeoPilot, dataStruct.exteriorSound )
+
+ RestoreCloakAfterRodeoSequence( rodeoPilot, dataStruct )
+ }
+ )
+
+ FirstPersonSequenceStruct sequence
+ sequence.attachment = "hijack"
+ //string batteryRipAnim = GetAnimFromAlias( titanSubClass, "pt_rodeo_back_right_hijack_battery" ) // Do this once the animations aren't named the same/enabled for different titans
+ //printt( "Battery Rip Anim: " + batteryRipAnim )
+ sequence.thirdPersonAnim = GetAnimFromAlias( titanType, "pt_rodeo_grenade" )
+ sequence.firstPersonAnim = GetAnimFromAlias( titanType, "ptpov_rodeo_grenade" )
+
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( dataStruct.interiorSound, dataStruct.exteriorSound, rodeoPilot, rodeoTitan ) //Play sound on rodeoPilot instead of panel
+
+ waitthread FirstPersonSequence( sequence, rodeoPilot, rodeoTitan )
+}
+
+void function PlayerAppliesBatteryPack_DelayedClearSyncedEntity( entity rodeoPilot, entity titanSoul )
+{
+ if ( !IsValid( rodeoPilot ) )
+ {
+ return
+ }
+
+ if ( !IsValid( titanSoul ) )
+ {
+ return
+ }
+
+ if ( titanSoul.soul.batteryContainerBeingUsed )
+ {
+ return
+ }
+
+ rodeoPilot.SetSyncedEntity( null )
+}
+
+void function PlayerAppliesBatteryPack( entity rodeoPilot, entity rodeoTitan, entity titanSoul, RodeoPackageStruct rodeoPackage )
+{
+
+ entity battery
+
+ #if MP
+ bool nukeVersion = false
+ if ( PlayerWantsToThrowNukeGrenade( rodeoPilot ) )
+ {
+ nukeVersion = true
+ // battery = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS )
+ }
+ else
+ {
+
+ battery = GetBatteryOnBack( rodeoPilot )
+ battery.Hide() //Hide it because the animation has a battery model already
+ }
+ #else
+ battery = GetBatteryOnBack( rodeoPilot )
+ battery.Hide() //Hide it because the animation has a battery model already
+ #endif
+
+ entity rodeoPanel = titanSoul.soul.batteryContainer
+
+ entity tempBattery3p
+ tempBattery3p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS )
+ tempBattery3p.SetParent( rodeoPilot, "R_HAND", false, 0.0 )
+ tempBattery3p.RemoveFromSpatialPartition()
+
+ entity tempBattery1p
+ tempBattery1p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS )
+ tempBattery1p.SetParent( rodeoPilot.GetFirstPersonProxy(), "R_HAND", false, 0.0 )
+ tempBattery1p.RemoveFromSpatialPartition()
+
+ #if MP
+ if ( nukeVersion )
+ tempBattery1p.SetSkin( 1 )
+ #endif
+ if ( IsAmpedBattery( battery ) )
+ {
+ tempBattery1p.SetSkin( 2 )
+ tempBattery3p.SetSkin( 2 )
+ }
+
+ rodeoPilot.p.rodeoAnimTempProps.append( tempBattery3p )
+ rodeoPilot.p.rodeoAnimTempProps.append( tempBattery1p )
+
+ string soundAlias = "rodeo_battery_return"
+ string animAlias = "rodeo_back_right_apply_battery"
+
+ #if MP
+ if ( nukeVersion )
+ {
+ soundAlias = "nuke_rodeo_battery_return"
+ animAlias = "nuke_rodeo_back_right_apply_battery"
+ }
+ #endif
+
+ string titanType = GetSoulTitanSubClass( titanSoul )
+ RodeoRiderSequenceStruct dataStruct
+ dataStruct.interiorSound = GetAudioFromAlias( titanType, soundAlias + "_1p" )
+ dataStruct.exteriorSound = GetAudioFromAlias( titanType, soundAlias + "_3p" )
+ DisableCloakBeforeRodeoSequence( rodeoPilot, dataStruct )
+
+ OnThreadEnd(
+ function() : ( battery, titanSoul, titanType, rodeoPilot, dataStruct )
+ {
+ if ( IsValid( battery ) )
+ battery.Show()
+
+ entity batteryContainer = titanSoul.soul.batteryContainer
+ if ( IsValid( batteryContainer ) )
+ {
+ if ( IsValid( battery ) )
+ {
+ batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up" ) )
+ }
+ else
+ {
+ batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up_idle" ) )
+ batteryContainer.Anim_DisableSequenceTransition()
+
+ }
+
+ if ( IsValid( rodeoPilot ) && IsValid( titanSoul ) )
+ {
+ delaythread( 0.1 ) PlayerAppliesBatteryPack_DelayedClearSyncedEntity( rodeoPilot, titanSoul )
+ }
+
+ titanSoul.soul.batteryContainerBeingUsed = false
+ }
+
+
+ if ( !IsValid( rodeoPilot ) )
+ return
+
+ ClearRodeoAnimTempProps( rodeoPilot )
+
+ StopSoundOnEntity( rodeoPilot, dataStruct.interiorSound )
+ StopSoundOnEntity( rodeoPilot, dataStruct.exteriorSound )
+
+ RestoreCloakAfterRodeoSequence( rodeoPilot, dataStruct )
+ }
+ )
+
+ FirstPersonSequenceStruct sequence
+ sequence.attachment = "hijack"
+ string batteryApplicationAnim = GetAnimFromAlias( titanType, "pt_" + animAlias ) // default, old style
+ //printt( "Battery Application Anim: " + batteryApplicationAnim )
+ sequence.thirdPersonAnim = batteryApplicationAnim
+ sequence.firstPersonAnim = GetAnimFromAlias( titanType, "ptpov_" + animAlias )
+
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( dataStruct.interiorSound, dataStruct.exteriorSound, rodeoPilot, rodeoTitan ) //Play sound on rodeoPilot instead of panel
+
+ entity batteryContainer = titanSoul.soul.batteryContainer
+ if ( batteryContainer )
+ {
+ rodeoPilot.SetSyncedEntity( batteryContainer )
+ }
+
+ waitthread FirstPersonSequence( sequence, rodeoPilot, rodeoTitan )
+
+ //Time passed, need to update titan reference
+ rodeoTitan = titanSoul.GetTitan()
+ Assert( IsAlive( rodeoTitan ) )
+
+
+ #if MP
+ if ( nukeVersion )
+ thread RodeoForceNuke( rodeoPilot )
+ else
+ Rodeo_PilotAddsBatteryToFriendlyTitan( rodeoPilot, rodeoTitan )
+ #else
+ Rodeo_PilotAddsBatteryToFriendlyTitan( rodeoPilot, rodeoTitan )
+ #endif
+
+}
+
+void function ClearRodeoAnimTempProps( entity player )
+{
+ foreach( tempProp in player.p.rodeoAnimTempProps )
+ {
+ if ( IsValid( tempProp ) )
+ tempProp.Destroy()
+ }
+
+ player.p.rodeoAnimTempProps.clear()
+}
+
+void function RodeoBatteryPackRemovalDamage( entity attacker, entity victim, entity victimTitanSoul )
+{
+ victimTitanSoul.e.lastRodeoAttacker = attacker
+
+ int damageAmount = GetSegmentHealthForTitan( victim )
+
+ if ( PlayerHasBattery( attacker ) ) //i.e. you are throwing a grenade
+ damageAmount /= 2
+
+ SetSoulBatteryCount( victimTitanSoul, GetSoulBatteryCount( victimTitanSoul ) - 1 )
+
+ int damageScriptType = damageTypes.rodeoBatteryRemoval
+
+ if ( GetDoomedState( victim ) )
+ {
+ damageAmount = victim.GetHealth() + 1
+ }
+ else if ( IsHardcoreGameMode() )
+ {
+ damageAmount = victim.GetHealth()
+ }
+
+ table damageTable =
+ {
+ scriptType = damageScriptType
+ forceKill = false
+ damageSourceId = eDamageSourceId.rodeo_battery_removal
+ origin = victim.GetOrigin()
+ hitbox = 2
+ }
+
+ victim.TakeDamage( damageAmount, attacker, attacker, damageTable )
+ if ( victim.IsNPC() )
+ victim.SetEnemyLKP( attacker, attacker.GetOrigin() )
+
+ entity batteryContainer = victimTitanSoul.soul.batteryContainer
+ int hatchAttachmentIndex = batteryContainer.LookupAttachment( "REF" )
+
+ TitanLoseSegementFX( victim, attacker, victim.GetAttachmentOrigin( hatchAttachmentIndex ) )
+
+ if ( IsSingleplayer() && attacker.IsPlayer() )
+ {
+ UnlockAchievement( attacker, achievements.RODEO )
+ }
+}
+
+void function Rodeo_DropAllBatteriesOnDeath( entity player, entity attacker, var damageInfo )
+{
+ Rodeo_DropAllBatteries( player )
+}
+
+void function Rodeo_ApplyAllBatteriesOnEmbark( entity player, entity titan )
+{
+ thread Rodeo_ApplyAllBatteriesOnEmbark_Thread( player, titan )
+}
+
+void function Rodeo_ApplyAllBatteriesOnEmbark_Thread( entity player, entity titan )
+{
+ player.EndSignal( "OnDeath" )
+
+ if ( !PlayerHasBattery( player ) )
+ return
+
+ entity soul = player.GetTitanSoul()
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "OnTitanDeath" )
+
+ table<string,bool> e
+ e[ "hadAmped" ] <- false
+
+ while ( GetPlayerBatteryCount( player ) > 0 )
+ {
+ thread Rodeo_ApplyBatteryDelayed( player, e )
+ }
+
+ wait 0.4
+
+ MessagePlayerGivingBatteryToTitan( player, player, eEventNotifications.Rodeo_YouEmbarkedWithABattery, -1, e[ "hadAmped" ] )
+}
+
+void function Rodeo_ApplyBatteryDelayed( entity player, table<string,bool> e )
+{
+ entity battery = Rodeo_TakeBatteryAwayFromPilot( player )
+ e[ "hadAmped" ] = e[ "hadAmped" ] || IsAmpedBattery( battery )
+ int skin = battery.GetSkin()
+ battery.Destroy()
+
+ entity dummyBattery = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS )
+ dummyBattery.SetSkin( skin )
+ dummyBattery.Hide()
+
+ entity soul = player.GetTitanSoul()
+
+ OnThreadEnd(
+ function() : ( dummyBattery ) {
+ if ( IsValid( dummyBattery ) )
+ dummyBattery.Destroy()
+ }
+ )
+
+ if ( !IsValid( soul ) )
+ return
+
+ dummyBattery.EndSignal( "OnDestroy" )
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "OnTitanDeath" )
+
+ wait 0.4 // delay so that it applies the battery when the player is inside the titan, so he can see the health bar change
+
+ if ( IsValid( soul.GetTitan() ) )
+ Rodeo_ApplyBatteryToTitan( dummyBattery, soul.GetTitan() )
+}
+
+void function Rodeo_GiveExecutingTitanABattery( entity attacker )
+{
+ Rodeo_ApplyBatteryToTitan( null, attacker )
+ MessagePlayerGivingBatteryToTitan( attacker, attacker, eEventNotifications.Rodeo_TitanPickedUpBattery, -1, false )
+}
+
+void function Rodeo_GiveBatteryToPlayer( entity player )
+{
+ if ( PlayerHasMaxBatteryCount( player ) )
+ return
+
+ entity battery = Rodeo_CreateBatteryPack()
+ Rodeo_OnTouchBatteryPack_Internal( player, battery ) //Just setting the origin to the player's origin also works, but it will parent weirdly to a pilot's back. probably because we end up doing 2 SetOrigins in the same frame
+}
+
+void function Burnmeter_AmpedBattery( entity player )
+{
+ #if MP
+ Burnmeter_EmergencyBattery( player )
+ entity battery = GetBatteryOnBack( player )
+
+ if ( battery == null ) // not ideal but at least the game won't crash
+ return
+
+ battery.SetSkin( 2 ) // yellow - CHANGE SKIN TO ORANGE someday
+ Battery_StartFX( battery )
+ #endif
+}
+
+void function Burnmeter_EmergencyBattery( entity player )
+{
+ entity battery = Rodeo_CreateBatteryPack()
+ if ( !PlayerHasMaxBatteryCount( player ) )
+ {
+ Rodeo_OnTouchBatteryPack_Internal( player, battery ) //Just setting the origin to the player's origin also works, but it will parent weirdly to a pilot's back. probably because we end up doing 2 SetOrigins in the same frame
+ return
+ }
+ else
+ {
+ //Based off ThrowBattery
+ vector ornull thrownSpot = CalculateSpotForThrownBattery( player, battery )
+
+ if ( thrownSpot == null )
+ thrownSpot = player.GetOrigin()
+
+ expect vector( thrownSpot )
+
+ vector viewVector = player.GetViewVector()
+
+ //printt( "viewVector: " + viewVector )
+ //battery.SetPhysics( MOVETYPE_FLYGRAVITY )
+
+ battery.SetParent( player ) //HACK: Clear Ground Entity of battery. Not really sure why this is needed
+ battery.ClearParent()
+
+ battery.SetAngles( < 0, 0, 0 > )
+ battery.SetOrigin( thrownSpot )
+
+ vector playerVel = player.GetVelocity()
+ vector verticalAdjustment = < 0, 0, 0 >
+ if ( playerVel.z == 0 )
+ verticalAdjustment = < 0, 0, 100 >
+
+ vector batteryVel = playerVel + viewVector * 50 + verticalAdjustment
+ battery.SetVelocity( batteryVel )
+
+ }
+
+}
+
+entity function Rodeo_CreateBatteryPack( entity titanStolenFrom = null )
+{
+ entity batteryPack = CreateEntity( "item_titan_battery" )
+ batteryPack.SetValueForModelKey( RODEO_BATTERY_MODEL )
+ batteryPack.kv.fadedist = 10000
+ DispatchSpawn( batteryPack )
+ batteryPack.SetModel( RODEO_BATTERY_MODEL )
+ batteryPack.s.touchEnabledTime <- 0
+ batteryPack.s.batteryCarriedStatusEffect <- 0
+
+ batteryPack.Minimap_SetAlignUpright( true )
+ batteryPack.Minimap_SetZOrder( MINIMAP_Z_OBJECT )
+ batteryPack.Minimap_SetClampToEdge( false )
+ batteryPack.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ batteryPack.Minimap_AlwaysShow( TEAM_IMC, null )
+
+ Battery_StartFX( batteryPack )
+
+ if ( HAS_BATTERY_THIEF_ICON && titanStolenFrom != null )
+ {
+ Assert( titanStolenFrom.IsTitan() )
+ if ( titanStolenFrom.IsPlayer() )
+ {
+ batteryPack.SetBossPlayer( titanStolenFrom )
+ }
+ else
+ {
+ entity titanOwner = titanStolenFrom.GetBossPlayer()
+ if ( IsValid( titanOwner ) )
+ batteryPack.SetBossPlayer( titanOwner )
+ }
+
+ thread ClearBatteryBossPlayerAfterDelay( batteryPack, titanStolenFrom, RODEO_BATTERY_THIEF_ICON_DURATION )
+ }
+
+ batteryPack.Highlight_SetInheritHighlight( true )
+
+ if ( IsSingleplayer() )
+ {
+ thread AttachTriggerToBattery( batteryPack )
+ }
+
+ //thread MonitorBatteryVelocity( batteryPack )
+ return batteryPack
+}
+
+void function Battery_StartFX( entity battery )
+{
+ Battery_StopFX( battery ) //Clear existing fx first. Not quite ideal but easier to do this than have bug potential for FX to stack on top of each other.
+ int attachID = battery.LookupAttachment( "fx_center" )
+
+ asset fx = BATTERY_FX_FRIENDLY
+ if ( IsAmpedBattery( battery ) )
+ fx = BATTERY_FX_AMPED
+
+ battery.e.fxArray.append( StartParticleEffectOnEntity_ReturnEntity( battery, GetParticleSystemIndex( fx ), FX_PATTACH_POINT_FOLLOW, attachID ) )
+}
+
+void function Battery_StopFX( entity battery )
+{
+ foreach( fx in battery.e.fxArray )
+ {
+ EffectStop( fx )
+ }
+
+ battery.e.fxArray.clear()
+}
+
+void function Battery_StopFXAndHideIconForPlayer( entity player )
+{
+ if ( !PlayerHasBattery( player ) )
+ return
+
+ entity battery = GetBatteryOnBack( player )
+
+ Battery_StopFX( battery )
+ battery.ClearBossPlayer() //Boss player controls visibility of icon
+}
+
+void function AttachTriggerToBattery( entity batteryPack )
+{
+ entity trigger = CreateEntity( "trigger_cylinder" )
+ trigger.SetRadius( 100 )
+ trigger.SetAboveHeight( 100 )
+ trigger.SetBelowHeight( 100 ) //i.e. make the trigger a sphere as opposed to a cylinder
+ trigger.SetOrigin( batteryPack.GetOrigin() )
+ trigger.SetParent( batteryPack )
+ trigger.kv.triggerFilterNpc = "none" // none
+ trigger.kv.triggerFilterPlayer = "titan" // titan players only
+ DispatchSpawn( trigger )
+ trigger.SetEnterCallback( BatteryTrigger_ApplyBattery )
+}
+
+void function BatteryTrigger_ApplyBattery( entity trigger, entity player )
+{
+ if ( player.IsTitan() )
+ {
+ entity batteryPack = trigger.GetParent()
+
+ if ( batteryPack != null )
+ {
+ Rodeo_OnTouchBatteryPack( player, batteryPack )
+ }
+ }
+}
+
+void function Rodeo_PilotPicksUpBattery_Silent( entity pilot, entity battery )
+{
+ Assert( battery.GetParent() == null )
+
+ if ( PlayerHasBattery( pilot ) )
+ {
+ battery.Destroy()
+ battery = GetBatteryOnBack( pilot )
+ }
+
+ SetPlayerBatteryCount( pilot, GetPlayerBatteryCount( pilot ) + 1 )
+ if ( GetPlayerBatteryCount( pilot ) == 1 )
+ {
+ battery.SetParent( pilot, "BATTERY_ATTACH" )
+ battery.MarkAsNonMovingAttachment()
+ battery.RemoveFromSpatialPartition()
+ SetBatteryOnBack( pilot, battery )
+ }
+
+ if ( GAMETYPE == FREE_AGENCY && PlayerHasMaxBatteryCount( pilot ) && PlayerEarnMeter_GetOwnedFrac( pilot ) < 1.0 )
+ {
+ Rodeo_RemoveAllBatteriesOffPlayer( pilot )
+ return
+ }
+
+ if ( battery.s.batteryCarriedStatusEffect == 0 )
+ battery.s.batteryCarriedStatusEffect = StatusEffect_AddEndless( battery, eStatusEffect.battery_carried, 1.0 )
+ battery.Minimap_Hide( TEAM_MILITIA, null )
+ battery.Minimap_Hide( TEAM_IMC, null )
+}
+
+void function Rodeo_PilotPicksUpBattery( entity pilot, entity battery )
+{
+ Rodeo_PilotPicksUpBattery_Silent( pilot, battery )
+ EmitSoundOnEntityOnlyToPlayer( pilot, pilot, PILOT_PICKS_UP_BATTERY_SOUND )
+ //AddPlayerHeldButtonEventCallback( player, IN_USE, Rodeo_PilotThrowsBattery, RODEO_THROW_BATTERY_BUTTON_HOLD_TIME )
+}
+
+entity function Rodeo_TakeBatteryAwayFromPilot( entity pilot )
+{
+ //RemovePlayerHeldButtonEventCallback( player, IN_USE, Rodeo_PilotThrowsBattery, RODEO_THROW_BATTERY_BUTTON_HOLD_TIME )
+ SetPlayerBatteryCount( pilot, GetPlayerBatteryCount( pilot ) - 1 )
+
+ if ( GetPlayerBatteryCount( pilot ) == 0 )
+ {
+ entity battery = GetBatteryOnBack( pilot )
+ Assert( IsValid( battery ) )
+ Assert( battery.GetParent() == pilot )
+
+ SetBatteryOnBack( pilot, null ) //Defensive fix for 209362. Set it to null before doing any other actions on it which might cause execution to jump somewhere else. I think doing PutEntityInSafeSpot() might cause this?
+ battery.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ battery.Minimap_AlwaysShow( TEAM_IMC, null )
+
+ battery.s.touchEnabledTime = Time() + 0.3
+
+ Battery_StartFX( battery ) //Needed to properly restore effect when player is killed while cloaked and carrying a battery
+
+ battery.Show()
+
+ if ( battery.s.batteryCarriedStatusEffect > 0 )
+ {
+ StatusEffect_Stop( battery, battery.s.batteryCarriedStatusEffect )
+ battery.s.batteryCarriedStatusEffect = 0
+ }
+
+ battery.ClearParent()
+ battery.AddToSpatialPartition()
+ battery.SetAngles( <0, 0, 0 > )
+ battery.SetVelocity( < 0, 0, 1 > )
+ PutEntityInSafeSpot( battery, pilot, null, pilot.GetOrigin(), battery.GetOrigin() ) //This might cause thread of execution to jump somewhere else, see 209362
+ return battery
+ }
+ else
+ {
+ return null
+ }
+
+ unreachable
+}
+
+void function Rodeo_PilotThrowsBattery( entity pilot )
+{
+ if ( pilot.ContextAction_IsActive() ) //Maybe letting you throw the battery out of the dropship might be cool?
+ return
+
+ entity battery = GetBatteryOnBack( pilot )
+
+ vector ornull thrownSpot = CalculateSpotForThrownBattery( pilot, battery )
+
+ if ( thrownSpot == null )
+ {
+ EmitSoundOnEntityOnlyToPlayer( pilot, pilot, "CoOp_SentryGun_DeploymentDeniedBeep" )
+ return
+ }
+
+ expect vector( thrownSpot )
+
+ vector viewVector = pilot.GetViewVector()
+
+ //printt( "viewVector: " + viewVector )
+
+ entity playerBattery = Rodeo_TakeBatteryAwayFromPilot( pilot )
+ Assert( playerBattery == battery )
+
+ //battery.SetPhysics( MOVETYPE_FLYGRAVITY )
+
+ battery.SetAngles( < 0, 0, 0 > )
+ battery.SetOrigin( thrownSpot )
+ vector pilotVel = pilot.GetVelocity()
+ vector verticalAdjustment = < 0, 0, 0 >
+ if ( pilotVel.z == 0 )
+ verticalAdjustment = < 0, 0, 200 >
+
+ vector batteryVel = pilotVel + viewVector * 300 + verticalAdjustment
+ //printt( "batteryVel: " + batteryVel)
+ //battery.SetVelocity( Vector( 0, 0, 0 ) )
+ battery.SetVelocity( batteryVel )
+
+ MessageToPlayer( pilot, eEventNotifications.Rodeo_YouDroppedABattery )
+}
+
+vector ornull function CalculateSpotForThrownBattery( entity pilot, entity battery )
+{
+ vector viewVector = pilot.GetViewVector()
+ vector eyePos = pilot.EyePosition()
+ vector batteryMins = battery.GetBoundingMins()
+ vector batteryMaxs = battery.GetBoundingMaxs()
+ vector endPos = eyePos + viewVector * 100
+ TraceResults hullResult = TraceHull( eyePos, endPos, batteryMins, batteryMaxs, pilot, TRACE_MASK_SOLID | TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE )
+
+ //PrintTraceResults( hullResult )
+
+ if ( hullResult.startSolid )
+ return null
+
+ if ( hullResult.hitEnt == pilot )
+ return null
+
+ if ( hullResult.fraction == 1.0 )
+ return endPos
+
+ return hullResult.endPos
+}
+
+void function Rodeo_DropAllBatteries( entity player )
+{
+ if ( !PlayerHasBattery( player ) )
+ return
+
+ while ( GetPlayerBatteryCount( player ) > 1 )
+ {
+ entity newBattery = Rodeo_CreateBatteryPack()
+ newBattery.s.touchEnabledTime = Time() + 0.3
+ //look into using the players bounds for placement, instead of hardcoded numbers
+ array<vector> offsets = [<0,0,0>, <30,0,0>, <0,30,0>, <0,-30,0> ]
+ newBattery.SetOrigin( player.GetWorldSpaceCenter() + offsets[ GetPlayerBatteryCount( player ) ] ) //Temp fix, should change the origin
+ newBattery.SetAngles( <0, 0, 0 > )
+ vector baseVelocity = player.GetVelocity()
+ baseVelocity.z = 0
+ newBattery.SetVelocity( baseVelocity + AnglesToForward( <0, RandomInt( 360.0 ), 0 > ) * 100 + <0,0,1> )
+ Rodeo_TakeBatteryAwayFromPilot( player )
+ }
+
+ entity battery = Rodeo_TakeBatteryAwayFromPilot( player )
+ Assert ( IsValid( battery ) )
+}
+
+void function Rodeo_RemoveBatteryOffPlayer( entity player ) //Meant to be used in prematch etc.
+{
+ if ( !PlayerHasBattery( player ) )
+ return
+
+ entity battery = Rodeo_TakeBatteryAwayFromPilot( player )
+ if ( IsValid( battery ) )
+ {
+ battery.Destroy()
+ }
+}
+
+void function Rodeo_RemoveAllBatteriesOffPlayer( entity player ) //Meant to be used in prematch etc.
+{
+ if ( !PlayerHasBattery( player ) )
+ return
+
+ while ( GetPlayerBatteryCount( player ) > 0 )
+ {
+ Rodeo_RemoveBatteryOffPlayer( player )
+ }
+}
+
+void function Rodeo_ApplyBatteryToTitan( entity battery, entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+
+ if ( !IsValid( soul ) )
+ return
+
+ int healingAmount
+ if ( IsSingleplayer() )
+ healingAmount = 2000
+ else
+ healingAmount = GetSegmentHealthForTitan( titan )
+
+ int health = titan.GetHealth()
+ int maxHealth = titan.GetMaxHealth()
+
+ SetSoulBatteryCount( soul, GetSoulBatteryCount( soul ) + 1 )
+
+ int healthDifference = maxHealth - health
+
+ if ( IsSingleplayer() )
+ {
+ if ( soul.IsDoomed() )
+ UndoomTitan( titan, 1 )
+ else if ( healthDifference >= healingAmount )
+ titan.SetHealth( titan.GetHealth() + healingAmount )
+ else
+ titan.SetHealth( titan.GetMaxHealth() )
+
+ if ( GetHealthFrac( titan ) >= BATTERY_PICKUP_IGNORE_FRAC )
+ {
+ titan.SetHealth( titan.GetMaxHealth() )
+ }
+
+ titan.GetTitanSoul().nextRegenTime = Time()
+ if ( healthDifference < healingAmount )
+ {
+ titan.GetTitanSoul().SetShieldHealth( healingAmount - healthDifference + titan.GetTitanSoul().GetShieldHealth() )
+ }
+
+ if ( GetShieldHealthFrac( titan ) >= BATTERY_PICKUP_IGNORE_FRAC )
+ {
+ titan.GetTitanSoul().SetShieldHealth( titan.GetTitanSoul().GetShieldHealthMax() )
+ }
+
+ }
+ else if ( IsMultiplayer() )
+ {
+ if ( SoulHasPassive( soul, ePassives.PAS_VANGUARD_DOOM ) && soul.IsDoomed() )
+ {
+ UndoomTitan( titan, 1 )
+ titan.SetHealth( 1 )
+ }
+ float coreFrac = GetCurrentPlaylistVarFloat( "battery_core_frac", 0.2 )
+ float shieldFrac = GetCurrentPlaylistVarFloat( "battery_shield_frac", 1.0 )
+ float ampedHealthSegmentFrac = GetCurrentPlaylistVarFloat( "amped_battery_health_frac", 2.0 )
+ float healthSegmentFrac = GetCurrentPlaylistVarFloat( "battery_health_frac", 0.5 )
+
+ AddCreditToTitanCoreBuilder( titan, coreFrac ) //Always give core
+
+ int shieldHealth = soul.GetShieldHealth()
+ int shieldMaxHealth = soul.GetShieldHealthMax()
+
+ int shieldDifference = shieldMaxHealth - shieldHealth
+
+ bool batteryIsAmped = IsAmpedBattery( battery )
+ float frac = batteryIsAmped ? ampedHealthSegmentFrac : healthSegmentFrac
+
+ int addHealth = int( healingAmount * frac )
+
+ int totalHealth = minint( titan.GetMaxHealth(), titan.GetHealth() + addHealth )
+ if ( soul.IsDoomed() && batteryIsAmped )
+ {
+ UndoomTitan( titan, 1 )
+ soul.SetShieldHealth( soul.GetShieldHealthMax() )
+ }
+ else
+ {
+ titan.SetHealth( totalHealth )
+ soul.SetShieldHealth( soul.GetShieldHealthMax() )
+ }
+ }
+
+ if ( battery != null )
+ {
+ Assert( battery.GetParent() == null )
+ battery.Destroy()
+ }
+}
+
+bool function Rodeo_OnTouchBatteryPack( entity player, entity batteryPack )
+{
+ Rodeo_OnTouchBatteryPack_Internal( player, batteryPack )
+
+ //Basically always return false since we don't want the battery pack to go away when being touched. ApplyBatteryToTitan() etc will deal with lifetime of battery
+ return false
+}
+
+void function Rodeo_OnTouchBatteryPack_Internal( entity player, entity batteryPack )
+{
+ float currentTime = Time()
+
+ if ( currentTime < batteryPack.s.touchEnabledTime )
+ return
+
+ if ( !IsAlive( player ) )
+ return
+
+ if ( player.IsPhaseShifted() )
+ return
+
+ if ( IsValid( batteryPack.GetParent() ) )
+ return
+
+ if ( PlayerHasMaxBatteryCount( player ) )
+ {
+ if ( IsSingleplayer() )
+ {
+ MessageToPlayer( player, eEventNotifications.BATT_Full, batteryPack )
+ }
+ return
+ }
+
+ if ( player.IsTitan() )
+ {
+ //Try Titans not being able to pick up battery
+ if ( GetCurrentPlaylistVarInt( "rodeo_battery_disembark_to_pickup", 1 ) == 1 )
+ {
+ if ( currentTime - player.p.batteryLastTouchedNotificationTime > 5.0 )
+ {
+ MessageToPlayer( player, eEventNotifications.Rodeo_DisembarkToPickUpBattery )
+ player.p.batteryLastTouchedNotificationTime = currentTime
+
+ }
+ }
+ else
+ {
+ if ( IsSingleplayer() )
+ {
+ if ( player.GetHealth() >= player.GetMaxHealth() * BATTERY_PICKUP_IGNORE_FRAC && player.GetTitanSoul().GetShieldHealth() >= player.GetTitanSoul().GetShieldHealthMax() * BATTERY_PICKUP_IGNORE_FRAC )
+ {
+ MessageToPlayer( player, eEventNotifications.BATT_HealthFull, batteryPack )
+ return
+ }
+ }
+ bool amped = IsAmpedBattery( batteryPack )
+
+ Rodeo_ApplyBatteryToTitan( batteryPack, player )
+ MessagePlayerGivingBatteryToTitan( player, player, eEventNotifications.Rodeo_TitanPickedUpBattery, -1, amped )
+ }
+ return
+ }
+ else
+ {
+ if ( IsCloaked( player ) )
+ Battery_StopFX( batteryPack ) //Will be turned on again when player loses cloak
+
+ Rodeo_PilotPicksUpBattery( player, batteryPack )
+ AddPlayerScore( player, "PilotBatteryPickup" )
+// MessageToPlayer( player, eEventNotifications.Rodeo_PilotPickedUpBattery )
+ return
+ }
+}
+
+void function Rodeo_PilotAddsBatteryToFriendlyTitan( entity rider, entity titan )
+{
+ if ( !titan.IsTitan() )
+ return
+
+ if ( titan.GetTeam() != rider.GetTeam() )
+ return
+
+ if ( !PlayerHasBattery( rider ) )
+ return
+
+ entity battery = Rodeo_TakeBatteryAwayFromPilot( rider )
+ bool amped = IsAmpedBattery( battery )
+
+ if ( file.applyBatteryCallback != null )
+ file.applyBatteryCallback( rider, titan, battery )
+
+ Rodeo_ApplyBatteryToTitan( battery, titan ) //This destroys the battery
+
+ AddPlayerScore( rider, "PilotBatteryApplied" )
+
+ EmitSoundOnEntityOnlyToPlayer( rider, rider, PILOT_APPLIES_BATTERY_TO_TITAN_HEALTH_RESTORED_SOUND )
+
+ if ( titan.IsPlayer() )
+ MessagePlayerGivingBatteryToTitan( titan, rider, eEventNotifications.Rodeo_PilotAppliedBatteryToYou, eEventNotifications.Rodeo_YouAppliedBatteryToTitan, amped )
+ else
+ MessagePlayerGivingBatteryToTitan( titan, rider, eEventNotifications.Rodeo_PilotAppliedBatteryToYourPetTitan, eEventNotifications.Rodeo_YouAppliedBatteryToPetTitan, amped )
+}
+
+void function MessagePlayerGivingBatteryToTitan( entity receivingTitan, entity givingPlayer, int enumForRecevingHealth, int enumForGivingHealth, bool wasAmped )
+{
+ entity receivingPlayer = receivingTitan
+
+ if ( !receivingTitan.IsPlayer() )
+ receivingPlayer = receivingTitan.GetBossPlayer()
+
+ if ( !IsValid( receivingPlayer ) )
+ return
+
+ MessageToPlayer( receivingPlayer, enumForRecevingHealth, givingPlayer, wasAmped )
+ if ( givingPlayer != receivingPlayer )
+ MessageToPlayer( givingPlayer, enumForGivingHealth, receivingTitan, wasAmped )
+}
+
+bool function IsTitanAtFullHealth( entity receivingTitan )
+{
+ if ( !receivingTitan.IsTitan() )
+ return false
+
+ return ( receivingTitan.GetHealth() == receivingTitan.GetMaxHealth() )
+}
+
+function DebugRodeoTimes()
+{
+ array<string> settings = [ "atlas", "ogre", "stryder" ]
+
+ array< asset > models = [ $"models/Humans/imc_pilot/male_cq/imc_pilot_male_cq.mdl", $"models/humans/pilot/female_cq/pilot_female_cq.mdl" ]
+ table times = {}
+
+ array<string> rodeoAnims = [
+ "pt_rodeo_move_back_entrance",
+ "pt_rodeo_move_right_entrance",
+ "pt_rodeo_move_front_entrance",
+ "pt_rodeo_move_front_lower_entrance",
+ "pt_rodeo_move_back_mid_entrance",
+ "pt_rodeo_move_back_lower_entrance",
+ "pt_rodeo_move_left_entrance"
+ ]
+
+ foreach ( model in models )
+ {
+ times[ model ] <- []
+ entity prop = CreatePropDynamic( model, Vector(0,0,0), Vector(0,0,0) )
+ printt( "Human model: " + model )
+
+ foreach ( setting in settings )
+ {
+ foreach ( alias in rodeoAnims )
+ {
+ string animation = GetAnimFromAlias( setting, alias )
+ float time = prop.GetSequenceDuration( animation )
+ times[ model ].append( { time = time, animation = animation } )
+ }
+ }
+
+ prop.Kill_Deprecated_UseDestroyInstead()
+ }
+
+ printt( "Time comparison: " )
+ bool wrong = false
+ for ( int i = 0; i < times[ models[0] ].len(); i++ )
+ {
+ if ( times[models[0]][i].time == times[models[1]][i].time )
+ {
+ printt( " MATCH: " + ( i + 1 ) + " times: " + times[models[0]][i].time + " " + times[models[1]][i].time + " " + times[models[1]][i].animation )
+ }
+ else
+ {
+ printt( "MISMATCH: " + ( i + 1 ) + " times: " + times[models[0]][i].time + " " + times[models[1]][i].time + " " + times[models[1]][i].animation )
+ }
+ if ( ( i + 1 ) % rodeoAnims.len() == 0 )
+ printt( " " )
+ }
+ Assert( !wrong, "Times did not match between male and female, see above" )
+}
+
+void function SetBatteryOnBack( entity player, entity battery )
+{
+ player.SetPlayerNetEnt( "batteryOnBack", battery )
+}
+
+bool function ClientCommand_RequestRodeoBattery( entity player, array<string> args )
+{
+ //PrintFunc()
+ if ( !ShouldLetPlayerRequestBattery( player ) )
+ return true
+
+ player.SetPlayerNetTime( "requestRodeoBatteryLastUsedTime", Time() )
+
+ foreach( friendlyPlayer in GetPlayerArrayOfTeam( player.GetTeam() ) )
+ {
+ if ( friendlyPlayer == player )
+ continue
+
+ if ( friendlyPlayer.IsTitan() )
+ continue
+
+ //Could check to see if players actually have a battery here, but that stops players from being told that they should pick up a battery for someone in need
+ MessageToPlayer( friendlyPlayer, eEventNotifications.Rodeo_RequestBattery, player )
+ }
+
+ return true
+}
+
+bool function ClientCommand_OfferRodeoBattery( entity player, array<string> args )
+{
+ //PrintFunc()
+ if ( args.len() != 1 )
+ return true
+
+ int friendlyTitanEntIndex = args[ 0 ].tointeger()
+
+ if ( friendlyTitanEntIndex < 1 ) //Data sanitation. GetEntByIndex() will assert if passed a negative number. 0 is always world spawn, so the first valid argument is 1
+ return true
+
+ entity friendlyTitan = GetEntByIndex( friendlyTitanEntIndex )
+
+ if ( !ShouldShowOfferRodeoBatteryHint( player, friendlyTitan ) )
+ return true
+
+ entity battery = GetBatteryOnBack( player )
+
+ MessageToPlayer( friendlyTitan, eEventNotifications.Rodeo_FriendlyPickedUpBattery, player, battery.GetEncodedEHandle() )
+
+ player.SetPlayerNetTime( "offerRodeoBatteryLastUsedTime", Time() )
+
+ return true
+
+}
+
+void function PlayerRodeoViewCone( entity player, string rodeoTargetType )
+{
+ player.PlayerCone_FromAnim()
+ player.GetFirstPersonProxy().HideFirstPersonProxy()
+ OpenViewCone( player )
+ player.PlayerCone_Disable()
+ player.EnableWorldSpacePlayerEyeAngles()
+}
+
+
+void function OpenViewCone( entity player )
+{
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -179 )
+ player.PlayerCone_SetMaxYaw( 181 )
+ player.PlayerCone_SetMinPitch( -60 )
+ player.PlayerCone_SetMaxPitch( 60 )
+}
+
+bool function PilotCanApplyBattery( entity rodeoPilot, entity rodeoTitan )
+{
+ if ( !IsAlive( rodeoTitan ) )
+ return false
+
+ if ( rodeoTitan.GetTeam() != rodeoPilot.GetTeam() )
+ return false
+
+ if ( !PlayerHasBattery( rodeoPilot ) )
+ return false
+
+ entity titanSoul = rodeoTitan.GetTitanSoul()
+ Assert( IsValid( titanSoul ) )
+
+ string titanType = GetSoulTitanSubClass( titanSoul )
+
+ return true
+}
+
+void function ClearBatteryBossPlayerAfterDelay( entity battery, entity titan, float delay )
+{
+ entity soul = titan.GetTitanSoul()
+
+ if ( !IsValid( soul ) )
+ return
+
+ soul.EndSignal( "OnTitanDeath" ) //End signal on soul to properly handle pilot getting in/out of titan
+ battery.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( battery )
+ {
+ if ( IsValid( battery ) )
+ battery.ClearBossPlayer()
+ }
+ )
+
+ wait delay
+}
+
+const float BATTERY_USES_ATTACKER_ORIGIN_THRESHOLD = 500 * 500 //500 seems like a lot, but the Titan melee execution sequences can go pretty far
+
+void function TitanDropsBatteryOnDeath( entity titan, var damageInfo ) //Todo: Might want to do something special for titan melee execution, so the attacker automatically gets a battery
+{
+ if ( !titan.IsTitan() )
+ return
+
+ entity battery = Rodeo_CreateBatteryPack()
+ vector titanOrigin = titan.GetOrigin()
+ battery.SetOrigin( titanOrigin )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ vector safeOrigin = titanOrigin
+ vector attackerOrigin
+ if ( IsValid( attacker ) )
+ {
+ vector attackerOrigin = attacker.GetOrigin()
+ float distSqr = DistanceSqr( attackerOrigin, titanOrigin )
+ //printt( "Distance sqr: " + distSqr )
+ if ( distSqr <= ( BATTERY_USES_ATTACKER_ORIGIN_THRESHOLD ) ) //
+ {
+ //printt( "Putting attackerOrigin as safeOrigin" )
+
+ safeOrigin = attackerOrigin
+ }
+ }
+
+ bool result = PutEntityInSafeSpot( battery, null, null, safeOrigin, titanOrigin )
+ if ( !result )
+ {
+ battery.Destroy() //Can't put the battery anywhere safe, so just destroy it.
+ //printt( "Destroy battery since we can't put it in a safe spot" )
+ }
+}
+
+void function ShowRequestRodeoBatteryHint_OnDamage( entity playerTitan, var damageInfo )
+{
+ ShowRequestRodeoBatteryHint( playerTitan )
+}
+
+void function ShowRequestRodeoBatteryHint_OnPilotBecomesTitan( entity player, entity titan )
+{
+ //printt( "player health: " + player.GetHealth() )
+ ShowRequestRodeoBatteryHint( player )
+}
+
+void function ShowRequestRodeoBatteryHint( entity playerTitan )
+{
+ //PrintFunc()
+ if ( !ShouldLetPlayerRequestBattery( playerTitan ) )
+ return
+
+ float currentTime = Time()
+
+ if ( playerTitan.p.rodeoRequestBatteryHintLastShownTime > 0.0 && currentTime < playerTitan.p.rodeoRequestBatteryHintLastShownTime + REQUEST_RODEO_BATTERY_HINT_COOLDOWN ) //Use a different cooldown for the hint as opposed to the ability
+ {
+ //printt( "Current time: " + currentTime + ", lastShownTime: " + playerTitan.p.rodeoRequestBatteryHintLastShownTime + ", cooldown: " + REQUEST_RODEO_BATTERY_HINT_COOLDOWN )
+ return
+ }
+
+ int stringID = GetStringID( "#RODEO_REQUEST_BATTERY_HINT" )
+ MessageToPlayer( playerTitan, eEventNotifications.Rodeo_ShowBatteryHint, null, stringID )
+
+ playerTitan.p.rodeoRequestBatteryHintLastShownTime = currentTime
+}
+
+void function SetSoulBatteryCount( entity soul, int count )
+{
+ count = maxint( 0, count )
+
+ soul.SetTitanSoulNetInt( "rodeoBatteryCount", count )
+}
+
+void function PilotBattery_SetMaxCount( int batteryCount )
+{
+ file.maxPilotBatteryCount = batteryCount
+}
+
+bool function PlayerHasMaxBatteryCount( entity player )
+{
+ if ( !PlayerHasBattery( player ) )
+ {
+ Assert( GetPlayerBatteryCount( player ) == 0 )
+ return false
+ }
+
+ return GetPlayerBatteryCount( player ) == file.maxPilotBatteryCount
+}
+
+void function ThrowRiderOff( entity rider, entity titan, vector direction, bool adjustAirControl = true )
+{
+ if ( GetBugReproNum() == 112023 ) //Track down why eye angles of rider snaps violently when titan is looking downwards
+ {
+ thread AnglesDebug( rider )
+ }
+
+ rider.p.rodeoShouldAdjustJumpOffVelocity = false
+
+ rider.Signal( "RodeoOver" )
+ rider.ClearParent()
+
+ #if DEV
+ if ( GetDebugRodeoPrint() )
+ printt( "Throw Rider off: origin before vertical adjustment: " + rider.GetOrigin() )
+ #endif
+
+ rider.SetOrigin( rider.GetOrigin() + Vector( 0, 0, 100 ) )
+
+ #if DEV
+ if ( GetDebugRodeoPrint() )
+ printt( "Throw Rider off: origin after vertical adjustment: " + rider.GetOrigin() )
+ #endif
+
+ //printt( "Rider eye angles: " + rider.EyeAngles() + ", rider Angles: " + rider.GetAngles() + " titan eye Angles:" + titan.EyeAngles() )
+
+ // Set it higher in SP so bosses less exploitable
+ #if SP
+ direction += Vector( 0, 0, SP_RODEO_BOOST )
+ #endif
+
+ rider.SetVelocity( direction )
+ rider.JumpedOffRodeo()
+
+ int attachIndex = titan.LookupAttachment( "hijack" ) //TODO: Hardcoded, no way to get rodeopackage.attachpoint easily at this point anymore!
+ vector startPos = titan.GetAttachmentOrigin( attachIndex )
+
+ //printt( "startPos of attachment: " + startPos )
+
+ if ( !PlayerCanTeleportHere( rider, startPos, titan ) )
+ {
+ startPos = titan.GetOrigin()
+ if ( !PlayerCanTeleportHere( rider, startPos, titan ) )
+ startPos = rider.GetOrigin()
+ }
+
+ PutEntityInSafeSpot( rider, titan, null, startPos, rider.GetOrigin() )
+
+ #if DEV
+ if ( GetDebugRodeoPrint() )
+ printt( "Throw Rider off: origin after PutEntityInSafeSpot: " + rider.GetOrigin() )
+ #endif
+
+ if ( adjustAirControl )
+ thread PostRodeoAirControl( rider )
+}
+
+void function PostRodeoAirControl( entity player )
+{
+ player.Signal( "PostRodeoAirControl" )
+ player.EndSignal( "PostRodeoAirControl" )
+ player.EndSignal( "OnDeath" )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ RestorePlayerAirControl( player )
+ }
+ )
+
+ const float POST_RODEO_AIR_CONTROL_DURATION = 0.75
+ const float POST_RODEO_AIR_CONTROL_SCALE = 0.5
+ const float POST_RODEO_AIR_CONTROL_JUMP_DELAY = 0.45
+
+ // give the player time to be thrown in the proper direction before they get back double jump
+ RemovePlayerAirControl( player )
+ player.ConsumeDoubleJump()
+ wait POST_RODEO_AIR_CONTROL_JUMP_DELAY
+ player.TouchGround()
+
+ float startTime = Time()
+ while ( Time() - startTime < POST_RODEO_AIR_CONTROL_DURATION && !player.IsOnGround() && !player.IsWallRunning() && !player.IsWallHanging() )
+ {
+ float elapsedTime = Time() - startTime
+ player.kv.airSpeed = player.GetPlayerSettingsField( "airSpeed" ) * POST_RODEO_AIR_CONTROL_SCALE * (1 - (elapsedTime / POST_RODEO_AIR_CONTROL_DURATION))
+ player.kv.airAcceleration = player.GetPlayerSettingsField( "airAcceleration" ) * POST_RODEO_AIR_CONTROL_SCALE * (1 - (elapsedTime / POST_RODEO_AIR_CONTROL_DURATION))
+ //printt( "scale", POST_RODEO_AIR_CONTROL_SCALE * (1 - (elapsedTime / POST_RODEO_AIR_CONTROL_DURATION)) )
+
+ WaitFrame()
+ }
+}
+
+void function AnglesDebug( rider )
+{
+ printt( "Begin Angles Debug, Rider eye angles: " + rider.EyeAngles() + ", rider Angles: " + rider.GetAngles() )
+ while( !rider.IsOnGround() )
+ {
+ printt( "Rider eye angles: " + rider.EyeAngles() + ", rider Angles: " + rider.GetAngles() )
+ WaitFrame()
+ }
+
+ printt( "End Angles Debug, Rider eye angles: " + rider.EyeAngles() + ", rider Angles: " + rider.GetAngles() )
+}
+
+
+
+void function SetPlayerBatteryCount( entity player, int count )
+{
+ Assert( count <= file.maxPilotBatteryCount )
+ Assert( count >= 0 )
+ player.SetPlayerNetInt( "batteryCount", count )
+}
+
+int function GetPlayerBatteryCount( entity player )
+{
+ return player.GetPlayerNetInt( "batteryCount" )
+}
+
+void function DisableBTRodeo( entity soul )
+{
+ string settings = GetSoulPlayerSettings( soul )
+ var rodeoAllow = Dev_GetPlayerSettingByKeyField_Global( settings, "rodeo_allow" )
+
+ if ( rodeoAllow == null )
+ return
+
+ if ( rodeoAllow == 0 )
+ {
+ soul.SetIsValidRodeoTarget( false )
+ }
+}
+void function RemovePlayerAirControl( entity player ) //This function should really be in a server only SP & MP utility script file. No such file exists as of right now.
+{
+ Assert( player.IsPlayer() )
+ player.kv.airSpeed = 0
+ player.kv.airAcceleration = 0
+}
+
+void function RestorePlayerAirControl( entity player ) //This function should really be in a server only SP & MP utility script file. No such file exists as of right now.
+{
+ Assert( player.IsPlayer() )
+ player.kv.airSpeed = player.GetPlayerSettingsField( "airSpeed" )
+ player.kv.airAcceleration = player.GetPlayerSettingsField( "airAcceleration" )
+}
+
+bool function ShouldThrowGrenadeInHatch( entity rodeoPilot )
+{
+ bool batteryPullingDisabled = (GetCurrentPlaylistVarInt( "rodeo_battery_disable_pulls_from_titans", 0 ) == 1)
+ if ( batteryPullingDisabled )
+ return true
+
+ #if MP
+ if ( PlayerWantsToThrowNukeGrenade( rodeoPilot ) )
+ return false
+ #endif
+
+ if ( PlayerHasBattery( rodeoPilot ) )
+ return true
+
+ return false
+}
+
+
+#if DEV
+void function SetDebugRodeoPrint( bool value )
+{
+ file.debugRodeoPrint = value
+}
+
+bool function GetDebugRodeoPrint()
+{
+ return file.debugRodeoPrint
+}
+#endif
+
+#if MP
+void function SetApplyBatteryCallback( void functionref(entity,entity,entity) func )
+{
+ file.applyBatteryCallback = func
+}
+
+bool function PlayerWantsToThrowNukeGrenade( entity player )
+{
+ return ( player in file.playersThatWantToUseRodeoGrenade )
+}
+
+bool function HasSuperRodeoGrenade( entity player )
+{
+ // HACK: because we ran out of player global net ints for "numSuperRodeoGrenades" in bounty hunt
+ if ( GameRules_GetGameMode() != FD )
+ return false
+ return player.GetPlayerNetInt( "numSuperRodeoGrenades" ) > 0
+}
+
+void function DeductSuperRodeoGrenade( entity player, int amount )
+{
+ int num = player.GetPlayerNetInt( "numSuperRodeoGrenades" )
+ player.SetPlayerNetInt( "numSuperRodeoGrenades", num-amount )
+}
+
+void function RodeoForceNuke( entity pilot )
+{
+ entity titan = GetTitanBeingRodeoed( pilot )
+ if ( !IsValid( titan ) )
+ return
+
+ if ( !titan.IsNPC() || titan.GetTitanSoul().IsEjecting() )
+ return
+
+ table damageTable =
+ {
+ scriptType = damageTypes.rodeoBatteryRemoval
+ forceKill = false
+ damageSourceId = eDamageSourceId.core_overload
+ origin = titan.GetOrigin()
+ hitbox = 2
+ }
+ titan.TakeDamage( 1, pilot, pilot, damageTable )
+
+ if ( !IsAlive( titan ) || titan.GetTitanSoul().IsEjecting() )
+ return
+
+ DeductSuperRodeoGrenade( pilot, 1 )
+
+ // THROW RODEO RIDER OFF
+ entity soul = titan.GetTitanSoul()
+ soul.soul.nukeAttacker = pilot
+ NPC_SetNuclearPayload( titan )
+
+ vector ejectAngles = titan.GetAngles()
+ ejectAngles.x = 270
+ vector riderEjectAngles = AnglesCompose( ejectAngles, < 5, 0, 0 > )
+
+ float speed = RandomFloatRange( 1900, 2100 )
+ float gravityScale = expect float ( pilot.GetPlayerSettingsField( "gravityscale" ) )
+ vector riderVelocity = AnglesToForward( riderEjectAngles ) * (speed * gravityScale) * 0.95
+ ThrowRiderOff( pilot, titan, riderVelocity )
+
+ if ( titan.ContextAction_IsBusy() )
+ titan.ContextAction_ClearBusy()
+ thread TitanEjectPlayer( titan, true )
+}
+
+void function OpenRodeoNukeWindow( entity player, entity titan )
+{
+ player.EndSignal( "RodeoNukeWindowEnded" )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "RodeoOver" )
+ titan.EndSignal( "OnDeath" )
+
+ if ( player in file.playersThatWantToUseRodeoGrenade )
+ delete file.playersThatWantToUseRodeoGrenade[ player ]
+
+ if ( player.GetTeam() == titan.GetTeam() )
+ return
+
+ if ( !HasSuperRodeoGrenade( player ) )
+ return
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_NukeGrenadeWindowOpen" )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_NukeGrenadeWindowClosed" )
+ }
+ )
+
+ player.WaitSignal( "TryNukeGrenade" )
+
+ if ( !HasSuperRodeoGrenade( player ) )
+ return
+
+ file.playersThatWantToUseRodeoGrenade[ player ] <- true
+
+ MessageToPlayer( player, eEventNotifications.FD_SuperRodeoUsed )
+ Rodeo_MoveBatteryDown( titan.GetTitanSoul() )
+}
+
+bool function ClientCommand_TryNukeGrenade( entity player, array<string> args )
+{
+ if ( HasSuperRodeoGrenade( player ) )
+ player.Signal( "TryNukeGrenade" )
+
+ return true
+}
+#endif \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sh_calling_cards.gnut b/Northstar.CustomServers/mod/scripts/vscripts/sh_calling_cards.gnut
new file mode 100644
index 00000000..67461945
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/sh_calling_cards.gnut
@@ -0,0 +1,424 @@
+global function ShCallingCards_Init
+
+global function PlayerCallingCard_GetActive
+global function PlayerCallingCard_GetActiveIndex
+
+global function CallingCard_GetRef
+global function CallingCard_GetImage
+global function CallingCards_GetCount
+global function CallingCard_GetByIndex
+global function CallingCard_GetByRef
+
+global function PlayerCallsignIcon_GetActive
+global function PlayerCallsignIcon_GetActiveIndex
+
+global function CallsignIcon_GetRef
+global function CallsignIcon_GetImage
+global function CallingCard_GetLayout
+global function CallsignIcon_GetSmallImage
+global function CallsignIcons_GetCount
+global function CallsignIcon_GetByIndex
+global function CallsignIcon_GetByRef
+
+global function PlayerCallingCard_RefOverride
+
+#if SERVER
+ global function PlayerCallsignIcon_SetActive
+ global function PlayerCallingCard_SetActiveByRef
+ global function PlayerCallsignIcon_SetActiveByRef
+#endif
+
+global struct CallingCard
+{
+ int index = -1
+ string ref = ""
+ asset image = $""
+ int layoutType = 0
+}
+
+global struct CallsignIcon
+{
+ int index = -1
+ string ref = ""
+ asset image = $""
+ asset smallImage = $""
+ int layoutType = 0
+}
+
+struct
+{
+ table<string, CallingCard> callingCards
+ array<string> callingCardRefs
+
+ table<string, CallsignIcon> callsignIcons
+ array<string> callsignIconRefs
+
+ int nextCallingCardIndex = 0
+ int nextCallsignIconIndex = 0
+} file
+
+void function ShCallingCards_Init()
+{
+ bool initialized = ( file.callingCardRefs.len() > 0 )
+
+ if ( !initialized )
+ {
+ var dataTable = GetDataTable( $"datatable/calling_cards.rpak" )
+ for ( int row = 0; row < GetDatatableRowCount( dataTable ); row++ )
+ {
+ string cardRef = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, CALLING_CARD_REF_COLUMN_NAME ) )
+ asset image = GetDataTableAsset( dataTable, row, GetDataTableColumnByName( dataTable, CALLING_CARD_IMAGE_COLUMN_NAME ) )
+ int layoutType = GetDataTableInt( dataTable, row, GetDataTableColumnByName( dataTable, CALLING_CARD_LAYOUT_COLUMN_NAME ) )
+
+ CallingCard callingCard
+ callingCard.ref = cardRef
+ callingCard.image = image
+ callingCard.index = row
+ callingCard.layoutType = layoutType
+
+ file.callingCards[cardRef] <- callingCard
+ file.callingCardRefs.append( cardRef )
+ }
+ }
+
+ if ( !initialized )
+ {
+ var dataTable = GetDataTable( $"datatable/callsign_icons.rpak" )
+ for ( int row = 0; row < GetDatatableRowCount( dataTable ); row++ )
+ {
+ string iconRef = GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, CALLSIGN_ICON_REF_COLUMN_NAME ) )
+ asset image = GetDataTableAsset( dataTable, row, GetDataTableColumnByName( dataTable, CALLSIGN_ICON_IMAGE_COLUMN_NAME ) )
+ asset smallImage = GetDataTableAsset( dataTable, row, GetDataTableColumnByName( dataTable, CALLSIGN_ICON_SMALL_IMAGE_COLUMN_NAME ) )
+
+ CallsignIcon callsignIcon
+ callsignIcon.ref = iconRef
+ callsignIcon.image = image
+ callsignIcon.smallImage = smallImage
+ callsignIcon.index = row
+
+ file.callsignIcons[iconRef] <- callsignIcon
+ file.callsignIconRefs.append( iconRef )
+ }
+ }
+
+ #if SERVER
+ AddCallback_OnClientConnecting( OnClientConnecting )
+ AddCallback_OnTitanBecomesPilot( OnClassChangeBecomePilot )
+ AddCallback_OnPilotBecomesTitan( OnClassChangeBecomeTitan )
+ #endif
+}
+
+#if SERVER
+void function OnClientConnecting( entity player )
+{
+ // hack - don't do this because pdefs aren't fully working
+
+ // initialize the persistent network vars
+ // string ref = CallingCard_GetRef( PlayerCallingCard_GetActive( player ) )
+ // PlayerCallingCard_SetActiveByRef( player, ref )
+ //
+ // CallsignIcon callsignIcon = PlayerCallsignIcon_GetActive( player )
+ //
+ // PlayerCallsignIcon_SetActive( player, callsignIcon )
+ // player.SetTargetInfoIcon( callsignIcon.smallImage )
+}
+#endif
+
+#if DEV
+CallingCard function DEV_GetNextCallingCard()
+{
+ int index = file.nextCallingCardIndex
+ printt( "using CallingCard index", index )
+ file.nextCallingCardIndex++
+ file.nextCallingCardIndex = file.nextCallingCardIndex % file.callingCardRefs.len()
+
+ string ref = file.callingCardRefs[index]
+ return file.callingCards[ref]
+}
+
+CallsignIcon function DEV_GetNextCallsignIcon()
+{
+ int index = file.nextCallsignIconIndex
+ printt( "using CallsignIcon index", index )
+ file.nextCallsignIconIndex++
+ file.nextCallsignIconIndex = file.nextCallsignIconIndex % file.callsignIconRefs.len()
+
+ string ref = file.callsignIconRefs[index]
+ return file.callsignIcons[ref]
+}
+#endif
+
+int function PlayerCallingCard_GetActiveIndex( entity player )
+{
+ #if CLIENT
+ int index
+ if ( player != GetLocalClientPlayer() )
+ index = player.GetPlayerNetInt( "activeCallingCardIndex" )
+ else
+ index = player.GetPersistentVarAsInt( "activeCallingCardIndex" )
+ #else
+ int index = player.GetPersistentVarAsInt( "activeCallingCardIndex" )
+ #endif
+ return index
+}
+
+CallingCard function PlayerCallingCard_GetActive( entity player )
+{
+ int index = PlayerCallingCard_GetActiveIndex( player )
+ string ref = file.callingCardRefs[index]
+ #if CLIENT || UI
+ ref = PlayerCallingCard_RefOverride( player, ref )
+ #endif
+ return file.callingCards[ref]
+}
+
+string function CallingCard_GetRef( CallingCard callingCard )
+{
+ return callingCard.ref
+}
+
+asset function CallingCard_GetImage( CallingCard callingCard )
+{
+ return callingCard.image
+}
+
+int function CallingCard_GetLayout( CallingCard callingCard )
+{
+ return callingCard.layoutType
+}
+
+int function CallingCards_GetCount()
+{
+ return file.callingCards.len()
+}
+
+CallingCard function CallingCard_GetByIndex( int index )
+{
+ // JFS: handle players with invalid indices
+ //Assert( index < CallingCards_GetCount() )
+ if ( index >= file.callingCards.len() )
+ return file.callingCards["callsign_16_col"]
+
+ return file.callingCards[file.callingCardRefs[index]]
+}
+
+CallingCard function CallingCard_GetByRef( string ref )
+{
+ return file.callingCards[ref]
+}
+
+
+int function PlayerCallsignIcon_GetActiveIndex( entity player )
+{
+ #if CLIENT
+ int index
+ if ( player != GetLocalClientPlayer() )
+ index = player.GetPlayerNetInt( "activeCallsignIconIndex" )
+ else
+ index = player.GetPersistentVarAsInt( "activeCallsignIconIndex" )
+ #else
+ int index = player.GetPersistentVarAsInt( "activeCallsignIconIndex" )
+ #endif
+ return index
+}
+
+CallsignIcon function PlayerCallsignIcon_GetActive( entity player )
+{
+ int index = PlayerCallsignIcon_GetActiveIndex( player )
+ string ref = file.callsignIconRefs[index]
+ return file.callsignIcons[ref]
+}
+
+string function CallsignIcon_GetRef( CallsignIcon callsignIcon )
+{
+ return callsignIcon.ref
+}
+
+asset function CallsignIcon_GetImage( CallsignIcon callsignIcon )
+{
+ return callsignIcon.image
+}
+
+asset function CallsignIcon_GetSmallImage( CallsignIcon callsignIcon )
+{
+ return callsignIcon.smallImage
+}
+
+int function CallsignIcons_GetCount()
+{
+ return file.callsignIcons.len()
+}
+
+CallsignIcon function CallsignIcon_GetByIndex( int index )
+{
+ // JFS: handle players with invalid indices
+ // Assert( index < CallsignIcons_GetCount() )
+
+ if ( index >= file.callsignIconRefs.len() )
+ index = 0
+
+ return file.callsignIcons[file.callsignIconRefs[index]]
+}
+
+CallsignIcon function CallsignIcon_GetByRef( string ref )
+{
+ return file.callsignIcons[ref]
+}
+
+
+const table< string, string > dynamicCardRefMap = {
+ callsign_fd_ion_dynamic = "ion",
+ callsign_fd_tone_dynamic = "tone",
+ callsign_fd_scorch_dynamic = "scorch",
+ callsign_fd_legion_dynamic = "legion",
+ callsign_fd_northstar_dynamic = "northstar",
+ callsign_fd_ronin_dynamic = "ronin",
+ callsign_fd_monarch_dynamic = "vanguard",
+}
+
+const table< string, array<string> > dynamicCardMap = {
+ callsign_fd_ion_dynamic =
+ [
+ "callsign_fd_ion_dynamic",
+ "callsign_fd_ion_dynamic",
+ "callsign_fd_ion_hard",
+ "callsign_fd_ion_master",
+ "callsign_fd_ion_insane",
+ ],
+
+ callsign_fd_tone_dynamic =
+ [
+ "callsign_fd_tone_dynamic",
+ "callsign_fd_tone_dynamic",
+ "callsign_fd_tone_hard",
+ "callsign_fd_tone_master",
+ "callsign_fd_tone_insane",
+ ],
+
+ callsign_fd_scorch_dynamic =
+ [
+ "callsign_fd_scorch_dynamic",
+ "callsign_fd_scorch_dynamic",
+ "callsign_fd_scorch_hard",
+ "callsign_fd_scorch_master",
+ "callsign_fd_scorch_insane",
+ ],
+
+ callsign_fd_legion_dynamic =
+ [
+ "callsign_fd_legion_dynamic",
+ "callsign_fd_legion_dynamic",
+ "callsign_fd_legion_hard",
+ "callsign_fd_legion_master",
+ "callsign_fd_legion_insane",
+ ],
+
+ callsign_fd_northstar_dynamic =
+ [
+ "callsign_fd_northstar_dynamic",
+ "callsign_fd_northstar_dynamic",
+ "callsign_fd_northstar_hard",
+ "callsign_fd_northstar_master",
+ "callsign_fd_northstar_insane",
+ ],
+
+ callsign_fd_ronin_dynamic =
+ [
+ "callsign_fd_ronin_dynamic",
+ "callsign_fd_ronin_dynamic",
+ "callsign_fd_ronin_hard",
+ "callsign_fd_ronin_master",
+ "callsign_fd_ronin_insane",
+ ],
+
+ callsign_fd_monarch_dynamic =
+ [
+ "callsign_fd_monarch_dynamic",
+ "callsign_fd_monarch_dynamic",
+ "callsign_fd_monarch_hard",
+ "callsign_fd_monarch_master",
+ "callsign_fd_monarch_insane",
+ ],
+}
+
+string function PlayerCallingCard_RefOverride( entity player, string ref )
+{
+ const string CARD_DYNAMIC = "_dynamic"
+
+ if ( ref.find( CARD_DYNAMIC ) == null )
+ return ref
+
+ if ( ref.find( CARD_DYNAMIC ) != ref.len() - CARD_DYNAMIC.len() )
+ return ref
+
+ if ( ref in dynamicCardRefMap )
+ {
+ string titanRef = dynamicCardRefMap[ref]
+ int highestDifficulty = FD_GetHighestDifficultyForTitan( player, titanRef )
+
+ return dynamicCardMap[ref][minint( highestDifficulty, dynamicCardMap[ref].len() - 1 )]
+ }
+
+ return ref
+}
+
+#if SERVER
+/*
+InitUnlockAsEntitlement( "callsign_fd_ion_dynamic", "", ET_DLC7_ION_WARPAINT )
+InitUnlockAsEntitlement( "callsign_fd_tone_dynamic", "", ET_DLC7_TONE_WARPAINT )
+InitUnlockAsEntitlement( "callsign_fd_scorch_dynamic", "", ET_DLC7_SCORCH_WARPAINT )
+InitUnlockAsEntitlement( "callsign_fd_legion_dynamic", "", ET_DLC7_LEGION_WARPAINT )
+InitUnlockAsEntitlement( "callsign_fd_northstar_dynamic", "", ET_DLC7_NORTHSTAR_WARPAINT )
+InitUnlockAsEntitlement( "callsign_fd_ronin_dynamic", "", ET_DLC7_RONIN_WARPAINT )
+InitUnlockAsEntitlement( "callsign_fd_monarch_dynamic", "", ET_DLC7_MONARCH_WARPAINT )
+*/
+
+void function PlayerCallingCard_SetActiveByIndex( entity player, int index )
+{
+// if ( player.GetPersistentVarAsInt( "activeCallingCardIndex" ) != index )
+ player.SetCallingCard( index )
+
+ player.SetPlayerNetInt( "activeCallingCardIndex", index )
+ player.SetPersistentVar( "activeCallingCardIndex", index )
+}
+
+void function PlayerCallingCard_SetActiveByRef( entity player, string ref )
+{
+ PlayerCallingCard_SetActiveByIndex( player, file.callingCards[ref].index )
+
+ if ( PlayerCallingCard_RefOverride( player, ref ) != ref )
+ player.SetCallingCard( file.callingCards[PlayerCallingCard_RefOverride( player, ref )].index )
+}
+
+void function PlayerCallsignIcon_SetActiveByIndex( entity player, int index )
+{
+// if ( player.GetPersistentVarAsInt( "activeCallsignIconIndex" ) != index )
+ player.SetCallSign( index )
+
+ player.SetPlayerNetInt( "activeCallsignIconIndex", index )
+ player.SetPersistentVar( "activeCallsignIconIndex", index )
+}
+
+void function PlayerCallsignIcon_SetActive( entity player, CallsignIcon callsignIcon )
+{
+ PlayerCallsignIcon_SetActiveByIndex( player, callsignIcon.index )
+}
+
+void function PlayerCallsignIcon_SetActiveByRef( entity player, string ref )
+{
+ PlayerCallsignIcon_SetActiveByIndex( player, file.callsignIcons[ref].index )
+}
+
+void function OnClassChangeBecomePilot( entity player, entity titan )
+{
+ CallsignIcon callsignIcon = PlayerCallsignIcon_GetActive( player )
+ player.SetTargetInfoIcon( callsignIcon.smallImage )
+}
+
+void function OnClassChangeBecomeTitan( entity player, entity titan )
+{
+ string titanRef = GetTitanCharacterNameFromSetFile( player.GetPlayerSettings() )
+ player.SetTargetInfoIcon( GetTitanCoreIcon( titanRef ) )
+}
+
+#endif \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sh_loadouts_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/sh_loadouts_mp.nut
new file mode 100644
index 00000000..3b1c8a8a
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/sh_loadouts_mp.nut
@@ -0,0 +1,19 @@
+// despite the name this is NOT a shared script, it only runs on the server
+// todo: move all contents of this script to one called _loadouts_mp.nut, naming here is confusing af
+
+// current contents of this script are just random placeholder funcs i wasn't sure about the proper location of
+
+global function GetNPCDefaultWeaponForLevel
+global function GetTitanLoadoutForCurrentMap
+
+TitanLoadoutDef function GetTitanLoadoutForCurrentMap()
+{
+ TitanLoadoutDef loadout
+ return loadout
+}
+
+NPCDefaultWeapon ornull function GetNPCDefaultWeaponForLevel( entity npc )
+{
+ return null
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sh_northstar_utils.gnut b/Northstar.CustomServers/mod/scripts/vscripts/sh_northstar_utils.gnut
new file mode 100644
index 00000000..20d742d0
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/sh_northstar_utils.gnut
@@ -0,0 +1,42 @@
+globalize_all_functions
+
+enum eNorthstarLobbyType
+{
+ PrivateMatchLobby, // normal vanilla private lobby
+ IntermissionLobby, // similar to tf1's intermission lobby, chooses next map automatically
+ CompetitiveLobby // similar to vanilla privates, but with ready up system
+}
+
+// whether the server is a modded, northstar server
+bool function IsNorthstarServer()
+{
+ bool isModded = true // TEMP for testing
+ try
+ {
+ // need this in a trycatch because the var might not exist atm
+ isModded = GetConVarInt( "northstar_is_modded_server" ) == 1
+ } catch ( ex ) {}
+
+ return isModded
+}
+
+// whether the game should return to the lobby on GameRules_EndMatch()
+bool function ShouldReturnToLobby()
+{
+ return GetConVarBool( "ns_should_return_to_lobby" )
+}
+
+int function GetNorthstarLobbyType()
+{
+ if ( !IsNorthstarServer() )
+ return eNorthstarLobbyType.PrivateMatchLobby
+
+ int lobbyType = eNorthstarLobbyType.PrivateMatchLobby
+ try
+ {
+ // need this in a trycatch because the var might not exist atm
+ lobbyType = GetConVarInt( "northstar_lobby_type" )
+ } catch ( ex ) {}
+
+ return lobbyType
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sh_remote_functions_mp_custom.gnut b/Northstar.CustomServers/mod/scripts/vscripts/sh_remote_functions_mp_custom.gnut
new file mode 100644
index 00000000..c1e49e76
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/sh_remote_functions_mp_custom.gnut
@@ -0,0 +1,20 @@
+untyped
+global function InitCustomNetworkVars
+global function AddCallback_OnRegisteringCustomNetworkVars
+
+struct {
+ array<void functionref()> onRegisteringCustomNetworkVarsCallbacks
+} file
+
+void function InitCustomNetworkVars()
+{
+ print( "InitCustomNetworkVars" )
+
+ foreach ( void functionref() callback in file.onRegisteringCustomNetworkVarsCallbacks )
+ callback()
+}
+
+void function AddCallback_OnRegisteringCustomNetworkVars( void functionref() callback )
+{
+ file.onRegisteringCustomNetworkVarsCallbacks.append( callback )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sh_stats.gnut b/Northstar.CustomServers/mod/scripts/vscripts/sh_stats.gnut
new file mode 100644
index 00000000..31634a9b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/sh_stats.gnut
@@ -0,0 +1,526 @@
+
+global function InitStatsTables
+global function IsValidStat
+global function GetPlayerStatInt
+global function GetPlayerStatFloat
+global function GetPlayerStat_AllCompetitiveModesAndMapsInt
+global function GetStatVar
+global function GetStatVarType
+global function GetStatVarLocalizedUnlock
+global function Stats_GetFixedSaveVar
+global function FD_GetHighestDifficultyForTitan
+
+/*void function AddItemsToStatsList( array<string> refs )
+{
+ foreach ( ref in refs )
+ shGlobalMP.statsItemsList.append( ref )
+}*/
+
+void function InitStatsTables()
+{
+ int persistenceItemsCount = PersistenceGetEnumCount( "loadoutWeaponsAndAbilities" )
+ for ( int i = 0; i < persistenceItemsCount; i++ )
+ {
+ string enumName = PersistenceGetEnumItemNameForIndex( "loadoutWeaponsAndAbilities", i )
+ if ( enumName != "" )
+ shGlobalMP.statsItemsList.append( enumName )
+ }
+
+ //##############################################
+ // GAMES STATS
+ //##############################################
+
+ AddPersistentStatCategory( "game_stats" )
+
+ AddPersistentStat( "game_stats", "game_joined", "", "mapStats[%mapname%].gamesJoined[%gamemode%]", "#" )
+ AddPersistentStat( "game_stats", "game_completed", "", "mapStats[%mapname%].gamesCompleted[%gamemode%]", "#" )
+ AddPersistentStat( "game_stats", "game_won", "", "mapStats[%mapname%].gamesWon[%gamemode%]", "#" )
+ AddPersistentStat( "game_stats", "game_lost", "", "mapStats[%mapname%].gamesLost[%gamemode%]", "#" )
+ AddPersistentStat( "game_stats", "mvp", "", "mapStats[%mapname%].topPlayerOnTeam[%gamemode%]", "#" )
+ AddPersistentStat( "game_stats", "top3OnTeam", "", "mapStats[%mapname%].top3OnTeam[%gamemode%]", "#" )
+ AddPersistentStat( "game_stats", "hoursPlayed", "", "mapStats[%mapname%].hoursPlayed[%gamemode%]", "#" )
+ AddPersistentStat( "game_stats", "perfectMatches", "", "mapStats[%mapname%].perfectMatchesByDifficulty[%difficulty%]", "#" )
+ AddPersistentStat( "game_stats", "games_completed_fd", "", "mapStats[%mapname%].matchesByDifficulty[%difficulty%]", "#" )
+ AddPersistentStat( "game_stats", "games_won_fd", "", "mapStats[%mapname%].winsByDifficulty[%difficulty%]", "#" )
+
+ int gameModeCount = PersistenceGetEnumCount( "gameModes" )
+ for ( int modeIndex = 0; modeIndex < gameModeCount; modeIndex++ )
+ {
+ string gameModeName = PersistenceGetEnumItemNameForIndex( "gameModes", modeIndex )
+
+ AddPersistentStat( "game_stats", "mode_played", gameModeName, "gameStats.modesPlayed[" + gameModeName + "]", "#UNLOCK_MODE_PLAYED" )
+ AddPersistentStat( "game_stats", "mode_won", gameModeName, "gameStats.modesWon[" + gameModeName + "]", "#UNLOCK_MODE_WON" )
+
+ AddPersistentStat( "game_stats", "pvp_kills_by_mode", gameModeName, "gameStats.pvpKills[" + gameModeName + "]", "#UNLOCK_MODE_PILOT_KILLS" )
+ AddPersistentStat( "game_stats", "times_kd_2_to_1_by_mode", gameModeName, "gameStats.timesKillDeathRatio2to1[" + gameModeName + "]", "#UNLOCK_MODE_KD_2_1" )
+ AddPersistentStat( "game_stats", "times_kd_2_to_1_pvp_by_mode", gameModeName, "gameStats.timesKillDeathRatio2to1_pvp[" + gameModeName + "]", "#UNLOCK_MODE_PILOT_KD_2_1" )
+ }
+
+ AddPersistentStat( "game_stats", "mvp_total", "", "gameStats.mvp_total", "#UNLOCK_GAME_MVP" )
+ AddPersistentStat( "game_stats", "game_completed_total", "", "gameStats.gamesCompletedTotal", "#UNLOCK_GAME_COMPLETED" )
+ AddPersistentStat( "game_stats", "game_won_total", "", "gameStats.gamesWonTotal", "#UNLOCK_GAME_WON" )
+
+ //##############################################
+ // TIME STATS
+ //##############################################
+
+ AddPersistentStatCategory( "time_stats" )
+
+ AddPersistentStatFloat( "time_stats", "hours_total", "", "timeStats.total", "#UNLOCK_TIME_HOURS" )
+ AddPersistentStatFloat( "time_stats", "hours_as_pilot", "", "timeStats.asPilot", "#UNLOCK_TIME_HOURS_PILOT" )
+ AddPersistentStatFloat( "time_stats", "hours_wallrunning", "", "timeStats.wallrunning", "#UNLOCK_TIME_HOURS_WALLRUN" )
+ AddPersistentStatFloat( "time_stats", "hours_inAir", "", "timeStats.inAir", "#UNLOCK_TIME_HOURS_AIR" )
+ AddPersistentStatFloat( "time_stats", "hours_as_titan", "", "timeStats.asTitanTotal", "#UNLOCK_TIME_HOURS_TITAN" )
+
+ AddPersistentStatFloat( "time_stats", "hours_dead", "", "timeStats.dead", "#" )
+ AddPersistentStatFloat( "time_stats", "hours_wallhanging", "", "timeStats.wallhanging", "#" )
+
+ // hours_as_titan_stryder
+ // hours_as_titan_atlas
+ // hours_as_titan_ogre
+ foreach ( titan, alias in GetAsTitanTypes() )
+ {
+ AddPersistentStatFloat( "time_stats", "hours_as_titan_" + alias, "", "timeStats.asTitan[" + alias + "]", "#UNLOCK_TIME_HOURS_TITAN_SPECIFIC" )
+ }
+
+ //##############################################
+ // DISTANCE STATS
+ //##############################################
+
+ AddPersistentStatCategory( "distance_stats" )
+
+ AddPersistentStatFloat( "distance_stats", "total", "", "distanceStats.total", "#UNLOCK_DISTANCE_KM" )
+ AddPersistentStatFloat( "distance_stats", "asPilot", "", "distanceStats.asPilot", "#UNLOCK_DISTANCE_KM_PILOT" )
+ AddPersistentStatFloat( "distance_stats", "wallrunning", "", "distanceStats.wallrunning", "#UNLOCK_DISTANCE_KM_WALLRUN" )
+ AddPersistentStatFloat( "distance_stats", "inAir", "", "distanceStats.inAir", "#UNLOCK_DISTANCE_KM_AIR" )
+ AddPersistentStatFloat( "distance_stats", "asTitan", "", "distanceStats.asTitanTotal", "#UNLOCK_TIME_HOURS_TITAN" )
+
+ AddPersistentStatFloat( "distance_stats", "ziplining", "", "distanceStats.ziplining", "#" )
+ AddPersistentStatFloat( "distance_stats", "onFriendlyTitan", "", "distanceStats.onFriendlyTitan", "#" )
+ AddPersistentStatFloat( "distance_stats", "onEnemyTitan", "", "distanceStats.onEnemyTitan", "#" )
+
+ foreach ( titan, alias in GetAsTitanTypes() )
+ {
+ AddPersistentStatFloat( "distance_stats", titan, "", "distanceStats.asTitan[" + alias + "]", "#UNLOCK_DISTANCE_KM_TITAN_SPECIFIC" )
+ }
+
+ //##############################################
+ // WEAPON STATS
+ //##############################################
+
+ AddPersistentStatCategory( "weapon_stats" )
+
+ foreach ( string ref in shGlobalMP.statsItemsList )
+ {
+ AddPersistentStat( "weapon_stats", "shotsHit", ref, "weaponStats[" + ref + "].shotsHit", "#UNLOCK_WEAPON_SHOTS_HIT" )
+ AddPersistentStat( "weapon_stats", "headshots", ref, "weaponStats[" + ref + "].headshots", "#UNLOCK_WEAPON_HEADSHOTS" )
+ AddPersistentStat( "weapon_stats", "critHits", ref, "weaponStats[" + ref + "].critHits", "#UNLOCK_WEAPON_SHOTS_CRIT" )
+ AddPersistentStatFloat( "weapon_stats", "hoursUsed", ref, "weaponStats[" + ref + "].hoursUsed", "#UNLOCK_WEAPON_HOURS_USED" )
+ AddPersistentStatFloat( "weapon_stats", "hoursEquipped", ref, "weaponStats[" + ref + "].hoursEquipped", "#UNLOCK_WEAPON_HOURS_EQUIPPED" )
+
+ AddPersistentStat( "weapon_stats", "shotsFired", ref, "weaponStats[" + ref + "].shotsFired", "#" )
+ AddPersistentStat( "weapon_stats", "titanDamage", ref, "weaponStats[" + ref + "].titanDamage", "#UNLOCK_WEAPON_TITAN_DAMAGE" )
+ }
+
+ //##############################################
+ // KILLS STATS FOR WEAPON
+ //##############################################
+
+ AddPersistentStatCategory( "weapon_kill_stats" )
+
+ foreach ( string ref in shGlobalMP.statsItemsList )
+ {
+ AddPersistentStat( "weapon_kill_stats", "total", ref, "weaponKillStats[" + ref + "].total", "#UNLOCK_WEAPON_KILLS" )
+ AddPersistentStat( "weapon_kill_stats", "pilots", ref, "weaponKillStats[" + ref + "].pilots", "#UNLOCK_WEAPON_PILOT_KILLS" )
+ AddPersistentStat( "weapon_kill_stats", "ejecting_pilots", ref, "weaponKillStats[" + ref + "].ejecting_pilots", "#UNLOCK_WEAPON_GOOSER_KILLS" )
+ AddPersistentStat( "weapon_kill_stats", "titansTotal", ref, "weaponKillStats[" + ref + "].titansTotal", "#UNLOCK_WEAPON_TITAN_KILLS" )
+ AddPersistentStat( "weapon_kill_stats", "assistsTotal", ref, "weaponKillStats[" + ref + "].assistsTotal", "#UNLOCK_WEAPON_ASSISTS" )
+ AddPersistentStat( "weapon_kill_stats", "killingSprees", ref, "weaponKillStats[" + ref + "].killingSprees", "#UNLOCK_WEAPON_KILLING_SPREES" )
+
+ AddPersistentStat( "weapon_kill_stats", "spectres", ref, "weaponKillStats[" + ref + "].spectres", "#" )
+ AddPersistentStat( "weapon_kill_stats", "marvins", ref, "weaponKillStats[" + ref + "].marvins", "#" )
+ AddPersistentStat( "weapon_kill_stats", "grunts", ref, "weaponKillStats[" + ref + "].grunts", "#" )
+ AddPersistentStat( "weapon_kill_stats", "ai", ref, "weaponKillStats[" + ref + "].ai", "#" )
+
+ foreach ( titan, alias in GetPluralTitanTypes() )
+ {
+ AddPersistentStat( "weapon_kill_stats", titan, ref, "weaponKillStats[" + ref + "].titans[" + alias + "]", "#" )
+ }
+
+ // fix this so it doesn't need explicit list of titans
+ AddPersistentStat( "weapon_kill_stats", "npcTitans_ion", ref, "weaponKillStats[" + ref + "].npcTitans[ion]", "#" )
+ AddPersistentStat( "weapon_kill_stats", "npcTitans_scorch", ref, "weaponKillStats[" + ref + "].npcTitans[scorch]", "#" )
+ AddPersistentStat( "weapon_kill_stats", "npcTitans_northstar", ref, "weaponKillStats[" + ref + "].npcTitans[northstar]", "#" )
+ AddPersistentStat( "weapon_kill_stats", "npcTitans_ronin", ref, "weaponKillStats[" + ref + "].npcTitans[ronin]", "#" )
+ AddPersistentStat( "weapon_kill_stats", "npcTitans_tone", ref, "weaponKillStats[" + ref + "].npcTitans[tone]", "#" )
+ AddPersistentStat( "weapon_kill_stats", "npcTitans_legion", ref, "weaponKillStats[" + ref + "].npcTitans[legion]", "#" )
+ AddPersistentStat( "weapon_kill_stats", "npcTitans_vanguard", ref, "weaponKillStats[" + ref + "].npcTitans[vanguard]", "#" )
+ }
+
+ //##############################################
+ // GENERAL KILLS STATS
+ //##############################################
+
+ AddPersistentStatCategory( "kills_stats" )
+
+ AddPersistentStat( "kills_stats", "total", "", "killStats.total", "#UNLOCK_KILLS_TOTAL" )
+ AddPersistentStat( "kills_stats", "totalWhileUsingBurnCard", "", "killStats.totalWhileUsingBurnCard", "#" )
+ AddPersistentStat( "kills_stats", "titansWhileTitanBCActive", "", "killStats.titansWhileTitanBCActive", "#" )
+ AddPersistentStat( "kills_stats", "totalPVP", "", "killStats.totalPVP", "#" )
+ AddPersistentStat( "kills_stats", "pilots", "", "killStats.pilots", "#UNLOCK_KILLS_PILOT" )
+ AddPersistentStat( "kills_stats", "spectres", "", "killStats.spectres", "#" )
+ AddPersistentStat( "kills_stats", "marvins", "", "killStats.marvins", "#" )
+ AddPersistentStat( "kills_stats", "grunts", "", "killStats.grunts", "#" )
+ AddPersistentStat( "kills_stats", "totalTitans", "", "killStats.totalTitans", "#UNLOCK_KILLS_TITAN" )
+ AddPersistentStat( "kills_stats", "totalPilots", "", "killStats.totalPilots", "#" )
+ AddPersistentStat( "kills_stats", "totalNPC", "", "killStats.totalNPC", "#" )
+ AddPersistentStat( "kills_stats", "totalTitansWhileDoomed", "", "killStats.totalTitansWhileDoomed", "#UNLOCK_KILLS_TITAN_WHILE_DOOMED" )
+ AddPersistentStat( "kills_stats", "asPilot", "", "killStats.asPilot", "#" )
+ AddPersistentStat( "kills_stats", "totalAssists", "", "killStats.totalAssists", "#UNLOCK_KILLS_ASSISTS" )
+
+ foreach ( titan, alias in GetAsTitanTypes() )
+ {
+ AddPersistentStat( "kills_stats", titan, "", "killStats.asTitan[" + alias + "]", "#UNLOCK_KILLS_ASTITAN" )
+ }
+
+ AddPersistentStat( "kills_stats", "killingSpressAs_ion", "", "killStats.killingSprees[ion]", "#UNLOCK_KILLS_SPREES_ASTITAN" )
+ AddPersistentStat( "kills_stats", "killingSpressAs_scorch", "", "killStats.killingSprees[scorch]", "#UNLOCK_KILLS_SPREES_ASTITAN" )
+ AddPersistentStat( "kills_stats", "killingSpressAs_northstar", "", "killStats.killingSprees[northstar]", "#UNLOCK_KILLS_SPREES_ASTITAN" )
+ AddPersistentStat( "kills_stats", "killingSpressAs_ronin", "", "killStats.killingSprees[ronin]", "#UNLOCK_KILLS_SPREES_ASTITAN" )
+ AddPersistentStat( "kills_stats", "killingSpressAs_tone", "", "killStats.killingSprees[tone]", "#UNLOCK_KILLS_SPREES_ASTITAN" )
+ AddPersistentStat( "kills_stats", "killingSpressAs_legion", "", "killStats.killingSprees[legion]", "#UNLOCK_KILLS_SPREES_ASTITAN" )
+ AddPersistentStat( "kills_stats", "killingSpressAs_vanguard", "", "killStats.killingSprees[vanguard]", "#UNLOCK_KILLS_SPREES_ASTITAN" )
+
+ AddPersistentStat( "kills_stats", "firstStrikes", "", "killStats.firstStrikes", "#UNLOCK_KILLS_FIRST_STRIKE" )
+ AddPersistentStat( "kills_stats", "ejectingPilots", "", "killStats.ejectingPilots", "#UNLOCK_KILLS_GOOSER" )
+ AddPersistentStat( "kills_stats", "whileEjecting", "", "killStats.whileEjecting", "#" )
+ AddPersistentStat( "kills_stats", "cloakedPilots", "", "killStats.cloakedPilots", "#" )
+ AddPersistentStat( "kills_stats", "whileCloaked", "", "killStats.whileCloaked", "#" )
+ AddPersistentStat( "kills_stats", "wallrunningPilots", "", "killStats.wallrunningPilots", "#" )
+ AddPersistentStat( "kills_stats", "whileWallrunning", "", "killStats.whileWallrunning", "#" )
+ AddPersistentStat( "kills_stats", "wallhangingPilots", "", "killStats.wallhangingPilots", "#" )
+ AddPersistentStat( "kills_stats", "whileWallhanging", "", "killStats.whileWallhanging", "#" )
+
+ AddPersistentStat( "kills_stats", "pilotExecution", "", "killStats.pilotExecution", "#" )
+ AddPersistentStat( "kills_stats", "pilotExecutePilot", "", "killStats.pilotExecutePilot", "#UNLOCK_KILLS_PILOT_EXECUTION" )
+ AddPersistentStat( "kills_stats", "pilotExecutePilotWhileCloaked", "", "killStats.pilotExecutePilotWhileCloaked", "#UNLOCK_KILLS_PILOT_EXECUTION_WHILE_CLOAKED" )
+ AddPersistentStat( "kills_stats", "pilotKillsWithHoloPilotActive", "", "killStats.pilotKillsWithHoloPilotActive", "#UNLOCK_KILLS_PILOT_KILLS_WHILE_HOLOPILOT_ACTIVE" )
+ AddPersistentStat( "kills_stats", "pilotKillsWithAmpedWallActive", "", "killStats.pilotKillsWithAmpedWallActive", "#UNLOCK_KILLS_PILOT_KILLS_WHILE_AMPEDWALL_ACTIVE" )
+
+ int pilotExecutionCount = PersistenceGetEnumCount( "pilotExecution" )
+ for ( int i = 0; i < pilotExecutionCount; i++ )
+ {
+ string executionRef = PersistenceGetEnumItemNameForIndex( "pilotExecution", i )
+ if ( executionRef != "" )
+ AddPersistentStat( "kills_stats", "pilotExecutePilotUsing_" + executionRef, "", "killStats.pilotExecutePilotByType[" + executionRef + "]", "#UNLOCK_KILLS_PILOT_EXECUTION_USING_TELEFRAG" ) // will need to modify string if other unlock refs are used
+ }
+
+ AddPersistentStat( "kills_stats", "pilotKickMelee", "", "killStats.pilotKickMelee", "#" )
+ AddPersistentStat( "kills_stats", "pilotKickMeleePilot", "", "killStats.pilotKickMeleePilot", "#" )
+ AddPersistentStat( "kills_stats", "titanMelee", "", "killStats.titanMelee", "#" )
+ AddPersistentStat( "kills_stats", "titanMeleePilot", "", "killStats.titanMeleePilot", "#" )
+ AddPersistentStat( "kills_stats", "titanStepCrush", "", "killStats.titanStepCrush", "#" )
+ AddPersistentStat( "kills_stats", "titanStepCrushPilot", "", "killStats.titanStepCrushPilot", "#" )
+
+ foreach ( titan, alias in GetCapitalizedTitanTypes() )
+ {
+ AddPersistentStat( "kills_stats", "titanExocution" + titan, "", "killStats.titanExocution" + titan, "#UNLOCK_KILLS_TITAN_EXECUTION" )
+ }
+
+ AddPersistentStat( "kills_stats", "titanFallKill", "", "killStats.titanFallKill", "#UNLOCK_KILLS_TITANFALL" )
+ AddPersistentStat( "kills_stats", "petTitanKillsFollowMode", "", "killStats.petTitanKillsFollowMode", "#" )
+ AddPersistentStat( "kills_stats", "petTitanKillsGuardMode", "", "killStats.petTitanKillsGuardMode", "#" )
+ AddPersistentStat( "kills_stats", "rodeo_total", "", "killStats.rodeo_total", "#UNLOCK_KILLS_RODEO" )
+ AddPersistentStat( "kills_stats", "pilot_headshots_total", "", "killStats.pilot_headshots_total", "#UNLOCK_KILLS_HEADSHOT" )
+ AddPersistentStat( "kills_stats", "evacShips", "", "killStats.evacShips", "#" )
+ AddPersistentStat( "kills_stats", "flyers", "", "killStats.flyers", "#" )
+ AddPersistentStat( "kills_stats", "nuclearCore", "", "killStats.nuclearCore", "#" )
+ AddPersistentStat( "kills_stats", "evacuatingEnemies", "", "killStats.evacuatingEnemies", "#" )
+ AddPersistentStat( "kills_stats", "coopChallenge_NukeTitan_Kills", "", "killStats.coopChallenge_NukeTitan_Kills", "#" )
+ AddPersistentStat( "kills_stats", "coopChallenge_MortarTitan_Kills", "", "killStats.coopChallenge_MortarTitan_Kills", "#" )
+ AddPersistentStat( "kills_stats", "coopChallenge_EmpTitan_Kills", "", "killStats.coopChallenge_EmpTitan_Kills", "#" )
+ AddPersistentStat( "kills_stats", "coopChallenge_SuicideSpectre_Kills", "", "killStats.coopChallenge_SuicideSpectre_Kills", "#" )
+ AddPersistentStat( "kills_stats", "coopChallenge_Turret_Kills", "", "killStats.coopChallenge_Turret_Kills", "#" )
+ AddPersistentStat( "kills_stats", "coopChallenge_CloakDrone_Kills", "", "killStats.coopChallenge_CloakDrone_Kills", "#" )
+ AddPersistentStat( "kills_stats", "coopChallenge_BubbleShieldGrunt_Kills", "", "killStats.coopChallenge_BubbleShieldGrunt_Kills", "#" )
+ AddPersistentStat( "kills_stats", "coopChallenge_Dropship_Kills", "", "killStats.coopChallenge_Dropship_Kills", "#" )
+ AddPersistentStat( "kills_stats", "coopChallenge_Sniper_Kills", "", "killStats.coopChallenge_Sniper_Kills", "#" )
+ AddPersistentStat( "kills_stats", "ampedVortexKills", "", "killStats.ampedVortexKills", "#" )
+ AddPersistentStat( "kills_stats", "meleeWhileCloaked", "", "killStats.meleeWhileCloaked", "#" )
+ AddPersistentStat( "kills_stats", "pilotKillsWhileUsingActiveRadarPulse", "", "killStats.pilotKillsWhileUsingActiveRadarPulse", "#" )
+ AddPersistentStat( "kills_stats", "titanKillsAsPilot", "", "killStats.titanKillsAsPilot", "#UNLOCK_KILLS_PVT" )
+ AddPersistentStat( "kills_stats", "pilotKillsWhileStimActive", "", "killStats.pilotKillsWhileStimActive", "#" )
+ AddPersistentStat( "kills_stats", "pilotKillsAsTitan", "", "killStats.pilotKillsAsTitan", "#UNLOCK_KILLS_TVP" )
+ AddPersistentStat( "kills_stats", "pilotKillsAsPilot", "", "killStats.pilotKillsAsPilot", "#" )
+ AddPersistentStat( "kills_stats", "titanKillsAsTitan", "", "killStats.titanKillsAsTitan", "#" )
+
+ #if SERVER
+ AddPersistentStat( "kills_stats", "pilotExecutePilotUsing_execution_telefrag", "", "killStats.pilotExecutePilotUsing_execution_telefrag", "#" )
+ #endif
+
+ //##############################################
+ // GENERAL DEATHS STATS
+ //##############################################
+
+ AddPersistentStatCategory( "deaths_stats" )
+
+ AddPersistentStat( "deaths_stats", "total", "", "deathStats.total" )
+ AddPersistentStat( "deaths_stats", "totalPVP", "", "deathStats.totalPVP" )
+ AddPersistentStat( "deaths_stats", "asPilot", "", "deathStats.asPilot" )
+
+ foreach ( titan, alias in GetAsTitanTypes() )
+ {
+ AddPersistentStat( "deaths_stats", titan, "", "deathStats.asTitan[" + alias + "]" )
+ }
+
+ AddPersistentStat( "deaths_stats", "byPilots", "", "deathStats.byPilots" )
+
+ foreach ( titan, alias in GetByTitanTypes() )
+ {
+ AddPersistentStat( "deaths_stats", titan, "", "deathStats.byTitans[" + alias + "]" )
+ }
+
+ AddPersistentStat( "deaths_stats", "bySpectres", "", "deathStats.bySpectres" )
+ AddPersistentStat( "deaths_stats", "byGrunts", "", "deathStats.byGrunts" )
+
+ foreach ( titan, alias in GetAsNPCTitanTypes() )
+ {
+ AddPersistentStat( "deaths_stats", titan, "", "deathStats.byNPCTitans[" + alias + "]" )
+ }
+ AddPersistentStat( "deaths_stats", "suicides", "", "deathStats.suicides" )
+ AddPersistentStat( "deaths_stats", "whileEjecting", "", "deathStats.whileEjecting" )
+
+
+ array<string> titanChassis = ["ion", "scorch", "northstar", "ronin", "tone", "legion", "vanguard"]
+
+ AddPersistentStatCategory( "titan_stats" )
+
+ foreach ( titan, chassis in GetCapitalizedTitanTypes() )
+ {
+ AddPersistentStat( "titan_stats", "pilots", chassis, "titanStats[" + chassis + "].pilots", "#UNLOCK_TITAN_PILOT_KILLS" )
+ AddPersistentStat( "titan_stats", "titansTotal", chassis, "titanStats[" + chassis + "].titansTotal", "#UNLOCK_TITAN_TITAN_KILLS" )
+ AddPersistentStat( "titan_stats", "titanDamage", chassis, "titanStats[" + chassis + "].titanDamage", "#UNLOCK_TITAN_TITAN_DAMAGE" )
+ AddPersistentStat( "titan_stats", "coresEarned", chassis, "titanStats[" + chassis + "].coresEarned" )
+ AddPersistentStat( "titan_stats", "pilotsAsPrime", chassis, "titanStats[" + chassis + "].pilotsAsPrime", "#UNLOCK_TITAN_PRIME_PILOT_KILLS" )
+ AddPersistentStat( "titan_stats", "titansAsPrime", chassis, "titanStats[" + chassis + "].titansAsPrime", "#UNLOCK_TITAN_PRIME_TITAN_KILLS" )
+ AddPersistentStat( "titan_stats", "executionsAsPrime", chassis, "titanStats[" + chassis + "].executionsAsPrime", "#UNLOCK_TITAN_PRIME_EXECUTIONS" )
+ AddPersistentStat( "titan_stats", "matchesByDifficulty", chassis, "titanStats[" + chassis + "].matchesByDifficulty[%difficulty%]", "" )
+ AddPersistentStat( "titan_stats", "perfectMatchesByDifficulty", chassis, "titanStats[" + chassis + "].perfectMatchesByDifficulty[%difficulty%]", "" )
+ }
+
+ //##############################################
+ // MISC STATS
+ //##############################################
+
+ AddPersistentStatCategory( "misc_stats" )
+
+ AddPersistentStat( "misc_stats", "titanFalls", "", "miscStats.titanFalls", "#UNLOCK_MISC_TITANFALLS" )
+ AddPersistentStat( "misc_stats", "titanFallsFirst", "", "miscStats.titanFallsFirst", "#UNLOCK_MISC_TITANFALLS_FIRST" )
+ AddPersistentStat( "misc_stats", "titanEmbarks", "", "miscStats.titanEmbarks", "#" )
+ AddPersistentStat( "misc_stats", "rodeos", "", "miscStats.rodeos", "#UNLOCK_MISC_RODEOS" )
+ AddPersistentStat( "misc_stats", "rodeosFromEject", "", "miscStats.rodeosFromEject", "#UNLOCK_MISC_RODOES_EJECT" )
+ AddPersistentStat( "misc_stats", "timesEjected", "", "miscStats.timesEjected", "#" )
+ AddPersistentStat( "misc_stats", "timesEjectedNuclear", "", "miscStats.timesEjectedNuclear", "#" )
+ AddPersistentStat( "misc_stats", "burnCardsEarned", "", "miscStats.burnCardsEarned", "#" )
+ AddPersistentStat( "misc_stats", "burnCardsSpent", "", "miscStats.burnCardsSpent", "#" )
+ AddPersistentStat( "misc_stats", "boostsActivated", "", "miscStats.boostsActivated", "#" )
+ AddPersistentStat( "misc_stats", "spectreLeeches", "", "miscStats.spectreLeeches", "#" )
+ AddPersistentStat( "misc_stats", "spectreLeechesByMap", "", "miscStats.spectreLeechesByMap[%mapname%]", "#" )
+ AddPersistentStat( "misc_stats", "evacsAttempted", "", "miscStats.evacsAttempted", "#" )
+ AddPersistentStat( "misc_stats", "evacsSurvived", "", "miscStats.evacsSurvived", "#UNLOCK_MISC_EVACS" )
+ AddPersistentStat( "misc_stats", "flagsCaptured", "", "miscStats.flagsCaptured", "#" )
+ AddPersistentStat( "misc_stats", "flagsReturned", "", "miscStats.flagsReturned", "#" )
+ AddPersistentStat( "misc_stats", "arcCannonMultiKills", "", "miscStats.arcCannonMultiKills", "#" )
+ AddPersistentStat( "misc_stats", "gruntsConscripted", "", "miscStats.gruntsConscripted", "#" )
+ AddPersistentStat( "misc_stats", "hardpointsCaptured", "", "miscStats.hardpointsCaptured", "#" )
+ AddPersistentStat( "misc_stats", "challengeTiersCompleted", "", "miscStats.challengeTiersCompleted", "#" )
+ AddPersistentStat( "misc_stats", "challengesCompleted", "", "miscStats.challengesCompleted", "#" )
+ AddPersistentStat( "misc_stats", "dailyChallengesCompleted", "", "miscStats.dailyChallengesCompleted", "#" )
+ AddPersistentStat( "misc_stats", "timesLastTitanRemaining", "", "miscStats.timesLastTitanRemaining", "#" )
+ AddPersistentStat( "misc_stats", "killingSprees", "", "miscStats.killingSprees", "#UNLOCK_MISC_KILLING_SPREES" )
+ AddPersistentStat( "misc_stats", "coopChallengesCompleted", "", "miscStats.coopChallengesCompleted", "#" )
+
+ //##############################################
+ // FD STATS
+ //##############################################
+
+ AddPersistentStatCategory( "fd_stats" )
+
+ AddPersistentStat( "fd_stats", "arcMinesPlaced", "", "fdStats.arcMinesPlaced", "#UNLOCK_MISC_ARC_MINE_PLACE" )
+ AddPersistentStat( "fd_stats", "turretsPlaced", "", "fdStats.turretsPlaced", "#UNLOCK_MISC_TURRET_PLACE" )
+ AddPersistentStat( "fd_stats", "rodeos", "", "fdStats.rodeos", "#UNLOCK_FD_RODEOS" )
+ AddPersistentStat( "fd_stats", "rodeoNukes", "", "fdStats.rodeoNukes", "#UNLOCK_MISC_RODEO_NUKES" )
+ AddPersistentStat( "fd_stats", "arcMineZaps", "", "fdStats.arcMineZaps", "#UNLOCK_MISC_ARC_MINE_ZAPS" )
+ AddPersistentStat( "fd_stats", "turretKills", "", "fdStats.turretKills", "#UNLOCK_MISC_TURRET_KILLS" )
+ AddPersistentStat( "fd_stats", "harvesterBoosts", "", "fdStats.harvesterBoosts", "#UNLOCK_MISC_HARVESTER_BOOSTS" )
+ AddPersistentStat( "fd_stats", "wavesComplete", "", "fdStats.wavesComplete", "#UNLOCK_MISC_WAVES_COMPLETE" )
+ AddPersistentStat( "fd_stats", "easyWins", "", "fdStats.easyWins", "#UNLOCK_FD_EASY_WINS" )
+ AddPersistentStat( "fd_stats", "normalWins", "", "fdStats.normalWins", "#UNLOCK_FD_NORMAL_WINS" )
+ AddPersistentStat( "fd_stats", "hardWins", "", "fdStats.hardWins", "#UNLOCK_FD_HARD_WINS" )
+ AddPersistentStat( "fd_stats", "masterWins", "", "fdStats.masterWins", "#UNLOCK_FD_MASTER_WINS" )
+ AddPersistentStat( "fd_stats", "insaneWins", "", "fdStats.insaneWins", "#UNLOCK_FD_INSANE_WINS" )
+ AddPersistentStat( "fd_stats", "highestTitanFDLevel", "", "fdStats.highestTitanFDLevel", "#UNLOCK_FD_TITAN_LEVEL" )
+
+ //#############################################################
+ // DEV ONLY STATS (NOT TRACKED IN RETAIL FOR PLAYER DISPLAY)
+ //#############################################################
+
+ AddPersistentStatCategory( "dev_stats" )
+
+ AddPersistentStat( "dev_stats", "rank_skill", "", DEV_STAT )
+ AddPersistentStat( "dev_stats", "raw_rank_skill", "", DEV_STAT )
+}
+
+void function AddPersistentStatCategory( string category )
+{
+ shGlobalMP.playerStatVars[ category ] <- {}
+}
+
+void function AddPersistentStat( string category, string alias, string subAlias, string variable, string localizedUnlock = "" )
+{
+ if ( !( alias in shGlobalMP.playerStatVars[ category ] ) )
+ shGlobalMP.playerStatVars[ category ][ alias ] <- {}
+ Assert( !( variable in shGlobalMP.playerStatVars[ category ][ alias ] ) )
+
+ PlayerStatData playerStatData
+ playerStatData.statVar = variable
+ playerStatData.statType = ePlayerStatType.INT
+ playerStatData.localizedUnlock = localizedUnlock
+ shGlobalMP.playerStatVars[ category ][ alias ][ subAlias ] <- playerStatData
+}
+
+void function AddPersistentStatInt( string category, string alias, string subAlias, string variable, string localizedUnlock = "" )
+{
+ if ( !( alias in shGlobalMP.playerStatVars[ category ] ) )
+ shGlobalMP.playerStatVars[ category ][ alias ] <- {}
+ Assert( !( variable in shGlobalMP.playerStatVars[ category ][ alias ] ) )
+
+ PlayerStatData playerStatData
+ playerStatData.statVar = variable
+ playerStatData.statType = ePlayerStatType.INT
+ playerStatData.localizedUnlock = localizedUnlock
+ shGlobalMP.playerStatVars[ category ][ alias ][ subAlias ] <- playerStatData
+}
+
+void function AddPersistentStatFloat( string category, string alias, string subAlias, string variable, string localizedUnlock = "" )
+{
+ if ( !( alias in shGlobalMP.playerStatVars[ category ] ) )
+ shGlobalMP.playerStatVars[ category ][ alias ] <- {}
+ Assert( !( variable in shGlobalMP.playerStatVars[ category ][ alias ] ) )
+
+ PlayerStatData playerStatData
+ playerStatData.statVar = variable
+ playerStatData.statType = ePlayerStatType.FLOAT
+ playerStatData.localizedUnlock = localizedUnlock
+ shGlobalMP.playerStatVars[ category ][ alias ][ subAlias ] <- playerStatData
+}
+
+bool function IsValidStat( string category, string alias, string subAlias )
+{
+ if ( category == "" || alias == "" )
+ return false
+
+ if ( !( category in shGlobalMP.playerStatVars ) )
+ return false
+
+ if ( !( alias in shGlobalMP.playerStatVars[ category ] ) )
+ return false
+
+ return ( subAlias in shGlobalMP.playerStatVars[ category ][ alias ] )
+}
+
+string function GetStatVar( string category, string alias, string subAlias = "" )
+{
+ Assert( category in shGlobalMP.playerStatVars, "Invalid stat category " + category )
+ Assert( alias in shGlobalMP.playerStatVars[ category ], "No stat alias " + alias + " in category " + category )
+
+
+ Assert( subAlias in shGlobalMP.playerStatVars[ category ][ alias ] )
+ return shGlobalMP.playerStatVars[ category ][ alias ][ subAlias ].statVar
+}
+
+int function GetStatVarType( string category, string alias, string subAlias = "" )
+{
+ Assert( category in shGlobalMP.playerStatVars, "Invalid stat category " + category )
+ Assert( alias in shGlobalMP.playerStatVars[ category ], "No stat alias " + alias + " in category " + category )
+
+ Assert( subAlias in shGlobalMP.playerStatVars[ category ][ alias ] )
+ return shGlobalMP.playerStatVars[ category ][ alias ][ subAlias ].statType
+}
+
+string function GetStatVarLocalizedUnlock( string category, string alias, string subAlias = "" )
+{
+ Assert( category in shGlobalMP.playerStatVars, "Invalid stat category " + category )
+ Assert( alias in shGlobalMP.playerStatVars[ category ], "No stat alias " + alias + " in category " + category )
+
+ Assert( subAlias in shGlobalMP.playerStatVars[ category ][ alias ] )
+ return shGlobalMP.playerStatVars[ category ][ alias ][ subAlias ].localizedUnlock
+}
+
+int function GetPlayerStatInt( entity player, string category, string alias, string subAlias = "" )
+{
+ Assert( IsUI() || IsValid( player ) )
+
+ string statString = GetStatVar( category, alias, subAlias )
+ return player.GetPersistentVarAsInt( statString )
+}
+
+float function GetPlayerStatFloat( entity player, string category, string alias, string subAlias = "" )
+{
+ Assert( IsUI() || IsValid( player ) )
+
+ string statString = GetStatVar( category, alias, subAlias )
+ return expect float( player.GetPersistentVar( statString ) )
+}
+
+
+string function Stats_GetFixedSaveVar( string saveVar, string mapName, string modeName, string difficultyLevel )
+{
+ string fixedSaveVar = saveVar
+ fixedSaveVar = StringReplace( fixedSaveVar, "%mapname%", mapName )
+ fixedSaveVar = StringReplace( fixedSaveVar, "%gamemode%", modeName )
+ fixedSaveVar = StringReplace( fixedSaveVar, "%difficulty%", difficultyLevel )
+
+ return fixedSaveVar
+}
+
+int function GetPlayerStat_AllCompetitiveModesAndMapsInt( entity player, string category, string alias, string subAlias = "" )
+{
+ Assert( IsUI() || IsValid( player ) )
+
+ int count = 0
+
+ int numMaps = PersistenceGetEnumCount( "maps" )
+ int numModes = PersistenceGetEnumCount( "gameModes" )
+
+ string statVarName = GetStatVar( category, alias, subAlias )
+ string fixedSaveVar
+
+ for ( int mode = 0; mode < numModes; mode++ )
+ {
+ for( int map = 0; map < numMaps; map++ )
+ {
+ fixedSaveVar = Stats_GetFixedSaveVar( statVarName, string( map ), string( mode ), "0" )
+ count += expect int( player.GetPersistentVar( fixedSaveVar ) )
+ }
+ }
+
+ return count
+}
+
+
+int function FD_GetHighestDifficultyForTitan( entity player, string titanRef )
+{
+ string statVar = GetStatVar( "titan_stats", "matchesByDifficulty", titanRef )
+
+ int highestDifficulty = 0
+ for ( int difficulty = 0; difficulty < 5; difficulty++ )
+ {
+ string persistentVar = Stats_GetFixedSaveVar( statVar, "", "", string( difficulty ) )
+ if ( player.GetPersistentVarAsInt( persistentVar ) > 0 )
+ highestDifficulty = difficulty
+ }
+
+ return highestDifficulty
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/superbar/orbitalstrike.nut b/Northstar.CustomServers/mod/scripts/vscripts/superbar/orbitalstrike.nut
new file mode 100644
index 00000000..7e622432
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/superbar/orbitalstrike.nut
@@ -0,0 +1,167 @@
+untyped
+
+global function Orbitalstrike_Init
+
+global function OrbitalStrike
+global function CalculateStrikeDelay
+
+const STRIKE_MODEL = $"models/containers/can_red_soda.mdl"
+const ROCKET_START_HEIGHT = 6000
+const LASER_START_HEIGHT = 1000 // TODO: Make this taller after making the trace go through sky
+const LASER_TIME_LENGTH = 7 // Must match charge length in the weapon
+const LASER_DAMAGE = 300
+const LASER_DAMAGE_RADIUS = 300
+const SPAWN_DELAY = 0.2
+
+table file =
+{
+ impactEffectTable = null
+}
+
+
+function Orbitalstrike_Init()
+{
+ PrecacheParticleSystem( $"ar_rocket_strike_small_friend" )
+ PrecacheParticleSystem( $"ar_rocket_strike_small_foe" )
+ PrecacheParticleSystem( $"ar_rocket_strike_large_friend" )
+ PrecacheParticleSystem( $"ar_rocket_strike_large_foe" )
+ PrecacheParticleSystem( $"wpn_orbital_beam" )
+
+ //if ( IsServer() )
+ // file.impactEffectTable <- PrecacheImpactEffectTable( GetWeaponInfoFileKeyField_Global( "mp_projectile_orbital_strike", "impact_effect_table" ) )
+ #if SERVER
+ file.impactEffectTable = PrecacheImpactEffectTable( "orbital_strike" )
+ #endif
+
+
+
+ RegisterSignal( "TargetDesignated" )
+ RegisterSignal( "BeginLaser" )
+ RegisterSignal( "MoveLaser" )
+ RegisterSignal( "FreezeLaser" )
+ RegisterSignal( "EndLaser" )
+}
+
+
+function CalculateStrikeDelay( index, stepCount, duration )
+{
+ local lastStepDelay = 0
+ if ( index )
+ {
+ local stepFrac = (index - 1) / stepCount.tofloat()
+ stepFrac = 1 - (1 - stepFrac) * (1 - stepFrac)
+ lastStepDelay = stepFrac * (duration)
+ }
+
+ local stepFrac = index / stepCount.tofloat()
+ stepFrac = 1 - (1 - stepFrac) * (1 - stepFrac)
+ return (stepFrac * (duration)) - lastStepDelay
+}
+
+
+function OrbitalStrike( entity player, vector targetPos, numRockets = 12, float radius = 256.0, float totalTime = 3.0, extraStartDelay = null )
+{
+ int team = player.GetTeam()
+ CreateNoSpawnArea( TEAM_INVALID, TEAM_INVALID, targetPos, totalTime, radius )
+
+ if ( extraStartDelay != null )
+ wait extraStartDelay
+
+ // Trace down from max z height until we hit something so we know where rockets should land
+ // This makes calling in orbital strike indoors land on the roof like it should, not indoors
+ //local downStartPos = Vector( targetPos.x, targetPos.y, 16384 )
+ //TraceResults downResult = TraceLine( downStartPos, targetPos, null, (TRACE_MASK_NPCSOLID_BRUSHONLY|TRACE_MASK_WATER), TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( downStartPos+ Vector(0,10,0), targetPos + Vector(0,10,0), 255, 255, 255, true, 60 )
+
+ /*
+ while ( true ) // retrace because we hit a sky brush from outside the level, not the ground
+ {
+ if ( !downResult.hitSky )
+ break
+ printt( "Hit sky" )
+ downStartPos = downResult.endPos
+ downStartPos.z -= 5
+ downResult = TraceLine( downStartPos, targetPos, null, (TRACE_MASK_NPCSOLID_BRUSHONLY|TRACE_MASK_WATER), TRACE_COLLISION_GROUP_NONE )
+ DebugDrawLine( downStartPos, downResult.endPos, 0, 255, 0, true, 60.0 )
+ DebugDrawLine( downStartPos + Vector(10,0,0), targetPos + Vector(10,0,0), 255, 255, 0, true, 60.0 )
+ }
+ */
+
+ /*
+ local upEndPos = targetPos + Vector( 0, 0, ROCKET_START_HEIGHT )
+ TraceResults upResult = TraceLine( downResult.endPos, upEndPos, null, (TRACE_MASK_NPCSOLID_BRUSHONLY|TRACE_MASK_WATER), TRACE_COLLISION_GROUP_NONE )
+ local spawnPos = upResult.endPos
+
+ local rocketPos
+ local min = radius * -1
+ local max = radius
+ local rocket
+ */
+
+ vector rocketOrigin = GetRocketSpawnOrigin( targetPos )
+
+ entity rocket = SpawnRocket( rocketOrigin, Vector( 90, 0, 0 ), player, team ) // First rocket hits center target
+ EmitSoundOnEntity( rocket, "weapon_titanmortar_fire" )
+ EmitSoundOnEntity( rocket, "weapon_titanmortar_projectile" )
+
+ for ( int i = 1; i < numRockets; i++ )
+ {
+ wait CalculateStrikeDelay( i, numRockets, totalTime )
+
+ vector offset = Normalize( Vector( RandomFloatRange( -1.0, 1.0 ), RandomFloatRange( -1.0, 1.0 ), 0 ) )
+ vector rocketPos = rocketOrigin + ( offset * RandomFloat( radius ) )
+
+ entity rocket = SpawnRocket( rocketPos, Vector( 90, 0, 0 ), player, team )
+ EmitSoundOnEntity( rocket, "weapon_titanmortar_fire" )
+ EmitSoundOnEntity( rocket, "weapon_titanmortar_projectile" )
+ }
+}
+
+vector function GetRocketSpawnOrigin( vector point )
+{
+ vector skyPos = GetSkyOriginAbovePoint( point )
+ TraceResults traceResult = TraceLine( skyPos, point, null, (TRACE_MASK_SHOT), TRACE_COLLISION_GROUP_NONE )
+ vector rocketOrigin = traceResult.endPos
+ rocketOrigin.z += 6000
+ if ( rocketOrigin.z > skyPos.z - 1 )
+ rocketOrigin.z = skyPos.z - 1
+ return rocketOrigin
+}
+
+vector function GetSkyOriginAbovePoint( vector point )
+{
+ vector skyOrigin = Vector( point.x, point.y, MAX_WORLD_COORD )
+ vector traceFromPos = Vector( point.x, point.y, point.z )
+
+ while ( true )
+ {
+ TraceResults traceResult = TraceLine( traceFromPos, skyOrigin, null, (TRACE_MASK_SHOT), TRACE_COLLISION_GROUP_NONE )
+
+ if ( traceResult.hitSky )
+ {
+ skyOrigin = traceResult.endPos
+ break
+ }
+
+ traceFromPos = traceResult.endPos
+ traceFromPos.z += 1
+ }
+
+ return skyOrigin
+}
+
+entity function SpawnRocket( vector spawnPos, vector spawnAng, entity owner, int team )
+{
+ entity rocket = CreateEntity( "rpg_missile" )
+ rocket.SetOrigin( spawnPos )
+ rocket.SetAngles( spawnAng )
+ rocket.SetOwner( owner )
+ SetTeam( rocket, team )
+ rocket.SetModel( $"models/weapons/bullets/projectile_rocket.mdl" )
+ rocket.SetImpactEffectTable( file.impactEffectTable )
+ rocket.SetWeaponClassName( "mp_titanweapon_orbital_strike" )
+ rocket.kv.damageSourceId = eDamageSourceId.mp_titanweapon_orbital_strike
+ DispatchSpawn( rocket )
+
+ return rocket
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/superbar/smokescreen.nut b/Northstar.CustomServers/mod/scripts/vscripts/superbar/smokescreen.nut
new file mode 100644
index 00000000..6bbb3e89
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/superbar/smokescreen.nut
@@ -0,0 +1,417 @@
+
+global function Smokescreen_Init
+global function Smokescreen
+global function IsOriginTouchingSmokescreen
+global function IsRayTouchingSmokescreen
+
+#if DEV
+const bool SMOKESCREEN_DEBUG = false
+#endif
+
+global struct SmokescreenStruct
+{
+ vector origin
+ vector angles
+ bool fxUseWeaponOrProjectileAngles = false
+
+ float lifetime = 5.0
+ int ownerTeam = TEAM_ANY
+
+ asset smokescreenFX = FX_ELECTRIC_SMOKESCREEN
+ float fxXYRadius = 230.0 // single fx xy radius used to create nospawn area and block traces
+ float fxZRadius = 170.0 // single fx z radius used to create nospawn area and block traces
+ string deploySound1p = SFX_SMOKE_DEPLOY_1P
+ string deploySound3p = SFX_SMOKE_DEPLOY_3P
+ string stopSound1p = ""
+ string stopSound3p = ""
+ int damageSource = eDamageSourceId.mp_titanability_smoke
+
+ bool blockLOS = true
+ bool shouldHibernate = true
+
+ bool isElectric = true
+ entity attacker
+ entity inflictor
+ entity weaponOrProjectile
+ float damageDelay = 2.0
+ float damageInnerRadius = 320.0
+ float damageOuterRadius = 350.0
+ float dangerousAreaRadius = -1.0
+ int dpsPilot = 30
+ int dpsTitan = 2200
+
+ array<vector> fxOffsets
+}
+
+struct SmokescreenFXStruct
+{
+ vector center // center of all fx positions
+ vector mins // approx mins of all fx relative to center
+ vector maxs // approx maxs of all fx relative to center
+ float radius // approx radius of all fx relative to center
+ array<vector> fxWorldPositions
+ int ownerTeam = TEAM_ANY
+}
+
+struct
+{
+ array<SmokescreenFXStruct> allSmokescreenFX
+ table<entity, float> nextSmokeSoundTime
+} file
+
+void function Smokescreen_Init()
+{
+ PrecacheParticleSystem( FX_ELECTRIC_SMOKESCREEN )
+ PrecacheParticleSystem( FX_ELECTRIC_SMOKESCREEN_BURN )
+ #if MP
+ PrecacheParticleSystem( FX_ELECTRIC_SMOKESCREEN_HEAL )
+ #endif
+ PrecacheParticleSystem( FX_GRENADE_SMOKESCREEN )
+
+ PrecacheSprite( $"sprites/physbeam.vmt" )
+ PrecacheSprite( $"sprites/glow01.vmt" )
+
+#if SERVER
+ AddDamageCallbackSourceID( eDamageSourceId.mp_titanability_smoke, TitanElectricSmoke_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_grenade_electric_smoke, GrenadeElectricSmoke_DamagedPlayerOrNPC )
+#endif
+}
+
+void function Smokescreen( SmokescreenStruct smokescreen )
+{
+ SmokescreenFXStruct fxInfo = Smokescreen_CalculateFXStruct( smokescreen )
+ file.allSmokescreenFX.append( fxInfo )
+
+ array<entity> thermiteBurns = GetActiveThermiteBurnsWithinRadius( fxInfo.center, fxInfo.radius )
+ foreach ( thermiteBurn in thermiteBurns )
+ {
+ entity owner = thermiteBurn.GetOwner()
+
+ if ( IsValid( owner ) && owner.GetTeam() != smokescreen.ownerTeam )
+ thermiteBurn.Destroy()
+ }
+
+ entity traceBlocker
+
+ if ( smokescreen.blockLOS )
+ traceBlocker = Smokescreen_CreateTraceBlockerVol( smokescreen, fxInfo )
+
+#if DEV
+ if ( SMOKESCREEN_DEBUG )
+ DebugDrawCircle( fxInfo.center, <0,0,0>, fxInfo.radius + 240.0, 255, 255, 0, true, smokescreen.lifetime )
+#endif
+ CreateNoSpawnArea( TEAM_ANY, TEAM_ANY, fxInfo.center, smokescreen.lifetime, fxInfo.radius + 240.0 )
+
+ if ( IsValid( smokescreen.attacker ) && smokescreen.attacker.IsPlayer() )
+ {
+ EmitSoundAtPositionExceptToPlayer( TEAM_ANY, fxInfo.center, smokescreen.attacker, smokescreen.deploySound3p )
+ EmitSoundAtPositionOnlyToPlayer( TEAM_ANY, fxInfo.center, smokescreen.attacker, smokescreen.deploySound1p)
+ }
+ else
+ {
+ EmitSoundAtPosition( TEAM_ANY, fxInfo.center, smokescreen.deploySound3p )
+ }
+
+ array<entity> fxEntities = SmokescreenFX( smokescreen, fxInfo )
+ if ( smokescreen.isElectric )
+ thread SmokescreenAffectsEntitiesInArea( smokescreen, fxInfo )
+ //thread CreateSmokeSightTrigger( fxInfo.center, smokescreen.ownerTeam, smokescreen.lifetime ) // disabling for now, this should use the calculated radius if reenabled
+
+ thread DestroySmokescreen( smokescreen, smokescreen.lifetime, fxInfo, traceBlocker, fxEntities )
+}
+
+SmokescreenFXStruct function Smokescreen_CalculateFXStruct( SmokescreenStruct smokescreen )
+{
+ SmokescreenFXStruct fxInfo
+
+ foreach ( i, position in smokescreen.fxOffsets )
+ {
+ //mins
+ if ( i == 0 || position.x < fxInfo.mins.x )
+ fxInfo.mins = <position.x, fxInfo.mins.y, fxInfo.mins.z>
+
+ if ( i == 0 || position.y < fxInfo.mins.y )
+ fxInfo.mins = <fxInfo.mins.x, position.y, fxInfo.mins.z>
+
+ if ( i == 0 || position.z < fxInfo.mins.z )
+ fxInfo.mins = <fxInfo.mins.x, fxInfo.mins.y, position.z>
+
+ // maxs
+ if ( i == 0 || position.x > fxInfo.maxs.x )
+ fxInfo.maxs = <position.x, fxInfo.maxs.y, fxInfo.maxs.z>
+
+ if ( i == 0 || position.y > fxInfo.maxs.y )
+ fxInfo.maxs = <fxInfo.maxs.x, position.y, fxInfo.maxs.z>
+
+ if ( i == 0 || position.z > fxInfo.maxs.z )
+ fxInfo.maxs = <fxInfo.maxs.x, fxInfo.maxs.y, position.z>
+ }
+
+ vector offsetCenter = fxInfo.mins + ( fxInfo.maxs - fxInfo.mins ) * 0.5
+
+ float xyRadius = smokescreen.fxXYRadius * 0.7071
+ float zRadius = smokescreen.fxZRadius * 0.7071
+
+ fxInfo.mins = <fxInfo.mins.x - xyRadius, fxInfo.mins.y - xyRadius, fxInfo.mins.z - zRadius> - offsetCenter
+ fxInfo.maxs = <fxInfo.maxs.x + xyRadius, fxInfo.maxs.y + xyRadius, fxInfo.maxs.z + zRadius> - offsetCenter
+
+ float radiusSqr
+ float singleFXRadius = max( smokescreen.fxXYRadius, smokescreen.fxZRadius )
+
+ vector forward = AnglesToForward( smokescreen.angles )
+ vector right = AnglesToRight( smokescreen.angles )
+ vector up = AnglesToUp( smokescreen.angles )
+
+ foreach ( i, position in smokescreen.fxOffsets )
+ {
+ float distanceSqr = DistanceSqr( position, offsetCenter )
+
+ if ( radiusSqr < distanceSqr )
+ radiusSqr = distanceSqr
+
+ fxInfo.fxWorldPositions.append( smokescreen.origin + ( position.x * forward ) + ( position.y * right ) + ( position.z * up ) )
+ }
+
+ fxInfo.center = smokescreen.origin + ( offsetCenter.x * forward ) + ( offsetCenter.y * right ) + ( offsetCenter.z * up )
+ fxInfo.radius = sqrt( radiusSqr ) + singleFXRadius
+ fxInfo.ownerTeam = smokescreen.ownerTeam
+
+ return fxInfo
+}
+
+void function SmokescreenAffectsEntitiesInArea( SmokescreenStruct smokescreen, SmokescreenFXStruct fxInfo )
+{
+ float startTime = Time()
+ float tickRate = 0.1
+
+ float dpsPilot = smokescreen.dpsPilot * tickRate
+ float dpsTitan = smokescreen.dpsTitan * tickRate
+ Assert( dpsPilot || dpsTitan > 0, "Electric smokescreen with 0 damage created" )
+
+ entity aiDangerTarget = CreateEntity( "info_target" )
+ DispatchSpawn( aiDangerTarget )
+ aiDangerTarget.SetOrigin( fxInfo.center )
+ SetTeam( aiDangerTarget, smokescreen.ownerTeam )
+
+ float dangerousAreaRadius = smokescreen.damageOuterRadius
+ if ( smokescreen.dangerousAreaRadius != -1.0 )
+ dangerousAreaRadius = smokescreen.dangerousAreaRadius
+
+ AI_CreateDangerousArea_Static( aiDangerTarget, smokescreen.weaponOrProjectile, dangerousAreaRadius, TEAM_INVALID, true, true, fxInfo.center )
+
+ OnThreadEnd(
+ function () : ( aiDangerTarget )
+ {
+ aiDangerTarget.Destroy()
+ }
+ )
+
+ wait smokescreen.damageDelay
+
+ while ( Time() - startTime <= smokescreen.lifetime )
+ {
+#if DEV
+ if ( SMOKESCREEN_DEBUG )
+ {
+ DebugDrawCircle( fxInfo.center, <0,0,0>, smokescreen.damageInnerRadius, 255, 0, 0, true, tickRate )
+ DebugDrawCircle( fxInfo.center, <0,0,0>, smokescreen.damageOuterRadius, 255, 0, 0, true, tickRate )
+ }
+#endif
+
+ RadiusDamage(
+ fxInfo.center, // center
+ smokescreen.attacker, // attacker
+ smokescreen.inflictor, // inflictor
+ dpsPilot, // damage
+ dpsTitan, // damageHeavyArmor
+ smokescreen.damageInnerRadius, // innerRadius
+ smokescreen.damageOuterRadius, // outerRadius
+ SF_ENVEXPLOSION_MASK_BRUSHONLY, // flags
+ 0.0, // distanceFromAttacker
+ 0.0, // explosionForce
+ DF_ELECTRICAL | DF_NO_HITBEEP, // scriptDamageFlags
+ smokescreen.damageSource ) // scriptDamageSourceIdentifier
+
+ wait tickRate
+ }
+}
+
+entity function Smokescreen_CreateTraceBlockerVol( SmokescreenStruct smokescreen, SmokescreenFXStruct fxInfo )
+{
+ entity traceBlockerVol = CreateEntity( "trace_volume" )
+ traceBlockerVol.kv.targetname = UniqueString( "smokescreen_traceblocker_vol" )
+ traceBlockerVol.kv.origin = fxInfo.center
+ traceBlockerVol.kv.angles = smokescreen.angles
+ DispatchSpawn( traceBlockerVol )
+ traceBlockerVol.SetBox( fxInfo.mins * 0.9, fxInfo.maxs * 0.9 )
+
+#if DEV
+ if ( SMOKESCREEN_DEBUG )
+ DrawAngledBox( fxInfo.center, smokescreen.angles, fxInfo.mins, fxInfo.maxs, 255, 0, 0, true, smokescreen.lifetime - 0.6 )
+#endif
+
+ return traceBlockerVol
+}
+
+array<entity> function SmokescreenFX( SmokescreenStruct smokescreen, SmokescreenFXStruct fxInfo )
+{
+ array<entity> fxEntities
+
+ foreach ( position in fxInfo.fxWorldPositions )
+ {
+#if DEV
+ if ( SMOKESCREEN_DEBUG )
+ DebugDrawCircle( position, <0.0, 0.0, 0.0>, smokescreen.fxXYRadius, 0, 0, 255, true, smokescreen.lifetime )
+#endif
+ int fxID = GetParticleSystemIndex( smokescreen.smokescreenFX )
+ vector angles = smokescreen.fxUseWeaponOrProjectileAngles ? smokescreen.weaponOrProjectile.GetAngles() : <0.0, 0.0, 0.0>
+ entity fxEnt = StartParticleEffectInWorld_ReturnEntity( fxID, position, angles )
+ float fxLife = smokescreen.lifetime
+
+ EffectSetControlPointVector( fxEnt, 1, <fxLife, 0.0, 0.0> )
+
+ if ( !smokescreen.shouldHibernate )
+ fxEnt.DisableHibernation()
+
+ fxEntities.append( fxEnt )
+ }
+
+ return fxEntities
+}
+
+void function DestroySmokescreen( SmokescreenStruct smokescreen, float lifetime, SmokescreenFXStruct fxInfo, entity traceBlocker, array<entity> fxEntities )
+{
+ float timeToWait = 0.0
+
+ timeToWait = max( lifetime - 0.5, 0.0 )
+
+ wait( timeToWait )
+ if ( IsValid( traceBlocker ) )
+ traceBlocker.Destroy()
+ file.allSmokescreenFX.fastremovebyvalue( fxInfo )
+
+ StopSoundAtPosition( fxInfo.center, smokescreen.deploySound1p )
+ StopSoundAtPosition( fxInfo.center, smokescreen.deploySound3p )
+
+ if ( IsValid( smokescreen.attacker ) && smokescreen.attacker.IsPlayer() )
+ {
+ if ( smokescreen.stopSound3p != "" )
+ EmitSoundAtPositionExceptToPlayer( TEAM_ANY, fxInfo.center, smokescreen.attacker, smokescreen.stopSound3p )
+
+ if ( smokescreen.stopSound1p != "" )
+ EmitSoundAtPositionOnlyToPlayer( TEAM_ANY, fxInfo.center, smokescreen.attacker, smokescreen.stopSound1p)
+ }
+ else
+ {
+ if ( smokescreen.stopSound3p != "" )
+ EmitSoundAtPosition( TEAM_ANY, fxInfo.center, smokescreen.stopSound3p )
+ }
+
+ timeToWait = max( ( lifetime + 0.1 ) - timeToWait, 0.0 )
+ wait( timeToWait )
+
+ foreach ( fxEnt in fxEntities )
+ {
+ if ( IsValid( fxEnt ) )
+ fxEnt.Destroy()
+ }
+}
+
+bool function IsOriginTouchingSmokescreen( vector origin, int teamToIgnore = TEAM_UNASSIGNED )
+{
+ foreach ( fxInfo in file.allSmokescreenFX )
+ {
+ if ( teamToIgnore == fxInfo.ownerTeam )
+ continue
+
+ if ( DistanceSqr( origin, fxInfo.center ) < fxInfo.radius * fxInfo.radius )
+ return true
+ }
+
+ return false
+}
+
+bool function IsRayTouchingSmokescreen( vector rayStart, vector rayEnd, int teamToIgnore = TEAM_UNASSIGNED )
+{
+ foreach ( fxInfo in file.allSmokescreenFX )
+ {
+ if ( teamToIgnore == fxInfo.ownerTeam )
+ continue
+
+ if ( IntersectRayWithSphere( rayStart, rayEnd, fxInfo.center, fxInfo.radius ).result )
+ return true
+ }
+
+ return false
+}
+
+#if SERVER
+void function TitanElectricSmoke_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ if ( !IsAlive( ent ) )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( ent.GetTeam() == attacker.GetTeam() )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ PlayDamageSounds( ent, attacker, ELECTRIC_SMOKESCREEN_SFX_DAMAGE_TITAN_1P, ELECTRIC_SMOKESCREEN_SFX_DAMAGE_TITAN_3P, ELECTRIC_SMOKESCREEN_SFX_DAMAGE_PILOT_1P, ELECTRIC_SMOKESCREEN_SFX_DAMAGE_PILOT_3P )
+}
+
+void function GrenadeElectricSmoke_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ if ( !IsAlive( ent ) )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ PlayDamageSounds( ent, attacker, ELECTRIC_SMOKE_GRENADE_SFX_DAMAGE_TITAN_1P, ELECTRIC_SMOKE_GRENADE_SFX_DAMAGE_TITAN_3P, ELECTRIC_SMOKE_GRENADE_SFX_DAMAGE_PILOT_1P, ELECTRIC_SMOKE_GRENADE_SFX_DAMAGE_PILOT_3P )
+}
+
+void function PlayDamageSounds( entity ent, entity attacker, string titan1P_SFX, string titan3P_SFX, string pilot1P_SFX, string pilot3P_SFX )
+{
+ float currentTime = Time()
+
+ if ( !( ent in file.nextSmokeSoundTime ) )
+ {
+ if ( ent.IsPlayer() )
+ file.nextSmokeSoundTime[ ent ] <- currentTime
+ else
+ file.nextSmokeSoundTime[ ent ] <- currentTime + RandomFloat( 0.5 )
+ }
+
+ if ( file.nextSmokeSoundTime[ ent ] <= currentTime )
+ {
+ if ( ent.IsPlayer() )
+ {
+ if ( ent.IsTitan() )
+ {
+ EmitSoundOnEntityExceptToPlayer( ent, ent, titan3P_SFX )
+ EmitSoundOnEntityOnlyToPlayer( ent, ent, titan1P_SFX )
+ file.nextSmokeSoundTime[ ent ] = currentTime + RandomFloatRange( 0.75, 1.25 )
+ }
+ else
+ {
+ EmitSoundOnEntityExceptToPlayer( ent, ent, pilot3P_SFX )
+ EmitSoundOnEntityOnlyToPlayer( ent, ent, pilot1P_SFX )
+ }
+
+ if ( IsValid( attacker ) && attacker.IsPlayer() )
+ EmitSoundOnEntityOnlyToPlayer( attacker, attacker, "Player.Hitbeep" )
+ }
+ else
+ {
+ if ( ent.IsTitan() )
+ EmitSoundOnEntity( ent, titan3P_SFX )
+ else if ( IsHumanSized( ent ) )
+ EmitSoundOnEntity( ent, pilot3P_SFX )
+ }
+
+ file.nextSmokeSoundTime[ ent ] = currentTime + RandomFloatRange( 0.75, 1.25 )
+ }
+}
+#endif \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sv_globals.gnut b/Northstar.CustomServers/mod/scripts/vscripts/sv_globals.gnut
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/sv_globals.gnut
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan/_battery_generator.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan/_battery_generator.gnut
new file mode 100644
index 00000000..567ad6e7
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan/_battery_generator.gnut
@@ -0,0 +1,128 @@
+global function InitDestroyableGenerator
+global function ClearGenerators
+
+const GENERATOR_HEALTH = 200
+
+const MODEL_DESTROYED_GENERATOR = $"models/beacon/charge_generator_01_destroyed.mdl"
+const FX_GENERATOR_EXP = $"P_generator_exp"
+
+struct
+{
+ array<entity> generators
+} file
+
+void function InitDestroyableGenerator()
+{
+ AddSpawnCallbackEditorClass( "script_ref", "script_battery_generator", SpawnPropGenerator )
+ AddSpawnCallback_ScriptName( "prop_battery_generator", PropBatteryGeneratorThink )
+
+ PrecacheModel( MODEL_GENERATOR )
+ PrecacheModel( MODEL_DESTROYED_GENERATOR )
+ PrecacheParticleSystem( FX_GENERATOR_EXP )
+}
+
+void function SpawnPropGenerator( entity generatorRef )
+{
+ entity generator = CreatePropScript( MODEL_GENERATOR, generatorRef.GetOrigin(), generatorRef.GetAngles(), 6 )
+ thread PropBatteryGeneratorThink( generator )
+ generatorRef.Destroy()
+}
+
+void function PropBatteryGeneratorThink( entity generator )
+{
+ SetObjectCanBeMeleed( generator, true )
+ SetVisibleEntitiesInConeQueriableEnabled( generator, true )
+ generator.SetTakeDamageType( DAMAGE_EVENTS_ONLY )
+ generator.SetDamageNotifications( true )
+ generator.SetMaxHealth( GENERATOR_HEALTH )
+ generator.SetHealth( GENERATOR_HEALTH )
+ generator.DisableHibernation()
+ AddEntityCallback_OnDamaged( generator, GeneratorOnDamage )
+
+ entity trigger = CreateEntity( "trigger_cylinder" )
+ trigger.SetRadius( 150 )
+ trigger.SetAboveHeight( 150 )
+ trigger.SetBelowHeight( 150 ) //i.e. make the trigger a sphere as opposed to a cylinder
+ trigger.SetOrigin( generator.GetOrigin() )
+ trigger.SetParent( generator )
+ DispatchSpawn( trigger )
+ trigger.SetEnterCallback( GeneratorTriggerThink )
+
+
+ file.generators.append( generator )
+}
+
+void function GeneratorTriggerThink( entity trigger, entity ent )
+{
+ if ( ent.IsTitan() || IsSuperSpectre( ent ) )
+ {
+ entity generator = trigger.GetParent()
+
+ if ( generator != null )
+ {
+ GeneratorDestroy( generator )
+ }
+ }
+}
+
+void function GeneratorOnDamage( entity generator, var damageInfo )
+{
+ if ( !IsValid( generator ) )
+ {
+ // sometimes OnDamage gets called twice in the same frame, ( scorch's fire )
+ // and first call destroys generator in GeneratorDestroy()
+ return
+ }
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ int health = generator.GetHealth()
+ health -= int( damage )
+
+ if ( health <= 0 )
+ GeneratorDestroy( generator )
+ else
+ generator.SetHealth( health )
+}
+
+void function GeneratorDestroy( entity generator )
+{
+ int solidType = 6 //phys collision
+ entity destroyedProp = CreatePropDynamic( MODEL_DESTROYED_GENERATOR, generator.GetOrigin(), generator.GetAngles(), solidType )
+ if ( generator.GetParent() )
+ destroyedProp.SetToSameParentAs( generator )
+
+ destroyedProp.AllowMantle()
+ destroyedProp.DisableHibernation()
+ int fxID = GetParticleSystemIndex( FX_GENERATOR_EXP )
+ vector origin = generator.GetOrigin()
+ vector up = generator.GetUpVector()
+
+ EmitSoundOnEntity( destroyedProp, "BatteryCrate_Explosion" )
+ StartParticleEffectOnEntity( destroyedProp, fxID, FX_PATTACH_ABSORIGIN_FOLLOW, -1 )
+
+ entity battery = CreateTitanBattery( origin + ( up * 40 ) )
+ battery.DisableHibernation()
+
+ //throw out the battery
+ vector right = generator.GetRightVector() * RandomFloatRange( -0.5, 0.5 )
+ vector forward = generator.GetForwardVector() * RandomFloatRange( -0.5, 0.5 )
+ vector velocity = Normalize( up + right + forward ) * 10
+
+ //for moving geo
+ vector parentVelocity = generator.GetVelocity()
+
+ battery.SetVelocity( velocity + parentVelocity )
+
+ file.generators.fastremovebyvalue( generator )
+ generator.Destroy()
+}
+
+void function ClearGenerators()
+{
+ foreach ( g in file.generators )
+ {
+ g.Destroy()
+ }
+ file.generators = []
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan/_replacement_titans.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan/_replacement_titans.gnut
new file mode 100644
index 00000000..c9d986bc
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan/_replacement_titans.gnut
@@ -0,0 +1,1183 @@
+untyped
+
+global function ReplacementTitans_Init
+
+global function EmptyTitanPlaysAnim
+global function TryReplacementTitanReadyAnnouncement
+
+global function IsReplacementTitanAvailable
+
+global function SetTitanRespawnTimer
+global function GetTitanRespawnTimer
+global function DecrementBuildTimer
+global function ReplacementTitanTimerFinished
+global function GetAttachmentAtTimeFromModel
+global function TryETATitanReadyAnnouncement
+global function TryUpdateTitanRespawnTimerForNewTitanSelection
+global function IsReplacementDropInProgress
+
+global function req
+global function ReplacementTitan
+global function TryAnnounceTitanfallWarningToEnemyTeam
+global function GetTitanForPlayer
+
+
+global function ShouldSetTitanRespawnTimer
+
+global function PauseTitanTimers
+global function PauseTitansThink
+
+global function IsReplacementTitanAvailableForGameState
+
+global function SetReplacementTitanGamemodeRules
+global function SetRequestTitanGamemodeRules
+
+global function CreateTitanForPlayerAndHotdrop
+
+struct {
+ array<int> ETATimeThresholds = [ 120, 60, 30, 15 ]
+ float ETA2MinUpperBound = 123
+ float ETA2MinLowerBound = 115
+ float ETA60sUpperBound = 63
+ float ETA60sLowerBound = 55
+ float ETA30sUpperBound = 33
+ float ETA30sLowerBound = 25
+ float ETA15sUpperBound = 18
+ float ETA15sLowerBound = 12
+ float ETAAnnouncementAllowanceTime = 6.0
+
+ bool buildTimerDisabled = false
+
+ table warpFallDebounce = {}
+
+ bool functionref( entity ) ReplacementTitanGamemodeRules
+ bool functionref( entity, vector ) RequestTitanGamemodeRules
+
+} file
+
+const nagInterval = 40
+
+global const float WARPFALL_SOUND_DELAY = 1.1
+global const float WARPFALL_FX_DELAY = 0.9
+
+function ReplacementTitans_Init()
+{
+ ReplacementTitansDrop_Init()
+
+ RegisterSignal( "titan_impact" )
+
+ RegisterSignal( "SetTitanRespawnTimer" )
+ RegisterSignal( "CalledInReplacementTitan" )
+
+ PrecacheEffect( TURBO_WARP_FX )
+ PrecacheEffect( TURBO_WARP_COMPANY )
+
+
+ AddCallback_OnClientConnecting( ReplacementTitan_InitPlayer )
+ AddClientCommandCallback( "ClientCommand_RequestTitan", ClientCommand_RequestTitan )
+ AddSoulDeathCallback( ResetTitanReplacementAnnouncements )
+
+ level.maxTitansPerTeam <- 2
+
+ if ( file.ReplacementTitanGamemodeRules == null )
+ file.ReplacementTitanGamemodeRules = ReplacementTitanGamemodeRules_Default
+ if ( file.RequestTitanGamemodeRules == null )
+ file.RequestTitanGamemodeRules = RequestTitanGamemodeRules_Default
+
+ FlagInit( "LevelHasRoof" )
+}
+
+
+void function ReplacementTitan_InitPlayer( entity player )
+{
+ player.p.replacementTitanETATimer = GetTimeLimit_ForGameMode() * 60.0
+}
+
+
+bool function IsReplacementTitanAvailable( player, timeBuffer = 0 )
+{
+ expect entity( player )
+
+ if ( !IsReplacementTitanAvailableForGameState() )
+ return false
+
+ if ( player.IsTitan() )
+ return false
+
+ if ( IsAlive( player.GetPetTitan() ) )
+ return false
+
+ if ( player.isSpawning )
+ return false
+
+ if ( !file.ReplacementTitanGamemodeRules( player ) )
+ return false
+
+ switch ( Riff_TitanAvailability() )
+ {
+ case eTitanAvailability.Default:
+ if ( player.titansBuilt == 0 )
+ return true
+ else
+ break
+
+ default:
+ return Riff_IsTitanAvailable( player )
+ }
+
+ if ( player.IsBot() )
+ return true
+
+ return ReplacementTitanTimerFinished( player, timeBuffer )
+}
+
+function IsReplacementTitanAvailableForGameState()
+{
+ #if HAS_GAMEMODES
+ local currentGameState = GetGameState()
+
+ switch ( currentGameState ) //need to add a new entry in here for every new game state we make
+ {
+ case eGameState.WaitingForCustomStart:
+ case eGameState.WaitingForPlayers:
+ case eGameState.PickLoadout:
+ case eGameState.Prematch:
+ case eGameState.SwitchingSides:
+ case eGameState.Postmatch:
+ return false
+
+ case eGameState.Playing:
+ case eGameState.SuddenDeath:
+ return true
+
+ case eGameState.WinnerDetermined:
+ case eGameState.Epilogue:
+ {
+ if ( IsRoundBased() )
+ {
+ if ( !IsRoundBasedGameOver() )
+ return false
+
+ if ( !ShouldRunEvac() )
+ return false
+ }
+
+ return true
+ }
+
+ default:
+ Assert( false, "Unknown Game State: " + currentGameState )
+ return false
+ }
+ #endif
+
+ return true
+}
+
+void function SetReplacementTitanGamemodeRules( bool functionref( entity ) rules )
+{
+ file.ReplacementTitanGamemodeRules = rules
+}
+
+void function SetRequestTitanGamemodeRules( bool functionref( entity, vector ) rules )
+{
+ file.RequestTitanGamemodeRules = rules
+}
+
+bool function ReplacementTitanGamemodeRules_Default( entity player )
+{
+ return true
+}
+
+bool function RequestTitanGamemodeRules_Default( entity player, vector origin )
+{
+ return true
+}
+
+float function GetTitanRespawnTimer( entity player )
+{
+ return player.GetNextTitanRespawnAvailable() - Time()
+}
+
+
+#if SP
+void function DecrementBuildTimer( entity player, float amount )
+{
+ if ( !player.IsTitan() )
+ return
+ // core ability in use
+ if ( TitanCoreInUse( player ) )
+ return
+
+ if ( !IsAlive( player ) )
+ return
+
+ SetTitanCoreTimer( player, GetTitanCoreTimer( player ) - amount )
+}
+#endif
+
+#if MP
+void function DecrementBuildTimer( entity player, float amount )
+{
+ Assert( !TitanDamageRewardsTitanCoreTime() || !player.IsTitan() )
+
+ amount = ModifyBuildTimeForPlayerBonuses( player, amount )
+
+ bool shouldDecrementBuildTimer = true
+
+ if ( player.IsTitan() )
+ {
+ // core ability in use
+ if ( TitanCoreInUse( player ) )
+ return
+
+ if ( !IsAlive( player ) )
+ return
+ }
+ else
+ {
+ //Don't decrement build time for Titan if already have Titan in map
+ if ( player.GetPetTitan() )
+ return
+ }
+
+ if ( player.IsTitan() )
+ {
+ SetTitanCoreTimer( player, GetTitanCoreTimer( player ) - amount )
+ }
+ else if ( shouldDecrementBuildTimer )
+ {
+ float remainingTime = GetTitanRespawnTimer( player )
+ SetTitanRespawnTimer( player, remainingTime - amount )
+ }
+}
+#endif
+
+float function ModifyBuildTimeForPlayerBonuses( entity player, float amount )
+{
+ if ( PlayerHasServerFlag( player, SFLAG_FAST_BUILD2 ) )
+ amount *= 2.0
+ else if ( PlayerHasServerFlag( player, SFLAG_FAST_BUILD1 ) )
+ amount *= 1.5
+
+ return amount
+}
+
+
+void function TryUpdateTitanRespawnTimerForNewTitanSelection( entity player )
+{
+ if ( GetCurrentPlaylistVarInt( "titan_build_time_use_set_file", 0 ) == 1 )
+ {
+ if ( ShouldSetTitanRespawnTimer( player ) )
+ {
+ if ( player.GetTitanBuildTime() != GetTitanBuildTime( player ) )
+ {
+ float timeElapsed = player.GetTitanBuildTime() - ( player.GetNextTitanRespawnAvailable() - Time() )
+ ResetTitanBuildTime( player ) // update titan build time here
+ float newTime = Time() + ( player.GetTitanBuildTime() - timeElapsed )
+ player.SetNextTitanRespawnAvailable( max( 0, newTime ) )
+ }
+ }
+ }
+}
+
+void function SetTitanRespawnTimer( entity player, float timeDiff )
+{
+ //printt( "SetTitanRespawnTimer with timeDiff: " + timeDiff )
+ if ( ShouldSetTitanRespawnTimer( player ) == false )
+ return
+
+ float newTime = Time() + timeDiff
+ player.SetNextTitanRespawnAvailable( max( Time() - 1, newTime ) )
+
+ thread WaitToAnnounceTitanETA( player, timeDiff )
+}
+
+bool function ShouldSetTitanRespawnTimer( player )
+{
+ if ( Riff_TitanAvailability() == eTitanAvailability.Custom )
+ return false
+
+ if ( Riff_TitanAvailability() == eTitanAvailability.Default )
+ return true
+
+ if ( player.IsTitan() )
+ return true
+
+ if ( IsValid( player.GetPetTitan() ) )
+ return true
+
+ if ( player.GetNextTitanRespawnAvailable() < 0 )
+ return false
+
+ return true
+}
+
+
+
+function WaitToAnnounceTitanETA( entity player, timeDiff )
+{
+ player.EndSignal( "OnDestroy" )
+ player.Signal( "SetTitanRespawnTimer" )
+ player.EndSignal( "SetTitanRespawnTimer" )
+ player.EndSignal( "CalledInReplacementTitan" )
+ player.EndSignal( "ChoseToSpawnAsTitan" )
+
+ if ( timeDiff > 0 )
+ wait GetTimeTillNextETAAnnouncement( player )
+
+ TryETATitanReadyAnnouncement( player )
+}
+
+float function GetTimeTillNextETAAnnouncement( entity player )
+{
+// if ( !IsValid( player ) )
+// return 0
+
+ float timeTillNextTitan = player.GetNextTitanRespawnAvailable() - Time()
+ if ( timeTillNextTitan <= 0 )
+ {
+ //printt( "Waiting 0, Titan Ready" )
+ return 0
+ }
+
+// if ( !( "replacementTitanETATimer" in player.s ) )
+// return 0
+
+ if ( timeTillNextTitan >= file.ETA2MinUpperBound && player.p.replacementTitanETATimer > 120 ) //Give some leadup time to conversation starting
+ {
+ //printt( "Waiting " + ( timeTillNextTitan - file.ETA2MinUpperBound ) + " till 2 min announcement" )
+ return timeTillNextTitan - file.ETA2MinUpperBound
+ }
+
+ if ( timeTillNextTitan >= file.ETA2MinLowerBound && player.p.replacementTitanETATimer > 120 )
+ {
+ //printt( "Waiting 0 till 2 min announcement" )
+ return 0 //Play 2 min ETA announcement immediately
+ }
+
+ if ( timeTillNextTitan >= file.ETA60sUpperBound && player.p.replacementTitanETATimer > 60 )
+ {
+ //printt( "Waiting " + ( timeTillNextTitan - file.ETA60sUpperBound ) + " till 60s announcement" )
+ return timeTillNextTitan - file.ETA60sUpperBound
+ }
+
+ if ( timeTillNextTitan >= file.ETA60sLowerBound && player.p.replacementTitanETATimer > 60 )
+ {
+ //printt( "Waiting 0 till 60s announcement" )
+ return 0
+ }
+
+ if ( timeTillNextTitan >= file.ETA30sUpperBound && player.p.replacementTitanETATimer > 30 )
+ {
+ //printt( "Waiting " + ( timeTillNextTitan - file.ETA30sUpperBound ) + " till 30s announcement" )
+ return timeTillNextTitan - file.ETA30sUpperBound
+ }
+
+ if ( timeTillNextTitan >= file.ETA30sLowerBound && player.p.replacementTitanETATimer > 30 )
+ {
+ //printt( "Waiting 0 till 30 announcement" )
+ return 0
+ }
+
+ if ( timeTillNextTitan >= file.ETA15sUpperBound && player.p.replacementTitanETATimer > 15 )
+ {
+ //printt( "Waiting " + ( timeTillNextTitan - file.ETA15sUpperBound ) + " till 15s announcement" )
+ return timeTillNextTitan - file.ETA15sUpperBound
+ }
+
+ if ( timeTillNextTitan >= file.ETA15sLowerBound && player.p.replacementTitanETATimer > 15 )
+ {
+ //printt( "Waiting 0 till 15s announcement" )
+ return 0
+ }
+
+ //printt( "Waiting " + timeTillNextTitan + " till next Titan" )
+ return timeTillNextTitan
+
+
+}
+
+function TryETATitanReadyAnnouncement( entity player )
+{
+ //printt( "TryETATitanReadyAnnouncement" )
+ if ( !IsAlive( player ) )
+ return
+
+ if ( GetPlayerTitanInMap( player ) )
+ return
+
+ if ( player.GetNextTitanRespawnAvailable() < 0 )
+ return
+
+ if ( GetGameState() > eGameState.SuddenDeath )
+ return
+
+ if ( GameTime_PlayingTime() < 5.0 )
+ return
+
+ local timeTillNextTitan = player.GetNextTitanRespawnAvailable() - Time()
+ //printt( "TryETATitanReadyAnnouncement timetillNextTitan: " + timeTillNextTitan )
+ if ( floor(timeTillNextTitan) <= 0 )
+ {
+ //Titan is ready, let TryReplacementTitanReadyAnnouncement take care of it
+ TryReplacementTitanReadyAnnouncement( player )
+ return
+ }
+
+ //This entire loop is probably too complicated now for what it's doing. Simplify next game!
+ //Loop might be pretty hard to read, a particular iteration of the loop is written in comments below
+ for ( int i = 0; i < file.ETATimeThresholds.len(); ++i )
+ {
+ if ( fabs( timeTillNextTitan - file.ETATimeThresholds[ i ] ) < file.ETAAnnouncementAllowanceTime )
+ {
+ if ( player.p.replacementTitanETATimer > file.ETATimeThresholds[ i ] )
+ {
+ if ( player.titansBuilt )
+ PlayConversationToPlayer( "TitanReplacementETA" + file.ETATimeThresholds[ i ] + "s" , player )
+ else
+ PlayConversationToPlayer( "FirstTitanETA" + file.ETATimeThresholds[ i ] + "s", player )
+
+ player.p.replacementTitanETATimer = float ( file.ETATimeThresholds[ i ] )
+ wait timeTillNextTitan - file.ETATimeThresholds[ i ]
+ if ( IsAlive( player ) )
+ SetTitanRespawnTimer( player, player.GetNextTitanRespawnAvailable() - Time() )
+ return
+ }
+ }
+ }
+
+ /*if ( fabs( timeTillNextTitan - 120 ) < ETAAnnouncementAllowanceTime && player.p.replacementTitanETATimer > 120 )
+ {
+ if ( player.titansBuilt )
+ PlayConversationToPlayer( "TitanReplacementETA120s", player )
+ else
+ PlayConversationToPlayer( "FirstTitanETA120s", player )
+ player.p.replacementTitanETATimer = 120
+ wait timeTillNextTitan - 120
+ SetTitanRespawnTimer( player, player.GetNextTitanRespawnAvailable() - Time() )
+ return
+ }
+ */
+
+}
+
+function TryReplacementTitanReadyAnnouncement( entity player )
+{
+ while( true )
+ {
+ //printt( "TryReplacementTitanReadyAnnouncementLoop" )
+ if ( !IsAlive( player ) )
+ return
+
+ if ( GetGameState() > eGameState.SuddenDeath )
+ return
+
+ if ( GetPlayerTitanInMap( player ) )
+ return
+
+ if ( level.nv.titanDropEnabledForTeam != TEAM_BOTH && level.nv.titanDropEnabledForTeam != player.GetTeam() )
+ return
+
+ if ( player.p.replacementTitanReady_lastNagTime == 0 || Time() - player.p.replacementTitanReady_lastNagTime >= nagInterval )
+ {
+ //Don't play Titan Replacement Announcements if you don't have it ready
+ switch ( Riff_TitanAvailability() )
+ {
+ case eTitanAvailability.Default:
+ break
+
+ default:
+ if ( !Riff_IsTitanAvailable( player ) )
+ return
+ }
+
+ if ( player.titansBuilt )
+ {
+ PlayConversationToPlayer( "TitanReplacementReady", player )
+ }
+ else
+ {
+ PlayConversationToPlayer( "FirstTitanReady", player )
+ }
+ player.p.replacementTitanReady_lastNagTime = Time()
+ }
+
+ wait 5.0 // Once every 5 seconds should be fine
+ }
+}
+
+void function ResetTitanReplacementAnnouncements( entity soul, var damageInfo )
+{
+ entity player = soul.GetBossPlayer()
+
+ if ( !IsValid( player ) )
+ return
+
+ player.p.replacementTitanETATimer = expect float( level.nv.gameEndTime )
+}
+
+function req()
+{
+ ReplacementTitan( GetPlayerArray()[0] )
+}
+
+bool function ClientCommand_RequestTitan( entity player, array<string> args )
+{
+ ReplacementTitan( player ) //Separate function because other functions will call ReplacementTitan
+ return true
+}
+
+// This a baseline titan request function; the only things that prevent this from happening are
+// common cases; wrong gamestate, already has a titan, is currently dead, etc...
+bool function RequestTitan( entity player )
+{
+ if ( !IsReplacementTitanAvailableForGameState() )
+ return false
+
+ if ( player.IsTitan() )
+ return false
+
+ if ( IsAlive( player.GetPetTitan() ) )
+ return false
+
+ if ( player.isSpawning )
+ return false
+
+ if ( !IsAlive( player ) )
+ return false
+
+ Point spawnPoint = GetTitanReplacementPoint( player, false )
+ local origin = spawnPoint.origin
+ Assert( origin )
+
+ //Check titanfall request against any custom gamemode rules
+ if ( !file.RequestTitanGamemodeRules( player, spawnPoint.origin ) )
+ return false
+
+ //if ( ShouldDoTitanfall() )
+ thread CreateTitanForPlayerAndHotdrop( player, spawnPoint )
+ //else
+ // thread ForcePilotToBecomeTitan( player )
+
+ return true
+}
+
+bool function ReplacementTitan( entity player )
+{
+ if ( !IsAlive( player ) )
+ {
+ printt( "ReplacementTitan", player, player.entindex(), "failed", "IsAlive( player ) was false" )
+ return false
+ }
+
+ if ( !IsReplacementTitanAvailable( player, 0 ) )
+ {
+ printt( "ReplacementTitan", player, player.entindex(), "failed", "IsReplacementTitanAvailable was false" )
+ return false
+ }
+
+ entity titan = GetPlayerTitanInMap( player )
+ if ( IsAlive( titan ) )
+ {
+ printt( "ReplacementTitan", player, player.entindex(), "failed", "GetPlayerTitanInMap was true" )
+ return false
+ }
+
+ if ( player in file.warpFallDebounce )
+ {
+ if ( Time() - file.warpFallDebounce[ player ] < 3.0 )
+ {
+ printt( "ReplacementTitan", player, player.entindex(), "failed", "player in file.warpFallDebounce was true" )
+ return false
+ }
+ }
+
+ Point spawnPoint = GetTitanReplacementPoint( player, false )
+ local origin = spawnPoint.origin
+ Assert( origin )
+
+ #if MP
+ PIN_PlayerAbility( player, "titanfall", "titanfall", {pos = origin} )
+ #endif
+
+ //Check titanfall request against any custom gamemode rules
+ if ( !file.RequestTitanGamemodeRules( player, spawnPoint.origin ) )
+ return false
+
+ #if SP
+ thread CreateTitanForPlayerAndHotdrop( player, spawnPoint )
+ #endif
+
+ #if MP
+ if ( ShouldDoTitanfall() )
+ thread CreateTitanForPlayerAndHotdrop( player, spawnPoint )
+ else
+ thread ForcePilotToBecomeTitan( player )
+ #endif
+
+ return true
+}
+
+#if MP
+
+void function ForcePilotToBecomeTitan( entity player )
+{
+ float fadeTime = 0.5
+ float holdTime = 2.0
+
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnDestroy" )
+
+ if ( GAMETYPE != SST )
+ {
+ #if FACTION_DIALOGUE_ENABLED
+ PlayFactionDialogueToPlayer( "mp_titanInbound" , player )
+ #else
+ if ( player.titansBuilt )
+ PlayConversationToPlayer( "TitanReplacement", player )
+ else
+ PlayConversationToPlayer( "FirstTitanInbound", player )
+ #endif
+ }
+
+ player.Signal( "RodeoOver" )
+ player.Signal( "ScriptAnimStop" )
+
+ table<string,bool> e = {}
+ e.settingsRestored <- false
+
+ OnThreadEnd(
+ function() : ( player, e )
+ {
+ if ( IsValid( player ) && !e.settingsRestored )
+ {
+ Rodeo_Allow( player )
+ player.Show()
+ player.MakeVisible()
+ }
+ }
+ )
+ Rodeo_Disallow( player )
+
+ ScreenFadeToBlack( player, fadeTime, holdTime )
+ player.DissolveNonLethal( ENTITY_DISSOLVE_CORE, Vector( 0, 0, 0 ), 500 )
+
+ wait fadeTime
+ player.SetInvulnerable()
+ player.Hide()
+
+ wait holdTime
+ ScreenFadeFromBlack( player, 1.0, 0.5 )
+ waitthread TitanPlayerHotDropsIntoLevel( player )
+ e.settingsRestored = true
+ Rodeo_Allow( player )
+ player.Show()
+ player.MakeVisible()
+ player.ClearInvulnerable()
+}
+#endif
+
+bool function IsReplacementDropInProgress( entity player )
+{
+ return expect bool( player.s.replacementDropInProgress )
+}
+
+void function CreateTitanForPlayerAndHotdrop( entity player, Point spawnPoint, TitanLoadoutDef ornull overrideLoadout = null )
+{
+ Assert( IsValid( player ) )
+
+ if ( player.isSpawning )
+ {
+ printt( "CreateTitanForPlayerAndHotdrop", player, player.entindex(), "failed", "player.isSpawning was true" )
+ return
+ }
+
+ if ( player.s.replacementDropInProgress )
+ {
+ printt( "CreateTitanForPlayerAndHotdrop", player, player.entindex(), "failed", "player.s.replacementDropInProgress was true" )
+ return
+ }
+
+ player.s.replacementDropInProgress = true
+
+ entity titanFallDisablingEntity = CreateInfoTarget()
+
+ OnThreadEnd(
+ function() : ( player, titanFallDisablingEntity )
+ {
+ if ( IsValid( titanFallDisablingEntity ) ) //As a fail safe. Should have been cleaned up in OnThreadEnd of CleanupTitanFallDisablingEntity
+ titanFallDisablingEntity.Destroy()
+
+ if ( !IsValid( player ) )
+ return
+
+ player.s.replacementDropInProgress = false
+ player.ClearHotDropImpactTime()
+ }
+ )
+
+ player.EndSignal( "OnDestroy" )
+
+ if ( GAMETYPE != SST )
+ {
+ #if FACTION_DIALOGUE_ENABLED
+ PlayFactionDialogueToPlayer( "mp_titanInbound" , player )
+ #else
+ if ( player.titansBuilt )
+ PlayConversationToPlayer( "TitanReplacement", player )
+ else
+ PlayConversationToPlayer( "FirstTitanInbound", player )
+ #endif
+ }
+
+ vector origin = spawnPoint.origin
+ vector angles
+ if ( spawnPoint.angles != < 0.0, 0.0, 0.0 > )
+ angles = spawnPoint.angles
+ else
+ angles = VectorToAngles( FlattenVector( player.GetViewVector() ) * -1 ) // face the player
+
+ printt( "Dropping replacement titan at " + origin + " with angles " + angles )
+
+ #if HAS_STATS
+ UpdatePlayerStat( player, "misc_stats", "titanFalls" )
+ #endif
+ #if SERVER && MP
+ PIN_AddToPlayerCountStat( player, "titanfalls" )
+ #endif
+
+ if ( !level.firstTitanfall )
+ {
+ AddPlayerScore( player, "FirstTitanfall", player )
+
+ #if HAS_STATS
+ UpdatePlayerStat( player, "misc_stats", "titanFallsFirst" )
+ #endif
+
+ level.firstTitanfall = true
+ }
+ else
+ {
+ AddPlayerScore( player, "Titanfall", player )
+ }
+
+
+ player.Signal( "CalledInReplacementTitan" )
+
+ int playerTeam = player.GetTeam()
+
+ TryAnnounceTitanfallWarningToEnemyTeam( playerTeam, origin )
+
+ titanFallDisablingEntity.SetOrigin( origin )
+ DisableTitanfallForLifetimeOfEntityNearOrigin( titanFallDisablingEntity, origin, TITANHOTDROP_DISABLE_ENEMY_TITANFALL_RADIUS )
+
+ entity titan
+ string animation
+
+ string regularTitanfallAnim = "at_hotdrop_drop_2knee_turbo"
+
+ TitanLoadoutDef loadout
+ if ( overrideLoadout == null )
+ {
+ loadout = GetTitanLoadoutForPlayer( player )
+ }
+ else
+ {
+ loadout = expect TitanLoadoutDef( overrideLoadout )
+ }
+ bool hasWarpfall = loadout.passive3 == "pas_warpfall"
+ if ( hasWarpfall || Flag( "LevelHasRoof" ) )
+ {
+ ClearTitanAvailable( player ) //Normally this is done when the Titan is spawned, but for warpfall the Titan isn't spawned instaneously after requesting it.
+
+ file.warpFallDebounce[ player ] <- Time()
+ animation = "at_hotdrop_drop_2knee_turbo_upgraded"
+ string settings = loadout.setFile
+ asset model = GetPlayerSettingsAssetForClassName( settings, "bodymodel" )
+ Attachment warpAttach = GetAttachmentAtTimeFromModel( model, animation, "offset", origin, angles, 0 )
+
+ entity fakeTitan = CreatePropDynamic( model )
+ float impactTime = GetHotDropImpactTime( fakeTitan, animation )
+
+ float diff = 0.0
+
+ if ( !hasWarpfall ) // this means the level requested the warpfall
+ {
+ float regularImpactTime = GetHotDropImpactTime( fakeTitan, regularTitanfallAnim ) - (WARPFALL_SOUND_DELAY + WARPFALL_FX_DELAY)
+ diff = ( regularImpactTime - impactTime )
+ impactTime = regularImpactTime
+ }
+
+ fakeTitan.Kill_Deprecated_UseDestroyInstead()
+
+ local impactStartTime = Time()
+ impactTime += (WARPFALL_SOUND_DELAY + WARPFALL_FX_DELAY)
+ player.SetHotDropImpactDelay( impactTime )
+ Remote_CallFunction_Replay( player, "ServerCallback_ReplacementTitanSpawnpoint", origin.x, origin.y, origin.z, Time() + impactTime )
+
+ EmitDifferentSoundsAtPositionForPlayerAndWorld( "Titan_1P_Warpfall_CallIn", "Titan_3P_Warpfall_CallIn", origin, player, playerTeam )
+
+ wait diff
+
+ wait WARPFALL_SOUND_DELAY
+
+ // "Titan_1P_Warpfall_Start" - for first person warp calls, starting right on the button press
+ // "Titan_3P_Warpfall_Start" - for any 3P other player or NPC when they call in a warp, starting right on their button press
+ EmitSoundAtPositionOnlyToPlayer( playerTeam, origin, player, "Titan_1P_Warpfall_Start" )
+ EmitSoundAtPositionExceptToPlayer( playerTeam, origin, player, "Titan_3P_Warpfall_Start" )
+
+ PlayFX( TURBO_WARP_FX, warpAttach.position + Vector(0,0,-104), warpAttach.angle )
+
+ wait WARPFALL_FX_DELAY
+
+ titan = CreateAutoTitanForPlayer_FromTitanLoadout( player, loadout, origin, angles )
+ DispatchSpawn( titan )
+ thread PlayFXOnEntity( TURBO_WARP_COMPANY, titan, "offset" )
+ }
+ else
+ {
+ animation = regularTitanfallAnim
+
+ titan = CreateAutoTitanForPlayer_FromTitanLoadout( player, loadout, origin, angles )
+ DispatchSpawn( titan )
+
+ float impactTime = GetHotDropImpactTime( titan, animation )
+ player.SetHotDropImpactDelay( impactTime )
+ Remote_CallFunction_Replay( player, "ServerCallback_ReplacementTitanSpawnpoint", origin.x, origin.y, origin.z, Time() + impactTime )
+ }
+
+ SetActiveTitanLoadoutIndex( player, GetPersistentSpawnLoadoutIndex( player, "titan" ) )
+ #if MP
+ SetActiveTitanLoadout( player )
+ #endif
+ if ( player in file.warpFallDebounce )
+ delete file.warpFallDebounce[ player ]
+
+ titan.EndSignal( "OnDeath" )
+ Assert( IsAlive( titan ) )
+
+ // dont let AI titan get enemies while dropping. Don't do trigger checks
+ titan.SetEfficientMode( true )
+ titan.SetTouchTriggers( false )
+ titan.SetNoTarget( true )
+ titan.SetAimAssistAllowed( false )
+
+#if R1_VGUI_MINIMAP
+ thread PingMinimapDuringHotdrop( player, titan, origin )
+#endif
+
+ thread CleanupTitanFallDisablingEntity( titanFallDisablingEntity, titan ) //needs to be here after titan is created
+ waitthread PlayersTitanHotdrops( titan, origin, angles, player, animation ) //Note that this function returns after the titan has played the landing anim, not when the titan hits the ground
+
+ titan.SetEfficientMode( false )
+ titan.SetTouchTriggers( true )
+ titan.SetAimAssistAllowed( true )
+
+ player.Signal( "titan_impact" )
+
+ thread TitanNPC_WaitForBubbleShield_StartAutoTitanBehavior( titan )
+}
+
+void function CleanupTitanFallDisablingEntity( entity titanFallDisablingEntity, entity titan )
+{
+ titanFallDisablingEntity.EndSignal( "OnDestroy" ) //titanFallDisablingEntity can be destroyed multiple ways
+ titan.EndSignal( "ClearDisableTitanfall" ) //This is awkward, CreateBubbleShield() and OnHotDropImpact() signals this to deestroy CleanupTitanFallDisablingEntity
+ titan.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( titanFallDisablingEntity )
+ {
+ if( IsValid( titanFallDisablingEntity ) )
+ titanFallDisablingEntity.Destroy()
+
+ }
+ )
+
+ WaitForever()
+}
+
+void function DrawReplacementTitanLocation( entity player, vector origin, float delay )
+{
+ // have to keep resending this info because a dead player won't see it
+ player.EndSignal( "OnDestroy" )
+ float endTime = Time() + delay
+
+ for ( ;; )
+ {
+ if ( !IsAlive( player ) )
+ {
+ player.WaitSignal( "OnRespawned" )
+ continue
+ }
+
+ float remainingTime = endTime - Time()
+ if ( remainingTime <= 0 )
+ return
+
+ player.SetHotDropImpactDelay( remainingTime )
+ Remote_CallFunction_Replay( player, "ServerCallback_ReplacementTitanSpawnpoint", origin.x, origin.y, origin.z, Time() + remainingTime )
+ player.WaitSignal( "OnDeath" )
+ }
+}
+
+void function TryAnnounceTitanfallWarningToEnemyTeam( int team, vector origin )
+{
+ float innerDistance = TITANFALL_OUTER_RADIUS * TITANFALL_OUTER_RADIUS
+ float outerDistance = innerDistance * 4.0
+
+ array<entity> enemies = GetPlayerArrayOfEnemies( team )
+ foreach ( entity enemyPlayer in enemies )
+ {
+ float distSqr = DistanceSqr( origin, enemyPlayer.GetOrigin() )
+ if ( distSqr > outerDistance )
+ continue
+
+ if ( distSqr < innerDistance )
+ Remote_CallFunction_NonReplay( enemyPlayer, "ServerCallback_TitanFallWarning", true )
+ else
+ Remote_CallFunction_NonReplay( enemyPlayer, "ServerCallback_TitanFallWarning", false )
+ }
+}
+
+TitanSettings function GetTitanForPlayer( entity player )
+{
+ string ornull currentTitanSettings
+ array<string> currentTitanMods
+
+ if ( player.IsBot() )
+ {
+ string botTitanSettings = GetConVarString( "bot_titan_settings" )
+ array<string> legalLoadouts = GetAllowedTitanSetFiles()
+ if ( legalLoadouts.contains( botTitanSettings ) )
+ currentTitanSettings = botTitanSettings
+ else
+ currentTitanSettings = legalLoadouts.getrandom()
+ }
+
+ if ( currentTitanSettings == null )
+ {
+ TitanLoadoutDef loadout = GetTitanLoadoutForPlayer( player )
+ currentTitanSettings = loadout.setFile
+ foreach ( mod in loadout.setFileMods )
+ {
+ currentTitanMods.append( mod )
+ }
+ }
+
+ if ( DebugNewTitanModels() )
+ {
+ switch ( currentTitanSettings )
+ {
+ case "titan_atlas":
+ currentTitanSettings = "titan_medium_ajax"
+ break
+ case "titan_stryder":
+ currentTitanSettings = "titan_light_locust"
+ break
+ case "titan_ogre":
+ currentTitanSettings = "titan_heavy_ogre"
+ break
+ }
+ }
+
+ TitanSettings titanSettings
+ titanSettings.titanSetFile = expect string( currentTitanSettings )
+ titanSettings.titanSetFileMods = currentTitanMods
+ return titanSettings
+}
+
+Attachment function GetAttachmentAtTimeFromModel( asset model, string animation, string attachment, vector origin, vector angles, float time )
+{
+ entity dummy = CreatePropDynamic( model, origin, angles )
+ Attachment start = dummy.Anim_GetAttachmentAtTime( animation, attachment, time )
+ dummy.Destroy()
+ return start
+}
+
+#if R1_VGUI_MINIMAP
+function PingMinimapDuringHotdrop( player, titan, impactOrigin )
+{
+ expect entity( player )
+ expect entity( titan )
+
+ player.EndSignal( "titan_impact" )
+ player.EndSignal( "OnDestroy" )
+ titan.EndSignal( "OnDeath" )
+
+ titan.Minimap_Hide( TEAM_IMC, null )
+ titan.Minimap_Hide( TEAM_MILITIA, null )
+
+ OnThreadEnd(
+ function() : ( player, titan )
+ {
+ if ( !IsAlive( titan ) )
+ return
+
+ titan.Minimap_DisplayDefault( TEAM_IMC, null )
+ titan.Minimap_DisplayDefault( TEAM_MILITIA, null )
+ }
+ )
+
+ while ( true )
+ {
+ Minimap_CreatePingForPlayer( player, impactOrigin, $"vgui/HUD/threathud_titan_friendlyself", 0.5 )
+ wait 0.4
+ }
+}
+#endif
+
+function EmptyTitanPlaysAnim( titan )
+{
+ local idleAnimAlias = "at_atlas_getin_idle"
+ if ( titan.HasKey( "idleAnim" ) )
+ idleAnimAlias = titan.GetValueForKey( "idleAnim" )
+
+ thread PlayAnim( titan, idleAnimAlias )
+}
+
+function FreeSpawnpointOnEnterTitan( spawnpoint, titan )
+{
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "TitanEntered" )
+
+ OnThreadEnd(
+ function() : ( spawnpoint, titan )
+ {
+ Assert( IsValid( titan ) )
+ spawnpoint.e.spawnPointInUse = false
+ }
+ )
+
+ titan.WaitSignal( "TitanBeingEntered" )
+}
+
+
+function DebugText( origin, text, time )
+{
+ local endTime = Time() + time
+
+ while( Time() < endTime )
+ {
+ DebugDrawText( origin, text, true, 1.0 )
+ wait 1
+ }
+}
+
+
+
+bool function ReplacementTitanTimerFinished( player, timeBuffer = 0 )
+{
+ local nextTitanTime = player.GetNextTitanRespawnAvailable()
+ if ( nextTitanTime < 0 )
+ return false
+
+ return nextTitanTime - Time() <= timeBuffer
+}
+
+
+struct
+{
+ float titanTimerPauseTime = 0
+ table<entity, float> playerPauseStartTimes
+
+} protoFile
+
+
+void function PauseTitansThink()
+{
+ bool titan
+ while ( true )
+ {
+ array<entity> players = GetPlayerArray()
+
+ bool foundTitan = false
+ foreach ( player in players )
+ {
+ if ( player.IsTitan() || IsValid( player.GetPetTitan() ) )
+ {
+ foundTitan = true
+ break
+ }
+ }
+
+ if ( foundTitan && protoFile.titanTimerPauseTime == 0 )
+ thread PauseTitanTimers()
+ else if ( !foundTitan && protoFile.titanTimerPauseTime != 0 )
+ thread PauseTitanTimers()
+
+ WaitFrame()
+ }
+}
+
+
+void function PauseTitanTimers()
+{
+ RegisterSignal( "PauseTitanTimers" )
+ svGlobal.levelEnt.Signal( "PauseTitanTimers" )
+ svGlobal.levelEnt.EndSignal( "PauseTitanTimers" )
+
+ if ( protoFile.titanTimerPauseTime != 0 )
+ {
+ protoFile.playerPauseStartTimes = {}
+ protoFile.titanTimerPauseTime = 0
+ return
+ }
+
+ protoFile.titanTimerPauseTime = Time()
+ float lastTime = Time()
+
+ while ( true )
+ {
+ array<entity> players = GetPlayerArray()
+
+ float addTime = Time() - protoFile.titanTimerPauseTime
+
+ foreach ( player in players )
+ {
+ if ( player.IsTitan() )
+ {
+ if ( player in protoFile.playerPauseStartTimes )
+ delete protoFile.playerPauseStartTimes[player]
+
+ continue
+ }
+
+ if ( IsValid( player.GetPetTitan() ) )
+ {
+ if ( player in protoFile.playerPauseStartTimes )
+ delete protoFile.playerPauseStartTimes[player]
+
+ continue
+ }
+
+ if ( Time() > player.GetNextTitanRespawnAvailable() )
+ {
+ if ( player in protoFile.playerPauseStartTimes )
+ delete protoFile.playerPauseStartTimes[player]
+
+ continue
+ }
+
+ if ( !(player in protoFile.playerPauseStartTimes) )
+ {
+ protoFile.playerPauseStartTimes[player] <- player.GetNextTitanRespawnAvailable()
+ }
+
+ protoFile.playerPauseStartTimes[player] += Time() - lastTime
+
+ player.SetNextTitanRespawnAvailable( protoFile.playerPauseStartTimes[player] )
+ }
+
+ lastTime = Time()
+ wait 0.1
+ }
+}
+
+bool function ShouldDoTitanfall()
+{
+ if ( svGlobal.forceDisableTitanfalls )
+ return false
+
+ return ( GetCurrentPlaylistVarInt( "enable_titanfalls", 1 ) == 1 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan/_replacement_titans_drop.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan/_replacement_titans_drop.gnut
new file mode 100644
index 00000000..5970f7ea
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan/_replacement_titans_drop.gnut
@@ -0,0 +1,443 @@
+global function ReplacementTitansDrop_Init
+global function GetTitanReplacementPoint
+global function HullTraceDropPoint
+global function DebugTitanSpawn
+global function TitanFindDropNodes
+global function TitanHulldropSpawnpoint
+
+global const TITANDROP_LOS_DIST = 2000 // 2D distance at which we do the line of sight check to see where the player wants to call in the titan
+global const TITANDROP_MIN_FOV = 10
+global const TITANDROP_MAX_FOV = 80
+global const TITANDROP_FOV_PENALTY = 8
+global const TITANDROP_PATHNODESEARCH_EXACTDIST = 500 // within this distance, we use the position the player is looking for the pathnode search
+global const TITANDROP_PATHNODESEARCH_DISTFRAC = 0.8 // beyond that distance, we use this fraction of how far the player is looking.
+global const TITANDROP_GROUNDSEARCH_ZDIR = -1.0 // if the player's not looking at anything, we search downward for ground at this slope
+global const TITANDROP_GROUNDSEARCH_FORWARDDIST = 350 // if the player's not looking at anything, we search for ground starting this many units in front of the player
+global const TITANDROP_GROUNDSEARCH_DIST = 1000 // if the player's not looking at anything, we search for ground this many units forward (max)
+global const TITANDROP_FALLBACK_DIST = 150 // if the ground search hits, we go this many units forward from it
+
+struct
+{
+ int replacementSpawnpointsID
+} file
+
+void function ReplacementTitansDrop_Init()
+{
+ AddSpawnCallback( "info_spawnpoint_titan", AddDroppoint )
+ AddSpawnCallback( "info_spawnpoint_titan_start", AddDroppoint )
+ AddSpawnCallback( "info_replacement_titan_spawn", AddDroppoint )
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+ file.replacementSpawnpointsID = CreateScriptManagedEntArray()
+}
+
+void function EntitiesDidLoad()
+{
+}
+
+
+void function AddDroppoint( entity ent )
+{
+ AddToScriptManagedEntArray( file.replacementSpawnpointsID, ent )
+}
+
+void function DebugTitanSpawn()
+{
+ thread DebugTitanSpawnThread()
+}
+
+void function DebugTitanSpawnThread()
+{
+ entity player = GetPlayerArray()[0]
+
+ float interval = 0.1
+
+ FlightPath flightPath = GetAnalysisForModel( GetFlightPathModel( "fp_titan_model" ), HOTDROP_TURBO_ANIM )
+ int dataIndex = GetAnalysisDataIndex( flightPath )
+
+ for ( ;; )
+ {
+ if ( !IsValid( player ) )
+ {
+ wait interval
+ continue
+ }
+
+ vector playerOrg = player.GetOrigin()
+ vector playerEyeForward = player.GetViewVector()
+ vector playerEyePos = player.EyePosition()
+ vector playerEyeAngles = player.EyeAngles()
+ float yaw = playerEyeAngles.y
+ vector ornull desiredPos = GetReplacementTrace( playerEyePos, playerEyeForward )
+ vector pathNodeSearchPos
+ if ( desiredPos == null )
+ {
+ pathNodeSearchPos = GetPathNodeSearchPos( playerOrg, playerEyePos, playerEyeForward, true )
+ }
+ else
+ {
+ expect vector( desiredPos )
+ DebugDrawCircle( desiredPos, Vector(0,0,0), 10, 128, 255, 128, true, interval )
+ DebugDrawText( desiredPos + Vector(0,0,60), "Looking here", false, interval )
+ pathNodeSearchPos = GetPathNodeSearchPosWithLookPos( playerOrg, playerEyePos, playerEyeForward, desiredPos, true )
+ }
+
+ DebugDrawCircle( pathNodeSearchPos, Vector(0,0,0), 10, 128, 128, 255, true, interval )
+ DebugDrawText( pathNodeSearchPos + Vector(0,0,40), "Searching from here", false, interval )
+
+ DebugDrawLine( playerOrg, playerOrg + AnglesToForward( Vector( 0, yaw - TITANDROP_MIN_FOV, 0 ) ) * 500, 200, 200, 200, true, interval )
+ DebugDrawLine( playerOrg, playerOrg + AnglesToForward( Vector( 0, yaw + TITANDROP_MIN_FOV, 0 ) ) * 500, 200, 200, 200, true, interval )
+ DebugDrawLine( playerOrg, playerOrg + AnglesToForward( Vector( 0, yaw - TITANDROP_MAX_FOV, 0 ) ) * 500, 128, 128, 128, true, interval )
+ DebugDrawLine( playerOrg, playerOrg + AnglesToForward( Vector( 0, yaw + TITANDROP_MAX_FOV, 0 ) ) * 500, 128, 128, 128, true, interval )
+
+ int node = GetBestNodeForPosInWedge( pathNodeSearchPos, playerEyePos, yaw, TITANDROP_MIN_FOV, TITANDROP_MAX_FOV, TITANDROP_FOV_PENALTY, dataIndex, /*ANALYSIS_STEPS*/ 8 )
+
+ if ( node >= 0 )
+ {
+ Assert( NodeHasFlightPath( dataIndex, node ) )
+
+ vector pos = GetNodePos( node )
+ DebugDrawCircle( pos, Vector(0,0,0), 25, 255, 255, 128, true, interval )
+ DebugDrawText( pos + Vector(0,0,20), "Best node", false, interval )
+ }
+
+ Point actualResult = GetTitanReplacementPoint( player, true )
+ vector actualPos = actualResult.origin
+ DebugDrawCircle( actualPos, Vector(0,0,0), 32, 255, 255, 255, true, interval )
+ DebugDrawLine( actualPos, actualPos + AnglesToForward( actualResult.angles ) * 40, 255, 255, 255, true, interval )
+ DebugDrawText( actualPos, "Final location", false, interval )
+
+ wait interval
+ }
+}
+
+Point function GetTitanReplacementPoint( entity player, bool forDebugging = false )
+{
+ vector playerEyePos = player.EyePosition()
+ vector playerEyeAngles = player.EyeAngles()
+ vector playerOrg = player.GetOrigin()
+
+ return CalculateTitanReplacementPoint( playerOrg, playerEyePos, playerEyeAngles, forDebugging )
+}
+
+Point function CalculateTitanReplacementPoint( vector playerOrg, vector playerEyePos, vector playerEyeAngles, bool forDebugging = false )
+{
+ //local playerEyePos = Vector(-281.036224, 34.857925, 860.031250)
+ //local playerEyeAngles = Vector(60.055622, 80.775780, 0.000000)
+ //local playerOrg = Vector(-281.036224, 34.857925, 800.031250)
+
+ if ( !forDebugging )
+ printt( "Requested replacement Titan from eye pos " + playerEyePos + " view angles " + playerEyeAngles + " player origin " + playerOrg + " map " + GetMapName() )
+
+ vector playerEyeForward = AnglesToForward( playerEyeAngles )
+
+ // use the flightPath to find a position
+ FlightPath flightPath = GetAnalysisForModel( GetFlightPathModel( "fp_titan_model" ), HOTDROP_TURBO_ANIM )
+ int dataIndex = GetAnalysisDataIndex( flightPath )
+
+ var dropPoint
+ vector ornull traceOrigin = GetReplacementTrace( playerEyePos, playerEyeForward )
+ bool traceOriginIsNull = traceOrigin == null
+
+ if ( !traceOriginIsNull )
+ {
+ expect vector( traceOrigin )
+
+ dropPoint = TitanHulldropSpawnpoint( flightPath, traceOrigin, 0 )
+ if ( dropPoint != null && !NearTitanfallBlocker( dropPoint ) )
+ {
+ expect vector( dropPoint )
+ if ( EdgeTraceDropPoint( dropPoint ) )
+ {
+ if ( SafeForTitanFall( dropPoint ) && TitanTestDropPoint( dropPoint, flightPath ) )
+ {
+ vector yawVec = playerEyePos - dropPoint
+ vector yawAngles = VectorToAngles( yawVec )
+ yawAngles.x = 0
+ yawAngles.z = 0
+ // add some randomness
+ yawAngles.y += RandomFloatRange( -60, 60 )
+ if ( yawAngles.y < 0 )
+ yawAngles.y += 360
+ else if ( yawAngles.y > 360 )
+ yawAngles.y -= 360
+
+ Point point
+ point.origin = dropPoint
+ point.angles = yawAngles
+ return point
+ }
+ }
+ }
+ }
+
+ vector pathNodeSearchPos
+ if ( !traceOriginIsNull )
+ {
+ expect vector( traceOrigin )
+ pathNodeSearchPos = GetPathNodeSearchPosWithLookPos( playerOrg, playerEyePos, playerEyeForward, traceOrigin, false )
+ }
+ else
+ {
+ pathNodeSearchPos = GetPathNodeSearchPos( playerOrg, playerEyePos, playerEyeForward, false )
+ }
+
+ int node = GetBestNodeForPosInWedge( pathNodeSearchPos, playerEyePos, playerEyeAngles.y, TITANDROP_MIN_FOV, TITANDROP_MAX_FOV, TITANDROP_FOV_PENALTY, dataIndex, /*ANALYSIS_STEPS*/ 8 )
+
+ if ( node < 0 )
+ {
+ // This won't ever happen on a map with any reasonably placed path nodes.
+ entity spawner = FindSpawnpoint_ForReplacementTitan( playerOrg )
+ Assert( spawner )
+ Point point
+ point.origin = spawner.GetOrigin()
+ return point
+ }
+
+ Assert( NodeHasFlightPath( dataIndex, node ) )
+
+ vector nodeOrigin = GetNodePos( node )
+ vector dir = nodeOrigin - playerEyePos
+ vector angles = VectorToAngles( dir )
+ float yaw = angles.y + 180
+
+ if ( yaw < 0 )
+ yaw += 360
+ else if ( yaw > 360 )
+ yaw -= 360
+
+ var yawResult = GetSpawnPoint_ClosestYaw( node, dataIndex, yaw, 360.0 )
+ Assert( yawResult != null )
+ yaw = expect float( yawResult )
+ Assert( yaw >= 0 )
+ Assert( yaw <= 360 )
+
+ Point point
+ point.origin = nodeOrigin
+ point.angles = Vector( 0, yaw, 0 )
+ return point
+}
+
+vector function GetPathNodeSearchPosWithLookPos( vector playerOrg, vector playerEyePos, vector playerEyeForward, vector playerLookPos, bool debug )
+{
+ float dist2DSqr = Distance2DSqr( playerOrg, playerLookPos )
+ if ( dist2DSqr > (TITANDROP_PATHNODESEARCH_EXACTDIST / TITANDROP_PATHNODESEARCH_DISTFRAC) * (TITANDROP_PATHNODESEARCH_EXACTDIST / TITANDROP_PATHNODESEARCH_DISTFRAC) )
+ {
+ return playerOrg + (playerLookPos - playerOrg) * TITANDROP_PATHNODESEARCH_DISTFRAC
+ }
+ else if ( dist2DSqr > TITANDROP_PATHNODESEARCH_EXACTDIST * TITANDROP_PATHNODESEARCH_EXACTDIST )
+ {
+ vector dir = Normalize( playerLookPos - playerOrg )
+ return playerOrg + dir * TITANDROP_PATHNODESEARCH_EXACTDIST
+ }
+ else
+ {
+ return playerLookPos
+ }
+
+ unreachable
+}
+
+vector function GetPathNodeSearchPos( vector playerOrg, vector playerEyePos, vector playerEyeForward, bool debug )
+{
+ vector diagonallyDown = Normalize( <playerEyeForward.x, playerEyeForward.y, 0> )
+ diagonallyDown.z = TITANDROP_GROUNDSEARCH_ZDIR
+
+ vector startPos = playerEyePos + playerEyeForward * TITANDROP_GROUNDSEARCH_FORWARDDIST
+ vector endPos = startPos + diagonallyDown * TITANDROP_GROUNDSEARCH_DIST
+
+ TraceResults result = TraceLine( startPos, endPos, null, TRACE_MASK_SOLID_BRUSHONLY, TRACE_COLLISION_GROUP_NONE )
+
+ if ( debug )
+ {
+ DebugDrawLine( playerEyePos, startPos, 128,128,200, true, 0.1 )
+ DebugDrawLine( startPos, result.endPos, 128,128,200, true, 0.1 )
+ if ( result.fraction < 1 )
+ DebugDrawLine( result.endPos, result.endPos + playerEyeForward * TITANDROP_FALLBACK_DIST, 128,128,200, true, 0.1 )
+ }
+
+ if ( result.fraction < 1 )
+ return result.endPos + playerEyeForward * TITANDROP_FALLBACK_DIST
+
+ return playerEyePos + playerEyeForward * TITANDROP_FALLBACK_DIST
+}
+
+// Returns a position vector or null
+vector ornull function GetReplacementTrace( vector startPos, vector viewVector )
+{
+ float viewDirLen2D = Length2D( viewVector )
+ if ( viewDirLen2D < 0.1 )
+ viewDirLen2D = 0.1
+
+ vector endPos = startPos + ( viewVector * ( TITANDROP_LOS_DIST / viewDirLen2D ) )
+ int mask = TRACE_MASK_SOLID & (~CONTENTS_WINDOW)
+ TraceResults result = TraceLine( startPos, endPos, null, mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( result.endPos, endPos, 255, 0, 0, true, 20.0 )
+ //DebugDrawLine( startPos, result.endPos, 0, 255, 0, true, 20.0 )
+
+ if ( result.fraction == 1 )
+ return null
+
+ entity hitEnt = result.hitEnt
+ if ( IsValid( hitEnt ) && ( hitEnt.IsTitan() || hitEnt.IsPlayer() || hitEnt.IsNPC() ) )
+ {
+ endPos = OriginToGround( hitEnt.GetOrigin() )
+ }
+ else
+ {
+ endPos = result.endPos
+
+ if ( result.surfaceNormal.Dot( <0.0, 0.0, 1.0> ) < 0.7 )
+ {
+ //DebugDrawLine( endPos, Vector(0,0,0), 0, 200, 0, true, 5.0 )
+ // pull it back towards player
+ float titanRadius = GetBoundsMax( HULL_TITAN ).x * 1.2
+ endPos -= viewVector * titanRadius
+ endPos += result.surfaceNormal * titanRadius
+
+ endPos = OriginToGround( endPos )
+ }
+ }
+
+ vector ornull clampedEndPos = NavMesh_ClampPointForHullWithExtents( endPos, HULL_TITAN, <160.0, 160.0, 80.0> )
+
+ if ( !clampedEndPos )
+ return null
+
+ expect vector( clampedEndPos )
+
+ vector dir = clampedEndPos - startPos
+ if ( DotProduct2D( dir, viewVector ) < 0 )
+ return null
+
+ return clampedEndPos
+}
+
+var function HullTraceDropPoint( FlightPath flightPath, vector baseOrigin, float heightCapMax = 190 )
+{
+ float heightCapMin = -512
+ vector startOrigin = baseOrigin + Vector( 0,0,1000 )
+ vector endOrigin = baseOrigin + Vector( 0,0, heightCapMin )
+
+ int mask = flightPath.traceMask
+
+ TraceResults result = TraceHull( startOrigin, endOrigin, flightPath.mins, flightPath.maxs, null, mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( startOrigin, result.endPos, 0, 255, 0, true, 5.0 )
+ //DebugDrawLine( result.endPos, endOrigin, 255, 0, 0, true, 5.0 )
+
+// DebugDrawLine( startOrigin, baseOrigin, 0, 255, 0, true, 5.0 )
+// DebugDrawLine( baseOrigin, endOrigin, 255, 0, 0, true, 5.0 )
+// local offset = Vector(0.15, 0.15, 0.0 )
+// DebugDrawLine( startOrigin + offset, result.endPos + offset, 0, 255, 0, true, 5.0 )
+// DebugDrawLine( result.endPos + offset, endOrigin + offset, 255, 0, 0, true, 5.0 )
+// DrawArrow( baseOrigin, Vector(0,0,0), 5.0, 50 )
+// DebugDrawLine( result.endPos, baseOrigin, 255, 255, 255, true, 4.5 )
+
+/*
+ printt( " " )
+ printt( "Hull drop " )
+ printt( "start " + startOrigin )
+ printt( "end " + endOrigin )
+ printt( "hit " + result.endPos )
+ printt( "mins " + flightPath.mins + " maxs " + flightPath.maxs )
+ printt( "mask " + mask )
+*/
+ if ( result.allSolid || result.startSolid || result.hitSky )
+ return null
+
+ if ( result.fraction == 0 || result.fraction == 1 )
+ return null
+
+ if ( fabs( result.endPos.z - baseOrigin.z ) > heightCapMax )
+ return null
+
+ return result.endPos
+}
+
+
+entity function FindSpawnpoint_ForReplacementTitan( vector origin )
+{
+ Assert( GetScriptManagedEntArrayLen( file.replacementSpawnpointsID ) > 0 )
+
+ array<entity> spawnpoints = GetScriptManagedEntArray( file.replacementSpawnpointsID )
+ entity selectedSpawnpoint = spawnpoints[0]
+
+ float closestDist = -1
+ foreach ( spawnpoint in spawnpoints )
+ {
+ if ( spawnpoint.e.spawnPointInUse )
+ continue
+ if ( spawnpoint.IsOccupied() )
+ continue
+
+ float dist = DistanceSqr( spawnpoint.GetOrigin(), origin )
+ if ( closestDist == -1 || dist < closestDist )
+ {
+ closestDist = dist
+ selectedSpawnpoint = spawnpoint
+ }
+
+ }
+
+ Assert( selectedSpawnpoint )
+ return selectedSpawnpoint
+}
+
+bool function TitanFindDropNodes( FlightPath flightPath, vector baseOrigin, float yaw )
+{
+// return TitanFindDropNodesReloadable( flightPath, baseOrigin, yaw )
+//}
+//function TitanFindDropNodesReloadable( flightPath, baseOrigin, yaw )
+//{
+ if ( NearTitanfallBlocker( baseOrigin ) )
+ return false
+
+ asset model = flightPath.model
+ string animation = flightPath.anim
+ //local flightPath = GetAnalysisForModel( model, animation )
+
+ vector origin = baseOrigin
+ vector angles = Vector(0,yaw,0)
+ //entity titan = CreatePropDynamic( model, origin, Vector(0,0,0) )
+ //entity titan = CreateNPCTitanFromSettings( "titan_atlas", TEAM_IMC, origin, angles )
+
+ entity titan = expect entity( level.ainTestTitan )
+
+ titan.SetModel( model )
+ titan.SetAngles( angles )
+ titan.SetOrigin( origin )
+
+ float impactTime = GetHotDropImpactTime( titan, animation )
+ Attachment result = titan.Anim_GetAttachmentAtTime( animation, "OFFSET", impactTime )
+ vector maxs = titan.GetBoundingMaxs()
+ vector mins = titan.GetBoundingMins()
+ int mask = titan.GetPhysicsSolidMask()
+ origin = ModifyOriginForDrop( origin, mins, maxs, result.position, mask )
+ titan.SetOrigin( origin )
+
+ // Don't use nodes on top of the roof in kodai
+ if ( GetMapName() == "mp_forwardbase_kodai" && origin.z > 1200 )
+ return false
+
+ if ( !TitanTestDropPoint( origin, flightPath ) )
+ return false
+
+ if ( !TitanCanStand( titan ) )
+ return false
+
+ if ( TitanHulldropSpawnpoint( flightPath, origin, 0 ) == null )
+ return false
+
+ if ( !EdgeTraceDropPoint( origin ) )
+ return false
+
+ return true
+}
+
+
+var function TitanHulldropSpawnpoint( FlightPath flightPath, vector origin, float _ )
+{
+ return HullTraceDropPoint( flightPath, origin, 20 )
+}
+
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_commands.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_commands.gnut
new file mode 100644
index 00000000..06232c08
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_commands.gnut
@@ -0,0 +1,49 @@
+untyped
+
+global function TitanCommands_Init
+
+
+function TitanCommands_Init()
+{
+ if ( GetCurrentPlaylistVarInt( "titan_move_command_enabled", 0 ) == 0 )
+ return
+
+ AddClientCommandCallback( "PrototypeOrderTitanMove", Prototype_OrderTitanMove )
+ RegisterSignal( "Prototype_TitanMove" )
+}
+
+bool function Prototype_OrderTitanMove( entity player, array<string> args )
+{
+ Assert( args.len() == 3 )
+ vector pos = Vector( args[0].tofloat(), args[1].tofloat(), args[2].tofloat() )
+
+ DebugDrawLine( pos, pos + Vector(0,0,500), 255, 0, 0, true, 5.0 )
+ entity titan = player.GetPetTitan()
+ if ( !IsAlive( titan ) )
+ return true
+
+ thread Prototype_TitanMove( player, titan, pos )
+
+ return true
+}
+
+void function Prototype_TitanMove( entity player, entity titan, vector origin )
+{
+ titan.Signal( "Prototype_TitanMove" )
+ titan.EndSignal( "Prototype_TitanMove" )
+ titan.EndSignal( "ChangedTitanMode" )
+ titan.EndSignal( "OnDeath" )
+ local mode = player.GetPetTitanMode()
+ if ( mode != eNPCTitanMode.STAY ) // assuming there are only 2 modes
+ {
+ player.SetPetTitanMode( eNPCTitanMode.STAY )
+ titan.DisableBehavior( "Follow" )
+ #if R1_VGUI_MINIMAP
+ titan.Minimap_SetBossPlayerMaterial( $"vgui/HUD/threathud_titan_friendlyself_guard" )
+ #endif
+
+ titan.AssaultSetOrigin( origin )
+ }
+
+ AssaultOrigin( titan, origin, 100 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_health.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_health.gnut
new file mode 100644
index 00000000..d600cb03
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_health.gnut
@@ -0,0 +1,1072 @@
+global function TitanHealth_Init
+
+global function Titan_PlayerTookDamage
+global function Titan_NPCTookDamage
+
+global function GetShieldRegenTime
+global function GetShieldRegenDelay
+global function PlayerHasAutoEject
+global function SetTitanCoreTimer
+global function GetTitanCoreTimer
+
+global function AddCreditToTitanCoreBuilderForTitanDamageInflicted
+global function AddCreditToTitanCoreBuilderForTitanDamageReceived
+global function AddCreditToTitanCoreBuilderForDoomInflicted
+global function AddCreditToTitanCoreBuilderForDoomEntered
+global function AddCreditToTitanCoreBuilder
+
+global function TitanShieldRegenThink
+
+global function IsRodeoDamageFromBatteryPack
+global function IsKillshot
+
+global function DoomedHealthThink
+global function UndoomTitan
+global function RestoreTitan
+
+global const SIGNAL_TITAN_HEALTH_REGEN = "BeginTitanHealthRegen"
+global const SIGNAL_TITAN_SHIELD_REGEN = "BeginTitanShieldRegen"
+
+global const TITAN_HEALTH_REGEN_DELAY_MAX = 0.7 // 2.2
+
+#if MP
+// PROTO : Was 99, 49 is for test
+global const TITAN_REGEN_MIN_DAMAGE = 49
+global const TITAN_REGEN_MIN_DAMAGE_DELAY = 0.5
+#elseif SP
+global const TITAN_REGEN_MIN_DAMAGE = 70
+global const TITAN_REGEN_MIN_DAMAGE_DELAY = 0.5
+#endif
+
+// titan health system
+const TITAN_HEALTH_HISTORY_FALLOFF_START = 0 // how many seconds until shield begins to regen
+
+const float TITAN_HEALTH_HISTORY_FALLOFF_END = 4.0
+
+struct
+{
+ float earn_meter_titan_multiplier
+} file
+
+void function TitanHealth_Init()
+{
+ RegisterSignal( SIGNAL_TITAN_HEALTH_REGEN )
+ RegisterSignal( SIGNAL_TITAN_SHIELD_REGEN )
+ RegisterSignal( "Doomed" )
+ RegisterSignal( "TitanUnDoomed" )
+ RegisterSignal( "StopShieldRegen" )
+ RegisterSignal( "WeakTitanHealthInitialized" )
+
+ file.earn_meter_titan_multiplier = GetCurrentPlaylistVarFloat( "earn_meter_titan_multiplier", 1.0 )
+
+ if ( IsMenuLevel() )
+ return
+
+ HealthRegenInit()
+ AddSoulInitFunc( TitanShieldRegenThink ) //This runs even if playlist var titan_shield_regen is set to 0 because it also does stuff like give friendly Pilots protection with shield, etc
+ AddSoulDeathCallback( Titan_MonarchCleanup )
+}
+
+void function UndoomTitan( entity titan, int numSegments )
+{
+ entity soul = titan.GetTitanSoul()
+ string settings = GetSoulPlayerSettings( soul )
+
+ soul.DisableDoomed()
+ int maxHealth
+ int segmentHealth = GetSegmentHealthForTitan( titan )
+ if ( titan.IsNPC() )
+ {
+ maxHealth = int( GetPlayerSettingsFieldForClassName_Health( settings ) )
+ if ( titan.ai.titanSpawnLoadout.setFileMods.contains( "fd_health_upgrade" ) )
+ maxHealth += segmentHealth
+ if ( soul.soul.titanLoadout.setFileMods.contains( "core_health_upgrade" ) )
+ maxHealth += segmentHealth
+ }
+ else
+ {
+ maxHealth = int( titan.GetPlayerModHealth() )
+ }
+ titan.SetMaxHealth( maxHealth )
+ titan.SetHealth( segmentHealth * numSegments )
+ SetSoulBatteryCount( soul, numSegments )
+
+ titan.Signal( "TitanUnDoomed" )
+ UndoomTitan_Body( titan )
+ thread TitanShieldRegenThink( soul )
+}
+
+void function RestoreTitan( entity titan, float percent = 0.625 )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( soul.IsDoomed() )
+ UndoomTitan( titan, 1 )
+
+ soul.nextRegenTime = 0.0
+ soul.SetShieldHealth( soul.GetShieldHealthMax() )
+ int minHealth = int( titan.GetMaxHealth() * percent )
+ if ( titan.GetHealth() < minHealth )
+ {
+ titan.SetHealth( minHealth )
+ int segmentHealth = GetSegmentHealthForTitan( titan )
+ int segments = int( minHealth / float( segmentHealth ) )
+ SetSoulBatteryCount( soul, segments )
+ }
+}
+
+bool function IsRodeoDamage( entity soul, var damageInfo )
+{
+ entity titan = soul.GetTitan()
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !attacker.IsPlayer() )
+ {
+ entity rider = GetRodeoPilot( titan )
+ if ( rider == attacker )
+ return true
+ else
+ return false
+ }
+
+ if ( attacker.GetTitanSoulBeingRodeoed() != soul )
+ return false
+
+ return true
+}
+
+bool function IsCoopRodeoDamage( entity soul, var damageInfo )
+{
+ entity titan = soul.GetTitan()
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity rider = GetRodeoPilot( titan )
+ if ( rider == attacker )
+ return true
+ else
+ return false
+
+ unreachable
+}
+
+
+void function CheckRodeoRiderHitsTitan( entity soul, var damageInfo )
+{
+ if ( IsRodeoDamage( soul, damageInfo ) )
+ {
+ //Set Last Attack Time so warning is triggered
+ soul.SetLastRodeoHitTime( Time() )
+
+ DamageInfo_AddCustomDamageType( damageInfo, DF_RODEO )
+ }
+}
+
+bool function ShouldMultiplyRodeoDamage( var damageInfo )
+{
+ switch ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) )
+ {
+ case eDamageSourceId.mp_weapon_smr:
+ case eDamageSourceId.mp_titanability_smoke:
+ return false
+
+ case eDamageSourceId.mp_weapon_defender :
+ return true
+ }
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_EXPLOSION )
+ return false
+
+ return true
+}
+
+bool function IsRodeoDamageFromBatteryPack( entity soul, var damageInfo )
+{
+ if ( !IsRodeoDamage( soul, damageInfo ) )
+ return false
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) != damageTypes.rodeoBatteryRemoval )
+ return false
+
+ return true
+}
+
+
+int function ShieldHealthUpdate( entity titan, var damageInfo, bool critHit )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( DamageInfo_GetForceKill( damageInfo ) )
+ {
+ soul.SetShieldHealth( 0 )
+ return 0
+ }
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_BYPASS_SHIELD )
+ return 0
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+
+ Assert( soul == titan.GetTitanSoul() )
+ int shieldHealth = soul.GetShieldHealth()
+
+ if ( soul.e.forcedRegenTime <= Time() )
+ soul.nextRegenTime = CalculateNextRegenTime( damage, damageType, critHit, expect float( soul.nextRegenTime ), GetShieldRegenDelay( soul ) )
+
+ int result = 0
+ if ( shieldHealth )
+ {
+ DamageInfo_AddCustomDamageType( damageInfo, DF_SHIELD_DAMAGE )
+ result = int( ShieldModifyDamage( titan, damageInfo ) )
+ }
+ else
+ {
+ TakeAwayFriendlyRodeoPlayerProtection( titan )
+ }
+
+ return result
+}
+
+
+void function PlayerOrNPCTitanTookDamage( entity titan, var damageInfo, bool critHit, TitanDamage titanDamage )
+{
+ entity soul = titan.GetTitanSoul()
+
+ if ( !IsValid( soul ) ) //Defensive fix for transient times in frame where Titan can have no soul but be damaged, e.g. immediately after embark
+ return
+
+ // zero out small forces
+ if ( LengthSqr( DamageInfo_GetDamageForce( damageInfo ) ) < 30000 * 30000 )
+ DamageInfo_SetDamageForce( damageInfo, < 0, 0, 0 > )
+
+ titanDamage.shieldDamage = CheckSpecialCaseShieldDamage( soul, titan, damageInfo )
+ if ( titanDamage.shieldDamage < 0 )
+ {
+ CheckRodeoRiderHitsTitan( soul, damageInfo )
+ titanDamage.shieldDamage = ShieldHealthUpdate( titan, damageInfo, critHit )
+ }
+
+ HandleKillshot( titan, damageInfo, titanDamage )
+
+ // health regen based on how much damage dealt to titan
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ bool rodeoDamage = ( ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_RODEO ) > 0 )
+
+ if ( soul.e.forcedRegenTime <= Time() )
+ soul.nextHealthRegenTime = CalculateNextRegenTime( damage, damageType, critHit || rodeoDamage, expect float( soul.nextHealthRegenTime ), GetHealthRegenDelay( soul ) )
+}
+
+int function CheckSpecialCaseShieldDamage( entity soul, entity titan, var damageInfo )
+{
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == damagedef_suicide )
+ return 0
+
+ // no protection from doomed health loss
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return 0
+
+ if ( IsTitanWithinBubbleShield( titan ) || TitanHasBubbleShieldWeapon( titan ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return 0
+ }
+
+ return -1
+}
+
+void function Titan_NPCTookDamage( entity titan, var damageInfo, TitanDamage titanDamage )
+{
+ Assert( titan.IsTitan() )
+ Assert( DamageInfo_GetDamage( damageInfo ) > 0 )
+
+ // dead entities can take damage
+ if ( !IsAlive( titan ) )
+ return
+
+ entity soul = titan.GetTitanSoul()
+
+ if ( !IsValid( soul ) ) //Defensive fix for transient times in frame where Titan can have no soul but be damaged, e.g. immediately after embark
+ return
+
+ bool critHit = false
+ if ( CritWeaponInDamageInfo( damageInfo ) )
+ critHit = IsCriticalHit( DamageInfo_GetAttacker( damageInfo ), titan, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) )
+
+ if ( critHit )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( HeavyArmorCriticalHitRequired( damageInfo ) && CritWeaponInDamageInfo( damageInfo ) && !critHit && IsValid( attacker ) && !attacker.IsTitan())
+ {
+ float shieldHealth = float( titan.GetTitanSoul().GetShieldHealth() )
+ float damage = DamageInfo_GetDamage( damageInfo )
+ if ( shieldHealth - damage <= 0 )
+ {
+ if ( shieldHealth > 0 )
+ DamageInfo_SetDamage( damageInfo, shieldHealth )
+ else
+ DamageInfo_SetDamage( damageInfo, 0 )
+ }
+ }
+
+ PlayerOrNPCTitanTookDamage( titan, damageInfo, critHit, titanDamage )
+
+ RecordDamageToNPCTitanSoul( soul, damageInfo )
+
+ entity owner = GetPetTitanOwner( titan )
+ if ( IsValid( owner ) )
+ AutoTitan_TryMultipleTitanCallout( titan, damageInfo )
+
+ if ( GetDoomedState( titan ) )
+ titanDamage.shieldDamage = 0
+}
+
+void function Titan_PlayerTookDamage( entity player, var damageInfo, entity attacker, bool critHit, TitanDamage titanDamage )
+{
+ Assert( player.IsTitan() )
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ if ( !IsAlive( player ) )
+ return
+
+ entity soul = player.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Defensive fix for transient times in frame where Titan can have no soul but be damaged, e.g. immediately after embark
+ return
+
+ if ( damage > 0 )
+ AdjustVelocityFromHit( player, damageInfo, attacker, damage, critHit )
+
+ if ( IsDemigod( player ) )
+ EntityDemigod_TryAdjustDamageInfo( player, damageInfo )
+
+ bool critHit = false
+ if ( CritWeaponInDamageInfo( damageInfo ) )
+ critHit = IsCriticalHit( attacker, player, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) )
+
+ if ( critHit )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+
+ #if MP
+ if ( HeavyArmorCriticalHitRequired( damageInfo ) && CritWeaponInDamageInfo( damageInfo ) && !critHit && IsValid( attacker ) && !attacker.IsTitan())
+ {
+ float shieldHealth = float( player.GetTitanSoul().GetShieldHealth() )
+ if ( shieldHealth - damage <= 0 )
+ {
+ if ( shieldHealth > 0 )
+ DamageInfo_SetDamage( damageInfo, shieldHealth )
+ else
+ DamageInfo_SetDamage( damageInfo, 0 )
+ }
+ }
+ #endif
+
+ PlayerOrNPCTitanTookDamage( player, damageInfo, critHit, titanDamage )
+}
+
+bool function IsKillshot( entity ent, var damageInfo, entity titanSoul )
+{
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int health = ent.GetHealth()
+
+ if ( health - damage > DOOMED_MIN_HEALTH )
+ return false
+
+ return true
+}
+
+bool function ShouldDoomTitan( entity ent, var damageInfo )
+{
+ if ( DoomStateDisabled() )
+ return false
+
+ if ( GetDoomedState( ent ) )
+ return false
+
+ if ( DamageInfo_GetForceKill( damageInfo ) )
+ return false
+
+ float doomedHealth = GetTitanSoulDoomedHealth( ent.GetTitanSoul() )
+ if ( doomedHealth <= 0 )
+ return false
+
+ entity soul = ent.GetTitanSoul()
+ if ( soul.soul.skipDoomState )
+ return false
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_SKIP_DAMAGE_PROT )
+ return doomedHealth > ( DamageInfo_GetDamage( damageInfo ) - ent.GetHealth() )
+
+ bool skipDoom = ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_SKIPS_DOOMED_STATE ) > 0
+ return !skipDoom
+}
+
+bool function HandleKillshot( entity ent, var damageInfo, TitanDamage titanDamage )
+{
+ #if NPC_TITAN_PILOT_PROTOTYPE
+ if ( TitanHasNpcPilot( ent ) ) //an npc titan that was dropped by an npc human
+ {
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int health = ent.GetHealth()
+
+ if ( health - damage <= 0 )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ thread TitanEjectPlayer( ent )
+ }
+
+ return
+ }
+ #endif
+
+ if ( ent.IsPlayer() && ent.IsBuddhaMode() )
+ return false
+
+ entity titanSoul = ent.GetTitanSoul()
+
+ if ( IsKillshot( ent, damageInfo, titanSoul ) )
+ {
+ entity boss = titanSoul.GetBossPlayer()
+ Soul_SetLastAttackInfo( titanSoul, damageInfo )
+
+ if ( ShouldDoomTitan( ent, damageInfo ) )
+ {
+ // Added via AddCallback_OnTitanDoomed
+ foreach ( callbackFunc in svGlobal.onTitanDoomedCallbacks )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+
+ if ( IsMultiplayer() )
+ {
+ entity attacker = expect entity( expect table( titanSoul.lastAttackInfo ).attacker )
+ if ( IsValid( attacker ) )
+ {
+ entity bossPlayer = attacker.GetBossPlayer()
+ if ( attacker.IsNPC() && IsValid( bossPlayer ) )
+ attacker = bossPlayer
+
+ if ( attacker.IsPlayer() )
+ ScoreEvent_TitanDoomed( ent, attacker, damageInfo )
+ }
+ }
+
+ thread DoomedHealthThink( titanSoul, damageInfo )
+
+ titanDamage.doomedNow = true
+ titanDamage.doomedDamage = int( DamageInfo_GetDamage( damageInfo ) )
+
+ int health = ent.GetHealth()
+ DamageInfo_SetDamage( damageInfo, health - 1 )
+ return true
+ }
+ else
+ {
+ // handle auto eject here
+ if ( ent.IsPlayer() && PlayerHasAutoEject( ent ) )
+ {
+ int health = ent.GetHealth()
+ DamageInfo_SetDamage( damageInfo, health - 1 )
+ thread HandleAutoEject( ent, titanSoul )
+ return false
+ }
+ }
+ }
+
+ // Handle doom state damage
+ if ( GetDoomedState( ent ) )
+ {
+ // as long as we're dying but not yet ejecting, the last player to damage us gets credit
+ if ( titanSoul.IsEjecting() )
+ {
+ Soul_SetLastAttackInfo( titanSoul, damageInfo )
+ }
+ else if ( ent.IsPlayer() && PlayerHasAutoEject( ent ) ) //Handle auto eject for when the frame in which Titan became doomed was not valid for ejecting, e.g. melee
+ {
+ int health = ent.GetHealth()
+ DamageInfo_SetDamage( damageInfo, health - 1 )
+ thread HandleAutoEject( ent, titanSoul )
+ return false
+ }
+
+ // protect players who eject early
+ // if ( ent.IsPlayer() && IsEjectProtected( ent, damageInfo ) )
+ // DamageInfo_SetDamage( damageInfo, 0 )
+
+ // slight protection to prevent multiple rapid damage events from eating through doomed state health
+ if ( Time() - titanSoul.soul.doomedStartTime < TITAN_DOOMED_INVUL_TIME && !DamageInfo_GetForceKill( damageInfo ) )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ }
+ else
+ {
+ Soul_SetLastAttackInfo( titanSoul, damageInfo )
+ }
+
+ return false
+}
+
+bool function PlayerHasAutoEject( entity player )
+{
+ if ( player.IsBot() )
+ return false
+
+ if ( !PlayerHasPassive( player, ePassives.PAS_AUTO_EJECT ) )
+ return false
+
+ return true
+}
+
+
+void function AdjustVelocityFromHit( entity player, var damageInfo, entity attacker, float damage, bool critHit )
+{
+/*
+ if ( DamageInfo_GetDamageCriticalHitScale( damageInfo ) > 1.0 )
+ {
+ // if you can crit, you have to crit!
+ if ( !critHit )
+ return
+ }
+*/
+
+ //printt( " " )
+ //printt( "damage: " + damage )
+
+ vector damageForward = DamageInfo_GetDamageForce( damageInfo )
+ damageForward.z = 0
+ //printt( "damageForward " + damageForward )
+
+ damageForward.Norm()
+
+ //vector org = DamageInfo_GetDamagePosition( damageInfo )
+ //DebugDrawLine( org, org + damageForward * 250, 255, 0, 0, true, 5.0 )
+
+ vector velocity = player.GetVelocity()
+ vector velForward = player.GetVelocity()
+ velForward.z = 0
+ velForward.Norm()
+
+ //DebugDrawLine( org, org + velForward * 250, 0, 255, 0, true, 5.0 )
+
+ float dot = DotProduct( velForward, damageForward )
+
+ // only stop from the ~front cone
+ if ( dot >= -0.5 )
+ return
+
+ float speedPercent
+
+ switch ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) )
+ {
+ //case eDamageSourceId.mp_titanweapon_40mm:
+ // speedPercent = GraphCapped( damage, 0, 750, 1, 0 )
+ // break
+
+ case eDamageSourceId.mp_titanweapon_xo16:
+ speedPercent = 0.075
+ break
+
+ default:
+ speedPercent = GraphCapped( damage, 0, 2500, 0, 1.0 )
+ }
+
+ //float dif = GraphCapped( dot, -1, -0.5, 1, 0 )
+ //speedPercent = speedPercent * dif + ( 1.0 - dif )
+
+ speedPercent *= GraphCapped( dot, -1.0, -0.5, 1, 0 )
+
+ //printt( " " )
+ //printt( "Damage: " + damage )
+ //printt( "dot: " + dot )
+ //printt( "speedPercent: " + speedPercent )
+ speedPercent = 1.0 - speedPercent
+ // make the dot into a tighter range
+ //dot += 0.5
+ //dot *= -2.0
+
+ //printt( "modifier: " + ( speedPercent ) )
+ velocity *= ( speedPercent )
+ player.SetVelocity( velocity )
+}
+
+
+
+void function DoomedHealthThink( entity titanSoul, var damageInfo )
+{
+ Assert( expect table( titanSoul.lastAttackInfo ).attacker, "Player entered reserve health with no attacker" )
+
+ entity soulOwner = titanSoul.GetTitan()
+ Assert( IsValid( soulOwner ), "Invalid owner " + soulOwner )
+
+ titanSoul.soul.doomedStartTime = Time()
+
+ // kill any existing health regen thread
+ titanSoul.Signal( SIGNAL_TITAN_HEALTH_REGEN )
+ titanSoul.Signal( SIGNAL_TITAN_SHIELD_REGEN )
+
+ titanSoul.EndSignal( "OnDestroy" )
+ titanSoul.EndSignal( "OnTitanDeath" )
+
+ float tickRate = 0.15
+ float maxDoomedHealth = GetTitanSoulDoomedHealth( titanSoul )
+ float doomedHealth = maxDoomedHealth
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_SKIP_DAMAGE_PROT )
+ doomedHealth = min( doomedHealth + soulOwner.GetHealth() - DamageInfo_GetDamage( damageInfo ), doomedHealth )
+
+ float DPS = (doomedHealth / TITAN_DOOMED_MAX_DURATION )
+
+ titanSoul.EnableDoomed()
+ titanSoul.doomedTime = Time()
+ soulOwner.SetDoomed()
+ DoomTitan( soulOwner )
+ soulOwner.Signal( "Doomed" )
+ titanSoul.Signal( "Doomed" )
+
+ // allow the damage to go through before resetting the health, so that we get proper damage indicators, etc...
+ // this process should also be in code
+ WaitEndFrame()
+
+ // grab the soul owner again since there was a wait
+ soulOwner = titanSoul.GetTitan()
+ if ( !IsValid( soulOwner ) )
+ return
+
+ if ( PROTO_AlternateDoomedState() )
+ {
+ //printt( soulOwner.GetHealth() )
+ soulOwner.SetHealth( doomedHealth )
+ soulOwner.SetMaxHealth( maxDoomedHealth )
+ //soulOwner.SetHealthPerSegment( 0 )
+
+ soulOwner.ClearDoomed()
+
+ if ( soulOwner.IsPlayer() && PlayerHasAutoEject( soulOwner ) )
+ {
+ HandleAutoEject( soulOwner, titanSoul )
+ }
+ else
+ {
+ //If it's an auto-titan with auto-eject, this just instantly kills it.
+ var attacker = ( "attacker" in titanSoul.lastAttackInfo ) ? expect table( titanSoul.lastAttackInfo ).attacker : null
+ expect entity( attacker )
+ var inflictor = ( "inflictor" in titanSoul.lastAttackInfo ) ? expect table( titanSoul.lastAttackInfo ).inflictor : null
+ expect entity( inflictor )
+ var damageSource = ( "damageSourceId" in titanSoul.lastAttackInfo ) ? expect table( titanSoul.lastAttackInfo ).damageSourceId : -1
+ int damageFlags = expect int( expect table( titanSoul.lastAttackInfo ).scriptType )
+ if ( SoulHasPassive( titanSoul, ePassives.PAS_AUTO_EJECT ) )
+ {
+ int scriptDamageType = damageTypes.titanEjectExplosion | damageFlags
+ soulOwner.Die( attacker, inflictor, { scriptType = scriptDamageType, damageSourceId = damageSource } )
+ }
+ }
+ return
+ }
+ soulOwner.SetHealth( doomedHealth )
+ soulOwner.SetMaxHealth( maxDoomedHealth )
+ //soulOwner.SetHealthPerSegment( 0 )
+
+ string settings = GetSoulPlayerSettings( titanSoul )
+ float damageMod = 1.0
+ while ( true )
+ {
+ table lastAttackInfo = expect table( titanSoul.lastAttackInfo )
+
+ table extraDeathInfo = {}
+ extraDeathInfo.scriptType <- (DF_NO_INDICATOR | DF_DOOMED_HEALTH_LOSS)
+ if ( expect int( lastAttackInfo.scriptType ) & DF_BURN_CARD_WEAPON )
+ extraDeathInfo.scriptType = expect int( extraDeathInfo.scriptType ) | DF_BURN_CARD_WEAPON
+ if ( expect int( lastAttackInfo.scriptType ) & DF_VORTEX_REFIRE )
+ extraDeathInfo.scriptType = expect int( extraDeathInfo.scriptType ) | DF_VORTEX_REFIRE
+
+ extraDeathInfo.damageSourceId <- lastAttackInfo.damageSourceId
+
+ entity soulOwner = titanSoul.GetTitan()
+ if ( !IsValid( soulOwner ) )
+ return
+ if ( soulOwner.IsPlayer() )
+ {
+ //if ( PlayerHasPassive( soulOwner, ePassives.PAS_DOOMED_TIME ) )
+ // damageMod = 0.4
+ //else
+ // damageMod = 1.0
+
+ if ( PlayerHasAutoEject( soulOwner ) )
+ {
+ //printt( "About to Auto Eject" )
+ // do it in the loop cause player could somehow get in a titan in doomed state
+ HandleAutoEject( soulOwner, titanSoul )
+ }
+ }
+
+ float dmgAmount = DPS * tickRate * damageMod
+
+ soulOwner.TakeDamage( dmgAmount, expect entity( lastAttackInfo.attacker ), expect entity( lastAttackInfo.inflictor ), extraDeathInfo )
+
+ wait tickRate
+ }
+}
+
+void function HandleAutoEject( entity rider, entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "OnTitanDeath" )
+
+ thread TitanEjectPlayer( rider )
+ if ( soul.IsEjecting() )
+ {
+ // so we don't cloak the titan during the ejection animation
+ if ( GetNuclearPayload( rider ) > 0 )
+ wait 2.0
+ else
+ wait 1.0
+
+ EnableCloak( rider, 7.0 )
+ return
+ }
+}
+
+void function TitanShieldRegenThink( entity soul )
+{
+ thread TitanShieldRegenThink_Internal( soul )
+}
+
+// HACK: this technically doesn't work properly because server framerate and all that jazz. Should really be in code.
+void function TitanShieldRegenThink_Internal( entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "Doomed" )
+ soul.EndSignal( "StopShieldRegen" )
+
+ //Shield starts at 0 health for now
+ string settings = GetSoulPlayerSettings( soul )
+ bool hasShield = Dev_GetPlayerSettingByKeyField_Global( settings, "start_with_shields" ) == 1
+
+ if ( !hasShield )
+ soul.SetShieldHealth( 0 )
+
+ int lastShieldHealth = soul.GetShieldHealth()
+ bool shieldHealthSound = false
+ int maxShield = soul.GetShieldHealthMax()
+ float lastTime = Time()
+
+ while ( true )
+ {
+ entity titan = soul.GetTitan()
+ if ( !IsValid( titan ) )
+ return
+
+ int shieldHealth = soul.GetShieldHealth()
+ Assert( titan )
+
+ if ( lastShieldHealth <= 0 && shieldHealth && titan.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, titan, "titan_energyshield_up_1P" )
+ shieldHealthSound = true
+ if ( titan.IsTitan() )
+ {
+ GiveFriendlyRodeoPlayerProtection( titan )
+ }
+ else
+ {
+ if ( titan.IsPlayer() )
+ {
+ printt( "Player was " + titan.GetPlayerSettings() )
+ }
+
+ printt( "ERROR! Expected Titan, but got " + titan )
+ }
+ }
+ else if ( shieldHealthSound && shieldHealth == soul.GetShieldHealthMax() )
+ {
+ shieldHealthSound = false
+ }
+ else if ( lastShieldHealth > shieldHealth && shieldHealthSound )
+ {
+ StopSoundOnEntity( titan, "titan_energyshield_up_1P" )
+ shieldHealthSound = false
+ }
+
+ if ( Time() >= soul.nextRegenTime && TitanHasRegenningShield( soul ) )
+ {
+ float shieldRegenRate = maxShield / ( GetShieldRegenTime( soul ) / SHIELD_REGEN_TICK_TIME )
+
+ if ( SoulHasPassive( soul, ePassives.PAS_SHIELD_BOOST ) )
+ shieldRegenRate = SHIELD_BEACON_REGEN_RATE
+
+ float frameTime = max( 0.0, Time() - lastTime )
+ shieldRegenRate = shieldRegenRate * frameTime / SHIELD_REGEN_TICK_TIME
+ // Faster shield recharge if we have Fusion Core active ability ( Stryder Signature )
+ //if ( titan.IsPlayer() && PlayerHasPassive( titan, ePassives.PAS_FUSION_CORE ) )
+ // shieldRegenRate *= 1.25
+
+ soul.SetShieldHealth( minint( soul.GetShieldHealthMax(), int( shieldHealth + shieldRegenRate ) ) )
+ }
+
+ lastShieldHealth = shieldHealth
+ lastTime = Time()
+ WaitFrame()
+ }
+}
+
+float function GetShieldRegenTime( entity soul )
+{
+ float time
+ if ( SoulHasPassive( soul, ePassives.PAS_SHIELD_REGEN ) )
+ time = TITAN_SHIELD_REGEN_TIME * 0.5
+ else
+ time = TITAN_SHIELD_REGEN_TIME
+
+ return time
+}
+
+float function GetHealthRegenDelay( entity soul )
+{
+ if ( GetDoomedState( soul.GetTitan() ) )
+ return TITAN_DOOMED_REGEN_DELAY
+
+ return GetShieldRegenDelay( soul )
+}
+
+float function GetShieldRegenDelay( entity soul )
+{
+ float regenDelay = TITAN_SHIELD_REGEN_DELAY
+
+ string settings = GetSoulPlayerSettings( soul )
+ regenDelay = expect float( Dev_GetPlayerSettingByKeyField_Global( settings, "titan_regen_delay" ) )
+
+ float delay
+ if ( SoulHasPassive( soul, ePassives.PAS_SHIELD_REGEN ) )
+ delay = regenDelay - 1.0
+ else
+ delay = regenDelay
+
+ if ( SoulHasPassive( soul, ePassives.PAS_SHIELD_BOOST ) )
+ delay = 2.0
+
+ return delay
+}
+
+void function RecordDamageToNPCTitanSoul( entity soul, var damageInfo )
+{
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ vector inflictOrigin = <0.0,0.0,0.0>
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ if ( IsValid( inflictor ) )
+ inflictOrigin = inflictor.GetOrigin()
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ array<string> weaponMods
+ if ( IsValid( weapon ) )
+ weaponMods = weapon.GetMods()
+
+ StoreDamageHistoryAndUpdate( soul, TITAN_HEALTH_HISTORY_FALLOFF_END, damage, inflictOrigin, DamageInfo_GetCustomDamageType( damageInfo ), DamageInfo_GetDamageSourceIdentifier( damageInfo ), attacker, weaponMods )
+}
+
+void function AutoTitan_TryMultipleTitanCallout( entity titan, var damageInfo )
+{
+ array<entity> titans = GetTitansHitMeInTime( titan.GetTitanSoul(), 5 )
+ entity enemy = titan.GetEnemy()
+ if ( IsAlive( enemy ) && enemy.IsTitan() && !titans.contains( enemy ) )
+ titans.append( enemy )
+
+ int totalEngagedTitans = titans.len()
+
+ if ( totalEngagedTitans == 1 )
+ PlayAutoTitanConversation( titan, "autoEngageTitan" )
+ else if ( totalEngagedTitans > 1 )
+ PlayAutoTitanConversation( titan, "autoEngageTitans" )
+}
+
+float function CalculateNextRegenTime( float damage, int damageType, bool critHit, float oldNextRegenTime, float maxRegenDelay )
+{
+ if ( damage >= TITAN_REGEN_MIN_DAMAGE || critHit || damageType & DF_STOPS_TITAN_REGEN )
+ {
+ if ( PROTO_VariableRegenDelay() )
+ {
+ // regen delay based on damage dealt
+ float minRegenDelay = 1.0
+ float regenDelay = GraphCapped( damage, 100, 1000, minRegenDelay, maxRegenDelay )
+
+ float nextRegenTime = oldNextRegenTime
+ float delayBasedOnCurrentTime = Time() + regenDelay
+ float delayBasedOnPreviousDelay = nextRegenTime + regenDelay
+ maxRegenDelay = Time() + maxRegenDelay
+
+ delayBasedOnCurrentTime = min( delayBasedOnCurrentTime, maxRegenDelay )
+ delayBasedOnPreviousDelay = min( delayBasedOnPreviousDelay, maxRegenDelay )
+ nextRegenTime = max( delayBasedOnCurrentTime, delayBasedOnPreviousDelay )
+
+ return nextRegenTime
+ }
+ else
+ {
+ // old style
+ return Time() + maxRegenDelay
+ }
+ }
+ else
+ {
+ float addTime = TITAN_REGEN_MIN_DAMAGE_DELAY
+
+ if ( oldNextRegenTime <= Time() + addTime )
+ return Time() + addTime
+ }
+
+ return oldNextRegenTime
+}
+
+void function AddCreditToTitanCoreBuilderForTitanDamageInflicted( entity titanAttacker, float damage )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ float rateRaw = CORE_BUILD_PERCENT_FROM_TITAN_DAMAGE_INFLICTED
+ float rate = (rateRaw * 0.01)
+ float credit = (rate * damage)
+ if ( credit > 0.0 )
+ AddCreditToTitanCoreBuilder( titanAttacker, credit )
+}
+
+void function AddCreditToTitanCoreBuilderForTitanDamageReceived( entity titanVictim, float damage )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ float rateRaw = CORE_BUILD_PERCENT_FROM_TITAN_DAMAGE_RECEIVED
+ float rate = (rateRaw * 0.01)
+ float credit = (rate * damage)
+ if ( credit > 0.0 )
+ AddCreditToTitanCoreBuilder( titanVictim, credit )
+}
+
+void function AddCreditToTitanCoreBuilderForDoomInflicted( entity titanAttacker )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ float valueRaw = CORE_BUILD_PERCENT_FROM_DOOM_INFLICTED
+ float credit = (valueRaw * 0.01)
+ if ( credit > 0.0 )
+ AddCreditToTitanCoreBuilder( titanAttacker, credit )
+}
+
+void function AddCreditToTitanCoreBuilderForDoomEntered( entity titanVictim )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ float valueRaw = CORE_BUILD_PERCENT_FROM_DOOM_ENTERED
+ float credit = (valueRaw * 0.01)
+ if ( credit > 0.0 )
+ AddCreditToTitanCoreBuilder( titanVictim, credit )
+}
+
+void function AddCreditToTitanCoreBuilder( entity titan, float credit )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) )
+ return
+
+ entity bossPlayer = soul.GetBossPlayer()
+
+ if ( titan.IsPlayer() )
+ {
+ if ( !IsValid( bossPlayer ) )
+ return
+
+ if ( bossPlayer.IsTitan() && TitanCoreInUse( bossPlayer ) )
+ return
+ }
+ else
+ {
+ Assert( titan.IsNPC() )
+ if ( TitanCoreInUse( titan ) )
+ return
+ }
+
+ if ( !IsAlive( titan ) )
+ return
+
+ if ( SoulHasPassive( soul, ePassives.PAS_VANGUARD_COREMETER ) )
+ credit *= 1.10
+
+ credit *= file.earn_meter_titan_multiplier
+ #if MP
+ if ( titan.IsPlayer() )
+ {
+ float coreModifier = titan.GetPlayerNetFloat( "coreMeterModifier" )
+ if ( coreModifier >= 0.5 )
+ credit *= FD_HOT_STREAK_MULTIPLIER
+ }
+ #endif
+
+ bool coreWasAvailable = false
+
+ if ( IsValid( bossPlayer ) )
+ coreWasAvailable = IsCoreChargeAvailable( bossPlayer, soul )
+
+ float oldTotalCredit = SoulTitanCore_GetNextAvailableTime( soul )
+ float newTotalCredit = (credit + oldTotalCredit)
+ if ( newTotalCredit >= 0.998 ) //JFS - the rui has a +0.001 for showing the meter as full. This fixes the case where the core meter displays 100 but can't be fired.
+ newTotalCredit = 1.0
+ SoulTitanCore_SetNextAvailableTime( soul, newTotalCredit )
+
+ if ( IsValid( bossPlayer ) && !coreWasAvailable && IsCoreChargeAvailable( bossPlayer, soul ) )
+ {
+ AddPlayerScore( bossPlayer, "TitanCoreEarned" )
+ #if MP
+ UpdateTitanCoreEarnedStat( bossPlayer, titan )
+ PIN_PlayerAbilityReady( bossPlayer, "core" )
+ #endif
+ }
+
+ #if MP
+ if ( IsValid( bossPlayer ) )
+ JFS_PlayerEarnMeter_CoreRewardUpdate( titan, oldTotalCredit, newTotalCredit )
+ #endif
+
+ #if HAS_TITAN_TELEMETRY
+ if ( titan.IsPlayer() )
+ {
+ if ( IsCoreChargeAvailable( titan, soul ) )
+ {
+ TitanHints_TryShowHint( titan, [OFFHAND_EQUIPMENT] )
+ }
+ }
+ #endif
+}
+
+float function GetTitanCoreTimer( entity titan )
+{
+ Assert( titan.IsTitan() )
+ entity soul = titan.GetTitanSoul()
+ Assert( soul )
+
+ return SoulTitanCore_GetNextAvailableTime( soul ) - Time()
+}
+
+
+
+void function SetTitanCoreTimer( entity titan, float timeDiff )
+{
+ Assert( !TitanDamageRewardsTitanCoreTime() )
+
+ Assert( titan.IsTitan() )
+ entity soul = titan.GetTitanSoul()
+ Assert( soul )
+
+ float newTime = Time() + timeDiff
+ SoulTitanCore_SetNextAvailableTime( soul, max( Time() - 1, newTime ) )
+}
+
+
+void function Titan_MonarchCleanup( entity soul, var damageInfo )
+{
+ entity titan = soul.GetTitan()
+
+ if ( !IsValid( titan ) )
+ return
+
+ int statesIndex = titan.FindBodyGroup( "states" )
+ if ( statesIndex <= -1 )
+ return
+
+ titan.SetBodygroup( statesIndex, 2 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_hints.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_hints.gnut
new file mode 100644
index 00000000..0e8b4b5b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_hints.gnut
@@ -0,0 +1,267 @@
+global function TitanHints_Init
+global function TitanHints_NotifyUsedOffhand
+global function TitanHints_ResetThresholds
+global function TitanHints_TryShowHint
+global function TitanHints_ShowHint
+
+const float FIGHT_START_THRESHOLD = 10.0
+const float FIGHT_HINT_THRESHOLD = 8.0
+const float TITAN_HINT_COOLDOWN = 15.0
+
+struct
+{
+ float titanFightStartTime = -99
+ float lastDidDamageTime = -99
+ float lastTookDamageTime = -99
+ float lastShowHintTime = -99
+ float lastDodgeTime = -99
+ table<int,float> titanHintThresholds
+ table<int,float> titanHintThresholdAdd
+ table<int,float> lastShowHintTimes
+} file
+
+void function TitanHints_Init()
+{
+ AddDamageCallback( "player", TitanHint_Player_OnDamaged )
+ AddDamageCallback( "npc_titan", TitanHint_NPC_OnDamaged )
+ AddDamageCallback( "npc_super_spectre", TitanHint_NPC_OnDamaged )
+
+ file.titanHintThresholds[ TITAN_HINT_DASH ] <- 5.0
+ file.titanHintThresholds[ OFFHAND_ORDNANCE ] <- 5.0
+ file.titanHintThresholds[ OFFHAND_SPECIAL ] <- 5.0
+ file.titanHintThresholds[ OFFHAND_ANTIRODEO ] <- 10.0
+ file.titanHintThresholds[ OFFHAND_EQUIPMENT ] <- 1.0
+
+ file.lastShowHintTimes[ TITAN_HINT_DASH ] <- -99.0
+ file.lastShowHintTimes[ OFFHAND_ORDNANCE ] <- -99.0
+ file.lastShowHintTimes[ OFFHAND_SPECIAL ] <- -99.0
+ file.lastShowHintTimes[ OFFHAND_ANTIRODEO ] <- -99.0
+ file.lastShowHintTimes[ OFFHAND_EQUIPMENT ] <- -99.0
+
+ file.titanHintThresholdAdd[ TITAN_HINT_DASH ] <- 0
+ file.titanHintThresholdAdd[ OFFHAND_ORDNANCE ] <- 0
+ file.titanHintThresholdAdd[ OFFHAND_SPECIAL ] <- 0
+ file.titanHintThresholdAdd[ OFFHAND_ANTIRODEO ] <- 0
+ file.titanHintThresholdAdd[ OFFHAND_EQUIPMENT ] <- 0
+
+ AddCallback_OnPlayerInventoryChanged( TitanHints_ResetThresholds )
+ AddSpawnCallback( "player", PlayerDidLoad )
+}
+
+void function PlayerDidLoad( entity player )
+{
+ AddPlayerMovementEventCallback( player, ePlayerMovementEvents.DODGE, OnPlayerDodge )
+}
+
+void function TitanHint_Player_OnDamaged( entity player, var damageInfo )
+{
+ if ( !player.IsTitan() )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !attacker.IsTitan() && !IsSuperSpectre(attacker) )
+ return
+
+ if ( attacker.GetTeam() == player.GetTeam() )
+ return
+
+ TrySetFightTime()
+
+ file.lastTookDamageTime = Time()
+
+ array<int> hintsToShow = [ TITAN_HINT_DASH, OFFHAND_EQUIPMENT, OFFHAND_SPECIAL, OFFHAND_ORDNANCE, OFFHAND_ANTIRODEO ]
+
+ if ( GetDoomedState( player ) || GetTitanCurrentRegenTab( player ) < 2 )
+ hintsToShow = [ TITAN_HINT_DASH, OFFHAND_SPECIAL ]
+
+ TitanHints_TryShowHint( player, hintsToShow, attacker )
+}
+
+void function TitanHint_NPC_OnDamaged( entity victim, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !attacker.IsPlayer() )
+ return
+
+ if ( !attacker.IsTitan() )
+ return
+
+ TrySetFightTime()
+
+ file.lastDidDamageTime = Time()
+
+ TitanHints_TryShowHint( attacker, [ OFFHAND_EQUIPMENT, OFFHAND_ORDNANCE, OFFHAND_ANTIRODEO ], victim )
+}
+
+// reset thresholds
+void function TitanHints_ResetThresholds( entity player )
+{
+ if ( !player.IsTitan() )
+ return
+
+ foreach ( index, value in file.titanHintThresholdAdd )
+ {
+ if ( index != TITAN_HINT_DASH ) // don't reset dash
+ file.titanHintThresholdAdd[ index ] = 0.0
+ }
+}
+
+// increase threshold for hints every time the player uses it
+void function TitanHints_NotifyUsedOffhand( int index )
+{
+ // never increment for core
+ if ( index == OFFHAND_EQUIPMENT )
+ return
+
+ if ( index in file.titanHintThresholds )
+ {
+ file.titanHintThresholdAdd[ index ] += TITAN_HINT_COOLDOWN
+ }
+}
+
+bool function TrySetFightTime()
+{
+ if (
+ Time() - file.lastTookDamageTime > FIGHT_START_THRESHOLD &&
+ Time() - file.lastDidDamageTime > FIGHT_START_THRESHOLD
+ )
+ {
+ file.titanFightStartTime = Time()
+ return true
+ }
+
+ return false
+}
+
+void function TitanHints_TryShowHint( entity player, array<int> indexes, entity enemy = null )
+{
+ if ( GetConVarInt( "hud_setting_showTips" ) == 0 )
+ return
+
+ float fightDuration = Time() - file.titanFightStartTime
+ if ( fightDuration < FIGHT_HINT_THRESHOLD )
+ return
+
+ if ( TitanCoreInUse( player ) )
+ return
+
+ foreach ( idx in indexes )
+ {
+ float threshold = file.titanHintThresholds[idx] + file.titanHintThresholdAdd[idx]
+
+ // have we been fighting for a while?
+ if ( fightDuration < max( threshold, TITAN_HINT_COOLDOWN ) )
+ continue
+
+ // have we already shown this hint?
+ if ( Time() - file.lastShowHintTimes[idx] < max( threshold, TITAN_HINT_COOLDOWN ) )
+ continue
+
+ // have we already shown a hint?
+ if ( Time() - file.lastShowHintTime < TITAN_HINT_COOLDOWN )
+ continue
+
+ if ( idx != TITAN_HINT_DASH )
+ {
+ // when did you last use this ability?
+ if ( Time() - player.p.lastTitanOffhandUseTime[idx] < threshold )
+ continue
+
+ entity weapon = player.GetOffhandWeapon( idx )
+
+ if ( weapon == null )
+ continue
+
+ // has this ability been available for a while?
+ if ( weapon.GetNextAttackAllowedTime() + threshold > Time() )
+ continue
+
+ var requiresLocks = weapon.GetWeaponInfoFileKeyField( "requires_lock" )
+
+ if ( requiresLocks != null )
+ {
+ expect int( requiresLocks )
+ if ( requiresLocks == 1 )
+ {
+ if ( weapon.SmartAmmo_IsEnabled() && !SmartAmmo_CanWeaponBeFired( weapon ) )
+ continue
+ }
+ }
+
+
+ int curEnergyCost = weapon.GetWeaponCurrentEnergyCost()
+ if ( !player.CanUseSharedEnergy( curEnergyCost ) )
+ continue
+
+ if ( weapon.IsChargeWeapon() )
+ {
+ if ( weapon.GetWeaponChargeFraction() > 0.0 )
+ continue
+ }
+
+ if ( weapon.GetWeaponPrimaryClipCount() < weapon.GetWeaponSettingInt( eWeaponVar.ammo_min_to_fire ) )
+ continue
+
+ // special core check
+ if ( idx == OFFHAND_EQUIPMENT )
+ {
+ if( !CheckCoreAvailable( weapon ) )
+ continue
+ if ( IsConversationPlaying() )
+ continue
+ }
+
+ var hintType = weapon.GetWeaponInfoFileKeyField( "hint_type" )
+ if ( hintType != null )
+ {
+ if ( hintType == "range_toggle" )
+ {
+ if ( enemy != null )
+ {
+ float dist = Distance2D( enemy.GetOrigin(), player.GetOrigin() )
+
+ if ( weapon.HasMod( "ammo_swap_ranged_mode" ) )
+ { // has long range mode, will tell to swap to short range
+ if ( dist > 2500 )
+ {
+ continue
+ }
+ }
+ else
+ { // has short range mode, will tell to swap to long range
+ if ( dist < 1500 )
+ {
+ continue
+ }
+ }
+ }
+ }
+ }
+
+ }
+ else
+ {
+ if ( Time() - file.lastDodgeTime < threshold )
+ continue
+
+ // should check if dodge is available here, but we can't seem to do that
+ }
+
+ // show hint
+ TitanHints_ShowHint( player, idx )
+ break
+ }
+}
+
+void function TitanHints_ShowHint( entity player, int idx )
+{
+ Remote_CallFunction_Replay( player, "ServerCallback_ShowOffhandWeaponHint", idx )
+ file.lastShowHintTimes[idx] = Time()
+ file.lastShowHintTime = Time()
+}
+
+void function OnPlayerDodge( entity player )
+{
+ file.lastDodgeTime = Time()
+ file.titanHintThresholdAdd[ TITAN_HINT_DASH ] += TITAN_HINT_COOLDOWN
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_hotdrop.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_hotdrop.gnut
new file mode 100644
index 00000000..e3410de8
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_hotdrop.gnut
@@ -0,0 +1,778 @@
+untyped
+
+global function TitanHotdrop_Init
+
+global function TitanHotDrop
+global function PlayersTitanHotdrops
+global function NPCTitanHotdrops
+global function NPCPrespawnWarpfallSequence
+global function WaitTillHotDropComplete
+global function OnTitanHotdropImpact
+global function PlayHotdropImpactFX
+global function TitanTestDropPoint
+global function EdgeTraceDropPoint
+
+
+global function GetHotDropImpactTime
+
+global function ModifyOriginForDrop
+
+global function NearTitanfallBlocker
+
+global function DevCheckInTitanfallBlocker
+
+global function DrawTitanfallBlockers
+
+global function DropPodFindDropNodes
+
+global function PlayDeathFromTitanFallSounds
+
+global const HOTDROP_FP_WARP = $"P_warpjump_FP"
+global const HOTDROP_TRAIL_FX = $"hotdrop_hld_warp"
+global int BUBBLE_SHIELD_FX_PARTICLE_SYSTEM_INDEX
+
+function TitanHotdrop_Init()
+{
+
+ RegisterSignal( "titan_impact" )
+ RegisterSignal( "TitanHotDropComplete" )
+ RegisterSignal( "BubbleShieldStatusUpdate" )
+
+ PrecacheEffect( HOTDROP_TRAIL_FX )
+ PrecacheEffect( HOTDROP_FP_WARP )
+
+ AddDamageCallbackSourceID( damagedef_titan_fall, TitanFall_DamagedPlayerOrNPC )
+
+ PrecacheImpactEffectTable( HOTDROP_IMPACT_FX_TABLE )
+
+ PrecacheModel( $"models/fx/xo_shield.mdl" )
+ PrecacheModel( $"models/fx/xo_shield_wall.mdl" )
+ BUBBLE_SHIELD_FX_PARTICLE_SYSTEM_INDEX = PrecacheParticleSystem( $"P_shield_hld_01_CP" )
+
+ BubbleShield_Init()
+}
+
+void function TitanHotDrop( entity titan, string animation, vector origin, vector angles, entity player, entity camera )
+{
+ Assert( titan.IsTitan(), titan + " is not a titan" )
+
+ titan.EndSignal( "OnDeath" )
+
+ HideName( titan )
+
+ array<entity> cleanup = [] // ents that will be deleted upon completion
+
+ OnThreadEnd(
+ function() : ( cleanup, titan, player, camera )
+ {
+ printt( "Post impact,anim is done" )
+ if ( IsValid( titan ) )
+ {
+ delete titan.s.hotDropPlayer
+ titan.e.isHotDropping = false
+ titan.Signal( "TitanHotDropComplete" )
+ if ( !IsFFAGame() )
+ titan.Minimap_DisplayDefault( titan.GetTeam(), null )
+ }
+
+ if ( IsValid( camera ) )
+ camera.ClearParent()
+
+ foreach ( entity ent in cleanup )
+ {
+ if ( IsValid_ThisFrame( ent ) )
+ {
+ // Delay enough seconds to allow titan hot drop smokeTrail FX to play fully
+ ent.Kill_Deprecated_UseDestroyInstead()
+ }
+ }
+
+ if ( IsValid( player ) )
+ ScreenFadeFromBlack( player, 0.2, 0.2 )
+ }
+ )
+
+ titan.s.hotDropPlayer <- player
+ titan.e.isHotDropping = true
+
+ origin += Vector(0,0,8 ) // work around for currently busted animation
+
+ entity ref = CreateScriptRef()
+ ref.SetOrigin( origin )
+ ref.SetAngles( angles )
+ ref.Show()
+ cleanup.append( ref )
+
+ // add smoke fx
+
+ TitanHotDrop_Smoke( cleanup, titan, titan.GetBossPlayer() )
+
+// "Titan_1P_Warpfall_Hotdrop" - for first person drops while inside the titan dropping into the level
+// "Titan_1P_Warpfall_Start" - for first person warp calls, starting right on the button press
+// "Titan_1P_Warpfall_WarpToLanding" - for first person from the visual of the titan appearing and falling
+// "Titan_3P_Warpfall_Start" - for any 3P other player or NPC when they call in a warp, starting right on their button press
+// "Titan_3P_Warpfall_WarpToLanding" - for any 3P other player or NPC from the visual of the titan appearing and falling
+ int teamNum = TEAM_UNASSIGNED
+ if ( IsValid( player ) )
+ teamNum = player.GetTeam();
+
+ EmitSoundAtPositionOnlyToPlayer( teamNum, origin, player, "Titan_1P_Warpfall_Hotdrop" )
+ EmitSoundAtPositionOnlyToPlayer( teamNum, origin, player, "Titan_1P_Warpfall_Start" )
+ EmitSoundAtPositionExceptToPlayer( teamNum, origin, player, "Titan_3P_Warpfall_Start" )
+ EmitSoundAtPositionExceptToPlayer( teamNum, origin, player, "Titan_3P_Warpfall_WarpToLanding" )
+
+ float duration = titan.GetSequenceDuration( animation )
+
+ Minimap_PingForTeam( titan.GetTeam(), origin, 64.0, duration, TEAM_COLOR_FRIENDLY / 255.0, 4, false )
+ if ( !IsFFAGame() )
+ titan.Minimap_Hide( titan.GetTeam(), null )
+
+ titan.NotSolid();
+ thread PlayAnimTeleport( titan, animation, ref )
+ titan.EndSignal( "OnAnimationDone" )
+
+ if ( player )
+ {
+ player.PlayerCone_SetMinYaw( -70 )
+ player.PlayerCone_SetMaxYaw( 70 )
+ player.PlayerCone_SetMinPitch( -90 )
+ player.PlayerCone_SetMaxPitch( 90 )
+ }
+
+ titan.WaitSignal( "titan_impact" )
+ player.ClearHotDropImpactTime()
+// wait duration - 1.25
+
+ titan.Solid();
+
+ ShowName( titan )
+
+ vector sourcePosition = origin
+ sourcePosition.z = sourcePosition.z + 5.0
+
+ Explosion_DamageDefSimple(
+ damagedef_titan_hotdrop,
+ origin,
+ titan, // attacker
+ titan, // inflictor
+ origin )
+
+ float zoomTime = 2.0
+ float rotateTime = 0.5
+
+ //printt( "Post impact, before anim is done" )
+
+ if ( IsValid( camera ) )
+ {
+ camera.ClearParent()
+
+ entity mover = CreateExpensiveScriptMover()
+ mover.SetOrigin( camera.GetOrigin() )
+ mover.SetAngles( camera.GetAngles() )
+ camera.SetParent( mover )
+
+ mover.NonPhysicsMoveTo( titan.GetWorldSpaceCenter(), zoomTime, zoomTime * 0.4, zoomTime * 0.4 )
+ cleanup.append( mover )
+
+ wait 0.5
+
+ ScreenFadeToBlackForever( player, 0.8 )
+
+ wait 0.6
+
+ mover.RotateTo( angles, rotateTime, rotateTime*0.2, rotateTime*0.2 )
+ }
+
+ WaittillAnimDone( titan )
+}
+
+entity function TitanHotDrop_Smoke( array<entity> cleanup, entity titan, entity player )
+{
+ entity smokeTrail = CreateEntity( "info_particle_system" )
+ if ( IsValid( player ) )
+ {
+ smokeTrail.SetOwner( player )
+ smokeTrail.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER
+ }
+
+ smokeTrail.SetValueForEffectNameKey( HOTDROP_TRAIL_FX ) // HOTDROP_FP_WARP
+ smokeTrail.kv.start_active = 1
+ DispatchSpawn( smokeTrail )
+ smokeTrail.SetParent( titan, "HATCH_HEAD" )
+ cleanup.append( smokeTrail )
+
+
+ smokeTrail = CreateEntity( "info_particle_system" )
+ if ( IsValid( player ) )
+ {
+ smokeTrail.SetOwner( player )
+ smokeTrail.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) //owner cant see
+ }
+
+ smokeTrail.SetValueForEffectNameKey( HOTDROP_TRAIL_FX ) // HOTDROP_FP_WARP
+ smokeTrail.kv.start_active = 1
+ DispatchSpawn( smokeTrail )
+ smokeTrail.SetParent( titan, "HATCH_HEAD" )
+ cleanup.append( smokeTrail )
+
+ return smokeTrail
+}
+
+void function PlayersTitanHotdrops( entity titan, vector origin, vector angles, entity player, string animation )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.s.disableAutoTitanConversation <- true // refactor: Should be created on spawn, and always exist -mackey
+
+ OnThreadEnd(
+ function() : ( titan, player )
+ {
+ if ( !IsValid( titan ) )
+ return
+
+ // removed so that model highlight always works for you autotitan
+// titan.DisableRenderAlways()
+
+ delete titan.s.hotDropPlayer
+ titan.e.isHotDropping = false
+ titan.Signal( "TitanHotDropComplete" )
+ DeleteAnimEvent( titan, "titan_impact" )
+ DeleteAnimEvent( titan, "second_stage" )
+ DeleteAnimEvent( titan, "set_usable" )
+ }
+ )
+
+ HideName( titan )
+ titan.s.hotDropPlayer <- player
+ titan.e.isHotDropping = true
+ titan.UnsetUsable() //Stop titan embark before it lands
+ AddAnimEvent( titan, "titan_impact", OnTitanHotdropImpact )
+ AddAnimEvent( titan, "second_stage", OnReplacementTitanSecondStage, origin )
+ AddAnimEvent( titan, "set_usable", SetTitanUsableByOwner )
+
+ string sfxFirstPerson
+ string sfxThirdPerson
+
+ switch ( animation )
+ {
+ case "at_hotdrop_drop_2knee_turbo_upgraded":
+ sfxFirstPerson = "Titan_1P_Warpfall_WarpToLanding_fast"
+ sfxThirdPerson = "Titan_3P_Warpfall_WarpToLanding_fast"
+ break
+
+ case "bt_hotdrop_skyway":
+ sfxFirstPerson = "titan_hot_drop_turbo_begin"
+ sfxThirdPerson = "titan_hot_drop_turbo_begin_3P"
+ break
+
+ case "at_hotdrop_drop_2knee_turbo":
+ sfxFirstPerson = "titan_hot_drop_turbo_begin"
+ sfxThirdPerson = "titan_hot_drop_turbo_begin_3P"
+ break
+
+ default:
+ Assert( 0, "Unknown anim " + animation )
+ }
+
+ float impactTime = GetHotDropImpactTime( titan, animation )
+ Attachment result = titan.Anim_GetAttachmentAtTime( animation, "OFFSET", impactTime )
+ vector maxs = titan.GetBoundingMaxs()
+ vector mins = titan.GetBoundingMins()
+ int mask = titan.GetPhysicsSolidMask()
+ origin = ModifyOriginForDrop( origin, mins, maxs, result.position, mask )
+
+ titan.SetInvulnerable() //Make Titan invulnerable until bubble shield is up. Cleared in OnTitanHotdropImpact
+
+ if ( SoulHasPassive( titan.GetTitanSoul(), ePassives.PAS_BUBBLESHIELD ) )
+ {
+ delaythread( impactTime ) CreateBubbleShield( titan, origin, angles )
+ }
+ else if ( SoulHasPassive( titan.GetTitanSoul(), ePassives.PAS_WARPFALL ) )
+ {
+ angles = AnglesCompose( angles, Vector( 0.0, 180.0, 0.0) )
+ }
+
+ //DrawArrow( origin, angles, 10, 150 )
+ // HACK: not really a hack, but this could be optimized to only render always for a given client
+ titan.EnableRenderAlways()
+
+ int teamNum = TEAM_UNASSIGNED
+ if ( IsValid( player ) )
+ teamNum = player.GetTeam()
+
+ EmitDifferentSoundsAtPositionForPlayerAndWorld( sfxFirstPerson, sfxThirdPerson, origin, player, teamNum )
+
+ SetStanceKneel( titan.GetTitanSoul() )
+
+ waitthread PlayAnimTeleport( titan, animation, origin, angles )
+
+ TitanCanStand( titan )
+ if ( !titan.GetCanStand() )
+ {
+ titan.SetOrigin( origin )
+ titan.SetAngles( angles )
+ }
+
+ titan.ClearInvulnerable() //Make Titan vulnerable again once he's landed
+
+ if ( !Flag( "DisableTitanKneelingEmbark" ) )
+ {
+ if ( IsValid( GetEmbarkPlayer( titan ) ) )
+ {
+ titan.SetTouchTriggers( true ) //Hack, potential fix for triggers bug. See bug 212751
+ //A player is trying to get in before the hotdrop animation has finished
+ //Wait until the embark animation has finished
+ WaittillAnimDone( titan )
+ return
+ }
+
+ titan.s.standQueued = false // SetStanceKneel should set this
+ SetStanceKneel( titan.GetTitanSoul() )
+ thread PlayAnim( titan, "at_MP_embark_idle_blended" )
+ }
+}
+
+float function GetHotDropImpactTime( entity titan, string animation )
+{
+ float impactTime = titan.GetScriptedAnimEventCycleFrac( animation, "titan_impact" )
+ if ( impactTime < 0.0 )
+ impactTime = titan.GetScriptedAnimEventCycleFrac( animation, "signal:titan_impact" )
+
+ Assert( impactTime > -1.0, "No event titan_impact in " + animation )
+
+ float duration = titan.GetSequenceDuration( animation )
+
+ impactTime *= duration
+
+ return impactTime
+}
+
+function NPCTitanHotdrops( entity titan, bool standImmediately, string titanfallAnim = "at_hotdrop_drop_2knee_turbo" )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ titan.e.isHotDropping = true
+ titan.s.bubbleShieldStatus <- 0
+
+ titan.SetEfficientMode( true )
+ titan.SetTouchTriggers( false )
+ titan.SetAimAssistAllowed( false )
+
+ float impactTime = GetHotDropImpactTime( titan, titanfallAnim )
+ vector origin = titan.GetOrigin()
+ vector angles = titan.GetAngles()
+
+ #if GRUNTCHATTER_ENABLED
+ GruntChatter_TryIncomingSpawn( titan, origin )
+ #endif
+
+ #if MP
+ TryAnnounceTitanfallWarningToEnemyTeam( titan.GetTeam(), origin )
+ #endif
+
+ if ( NPCShouldDoBubbleShieldAfterHotdrop( titan ) )
+ {
+ titan.SetNoTarget( true )
+ thread CreateGenericBubbleShield_Delayed( titan, origin, angles, impactTime - 0.1 )
+ }
+
+ waitthread PlayersTitanHotdrops( titan, origin, angles, null, titanfallAnim )
+
+ if ( standImmediately )
+ {
+ SetStanceStand( titan.GetTitanSoul() )
+ waitthread PlayAnimGravity( titan, "at_hotdrop_quickstand" )
+ }
+
+ titan.SetEfficientMode( false )
+ titan.SetTouchTriggers( true )
+ titan.SetAimAssistAllowed( true )
+
+ titan.e.isHotDropping = false
+ titan.Signal( "TitanHotDropComplete" )
+
+ titan.SetNoTarget( false )
+
+ while( titan.s.bubbleShieldStatus == 1 )
+ titan.WaitSignal( "BubbleShieldStatusUpdate" )
+}
+
+void function NPCPrespawnWarpfallSequence( string aiSettings, vector spawnOrigin, vector spawnAngle )
+{
+ string animation = "at_hotdrop_drop_2knee_turbo_upgraded"
+// string settings = GetTitanForPlayer( player ).titanSetFile
+ string playerSettings = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" ) )
+ asset model = GetPlayerSettingsAssetForClassName( playerSettings, "bodymodel" )
+ Attachment warpAttach = GetAttachmentAtTimeFromModel( model, animation, "offset", spawnOrigin, spawnAngle, 0 )
+
+ entity fakeTitan = CreatePropDynamic( model )
+ float impactTime = GetHotDropImpactTime( fakeTitan, animation )
+
+ #if SP //MP AI already call DisableTitanfallForLifetimeOfEntityNearOrigin() in SpawnNeutralAI()/SpawnTeamAI() functions. Pretty sure can just remove this for SP too
+ thread TemporarilyDisableTitanfallAroundRadius( spawnOrigin, 72, WARPFALL_SOUND_DELAY + WARPFALL_FX_DELAY ) //TODO: Look into getting rid of this. Doesn't play well with DisableTitanfallForLifetimeOfEntityNearOrigin. Only used in Beacon
+ #endif
+
+ fakeTitan.Kill_Deprecated_UseDestroyInstead()
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, spawnOrigin, "Titan_3P_Warpfall_CallIn" )
+
+ wait WARPFALL_SOUND_DELAY
+
+ // "Titan_1P_Warpfall_Start" - for first person warp calls, starting right on the button press
+ // "Titan_3P_Warpfall_Start" - for any 3P other player or NPC when they call in a warp, starting right on their button press
+ EmitSoundAtPosition( TEAM_UNASSIGNED, spawnOrigin, "Titan_3P_Warpfall_Start" )
+
+ PlayFX( TURBO_WARP_FX, warpAttach.position + Vector(0,0,-104), warpAttach.angle )
+
+ wait WARPFALL_FX_DELAY
+}
+
+void function WaitTillHotDropComplete( entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ // waits for him to drop in from the sky AND stand up
+ if ( titan.e.isHotDropping )
+ WaitSignal( titan, "TitanHotDropComplete" )
+}
+
+function CreateGenericBubbleShield_Delayed( entity titan, vector origin, vector angles, float delay = 0.0 )
+{
+ titan.EndSignal( "OnDestroy" )
+
+ if ( delay > 0 )
+ wait delay
+
+ titan.s.bubbleShieldStatus = 1
+ CreateGenericBubbleShield( titan, origin, angles )
+ titan.s.bubbleShieldStatus = 0
+ titan.Signal( "BubbleShieldStatusUpdate" )
+}
+
+
+vector function ModifyOriginForDrop( vector origin, vector mins, vector maxs, vector resultPos, int mask )
+{
+ TraceResults trace = TraceHull( resultPos + Vector(0,0,20), resultPos + Vector(0,0,-20), mins, maxs, null, mask, TRACE_COLLISION_GROUP_NONE )
+ float zDif = trace.endPos.z - resultPos.z
+ origin.z += zDif
+ origin.z += 3.0
+
+ return origin
+}
+
+void function OnReplacementTitanSecondStage( entity titan )
+{
+ vector origin = expect vector( GetOptionalAnimEventVar( titan, "second_stage" ) )
+
+ string sfxFirstPerson = "titan_drop_pod_turbo_landing"
+ string sfxThirdPerson = "titan_drop_pod_turbo_landing_3P"
+ entity player = titan.GetBossPlayer()
+ EmitDifferentSoundsAtPositionForPlayerAndWorld( sfxFirstPerson, sfxThirdPerson, origin, player, titan.GetTeam() )
+}
+
+void function OnTitanHotdropImpact( entity titan )
+{
+ ShowName( titan )
+ PlayHotdropImpactFX( titan )
+ titan.Signal( "ClearDisableTitanfall" )
+}
+
+function SetTitanUsable( titan )
+{
+ titan.SetUsableByGroup( "friendlies pilot" )
+}
+
+void function SetTitanUsableByOwner( entity titan )
+{
+ titan.SetUsableByGroup( "owner pilot" )
+}
+
+function PlayHotdropImpactFX( titan )
+{
+ expect entity( titan )
+ if ( !IsAlive( titan ) || !titan.IsTitan() )
+ return
+
+ local origin = titan.GetOrigin()
+
+ Explosion_DamageDefSimple(
+ damagedef_titan_fall,
+ origin,
+ titan, // attacker
+ titan, // inflictor
+ origin )
+
+
+ CreateShake( titan.GetOrigin(), 16, 150, 2, 1500 )
+ // No Damage - Only Force
+ // Push players
+ // Push radially - not as a sphere
+ // Test LOS before pushing
+ int flags = 15
+ vector impactOrigin = titan.GetOrigin() + Vector( 0,0,10 )
+ float impactRadius = 512
+ CreatePhysExplosion( impactOrigin, impactRadius, PHYS_EXPLOSION_LARGE, flags )
+}
+
+function NearTitanfallBlocker( baseOrigin )
+{
+ foreach ( hardpoint in level.testHardPoints )
+ {
+ local hpOrigin = hardpoint.GetOrigin()
+ hpOrigin.z -= 100 // why are hardpoints not really at the origin?
+ if ( Distance( hpOrigin, baseOrigin ) < SAFE_TITANFALL_DISTANCE )
+ return true
+ }
+
+ foreach ( flagSpawnPoint in level.testFlagSpawnPoints )
+ {
+ local fspOrigin = flagSpawnPoint.GetOrigin()
+ if ( Distance( fspOrigin, baseOrigin ) < SAFE_TITANFALL_DISTANCE_CTF )
+ return true
+ }
+
+ foreach ( blocker in level.titanfallBlockers )
+ {
+ if ( Distance2D( baseOrigin, blocker.origin ) > blocker.radius )
+ continue
+
+ if ( baseOrigin.z < blocker.origin.z )
+ continue
+
+ if ( baseOrigin.z > blocker.maxHeight )
+ continue
+
+ return true
+ }
+
+ return false
+}
+
+function DevCheckInTitanfallBlocker()
+{
+ if ( "toggleBlocker" in svGlobal.levelEnt.s )
+ {
+ svGlobal.levelEnt.s.toggleBlocker.Kill_Deprecated_UseDestroyInstead()
+ delete svGlobal.levelEnt.s.toggleBlocker
+ return
+ }
+
+ svGlobal.levelEnt.s.toggleBlocker <- CreateScriptRef()
+ svGlobal.levelEnt.s.toggleBlocker.EndSignal( "OnDestroy" )
+
+ entity player = GetPlayerArray()[0]
+ for ( ;; )
+ {
+ printt( "Inside Titanfall blocker: " + NearTitanfallBlocker( player.GetOrigin() ) )
+ DrawTitanfallBlockers()
+ wait 0.5
+ }
+}
+
+function DrawTitanfallBlockers()
+{
+ foreach ( hardpoint in level.testHardPoints )
+ {
+ vector hpOrigin = expect entity( hardpoint ).GetOrigin()
+ DebugDrawCircle( hpOrigin, Vector(0,0,0), SAFE_TITANFALL_DISTANCE, 255, 255, 0, true, 1.0 )
+ }
+
+ foreach ( flagSpawnPoint in level.testFlagSpawnPoints )
+ {
+ vector fspOrigin = expect entity( flagSpawnPoint ).GetOrigin()
+ DebugDrawCircle( fspOrigin, Vector(0,0,0), SAFE_TITANFALL_DISTANCE_CTF, 255, 255, 0, true, 1.0 )
+ }
+
+ foreach ( blocker in level.titanfallBlockers )
+ {
+ DebugDrawCircle( expect vector( blocker.origin ), Vector(0,0,0), expect float( blocker.radius ), 255, 255, 0, true, 1.0 )
+ vector org = Vector( blocker.origin.x, blocker.origin.y, blocker.maxHeight )
+ DebugDrawCircle( org, Vector(0,0,0), expect float( blocker.radius ), 255, 255, 0, true, 1.0 )
+ }
+}
+
+
+
+bool function EdgeTraceDropPoint( vector dropPoint )
+{
+ local offsetArray = [
+ Vector( 64,64,0 ),
+ Vector( -64,64,0 ),
+ Vector( 64,-64,0 ),
+ Vector( -64,-64,0 ),
+ ]
+ local maxDif = 48
+ local mask = TRACE_MASK_TITANSOLID | TRACE_MASK_PLAYERSOLID | TRACE_MASK_SOLID | TRACE_MASK_NPCSOLID
+ local totalDif = 0
+
+ foreach ( offset in offsetArray )
+ {
+ local startPos = dropPoint + Vector( 0, 0, 64 ) + offset
+ local endPos = dropPoint + Vector( 0, 0, -64 ) + offset
+ TraceResults result = TraceLine( startPos, endPos, null, mask, TRACE_COLLISION_GROUP_NONE )
+ local dif = fabs( result.endPos.z - dropPoint.z )
+ totalDif += dif
+
+ if ( dif > maxDif )
+ {
+ //DebugDrawLine( startPos, result.endPos, 200, 50, 50, true, 3 )
+ return false
+ }
+ //DebugDrawLine( startPos, result.endPos, 50, 50, 200, true, 3 )
+ }
+
+ if ( totalDif > ( maxDif * 2 ) )
+ {
+ // this should catch cases where a small item like a box or barrel stops the hull collision trace above the ground.
+ return false
+ }
+
+ return true
+}
+
+
+bool function DropPodFindDropNodes( FlightPath flightPath, vector origin, float yaw )
+{
+ if ( NearTitanfallBlocker( origin ) )
+ return false
+
+ //level.drawAnalysisPreview = true
+ if ( !TitanTestDropPoint( origin, flightPath ) )
+ return false
+
+ return EdgeTraceDropPoint( origin )
+}
+
+bool function TitanTestDropPoint( vector start, FlightPath flightPath )
+{
+ local draw = level.drawAnalysisPreview
+ local end = start + Vector(0,0,8000)
+
+ TraceResults result = TraceHull( start, end, flightPath.mins, flightPath.maxs, null, flightPath.traceMask, TRACE_COLLISION_GROUP_NONE )
+ if ( result.startSolid )
+ {
+ if ( draw )
+ {
+ DrawArrow( start, Vector(0,0,0), 5.0, 80 )
+ DebugDrawLine( start, result.endPos, 0, 255, 0, true, 5.0 )
+ DebugDrawLine( result.endPos, end, 255, 0, 0, true, 5.0 )
+ //local newstart = start + Vector(0,0,150)
+ //local reresult = TraceHull( newstart, start, flightPath.mins, flightPath.maxs, null, flightPath.traceMask, TRACE_COLLISION_GROUP_NONE )
+ //printt( "surface " + reresult.surfaceName )
+ //DebugDrawLine( newstart, reresult.endPos, 155, 0, 0, true, ANALYSIS_PREVIEW_TIME )
+ //DrawArrow( reresult.endPos, Vector(0,0,0), ANALYSIS_PREVIEW_TIME, 15 )
+ //
+// //DrawArrow( start, Vector(0,0,0), ANALYSIS_PREVIEW_TIME, 15 )
+ //DebugDrawLine( start, result.endPos, 255, 0, 0, true, ANALYSIS_PREVIEW_TIME )
+ //printt( "length " + Length( start - result.endPos ) )
+ }
+ return false
+ }
+
+ if ( result.fraction < 1 )
+ {
+ if ( result.hitSky )
+ {
+ if ( draw )
+ {
+ DebugDrawLine( start, end, 0, 0, 255, true, ANALYSIS_PREVIEW_TIME )
+ //DrawArrow( start, Vector(0,0,0), 1.0, 100 )
+ }
+ return true
+ }
+
+// if ( draw )
+// DebugDrawLine( orgs[i-1] + Vector(10,10,10), orgs[i]+ Vector(10,10,10), 255, 255, 0, true, ANALYSIS_PREVIEW_TIME )
+
+ // some fudge factor
+ if ( Distance( result.endPos, end ) > 16 )
+ {
+ if ( draw )
+ {
+ local offset = Vector(-0.1, -0.1, 0 )
+ DebugDrawLine( start + offset, result.endPos + offset, 0, 255, 0, true, ANALYSIS_PREVIEW_TIME )
+ DebugDrawLine( result.endPos + offset, end + offset, 255, 0, 0, true, ANALYSIS_PREVIEW_TIME )
+ //DebugDrawLine( start, end, 255, 0, 0, true, ANALYSIS_PREVIEW_TIME )
+ }
+ return false
+ }
+ }
+
+// DebugDrawLine( orgs[i-1], orgs[i], 0, 255, 0, true, ANALYSIS_PREVIEW_TIME )
+
+ if ( draw )
+ DebugDrawLine( start, end, 0, 255, 0, true, 0.2 )
+ return true
+}
+
+
+
+
+
+void function TitanFall_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ if ( !ent.IsPlayer() )
+ return
+
+ if ( !ent.IsTitan() )
+ return
+
+ vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo )
+ vector entityOrigin = ent.GetOrigin()
+ local distance = Distance( entityOrigin, damageOrigin )
+
+ // on top of them, let the titans fall where they may
+ if ( distance < TITANFALL_INNER_RADIUS )
+ return
+
+ if ( IsTitanWithinBubbleShield( ent ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ vector pushVector = Normalize( entityOrigin - damageOrigin )
+
+ vector traceEndOrigin = damageOrigin + (pushVector * TITANFALL_OUTER_RADIUS)
+ TraceResults traceResult = TraceHull( damageOrigin, traceEndOrigin, ent.GetBoundingMins(), ent.GetBoundingMins(), ent, TRACE_MASK_NPCSOLID_BRUSHONLY, TRACE_COLLISION_GROUP_NONE )
+
+ // no room to push them
+ if ( traceResult.fraction < 0.85 )
+ return
+
+ DamageInfo_ScaleDamage( damageInfo, 0.15 )
+
+ ent.SetVelocity( pushVector * 400 )
+ ent.SetStaggering()
+}
+
+function PlayDeathFromTitanFallSounds( player )
+{
+ if ( player.IsTitan() )
+ {
+ //printt( "Playing titanfall_on_titan at: "+ player.GetOrigin() )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, player.GetOrigin(), "titanfall_on_titan" )
+ }
+ else
+ {
+ //printt( "Playing titanfall_on_human at " + player.GetOrigin() )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, player.GetOrigin(), "titanfall_on_human" )
+ }
+}
+
+bool function NPCShouldDoBubbleShieldAfterHotdrop( entity titan )
+{
+ if ( titan.HasKey( "script_hotdrop" ) )
+ {
+ switch ( titan.kv.script_hotdrop )
+ {
+ case "4":
+ case "3":
+ printt( "DROP WITH NO BUBBLE" )
+ return false
+ }
+ }
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_triple_health.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_triple_health.gnut
new file mode 100644
index 00000000..7515b868
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan/_titan_triple_health.gnut
@@ -0,0 +1,524 @@
+untyped
+
+global function HealthRegenInit
+
+global function TitanLoseSegementFX //JFS: Only being used for Rodeo now, rename later if needed
+global function GibBodyPart
+
+const SEGMENT_DOWN_SOUNDS_3P = [
+ "titan_healthbar_tier3_down_3P_vs_3P", // 0 left (doom)
+ "titan_healthbar_tier2_down_3P_vs_3P", // 1 left
+ "titan_healthbar_tier1_down_3P_vs_3P", // 2 left
+ "titan_healthbar_tier1_down_3P_vs_3P" // shield gone
+]
+
+const SEGMENT_DOWN_SOUNDS_3P_ATTACKER = [
+ "titan_healthbar_tier3_down_1P_vs_3P", // 0 left (doom)
+ "titan_healthbar_tier2_down_1P_vs_3P", // 1 left
+ "titan_healthbar_tier1_down_1P_vs_3P", // 2 left
+ "titan_healthbar_tier1_down_1P_vs_3P" // shield gone
+]
+
+const SEGMENT_DOWN_SOUNDS_1P = [
+ "titan_healthbar_tier3_down_1P", // 0 left (doom)
+ "titan_healthbar_tier2_down_1P", // 1 left
+ "titan_healthbar_tier1_down_1P", // 2 left
+ "titan_healthbar_tier1_down_1P" // shield gone
+]
+
+const DAMAGE_FORGIVENESS_CEILING = 1200.0
+const DAMAGE_FORGIVENESS_FLOOR = 500.0
+const LOW_HEALTH_WARNING_SOUND = "Weapon_Vortex_Gun.ExplosiveWarningBeep"
+
+const TITAN_DAMAGE_MITIGATION_DAMAGESCALE = 0.5
+
+struct {
+ int shieldDecayRate = 2
+
+ table< entity, table< entity, float > > soulToSoulDamageMemory
+} file;
+
+function HealthRegenInit()
+{
+ if ( GetCurrentPlaylistVarInt( "titan_health_decay_amount", 0 ) > 0 )
+ {
+ AddSoulInitFunc( TitanHealthDecayThink )
+ }
+
+ AddSoulInitFunc( TitanHealthRegenThink )
+
+ if ( TitanShieldDecayEnabled() )
+ {
+ AddSoulInitFunc( TitanShieldDecayThink )
+ }
+
+ AddDamageCallback( "player", TitanSegmentedHealth_OnDamage )
+ AddDamageCallback( "npc_titan", TitanSegmentedHealth_OnDamage )
+ AddCallback_OnTitanDoomed( OnTitanDoomed )
+
+ RegisterSignal( "HealthSegmentLost" )
+}
+
+
+void function TitanHealthDecayThink( entity soul )
+{
+ thread TitanHealthDecayThinkInternal( soul )
+}
+
+void function TitanHealthDecayThinkInternal( entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "OnTitanDeath" )
+
+ soul.SetShieldHealth( 0 )
+
+ while ( 1 )
+ {
+ entity titan = soul.GetTitan()
+ int damageAmout = GetCurrentPlaylistVarInt( "titan_health_decay_amount", 0 )
+ titan.TakeDamage( damageAmout, null, null, { scriptType = DF_DOOMED_HEALTH_LOSS, damageSourceId = damagedef_suicide } )
+ WaitFrame()
+ }
+}
+
+void function TitanHealthRegenThink( entity soul )
+{
+ thread TitanHealthRegenThink_Internal( soul )
+}
+
+void function TitanHealthRegenThink_Internal( entity soul )
+{
+ soul.EndSignal( SIGNAL_TITAN_HEALTH_REGEN )
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "OnDestroy" )
+
+ if ( !soul.soul.regensHealth )
+ return
+
+ entity titan = soul.GetTitan()
+
+ if ( !IsValid( titan ) )
+ return
+
+ int healthPerTab = GetSegmentHealthForTitan( titan )
+
+ // set this if AI titans need to be aware of segment health. Not used currently
+ //titan.SetHealthPerSegment( healthPerTab )
+
+ int lastTitanHealth = titan.GetHealth()
+ bool regenSound = false
+ int maxHealth = titan.GetMaxHealth()
+ float lastTime = Time()
+
+ while ( 1 )
+ {
+ titan = soul.GetTitan()
+ if ( !IsAlive( titan ) )
+ return
+ int titanHealth = titan.GetHealth()
+ Assert( titan )
+
+ if ( !titan.IsTitan() )
+ return
+
+ if ( !soul.soul.regensHealth )
+ return
+
+ int currentRegenTab = GetTitanCurrentRegenTab( titan )
+
+ if ( currentRegenTab != GetSoulBatteryCount( soul ) )
+ SetSoulBatteryCount( soul, GetTitanCurrentRegenTab( titan ) )
+
+ int maxHealthForCurrentTab = currentRegenTab * healthPerTab
+
+ if ( titanHealth == maxHealthForCurrentTab )
+ {
+ if ( regenSound )
+ {
+ StopSoundOnEntity( titan, "titan_energyshield_up" )
+ regenSound = false
+ }
+ }
+
+ lastTitanHealth = titanHealth
+ lastTime = Time()
+ WaitFrame()
+ }
+}
+
+void function TitanSegmentedHealth_OnDamage( entity titan, var damageInfo )
+{
+ if ( !titan.IsTitan() )
+ return
+
+ entity soul = titan.GetTitanSoul()
+
+ if ( !IsValid( soul ) )
+ return
+
+ if ( ShouldReduceDamageForSegmentedHealth( soul, damageInfo ) )
+ DamageInfo_ScaleDamage( damageInfo, 0.3 )
+
+ thread TitanSegmentedHealth_OnDamage_Thread( soul, damageInfo )
+}
+
+bool function ShouldReduceDamageForSegmentedHealth( entity soul, damageInfo )
+{
+ if ( !soul.soul.rebooting )
+ return false
+
+ if ( IsRodeoDamageFromBatteryPack( soul, damageInfo ) )
+ return false
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return false
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOM_FATALITY )
+ return false
+
+ return true
+}
+
+function TitanSegmentedHealth_OnDamage_Thread( entity soul, damageInfo )
+{
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "Doomed" )
+
+ entity titan = soul.GetTitan()
+
+ vector damageOrigin = GetDamageOrigin( damageInfo, titan )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ int hitBox = DamageInfo_GetHitBox( damageInfo )
+
+ int healthFloor = CalculateHealthFloorForDamage( soul, titan, damageInfo )
+
+ bool skipDoom = ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_SKIPS_DOOMED_STATE ) > 0
+
+
+ WaitEndFrame()
+
+ titan = soul.GetTitan()
+ Assert( IsValid( titan ) )
+
+ if ( soul.soul.lastSegmentLossTime >= Time() )
+ return
+
+ if ( GetDoomedState( titan ) )
+ return
+
+ if ( titan.GetHealth() > healthFloor )
+ return
+
+ if ( !IsAlive( titan ) )
+ return
+
+ string settings = GetSoulPlayerSettings( soul )
+ if ( Dev_GetPlayerSettingByKeyField_Global( settings, "use_damage_states" ) == 1 )
+ UpdateDamageStateForTab( titan, GetTitanCurrentRegenTab( titan ), hitBox )
+
+ thread TitanLoseSegement( soul, titan, damageOrigin, damageAmount, attacker )
+}
+
+int function CalculateHealthFloorForDamage( entity soul, entity titan, damageInfo )
+{
+ //Lets you bypass the health segment limitation and remove an entire health segment.
+ /*if ( IsRodeoDamageFromBatteryPack( soul, damageInfo ) )
+ return minint( 0, int ( titan.GetHealth() - DamageInfo_GetDamage( damageInfo ) ) )
+ */
+ int oldTab = GetTitanCurrentRegenTab( titan )
+ return ( oldTab - 1 ) * GetSegmentHealthForTitan( titan )
+}
+
+void function TitanLoseSegement( entity soul, entity titan, vector damageOrigin, float damageAmount, entity attacker )
+{
+ if ( !IsValid( soul ) )
+ return
+
+ if ( !IsValid( titan ) )
+ return
+
+ if ( soul.soul.lastSegmentLossTime >= Time() )
+ return
+
+ soul.soul.lastSegmentLossTime = Time()
+
+ entity player
+ if ( titan.IsPlayer() )
+ player = titan
+
+ foreach ( callbackFunc in svGlobal.onTitanHealthSegmentLostCallbacks )
+ {
+ callbackFunc( titan, attacker )
+ }
+
+ // Added via AddTitanCallback_OnHealthSegmentLost
+ foreach ( callbackFunc in titan.e.entSegmentLostCallbacks )
+ {
+ callbackFunc( titan, attacker )
+ }
+
+ GiveDefenderAmmo( titan )
+
+ titan.Signal( "HealthSegmentLost" )
+
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "OnDestroy" )
+
+ if ( GetCurrentPlaylistVarInt( "titan_health_chicklet_fx", 0 ) == 1 )
+ TitanLoseSegementFX( titan, attacker, damageOrigin )
+
+ SetSoulBatteryCount( soul, GetTitanCurrentRegenTab( titan ) )
+}
+
+void function TitanLoseSegementFX( entity titan, entity attacker, vector damageOrigin )
+{
+ int handle = titan.GetEncodedEHandle()
+ int handleAttacker = -1
+
+ if ( IsValid( attacker ) )
+ handleAttacker = attacker.GetEncodedEHandle()
+
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_TitanLostHealthSegment", handle, handleAttacker, damageOrigin.x, damageOrigin.y, damageOrigin.z )
+ }
+
+ if ( !IsAlive( titan ) )
+ return
+
+ int currentRegenTab = minint( GetTitanCurrentRegenTab( titan ), SEGMENT_DOWN_SOUNDS_3P_ATTACKER.len()-1 )
+ if ( currentRegenTab < SEGMENT_DOWN_SOUNDS_3P_ATTACKER.len() )
+ {
+ if ( titan.IsPlayer() && IsAlive( attacker ) && attacker.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, attacker, SEGMENT_DOWN_SOUNDS_3P_ATTACKER[ currentRegenTab ] )
+ EmitSoundOnEntityOnlyToPlayer( titan, titan, SEGMENT_DOWN_SOUNDS_1P[ currentRegenTab ] )
+
+ // need a command here to play for not victim and not attacker
+ EmitSoundOnEntityExceptToPlayer( titan, titan, SEGMENT_DOWN_SOUNDS_3P[ currentRegenTab ] )
+ }
+ else if ( IsAlive( attacker ) && attacker.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, attacker, SEGMENT_DOWN_SOUNDS_3P_ATTACKER[ currentRegenTab ] )
+ EmitSoundOnEntityExceptToPlayer( titan, attacker, SEGMENT_DOWN_SOUNDS_3P[ currentRegenTab ] )
+ }
+ else if ( titan.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, titan, SEGMENT_DOWN_SOUNDS_1P[ currentRegenTab ] )
+ EmitSoundOnEntityExceptToPlayer( titan, titan, SEGMENT_DOWN_SOUNDS_3P[ currentRegenTab ] )
+ }
+ else
+ {
+ EmitSoundOnEntity( titan, SEGMENT_DOWN_SOUNDS_3P[ currentRegenTab ] )
+ }
+ }
+}
+
+
+void function UpdateDamageStateForTab( entity titan, int tab, int hitBox )
+{
+ if ( hitBox == -1 ) // not every hitbox has data defined
+ return
+
+ var bodyGroup = titan.GetBodyGroupNameFromHitboxId( hitBox ) // can be null
+
+ if ( bodyGroup == null )
+ {
+ printt( "bodyGroup was null" )
+ return
+ }
+
+#if MP
+ // these are flipped on purpose to prevent both legs or arms from being blown up
+ switch ( bodyGroup )
+ {
+ case "left_leg":
+ if ( IsBroken( titan, "right_leg" ) )
+ return
+ break
+
+ case "right_leg":
+ if ( IsBroken( titan, "left_leg" ) )
+ return
+ break
+
+ case "left_arm":
+ if ( IsBroken( titan, "right_arm" ) )
+ return
+ break
+
+ case "right_arm":
+ if ( IsBroken( titan, "left_arm" ) )
+ return
+ break
+
+ default:
+ return
+ }
+
+ GibBodyPart( titan, bodyGroup )
+#else
+ int maxTab = 3
+ int count = maxTab - tab
+
+ RecursiveGibBodyPart( titan, bodyGroup, count )
+#endif
+}
+
+void function RecursiveGibBodyPart( entity titan, var bodyGroup, int count )
+{
+ GibBodyPart( titan, bodyGroup )
+
+ count -= 1
+ if ( count <= 0 )
+ return
+
+ foreach ( siblingName in titan.s.skeletonData[bodyGroup].siblings )
+ {
+ // printt( count + " recurse: " + siblingName )
+ RecursiveGibBodyPart( titan, siblingName, count )
+ }
+}
+
+bool function IsBroken( entity titan, var bodyGroup )
+{
+ local bodyGroupIndex = titan.FindBodyGroup( bodyGroup )
+ local stateCount = GetStateCountForBodyGroup( titan, bodyGroup )
+ local bodyGroupState = titan.GetBodyGroupState( bodyGroupIndex )
+
+ //return ( bodyGroupState >= (stateCount - 1) )
+ return ( bodyGroupState > 0 )
+}
+
+void function GibBodyPart( entity titan, var bodyGroup )
+{
+ // if ( IsBodyGroupBroken( titan, bodyGroup ) )
+ // return
+
+ // titan.s.damageStateInfo[bodyGroup] = 1
+
+ local bodyGroupIndex = titan.FindBodyGroup( bodyGroup )
+ local stateCount = GetStateCountForBodyGroup( titan, bodyGroup )
+ local bodyGroupState = titan.GetBodyGroupState( bodyGroupIndex )
+
+ if ( bodyGroupState >= (stateCount - 1) )
+ return
+
+ titan.SetBodygroup( bodyGroupIndex, bodyGroupState + 1 )
+ // printt( "break: " + bodyGroup )
+}
+
+function GiveAttackerAmmo( entity titan )
+{
+}
+
+void function TemporaryInvul( entity titan )
+{
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "OnDeath" )
+ if ( titan.IsPlayer() )
+ {
+ titan.EndSignal( "DisembarkingTitan" )
+ titan.EndSignal( "TitanEjectionStarted" )
+ }
+
+ OnThreadEnd(
+ function() : ( titan )
+ {
+ if ( IsValid( titan ) )
+ titan.ClearInvulnerable()
+ }
+ )
+
+ titan.SetInvulnerable()
+ wait 0.25
+}
+
+void function GiveDefenderAmmo( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+
+ if ( IsSingleplayer() )
+ {
+ if ( titan.IsNPC() )
+ {
+ soul.SetNextCoreChargeAvailable( soul.GetNextCoreChargeAvailable() + 0.5 ) // shave time off core timer
+ }
+ }
+}
+
+void function OnTitanDoomed( entity titan, var damageInfo )
+{
+
+ if ( !IsAlive( titan ) )
+ return
+
+ entity soul = titan.GetTitanSoul()
+
+ if ( titan.IsPlayer() )
+ {
+ if ( SoulHasPassive( soul, ePassives.PAS_RONIN_AUTOSHIFT ) )
+ PhaseShift( titan, 0, 3.0 )
+
+ if ( SoulHasPassive( soul, ePassives.PAS_AUTO_EJECT ) )
+ return
+ }
+
+ soul.nextHealthRegenTime = Time()
+
+ vector damageOrigin = GetDamageOrigin( damageInfo, titan )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( TitanDamageRewardsTitanCoreTime() && (titan != attacker) )
+ {
+ AddCreditToTitanCoreBuilderForDoomEntered( titan )
+ if ( attacker.IsTitan() )
+ AddCreditToTitanCoreBuilderForDoomInflicted( attacker )
+ }
+
+ thread TitanLoseSegement( soul, titan, damageOrigin, damageAmount, attacker )
+
+ if ( SoulHasPassive( soul, ePassives.PAS_DOOMED_TIME ) )
+ return
+
+ if ( NoWeaponDoomState() )
+ TakeAllWeapons( titan )
+}
+
+void function OnTitanDeath( entity titan, var damageInfo )
+{
+ if ( !titan.IsTitan() )
+ return
+
+ if ( !PROTO_AlternateDoomedState() )
+ return
+
+ entity soul = titan.GetTitanSoul()
+ vector damageOrigin = GetDamageOrigin( damageInfo, titan )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ thread TitanLoseSegement( soul, titan, damageOrigin, damageAmount, attacker )
+}
+
+void function TitanShieldDecayThink( entity soul )
+{
+ thread TitanShieldDecayThinkInternal( soul )
+}
+
+void function TitanShieldDecayThinkInternal( entity soul )
+{
+ soul.EndSignal( "OnDestroy" ) //This needs to be OnDestroy instead of OnDeath because souls don't have a death animation
+ soul.EndSignal( "OnTitanDeath" )
+
+ while ( 1 )
+ {
+ if ( Time() >= soul.e.nextShieldDecayTime && !TitanHasRegenningShield( soul ) )
+ soul.SetShieldHealth( maxint( soul.GetShieldHealth() - file.shieldDecayRate, 0 ) )
+ WaitFrame()
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan/class_titan.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan/class_titan.gnut
new file mode 100644
index 00000000..5f72385e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan/class_titan.gnut
@@ -0,0 +1,77 @@
+untyped
+
+global function ClassTitan_Init
+
+global function Titan_AddPlayer
+global function Titan_OnPlayerDeath
+global function ClientCommand_TitanEject
+global function ApplyTitanLoadoutModifiers
+
+
+const TITAN_HATCHCOMMANDANIMTIME = 1.5 // cooldown time between toggling the cockpit state. Will be needed when we have animations to play
+
+const COCKPIT_JOLT_DAMAGE_MIN = 1
+const COCKPIT_JOLT_DAMAGE_MAX = 200
+const TITAN_STUMBLE_HEALTH_PERCENTAGE = 0.5
+
+string thisClassName = "titan"
+
+function ClassTitan_Init()
+{
+
+ AddClientCommandCallback( "TitanEject", ClientCommand_TitanEject ) //
+}
+
+function Titan_AddPlayer( player )
+{
+ player.playerClassData[thisClassName] <- {}
+ player.s.lastStaggerTime <- 0
+}
+
+
+// TODO: There should be an equivalent function for pilots
+TitanLoadoutDef function ApplyTitanLoadoutModifiers( entity player, TitanLoadoutDef loadout )
+{
+ return loadout
+}
+
+void function Titan_OnPlayerDeath( entity player, var damageInfo )
+{
+ player.p.storedWeapons.clear()
+}
+
+bool function PlayerCanEject( entity player )
+{
+ if ( !IsAlive( player ) )
+ return false
+
+ if ( !player.IsTitan() )
+ return false
+
+ if ( Riff_TitanExitEnabled() == eTitanExitEnabled.Never )
+ return false
+
+ //if ( !CanDisembark( player ) )
+ // return false
+
+ if ( IsPlayerDisembarking( player ) )
+ return false
+
+ if ( TitanEjectIsDisabled() )
+ return false
+
+ return true
+}
+
+bool function ClientCommand_TitanEject( entity player, array<string> args )
+{
+ if ( !PlayerCanEject( player ) )
+ return true
+
+ int ejectPressCount = args[ 0 ].tointeger()
+ if ( ejectPressCount < 3 )
+ return true
+
+ thread TitanEjectPlayer( player )
+ return true
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/titan_xp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/titan_xp.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/titan_xp.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/vehicle/_vehicle_behavior.gnut b/Northstar.CustomServers/mod/scripts/vscripts/vehicle/_vehicle_behavior.gnut
new file mode 100644
index 00000000..2d0dd920
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/vehicle/_vehicle_behavior.gnut
@@ -0,0 +1,6 @@
+global function CodeCallback_OnVehiclePass
+
+// params: vehicle, nodePassed, nextNode, nextNextNode
+void function CodeCallback_OnVehiclePass( table params )
+{
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/vehicle/_vehicle_dropship_new.nut b/Northstar.CustomServers/mod/scripts/vscripts/vehicle/_vehicle_dropship_new.nut
new file mode 100644
index 00000000..87010ca7
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/vehicle/_vehicle_dropship_new.nut
@@ -0,0 +1,528 @@
+untyped
+
+global function VehicleDropshipNew_Init
+
+global function InitLeanDropship
+global function CreateDropship
+global function GetDropshipSquadSize
+global function WarpinEffect
+global function WarpoutEffect
+global function WarpoutEffectFPS
+global function WaitForNPCsDeployed
+global function CreateNPCSForDropship
+global function GuyDeploysOffShip
+global function WaittillPlayDeployAnims
+global function GetDropshipRopeAttachments
+global function DelayDropshipDelete
+global function PlayDropshipRampDoorOpenSound
+global function PlayWarpFxOnPlayers
+global function DefensiveFreePlayers
+global function CreateDropshipAnimTable
+
+global function InitDropShipFlightPaths
+
+// _dropship
+//
+// dropship/passenger setup:
+// 1. Create an npc_dropship and target it with a point_template. Give the point_template a name.
+// 2. npc_dropship targets starting path_track (see below for path setup info) AND passenger spawning point_template
+// 3. passenger spawning point_template points to six passengers that will spawn and rappel at the unload spot
+//
+// path/unload spot setup:
+// 1. create your path out of path_track entities
+// 2. to create rappel targets, add six info_targets on the ground around your unload node, name them all the same
+// 3. on the node where the dropship will unload, create a key/value pair called "unload" and point it at the name you gave to the rappel target info_targets
+//
+// spawning in script:
+// 1. use GetEnt to get the point_template that targets the dropship
+// 2. pass that point_template into DropshipSpawn(), this returns with the dropship ent
+// 3. pass the dropship ent into DropshipDropshipFlyPathAndUnload() (thread it off if you want to do stuff right after)
+//
+
+
+const FX_DROPSHIP_THRUSTERS = $"xo_atlas_jet_large"
+const FX_GUNSHIP_DAMAGE = $"veh_gunship_damage_FULL"
+const FX_DROPSHIP_DEATH = $"P_veh_exp_crow"
+
+const DROPSHIP_ROPE_ENDPOINT_FX = $"runway_light_blue"
+const DROPSHIP_ACL_LIGHT_GREEN_FX = $"acl_light_green"
+const DROPSHIP_ACL_LIGHT_RED_FX = $"acl_light_red"
+const DROPSHIP_ACL_LIGHT_WHITE_FX = $"acl_light_white"
+
+const ENGAGEMENT_DIST = 1024
+const ENGAGEMENT_DIST_SQD = ENGAGEMENT_DIST * ENGAGEMENT_DIST
+
+const DEFAULT_READYANIM_BLENDTIME = 1.0
+
+table file = {
+ dropshipAttachments = null
+}
+
+function VehicleDropshipNew_Init()
+{
+
+ RegisterSignal( "sRampOpen" )
+ RegisterSignal( "sRampClose" )
+
+ #if SERVER
+ PrecacheParticleSystem( FX_GUNSHIP_CRASH_EXPLOSION_ENTRANCE )
+ PrecacheParticleSystem( FX_GUNSHIP_CRASH_EXPLOSION_EXIT )
+ PrecacheParticleSystem( FX_DROPSHIP_DEATH )
+ AddDeathCallback( "npc_dropship", OnNPCDropshipDeath )
+ AddDamageCallback( "npc_dropship", OnDropshipDamaged )
+ #endif
+
+ PrecacheParticleSystem( FX_HORNET_DEATH )
+ PrecacheParticleSystem( FX_GUNSHIP_DAMAGE )
+ PrecacheParticleSystem( FX_DROPSHIP_THRUSTERS )
+ PrecacheParticleSystem( DROPSHIP_ROPE_ENDPOINT_FX )
+ PrecacheParticleSystem( DROPSHIP_ACL_LIGHT_GREEN_FX )
+ PrecacheParticleSystem( DROPSHIP_ACL_LIGHT_RED_FX )
+ PrecacheParticleSystem( DROPSHIP_ACL_LIGHT_WHITE_FX )
+
+ level.DROPSHIP_DEFAULT_AIRSPEED <- 750
+
+ PrecacheEntity( "keyframe_rope" )
+ PrecacheModel( DROPSHIP_MODEL )
+
+ PrecacheSprite( $"sprites/laserbeam.vmt" )
+ PrecacheSprite( $"sprites/glow_05.vmt" )
+
+
+ //Array of all attachments in the dropship model. Used in DropshipDamageEffects
+ local names = []
+ names.append( "FRONT_TURRET" )
+ names.append( "BOMB_L" )
+ names.append( "BOMB_R" )
+ names.append( "Spotlight" )
+ names.append( "Light_Red0" )
+ names.append( "Light_Red1" )
+ names.append( "Light_Red2" )
+ names.append( "Light_Red3" )
+ names.append( "HeadlightLeft" )
+ names.append( "RopeAttachLeftA" )
+ names.append( "RopeAttachLeftB" )
+ names.append( "RopeAttachLeftC" )
+ names.append( "L_exhaust_rear_1" )
+ names.append( "L_exhaust_rear_2" )
+ names.append( "L_exhaust_front_1" )
+ names.append( "Light_Green0" )
+ names.append( "Light_Green1" )
+ names.append( "Light_Green2" )
+ names.append( "Light_Green3" )
+ names.append( "HeadlightRight" )
+ names.append( "RopeAttachRightA" )
+ names.append( "RopeAttachRightB" )
+ names.append( "RopeAttachRightC" )
+ names.append( "R_exhaust_rear_1" )
+ names.append( "R_exhaust_rear_2" )
+ names.append( "R_exhaust_front_1" )
+
+ file.dropshipAttachments = names
+
+ level.DSAIziplineAnims <- {}
+ level.DSAIziplineAnims[ "left" ] <- []
+ level.DSAIziplineAnims[ "left" ].append( { idle = "pt_dropship_rider_L_A_idle", attach = "RopeAttachLeftA" } )
+ level.DSAIziplineAnims[ "left" ].append( { idle = "pt_dropship_rider_L_C_idle", attach = "RopeAttachLeftC" } )
+ level.DSAIziplineAnims[ "left" ].append( { idle = "pt_dropship_rider_L_B_idle", attach = "RopeAttachLeftB" } )
+
+ level.DSAIziplineAnims[ "right" ] <- []
+ level.DSAIziplineAnims[ "right" ].append( { idle = "pt_dropship_rider_R_A_idle", attach = "RopeAttachRightA" } )
+ level.DSAIziplineAnims[ "right" ].append( { idle = "pt_dropship_rider_R_C_idle", attach = "RopeAttachRightC" } )
+ level.DSAIziplineAnims[ "right" ].append( { idle = "pt_dropship_rider_R_B_idle", attach = "RopeAttachRightB" } )
+
+ level.DSAIziplineAnims[ "both" ] <- []
+ level.DSAIziplineAnims[ "both" ].append( { idle = "pt_dropship_rider_L_A_idle", attach = "RopeAttachLeftA" } )
+ level.DSAIziplineAnims[ "both" ].append( { idle = "pt_dropship_rider_R_A_idle", attach = "RopeAttachRightA" } )
+ level.DSAIziplineAnims[ "both" ].append( { idle = "pt_dropship_rider_L_C_idle", attach = "RopeAttachLeftC" } )
+ level.DSAIziplineAnims[ "both" ].append( { idle = "pt_dropship_rider_R_B_idle", attach = "RopeAttachRightC" } )
+ level.DSAIziplineAnims[ "both" ].append( { idle = "pt_dropship_rider_L_B_idle", attach = "RopeAttachLeftB" } )
+ level.DSAIziplineAnims[ "both" ].append( { idle = "pt_dropship_rider_R_B_idle", attach = "RopeAttachRightB" } )
+}
+
+function InitLeanDropship( dropship )
+{
+ if ( dropship.kv.desiredSpeed.tofloat() <= 0 )
+ {
+ dropship.kv.desiredSpeed = level.DROPSHIP_DEFAULT_AIRSPEED
+ }
+
+ //dropship.s.dropFunc <- ShipDropsGuys
+}
+
+array<entity> function CreateNPCSForDropship( entity ship, array<entity functionref( int, vector, vector )> spawnFuncs, string side = "both" )
+{
+ int count = minint( spawnFuncs.len(), level.DSAIziplineAnims[ side ].len() )
+
+ int team = ship.GetTeam()
+ string squadName = expect string( ship.kv.squadname )
+ vector origin = ship.GetOrigin()
+ vector angles = ship.GetAngles()
+
+ array<entity> guys = []
+
+ if ( Flag( "disable_npcs" ) )
+ return guys //i.e. empty aray
+
+ local guy
+
+ // this is to maintain sketchy support for just passing an array of 1 spawn function
+ entity functionref( int, vector, vector ) spawnFunc = spawnFuncs[0]
+
+ for ( int i = 0; i < count; i++ )
+ {
+ if ( i < spawnFuncs.len() )
+ spawnFunc = spawnFuncs[i]
+
+ entity guy = spawnFunc( team, origin, angles )
+ guy.kv.squadname = squadName
+ DispatchSpawn( guy )
+
+ if ( !IsAlive( guy ) )
+ continue
+
+ guys.append( guy )
+
+ local seat = i
+ table Table = CreateDropshipAnimTable( ship, side, seat )
+
+ thread GuyDeploysOffShip( guy, Table )
+ }
+
+ return guys
+}
+
+
+
+table function CreateDropshipAnimTable( ship, side, seat )
+{
+ table Table
+
+ Table.idleAnim <- level.DSAIziplineAnims[ side ][ seat ].idle
+ Table.deployAnim <- "zipline"
+ Table.shipAttach <- level.DSAIziplineAnims[ side ][ seat ].attach
+ Table.attachIndex <- null
+ Table.ship <- ship
+ Table.side <- side
+ Table.blendTime <- DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME
+
+ return Table
+}
+
+function WaitForNPCsDeployed( npcArray )
+{
+ local ent = CreateScriptRef()
+ ent.s.count <- 0
+
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ if ( IsValid( ent ) )
+ ent.Kill_Deprecated_UseDestroyInstead()
+ }
+ )
+
+ local func =
+ function( ent, guy )
+ {
+ ent.s.count++
+
+ WaitSignal( guy, "npc_deployed", "OnDeath", "OnDestroy" )
+ ent.s.count--
+
+ if ( !ent.s.count )
+ ent.Kill_Deprecated_UseDestroyInstead()
+ }
+
+ foreach ( entity guy in npcArray )
+ {
+ if ( IsAlive( guy ) )
+ thread func( ent, guy )
+ }
+
+ ent.WaitSignal( "OnDestroy" )
+}
+
+function InitDropShipFlightPaths( spawnPoints )
+{
+ entity tempDropShip = CreateEntity( "prop_dynamic" )
+ tempDropShip.kv.spawnflags = 0
+ tempDropShip.SetModel( DROPSHIP_MODEL )
+
+ DispatchSpawn( tempDropShip )
+
+ foreach ( spawnPoint in spawnPoints )
+ {
+ tempDropShip.SetOrigin( spawnPoint.GetOrigin() )
+
+ spawnPoint.s.dropShipPathAnims <- {}
+ spawnPoint.s.dropShipPathAnims[ "gd_goblin_zipline_strafe" ] <- GetDropShipAnimOffset( tempDropShip, "gd_goblin_zipline_strafe", spawnPoint )
+ spawnPoint.s.dropShipPathAnims[ "gd_goblin_zipline_dive" ] <- GetDropShipAnimOffset( tempDropShip, "gd_goblin_zipline_dive", spawnPoint )
+ }
+
+ tempDropShip.Destroy()
+}
+
+entity function CreateDropship( int team, vector origin, vector angles )
+{
+ entity dropship = CreateEntity( "npc_dropship" )
+ dropship.kv.teamnumber = team
+ dropship.SetOrigin( origin )
+ dropship.SetAngles( angles )
+ return dropship
+}
+
+function GetDropShipAnimOffset( dropShip, animName, refEnt )
+{
+ local animStart = dropShip.Anim_GetStartForRefPoint_Old( animName, refEnt.GetOrigin(), refEnt.GetAngles() )
+ return animStart.origin - refEnt.GetOrigin()
+}
+
+
+
+function GetDropshipSquadSize( squadname )
+{
+ local squadsize = 0
+ array<entity> dropships = GetNPCArrayByClass( "npc_dropship" )
+
+ //printl( dropships.len()+ " dropships, checking squadname: " + squadname )
+ foreach ( ship in dropships )
+ if ( ship.kv.squadname == squadname )
+ squadsize++
+
+ //printl( dropships.len()+ " dropships, squadsize: " + squadsize )
+ return squadsize
+}
+
+function DelayDropshipDelete( dropship )
+{
+ dropship.EndSignal( "OnDeath" )
+
+ //very defensive check
+ DefensiveFreePlayers( dropship )
+
+ WaitFrame() // so the dropship wont pop out before it warps out
+
+ dropship.Kill_Deprecated_UseDestroyInstead()
+}
+
+function DefensiveFreePlayers( dropship )
+{
+ array<entity> players = GetPlayerArrayOfTeam( dropship.GetTeam() )
+ foreach ( player in players )
+ {
+ if ( !IsValid( player ) )
+ continue
+
+ if ( player.GetParent() != dropship )
+ continue
+
+ player.ClearParent() //Clear parent before dropship gets deleted with players still attached to it. Defensive fix for bug 178543
+
+ KillPlayer( player, eDamageSourceId.fall )
+ }
+}
+
+void function OnNPCDropshipDeath( entity dropship, var damageInfo )
+{
+ if ( !IsValid( dropship ) )
+ return
+
+ asset modelName = dropship.GetModelName()
+
+ vector dropshipOrigin = dropship.GetOrigin()
+
+ PlayFX( $"P_veh_exp_crow", dropshipOrigin )
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, dropshipOrigin, "Goblin_Dropship_Explode" )
+}
+
+void function OnDropshipDamaged( entity dropship, var damageInfo )
+{
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ //Tried to give visual shield indicator, but it doesn't seem to work?
+ //DamageInfo_AddCustomDamageType( damageInfo, DF_SHIELD_DAMAGE )
+
+ // store the damage so all hits can be tallied
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ Assert( IsValid( inflictor ) )//Done so we can still get the error in dev
+ if ( !IsValid( inflictor ) ) //JFS Defensive fix
+ return
+
+ StoreDamageHistoryAndUpdate( dropship, 120.0, DamageInfo_GetDamage( damageInfo ), inflictor.GetOrigin(), DamageInfo_GetCustomDamageType( damageInfo ), DamageInfo_GetDamageSourceIdentifier( damageInfo ), DamageInfo_GetAttacker( damageInfo ) )
+
+ if ( DamageInfo_GetDamage( damageInfo ) < 450 )
+ return
+
+ vector pos = DamageInfo_GetDamagePosition( damageInfo )
+ PlayFX( FX_GUNSHIP_DAMAGE, pos )
+}
+
+void function GuyDeploysOffShip( entity guy, table Table )
+{
+ guy.EndSignal( "OnDeath" )
+ entity ship = expect entity( Table.ship )
+ local shipAttach = Table.shipAttach
+
+ OnThreadEnd(
+ function() : ( guy, ship )
+ {
+ if ( !IsValid( guy ) )
+ return
+
+ if ( ship != null )
+ {
+ if ( !IsAlive( ship ) && IsAlive( guy ) )
+ {
+ // try to transfer the last attacker from the ship to the attached guys.
+ entity attacker = null
+ entity lastAttacker = GetLastAttacker( ship )
+ if ( IsValid( lastAttacker ) )
+ attacker = lastAttacker
+
+ guy.TakeDamage( 500, attacker, attacker, null )
+ }
+ }
+
+ if ( !IsAlive( guy ) )
+ guy.BecomeRagdoll( Vector(0,0,0), false )
+ }
+ )
+
+ guy.SetEfficientMode( true )
+ HideName( guy )
+
+ Assert( shipAttach, "Ship but no shipAttach" )
+ ship.EndSignal( "OnDeath" )
+ GuyAnimatesRelativeToShipAttachment( guy, Table )
+
+ WaittillPlayDeployAnims( ship )
+
+ GuyAnimatesOut( guy, Table )
+}
+
+function WaittillPlayDeployAnims( ref )
+{
+ waitthread WaittillPlayDeployAnimsThread( ref )
+}
+
+function WaittillPlayDeployAnimsThread( ref )
+{
+ ref.EndSignal( "OnDeath" )
+
+ ref.WaitSignal( "deploy" )
+}
+
+void function GuyAnimatesOut( entity guy, table Table )
+{
+ switch ( Table.side )
+ {
+ case "left":
+ case "right":
+ case "both":
+ case "zipline":
+ waitthread GuyZiplinesToGround( guy, Table )
+ break
+
+ default:
+ thread PlayAnim( guy, Table.deployAnim, Table.ship, Table.shipAttach )
+ break
+ }
+
+
+ guy.SetEfficientMode( false )
+ guy.SetNameVisibleToOwner( true )
+
+ WaittillAnimDone( guy )
+ guy.ClearParent()
+
+ UpdateEnemyMemoryFromTeammates( guy )
+
+ guy.Signal( "npc_deployed" )
+}
+
+function GuyAnimatesRelativeToShipAttachment( guy, Table )
+{
+ expect entity( guy )
+ Table.attachIndex <- Table.ship.LookupAttachment( Table.shipAttach )
+ guy.SetOrigin( Table.ship.GetOrigin() )
+
+ guy.SetParent( Table.ship, Table.shipAttach, false, 0 )
+
+ guy.Anim_ScriptedPlay( Table.idleAnim )
+}
+
+table<string, array<string> > function GetDropshipRopeAttachments( string side = "both" )
+{
+ table<string, array<string> > attachments
+
+ if ( side == "both" )
+ {
+ attachments[ "left" ] <- []
+ attachments[ "right" ] <- []
+
+ foreach ( seat, Table in level.DSAIziplineAnims[ "left"] )
+ {
+ attachments[ "left" ].append( expect string( Table.attach ) )
+ }
+
+ foreach ( seat, Table in level.DSAIziplineAnims[ "right"] )
+ {
+ attachments[ "right" ].append( expect string( Table.attach ) )
+ }
+ }
+ else
+ {
+ attachments[ side ] <- []
+
+ foreach ( seat, Table in level.DSAIziplineAnims[ side ] )
+ {
+ attachments[ side ].append( expect string( Table.attach ) )
+ }
+ }
+
+ return attachments
+}
+
+function PlayDropshipRampDoorOpenSound( entity dropship )
+{
+ entity snd = CreateOwnedScriptMover( dropship )
+ snd.SetParent( dropship, "RAMPDOORLIP" )
+
+ EmitSoundOnEntity( snd, "fracture_scr_intro_dropship_dooropen" )
+}
+
+void function WarpoutEffect( entity dropship )
+{
+ if ( !IsValid( dropship ) )
+ return
+
+ __WarpOutEffectShared( dropship )
+
+ thread DelayDropshipDelete( dropship )
+}
+
+void function WarpoutEffectFPS( entity dropship )
+{
+ __WarpOutEffectShared( dropship )
+}
+
+void function WarpinEffect( asset model, string animation, vector origin, vector angles, string sfx = "" )
+{
+ //we need a temp dropship to get the anim offsets
+ Point start = GetWarpinPosition( model, animation, origin, angles )
+
+ __WarpInEffectShared( start.origin, start.angles, sfx )
+}
+
+function PlayWarpFxOnPlayers( guys )
+{
+ foreach ( entity guy in guys )
+ {
+ if ( !IsAlive( guy ) )
+ continue
+
+ Remote_CallFunction_Replay( guy, "ServerCallback_PlayScreenFXWarpJump" )
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapon_xp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/weapon_xp.gnut
new file mode 100644
index 00000000..37b89169
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapon_xp.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_arc_cannon.nut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_arc_cannon.nut
new file mode 100644
index 00000000..1601330c
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_arc_cannon.nut
@@ -0,0 +1,1032 @@
+untyped
+
+global function ArcCannon_Init
+
+global function ArcCannon_PrecacheFX
+global function ArcCannon_Start
+global function ArcCannon_Stop
+global function ArcCannon_ChargeBegin
+global function ArcCannon_ChargeEnd
+global function FireArcCannon
+global function ArcCannon_HideIdleEffect
+#if SERVER
+ global function AddToArcCannonTargets
+ global function RemoveArcCannonTarget
+ global function ConvertTitanShieldIntoBonusCharge
+#endif
+global function GetArcCannonChargeFraction
+
+global function IsEntANeutralMegaTurret
+global function CreateArcCannonBeam
+
+
+// Aiming & Range
+global const DEFAULT_ARC_CANNON_FOVDOT = 0.98 // First target must be within this dot to be zapped and start a chain
+global const DEFAULT_ARC_CANNON_FOVDOT_MISSILE = 0.95 // First target must be within this dot to be zapped and start a chain ( if it's a missile, we allow more leaniency )
+global const ARC_CANNON_RANGE_CHAIN = 400 // Max distance we can arc from one target to another
+global const ARC_CANNON_TITAN_RANGE_CHAIN = 900 // Max distance we can arc from one target to another
+global const ARC_CANNON_CHAIN_COUNT_MIN = 5 // Max number of chains at no charge
+global const ARC_CANNON_CHAIN_COUNT_MAX = 5 // Max number of chains at full charge
+global const ARC_CANNON_CHAIN_COUNT_NPC = 2 // Number of chains when an NPC fires the weapon
+global const ARC_CANNON_FORK_COUNT_MAX = 1 // Number of forks that can come out of one target to other targets
+global const ARC_CANNON_FORK_DELAY = 0.1
+
+global const ARC_CANNON_RANGE_CHAIN_BURN = 400
+global const ARC_CANNON_TITAN_RANGE_CHAIN_BURN = 900
+global const ARC_CANNON_CHAIN_COUNT_MIN_BURN = 100 // Max number of chains at no charge
+global const ARC_CANNON_CHAIN_COUNT_MAX_BURN = 100 // Max number of chains at full charge
+global const ARC_CANNON_CHAIN_COUNT_NPC_BURN = 10 // Number of chains when an NPC fires the weapon
+global const ARC_CANNON_FORK_COUNT_MAX_BURN = 10 // Number of forks that can come out of one target to other targets
+global const ARC_CANNON_BEAM_LIFETIME_BURN = 1
+
+// Visual settings
+global const ARC_CANNON_BOLT_RADIUS_MIN = 32 // Bolt radius at no charge ( not actually sure what this does to the beam lol )
+global const ARC_CANNON_BOLT_RADIUS_MAX = 640 // Bold radius at full charge ( not actually sure what this does to the beam lol )
+global const ARC_CANNON_BOLT_WIDTH_MIN = 1 // Bolt width at no charge
+global const ARC_CANNON_BOLT_WIDTH_MAX = 26 // Bolt width at full charge
+global const ARC_CANNON_BOLT_WIDTH_NPC = 8 // Bolt width when used by NPC
+global const ARC_CANNON_BEAM_COLOR = "150 190 255"
+global const ARC_CANNON_BEAM_LIFETIME = 0.75
+
+// Player Effects
+global const ARC_CANNON_TITAN_SCREEN_SFX = "Null_Remove_SoundHook"
+global const ARC_CANNON_PILOT_SCREEN_SFX = "Null_Remove_SoundHook"
+global const ARC_CANNON_EMP_DURATION_MIN = 0.1
+global const ARC_CANNON_EMP_DURATION_MAX = 1.8
+global const ARC_CANNON_EMP_FADEOUT_DURATION = 0.4
+global const ARC_CANNON_SCREEN_EFFECTS_MIN = 0.01
+global const ARC_CANNON_SCREEN_EFFECTS_MAX = 0.02
+global const ARC_CANNON_SCREEN_THRESHOLD = 0.3385
+global const ARC_CANNON_3RD_PERSON_EFFECT_MIN_DURATION = 0.2
+
+// Damage
+global const ARC_CANNON_DAMAGE_FALLOFF_SCALER = 0.75 // Amount of damage carried on to the next target in the chain lightning. If 0.75, then a target that would normally take 100 damage will take 75 damage if they are one chain deep, or 56 damage if 2 levels deep
+global const ARC_CANNON_DAMAGE_CHARGE_RATIO = 0.85 // What amount of charge is required for full damage.
+global const ARC_CANNON_DAMAGE_CHARGE_RATIO_BURN = 0.676 // What amount of charge is required for full damage.
+global const ARC_CANNON_CAPACITOR_CHARGE_RATIO = 1.0
+
+// Options
+global const ARC_CANNON_TARGETS_MISSILES = 1 // 1 = arc cannon zaps missiles that are active, 0 = missiles are ignored by arc cannon
+
+//Mods
+global const OVERCHARGE_MAX_SHIELD_DECAY = 0.2
+global const OVERCHARGE_SHIELD_DECAY_MULTIPLIER = 0.04
+global const OVERCHARGE_BONUS_CHARGE_FRACTION = 0.05
+
+global const SPLITTER_DAMAGE_FALLOFF_SCALER = 0.6
+global const SPLITTER_FORK_COUNT_MAX = 10
+
+global const ARC_CANNON_SIGNAL_DEACTIVATED = "ArcCannonDeactivated"
+global const ARC_CANNON_SIGNAL_CHARGEEND = "ArcCannonChargeEnd"
+
+global const ARC_CANNON_BEAM_EFFECT = $"wpn_arc_cannon_beam"
+global const ARC_CANNON_BEAM_EFFECT_MOD = $"wpn_arc_cannon_beam_mod"
+
+global const ARC_CANNON_FX_TABLE = "exp_arc_cannon"
+
+global const ArcCannonTargetClassnames = {
+ [ "npc_drone" ] = true,
+ [ "npc_dropship" ] = true,
+ [ "npc_marvin" ] = true,
+ [ "npc_prowler" ] = true,
+ [ "npc_soldier" ] = true,
+ [ "npc_soldier_heavy" ] = true,
+ [ "npc_soldier_shield" ] = true,
+ [ "npc_spectre" ] = true,
+ [ "npc_stalker" ] = true,
+ [ "npc_super_spectre" ] = true,
+ [ "npc_titan" ] = true,
+ [ "npc_turret_floor" ] = true,
+ [ "npc_turret_mega" ] = true,
+ [ "npc_turret_sentry" ] = true,
+ [ "npc_frag_drone" ] = true,
+ [ "player" ] = true,
+ [ "prop_dynamic" ] = true,
+ [ "prop_script" ] = true,
+ [ "grenade_frag" ] = true,
+ [ "rpg_missile" ] = true,
+ [ "script_mover" ] = true,
+ [ "turret" ] = true,
+}
+
+struct {
+ array<string> missileCheckTargetnames = [
+ // "Arc Pylon",
+ "Arc Ball"
+ ]
+} file;
+
+function ArcCannon_Init()
+{
+ RegisterSignal( ARC_CANNON_SIGNAL_DEACTIVATED )
+ RegisterSignal( ARC_CANNON_SIGNAL_CHARGEEND )
+ PrecacheParticleSystem( ARC_CANNON_BEAM_EFFECT )
+ PrecacheParticleSystem( ARC_CANNON_BEAM_EFFECT_MOD )
+ PrecacheImpactEffectTable( ARC_CANNON_FX_TABLE )
+
+ #if CLIENT
+ AddDestroyCallback( "mp_titanweapon_arc_cannon", ClientDestroyCallback_ArcCannon_Stop )
+ #else
+ level._arcCannonTargetsArrayID <- CreateScriptManagedEntArray()
+ #endif
+
+ PrecacheParticleSystem( $"impact_arc_cannon_titan" )
+}
+
+function ArcCannon_PrecacheFX()
+{
+ PrecacheParticleSystem( $"wpn_arc_cannon_electricity_fp" )
+ PrecacheParticleSystem( $"wpn_arc_cannon_electricity" )
+
+ PrecacheParticleSystem( $"wpn_muzzleflash_arc_cannon_fp" )
+ PrecacheParticleSystem( $"wpn_muzzleflash_arc_cannon" )
+}
+
+function ArcCannon_Start( weapon )
+{
+ expect entity( weapon )
+ if ( !IsPilot( weapon.GetWeaponOwner() ) )
+ {
+ weapon.PlayWeaponEffectNoCull( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity", "muzzle_flash" )
+ weapon.EmitWeaponSound( "arc_cannon_charged_loop" )
+ }
+ else
+ {
+ weapon.EmitWeaponSound_1p3p( "Arc_Rifle_charged_Loop_1P", "Arc_Rifle_charged_Loop_3P" )
+ }
+}
+
+function ArcCannon_Stop( weapon, player = null )
+{
+ expect entity( weapon )
+ weapon.Signal( ARC_CANNON_SIGNAL_DEACTIVATED )
+
+ weapon.StopWeaponEffect( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity" )
+ weapon.StopWeaponSound( "arc_cannon_charged_loop" )
+}
+
+function ArcCannon_ChargeBegin( entity weapon )
+{
+ #if SERVER
+ if ( weapon.HasMod( "overcharge" ) )
+ {
+ entity weaponOwner = weapon.GetWeaponOwner()
+ if ( weaponOwner.IsTitan() )
+ {
+ entity soul = weaponOwner.GetTitanSoul()
+ thread ConvertTitanShieldIntoBonusCharge( soul, weapon )
+ }
+ }
+ #endif
+
+ #if CLIENT
+ if ( !weapon.ShouldPredictProjectiles() )
+ return
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+ Assert( weaponOwner.IsPlayer() )
+ weaponOwner.StartArcCannon();
+ #endif
+}
+
+function ArcCannon_ChargeEnd( entity weapon, entity player = null )
+{
+ #if SERVER
+ if ( IsValid( weapon ) )
+ weapon.Signal( ARC_CANNON_SIGNAL_CHARGEEND )
+ #endif
+
+ #if CLIENT
+ if ( weapon.GetWeaponOwner() == GetLocalViewPlayer() )
+ {
+ entity weaponOwner
+ if ( player != null )
+ weaponOwner = player
+ else
+ weaponOwner = weapon.GetWeaponOwner()
+
+ if ( IsValid( weaponOwner ) && weaponOwner.IsPlayer() )
+ weaponOwner.StopArcCannon()
+ }
+ #endif
+}
+
+#if SERVER
+function ConvertTitanShieldIntoBonusCharge( entity soul, entity weapon )
+{
+ weapon.EndSignal( ARC_CANNON_SIGNAL_CHARGEEND )
+ weapon.EndSignal( "OnDestroy" )
+
+ local maxShieldDecay = OVERCHARGE_MAX_SHIELD_DECAY
+ local bonusChargeFraction = OVERCHARGE_BONUS_CHARGE_FRACTION
+ local shieldDecayMultiplier = OVERCHARGE_SHIELD_DECAY_MULTIPLIER
+ int shieldHealthMax = soul.GetShieldHealthMax()
+ local chargeRatio = GetArcCannonChargeFraction( weapon )
+
+ while( 1 )
+ {
+ if ( !IsValid( soul ) || !IsValid( weapon ) )
+ break
+
+ local baseCharge = GetWeaponChargeFrac( weapon ) // + GetOverchargeBonusChargeFraction()
+ local charge = clamp ( baseCharge * ( 1 / chargeRatio ), 0.0, 1.0 )
+ if ( charge < 1.0 || maxShieldDecay > 0)
+ {
+ int shieldHealth = soul.GetShieldHealth()
+
+ //Slight inconsistency in server updates, this ensures it never takes too much.
+ if ( shieldDecayMultiplier > maxShieldDecay )
+ shieldDecayMultiplier = maxShieldDecay
+ maxShieldDecay -= shieldDecayMultiplier
+
+ local shieldDecayAmount = shieldHealthMax * shieldDecayMultiplier
+ local newShieldAmount = shieldHealth - shieldDecayAmount
+ soul.SetShieldHealth( max( newShieldAmount, 0 ) )
+ soul.nextRegenTime = Time() + GetShieldRegenTime( soul )
+
+ if ( shieldDecayAmount > shieldHealth )
+ bonusChargeFraction = bonusChargeFraction * ( shieldHealth / shieldDecayAmount )
+ weapon.SetWeaponChargeFraction( baseCharge + bonusChargeFraction )
+ }
+ wait 0.1
+ }
+}
+#endif
+
+function FireArcCannon( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ local weaponScriptScope = weapon.GetScriptScope()
+ local baseCharge = GetWeaponChargeFrac( weapon ) // + GetOverchargeBonusChargeFraction()
+ local charge = clamp( baseCharge * ( 1 / GetArcCannonChargeFraction( weapon ) ), 0.0, 1.0 )
+ float newVolume = GraphCapped( charge, 0.25, 1.0, 0.0, 1.0 )
+
+ weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 )
+
+ weapon.PlayWeaponEffect( $"wpn_muzzleflash_arc_cannon_fp", $"wpn_muzzleflash_arc_cannon", "muzzle_flash" )
+
+ local attachmentName = "muzzle_flash"
+ local attachmentIndex = weapon.LookupAttachment( attachmentName )
+ Assert( attachmentIndex >= 0 )
+ local muzzleOrigin = weapon.GetAttachmentOrigin( attachmentIndex )
+
+ //printt( "-------- FIRING ARC CANNON --------" )
+
+ table firstTargetInfo = GetFirstArcCannonTarget( weapon, attackParams )
+ if ( !IsValid( firstTargetInfo.target ) )
+ FireArcNoTargets( weapon, attackParams, muzzleOrigin )
+ else
+ FireArcWithTargets( weapon, firstTargetInfo, attackParams, muzzleOrigin )
+
+ return 1
+}
+
+table function GetFirstArcCannonTarget( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ entity owner = weapon.GetWeaponOwner()
+ local coneHeight = weapon.GetMaxDamageFarDist()
+
+ local angleToAxis = 2 // set this too high and auto-titans using it will error on FindVisibleEntitiesInCone
+ array<entity> ignoredEntities = [ owner, weapon ]
+ int traceMask = TRACE_MASK_SHOT
+ int flags = VIS_CONE_ENTS_TEST_HITBOXES
+ local antilagPlayer = null
+ if ( owner.IsPlayer() )
+ {
+ angleToAxis = owner.GetAttackSpreadAngle() * 0.11
+ antilagPlayer = owner
+ }
+
+ int ownerTeam = owner.GetTeam()
+
+ // Get a missile target and a non-missile target in the cone that the player can zap
+ // We do this in a separate check so we can use a wider cone to be more forgiving for targeting missiles
+ table firstTargetInfo = {}
+ firstTargetInfo.target <- null
+ firstTargetInfo.hitLocation <- null
+
+ for ( int i = 0; i < 2; i++ )
+ {
+ local missileCheck = i == 0
+ local coneAngle = angleToAxis
+ if ( missileCheck && owner.IsPlayer() ) // missile check only if owner is player
+ coneAngle *= 8.0
+
+ coneAngle = clamp( coneAngle, 0.1, 89.9 )
+
+ array<VisibleEntityInCone> results = FindVisibleEntitiesInCone( attackParams.pos, attackParams.dir, coneHeight, coneAngle, ignoredEntities, traceMask, flags, antilagPlayer )
+ foreach ( result in results )
+ {
+ entity visibleEnt = result.ent
+
+ if ( !IsValid( visibleEnt ) )
+ continue
+
+ if ( visibleEnt.IsPhaseShifted() )
+ continue
+
+ local classname = IsServer() ? visibleEnt.GetClassName() : visibleEnt.GetSignifierName()
+
+ if ( !( classname in ArcCannonTargetClassnames ) )
+ continue
+
+ if ( "GetTeam" in visibleEnt )
+ {
+ int visibleEntTeam = visibleEnt.GetTeam()
+ if ( visibleEntTeam == ownerTeam )
+ continue
+ if ( IsEntANeutralMegaTurret( visibleEnt, ownerTeam ) )
+ continue
+ }
+
+ expect string( classname )
+ string targetname = visibleEnt.GetTargetName()
+
+ if ( missileCheck && ( classname != "rpg_missile" && !file.missileCheckTargetnames.contains( targetname ) ) )
+ continue
+
+ if ( !missileCheck && ( classname == "rpg_missile" || file.missileCheckTargetnames.contains( targetname ) ) )
+ continue
+
+ firstTargetInfo.target = visibleEnt
+ firstTargetInfo.hitLocation = result.visiblePosition
+ break
+ }
+ }
+ //Creating a whiz-by sound.
+ weapon.FireWeaponBullet_Special( attackParams.pos, attackParams.dir, 1, 0, true, true, true, true, true, false, false )
+
+ return firstTargetInfo
+}
+
+function FireArcNoTargets( entity weapon, WeaponPrimaryAttackParams attackParams, muzzleOrigin )
+{
+ Assert( IsValid( weapon ) )
+ entity player = weapon.GetWeaponOwner()
+ local chargeFrac = GetWeaponChargeFrac( weapon )
+ local beamVec = attackParams.dir * weapon.GetMaxDamageFarDist()
+ local playerEyePos = player.EyePosition()
+ TraceResults traceResults = TraceLineHighDetail( playerEyePos, (playerEyePos + beamVec), weapon, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+ local beamEnd = traceResults.endPos
+
+ VortexBulletHit ornull vortexHit = VortexBulletHitCheck( player, playerEyePos, beamEnd )
+ if ( vortexHit )
+ {
+ expect VortexBulletHit( vortexHit )
+ #if SERVER
+ entity vortexWeapon = vortexHit.vortex.GetOwnerWeapon()
+ string className = vortexWeapon.GetWeaponClassName()
+ if ( vortexWeapon && ( className == "mp_titanweapon_vortex_shield" || className == "mp_titanweapon_vortex_shield_ion" ) )
+ {
+ // drain the vortex shield
+ VortexDrainedByImpact( vortexWeapon, weapon, null, null )
+ }
+ else if ( IsVortexSphere( vortexHit.vortex ) )
+ {
+ // do damage to vortex_sphere entities that isn't the titan "vortex shield"
+ local damageNear = weapon.GetWeaponInfoFileKeyField( "damage_near_value" )
+ local damage = damageNear * GraphCapped( chargeFrac, 0, 0.5, 0.0, 1.0 ) * 10 // do more damage the more charged the weapon is.
+ VortexSphereDrainHealthForDamage( vortexHit.vortex, damage )
+ }
+ #endif
+ beamEnd = vortexHit.hitPos
+ }
+
+ float radius = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_RADIUS_MIN, ARC_CANNON_BOLT_RADIUS_MAX )
+ local boltWidth = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_WIDTH_MIN, ARC_CANNON_BOLT_WIDTH_MAX )
+ if ( player.IsNPC() )
+ boltWidth = ARC_CANNON_BOLT_WIDTH_NPC
+ thread CreateArcCannonBeam( weapon, null, muzzleOrigin, beamEnd, player, ARC_CANNON_BEAM_LIFETIME, radius, boltWidth, 2, false, true )
+
+ #if SERVER
+ PlayImpactFXTable( expect vector( beamEnd ), player, ARC_CANNON_FX_TABLE, SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+ #endif
+}
+
+function FireArcWithTargets( entity weapon, table firstTargetInfo, WeaponPrimaryAttackParams attackParams, muzzleOrigin )
+{
+ local beamStart = muzzleOrigin
+ local beamEnd
+ entity player = weapon.GetWeaponOwner()
+ local chargeFrac = GetWeaponChargeFrac( weapon )
+ float radius = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_RADIUS_MIN, ARC_CANNON_BOLT_RADIUS_MAX )
+ float boltWidth = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_WIDTH_MIN, ARC_CANNON_BOLT_WIDTH_MAX )
+ local maxChains
+ local minChains
+
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ {
+ if ( player.IsNPC() )
+ maxChains = ARC_CANNON_CHAIN_COUNT_NPC_BURN
+ else
+ maxChains = ARC_CANNON_CHAIN_COUNT_MAX_BURN
+
+ minChains = ARC_CANNON_CHAIN_COUNT_MIN_BURN
+ }
+ else
+ {
+ if ( player.IsNPC() )
+ maxChains = ARC_CANNON_CHAIN_COUNT_NPC
+ else
+ maxChains = ARC_CANNON_CHAIN_COUNT_MAX
+
+ minChains = ARC_CANNON_CHAIN_COUNT_MIN
+ }
+
+ if ( !player.IsNPC() )
+ maxChains = Graph( chargeFrac, 0, 1, minChains, maxChains )
+
+ table zapInfo = {}
+ zapInfo.weapon <- weapon
+ zapInfo.player <- player
+ zapInfo.muzzleOrigin <- muzzleOrigin
+ zapInfo.radius <- radius
+ zapInfo.boltWidth <- boltWidth
+ zapInfo.maxChains <- maxChains
+ zapInfo.chargeFrac <- chargeFrac
+ zapInfo.zappedTargets <- {}
+ zapInfo.zappedTargets[ firstTargetInfo.target ] <- true
+ zapInfo.dmgSourceID <- weapon.GetDamageSourceID()
+ local chainNum = 1
+ thread ZapTargetRecursive( expect entity( firstTargetInfo.target), zapInfo, zapInfo.muzzleOrigin, expect vector( firstTargetInfo.hitLocation ), chainNum )
+}
+
+function ZapTargetRecursive( entity target, table zapInfo, beamStartPos, vector ornull firstTargetBeamEndPos = null, chainNum = 1 )
+{
+ if ( !IsValid( target ) )
+ return
+
+ if ( !IsValid( zapInfo.weapon ) )
+ return
+
+ Assert( target in zapInfo.zappedTargets )
+ if ( chainNum > zapInfo.maxChains )
+ return
+ vector beamEndPos
+ if ( firstTargetBeamEndPos == null )
+ beamEndPos = target.GetWorldSpaceCenter()
+ else
+ beamEndPos = expect vector( firstTargetBeamEndPos )
+
+ waitthread ZapTarget( zapInfo, target, beamStartPos, beamEndPos, chainNum )
+
+ // Get other nearby targets we can chain to
+ #if SERVER
+ if ( !IsValid( zapInfo.weapon ) )
+ return
+
+ var noArcing = expect entity( zapInfo.weapon ).GetWeaponInfoFileKeyField( "disable_arc" )
+
+ if ( noArcing != null && noArcing == 1 )
+ return // no chaining on new arc cannon
+
+ // NOTE: 'target' could be invalid at this point (no corpse)
+ array<entity> chainTargets = GetArcCannonChainTargets( beamEndPos, target, zapInfo )
+ foreach( entity chainTarget in chainTargets )
+ {
+ local newChainNum = chainNum
+ if ( chainTarget.GetClassName() != "rpg_missile" )
+ newChainNum++
+ zapInfo.zappedTargets[ chainTarget ] <- true
+ thread ZapTargetRecursive( chainTarget, zapInfo, beamEndPos, null, newChainNum )
+ }
+
+ if ( IsValid( zapInfo.player ) && zapInfo.player.IsPlayer() && zapInfo.zappedTargets.len() >= 5 )
+ {
+ #if HAS_STATS
+ if ( chainNum == 5 )
+ UpdatePlayerStat( expect entity( zapInfo.player ), "misc_stats", "arcCannonMultiKills", 1 )
+ #endif
+ }
+ #endif
+}
+
+function ZapTarget( zapInfo, target, beamStartPos, beamEndPos, chainNum = 1 )
+{
+ expect entity( target )
+ expect vector( beamStartPos )
+ expect vector( beamEndPos )
+
+ //DebugDrawLine( beamStartPos, beamEndPos, 255, 0, 0, true, 5.0 )
+ local boltWidth = zapInfo.boltWidth
+ if ( zapInfo.player.IsNPC() )
+ boltWidth = ARC_CANNON_BOLT_WIDTH_NPC
+ local firstBeam = ( chainNum == 1 )
+ #if SERVER
+ if ( firstBeam )
+ {
+ PlayImpactFXTable( beamEndPos, expect entity( zapInfo.player ), ARC_CANNON_FX_TABLE, SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+ }
+ #endif
+
+ thread CreateArcCannonBeam( zapInfo.weapon, target, beamStartPos, beamEndPos, zapInfo.player, ARC_CANNON_BEAM_LIFETIME, zapInfo.radius, boltWidth, 5, true, firstBeam )
+
+ #if SERVER
+ local isMissile = ( target.GetClassName() == "rpg_missile" )
+ if ( !isMissile )
+ wait ARC_CANNON_FORK_DELAY
+ else
+ wait 0.05
+
+ local deathPackage = damageTypes.arcCannon
+
+ float damageAmount
+ int damageMin
+ int damageMax
+
+ int damageFarValue = eWeaponVar.damage_far_value
+ int damageNearValue = eWeaponVar.damage_near_value
+ int damageFarValueTitanArmor = eWeaponVar.damage_far_value_titanarmor
+ int damageNearValueTitanArmor = eWeaponVar.damage_near_value_titanarmor
+ if ( zapInfo.player.IsNPC() )
+ {
+ damageFarValue = eWeaponVar.npc_damage_far_value
+ damageNearValue = eWeaponVar.npc_damage_near_value
+ damageFarValueTitanArmor = eWeaponVar.npc_damage_far_value_titanarmor
+ damageNearValueTitanArmor = eWeaponVar.npc_damage_near_value_titanarmor
+ }
+
+ if ( IsValid( target ) && IsValid( zapInfo.player ) )
+ {
+ bool hasFastPacitor = false
+ bool noArcing = false
+
+ if ( IsValid( zapInfo.weapon ) )
+ {
+ entity weap = expect entity( zapInfo.weapon )
+ hasFastPacitor = weap.GetWeaponInfoFileKeyField( "push_apart" ) != null && weap.GetWeaponInfoFileKeyField( "push_apart" ) == 1
+ noArcing = weap.GetWeaponInfoFileKeyField( "no_arcing" ) != null && weap.GetWeaponInfoFileKeyField( "no_arcing" ) == 1
+ }
+
+ if ( target.GetArmorType() == ARMOR_TYPE_HEAVY )
+ {
+ if ( IsValid( zapInfo.weapon ) )
+ {
+ entity weapon = expect entity( zapInfo.weapon )
+ damageMin = weapon.GetWeaponSettingInt( damageFarValueTitanArmor )
+ damageMax = weapon.GetWeaponSettingInt( damageNearValueTitanArmor )
+ }
+ else
+ {
+ damageMin = 100
+ damageMax = zapInfo.player.IsNPC() ? 1200 : 800
+ }
+ }
+ else
+ {
+ if ( IsValid( zapInfo.weapon ) )
+ {
+ entity weapon = expect entity( zapInfo.weapon )
+ damageMin = weapon.GetWeaponSettingInt( damageFarValue )
+ damageMax = weapon.GetWeaponSettingInt( damageNearValue )
+ }
+ else
+ {
+ damageMin = 120
+ damageMax = zapInfo.player.IsNPC() ? 140 : 275
+ }
+
+ if ( target.IsNPC() )
+ {
+ damageMin *= 3 // more powerful against NPC humans so they die easy
+ damageMax *= 3
+ }
+ }
+
+
+ local chargeRatio = GetArcCannonChargeFraction( zapInfo.weapon )
+ if ( IsValid( zapInfo.weapon ) && !zapInfo.weapon.GetWeaponSettingBool( eWeaponVar.charge_require_input ) )
+ {
+ // use distance for damage if the weapon auto-fires
+ entity weapon = expect entity( zapInfo.weapon )
+ float nearDist = weapon.GetWeaponSettingFloat( eWeaponVar.damage_near_distance )
+ float farDist = weapon.GetWeaponSettingFloat( eWeaponVar.damage_far_distance )
+
+ float dist = Distance( weapon.GetOrigin(), target.GetOrigin() )
+ damageAmount = GraphCapped( dist, farDist, nearDist, damageMin, damageMax )
+ }
+ else
+ {
+ // Scale damage amount based on how many chains deep we are
+ damageAmount = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, damageMin, damageMax )
+ }
+ local damageFalloff = ARC_CANNON_DAMAGE_FALLOFF_SCALER
+ if ( IsValid( zapInfo.weapon ) && zapInfo.weapon.HasMod( "splitter" ) )
+ damageFalloff = SPLITTER_DAMAGE_FALLOFF_SCALER
+ damageAmount *= pow( damageFalloff, chainNum - 1 )
+
+ local dmgSourceID = zapInfo.dmgSourceID
+
+ // Update Later - This shouldn't be done here, this is not where we determine if damage actually happened to the target
+ // move to Damaged callback instead
+ if ( damageAmount > 0 )
+ {
+ float empDuration = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, ARC_CANNON_EMP_DURATION_MIN, ARC_CANNON_EMP_DURATION_MAX )
+
+ if ( target.IsPlayer() && target.IsTitan() && !hasFastPacitor && !noArcing )
+ {
+ float empViewStrength = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, ARC_CANNON_SCREEN_EFFECTS_MIN, ARC_CANNON_SCREEN_EFFECTS_MAX )
+
+ if ( target.IsTitan() && zapInfo.chargeFrac >= ARC_CANNON_SCREEN_THRESHOLD )
+ {
+ Remote_CallFunction_Replay( target, "ServerCallback_TitanEMP", empViewStrength, empDuration, ARC_CANNON_EMP_FADEOUT_DURATION )
+ EmitSoundOnEntityOnlyToPlayer( target, target, ARC_CANNON_TITAN_SCREEN_SFX )
+ }
+ else if ( zapInfo.chargeFrac >= ARC_CANNON_SCREEN_THRESHOLD )
+ {
+ StatusEffect_AddTimed( target, eStatusEffect.emp, empViewStrength, empDuration, ARC_CANNON_EMP_FADEOUT_DURATION )
+ EmitSoundOnEntityOnlyToPlayer( target, target, ARC_CANNON_PILOT_SCREEN_SFX )
+ }
+ }
+
+ // Do 3rd person effect on the body
+ asset effect
+ string tag
+ target.TakeDamage( damageAmount, zapInfo.player, zapInfo.player, { origin = beamEndPos, force = Vector(0,0,0), scriptType = deathPackage, weapon = zapInfo.weapon, damageSourceId = dmgSourceID } )
+ //vector dir = Normalize( beamEndPos - beamStartPos )
+ //vector velocity = dir * 600
+ //PushPlayerAway( target, velocity )
+ //PushPlayerAway( expect entity( zapInfo.player ), -velocity )
+
+ if ( IsValid( zapInfo.weapon ) && hasFastPacitor )
+ {
+ if ( IsAlive( target ) && IsAlive( expect entity( zapInfo.player ) ) && target.IsTitan() )
+ {
+ float pushPercent = GraphCapped( damageAmount, damageMin, damageMax, 0.0, 1.0 )
+
+ if ( pushPercent > 0.6 )
+ PushPlayersApart( target, expect entity( zapInfo.player ), pushPercent * 400.0 )
+ }
+ }
+
+ if ( zapInfo.chargeFrac < ARC_CANNON_SCREEN_THRESHOLD )
+ empDuration = ARC_CANNON_3RD_PERSON_EFFECT_MIN_DURATION
+ else
+ empDuration += ARC_CANNON_EMP_FADEOUT_DURATION
+
+ if ( target.GetArmorType() == ARMOR_TYPE_HEAVY )
+ {
+ effect = $"impact_arc_cannon_titan"
+ tag = "exp_torso_front"
+ }
+ else
+ {
+ effect = $"P_emp_body_human"
+ tag = "CHESTFOCUS"
+ }
+
+ if ( target.IsPlayer() )
+ {
+ if ( target.LookupAttachment( tag ) != 0 )
+ ClientStylePlayFXOnEntity( effect, target, tag, empDuration )
+ }
+
+ if ( target.IsPlayer() )
+ EmitSoundOnEntityExceptToPlayer( target, target, "Titan_Blue_Electricity_Cloud" )
+ else
+ EmitSoundOnEntity( target, "Titan_Blue_Electricity_Cloud" )
+
+ thread FadeOutSoundOnEntityAfterDelay( target, "Titan_Blue_Electricity_Cloud", empDuration * 0.6666, empDuration * 0.3333 )
+ }
+ else
+ {
+ //Don't bounce if the beam is set to do 0 damage.
+ chainNum = zapInfo.maxChains
+ }
+
+ if ( isMissile )
+ {
+ if ( IsValid( zapInfo.player ) )
+ target.SetOwner( zapInfo.player )
+ target.MissileExplode()
+ }
+ }
+ #endif // SERVER
+}
+
+
+#if SERVER
+
+void function PushEntForTime( entity ent, vector velocity, float time )
+{
+ ent.EndSignal( "OnDeath" )
+ float endTime = Time() + time
+ float startTime = Time()
+ for ( ;; )
+ {
+ if ( Time() >= endTime )
+ break
+ float multiplier = Graph( Time(), startTime, endTime, 1.0, 0.0 )
+ vector currentVel = ent.GetVelocity()
+ currentVel += velocity * multiplier
+ ent.SetVelocity( currentVel )
+ WaitFrame()
+ }
+}
+
+array<entity> function GetArcCannonChainTargets( vector fromOrigin, entity fromTarget, table zapInfo )
+{
+ // NOTE: fromTarget could be null/invalid if it was a drone
+ array<entity> results = []
+ if ( !IsValid( zapInfo.player ) )
+ return results
+
+ int playerTeam = expect entity( zapInfo.player ).GetTeam()
+ array<entity> allTargets = GetArcCannonTargetsInRange( fromOrigin, playerTeam, expect entity( zapInfo.weapon ) )
+ allTargets = ArrayClosest( allTargets, fromOrigin )
+
+ local viewVector
+ if ( zapInfo.player.IsPlayer() )
+ viewVector = zapInfo.player.GetViewVector()
+ else
+ viewVector = AnglesToForward( zapInfo.player.EyeAngles() )
+
+ local eyePosition = zapInfo.player.EyePosition()
+
+ foreach ( ent in allTargets )
+ {
+ local forkCount = ARC_CANNON_FORK_COUNT_MAX
+ if ( zapInfo.weapon.HasMod( "splitter" ) )
+ forkCount = SPLITTER_FORK_COUNT_MAX
+ else if ( zapInfo.weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ forkCount = ARC_CANNON_FORK_COUNT_MAX_BURN
+
+ if ( results.len() >= forkCount )
+ break
+
+ if ( ent.IsPhaseShifted() )
+ continue
+
+ if ( ent.IsPlayer() )
+ {
+ // Ignore players that are passing damage to their parent. This is to address zapping a friendly rodeo player
+ local entParent = ent.GetParent()
+ if ( IsValid( entParent ) && ent.kv.PassDamageToParent.tointeger() )
+ continue
+
+ // only chains to other titan players for now
+ if ( !ent.IsTitan() )
+ continue
+ }
+
+ if ( ent.GetClassName() == "script_mover" )
+ continue
+
+ if ( IsEntANeutralMegaTurret( ent, playerTeam ) )
+ continue
+
+ if ( !IsAlive( ent ) )
+ continue
+
+ // Don't consider targets that already got zapped
+ if ( ent in zapInfo.zappedTargets )
+ continue
+
+ //Preventing the arc-cannon from firing behind.
+ local vecToEnt = ( ent.GetWorldSpaceCenter() - eyePosition )
+ vecToEnt.Norm()
+ local dotVal = DotProduct( vecToEnt, viewVector )
+ if ( dotVal < 0 )
+ continue
+
+ // Check if we can see them, they aren't behind a wall or something
+ local ignoreEnts = []
+ ignoreEnts.append( zapInfo.player )
+ ignoreEnts.append( ent )
+
+ foreach( zappedTarget, val in zapInfo.zappedTargets )
+ {
+ if ( IsValid( zappedTarget ) )
+ ignoreEnts.append( zappedTarget )
+ }
+
+ TraceResults traceResult = TraceLineHighDetail( fromOrigin, ent.GetWorldSpaceCenter(), ignoreEnts, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+
+ // Trace failed, lets try an eye to eye trace
+ if ( traceResult.fraction < 1 )
+ {
+ // 'fromTarget' may be invalid
+ if ( IsValid( fromTarget ) )
+ traceResult = TraceLineHighDetail( fromTarget.EyePosition(), ent.EyePosition(), ignoreEnts, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+ }
+
+ if ( traceResult.fraction < 1 )
+ continue
+
+ // Enemy is in visible, and within range.
+ if ( !results.contains( ent ) )
+ results.append( ent )
+ }
+
+ //printt( "NEARBY TARGETS VALID AND VISIBLE:", results.len() )
+
+ return results
+}
+#endif // SERVER
+
+bool function IsEntANeutralMegaTurret( ent, int playerTeam )
+{
+ expect entity( ent )
+
+ if ( ent.GetClassName() != "npc_turret_mega" )
+ return false
+ int entTeam = ent.GetTeam()
+ if ( entTeam == playerTeam )
+ return false
+ if ( !IsEnemyTeam( playerTeam, entTeam ) )
+ return true
+
+ return false
+}
+
+function ArcCannon_HideIdleEffect( entity weapon, delay )
+{
+ bool weaponOwnerIsPilot = IsPilot( weapon.GetWeaponOwner() )
+ weapon.EndSignal( ARC_CANNON_SIGNAL_DEACTIVATED )
+ if ( weaponOwnerIsPilot == false )
+ {
+ weapon.StopWeaponEffect( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity" )
+ weapon.StopWeaponSound( "arc_cannon_charged_loop" )
+ }
+ wait delay
+
+ if ( !IsValid( weapon ) )
+ return
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+ //The weapon can be valid, but the player isn't a Titan during melee execute.
+ // JFS: threads with waits should just end on "OnDestroy"
+ if ( !IsValid( weaponOwner ) )
+ return
+
+ if ( weapon != weaponOwner.GetActiveWeapon() )
+ return
+
+ if ( weaponOwnerIsPilot == false )
+ {
+ weapon.PlayWeaponEffectNoCull( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity", "muzzle_flash" )
+ weapon.EmitWeaponSound( "arc_cannon_charged_loop" )
+ }
+ else
+ {
+ weapon.EmitWeaponSound_1p3p( "Arc_Rifle_charged_Loop_1P", "Arc_Rifle_charged_Loop_3P" )
+ }
+}
+
+#if SERVER
+void function AddToArcCannonTargets( entity ent )
+{
+ AddToScriptManagedEntArray( level._arcCannonTargetsArrayID, ent );
+}
+
+function RemoveArcCannonTarget( ent )
+{
+ RemoveFromScriptManagedEntArray( level._arcCannonTargetsArrayID, ent )
+}
+
+array<entity> function GetArcCannonTargets( vector origin, int team )
+{
+ array<entity> targets = GetScriptManagedEntArrayWithinCenter( level._arcCannonTargetsArrayID, team, origin, ARC_CANNON_TITAN_RANGE_CHAIN )
+
+ if ( ARC_CANNON_TARGETS_MISSILES )
+ targets.extend( GetProjectileArrayEx( "rpg_missile", TEAM_ANY, team, origin, ARC_CANNON_TITAN_RANGE_CHAIN ) )
+
+ return targets
+}
+
+array<entity> function GetArcCannonTargetsInRange( vector origin, int team, entity weapon )
+{
+ array<entity> allTargets = GetArcCannonTargets( origin, team )
+ array<entity> targetsInRange
+
+ float titanDistSq
+ float distSq
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ {
+ titanDistSq = ARC_CANNON_TITAN_RANGE_CHAIN_BURN * ARC_CANNON_TITAN_RANGE_CHAIN_BURN
+ distSq = ARC_CANNON_RANGE_CHAIN_BURN * ARC_CANNON_RANGE_CHAIN_BURN
+ }
+ else
+ {
+ titanDistSq = ARC_CANNON_TITAN_RANGE_CHAIN * ARC_CANNON_TITAN_RANGE_CHAIN
+ distSq = ARC_CANNON_RANGE_CHAIN * ARC_CANNON_RANGE_CHAIN
+ }
+
+ foreach( target in allTargets )
+ {
+ float d = DistanceSqr( target.GetOrigin(), origin )
+ float validDist = target.IsTitan() ? titanDistSq : distSq
+ if ( d <= validDist )
+ targetsInRange.append( target )
+ }
+
+ return targetsInRange
+}
+#endif // SERVER
+
+function CreateArcCannonBeam( weapon, target, startPos, endPos, player, lifeDuration = ARC_CANNON_BEAM_LIFETIME, radius = 256, boltWidth = 4, noiseAmplitude = 5, hasTarget = true, firstBeam = false )
+{
+ Assert( startPos )
+ Assert( endPos )
+
+ //**************************
+ // LIGHTNING BEAM EFFECT
+ //**************************
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ lifeDuration = ARC_CANNON_BEAM_LIFETIME_BURN
+ // If it's the first beam and on client we do a special beam so it's lined up with the muzzle origin
+ #if CLIENT
+ if ( firstBeam )
+ thread CreateClientArcBeam( weapon, endPos, lifeDuration, target )
+ #endif
+
+ #if SERVER
+ // Control point sets the end position of the effect
+ entity cpEnd = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpEnd, UniqueString( "arc_cannon_beam_cpEnd" ) )
+ cpEnd.SetOrigin( endPos )
+ DispatchSpawn( cpEnd )
+
+ entity zapBeam = CreateEntity( "info_particle_system" )
+ zapBeam.kv.cpoint1 = cpEnd.GetTargetName()
+
+ zapBeam.SetValueForEffectNameKey( GetBeamEffect( weapon ) )
+
+ zapBeam.kv.start_active = 0
+ zapBeam.SetOwner( player )
+ zapBeam.SetOrigin( startPos )
+ if ( firstBeam )
+ {
+ zapBeam.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // everyone but owner
+ zapBeam.SetParent( player.GetActiveWeapon(), "muzzle_flash", false, 0.0 )
+ }
+ DispatchSpawn( zapBeam )
+
+ zapBeam.Fire( "Start" )
+ zapBeam.Fire( "StopPlayEndCap", "", lifeDuration )
+ zapBeam.Kill_Deprecated_UseDestroyInstead( lifeDuration )
+ cpEnd.Kill_Deprecated_UseDestroyInstead( lifeDuration )
+ #endif
+}
+
+function GetBeamEffect( weapon )
+{
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ return ARC_CANNON_BEAM_EFFECT_MOD
+
+ return ARC_CANNON_BEAM_EFFECT
+}
+
+#if CLIENT
+function CreateClientArcBeam( weapon, endPos, lifeDuration, target )
+{
+ Assert( IsClient() )
+
+ local beamEffect = GetBeamEffect( weapon )
+
+ // HACK HACK HACK HACK
+ string tag = "muzzle_flash"
+ if ( weapon.GetWeaponInfoFileKeyField( "client_tag_override" ) != null )
+ tag = expect string( weapon.GetWeaponInfoFileKeyField( "client_tag_override" ) )
+
+ local handle = weapon.PlayWeaponEffectReturnViewEffectHandle( beamEffect, $"", tag )
+ if ( !EffectDoesExist( handle ) )
+ return
+
+ EffectSetControlPointVector( handle, 1, endPos )
+
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ lifeDuration = ARC_CANNON_BEAM_LIFETIME_BURN
+
+ wait( lifeDuration )
+
+ if ( IsValid( weapon ) )
+ weapon.StopWeaponEffect( beamEffect, $"" )
+}
+
+void function ClientDestroyCallback_ArcCannon_Stop( entity ent )
+{
+ ArcCannon_Stop( ent )
+}
+#endif // CLIENT
+
+function GetArcCannonChargeFraction( weapon )
+{
+ if ( IsValid( weapon ) )
+ {
+ local chargeRatio = ARC_CANNON_DAMAGE_CHARGE_RATIO
+ if ( weapon.HasMod( "capacitor" ) )
+ chargeRatio = ARC_CANNON_CAPACITOR_CHARGE_RATIO
+ if ( weapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ chargeRatio = ARC_CANNON_DAMAGE_CHARGE_RATIO_BURN
+ return chargeRatio
+ }
+
+ return 0
+}
+
+function GetWeaponChargeFrac( weapon )
+{
+ if ( weapon.IsChargeWeapon() )
+ return weapon.GetWeaponChargeFraction()
+ return 1.0
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_at_turrets.gnut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_at_turrets.gnut
new file mode 100644
index 00000000..b061c182
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_at_turrets.gnut
@@ -0,0 +1,284 @@
+untyped
+
+global function ATTurrets_Init
+global function CreateATTurret
+global function ATTurretSettings
+//global function SetDriverOnTurret
+global function PROTO_ATTurretsEnabled
+global function PROTO_Simulate_Turret_Use
+
+const USE_DEBOUNCE_TIME = 0.3
+const FX_ANTI_TITAN_SHIELD_WALL = $"P_anti_titan_shield_3P"
+const vector AT_TURRET_SHIELD_COLOR = Vector( 115, 247, 255 )
+
+void function ATTurrets_Init()
+{
+ AddSpawnCallbackEditorClass( "turret", "turret_pilot_at", ATTurretSettings )
+ RegisterSignal( "ClearDriver" )
+ RegisterSignal( "DismebarkATTurret" )
+}
+
+void function CreateATTurret( vector origin, vector angles )
+{
+ entity turret = CreateEntity( "turret" )
+ turret.kv.editorclass = "turret_pilot_at"
+ turret.kv.settings = "PROTO_at_turret"
+ turret.kv.teamnumber = 0
+ turret.SetValueForModelKey( $"models/weapons/sentry_turret/sentry_turret.mdl" )
+ turret.kv.origin = origin
+ turret.kv.angles = angles
+ DispatchSpawn( turret )
+ ATTurretSettings( turret )
+}
+
+void function ATTurretSettings( entity turret )
+{
+ if ( PROTO_ATTurretsEnabled() )
+ {
+ turret.SetUsable()
+ turret.SetUsableByGroup( "pilot" )
+ turret.SetUsePrompts( "Hold %use% to use AT-Turret", "Press %use% to use AT-Turret" )
+ //AddCallback_OnUseEntity( turret, SetDriverOnTurret )
+ AddCallback_OnUseEntity( turret, PROTO_Simulate_Turret_Use )
+
+ local attachmentID = turret.LookupAttachment( "muzzle_flash" )
+ local origin = turret.GetAttachmentOrigin( attachmentID )
+ local angles = turret.GetAttachmentAngles( attachmentID )
+
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) )
+ DispatchSpawn( cpoint )
+
+ turret.e.shieldWallFX = CreateEntity( "info_particle_system" )
+ entity shieldWallFX = turret.e.shieldWallFX
+ shieldWallFX.SetValueForEffectNameKey( FX_ANTI_TITAN_SHIELD_WALL )
+ shieldWallFX.kv.start_active = 1
+ SetShieldWallCPoint( shieldWallFX, cpoint )
+ shieldWallFX.SetOwner( turret )
+ shieldWallFX.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // not owner only
+ shieldWallFX.kv.cpoint1 = cpoint.GetTargetName()
+ shieldWallFX.SetStopType( "destroyImmediately" )
+ shieldWallFX.SetOrigin( origin )
+ shieldWallFX.SetAngles( angles - Vector(0,0,90) )
+ shieldWallFX.SetParent( turret, "muzzle_flash", true, 0.0 )
+ DispatchSpawn( shieldWallFX )
+ SetShieldWallCPointOrigin( shieldWallFX, AT_TURRET_SHIELD_COLOR )
+ }
+ else
+ {
+ turret.DisableDraw()
+ turret.NotSolid()
+ }
+}
+
+bool function PROTO_ATTurretsEnabled()
+{
+ return ( GetCurrentPlaylistVarInt( "at_turrets_enabled", 0 ) == 1 )
+}
+
+/*/////////////////////////////////////////////////////////////////
+ WEAPON PROTOTYPE
+///////////////////////////////////////////////////////////////////*/
+
+function PROTO_Simulate_Turret_Use( turret, player )
+{
+ expect entity( turret )
+ expect entity( player )
+
+ if ( Time() < player.p.PROTO_UseDebounceEndTime )
+ return
+
+ PROTO_ActivateTurret( turret, player )
+}
+
+const array<int> TURRET_CANCEL_BUTTONS =
+[
+ IN_USE,
+ IN_DUCK,
+ IN_DUCKTOGGLE,
+ IN_WEAPON_CYCLE,
+ IN_MELEE,
+ IN_OFFHAND0,
+ IN_OFFHAND1,
+ IN_OFFHAND2,
+ IN_OFFHAND3,
+ IN_OFFHAND4,
+ IN_JUMP
+]
+
+void function PROTO_ActivateTurret( entity turret, entity player )
+{
+ if ( turret.GetOwner() == player )
+ {
+ player.Signal( "DismebarkATTurret" )
+ }
+ else
+ {
+ if ( turret.GetOwner() == null )
+ {
+ turret.DisableDraw()
+ turret.NotSolid()
+ SetShieldWallCPointOrigin( turret.e.shieldWallFX, < 0, 0, 0 > )
+ turret.SetOwner( player )
+ player.p.PROTO_UseDebounceEndTime = Time() + USE_DEBOUNCE_TIME
+ foreach( int button in TURRET_CANCEL_BUTTONS )
+ AddButtonPressedPlayerInputCallback( player, button, PROTO_DisembarkATTurret )
+ AddEntityCallback_OnDamaged( player, PlayerDamagedWhileOnTurret )
+ thread MonitorPilot( turret, player )
+ }
+ else
+ {
+ SendHudMessage( player, "Turret in use.", -1, 0.4, 255, 255, 0, 255, 0.0, 0.5, 0.0 )
+ }
+ }
+}
+
+void function PlayerDamagedWhileOnTurret( entity player, var damageInfo )
+{
+ if ( Time() < player.p.PROTO_UseDebounceEndTime )
+ return
+
+ player.Signal( "DismebarkATTurret" )
+}
+
+function MonitorPilot( entity turret, entity player )
+{
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "DismebarkATTurret")
+ turret.EndSignal( "OnDestroy" )
+
+ player.ForceStand()
+ entity playerMover = CreateOwnedScriptMover( player )
+ player.SetParent( playerMover, "ref", true )
+ vector forward = turret.GetForwardVector()
+ vector basePos = turret.GetOrigin() + forward * -25
+ vector startOrigin = player.GetOrigin()
+ float moveTime = 0.1
+ playerMover.NonPhysicsMoveTo( basePos, moveTime, 0.0, 0.0 )
+ playerMover.NonPhysicsRotateTo( turret.GetAngles(), moveTime, 0, 0 )
+ player.FreezeControlsOnServer()
+
+ StorePilotWeapons( player )
+
+ OnThreadEnd(
+ function() : ( turret, player, playerMover, startOrigin )
+ {
+ if ( IsValid( player ) )
+ {
+ player.ClearParent()
+ player.UnforceStand()
+ ClearPlayerAnimViewEntity( player )
+ player.UnfreezeControlsOnServer()
+ RetrievePilotWeapons( player )
+ ViewConeZeroInstant( player )
+ foreach( int button in TURRET_CANCEL_BUTTONS )
+ RemoveButtonPressedPlayerInputCallback( player, button, PROTO_DisembarkATTurret )
+ RemoveEntityCallback_OnDamaged( player, PlayerDamagedWhileOnTurret )
+ player.p.PROTO_UseDebounceEndTime = Time() + USE_DEBOUNCE_TIME
+ PutEntityInSafeSpot( player, turret, null, startOrigin, player.GetOrigin() )
+ }
+
+ if ( IsValid( turret ) )
+ {
+ turret.EnableDraw()
+ turret.Solid()
+ SetShieldWallCPointOrigin( turret.e.shieldWallFX, AT_TURRET_SHIELD_COLOR )
+ turret.SetOwner( null )
+ }
+
+ playerMover.Destroy()
+ }
+ )
+
+ wait moveTime
+
+ player.PlayerCone_SetSpecific( forward )
+ ViewConeZeroInstant( player )
+
+ // PROTO: Supporting ability to pick different turret weapons for turrets in LevelEd and the legacy Defender prototype turret
+ // We need a predator cannon style turret in SP.
+ if ( IsMultiplayer() )
+ {
+ //player.GiveWeapon( "mp_weapon_smr", [ "PROTO_at_turret" ] )
+ //player.SetActiveWeaponByName( "mp_weapon_smr" )
+
+ // modded code: smr's at turret mod does exist in release tf2
+ player.GiveWeapon( "mp_weapon_defender", [ "PROTO_at_turret" ] )
+ player.SetActiveWeaponByName( "mp_weapon_defender" )
+ }
+ else if ( turret.HasKey( "weaponsettings" ) )
+ {
+ // See if we have any special turret mods on this weapon
+ array<string> turretMods = []
+ array<string> mods = GetWeaponMods_Global( turret.kv.weaponsettings )
+ foreach ( mod in mods )
+ {
+ if ( mod.find( "PROTO_at_turret" ) == 0 )
+ turretMods.append( "PROTO_at_turret" )
+ }
+
+ player.GiveWeapon( turret.kv.weaponsettings, turretMods )
+ player.SetActiveWeaponByName( turret.kv.weaponsettings )
+ }
+
+ wait 0.1
+
+ player.UnfreezeControlsOnServer()
+
+ ViewConeLockedForward( player )
+
+ player.WaitSignal( "OnDeath" )
+}
+
+void function PROTO_DisembarkATTurret( entity player )
+{
+ if ( Time() < player.p.PROTO_UseDebounceEndTime )
+ return
+
+ player.Signal( "DismebarkATTurret" )
+}
+
+/*/////////////////////////////////////////////////////////////////
+ TURRET ENTITY PROTOTYPE
+///////////////////////////////////////////////////////////////////*/
+//
+//function SetDriverOnTurret( turret, player )
+//{
+// if ( turret.GetOwner() == player )
+// {
+// turret.SetOwner( null )
+// turret.ClearDriver()
+// player.Signal( "ClearDriver" )
+// }
+// else
+// {
+// entity oldOwner = expect entity( turret.GetOwner() )
+// if ( oldOwner != null )
+// {
+// oldOwner.Signal( "ClearDriver" )
+// turret.ClearDriver()
+// }
+// turret.SetOwner( player )
+// turret.SetDriver( player )
+// thread ClearDriverOnDeath( turret, player )
+// }
+// //turret.SetUsePrompts( "DEACTIVATE", "DEACTIVATE" )
+// //turret.SetOwner( player )
+// //turret.SetBossPlayer( player )
+// //turret.SetUsableByGroup( "owner pilot" )
+//}
+//
+//function ClearDriverOnDeath( turret, player )
+//{
+// player.EndSignal( "ClearDriver" )
+// player.EndSignal( "OnDestroy" )
+// turret.EndSignal( "OnDestroy" )
+//
+// player.WaitSignal( "OnDeath" )
+//
+// if ( IsValid( turret ) )
+// turret.ClearDriver()
+//}
+////TODO: Handle death and handle deactivate.
+//
+// \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_ball_lightning.gnut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_ball_lightning.gnut
new file mode 100644
index 00000000..9aae59e5
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_ball_lightning.gnut
@@ -0,0 +1,363 @@
+untyped
+
+global function BallLightning_Init
+global function AttachBallLightning
+global function AttachBallLightningToProp
+global function CreateBallLightning
+global function DestroyBallLightningOnEnt
+global function GetBallLightningFromEnt
+
+global function RegisterBallLightningDamage
+
+global function BallLightningZapFX
+global function BallLightningZapTargets
+global function BallLightningZapConnectionFX
+
+struct {
+ table< string, float > uniqueStrings
+} file
+
+function BallLightning_Init()
+{
+ PrecacheParticleSystem( BALL_LIGHTNING_ZAP_FX )
+
+ if ( BALL_LIGHTNING_FX_TABLE != "" )
+ PrecacheImpactEffectTable( BALL_LIGHTNING_FX_TABLE )
+
+ RegisterBallLightningDamage( eDamageSourceId.mp_weapon_arc_launcher )
+ RegisterBallLightningDamage( eDamageSourceId.mp_titanweapon_arc_ball )
+ RegisterBallLightningDamage( eDamageSourceId.mp_weapon_arc_trap )
+}
+
+function AttachBallLightning( entity weapon, entity projectile )
+{
+ Assert( !( "ballLightning" in projectile.s ) )
+
+ int damageSourceId
+ entity owner
+
+ if ( weapon.IsProjectile() )
+ {
+ owner = weapon.GetOwner()
+ damageSourceId = weapon.ProjectileGetDamageSourceID()
+ }
+ else
+ {
+ owner = weapon.GetWeaponOwner()
+ damageSourceId = weapon.GetDamageSourceID()
+ }
+
+
+ entity ball = CreateBallLightning( owner, damageSourceId, projectile.GetOrigin(), projectile.GetAngles() )
+ ball.SetParent( projectile )
+ projectile.s.ballLightning <- ball
+}
+
+void function DestroyBallLightningOnEnt( entity prop )
+{
+ if ( "ballLightning" in prop.s )
+ {
+ prop.s.ballLightning.Destroy()
+ delete prop.s.ballLightning
+ }
+}
+
+
+entity function AttachBallLightningToProp( entity prop, entity owner, int damageSourceId )
+{
+ entity ball = CreateBallLightning( owner, damageSourceId, prop.GetOrigin(), prop.GetAngles() )
+ ball.SetParent( prop )
+ prop.s.ballLightning <- ball
+ return ball
+}
+
+entity function CreateBallLightning( entity owner, int damageSourceId, vector origin, vector angles )
+{
+ entity ballLightning = CreateScriptMover( origin, angles )
+ ballLightning.SetOwner( owner )
+ SetTeam( ballLightning, owner.GetTeam() )
+
+ thread BallLightningThink( ballLightning, damageSourceId )
+ return ballLightning
+}
+
+void function RegisterBallLightningDamage( int damageSourceId )
+{
+ AddDamageCallbackSourceID( damageSourceId, OnBallLightningDamage )
+}
+
+void function OnBallLightningDamage( entity victim, var damageInfo )
+{
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ if ( damage <= 0 )
+ return
+
+ if ( victim.IsWorld() )
+ return
+
+ if ( victim.IsProjectile() )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & (DF_EXPLOSION | DF_IMPACT) )
+ return
+
+ // if ( IsHumanSized( victim ) )
+ // {
+ // DamageInfo_SetDamage( damageInfo, 0 )
+ // return
+ // }
+
+ entity ballLightning = DamageInfo_GetInflictor( damageInfo )
+
+ if ( victim == ballLightning )
+ return
+
+ if ( victim.GetParent() == ballLightning )
+ return
+
+ if ( !IsTargetEntValid( ballLightning, victim, ballLightning.e.ballLightningData ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ vector origin = DamageInfo_GetDamagePosition( damageInfo )
+ int hitBox = DamageInfo_GetHitBox( damageInfo )
+
+ string tag = GetEntityCenterTag( victim )
+ thread BallLightningZapConnectionFX( ballLightning, victim, tag, ballLightning.e.ballLightningData )
+ BallLightningZapFX( ballLightning, victim, tag, ballLightning.e.ballLightningData )
+}
+
+void function BallLightningThink( entity ballLightning, int damageSourceId )
+{
+ ballLightning.EndSignal( "OnDestroy" )
+
+ EmitSoundOnEntity( ballLightning, "Weapon_Arc_Ball_Loop" )
+
+ local data = {}
+
+ OnThreadEnd(
+ function() : ( ballLightning, data )
+ {
+ if ( IsValid( ballLightning ) )
+ StopSoundOnEntity( ballLightning, "Weapon_Arc_Ball_Loop" )
+ }
+ )
+
+ int inflictorTeam = ballLightning.GetTeam()
+ ballLightning.e.ballLightningTargetsIdx = CreateScriptManagedEntArray()
+
+ WaitEndFrame()
+
+ while( 1 )
+ {
+ for( int i=0; i<BALL_LIGHTNING_BURST_NUM; i++ )
+ {
+ if ( BALL_LIGHTNING_BURST_NUM > 1 )
+ wait BALL_LIGHTNING_BURST_PAUSE
+
+ vector origin = ballLightning.GetOrigin()
+ BallLightningZapTargets( ballLightning, origin, inflictorTeam, damageSourceId, ballLightning.e.ballLightningData, false )
+ }
+ wait BALL_LIGHTNING_BURST_DELAY
+ }
+}
+
+void function BallLightningZapTargets( entity ballLightning, vector origin, int inflictorTeam, int damageSourceId, BallLightningData fxData, bool single )
+{
+ RadiusDamage(
+ origin, // origin
+ ballLightning.GetOwner(), // owner
+ ballLightning, // inflictor
+ fxData.damageToPilots, // normal damage
+ fxData.damage, // heavy armor damage
+ fxData.radius, // inner radius
+ fxData.radius, // outer radius
+ SF_ENVEXPLOSION_NO_DAMAGEOWNER, // explosion flags
+ 0, // distanceFromAttacker
+ 0, // explosionForce
+ fxData.deathPackage, // damage flags
+ damageSourceId // damage source id
+ )
+}
+
+string function GetEntityCenterTag( entity target )
+{
+ string tag = "center"
+
+ if ( IsHumanSized( target ) )
+ tag = "CHESTFOCUS"
+ else if ( target.IsTitan() )
+ tag = "HIJACK"
+ else if ( IsSuperSpectre( target ) || IsAirDrone( target ) )
+ tag = "CHESTFOCUS"
+ else if ( IsDropship( target ) )
+ tag = "ORIGIN"
+ else if ( target.GetClassName() == "npc_turret_mega" )
+ tag = "ATTACH"
+
+ return tag
+}
+
+bool function IsTargetEntValid( entity ballLightning, entity target, BallLightningData fxData )
+{
+ if ( !IsValid( target ) )
+ return false
+
+ vector origin = ballLightning.GetOrigin()
+
+ if ( target == ballLightning )
+ return false
+
+ if ( target == ballLightning.GetParent() )
+ return false
+
+ if ( target.GetParent() == ballLightning.GetParent() )
+ return false
+
+ // if ( target.IsPlayer() && !target.IsTitan() )
+ // return false
+
+ if ( fabs( origin.z - target.GetOrigin().z ) > fxData.height )
+ return false
+
+ if ( GetBugReproNum() != 131703 )
+ {
+ if ( target.GetModelName() == $"" )
+ return false
+ }
+
+ if ( !( target.GetClassName() in ArcCannonTargetClassnames ) )
+ return false
+
+ vector entityCenter = target.GetWorldSpaceCenter()
+
+ if ( target.GetModelName() != $"" )
+ {
+ string tag = GetEntityCenterTag( target )
+ int index = target.LookupAttachment( tag )
+
+ if ( index == 0 )
+ return false
+
+ entityCenter = target.GetAttachmentOrigin( index )
+ }
+
+ vector fwd = AnglesToForward( ballLightning.GetAngles() )
+ vector fwdToEnemy = Normalize( entityCenter - ballLightning.GetOrigin() )
+
+ float dot = DotProduct( fwd, fwdToEnemy )
+
+ if ( dot < fxData.minDot )
+ return false
+
+
+ if ( IsHumanSized( target ) )
+ {
+ float maxDist = fxData.humanRadius
+ if ( Distance( entityCenter, ballLightning.GetOrigin() ) > maxDist )
+ return false
+ }
+
+ // array<entity> ignoreEnts = [ target, ballLightning ]
+ // if ( ballLightning.GetParent() != null )
+ // ignoreEnts.append( ballLightning.GetParent() )
+
+ // TraceResults trace = TraceLine( ballLightning.GetOrigin(), entityCenter, ignoreEnts, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS )
+
+ // if ( trace.fraction < 1 )
+ // return false
+
+ VortexBulletHit ornull vortexHit = VortexBulletHitCheck( ballLightning.GetOwner(), ballLightning.GetOrigin(), entityCenter )
+
+ if ( vortexHit )
+ return false
+
+ return true
+}
+
+void function BallLightningZapConnectionFX( entity ballLightning, entity target, string tag, BallLightningData fxData )
+{
+ if ( fxData.zapFx != $"" )
+ {
+ // Control point sets the end position of the effect
+ entity cpEnd = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpEnd, GetUniqueCpString() )
+ cpEnd.SetParent( target, tag, false, 0.0 )
+ DispatchSpawn( cpEnd )
+
+ entity zapBeam = CreateEntity( "info_particle_system" )
+ zapBeam.kv.cpoint1 = cpEnd.GetTargetName()
+
+ zapBeam.SetValueForEffectNameKey( fxData.zapFx )
+ zapBeam.kv.start_active = 0
+ zapBeam.SetOwner( ballLightning )
+ zapBeam.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ zapBeam.SetParent( ballLightning, "", false, 0.0 )
+ DispatchSpawn( zapBeam )
+
+ zapBeam.Fire( "Start" )
+
+ OnThreadEnd(
+ function() : ( zapBeam, cpEnd )
+ {
+ if ( IsValid( zapBeam ) )
+ zapBeam.Destroy()
+ if ( IsValid( cpEnd ) )
+ cpEnd.Destroy()
+ }
+ )
+
+ ballLightning.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDeath" )
+
+ if ( fxData.zapLifetime > 0 )
+ {
+ wait fxData.zapLifetime
+ }
+ }
+}
+
+void function BallLightningZapFX( entity ballLightning, entity target, string tag, BallLightningData fxData )
+{
+ int index = target.LookupAttachment( tag )
+
+ vector entityCenter = target.GetAttachmentOrigin( index )
+
+ if ( fxData.zapImpactTable != "" )
+ PlayImpactFXTable( entityCenter, ballLightning.GetOwner(), fxData.zapImpactTable, SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+
+ EmitSoundOnEntity( ballLightning, fxData.zapSound )
+ thread FadeOutSoundOnEntityAfterDelay( ballLightning, fxData.zapSound, 0.2, 0.2 )
+}
+
+// This is to minimize creation of new Unique Strings
+string function GetUniqueCpString()
+{
+ foreach ( string uString, float useTime in file.uniqueStrings )
+ {
+ if ( useTime + BALL_LIGHTNING_ZAP_LIFETIME*2 > Time() )
+ continue
+
+ file.uniqueStrings[ uString ] = Time()
+ return uString
+ }
+
+ string newString = UniqueString( "ball_lightning_cpEnd" )
+
+ // printt( "Generated new string " + newString )
+
+ file.uniqueStrings[ newString ] <- Time()
+ return newString
+}
+
+entity function GetBallLightningFromEnt( entity ent )
+{
+ if ( "ballLightning" in ent.s )
+ return expect entity( ent.s.ballLightning )
+
+ return null
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_cloaker.gnut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_cloaker.gnut
new file mode 100644
index 00000000..6ec0bc0a
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_cloaker.gnut
@@ -0,0 +1,121 @@
+untyped
+
+global function CloakerThink
+global function CloakerShouldCloakGuy
+
+global function CloakerCloaksGuy
+global function CloakerDeCloaksGuy
+
+function CloakerThink( entity cloaker, float radius, array<string> ents = [ "any" ], vector offset = Vector(0,0,0), var shouldCloakGuyFunc = null, float waitTime = 0.25 )
+{
+ OnThreadEnd(
+ function() : ( cloaker )
+ {
+ local cloakList = clone cloaker.s.cloakList
+ foreach ( entity guy, value in cloakList )
+ {
+ if ( !IsAlive( guy ) )
+ continue
+
+ CloakerDeCloaksGuy( guy )
+ }
+ }
+ )
+
+ cloaker.s.cloakList <- {}
+ cloaker.s.decloakList <- {}
+
+ while( 1 )
+ {
+ vector origin = cloaker.GetOrigin() + offset
+ array<entity> guys
+
+ foreach ( entType in ents )
+ {
+ switch ( entType )
+ {
+ case "player":
+ case "players":
+ guys.extend( GetPlayerArrayEx( "any", cloaker.GetTeam(), TEAM_ANY, origin, radius ) )
+ break;
+ default:
+ guys.extend( GetNPCArrayEx( entType, cloaker.GetTeam(), TEAM_ANY, origin, radius ) )
+ break
+ }
+ }
+ int index = 0
+
+ float startTime = Time()
+
+ table cloakList = expect table( cloaker.s.cloakList )
+ cloaker.s.decloakList = clone cloakList
+
+ foreach ( guy in guys )
+ {
+ //only do 5 distanceSqr / cansee checks per frame
+ if ( index++ > 5 )
+ {
+ wait 0.1
+ index = 0
+ origin = cloaker.GetOrigin() + offset
+ }
+
+ bool shouldCloakGuy = CloakerShouldCloakGuy( cloaker, guy )
+
+ if ( shouldCloakGuy )
+ shouldCloakGuy = expect bool( shouldCloakGuyFunc( cloaker, guy ) )
+
+ if ( shouldCloakGuy )
+ {
+ if ( guy in cloaker.s.decloakList )
+ delete cloaker.s.decloakList[ guy ]
+
+ if ( IsCloaked( guy ) )
+ continue
+
+ cloakList[ guy ] <- true
+ CloakerCloaksGuy( guy )
+ }
+ }
+
+ foreach ( entity guy, value in cloaker.s.decloakList )
+ {
+ // any guys still in the decloakList shouldn't be decloaked ... if alive.
+ Assert( guy in cloakList )
+ delete cloakList[ guy ]
+
+ if ( IsAlive( guy ) )
+ CloakerDeCloaksGuy( guy )
+ }
+
+ float endTime = Time()
+ float elapsedTime = endTime - startTime
+ if ( elapsedTime < waitTime )
+ wait waitTime - elapsedTime
+ }
+}
+
+void function CloakerCloaksGuy( guy )
+{
+ guy.SetCloakDuration( 2.0, -1, 0 )
+ EmitSoundOnEntity( guy, CLOAKED_DRONE_CLOAK_START_SFX )
+ EmitSoundOnEntity( guy, CLOAKED_DRONE_CLOAK_LOOP_SFX )
+ guy.Minimap_Hide( TEAM_IMC, null )
+ guy.Minimap_Hide( TEAM_MILITIA, null )
+}
+
+void function CloakerDeCloaksGuy( guy )
+{
+ guy.SetCloakDuration( 0, 0, 1.5 )
+ StopSoundOnEntity( guy, CLOAKED_DRONE_CLOAK_LOOP_SFX )
+ guy.Minimap_AlwaysShow( TEAM_IMC, null )
+ guy.Minimap_AlwaysShow( TEAM_MILITIA, null )
+}
+
+bool function CloakerShouldCloakGuy( entity cloaker, entity guy )
+{
+ if ( !IsAlive( guy ) )
+ return false
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_grenade.nut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_grenade.nut
new file mode 100644
index 00000000..c2036e85
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_grenade.nut
@@ -0,0 +1,604 @@
+untyped
+
+global function Grenade_FileInit
+global function GetGrenadeThrowSound_1p
+global function GetGrenadeDeploySound_1p
+global function GetGrenadeThrowSound_3p
+global function GetGrenadeDeploySound_3p
+global function GetGrenadeProjectileSound
+
+const DEFAULT_FUSE_TIME = 2.25
+const DEFAULT_WARNING_TIME = 1.0
+global const float DEFAULT_MAX_COOK_TIME = 99999.9 //Longer than an entire day. Really just an arbitrarily large number
+
+global function Grenade_OnWeaponTossReleaseAnimEvent
+global function Grenade_OnWeaponTossCancelDrop
+global function Grenade_OnWeaponDeactivate
+global function Grenade_OnWeaponTossPrep
+global function Grenade_OnProjectileIgnite
+
+#if SERVER
+ global function Grenade_OnPlayerNPCTossGrenade_Common
+ global function ProxMine_Triggered
+ global function EnableTrapWarningSound
+ global function AddToProximityTargets
+ global function ProximityMineThink
+#endif
+global function Grenade_Init
+
+const GRENADE_EXPLOSIVE_WARNING_SFX_LOOP = "Weapon_Vortex_Gun.ExplosiveWarningBeep"
+const EMP_MAGNETIC_FORCE = 1600
+const MAG_FLIGHT_SFX_LOOP = "Explo_MGL_MagneticAttract"
+
+//Proximity Mine Settings
+global const PROXIMITY_MINE_EXPLOSION_DELAY = 1.2
+global const PROXIMITY_MINE_ARMING_DELAY = 1.0
+const TRIGGERED_ALARM_SFX = "Weapon_ProximityMine_CloseWarning"
+global const THERMITE_GRENADE_FX = $"P_grenade_thermite"
+global const CLUSTER_BASE_FX = $"P_wpn_meteor_exp"
+
+global const ProximityTargetClassnames = {
+ [ "npc_soldier_shield" ] = true,
+ [ "npc_soldier_heavy" ] = true,
+ [ "npc_soldier" ] = true,
+ [ "npc_spectre" ] = true,
+ [ "npc_drone" ] = true,
+ [ "npc_titan" ] = true,
+ [ "npc_marvin" ] = true,
+ [ "player" ] = true,
+ [ "npc_turret_mega" ] = true,
+ [ "npc_turret_sentry" ] = true,
+ [ "npc_dropship" ] = true,
+}
+
+const SOLDIER_ARC_STUN_ANIMS = [
+ "pt_react_ARC_fall",
+ "pt_react_ARC_kneefall",
+ "pt_react_ARC_sidefall",
+ "pt_react_ARC_slowfall",
+ "pt_react_ARC_scream",
+ "pt_react_ARC_stumble_F",
+ "pt_react_ARC_stumble_R" ]
+
+function Grenade_FileInit()
+{
+ PrecacheParticleSystem( CLUSTER_BASE_FX )
+
+ RegisterSignal( "ThrowGrenade" )
+ RegisterSignal( "WeaponDeactivateEvent" )
+ RegisterSignal( "OnEMPPilotHit" )
+ RegisterSignal( "StopGrenadeClientEffects" )
+ RegisterSignal( "DisableTrapWarningSound" )
+
+ //Globalize( MagneticFlight )
+
+ #if CLIENT
+ AddDestroyCallback( "grenade_frag", ClientDestroyCallback_GrenadeDestroyed )
+ #endif
+
+ #if SERVER
+ level._empForcedCallbacks <- {}
+ level._proximityTargetArrayID <- CreateScriptManagedEntArray()
+
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_proximity_mine, ProxMine_Triggered )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_thermite_grenade, Thermite_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_frag_grenade, Frag_DamagedPlayerOrNPC )
+
+ level._empForcedCallbacks[eDamageSourceId.mp_weapon_grenade_emp] <- true
+ level._empForcedCallbacks[eDamageSourceId.mp_weapon_proximity_mine] <- true
+
+ PrecacheParticleSystem( THERMITE_GRENADE_FX )
+ #endif
+}
+
+void function Grenade_OnWeaponTossPrep( entity weapon, WeaponTossPrepParams prepParams )
+{
+ weapon.w.startChargeTime = Time()
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+ weapon.EmitWeaponSound_1p3p( GetGrenadeDeploySound_1p( weapon ), GetGrenadeDeploySound_3p( weapon ) )
+
+ #if SERVER
+ thread HACK_CookGrenade( weapon, weaponOwner )
+ thread HACK_DropGrenadeOnDeath( weapon, weaponOwner )
+ #elseif CLIENT
+ if ( weaponOwner.IsPlayer() )
+ {
+ weaponOwner.p.grenadePulloutTime = Time()
+ }
+ #endif
+}
+
+void function Grenade_OnWeaponDeactivate( entity weapon )
+{
+ StopSoundOnEntity( weapon, GRENADE_EXPLOSIVE_WARNING_SFX_LOOP )
+ weapon.Signal( "WeaponDeactivateEvent" )
+}
+
+void function Grenade_OnProjectileIgnite( entity weapon )
+{
+ printt( "Grenade_OnProjectileIgnite() callback." )
+}
+
+function Grenade_Init( entity grenade, entity weapon )
+{
+ entity weaponOwner = weapon.GetOwner()
+ if ( IsValid( weaponOwner ) )
+ SetTeam( grenade, weaponOwner.GetTeam() )
+
+ // JFS: this is because I don't know if the above line should be
+ // weapon.GetOwner() or it's a typo and should really be weapon.GetWeaponOwner()
+ // and it's too close to ship and who knows what effect that will have
+ entity owner = weapon.GetWeaponOwner()
+ if ( IsMultiplayer() && IsValid( owner ) )
+ {
+ if ( owner.IsNPC() )
+ {
+ SetTeam( grenade, owner.GetTeam() )
+ }
+ }
+
+ #if SERVER
+ bool smartPistolVisible = weapon.GetWeaponSettingBool( eWeaponVar.projectile_visible_to_smart_ammo )
+ if ( smartPistolVisible )
+ {
+ grenade.SetDamageNotifications( true )
+ grenade.SetTakeDamageType( DAMAGE_EVENTS_ONLY )
+ grenade.proj.onlyAllowSmartPistolDamage = true
+
+ if ( !grenade.GetProjectileWeaponSettingBool( eWeaponVar.projectile_damages_owner ) && !grenade.GetProjectileWeaponSettingBool( eWeaponVar.explosion_damages_owner ) )
+ SetCustomSmartAmmoTarget( grenade, true ) // prevent friendly target lockon
+ }
+ else
+ {
+ grenade.SetTakeDamageType( DAMAGE_NO )
+ }
+ #endif
+ if ( IsValid( weaponOwner ) )
+ grenade.s.originalOwner <- weaponOwner // for later in damage callbacks, to skip damage vs friendlies but not for og owner or his enemies
+}
+
+
+int function Grenade_OnWeaponToss_( entity weapon, WeaponPrimaryAttackParams attackParams, float directionScale )
+{
+ weapon.EmitWeaponSound_1p3p( GetGrenadeThrowSound_1p( weapon ), GetGrenadeThrowSound_3p( weapon ) )
+ bool projectilePredicted = PROJECTILE_PREDICTED
+ bool projectileLagCompensated = PROJECTILE_LAG_COMPENSATED
+#if SERVER
+ if ( weapon.IsForceReleaseFromServer() )
+ {
+ projectilePredicted = false
+ projectileLagCompensated = false
+ }
+#endif
+ entity grenade = Grenade_Launch( weapon, attackParams.pos, (attackParams.dir * directionScale), projectilePredicted, projectileLagCompensated )
+ entity weaponOwner = weapon.GetWeaponOwner()
+ weaponOwner.Signal( "ThrowGrenade" )
+
+ PlayerUsedOffhand( weaponOwner, weapon ) // intentionally here and in Hack_DropGrenadeOnDeath - accurate for when cooldown actually begins
+
+#if SERVER
+
+ #if BATTLECHATTER_ENABLED
+ TryPlayWeaponBattleChatterLine( weaponOwner, weapon )
+ #endif
+
+#endif
+
+ return weapon.GetWeaponSettingInt( eWeaponVar.ammo_per_shot )
+}
+
+var function Grenade_OnWeaponTossReleaseAnimEvent( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ var result = Grenade_OnWeaponToss_( weapon, attackParams, 1.0 )
+ return result
+}
+
+var function Grenade_OnWeaponTossCancelDrop( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ var result = Grenade_OnWeaponToss_( weapon, attackParams, 0.2 )
+ return result
+}
+
+// Can return entity or nothing
+entity function Grenade_Launch( entity weapon, vector attackPos, vector throwVelocity, bool isPredicted, bool isLagCompensated )
+{
+ #if CLIENT
+ if ( !weapon.ShouldPredictProjectiles() )
+ return null
+ #endif
+
+ //TEMP FIX while Deploy anim is added to sprint
+ float currentTime = Time()
+ if ( weapon.w.startChargeTime == 0.0 )
+ weapon.w.startChargeTime = currentTime
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+
+ var discThrow = weapon.GetWeaponInfoFileKeyField( "grenade_disc_throw" )
+
+ vector angularVelocity = Vector( 3600, RandomFloatRange( -1200, 1200 ), 0 )
+
+ if ( discThrow == 1 )
+ angularVelocity = Vector( 100, 100, RandomFloatRange( 1200, 2200 ) )
+
+
+ float fuseTime
+
+ float baseFuseTime = weapon.GetGrenadeFuseTime() //Note that fuse time of 0 means the grenade won't explode on its own, instead it depends on OnProjectileCollision() functions to be defined and explode there. Arguably in this case grenade_fuse_time shouldn't be 0, but an arbitrarily large number instead.
+ if ( baseFuseTime > 0.0 )
+ {
+ fuseTime = baseFuseTime - ( currentTime - weapon.w.startChargeTime )
+ if ( fuseTime <= 0 )
+ fuseTime = 0.001
+ }
+ else
+ {
+ fuseTime = baseFuseTime
+ }
+
+ int damageFlags = weapon.GetWeaponDamageFlags()
+ entity frag = weapon.FireWeaponGrenade( attackPos, throwVelocity, angularVelocity, fuseTime, damageFlags, damageFlags, isPredicted, isLagCompensated, true )
+ if ( frag == null )
+ return null
+
+ if ( discThrow == 1 ) // add wobble by pitching it slightly
+ {
+ Assert( !frag.IsMarkedForDeletion(), "Frag before .SetAngles() is marked for deletion." )
+ frag.SetAngles( frag.GetAngles() + < RandomFloatRange( 7,11 ),0,0 > )
+ //Assert( !frag.IsMarkedForDeletion(), "Frag after .SetAngles() is marked for deletion." )
+ if ( frag.IsMarkedForDeletion() )
+ {
+ CodeWarning( "Frag after .SetAngles() was marked for deletion." )
+ return null
+ }
+ }
+
+ Grenade_OnPlayerNPCTossGrenade_Common( weapon, frag )
+
+ return frag
+}
+
+void function Grenade_OnPlayerNPCTossGrenade_Common( entity weapon, entity frag )
+{
+ Grenade_Init( frag, weapon )
+ #if SERVER
+ thread TrapExplodeOnDamage( frag, 20, 0.0, 0.0 )
+
+ string projectileSound = GetGrenadeProjectileSound( weapon )
+ if ( projectileSound != "" )
+ EmitSoundOnEntity( frag, projectileSound )
+ #endif
+
+ if( weapon.HasMod( "burn_mod_emp_grenade" ) )
+ frag.InitMagnetic( EMP_MAGNETIC_FORCE, MAG_FLIGHT_SFX_LOOP )
+}
+
+struct CookGrenadeStruct //Really just a convenience struct so we can read the changed value of a bool in an OnThreadEnd
+{
+ bool shouldOverrideFuseTime = false
+}
+
+void function HACK_CookGrenade( entity weapon, entity weaponOwner )
+{
+ float maxCookTime = GetMaxCookTime( weapon )
+ if ( maxCookTime >= DEFAULT_MAX_COOK_TIME )
+ return
+
+ weaponOwner.EndSignal( "OnDeath" )
+ weaponOwner.EndSignal( "ThrowGrenade" )
+ weapon.EndSignal( "WeaponDeactivateEvent" )
+ weapon.EndSignal( "OnDestroy" )
+
+ /*CookGrenadeStruct grenadeStruct
+
+ OnThreadEnd(
+ function() : ( weapon, grenadeStruct )
+ {
+ if ( grenadeStruct.shouldOverrideFuseTime )
+ {
+ var minFuseTime = weapon.GetWeaponInfoFileKeyField( "min_fuse_time" )
+ printt( "minFuseTime: " + minFuseTime )
+ if ( minFuseTime != null )
+ {
+ expect float( minFuseTime )
+ printt( "Setting overrideFuseTime to : " + weapon.GetWeaponInfoFileKeyField( "min_fuse_time" ) )
+ weapon.w.overrideFuseTime = minFuseTime
+ }
+ }
+ }
+ )
+*/
+ if ( maxCookTime - DEFAULT_WARNING_TIME <= 0 )
+ {
+ EmitSoundOnEntity( weapon, GRENADE_EXPLOSIVE_WARNING_SFX_LOOP )
+ wait maxCookTime
+ }
+ else
+ {
+ wait( maxCookTime - DEFAULT_WARNING_TIME )
+
+ EmitSoundOnEntity( weapon, GRENADE_EXPLOSIVE_WARNING_SFX_LOOP )
+
+ wait( DEFAULT_WARNING_TIME )
+ }
+
+ if ( !IsValid( weapon.GetWeaponOwner() ) )
+ return
+
+ weapon.ForceReleaseFromServer() // Will eventually result in Grenade_OnWeaponToss_() or equivalent function
+
+ // JFS: prevent grenade cook exploit in coliseum
+ if ( GameRules_GetGameMode() == COLISEUM )
+ {
+ #if SERVER
+ int damageSource = weapon.GetDamageSourceID()
+
+ if ( damageSource == eDamageSourceId.mp_weapon_frag_grenade )
+ {
+ var impact_effect_table = weapon.GetWeaponInfoFileKeyField( "impact_effect_table" )
+ if ( impact_effect_table != null )
+ {
+ string fx = expect string( impact_effect_table )
+ PlayImpactFXTable( weaponOwner.EyePosition(), weaponOwner, fx )
+ }
+ weaponOwner.Die( weaponOwner, weapon, { damageSourceId = damageSource } )
+ }
+ #endif
+ }
+
+ weaponOwner.Signal( "ThrowGrenade" ) // Only necessary to end HACK_DropGrenadeOnDeath
+}
+
+
+void function HACK_WaitForGrenadeDropEvent( weapon, entity weaponOwner )
+{
+ weapon.EndSignal( "WeaponDeactivateEvent" )
+
+ weaponOwner.WaitSignal( "OnDeath" )
+}
+
+
+void function HACK_DropGrenadeOnDeath( entity weapon, entity weaponOwner )
+{
+ if ( weapon.HasMod( "burn_card_weapon_mod" ) ) //JFS: Primarily to stop boost grenade weapons (e.g. frag_drone ) not doing TryUsingBurnCardWeapon() when dropped through this function.
+ return
+
+ weaponOwner.EndSignal( "ThrowGrenade" )
+ weaponOwner.EndSignal( "OnDestroy" )
+
+ waitthread HACK_WaitForGrenadeDropEvent( weapon, weaponOwner )
+
+ if( !IsValid( weaponOwner ) || !IsValid( weapon ) || IsAlive( weaponOwner ) )
+ return
+
+ float elapsedTime = Time() - weapon.w.startChargeTime
+ float baseFuseTime = weapon.GetGrenadeFuseTime()
+ float fuseDelta = (baseFuseTime - elapsedTime)
+
+ if ( (baseFuseTime == 0.0) || (fuseDelta > -0.1) )
+ {
+ float forwardScale = weapon.GetWeaponSettingFloat( eWeaponVar.grenade_death_drop_velocity_scale )
+ vector velocity = weaponOwner.GetForwardVector() * forwardScale
+ velocity.z += weapon.GetWeaponSettingFloat( eWeaponVar.grenade_death_drop_velocity_extraUp )
+ vector angularVelocity = Vector( 0, 0, 0 )
+ float fuseTime = baseFuseTime ? baseFuseTime - elapsedTime : baseFuseTime
+
+ int primaryClipCount = weapon.GetWeaponPrimaryClipCount()
+ int ammoPerShot = weapon.GetWeaponSettingInt( eWeaponVar.ammo_per_shot )
+ weapon.SetWeaponPrimaryClipCountAbsolute( maxint( 0, primaryClipCount - ammoPerShot ) )
+
+ PlayerUsedOffhand( weaponOwner, weapon ) // intentionally here and in ReleaseAnimEvent - for cases where grenade is dropped on death
+
+ entity grenade = Grenade_Launch( weapon, weaponOwner.GetOrigin(), velocity, PROJECTILE_NOT_PREDICTED, PROJECTILE_NOT_LAG_COMPENSATED )
+ }
+}
+
+
+#if SERVER
+void function ProxMine_Triggered( entity ent, var damageInfo )
+{
+ if ( !IsValid( ent ) )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !IsValid( attacker ) )
+ return
+
+ if ( attacker == ent )
+ return
+
+ if ( ent.IsPlayer() || ent.IsNPC() )
+ thread ShowProxMineTriggeredIcon( ent )
+
+ //If this feature is good, we should add this to NPCs as well. Currently script errors if applied to an NPC.
+ //if ( ent.IsPlayer() )
+ // thread ProxMine_ShowOnMinimapTimed( ent, GetOtherTeam( ent.GetTeam() ), PROX_MINE_MARKER_TIME )
+}
+
+/*
+function ProxMine_ShowOnMinimapTimed( ent, teamToDisplayEntTo, duration )
+{
+ ent.Minimap_AlwaysShow( teamToDisplayEntTo, null )
+ Minimap_CreatePingForTeam( teamToDisplayEntTo, ent.GetOrigin(), $"vgui/HUD/titanFiringPing", 1.0 )
+
+ wait duration
+
+ if ( IsValid( ent ) && ent.IsPlayer() )
+ ent.Minimap_DisplayDefault( teamToDisplayEntTo, ent )
+}
+*/
+
+void function Thermite_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ if ( !IsValid( ent ) )
+ return
+
+ Thermite_DamagePlayerOrNPCSounds( ent )
+}
+
+void function Frag_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ #if MP
+ if ( !IsValid( ent ) || ent.IsPlayer() || ent.IsTitan() )
+ return
+
+ if ( ent.IsMechanical() )
+ DamageInfo_ScaleDamage( damageInfo, 0.5 )
+ #endif
+}
+
+#endif // SERVER
+
+
+#if CLIENT
+void function ClientDestroyCallback_GrenadeDestroyed( entity grenade )
+{
+}
+#endif // CLIENT
+
+#if SERVER
+function EnableTrapWarningSound( entity trap, delay = 0, warningSound = DEFAULT_WARNING_SFX )
+{
+ trap.EndSignal( "OnDestroy" )
+ trap.EndSignal( "DisableTrapWarningSound" )
+
+ if ( delay > 0 )
+ wait delay
+
+ while ( IsValid( trap ) )
+ {
+ EmitSoundOnEntity( trap, warningSound )
+ wait 1.0
+ }
+}
+
+void function AddToProximityTargets( entity ent )
+{
+ AddToScriptManagedEntArray( level._proximityTargetArrayID, ent );
+}
+
+function ProximityMineThink( entity proximityMine, entity owner )
+{
+ proximityMine.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( proximityMine )
+ {
+ if ( IsValid( proximityMine ) )
+ proximityMine.Destroy()
+ }
+ )
+ thread TrapExplodeOnDamage( proximityMine, 50 )
+
+ wait PROXIMITY_MINE_ARMING_DELAY
+
+ int teamNum = proximityMine.GetTeam()
+ float explodeRadius = proximityMine.GetDamageRadius()
+ float triggerRadius = ( ( explodeRadius * 0.75 ) + 0.5 )
+ local lastTimeNPCsChecked = 0
+ local NPCTickRate = 0.5
+ local PlayerTickRate = 0.2
+
+ // Wait for someone to enter proximity
+ while( IsValid( proximityMine ) && IsValid( owner ) )
+ {
+ if ( lastTimeNPCsChecked + NPCTickRate <= Time() )
+ {
+ array<entity> nearbyNPCs = GetNPCArrayEx( "any", TEAM_ANY, teamNum, proximityMine.GetOrigin(), triggerRadius )
+ foreach( ent in nearbyNPCs )
+ {
+ if ( ShouldSetOffProximityMine( proximityMine, ent ) )
+ {
+ ProximityMine_Explode( proximityMine )
+ return
+ }
+ }
+ lastTimeNPCsChecked = Time()
+ }
+
+ array<entity> nearbyPlayers = GetPlayerArrayEx( "any", TEAM_ANY, teamNum, proximityMine.GetOrigin(), triggerRadius )
+ foreach( ent in nearbyPlayers )
+ {
+ if ( ShouldSetOffProximityMine( proximityMine, ent ) )
+ {
+ ProximityMine_Explode( proximityMine )
+ return
+ }
+ }
+
+ wait PlayerTickRate
+ }
+}
+
+function ProximityMine_Explode( proximityMine )
+{
+ local explodeTime = Time() + PROXIMITY_MINE_EXPLOSION_DELAY
+ EmitSoundOnEntity( proximityMine, TRIGGERED_ALARM_SFX )
+
+ wait PROXIMITY_MINE_EXPLOSION_DELAY
+
+ if ( IsValid( proximityMine ) )
+ proximityMine.GrenadeExplode( proximityMine.GetForwardVector() )
+}
+
+bool function ShouldSetOffProximityMine( entity proximityMine, entity ent )
+{
+ if ( !IsAlive( ent ) )
+ return false
+
+ if ( ent.IsPhaseShifted() )
+ return false
+
+ TraceResults results = TraceLine( proximityMine.GetOrigin(), ent.EyePosition(), proximityMine, (TRACE_MASK_SHOT | CONTENTS_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+ if ( results.fraction >= 1 || results.hitEnt == ent )
+ return true
+
+ return false
+}
+
+#endif // SERVER
+
+
+
+float function GetMaxCookTime( entity weapon )
+{
+ var cookTime = weapon.GetWeaponInfoFileKeyField( "max_cook_time" )
+ if (cookTime == null )
+ return DEFAULT_MAX_COOK_TIME
+
+ expect float ( cookTime )
+ return cookTime
+}
+
+function GetGrenadeThrowSound_1p( weapon )
+{
+ return weapon.GetWeaponInfoFileKeyField( "sound_throw_1p" ) ? weapon.GetWeaponInfoFileKeyField( "sound_throw_1p" ) : ""
+}
+
+
+function GetGrenadeDeploySound_1p( weapon )
+{
+ return weapon.GetWeaponInfoFileKeyField( "sound_deploy_1p" ) ? weapon.GetWeaponInfoFileKeyField( "sound_deploy_1p" ) : ""
+}
+
+
+function GetGrenadeThrowSound_3p( weapon )
+{
+ return weapon.GetWeaponInfoFileKeyField( "sound_throw_3p" ) ? weapon.GetWeaponInfoFileKeyField( "sound_throw_3p" ) : ""
+}
+
+
+function GetGrenadeDeploySound_3p( weapon )
+{
+ return weapon.GetWeaponInfoFileKeyField( "sound_deploy_3p" ) ? weapon.GetWeaponInfoFileKeyField( "sound_deploy_3p" ) : ""
+}
+
+string function GetGrenadeProjectileSound( weapon )
+{
+ return expect string( weapon.GetWeaponInfoFileKeyField( "sound_grenade_projectile" ) ? weapon.GetWeaponInfoFileKeyField( "sound_grenade_projectile" ) : "" )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_particle_wall.gnut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_particle_wall.gnut
new file mode 100644
index 00000000..a46bfff8
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_particle_wall.gnut
@@ -0,0 +1,460 @@
+untyped
+
+global function ParticleWall_Init
+
+global function CreateTurretParticleWall
+global function CreateParticleWallFromOwner
+global function CreateShieldWithSettings
+global function DrainHealthOverTime
+
+global function CreateAmpedWallFromOwner
+
+global function CreateParticleWallForOwnerFromDirection
+
+global const SHIELD_WALL_COL_MODEL = $"models/fx/xo_shield_wall.mdl"
+global const SHIELD_WALL_FX = $"P_xo_shield_wall"
+
+global const TURRET_SHIELD_WALL_COL_MODEL = $"models/fx/turret_shield_wall.mdl"
+global const TURRET_SHIELD_WALL_FX = $"P_turret_shield_wall"
+
+global const AMPED_WALL_FX = $"P_xo_amped_wall"
+#if MP
+global const SHIELD_WALL_HEALTH = 2000
+global const TURRET_SHIELD_WALL_HEALTH = 3500//1750
+#else
+global const SHIELD_WALL_HEALTH = 1750
+global const TURRET_SHIELD_WALL_HEALTH = 1750
+#endif
+global const PAS_TONE_WALL_HEALTH = 3000
+global const PAS_TONE_WALL_DURATION_MULTIPLIER = 1.5
+global const SHIELD_WALL_DURATION = 8.0
+global const SHIELD_WALL_RADIUS = 180
+global const SHIELD_WALL_FOV = 120
+global const SHIELD_WALL_WIDTH = 156.0 // SHIELD_WALL_RADIUS * cos( SHIELD_WALL_FOV/2 )
+
+global function UpdateShieldWallColorForFrac
+global function PlayEffectOnVortexSphere
+global function SetVortexSphereShieldWallCPoint
+global function SetShieldWallCPoint
+global function StopShieldWallFX
+global function StopShieldWallFXOverTime
+global function SetShieldWallCPointOrigin
+
+function ParticleWall_Init()
+{
+ PrecacheParticleSystem( SHIELD_WALL_FX )
+ PrecacheModel( SHIELD_WALL_COL_MODEL )
+
+ PrecacheParticleSystem( TURRET_SHIELD_WALL_FX )
+ PrecacheModel( TURRET_SHIELD_WALL_COL_MODEL )
+
+ PrecacheParticleSystem( AMPED_WALL_FX )
+}
+
+void function CreateParticleWallFromOwner( entity weaponOwner, float duration, WeaponPrimaryAttackParams attackParams )
+{
+ vector dir = GetParticleWallAttackAnglesFromOwner( weaponOwner, attackParams )
+ CreateParticleWallForOwnerFromDirection( weaponOwner, duration, dir )
+}
+
+vector function GetParticleWallAttackAnglesFromOwner( entity weaponOwner, WeaponPrimaryAttackParams attackParams )
+{
+ if ( weaponOwner.IsNPC() )
+ return attackParams.dir
+
+ vector angles = weaponOwner.CameraAngles()
+ angles.x = 0
+ return AnglesToForward( angles )
+}
+
+void function CreateParticleWallForOwnerFromDirection( entity weaponOwner, float duration, vector dir )
+{
+ Assert( IsServer() )
+
+ entity titanSoul = weaponOwner.GetTitanSoul()
+
+ // JFS the weapon owner should always have a soul, at least on the server
+ if ( !IsValid( titanSoul ) )
+ return
+
+ vector origin = weaponOwner.GetOrigin()
+ vector safeSpot = origin
+ vector angles = VectorToAngles( dir )
+
+ if ( weaponOwner.IsNPC() )
+ {
+ // spawn in front of npc a bit
+ origin += dir * 100
+ }
+
+ float endTime = Time() + duration
+ titanSoul.SetDefensivePlacement( endTime, SHIELD_WALL_WIDTH, 0, true, safeSpot, dir )
+
+ Assert( weaponOwner.IsTitan() )
+ Assert( titanSoul )
+
+ int health
+ if ( SoulHasPassive( titanSoul, ePassives.PAS_TONE_WALL ) )
+ {
+ health = PAS_TONE_WALL_HEALTH
+ duration *= PAS_TONE_WALL_DURATION_MULTIPLIER
+ }
+ else
+ {
+ health = SHIELD_WALL_HEALTH
+ }
+ entity vortexSphere = CreateShieldWithSettings( origin + < 0, 0, -64 >, angles, SHIELD_WALL_RADIUS, SHIELD_WALL_RADIUS * 2, SHIELD_WALL_FOV, duration, health, SHIELD_WALL_FX )
+ thread DrainHealthOverTime( vortexSphere, vortexSphere.e.shieldWallFX, duration )
+
+ entity groundEntity = weaponOwner.GetGroundEntity()
+ if ( groundEntity != null && groundEntity.HasPusherRootParent() )
+ vortexSphere.SetParent( groundEntity, "", true, 0 )
+}
+
+entity function CreateTurretParticleWall( vector origin, vector angles, float duration )
+{
+ Assert( IsServer() )
+
+ entity vortexSphere = CreateTurretShieldWithSettings( origin + < 0, 0, -64 >, angles, SHIELD_WALL_RADIUS, int( SHIELD_WALL_RADIUS * 1.65 ), 270, duration, TURRET_SHIELD_WALL_HEALTH, TURRET_SHIELD_WALL_FX )
+ thread DrainHealthOverTime( vortexSphere, vortexSphere.e.shieldWallFX, duration )
+
+ return vortexSphere
+}
+
+entity function CreateShieldWithSettings( vector origin, vector angles, int radius, int height, int fov, float duration, int health, asset effectName )
+{
+ entity vortexSphere = CreateEntity( "vortex_sphere" )
+
+ vortexSphere.kv.spawnflags = SF_ABSORB_BULLETS | SF_BLOCK_OWNER_WEAPON | SF_BLOCK_NPC_WEAPON_LOF | SF_ABSORB_CYLINDER
+ vortexSphere.kv.enabled = 0
+ vortexSphere.kv.radius = radius
+ vortexSphere.kv.height = height
+ vortexSphere.kv.bullet_fov = fov
+ vortexSphere.kv.physics_pull_strength = 25
+ vortexSphere.kv.physics_side_dampening = 6
+ vortexSphere.kv.physics_fov = 360
+ vortexSphere.kv.physics_max_mass = 2
+ vortexSphere.kv.physics_max_size = 6
+
+ vortexSphere.SetAngles( angles ) // viewvec?
+ vortexSphere.SetOrigin( origin )
+ vortexSphere.SetMaxHealth( health )
+ vortexSphere.SetHealth( health )
+ vortexSphere.SetTakeDamageType( DAMAGE_YES )
+
+ DispatchSpawn( vortexSphere )
+
+ vortexSphere.Fire( "Enable" )
+ vortexSphere.Fire( "Kill", "", duration )
+
+ // Shield wall fx control point
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) )
+ DispatchSpawn( cpoint )
+
+ // Shield wall fx
+ entity shieldWallFX = PlayFXWithControlPoint( effectName, origin, cpoint, -1, null, angles, C_PLAYFX_LOOP )
+ vortexSphere.e.shieldWallFX = shieldWallFX
+ shieldWallFX.SetParent( vortexSphere )
+ SetVortexSphereShieldWallCPoint( vortexSphere, cpoint )
+ StopShieldWallFXOverTime( vortexSphere, duration )
+
+
+ thread StopFXOnDestroy( vortexSphere, shieldWallFX, duration )
+ return vortexSphere
+}
+
+//Turret Shields do not block npc line of fire.
+entity function CreateTurretShieldWithSettings( vector origin, vector angles, int radius, int height, int fov, float duration, int health, asset effectName )
+{
+ entity vortexSphere = CreateEntity( "vortex_sphere" )
+
+ vortexSphere.kv.spawnflags = SF_ABSORB_BULLETS | SF_BLOCK_OWNER_WEAPON | SF_ABSORB_CYLINDER
+ vortexSphere.kv.enabled = 0
+ vortexSphere.kv.radius = radius
+ vortexSphere.kv.height = height
+ vortexSphere.kv.bullet_fov = fov
+ vortexSphere.kv.physics_pull_strength = 25
+ vortexSphere.kv.physics_side_dampening = 6
+ vortexSphere.kv.physics_fov = 360
+ vortexSphere.kv.physics_max_mass = 2
+ vortexSphere.kv.physics_max_size = 6
+
+ vortexSphere.SetAngles( angles ) // viewvec?
+ vortexSphere.SetOrigin( origin )
+ vortexSphere.SetMaxHealth( health )
+ vortexSphere.SetHealth( health )
+ vortexSphere.SetTakeDamageType( DAMAGE_YES )
+
+ DispatchSpawn( vortexSphere )
+
+ vortexSphere.Fire( "Enable" )
+ vortexSphere.Fire( "Kill", "", duration )
+
+ // Shield wall fx control point
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) )
+ DispatchSpawn( cpoint )
+
+ // Shield wall fx
+ entity shieldWallFX = PlayFXWithControlPoint( effectName, origin, cpoint, -1, null, angles, C_PLAYFX_LOOP )
+ vortexSphere.e.shieldWallFX = shieldWallFX
+ shieldWallFX.SetParent( vortexSphere )
+ SetVortexSphereShieldWallCPoint( vortexSphere, cpoint )
+ StopShieldWallFXOverTime( vortexSphere, duration )
+
+
+ thread StopFXOnDestroy( vortexSphere, shieldWallFX, duration )
+ return vortexSphere
+}
+
+function StopFXOnDestroy( entity vortexSphere, entity shieldWallFX, float duration )
+{
+ vortexSphere.EndSignal( "OnDestroy" )
+ shieldWallFX.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( vortexSphere )
+ {
+ StopShieldWallFX( vortexSphere )
+ }
+ )
+
+ wait duration * 1.5
+}
+
+void function CreateAmpedWallFromOwner( entity weaponOwner, float duration, WeaponPrimaryAttackParams attackParams )
+{
+ Assert( IsNewThread(), "Must be threaded off" )
+ Assert( IsServer() )
+ entity titanSoul = weaponOwner.GetTitanSoul()
+
+ // JFS the weapon owner should always have a soul, at least on the server
+ if ( !IsValid( titanSoul ) )
+ return
+ Assert( weaponOwner.IsTitan() )
+
+ vector dir = GetParticleWallAttackAnglesFromOwner( weaponOwner, attackParams )
+ vector origin = weaponOwner.GetOrigin()
+ vector safeSpot = origin
+ vector angles = VectorToAngles( dir )
+ vector forward = AnglesToForward( angles )
+ angles = AnglesCompose( angles, <0,180,0> )
+
+ if ( weaponOwner.IsNPC() )
+ {
+ // spawn in front of npc a bit
+ origin += dir * 100
+ }
+
+ origin += dir * 500
+ origin += Vector(0,0,-64)
+
+ float endTime = Time() + duration
+ titanSoul.SetDefensivePlacement( endTime, SHIELD_WALL_WIDTH, 0, true, safeSpot, dir )
+
+ entity vortexSphere = CreateShieldWithSettings( origin, angles, SHIELD_WALL_RADIUS, SHIELD_WALL_RADIUS * 2, SHIELD_WALL_FOV, duration, SHIELD_WALL_HEALTH, AMPED_WALL_FX )
+ vortexSphere.EndSignal( "OnDestroy" )
+ entity shieldWallFX = vortexSphere.e.shieldWallFX
+ shieldWallFX.EndSignal( "OnDestroy" )
+ SetTargetName( vortexSphere, PROTO_AMPED_WALL ) // so projectiles pass through
+
+ SetShieldWallCPointOrigin( shieldWallFX, < BURN_CARD_WEAPON_HUD_COLOR[0], BURN_CARD_WEAPON_HUD_COLOR[1], BURN_CARD_WEAPON_HUD_COLOR[2] > )
+
+ float tickRate = 0.1
+ float dps = vortexSphere.GetMaxHealth() / duration
+ float dmgAmount = dps * tickRate
+
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+
+ float endSoundTime = endTime - 3.0 // so magic
+ thread PlayDelayedVortexEndSound( endSoundTime, vortexSphere )
+ bool playedEndSound = false
+ vector vortexOrigin = vortexSphere.GetOrigin()
+ entity mover = CreateScriptMover()
+
+ int weaponOwnerTeam = weaponOwner.GetTeam();
+
+ OnThreadEnd(
+ function() : ( vortexSphere, vortexOrigin, endTime, mover, weaponOwnerTeam )
+ {
+ if ( IsValid( vortexSphere ) )
+ {
+ StopSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+ StopSoundOnEntity( vortexSphere, "ShieldWall_End" )
+ }
+
+ if ( IsValid( mover ) )
+ mover.Destroy()
+
+ if ( endTime - Time() >= 1.0 )
+ EmitSoundAtPosition( weaponOwnerTeam, vortexOrigin, "ShieldWall_Destroyed" )
+ }
+ )
+
+ int rampOuts = 3
+ float rampOutTime = 0.75
+ float rampOutFinalFade = 1.0
+ float finalFadeExtraBuffer = 0.45
+
+ wait duration - ( rampOutTime * rampOuts + rampOutFinalFade + finalFadeExtraBuffer )
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_End" )
+
+ entity cpoint = GetShieldWallFXCPoint( shieldWallFX )
+
+ vector cpointOrigin = cpoint.GetOrigin()
+ mover.SetOrigin( cpointOrigin )
+ cpoint.SetParent( mover )
+ float rampTime1 = rampOutTime * 0.75
+ float rampTime2 = rampOutTime - rampTime1
+ for ( int i = 0; i < rampOuts; i++ )
+ {
+ mover.NonPhysicsMoveTo( <100,0,0>, rampTime1, rampTime1, 0.0 )
+ wait rampTime1
+ mover.NonPhysicsMoveTo( cpointOrigin, rampTime2, 0.0, rampTime2 )
+ wait rampTime2
+ }
+
+ mover.NonPhysicsMoveTo( <0,0,0>, rampOutFinalFade, 0.0, 0.0 )
+ wait rampOutFinalFade + finalFadeExtraBuffer
+}
+
+void function PlayDelayedVortexEndSound( float delay, entity vortexSphere )
+{
+ vortexSphere.EndSignal( "OnDestroy" )
+ wait delay
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_End" )
+}
+
+
+function DrainHealthOverTime( entity vortexSphere, entity shieldWallFX, float duration )
+{
+ vortexSphere.EndSignal( "OnDestroy" )
+ shieldWallFX.EndSignal( "OnDestroy" )
+
+ float startTime = Time()
+ float endTime = startTime + duration
+
+ float tickRate = 0.1
+ float dps = vortexSphere.GetMaxHealth() / duration
+ float dmgAmount = dps * tickRate
+
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+
+ float endSoundTime = endTime - 3.0
+ bool playedEndSound = false
+ vector vortexOrigin = vortexSphere.GetOrigin()
+
+ OnThreadEnd(
+ function() : ( vortexSphere, vortexOrigin, endTime )
+ {
+ if ( endTime - Time() < 1.0 )
+ return
+
+ int teamNum = TEAM_UNASSIGNED
+
+ if ( IsValid( vortexSphere ) )
+ {
+ StopSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+ StopSoundOnEntity( vortexSphere, "ShieldWall_End" )
+
+ teamNum = vortexSphere.GetTeam()
+ }
+
+ EmitSoundAtPosition( teamNum, vortexOrigin, "ShieldWall_Destroyed" )
+ }
+ )
+
+ while ( Time() < endTime )
+ {
+ if ( Time() > endSoundTime && !playedEndSound )
+ {
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_End" )
+ playedEndSound = true
+ }
+
+ //vortexSphere.SetHealth( vortexSphere.GetHealth() - dmgAmount )
+ UpdateShieldWallColorForFrac( shieldWallFX, GetHealthFrac( vortexSphere ) )
+ wait tickRate
+ }
+
+ StopSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+}
+
+function UpdateShieldWallColorForFrac( entity shieldWallFX, float colorFrac )
+{
+ vector color = GetShieldTriLerpColor( 1 - colorFrac )
+
+ if ( IsValid( shieldWallFX ) )
+ SetShieldWallCPointOrigin( shieldWallFX, color )
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+//
+// All functions that care about to-be-deprecated cpoint are below here:
+//
+////////////////////////////////////////////////////////////////////////////////////////////////
+
+void function PlayEffectOnVortexSphere( int fx, vector origin, vector angles, entity vortexSphere )
+{
+ if ( !IsValid( vortexSphere ) )
+ return
+ if ( !IsValid( vortexSphere.e.shieldWallFX ) )
+ return
+ entity cpoint = vortexSphere.e.shieldWallFX.e.cpoint
+ if ( !IsValid( cpoint ) )
+ return
+ StartParticleEffectInWorldWithControlPoint( fx, origin, angles, cpoint.GetOrigin() )
+}
+
+void function SetVortexSphereShieldWallCPoint( entity vortexSphere, entity cpoint )
+{
+ Assert( IsValid( vortexSphere ) )
+ Assert( IsValid( vortexSphere.e.shieldWallFX ) )
+ SetShieldWallCPoint( vortexSphere.e.shieldWallFX, cpoint )
+}
+
+void function SetShieldWallCPoint( entity shieldWallFX, entity cpoint )
+{
+ Assert( IsValid( shieldWallFX ) )
+ Assert( IsValid( cpoint ) )
+ shieldWallFX.e.cpoint = cpoint
+}
+
+void function StopShieldWallFX( entity vortexSphere )
+{
+ entity shieldWallFX = vortexSphere.e.shieldWallFX
+ vortexSphere.e.shieldWallFX = null
+
+ if ( !IsValid( shieldWallFX ) )
+ return
+
+ shieldWallFX.Fire( "StopPlayEndCap" )
+ shieldWallFX.Fire( "Kill", "", 1.0 )
+
+ if ( IsValid( shieldWallFX.e.cpoint ) )
+ shieldWallFX.e.cpoint.Fire( "Kill", "", 1.0 )
+ EffectStop( shieldWallFX )
+}
+
+void function StopShieldWallFXOverTime( entity vortexSphere, float duration )
+{
+ entity shieldWallFX = vortexSphere.e.shieldWallFX
+ shieldWallFX.Fire( "StopPlayEndCap", "", duration )
+ shieldWallFX.Fire( "Kill", "", duration )
+ shieldWallFX.e.cpoint.Fire( "Kill", "", duration )
+}
+
+void function SetShieldWallCPointOrigin( entity shieldWallFX, vector AT_TURRET_SHIELD_COLOR )
+{
+ Assert( IsValid( shieldWallFX ) )
+ if ( !IsValid( shieldWallFX.e.cpoint ) )
+ return
+ shieldWallFX.e.cpoint.SetOrigin( AT_TURRET_SHIELD_COLOR )
+}
+
+entity function GetShieldWallFXCPoint( entity shieldWallFX )
+{
+ Assert( IsValid( shieldWallFX.e.cpoint ) )
+ return shieldWallFX.e.cpoint
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_team_emp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_team_emp.gnut
new file mode 100644
index 00000000..41d42848
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_team_emp.gnut
@@ -0,0 +1,38 @@
+global function TeamEMP_Init
+global function EMPEffects
+
+void function TeamEMP_Init()
+{
+ RegisterSignal( "PlayerEMPed" )
+}
+
+void function EMPEffects( entity player, float time )
+{
+ player.nv.empEndTime = Time() + time
+
+ player.Signal( "PlayerEMPed" )
+
+ // remember this is a stack so you need to enable as many times as you disable
+ DisableOffhandWeapons( player )
+
+ thread RecoverFromEMP( player, time )
+}
+
+void function RecoverFromEMP( entity player, float time )
+{
+ svGlobal.levelEnt.EndSignal( "BurnMeter_PreMatchEnter" )
+ player.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ {
+ // remember this is a stack so you need to enable as many times as you disable
+ EnableOffhandWeapons( player )
+ }
+ }
+ )
+
+ wait time + 0.1
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_vortex.nut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_vortex.nut
new file mode 100644
index 00000000..f1e46a53
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_vortex.nut
@@ -0,0 +1,1983 @@
+untyped
+
+global function Vortex_Init
+
+global function CreateVortexSphere
+global function DestroyVortexSphereFromVortexWeapon
+global function EnableVortexSphere
+#if SERVER
+global function ValidateVortexImpact
+global function TryVortexAbsorb
+global function SetVortexSphereBulletHitRules
+global function SetVortexSphereProjectileHitRules
+#endif
+global function VortexDrainedByImpact
+global function VortexPrimaryAttack
+global function GetVortexSphereCurrentColor
+global function GetShieldTriLerpColor
+global function IsVortexing
+#if SERVER
+global function Vortex_HandleElectricDamage
+global function VortexSphereDrainHealthForDamage
+global function Vortex_CreateImpactEventData
+global function Vortex_SpawnHeatShieldPingFX
+#endif
+
+global function Vortex_SetTagName
+global function Vortex_SetBulletCollectionOffset
+
+global function CodeCallback_OnVortexHitBullet
+global function CodeCallback_OnVortexHitProjectile
+
+const AMPED_WALL_IMPACT_FX = $"P_impact_xo_shield_cp"
+
+global const PROTO_AMPED_WALL = "proto_amped_wall"
+global const GUN_SHIELD_WALL = "gun_shield_wall"
+const PROX_MINE_MODEL = $"models/weapons/caber_shot/caber_shot_thrown.mdl"
+
+const VORTEX_SPHERE_COLOR_CHARGE_FULL = <115, 247, 255> // blue
+const VORTEX_SPHERE_COLOR_CHARGE_MED = <200, 128, 80> // orange
+const VORTEX_SPHERE_COLOR_CHARGE_EMPTY = <200, 80, 80> // red
+const VORTEX_SPHERE_COLOR_PAS_ION_VORTEX = <115, 174, 255> // blue
+const AMPED_DAMAGE_SCALAR = 1.5
+
+const VORTEX_SPHERE_COLOR_CROSSOVERFRAC_FULL2MED = 0.75 // from zero to this fraction, fade between full and medium charge colors
+const VORTEX_SPHERE_COLOR_CROSSOVERFRAC_MED2EMPTY = 0.95 // from "full2med" to this fraction, fade between medium and empty charge colors
+
+const VORTEX_BULLET_ABSORB_COUNT_MAX = 32
+const VORTEX_PROJECTILE_ABSORB_COUNT_MAX = 32
+
+const VORTEX_TIMED_EXPLOSIVE_FUSETIME = 2.75 // fuse time for absorbed projectiles
+const VORTEX_TIMED_EXPLOSIVE_FUSETIME_WARNINGFRAC = 0.75 // wait this fraction of the fuse time before warning the player it's about to explode
+
+const VORTEX_EXP_ROUNDS_RETURN_SPREAD_XY = 0.15
+const VORTEX_EXP_ROUNDS_RETURN_SPREAD_Z = 0.075
+
+const VORTEX_ELECTRIC_DAMAGE_CHARGE_DRAIN_MIN = 0.1 // fraction of charge time
+const VORTEX_ELECTRIC_DAMAGE_CHARGE_DRAIN_MAX = 0.3
+
+//The shotgun spams a lot of pellets that deal too much damage if they return full damage.
+const VORTEX_SHOTGUN_DAMAGE_RATIO = 0.25
+
+
+const SHIELD_WALL_BULLET_FX = $"P_impact_xo_shield_cp"
+const SHIELD_WALL_EXPMED_FX = $"P_impact_exp_med_xo_shield_CP"
+
+const SIGNAL_ID_BULLET_HIT_THINK = "signal_id_bullet_hit_think"
+
+const VORTEX_EXPLOSIVE_WARNING_SFX_LOOP = "Weapon_Vortex_Gun.ExplosiveWarningBeep"
+
+const VORTEX_PILOT_WEAPON_WEAKNESS_DAMAGESCALE = 6.0
+
+// These match the strings in the WeaponEd dropdown box for vortex_refire_behavior
+global const VORTEX_REFIRE_NONE = ""
+global const VORTEX_REFIRE_ABSORB = "absorb"
+global const VORTEX_REFIRE_BULLET = "bullet"
+global const VORTEX_REFIRE_EXPLOSIVE_ROUND = "explosive_round"
+global const VORTEX_REFIRE_ROCKET = "rocket"
+global const VORTEX_REFIRE_GRENADE = "grenade"
+global const VORTEX_REFIRE_GRENADE_LONG_FUSE = "grenade_long_fuse"
+
+const VortexIgnoreClassnames = {
+ ["mp_titancore_flame_wave"] = true,
+ ["mp_ability_grapple"] = true,
+ ["mp_ability_shifter"] = true,
+}
+
+table vortexImpactWeaponInfo
+
+const DEG_COS_60 = cos( 60 * DEG_TO_RAD )
+
+function Vortex_Init()
+{
+ PrecacheParticleSystem( SHIELD_WALL_BULLET_FX )
+ GetParticleSystemIndex( SHIELD_WALL_BULLET_FX )
+ PrecacheParticleSystem( SHIELD_WALL_EXPMED_FX )
+ GetParticleSystemIndex( SHIELD_WALL_EXPMED_FX )
+ PrecacheParticleSystem( AMPED_WALL_IMPACT_FX )
+ GetParticleSystemIndex( AMPED_WALL_IMPACT_FX )
+
+ RegisterSignal( SIGNAL_ID_BULLET_HIT_THINK )
+ RegisterSignal( "VortexStopping" )
+
+ RegisterSignal( "VortexAbsorbed" )
+ RegisterSignal( "VortexFired" )
+ RegisterSignal( "Script_OnDamaged" )
+}
+
+#if SERVER
+var function VortexBulletHitRules_Default( entity vortexSphere, var damageInfo )
+{
+ return damageInfo
+}
+
+bool function VortexProjectileHitRules_Default( entity vortexSphere, entity attacker, bool takesDamageByDefault )
+{
+ return takesDamageByDefault
+}
+
+void function SetVortexSphereBulletHitRules( entity vortexSphere, var functionref( entity, var ) customRules )
+{
+ vortexSphere.e.BulletHitRules = customRules
+}
+
+void function SetVortexSphereProjectileHitRules( entity vortexSphere, bool functionref( entity, entity, bool ) customRules )
+{
+ vortexSphere.e.ProjectileHitRules = customRules
+}
+#endif
+function CreateVortexSphere( entity vortexWeapon, bool useCylinderCheck, bool blockOwnerWeapon, int sphereRadius = 40, int bulletFOV = 180 )
+{
+ entity owner = vortexWeapon.GetWeaponOwner()
+ Assert( owner )
+
+ #if SERVER
+ //printt( "util ent:", vortexWeapon.GetWeaponUtilityEntity() )
+ Assert ( !vortexWeapon.GetWeaponUtilityEntity(), "Tried to create more than one vortex sphere on a vortex weapon!" )
+
+ entity vortexSphere = CreateEntity( "vortex_sphere" )
+ Assert( vortexSphere )
+
+ int spawnFlags = SF_ABSORB_BULLETS | SF_BLOCK_NPC_WEAPON_LOF
+
+ if ( useCylinderCheck )
+ {
+ spawnFlags = spawnFlags | SF_ABSORB_CYLINDER
+ vortexSphere.kv.height = sphereRadius * 2
+ }
+
+ if ( blockOwnerWeapon )
+ spawnFlags = spawnFlags | SF_BLOCK_OWNER_WEAPON
+
+ vortexSphere.kv.spawnflags = spawnFlags
+
+ vortexSphere.kv.enabled = 0
+ vortexSphere.kv.radius = sphereRadius
+ vortexSphere.kv.bullet_fov = bulletFOV
+ vortexSphere.kv.physics_pull_strength = 25
+ vortexSphere.kv.physics_side_dampening = 6
+ vortexSphere.kv.physics_fov = 360
+ vortexSphere.kv.physics_max_mass = 2
+ vortexSphere.kv.physics_max_size = 6
+ Assert( owner.IsNPC() || owner.IsPlayer(), "Vortex script expects the weapon owner to be a player or NPC." )
+
+ SetVortexSphereBulletHitRules( vortexSphere, VortexBulletHitRules_Default )
+ SetVortexSphereProjectileHitRules( vortexSphere, VortexProjectileHitRules_Default )
+
+ DispatchSpawn( vortexSphere )
+
+ vortexSphere.SetOwner( owner )
+
+ if ( owner.IsNPC() )
+ {
+ vortexSphere.SetParent( owner, "PROPGUN" )
+ vortexSphere.SetLocalOrigin( Vector( 0, 35, 0 ) )
+ }
+ else
+ {
+ vortexSphere.SetParent( owner )
+ vortexSphere.SetLocalOrigin( Vector( 0, 10, -30 ) )
+ }
+ vortexSphere.SetAbsAngles( Vector( 0, 0, 0 ) ) //Setting local angles on a parented object is not supported
+
+ vortexSphere.SetOwnerWeapon( vortexWeapon )
+ vortexWeapon.SetWeaponUtilityEntity( vortexSphere )
+ #endif
+
+ SetVortexAmmo( vortexWeapon, 0 )
+}
+
+
+function EnableVortexSphere( entity vortexWeapon )
+{
+ string tagname = GetVortexTagName( vortexWeapon )
+ entity weaponOwner = vortexWeapon.GetWeaponOwner()
+ local hasBurnMod = vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod )
+
+ #if SERVER
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ Assert( vortexSphere )
+ vortexSphere.FireNow( "Enable" )
+
+ thread SetPlayerUsingVortex( weaponOwner, vortexWeapon )
+
+ Vortex_CreateAbsorbFX_ControlPoints( vortexWeapon )
+
+ // world (3P) version of the vortex sphere FX
+ vortexSphere.s.worldFX <- CreateEntity( "info_particle_system" )
+
+ if ( hasBurnMod )
+ {
+ if ( "fxChargingControlPointBurn" in vortexWeapon.s )
+ vortexSphere.s.worldFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxChargingControlPointBurn ) )
+ }
+ else
+ {
+ if ( "fxChargingControlPoint" in vortexWeapon.s )
+ vortexSphere.s.worldFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxChargingControlPoint ) )
+ }
+
+ vortexSphere.s.worldFX.kv.start_active = 1
+ vortexSphere.s.worldFX.SetOwner( weaponOwner )
+ vortexSphere.s.worldFX.SetParent( vortexWeapon, tagname )
+ vortexSphere.s.worldFX.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // not owner only
+ vortexSphere.s.worldFX.kv.cpoint1 = vortexWeapon.s.vortexSphereColorCP.GetTargetName()
+ vortexSphere.s.worldFX.SetStopType( "destroyImmediately" )
+
+ DispatchSpawn( vortexSphere.s.worldFX )
+ #endif
+
+ SetVortexAmmo( vortexWeapon, 0 )
+
+ #if CLIENT
+ if ( IsLocalViewPlayer( weaponOwner ) )
+ {
+ local fxAlias = null
+
+ if ( hasBurnMod )
+ {
+ if ( "fxChargingFPControlPointBurn" in vortexWeapon.s )
+ fxAlias = vortexWeapon.s.fxChargingFPControlPointBurn
+ }
+ else
+ {
+ if ( "fxChargingFPControlPoint" in vortexWeapon.s )
+ fxAlias = vortexWeapon.s.fxChargingFPControlPoint
+ }
+
+ if ( fxAlias )
+ {
+ int sphereClientFXHandle = vortexWeapon.PlayWeaponEffectReturnViewEffectHandle( fxAlias, $"", tagname )
+ thread VortexSphereColorUpdate( vortexWeapon, sphereClientFXHandle )
+ }
+ }
+ #elseif SERVER
+ asset fxAlias = $""
+
+ if ( hasBurnMod )
+ {
+ if ( "fxChargingFPControlPointReplayBurn" in vortexWeapon.s )
+ fxAlias = expect asset( vortexWeapon.s.fxChargingFPControlPointReplayBurn )
+ }
+ else
+ {
+ if ( "fxChargingFPControlPointReplay" in vortexWeapon.s )
+ fxAlias = expect asset( vortexWeapon.s.fxChargingFPControlPointReplay )
+ }
+
+ if ( fxAlias != $"" )
+ vortexWeapon.PlayWeaponEffect( fxAlias, $"", tagname )
+
+ thread VortexSphereColorUpdate( vortexWeapon )
+ #endif
+}
+
+
+function DestroyVortexSphereFromVortexWeapon( entity vortexWeapon )
+{
+ DisableVortexSphereFromVortexWeapon( vortexWeapon )
+
+ #if SERVER
+ DestroyVortexSphere( vortexWeapon.GetWeaponUtilityEntity() )
+ vortexWeapon.SetWeaponUtilityEntity( null )
+ #endif
+}
+
+void function DestroyVortexSphere( entity vortexSphere )
+{
+ if ( IsValid( vortexSphere ) )
+ {
+ vortexSphere.s.worldFX.Destroy()
+ vortexSphere.Destroy()
+ }
+}
+
+
+function DisableVortexSphereFromVortexWeapon( entity vortexWeapon )
+{
+ vortexWeapon.Signal( "VortexStopping" )
+
+ // server cleanup
+ #if SERVER
+ DisableVortexSphere( vortexWeapon.GetWeaponUtilityEntity() )
+ Vortex_CleanupAllEffects( vortexWeapon )
+ Vortex_ClearImpactEventData( vortexWeapon )
+ #endif
+
+ // client & server cleanup
+
+ if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ {
+ if ( "fxChargingFPControlPointBurn" in vortexWeapon.s )
+ vortexWeapon.StopWeaponEffect( expect asset( vortexWeapon.s.fxChargingFPControlPointBurn ), $"" )
+ if ( "fxChargingFPControlPointReplayBurn" in vortexWeapon.s )
+ vortexWeapon.StopWeaponEffect( expect asset( vortexWeapon.s.fxChargingFPControlPointReplayBurn ), $"" )
+ }
+ else
+ {
+ if ( "fxChargingFPControlPoint" in vortexWeapon.s )
+ vortexWeapon.StopWeaponEffect( expect asset( vortexWeapon.s.fxChargingFPControlPoint ), $"" )
+ if ( "fxChargingFPControlPointReplay" in vortexWeapon.s )
+ vortexWeapon.StopWeaponEffect( expect asset( vortexWeapon.s.fxChargingFPControlPointReplay ), $"" )
+ }
+}
+
+void function DisableVortexSphere( entity vortexSphere )
+{
+ if ( IsValid( vortexSphere ) )
+ {
+ vortexSphere.FireNow( "Disable" )
+ vortexSphere.Signal( SIGNAL_ID_BULLET_HIT_THINK )
+ }
+
+}
+
+
+#if SERVER
+function Vortex_CreateAbsorbFX_ControlPoints( entity vortexWeapon )
+{
+ entity player = vortexWeapon.GetWeaponOwner()
+ Assert( player )
+
+ // vortex swirling incoming rounds FX location control point
+ if ( !( "vortexBulletEffectCP" in vortexWeapon.s ) )
+ vortexWeapon.s.vortexBulletEffectCP <- null
+ vortexWeapon.s.vortexBulletEffectCP = CreateEntity( "info_placement_helper" )
+ SetTargetName( expect entity( vortexWeapon.s.vortexBulletEffectCP ), UniqueString( "vortexBulletEffectCP" ) )
+ vortexWeapon.s.vortexBulletEffectCP.kv.start_active = 1
+
+ DispatchSpawn( vortexWeapon.s.vortexBulletEffectCP )
+
+ vector offset = GetBulletCollectionOffset( vortexWeapon )
+ vector origin = player.OffsetPositionFromView( player.EyePosition(), offset )
+
+ vortexWeapon.s.vortexBulletEffectCP.SetOrigin( origin )
+ vortexWeapon.s.vortexBulletEffectCP.SetParent( player )
+
+ // vortex sphere color control point
+ if ( !( "vortexSphereColorCP" in vortexWeapon.s ) )
+ vortexWeapon.s.vortexSphereColorCP <- null
+ vortexWeapon.s.vortexSphereColorCP = CreateEntity( "info_placement_helper" )
+ SetTargetName( expect entity( vortexWeapon.s.vortexSphereColorCP ), UniqueString( "vortexSphereColorCP" ) )
+ vortexWeapon.s.vortexSphereColorCP.kv.start_active = 1
+
+ DispatchSpawn( vortexWeapon.s.vortexSphereColorCP )
+}
+
+
+function Vortex_CleanupAllEffects( entity vortexWeapon )
+{
+ Assert( IsServer() )
+
+ Vortex_CleanupImpactAbsorbFX( vortexWeapon )
+
+ if ( ( "vortexBulletEffectCP" in vortexWeapon.s ) && IsValid_ThisFrame( expect entity( vortexWeapon.s.vortexBulletEffectCP ) ) )
+ vortexWeapon.s.vortexBulletEffectCP.Destroy()
+
+ if ( ( "vortexSphereColorCP" in vortexWeapon.s ) && IsValid_ThisFrame( expect entity( vortexWeapon.s.vortexSphereColorCP ) ) )
+ vortexWeapon.s.vortexSphereColorCP.Destroy()
+}
+#endif // SERVER
+
+
+function SetPlayerUsingVortex( entity weaponOwner, entity vortexWeapon )
+{
+ weaponOwner.EndSignal( "OnDeath" )
+
+ weaponOwner.s.isVortexing <- true
+
+ vortexWeapon.WaitSignal( "VortexStopping" )
+
+ OnThreadEnd
+ (
+ function() : ( weaponOwner )
+ {
+ if ( IsValid_ThisFrame( weaponOwner ) && "isVortexing" in weaponOwner.s )
+ {
+ delete weaponOwner.s.isVortexing
+ }
+ }
+ )
+}
+
+
+function IsVortexing( entity ent )
+{
+ Assert( IsServer() )
+
+ if ( "isVortexing" in ent.s )
+ return true
+}
+
+
+#if SERVER
+function Vortex_HandleElectricDamage( entity ent, entity attacker, damage, entity weapon )
+{
+ if ( !IsValid( ent ) )
+ return damage
+
+ if ( !ent.IsTitan() )
+ return damage
+
+ if ( !ent.IsPlayer() && !ent.IsNPC() )
+ return damage
+
+ if ( !IsVortexing( ent ) )
+ return damage
+
+ entity vortexWeapon = ent.GetActiveWeapon()
+ if ( !IsValid( vortexWeapon ) )
+ return damage
+
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !IsValid( vortexSphere ) )
+ return damage
+
+ if ( !IsValid( vortexWeapon ) || !IsValid( vortexSphere ) )
+ return damage
+
+ // vortex FOV check
+ //printt( "sphere FOV:", vortexSphere.kv.bullet_fov )
+ local sphereFOV = vortexSphere.kv.bullet_fov.tointeger()
+ entity attackerWeapon = attacker.GetActiveWeapon()
+ int attachIdx = attackerWeapon.LookupAttachment( "muzzle_flash" )
+ vector beamOrg = attackerWeapon.GetAttachmentOrigin( attachIdx )
+ vector firingDir = beamOrg - vortexSphere.GetOrigin()
+ firingDir = Normalize( firingDir )
+ vector vortexDir = AnglesToForward( vortexSphere.GetAngles() )
+
+ float dot = DotProduct( vortexDir, firingDir )
+
+ float degCos = DEG_COS_60
+ if ( sphereFOV != 120 )
+ deg_cos( sphereFOV * 0.5 )
+
+ // not in the vortex cone
+ if ( dot < degCos )
+ return damage
+
+ if ( "fxElectricalExplosion" in vortexWeapon.s )
+ {
+ entity fxRef = CreateEntity( "info_particle_system" )
+ fxRef.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxElectricalExplosion ) )
+ fxRef.kv.start_active = 1
+ fxRef.SetStopType( "destroyImmediately" )
+ //fxRef.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER // HACK this turns on owner only visibility. Uncomment when we hook up dedicated 3P effects
+ fxRef.SetOwner( ent )
+ fxRef.SetOrigin( vortexSphere.GetOrigin() )
+ fxRef.SetParent( ent )
+
+ DispatchSpawn( fxRef )
+ fxRef.Kill_Deprecated_UseDestroyInstead( 1 )
+ }
+
+ return 0
+}
+
+// this function handles all incoming vortex impact events
+bool function TryVortexAbsorb( entity vortexSphere, entity attacker, vector origin, int damageSourceID, entity weapon, string weaponName, string impactType, entity projectile = null, damageType = null, reflect = false )
+{
+ if ( weaponName in VortexIgnoreClassnames )
+ return false
+
+ entity vortexWeapon = vortexSphere.GetOwnerWeapon()
+ entity owner = vortexWeapon.GetWeaponOwner()
+
+ // keep cycling the oldest hitscan bullets out
+ if( !reflect )
+ {
+ if ( impactType == "hitscan" )
+ Vortex_ClampAbsorbedBulletCount( vortexWeapon )
+ else
+ Vortex_ClampAbsorbedProjectileCount( vortexWeapon )
+ }
+
+ // vortex spheres tag refired projectiles with info about the original projectile for accurate duplication when re-absorbed
+ if ( projectile )
+ {
+
+ // specifically for tether, since it gets moved to the vortex area and can get absorbed in the process, then destroyed
+ if ( !IsValid( projectile ) )
+ return false
+
+ entity projOwner = projectile.GetOwner()
+ if ( IsValid( projOwner ) && projOwner.GetTeam() == owner.GetTeam() )
+ return false
+
+ if ( projectile.proj.hasBouncedOffVortex )
+ return false
+
+ if ( projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_ignores_vortex" ) == "fall_vortex" )
+ {
+ vector velocity = projectile.GetVelocity()
+ vector multiplier = < -0.25, -0.25, -0.25 >
+ velocity = < velocity.x * multiplier.x, velocity.y * multiplier.y, velocity.z * multiplier.z >
+ projectile.SetVelocity( velocity )
+ projectile.proj.hasBouncedOffVortex = true
+ return false
+ }
+
+ // if ( projectile.GetParent() == owner )
+ // return false
+
+ if ( "originalDamageSource" in projectile.s )
+ {
+ damageSourceID = expect int( projectile.s.originalDamageSource )
+
+ // Vortex Volley Achievement
+ if ( IsValid( owner ) && owner.IsPlayer() )
+ {
+ //if ( PlayerProgressionAllowed( owner ) )
+ // SetAchievement( owner, "ach_vortexVolley", true )
+ }
+ }
+
+ // Max projectile stat tracking
+ int projectilesInVortex = 1
+ projectilesInVortex += vortexWeapon.w.vortexImpactData.len()
+
+ if ( IsValid( owner ) && owner.IsPlayer() )
+ {
+ if ( PlayerProgressionAllowed( owner ) )
+ {
+ int record = owner.GetPersistentVarAsInt( "mostProjectilesCollectedInVortex" )
+ if ( projectilesInVortex > record )
+ owner.SetPersistentVar( "mostProjectilesCollectedInVortex", projectilesInVortex )
+ }
+
+ var impact_sound_1p = projectile.ProjectileGetWeaponInfoFileKeyField( "vortex_impact_sound_1p" )
+ if ( impact_sound_1p != null )
+ EmitSoundOnEntityOnlyToPlayer( vortexSphere, owner, impact_sound_1p )
+ }
+
+ var impact_sound_3p = projectile.ProjectileGetWeaponInfoFileKeyField( "vortex_impact_sound_3p" )
+ if ( impact_sound_3p != null )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, impact_sound_3p )
+ }
+ else
+ {
+ if ( IsValid( owner ) && owner.IsPlayer() )
+ {
+ var impact_sound_1p = GetWeaponInfoFileKeyField_Global( weaponName, "vortex_impact_sound_1p" )
+ if ( impact_sound_1p != null )
+ EmitSoundOnEntityOnlyToPlayer( vortexSphere, owner, impact_sound_1p )
+ }
+
+ var impact_sound_3p = GetWeaponInfoFileKeyField_Global( weaponName, "vortex_impact_sound_3p" )
+ if ( impact_sound_3p != null )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, impact_sound_3p )
+ }
+
+ local impactData = Vortex_CreateImpactEventData( vortexWeapon, attacker, origin, damageSourceID, weaponName, impactType )
+
+ VortexDrainedByImpact( vortexWeapon, weapon, projectile, damageType )
+ Vortex_NotifyAttackerDidDamage( expect entity( impactData.attacker ), owner, impactData.origin )
+
+ if ( impactData.refireBehavior == VORTEX_REFIRE_ABSORB )
+ return true
+
+ if ( vortexWeapon.GetWeaponClassName() == "mp_titanweapon_heat_shield" )
+ return true
+
+ if ( !Vortex_ScriptCanHandleImpactEvent( impactData ) )
+ return false
+
+ Vortex_StoreImpactEvent( vortexWeapon, impactData )
+
+ VortexImpact_PlayAbsorbedFX( vortexWeapon, impactData )
+
+ if ( impactType == "hitscan" )
+ vortexSphere.AddBulletToSphere();
+ else
+ vortexSphere.AddProjectileToSphere();
+
+ local maxShotgunPelletsToIgnore = VORTEX_BULLET_ABSORB_COUNT_MAX * ( 1 - VORTEX_SHOTGUN_DAMAGE_RATIO )
+ if ( IsPilotShotgunWeapon( weaponName ) && ( vortexWeapon.s.shotgunPelletsToIgnore + 1 ) < maxShotgunPelletsToIgnore )
+ vortexWeapon.s.shotgunPelletsToIgnore += ( 1 - VORTEX_SHOTGUN_DAMAGE_RATIO )
+
+ if ( reflect )
+ {
+ local attackParams = {}
+ attackParams.pos <- owner.EyePosition()
+ attackParams.dir <- owner.GetPlayerOrNPCViewVector()
+
+ int bulletsFired = VortexReflectAttack( vortexWeapon, attackParams, expect vector( impactData.origin ) )
+
+ Vortex_CleanupImpactAbsorbFX( vortexWeapon )
+ Vortex_ClearImpactEventData( vortexWeapon )
+
+ while ( vortexSphere.GetBulletAbsorbedCount() > 0 )
+ vortexSphere.RemoveBulletFromSphere();
+
+ while ( vortexSphere.GetProjectileAbsorbedCount() > 0 )
+ vortexSphere.RemoveProjectileFromSphere();
+ }
+
+ return true
+}
+#endif // SERVER
+
+function VortexDrainedByImpact( entity vortexWeapon, entity weapon, entity projectile, damageType )
+{
+ if ( vortexWeapon.HasMod( "unlimited_charge_time" ) )
+ return
+ if ( vortexWeapon.HasMod( "vortex_extended_effect_and_no_use_penalty" ) )
+ return
+
+ float amount
+ if ( projectile )
+ amount = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.vortex_drain )
+ else
+ amount = weapon.GetWeaponSettingFloat( eWeaponVar.vortex_drain )
+
+ if ( amount <= 0.0 )
+ return
+
+ if ( vortexWeapon.GetWeaponClassName() == "mp_titanweapon_vortex_shield_ion" )
+ {
+ entity owner = vortexWeapon.GetWeaponOwner()
+ int totalEnergy = owner.GetSharedEnergyTotal()
+ owner.TakeSharedEnergy( int( float( totalEnergy ) * amount ) )
+ }
+ else
+ {
+ float frac = min ( vortexWeapon.GetWeaponChargeFraction() + amount, 1.0 )
+ vortexWeapon.SetWeaponChargeFraction( frac )
+ }
+}
+
+
+function VortexSlowOwnerFromAttacker( entity player, entity attacker, vector velocity, float multiplier )
+{
+ vector damageForward = player.GetOrigin() - attacker.GetOrigin()
+ damageForward.z = 0
+ damageForward.Norm()
+
+ vector velForward = player.GetVelocity()
+ velForward.z = 0
+ velForward.Norm()
+
+ float dot = DotProduct( velForward, damageForward )
+ if ( dot >= -0.5 )
+ return
+
+ dot += 0.5
+ dot *= -2.0
+
+ vector negateVelocity = velocity * -multiplier
+ negateVelocity *= dot
+
+ velocity += negateVelocity
+ player.SetVelocity( velocity )
+}
+
+
+#if SERVER
+function Vortex_ClampAbsorbedBulletCount( entity vortexWeapon )
+{
+ if ( GetBulletsAbsorbedCount( vortexWeapon ) >= ( VORTEX_BULLET_ABSORB_COUNT_MAX - 1 ) )
+ Vortex_RemoveOldestAbsorbedBullet( vortexWeapon )
+}
+
+function Vortex_ClampAbsorbedProjectileCount( entity vortexWeapon )
+{
+ if ( GetProjectilesAbsorbedCount( vortexWeapon ) >= ( VORTEX_PROJECTILE_ABSORB_COUNT_MAX - 1 ) )
+ Vortex_RemoveOldestAbsorbedProjectile( vortexWeapon )
+}
+
+function Vortex_RemoveOldestAbsorbedBullet( entity vortexWeapon )
+{
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+
+ local bulletImpacts = Vortex_GetHitscanBulletImpacts( vortexWeapon )
+ local impactDataToRemove = bulletImpacts[ 0 ] // since it's an array, the first one will be the oldest
+
+ Vortex_RemoveImpactEvent( vortexWeapon, impactDataToRemove )
+
+ vortexSphere.RemoveBulletFromSphere()
+}
+
+function Vortex_RemoveOldestAbsorbedProjectile( entity vortexWeapon )
+{
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+
+ local projImpacts = Vortex_GetProjectileImpacts( vortexWeapon )
+ local impactDataToRemove = projImpacts[ 0 ] // since it's an array, the first one will be the oldest
+
+ Vortex_RemoveImpactEvent( vortexWeapon, impactDataToRemove )
+
+ vortexSphere.RemoveProjectileFromSphere()
+}
+
+function Vortex_CreateImpactEventData( entity vortexWeapon, entity attacker, vector origin, int damageSourceID, string weaponName, string impactType )
+{
+ entity player = vortexWeapon.GetWeaponOwner()
+ local impactData = {}
+
+ impactData.attacker <- attacker
+ impactData.origin <- origin
+ impactData.damageSourceID <- damageSourceID
+ impactData.weaponName <- weaponName
+ impactData.impactType <- impactType
+
+ impactData.refireBehavior <- VORTEX_REFIRE_NONE
+ impactData.absorbSFX <- "Vortex_Shield_AbsorbBulletSmall"
+ impactData.absorbSFX_1p_vs_3p <- null
+
+ impactData.team <- null
+ // sets a team even if the attacker disconnected
+ if ( IsValid_ThisFrame( attacker ) )
+ {
+ impactData.team = attacker.GetTeam()
+ }
+ else
+ {
+ // default to opposite team
+ if ( player.GetTeam() == TEAM_IMC )
+ impactData.team = TEAM_MILITIA
+ else
+ impactData.team = TEAM_IMC
+ }
+
+ impactData.absorbFX <- null
+ impactData.absorbFX_3p <- null
+ impactData.fxEnt_absorb <- null
+
+ impactData.explosionradius <- null
+ impactData.explosion_damage <- null
+ impactData.impact_effect_table <- -1
+ // -- everything from here down relies on being able to read a megaweapon file
+ if ( !( impactData.weaponName in vortexImpactWeaponInfo ) )
+ {
+ vortexImpactWeaponInfo[ impactData.weaponName ] <- {}
+ vortexImpactWeaponInfo[ impactData.weaponName ].absorbFX <- GetWeaponInfoFileKeyFieldAsset_Global( impactData.weaponName, "vortex_absorb_effect" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].absorbFX_3p <- GetWeaponInfoFileKeyFieldAsset_Global( impactData.weaponName, "vortex_absorb_effect_third_person" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].refireBehavior <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "vortex_refire_behavior" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].absorbSound <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "vortex_absorb_sound" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].absorbSound_1p_vs_3p <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "vortex_absorb_sound_1p_vs_3p" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].explosionradius <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "explosionradius" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].explosion_damage_heavy_armor <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "explosion_damage_heavy_armor" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].explosion_damage <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "explosion_damage" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].impact_effect_table <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "impact_effect_table" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].grenade_ignition_time <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "grenade_ignition_time" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].grenade_fuse_time <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "grenade_fuse_time" )
+ }
+
+ impactData.absorbFX = vortexImpactWeaponInfo[ impactData.weaponName ].absorbFX
+ impactData.absorbFX_3p = vortexImpactWeaponInfo[ impactData.weaponName ].absorbFX_3p
+ if ( impactData.absorbFX )
+ Assert( impactData.absorbFX_3p, "Missing 3rd person absorb effect for " + impactData.weaponName )
+ impactData.refireBehavior = vortexImpactWeaponInfo[ impactData.weaponName ].refireBehavior
+
+ local absorbSound = vortexImpactWeaponInfo[ impactData.weaponName ].absorbSound
+ if ( absorbSound )
+ impactData.absorbSFX = absorbSound
+
+ local absorbSound_1p_vs_3p = vortexImpactWeaponInfo[ impactData.weaponName ].absorbSound_1p_vs_3p
+ if ( absorbSound_1p_vs_3p )
+ impactData.absorbSFX_1p_vs_3p = absorbSound_1p_vs_3p
+
+ // info we need for refiring (some types of) impacts
+ impactData.explosionradius = vortexImpactWeaponInfo[ impactData.weaponName ].explosionradius
+ impactData.explosion_damage = vortexImpactWeaponInfo[ impactData.weaponName ].explosion_damage_heavy_armor
+ if ( impactData.explosion_damage == null )
+ impactData.explosion_damage = vortexImpactWeaponInfo[ impactData.weaponName ].explosion_damage
+ impactData.impact_effect_table = vortexImpactWeaponInfo[ impactData.weaponName ].impact_effect_table
+
+ return impactData
+}
+
+function Vortex_ScriptCanHandleImpactEvent( impactData )
+{
+ if ( impactData.refireBehavior == VORTEX_REFIRE_NONE )
+ return false
+
+ if ( !impactData.absorbFX )
+ return false
+
+ if ( impactData.impactType == "projectile" && !impactData.impact_effect_table )
+ return false
+
+ return true
+}
+
+function Vortex_StoreImpactEvent( entity vortexWeapon, impactData )
+{
+ vortexWeapon.w.vortexImpactData.append( impactData )
+}
+
+// safely removes data for a single impact event
+function Vortex_RemoveImpactEvent( entity vortexWeapon, impactData )
+{
+ Vortex_ImpactData_KillAbsorbFX( impactData )
+
+ vortexWeapon.w.vortexImpactData.fastremovebyvalue( impactData )
+}
+
+function Vortex_GetAllImpactEvents( entity vortexWeapon )
+{
+ return vortexWeapon.w.vortexImpactData
+}
+
+function Vortex_ClearImpactEventData( entity vortexWeapon )
+{
+ vortexWeapon.w.vortexImpactData = []
+}
+
+function VortexImpact_PlayAbsorbedFX( entity vortexWeapon, impactData )
+{
+ // generic shield ping FX
+ Vortex_SpawnShieldPingFX( vortexWeapon, impactData )
+
+ // specific absorb FX
+ impactData.fxEnt_absorb = Vortex_SpawnImpactAbsorbFX( vortexWeapon, impactData )
+}
+
+// FX played when something first enters the vortex sphere
+function Vortex_SpawnShieldPingFX( entity vortexWeapon, impactData )
+{
+ entity player = vortexWeapon.GetWeaponOwner()
+ Assert( player )
+
+ local absorbSFX = impactData.absorbSFX
+ //printt( "SFX absorb sound:", absorbSFX )
+ if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ EmitSoundOnEntity( vortexWeapon, "Vortex_Shield_Deflect_Amped" )
+ else
+ {
+ EmitSoundOnEntity( vortexWeapon, absorbSFX )
+ if ( impactData.absorbSFX_1p_vs_3p != null )
+ {
+ if ( IsValid( impactData.attacker ) && impactData.attacker.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( vortexWeapon, impactData.attacker, impactData.absorbSFX_1p_vs_3p )
+ }
+ }
+ }
+
+ entity pingFX = CreateEntity( "info_particle_system" )
+
+ if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ {
+ if ( "fxBulletHitBurn" in vortexWeapon.s )
+ pingFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxBulletHitBurn ) )
+ }
+ else
+ {
+ if ( "fxBulletHit" in vortexWeapon.s )
+ pingFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxBulletHit ) )
+ }
+
+ pingFX.kv.start_active = 1
+
+ DispatchSpawn( pingFX )
+
+ pingFX.SetOrigin( impactData.origin )
+ pingFX.SetParent( player )
+ pingFX.Kill_Deprecated_UseDestroyInstead( 0.25 )
+}
+
+function Vortex_SpawnHeatShieldPingFX( entity vortexWeapon, impactData, bool impactTypeIsBullet )
+{
+ entity player = vortexWeapon.GetWeaponOwner()
+ Assert( player )
+
+ if ( impactTypeIsBullet )
+ EmitSoundOnEntity( vortexWeapon, "heat_shield_stop_bullet" )
+ else
+ EmitSoundOnEntity( vortexWeapon, "heat_shield_stop_projectile" )
+
+ entity pingFX = CreateEntity( "info_particle_system" )
+
+ if ( "fxBulletHit" in vortexWeapon.s )
+ pingFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxBulletHit ) )
+
+ pingFX.kv.start_active = 1
+
+ DispatchSpawn( pingFX )
+
+ pingFX.SetOrigin( impactData.origin )
+ pingFX.SetParent( player )
+ pingFX.Kill_Deprecated_UseDestroyInstead( 0.25 )
+}
+
+function Vortex_SpawnImpactAbsorbFX( entity vortexWeapon, impactData )
+{
+ // in case we're in the middle of cleaning the weapon up
+ if ( !IsValid( vortexWeapon.s.vortexBulletEffectCP ) )
+ return
+
+ entity owner = vortexWeapon.GetWeaponOwner()
+ Assert( owner )
+
+ local fxRefs = []
+
+ // owner
+ {
+ entity fxRef = CreateEntity( "info_particle_system" )
+
+ fxRef.SetValueForEffectNameKey( expect asset( impactData.absorbFX ) )
+ fxRef.kv.start_active = 1
+ fxRef.SetStopType( "destroyImmediately" )
+ fxRef.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER
+ fxRef.kv.cpoint1 = vortexWeapon.s.vortexBulletEffectCP.GetTargetName()
+
+ DispatchSpawn( fxRef )
+
+ fxRef.SetOwner( owner )
+ fxRef.SetOrigin( impactData.origin )
+ fxRef.SetParent( owner )
+
+ fxRefs.append( fxRef )
+ }
+
+ // everyone else
+ {
+ entity fxRef = CreateEntity( "info_particle_system" )
+
+ fxRef.SetValueForEffectNameKey( expect asset( impactData.absorbFX_3p ) )
+ fxRef.kv.start_active = 1
+ fxRef.SetStopType( "destroyImmediately" )
+ fxRef.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // other only visibility
+ fxRef.kv.cpoint1 = vortexWeapon.s.vortexBulletEffectCP.GetTargetName()
+
+ DispatchSpawn( fxRef )
+
+ fxRef.SetOwner( owner )
+ fxRef.SetOrigin( impactData.origin )
+ fxRef.SetParent( owner )
+
+ fxRefs.append( fxRef )
+ }
+
+ return fxRefs
+}
+
+function Vortex_CleanupImpactAbsorbFX( entity vortexWeapon )
+{
+ foreach ( impactData in Vortex_GetAllImpactEvents( vortexWeapon ) )
+ {
+ Vortex_ImpactData_KillAbsorbFX( impactData )
+ }
+}
+
+function Vortex_ImpactData_KillAbsorbFX( impactData )
+{
+ foreach ( fxRef in impactData.fxEnt_absorb )
+ {
+ if ( !IsValid( fxRef ) )
+ continue
+
+ fxRef.Fire( "DestroyImmediately" )
+ fxRef.Kill_Deprecated_UseDestroyInstead()
+ }
+}
+
+bool function PlayerDiedOrDisconnected( entity player )
+{
+ if ( !IsValid( player ) )
+ return true
+
+ if ( !IsAlive( player ) )
+ return true
+
+ if ( IsDisconnected( player ) )
+ return true
+
+ return false
+}
+
+#endif // SERVER
+
+int function VortexPrimaryAttack( entity vortexWeapon, WeaponPrimaryAttackParams attackParams )
+{
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !vortexSphere )
+ return 0
+
+ #if SERVER
+ Assert( vortexSphere )
+ #endif
+
+ int totalfired = 0
+ int totalAttempts = 0
+
+ bool forceReleased = false
+ // in this case, it's also considered "force released" if the charge time runs out
+ if ( vortexWeapon.IsForceRelease() || vortexWeapon.GetWeaponChargeFraction() == 1 )
+ forceReleased = true
+
+ // PREDICTED REFIRES
+ // bullet impact events don't individually fire back per event because we aggregate and then shotgun blast them
+ int bulletsFired = Vortex_FireBackBullets( vortexWeapon, attackParams )
+ totalfired += bulletsFired
+
+ // UNPREDICTED REFIRES
+ #if SERVER
+ //printt( "server: force released?", forceReleased )
+
+ local unpredictedRefires = Vortex_GetProjectileImpacts( vortexWeapon )
+
+ // HACK we don't actually want to refire them with a spiral but
+ // this is to temporarily ensure compatibility with the Titan rocket launcher
+ if ( !( "spiralMissileIdx" in vortexWeapon.s ) )
+ vortexWeapon.s.spiralMissileIdx <- null
+ vortexWeapon.s.spiralMissileIdx = 0
+
+ foreach ( impactData in unpredictedRefires )
+ {
+ table fakeAttackParams = {pos = attackParams.pos, dir = attackParams.dir, firstTimePredicted = attackParams.firstTimePredicted, burstIndex = attackParams.burstIndex}
+ bool didFire = DoVortexAttackForImpactData( vortexWeapon, fakeAttackParams, impactData, totalAttempts )
+ if ( didFire )
+ totalfired++
+ totalAttempts++
+ }
+ //printt( "totalfired", totalfired )
+ #else
+ totalfired += GetProjectilesAbsorbedCount( vortexWeapon )
+ #endif
+
+ SetVortexAmmo( vortexWeapon, 0 )
+
+ vortexWeapon.Signal( "VortexFired" )
+
+ if ( forceReleased )
+ DestroyVortexSphereFromVortexWeapon( vortexWeapon )
+ else
+ DisableVortexSphereFromVortexWeapon( vortexWeapon )
+
+ return totalfired
+}
+
+int function Vortex_FireBackBullets( entity vortexWeapon, WeaponPrimaryAttackParams attackParams )
+{
+ int bulletCount = GetBulletsAbsorbedCount( vortexWeapon )
+ //Defensive Check - Couldn't repro error.
+ if ( "shotgunPelletsToIgnore" in vortexWeapon.s )
+ bulletCount = int( ceil( bulletCount - vortexWeapon.s.shotgunPelletsToIgnore ) )
+
+ if ( bulletCount )
+ {
+ bulletCount = minint( bulletCount, MAX_BULLET_PER_SHOT )
+
+ //if ( IsClient() && GetLocalViewPlayer() == vortexWeapon.GetWeaponOwner() )
+ // printt( "vortex firing", bulletCount, "bullets" )
+
+ float radius = LOUD_WEAPON_AI_SOUND_RADIUS_MP;
+ vortexWeapon.EmitWeaponNpcSound( radius, 0.2 )
+ int damageType = damageTypes.shotgun | DF_VORTEX_REFIRE
+ if ( bulletCount == 1 )
+ vortexWeapon.FireWeaponBullet( attackParams.pos, attackParams.dir, bulletCount, damageType )
+ else
+ ShotgunBlast( vortexWeapon, attackParams.pos, attackParams.dir, bulletCount, damageType )
+ }
+
+ return bulletCount
+}
+
+#if SERVER
+bool function Vortex_FireBackExplosiveRound( vortexWeapon, attackParams, impactData, sequenceID )
+{
+ expect entity( vortexWeapon )
+
+ // common projectile data
+ float projSpeed = 8000.0
+ int damageType = damageTypes.explosive | DF_VORTEX_REFIRE
+
+ vortexWeapon.EmitWeaponSound( "Weapon.Explosion_Med" )
+
+ vector attackPos
+ //Requires code feature to properly fire tracers from offset positions.
+ //if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ // attackPos = impactData.origin
+ //else
+ attackPos = Vortex_GenerateRandomRefireOrigin( vortexWeapon )
+
+ vector fireVec = Vortex_GenerateRandomRefireVector( vortexWeapon, VORTEX_EXP_ROUNDS_RETURN_SPREAD_XY, VORTEX_EXP_ROUNDS_RETURN_SPREAD_Z )
+
+ // fire off the bolt
+ entity bolt = vortexWeapon.FireWeaponBolt( attackPos, fireVec, projSpeed, damageType, damageType, PROJECTILE_NOT_PREDICTED, sequenceID )
+ if ( bolt )
+ {
+ bolt.kv.gravity = 0.3
+
+ Vortex_ProjectileCommonSetup( bolt, impactData )
+ }
+
+ return true
+}
+
+bool function Vortex_FireBackProjectileBullet( vortexWeapon, attackParams, impactData, sequenceID )
+{
+ expect entity( vortexWeapon )
+
+ // common projectile data
+ float projSpeed = 12000.0
+ int damageType = damageTypes.bullet | DF_VORTEX_REFIRE
+
+ vortexWeapon.EmitWeaponSound( "Weapon.Explosion_Med" )
+
+ vector attackPos
+ //Requires code feature to properly fire tracers from offset positions.
+ //if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ // attackPos = impactData.origin
+ //else
+ attackPos = Vortex_GenerateRandomRefireOrigin( vortexWeapon )
+
+ vector fireVec = Vortex_GenerateRandomRefireVector( vortexWeapon, 0.15, 0.1 )
+ //printt( Time(), fireVec ) // print for bug with random
+
+ // fire off the bolt
+ entity bolt = vortexWeapon.FireWeaponBolt( attackPos, fireVec, projSpeed, damageType, damageType, PROJECTILE_NOT_PREDICTED, sequenceID )
+ if ( bolt )
+ {
+ bolt.kv.gravity = 0.0
+
+ Vortex_ProjectileCommonSetup( bolt, impactData )
+ }
+
+ return true
+}
+
+vector function Vortex_GenerateRandomRefireOrigin( entity vortexWeapon, float distFromCenter = 3.0 )
+{
+ float distFromCenter_neg = distFromCenter * -1
+
+ vector attackPos = expect vector( vortexWeapon.s.vortexBulletEffectCP.GetOrigin() )
+
+ float x = RandomFloatRange( distFromCenter_neg, distFromCenter )
+ float y = RandomFloatRange( distFromCenter_neg, distFromCenter )
+ float z = RandomFloatRange( distFromCenter_neg, distFromCenter )
+
+ attackPos = attackPos + Vector( x, y, z )
+
+ return attackPos
+}
+
+vector function Vortex_GenerateRandomRefireVector( entity vortexWeapon, float vecSpread, float vecSpreadZ )
+{
+ float x = RandomFloatRange( vecSpread * -1, vecSpread )
+ float y = RandomFloatRange( vecSpread * -1, vecSpread )
+ float z = RandomFloatRange( vecSpreadZ * -1, vecSpreadZ )
+
+ vector fireVec = vortexWeapon.GetWeaponOwner().GetPlayerOrNPCViewVector() + Vector( x, y, z )
+ return fireVec
+}
+
+bool function Vortex_FireBackRocket( vortexWeapon, attackParams, impactData, sequenceID )
+{
+ expect entity( vortexWeapon )
+
+ // TODO prediction for clients
+ Assert( IsServer() )
+
+ entity rocket = vortexWeapon.FireWeaponMissile( attackParams.pos, attackParams.dir, 1800.0, damageTypes.largeCaliberExp | DF_VORTEX_REFIRE, damageTypes.largeCaliberExp | DF_VORTEX_REFIRE, false, PROJECTILE_NOT_PREDICTED )
+
+ if ( rocket )
+ {
+ rocket.kv.lifetime = RandomFloatRange( 2.6, 3.5 )
+
+ InitMissileForRandomDriftForVortexLow( rocket, expect vector( attackParams.pos ), expect vector( attackParams.dir ) )
+
+ Vortex_ProjectileCommonSetup( rocket, impactData )
+ }
+
+ return true
+}
+
+bool function Vortex_FireBackGrenade( entity vortexWeapon, attackParams, impactData, int attackSeedCount, float baseFuseTime )
+{
+ float x = RandomFloatRange( -0.2, 0.2 )
+ float y = RandomFloatRange( -0.2, 0.2 )
+ float z = RandomFloatRange( -0.2, 0.2 )
+
+ vector velocity = ( expect vector( attackParams.dir ) + Vector( x, y, z ) ) * 1500
+ vector angularVelocity = Vector( RandomFloatRange( -1200, 1200 ), 100, 0 )
+
+ bool hasIgnitionTime = vortexImpactWeaponInfo[ impactData.weaponName ].grenade_ignition_time > 0
+ float fuseTime = hasIgnitionTime ? 0.0 : baseFuseTime
+ const int HARDCODED_DAMAGE_TYPE = (damageTypes.explosive | DF_VORTEX_REFIRE)
+
+ entity grenade = vortexWeapon.FireWeaponGrenade( attackParams.pos, velocity, angularVelocity, fuseTime, HARDCODED_DAMAGE_TYPE, HARDCODED_DAMAGE_TYPE, PROJECTILE_NOT_PREDICTED, true, true )
+ if ( grenade )
+ {
+ Grenade_Init( grenade, vortexWeapon )
+ Vortex_ProjectileCommonSetup( grenade, impactData )
+ if ( hasIgnitionTime )
+ grenade.SetGrenadeIgnitionDuration( vortexImpactWeaponInfo[ impactData.weaponName ].grenade_ignition_time )
+ }
+
+ return (grenade ? true : false)
+}
+
+bool function DoVortexAttackForImpactData( entity vortexWeapon, attackParams, impactData, int attackSeedCount )
+{
+ bool didFire = false
+ switch ( impactData.refireBehavior )
+ {
+ case VORTEX_REFIRE_EXPLOSIVE_ROUND:
+ didFire = Vortex_FireBackExplosiveRound( vortexWeapon, attackParams, impactData, attackSeedCount )
+ break
+
+ case VORTEX_REFIRE_ROCKET:
+ didFire = Vortex_FireBackRocket( vortexWeapon, attackParams, impactData, attackSeedCount )
+ break
+
+ case VORTEX_REFIRE_GRENADE:
+ didFire = Vortex_FireBackGrenade( vortexWeapon, attackParams, impactData, attackSeedCount, 1.25 )
+ break
+
+ case VORTEX_REFIRE_GRENADE_LONG_FUSE:
+ didFire = Vortex_FireBackGrenade( vortexWeapon, attackParams, impactData, attackSeedCount, 10.0 )
+ break
+
+ case VORTEX_REFIRE_BULLET:
+ didFire = Vortex_FireBackProjectileBullet( vortexWeapon, attackParams, impactData, attackSeedCount )
+ break
+
+ case VORTEX_REFIRE_NONE:
+ break
+ }
+
+ return didFire
+}
+
+function Vortex_ProjectileCommonSetup( entity projectile, impactData )
+{
+ // custom tag it so it shows up correctly if it hits another vortex sphere
+ projectile.s.originalDamageSource <- impactData.damageSourceID
+
+ Vortex_SetImpactEffectTable_OnProjectile( projectile, impactData ) // set the correct impact effect table
+
+ projectile.SetVortexRefired( true ) // This tells code the projectile was refired from the vortex so that it uses "projectile_vortex_vscript"
+ projectile.SetModel( GetWeaponInfoFileKeyFieldAsset_Global( impactData.weaponName, "projectilemodel" ) )
+ projectile.SetWeaponClassName( impactData.weaponName ) // causes the projectile to use its normal trail FX
+
+ projectile.ProjectileSetDamageSourceID( impactData.damageSourceID ) // obit will show the owner weapon
+}
+
+// gives a refired projectile the correct impact effect table
+function Vortex_SetImpactEffectTable_OnProjectile( projectile, impactData )
+{
+ //Getting more info for bug 207595, don't check into Staging.
+ #if DEV
+ printt( "impactData.impact_effect_table ", impactData.impact_effect_table )
+ if ( impactData.impact_effect_table == "" )
+ PrintTable( impactData )
+ #endif
+
+ local fxTableHandle = GetImpactEffectTable( impactData.impact_effect_table )
+
+ projectile.SetImpactEffectTable( fxTableHandle )
+}
+#endif // SERVER
+
+// absorbed bullets are tracked with a special networked kv variable because clients need to know how many bullets to fire as well, when they are doing the client version of FireWeaponBullet
+int function GetBulletsAbsorbedCount( entity vortexWeapon )
+{
+ if ( !vortexWeapon )
+ return 0
+
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !vortexSphere )
+ return 0
+
+ return vortexSphere.GetBulletAbsorbedCount()
+}
+
+int function GetProjectilesAbsorbedCount( entity vortexWeapon )
+{
+ if ( !vortexWeapon )
+ return 0
+
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !vortexSphere )
+ return 0
+
+ return vortexSphere.GetProjectileAbsorbedCount()
+}
+
+#if SERVER
+function Vortex_GetProjectileImpacts( entity vortexWeapon )
+{
+ local impacts = []
+ foreach ( impactData in Vortex_GetAllImpactEvents( vortexWeapon ) )
+ {
+ if ( impactData.impactType == "projectile" )
+ impacts.append( impactData )
+ }
+
+ return impacts
+}
+
+function Vortex_GetHitscanBulletImpacts( entity vortexWeapon )
+{
+ local impacts = []
+ foreach ( impactData in Vortex_GetAllImpactEvents( vortexWeapon ) )
+ {
+ if ( impactData.impactType == "hitscan" )
+ impacts.append( impactData )
+ }
+
+ return impacts
+}
+
+int function GetHitscanBulletImpactCount( entity vortexWeapon )
+{
+ int count = 0
+ foreach ( impactData in Vortex_GetAllImpactEvents( vortexWeapon ) )
+ {
+ if ( impactData.impactType == "hitscan" )
+ count++
+ }
+
+ return count
+}
+#endif // SERVER
+
+// // lets the damage callback communicate to the attacker that he hit a vortex shield
+function Vortex_NotifyAttackerDidDamage( entity attacker, entity vortexOwner, hitPos )
+{
+ if ( !IsValid( attacker ) || !attacker.IsPlayer() )
+ return
+
+ if ( !IsValid( vortexOwner ) )
+ return
+
+ Assert( hitPos )
+
+ attacker.NotifyDidDamage( vortexOwner, 0, hitPos, 0, 0, DAMAGEFLAG_VICTIM_HAS_VORTEX, 0, null, 0 )
+}
+
+function SetVortexAmmo( entity vortexWeapon, count )
+{
+ entity owner = vortexWeapon.GetWeaponOwner()
+ if ( !IsValid_ThisFrame( owner ) )
+ return
+ #if CLIENT
+ if ( !IsLocalViewPlayer( owner ) )
+ return
+ #endif
+
+ vortexWeapon.SetWeaponPrimaryAmmoCount( count )
+}
+
+
+// sets the RGB color value for the vortex sphere FX based on current charge fraction
+function VortexSphereColorUpdate( entity weapon, sphereClientFXHandle = null )
+{
+ weapon.EndSignal( "VortexStopping" )
+
+ #if CLIENT
+ Assert( sphereClientFXHandle != null )
+ #endif
+ bool isIonVortex = weapon.GetWeaponClassName() == "mp_titanweapon_vortex_shield_ion"
+ entity weaponOwner = weapon.GetWeaponOwner()
+ float energyTotal = float ( weaponOwner.GetSharedEnergyTotal() )
+ while( IsValid( weapon ) && IsValid( weaponOwner ) )
+ {
+ vector colorVec
+ if ( isIonVortex )
+ {
+ float energyFrac = 1.0 - float( weaponOwner.GetSharedEnergyCount() ) / energyTotal
+ if ( weapon.HasMod( "pas_ion_vortex" ) )
+ colorVec = GetVortexSphereCurrentColor( energyFrac, VORTEX_SPHERE_COLOR_PAS_ION_VORTEX )
+ else
+ colorVec = GetVortexSphereCurrentColor( energyFrac )
+ }
+ else
+ {
+ colorVec = GetVortexSphereCurrentColor( weapon.GetWeaponChargeFraction() )
+ }
+
+
+ // update the world entity that is linked to the world FX playing on the server
+ #if SERVER
+ weapon.s.vortexSphereColorCP.SetOrigin( colorVec )
+ #else
+ // handles the server killing the vortex sphere without the client knowing right away,
+ // for example if an explosive goes off and we short circuit the charge timer
+ if ( !EffectDoesExist( sphereClientFXHandle ) )
+ break
+
+ EffectSetControlPointVector( sphereClientFXHandle, 1, colorVec )
+ #endif
+
+ WaitFrame()
+ }
+}
+
+vector function GetVortexSphereCurrentColor( float chargeFrac, vector fullHealthColor = VORTEX_SPHERE_COLOR_CHARGE_FULL )
+{
+ return GetTriLerpColor( chargeFrac, fullHealthColor, VORTEX_SPHERE_COLOR_CHARGE_MED, VORTEX_SPHERE_COLOR_CHARGE_EMPTY )
+}
+
+vector function GetShieldTriLerpColor( float frac )
+{
+ return GetTriLerpColor( frac, VORTEX_SPHERE_COLOR_CHARGE_FULL, VORTEX_SPHERE_COLOR_CHARGE_MED, VORTEX_SPHERE_COLOR_CHARGE_EMPTY )
+}
+
+vector function GetTriLerpColor( float fraction, vector color1, vector color2, vector color3 )
+{
+ float crossover1 = VORTEX_SPHERE_COLOR_CROSSOVERFRAC_FULL2MED // from zero to this fraction, fade between color1 and color2
+ float crossover2 = VORTEX_SPHERE_COLOR_CROSSOVERFRAC_MED2EMPTY // from crossover1 to this fraction, fade between color2 and color3
+
+ float r, g, b
+
+ // 0 = full charge, 1 = no charge remaining
+ if ( fraction < crossover1 )
+ {
+ r = Graph( fraction, 0, crossover1, color1.x, color2.x )
+ g = Graph( fraction, 0, crossover1, color1.y, color2.y )
+ b = Graph( fraction, 0, crossover1, color1.z, color2.z )
+ return <r, g, b>
+ }
+ else if ( fraction < crossover2 )
+ {
+ r = Graph( fraction, crossover1, crossover2, color2.x, color3.x )
+ g = Graph( fraction, crossover1, crossover2, color2.y, color3.y )
+ b = Graph( fraction, crossover1, crossover2, color2.z, color3.z )
+ return <r, g, b>
+ }
+ else
+ {
+ // for the last bit of overload timer, keep it max danger color
+ r = color3.x
+ g = color3.y
+ b = color3.z
+ return <r, g, b>
+ }
+
+ unreachable
+}
+
+// generic impact validation
+#if SERVER
+bool function ValidateVortexImpact( entity vortexSphere, entity projectile = null )
+{
+ Assert( IsServer() )
+
+ if ( !IsValid( vortexSphere ) )
+ return false
+
+ if ( !vortexSphere.GetOwnerWeapon() )
+ return false
+
+ entity vortexWeapon = vortexSphere.GetOwnerWeapon()
+ if ( !IsValid( vortexWeapon ) )
+ return false
+
+ if ( projectile )
+ {
+ if ( !IsValid_ThisFrame( projectile ) )
+ return false
+
+ if ( projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_ignores_vortex" ) == 1 )
+ return false
+
+ if ( projectile.ProjectileGetWeaponClassName() == "" )
+ return false
+
+ // TEMP HACK
+ if ( projectile.ProjectileGetWeaponClassName() == "mp_weapon_tether" )
+ return false
+ }
+
+ return true
+}
+#endif
+
+/********************************/
+/* Setting override functions */
+/********************************/
+
+function Vortex_SetTagName( entity weapon, string tagName )
+{
+ Vortex_SetWeaponSettingOverride( weapon, "vortexTagName", tagName )
+}
+
+function Vortex_SetBulletCollectionOffset( entity weapon, vector offset )
+{
+ Vortex_SetWeaponSettingOverride( weapon, "bulletCollectionOffset", offset )
+}
+
+function Vortex_SetWeaponSettingOverride( entity weapon, string setting, value )
+{
+ if ( !( setting in weapon.s ) )
+ weapon.s[ setting ] <- null
+ weapon.s[ setting ] = value
+}
+
+string function GetVortexTagName( entity weapon )
+{
+ if ( "vortexTagName" in weapon.s )
+ return expect string( weapon.s.vortexTagName )
+
+ return "vortex_center"
+}
+
+vector function GetBulletCollectionOffset( entity weapon )
+{
+ if ( "bulletCollectionOffset" in weapon.s )
+ return expect vector( weapon.s.bulletCollectionOffset )
+
+ entity owner = weapon.GetWeaponOwner()
+ if ( owner.IsTitan() )
+ return Vector( 300.0, -90.0, -70.0 )
+ else
+ return Vector( 80.0, 17.0, -11.0 )
+
+ unreachable
+}
+
+
+#if SERVER
+function VortexSphereDrainHealthForDamage( entity vortexSphere, damage )
+{
+ // don't drain the health of vortex_spheres that are set to be invulnerable. This is the case for the Particle Wall
+ if ( vortexSphere.IsInvulnerable() )
+ return
+
+ local result = {}
+ result.damage <- damage
+ vortexSphere.Signal( "Script_OnDamaged", result )
+
+ int currentHealth = vortexSphere.GetHealth()
+ Assert( damage >= 0 )
+ // JFS to fix phone home bug; we never hit the assert above locally...
+ damage = max( damage, 0 )
+ vortexSphere.SetHealth( currentHealth - damage )
+
+ entity vortexWeapon = vortexSphere.GetOwnerWeapon()
+ if ( IsValid( vortexWeapon ) && vortexWeapon.HasMod( "fd_gun_shield_redirect" ) )
+ {
+ entity owner = vortexWeapon.GetWeaponOwner()
+ if ( IsValid( owner ) && owner.IsTitan() )
+ {
+ entity soul = owner.GetTitanSoul()
+ if ( IsValid( soul ) )
+ {
+ int shieldRestoreAmount = int( damage ) //Might need tuning
+ soul.SetShieldHealth( min( soul.GetShieldHealth() + shieldRestoreAmount, soul.GetShieldHealthMax() ) )
+ }
+ }
+ }
+
+ UpdateShieldWallColorForFrac( vortexSphere.e.shieldWallFX, GetHealthFrac( vortexSphere ) )
+}
+#endif
+
+
+bool function CodeCallback_OnVortexHitBullet( entity weapon, entity vortexSphere, var damageInfo )
+{
+ bool isAmpedWall = vortexSphere.GetTargetName() == PROTO_AMPED_WALL
+ bool takesDamage = !isAmpedWall
+ bool adjustImpactAngles = !(vortexSphere.GetTargetName() == GUN_SHIELD_WALL)
+
+ #if SERVER
+ if ( vortexSphere.e.BulletHitRules != null )
+ {
+ vortexSphere.e.BulletHitRules( vortexSphere, damageInfo )
+ takesDamage = takesDamage && (DamageInfo_GetDamage( damageInfo ) > 0)
+ }
+ #endif
+
+ vector damageAngles = vortexSphere.GetAngles()
+
+ if ( adjustImpactAngles )
+ damageAngles = AnglesCompose( damageAngles, Vector( 90, 0, 0 ) )
+
+ int teamNum = vortexSphere.GetTeam()
+
+ #if CLIENT
+ vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo )
+ if ( !isAmpedWall )
+ {
+ // TODO: slightly change angles to match radius rotation of vortex cylinder
+ int effectHandle = StartParticleEffectInWorldWithHandle( GetParticleSystemIndex( SHIELD_WALL_BULLET_FX ), damageOrigin, damageAngles )
+ //local color = GetShieldTriLerpColor( 1 - GetHealthFrac( vortexSphere ) )
+ vector color = GetShieldTriLerpColor( 0.0 )
+ EffectSetControlPointVector( effectHandle, 1, color )
+ }
+
+ if ( takesDamage )
+ {
+ float damage = ceil( DamageInfo_GetDamage( damageInfo ) )
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ DamageFlyout( damage, damageOrigin, vortexSphere, false, false )
+ }
+
+ if ( DamageInfo_GetAttacker( damageInfo ) && DamageInfo_GetAttacker( damageInfo ).IsTitan() )
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Heavy.BulletImpact_1P_vs_3P" )
+ else
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Light.BulletImpact_1P_vs_3P" )
+ #else
+ if ( !isAmpedWall )
+ {
+ int fxId = GetParticleSystemIndex( SHIELD_WALL_BULLET_FX )
+ PlayEffectOnVortexSphere( fxId, DamageInfo_GetDamagePosition( damageInfo ), damageAngles, vortexSphere )
+ }
+
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ float damage = ceil( DamageInfo_GetDamage( damageInfo ) )
+
+ Assert( damage >= 0, "Bug 159851 - Damage should be greater than or equal to 0.")
+ damage = max( 0.0, damage )
+
+ if ( IsValid( weapon ) )
+ damage = HandleWeakToPilotWeapons( vortexSphere, weapon.GetWeaponClassName(), damage )
+
+ if ( takesDamage )
+ {
+ //JFS - Arc Round bug fix for Monarch. Projectiles vortex callback doesn't even have damageInfo, so the shield modifier here doesn't exist in VortexSphereDrainHealthForDamage like it should.
+ ShieldDamageModifier damageModifier = GetShieldDamageModifier( damageInfo )
+ damage *= damageModifier.damageScale
+ VortexSphereDrainHealthForDamage( vortexSphere, damage )
+ }
+
+ if ( DamageInfo_GetAttacker( damageInfo ) && DamageInfo_GetAttacker( damageInfo ).IsTitan() )
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Heavy.BulletImpact_3P_vs_3P" )
+ else
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Light.BulletImpact_3P_vs_3P" )
+ #endif
+
+ if ( isAmpedWall )
+ {
+ #if SERVER
+ DamageInfo_ScaleDamage( damageInfo, AMPED_DAMAGE_SCALAR )
+ #endif
+ return false
+ }
+
+ return true
+}
+
+bool function OnVortexHitBullet_BubbleShieldNPC( entity vortexSphere, var damageInfo )
+{
+ vector vortexOrigin = vortexSphere.GetOrigin()
+ vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo )
+
+ float distSq = DistanceSqr( vortexOrigin, damageOrigin )
+ if ( distSq < MINION_BUBBLE_SHIELD_RADIUS_SQR )
+ return false//the damage is coming from INSIDE the sphere
+
+ vector damageVec = damageOrigin - vortexOrigin
+ vector damageAngles = VectorToAngles( damageVec )
+ damageAngles = AnglesCompose( damageAngles, Vector( 90, 0, 0 ) )
+
+ int teamNum = vortexSphere.GetTeam()
+
+ #if CLIENT
+ int effectHandle = StartParticleEffectInWorldWithHandle( GetParticleSystemIndex( SHIELD_WALL_BULLET_FX ), damageOrigin, damageAngles )
+
+ vector color = GetShieldTriLerpColor( 0.9 )
+ EffectSetControlPointVector( effectHandle, 1, color )
+
+ if ( DamageInfo_GetAttacker( damageInfo ) && DamageInfo_GetAttacker( damageInfo ).IsTitan() )
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Heavy.BulletImpact_1P_vs_3P" )
+ else
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Light.BulletImpact_1P_vs_3P" )
+ #else
+ int fxId = GetParticleSystemIndex( SHIELD_WALL_BULLET_FX )
+ PlayEffectOnVortexSphere( fxId, DamageInfo_GetDamagePosition( damageInfo ), damageAngles, vortexSphere )
+ //VortexSphereDrainHealthForDamage( vortexSphere, DamageInfo_GetWeapon( damageInfo ), null )
+
+ if ( DamageInfo_GetAttacker( damageInfo ) && DamageInfo_GetAttacker( damageInfo ).IsTitan() )
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Heavy.BulletImpact_3P_vs_3P" )
+ else
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Light.BulletImpact_3P_vs_3P" )
+ #endif
+ return true
+}
+
+bool function CodeCallback_OnVortexHitProjectile( entity weapon, entity vortexSphere, entity attacker, entity projectile, vector contactPos )
+{
+ // code shouldn't call this on an invalid vortexsphere!
+ if ( !IsValid( vortexSphere ) )
+ return false
+
+ var ignoreVortex = projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_ignores_vortex" )
+ if ( ignoreVortex != null )
+ {
+ #if SERVER
+ if ( projectile.proj.hasBouncedOffVortex )
+ return false
+
+ vector velocity = projectile.GetVelocity()
+ vector multiplier
+
+ switch ( ignoreVortex )
+ {
+ case "drop":
+ multiplier = < -0.25, -0.25, 0.0 >
+ break
+
+ case "fall_vortex":
+ case "fall":
+ multiplier = < -0.25, -0.25, -0.25 >
+ break
+
+ case "mirror":
+ // bounce back, assume along xy axis
+ multiplier = < -1.0, -1.0, 1.0 >
+ break
+
+ default:
+ CodeWarning( "Unknown projectile_ignores_vortex " + ignoreVortex )
+ break
+ }
+
+ velocity = < velocity.x * multiplier.x, velocity.y * multiplier.y, velocity.z * multiplier.z >
+ projectile.proj.hasBouncedOffVortex = true
+ projectile.SetVelocity( velocity )
+ #endif
+ return false
+ }
+
+ bool adjustImpactAngles = !(vortexSphere.GetTargetName() == GUN_SHIELD_WALL)
+
+ vector damageAngles = vortexSphere.GetAngles()
+
+ if ( adjustImpactAngles )
+ damageAngles = AnglesCompose( damageAngles, Vector( 90, 0, 0 ) )
+
+ asset projectileSettingFX = projectile.GetProjectileWeaponSettingAsset( eWeaponVar.vortex_impact_effect )
+ asset impactFX = (projectileSettingFX != $"") ? projectileSettingFX : SHIELD_WALL_EXPMED_FX
+
+ bool isAmpedWall = vortexSphere.GetTargetName() == PROTO_AMPED_WALL
+ bool takesDamage = !isAmpedWall
+
+ #if SERVER
+ if ( vortexSphere.e.ProjectileHitRules != null )
+ takesDamage = vortexSphere.e.ProjectileHitRules( vortexSphere, attacker, takesDamage )
+ #endif
+ // hack to let client know about amped wall, and to amp the shot
+ if ( isAmpedWall )
+ impactFX = AMPED_WALL_IMPACT_FX
+
+ int teamNum = vortexSphere.GetTeam()
+
+ #if CLIENT
+ if ( !isAmpedWall )
+ {
+ int effectHandle = StartParticleEffectInWorldWithHandle( GetParticleSystemIndex( impactFX ), contactPos, damageAngles )
+ //local color = GetShieldTriLerpColor( 1 - GetHealthFrac( vortexSphere ) )
+ vector color = GetShieldTriLerpColor( 0.0 )
+ EffectSetControlPointVector( effectHandle, 1, color )
+ }
+
+ var impact_sound_1p = projectile.ProjectileGetWeaponInfoFileKeyField( "vortex_impact_sound_1p" )
+ if ( impact_sound_1p == null )
+ impact_sound_1p = "TitanShieldWall.Explosive.BulletImpact_1P_vs_3P"
+
+ EmitSoundAtPosition( teamNum, contactPos, impact_sound_1p )
+ #else
+ if ( !isAmpedWall )
+ {
+ int fxId = GetParticleSystemIndex( impactFX )
+ PlayEffectOnVortexSphere( fxId, contactPos, damageAngles, vortexSphere )
+ }
+
+ float damage = float( projectile.GetProjectileWeaponSettingInt( eWeaponVar.damage_near_value ) )
+ // once damageInfo is passed correctly we'll use that instead of looking up the values from the weapon .txt file.
+ // local damage = ceil( DamageInfo_GetDamage( damageInfo ) )
+
+ damage = HandleWeakToPilotWeapons( vortexSphere, projectile.ProjectileGetWeaponClassName(), damage )
+ damage = damage + CalculateTitanSniperExtraDamage( projectile, vortexSphere )
+
+ if ( takesDamage )
+ {
+ VortexSphereDrainHealthForDamage( vortexSphere, damage )
+ if ( IsValid( attacker ) && attacker.IsPlayer() )
+ attacker.NotifyDidDamage( vortexSphere, 0, contactPos, 0, damage, DF_NO_HITBEEP, 0, null, 0 )
+ }
+
+ var impact_sound_3p = projectile.ProjectileGetWeaponInfoFileKeyField( "vortex_impact_sound_3p" )
+
+ if ( impact_sound_3p == null )
+ impact_sound_3p = "TitanShieldWall.Explosive.BulletImpact_3P_vs_3P"
+
+ EmitSoundAtPosition( teamNum, contactPos, impact_sound_3p )
+
+ int damageSourceID = projectile.ProjectileGetDamageSourceID()
+ switch ( damageSourceID )
+ {
+ case eDamageSourceId.mp_titanweapon_dumbfire_rockets:
+ vector normal = projectile.GetVelocity() * -1
+ normal = Normalize( normal )
+ ClusterRocket_Detonate( projectile, normal )
+ CreateNoSpawnArea( TEAM_INVALID, TEAM_INVALID, contactPos, ( CLUSTER_ROCKET_BURST_COUNT / 5.0 ) * 0.5 + 1.0, CLUSTER_ROCKET_BURST_RANGE + 100 )
+ break
+
+ case eDamageSourceId.mp_weapon_grenade_electric_smoke:
+ ElectricGrenadeSmokescreen( projectile, FX_ELECTRIC_SMOKESCREEN_PILOT_AIR )
+ break
+
+ case eDamageSourceId.mp_weapon_grenade_emp:
+
+ if ( StatusEffect_Get( vortexSphere, eStatusEffect.destroyed_by_emp ) )
+ VortexSphereDrainHealthForDamage( vortexSphere, vortexSphere.GetHealth() )
+ break
+
+ case eDamageSourceId.mp_titanability_sonar_pulse:
+ if ( IsValid( attacker ) && attacker.IsTitan() )
+ {
+ int team = attacker.GetTeam()
+ PulseLocation( attacker, team, contactPos, false, false )
+ array<string> mods = projectile.ProjectileGetMods()
+ if ( mods.contains( "pas_tone_sonar" ) )
+ thread DelayedPulseLocation( attacker, team, contactPos, false, false )
+ }
+ break
+
+ }
+ #endif
+
+ // hack to let client know about amped wall, and to amp the shot
+ if ( isAmpedWall )
+ {
+ #if SERVER
+ projectile.proj.damageScale = AMPED_DAMAGE_SCALAR
+ #endif
+
+ return false
+ }
+
+ return true
+}
+
+bool function OnVortexHitProjectile_BubbleShieldNPC( entity vortexSphere, entity attacker, entity projectile, vector contactPos )
+{
+ vector vortexOrigin = vortexSphere.GetOrigin()
+
+ float dist = DistanceSqr( vortexOrigin, contactPos )
+ if ( dist < MINION_BUBBLE_SHIELD_RADIUS_SQR )
+ return false // the damage is coming from INSIDE THE SPHERE
+
+ vector damageVec = Normalize( contactPos - vortexOrigin )
+ vector damageAngles = VectorToAngles( damageVec )
+ damageAngles = AnglesCompose( damageAngles, Vector( 90, 0, 0 ) )
+
+ asset projectileSettingFX = projectile.GetProjectileWeaponSettingAsset( eWeaponVar.vortex_impact_effect )
+ asset impactFX = (projectileSettingFX != $"") ? projectileSettingFX : SHIELD_WALL_EXPMED_FX
+
+ int teamNum = vortexSphere.GetTeam()
+
+ #if CLIENT
+ int effectHandle = StartParticleEffectInWorldWithHandle( GetParticleSystemIndex( impactFX ), contactPos, damageAngles )
+
+ vector color = GetShieldTriLerpColor( 0.9 )
+ EffectSetControlPointVector( effectHandle, 1, color )
+
+ EmitSoundAtPosition( teamNum, contactPos, "TitanShieldWall.Explosive.BulletImpact_1P_vs_3P" )
+ #else
+ int fxId = GetParticleSystemIndex( impactFX )
+ PlayEffectOnVortexSphere( fxId, contactPos, damageAngles, vortexSphere )
+// VortexSphereDrainHealthForDamage( vortexSphere, null, projectile )
+
+ EmitSoundAtPosition( teamNum, contactPos, "TitanShieldWall.Explosive.BulletImpact_3P_vs_3P" )
+
+ if ( projectile.ProjectileGetDamageSourceID() == eDamageSourceId.mp_titanweapon_dumbfire_rockets )
+ {
+ vector normal = projectile.GetVelocity() * -1
+ normal = Normalize( normal )
+ ClusterRocket_Detonate( projectile, normal )
+ CreateNoSpawnArea( TEAM_INVALID, TEAM_INVALID, contactPos, ( CLUSTER_ROCKET_BURST_COUNT / 5.0 ) * 0.5 + 1.0, CLUSTER_ROCKET_BURST_RANGE + 100 )
+ }
+ #endif
+ return true
+}
+
+#if SERVER
+float function HandleWeakToPilotWeapons( entity vortexSphere, string weaponName, float damage )
+{
+ if ( vortexSphere.e.proto_weakToPilotWeapons ) //needs code for real, but this is fine for prototyping
+ {
+ // is weapon a pilot weapon?
+ local refType = GetWeaponInfoFileKeyField_Global( weaponName, "weaponClass" )
+ if ( refType == "human" )
+ {
+ damage *= VORTEX_PILOT_WEAPON_WEAKNESS_DAMAGESCALE
+ }
+ }
+
+ return damage
+}
+#endif
+
+// ???: reflectOrigin not used
+int function VortexReflectAttack( entity vortexWeapon, attackParams, vector reflectOrigin )
+{
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !vortexSphere )
+ return 0
+
+ #if SERVER
+ Assert( vortexSphere )
+ #endif
+
+ int totalfired = 0
+ int totalAttempts = 0
+
+ bool forceReleased = false
+ // in this case, it's also considered "force released" if the charge time runs out
+ if ( vortexWeapon.IsForceRelease() || vortexWeapon.GetWeaponChargeFraction() == 1 )
+ forceReleased = true
+
+ //Requires code feature to properly fire tracers from offset positions.
+ //if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ // attackParams.pos = reflectOrigin
+
+ // PREDICTED REFIRES
+ // bullet impact events don't individually fire back per event because we aggregate and then shotgun blast them
+
+ //Remove the below script after FireWeaponBulletBroadcast
+ //local bulletsFired = Vortex_FireBackBullets( vortexWeapon, attackParams )
+ //totalfired += bulletsFired
+ int bulletCount = GetBulletsAbsorbedCount( vortexWeapon )
+ if ( bulletCount > 0 )
+ {
+ if ( "ampedBulletCount" in vortexWeapon.s )
+ vortexWeapon.s.ampedBulletCount++
+ else
+ vortexWeapon.s.ampedBulletCount <- 1
+ vortexWeapon.Signal( "FireAmpedVortexBullet" )
+ totalfired += 1
+ }
+
+ // UNPREDICTED REFIRES
+ #if SERVER
+ //printt( "server: force released?", forceReleased )
+
+ local unpredictedRefires = Vortex_GetProjectileImpacts( vortexWeapon )
+
+ // HACK we don't actually want to refire them with a spiral but
+ // this is to temporarily ensure compatibility with the Titan rocket launcher
+ if ( !( "spiralMissileIdx" in vortexWeapon.s ) )
+ vortexWeapon.s.spiralMissileIdx <- null
+ vortexWeapon.s.spiralMissileIdx = 0
+ foreach ( impactData in unpredictedRefires )
+ {
+ bool didFire = DoVortexAttackForImpactData( vortexWeapon, attackParams, impactData, totalAttempts )
+ if ( didFire )
+ totalfired++
+ totalAttempts++
+ }
+ #endif
+
+ SetVortexAmmo( vortexWeapon, 0 )
+ vortexWeapon.Signal( "VortexFired" )
+
+#if SERVER
+ vortexSphere.ClearAllBulletsFromSphere()
+#endif
+
+ /*
+ if ( forceReleased )
+ DestroyVortexSphereFromVortexWeapon( vortexWeapon )
+ else
+ DisableVortexSphereFromVortexWeapon( vortexWeapon )
+ */
+
+ return totalfired
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_weapon_dialogue.nut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_weapon_dialogue.nut
new file mode 100644
index 00000000..04fd24d3
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_weapon_dialogue.nut
@@ -0,0 +1,44 @@
+untyped
+
+globalize_all_functions
+
+function Weapon_Dialogue_Init()
+{
+ RegisterConversation( "CoopTD_TurretAvailable", VO_PRIORITY_PLAYERSTATE ) // player turret becomes available
+ RegisterConversation( "CoopTD_TurretAvailableNag", VO_PRIORITY_PLAYERSTATE ) // player turret available nag
+ RegisterConversation( "CoopTD_TurretDestroyed", VO_PRIORITY_PLAYERSTATE ) // player turret destroyed
+ RegisterConversation( "CoopTD_TurretDeadAndReady", VO_PRIORITY_PLAYERSTATE ) // player turret destroyed, and one ready
+ RegisterConversation( "CoopTD_TurretKillstreak", VO_PRIORITY_PLAYERSTATE ) // player turret has lots of kills
+ RegisterConversation( "CoopTD_TurretKilledTitan", VO_PRIORITY_PLAYERSTATE ) // player turret killed a titan
+ RegisterConversation( "CoopTD_TurretKilledTitan_Multi", VO_PRIORITY_PLAYERSTATE ) // player turret killed multiple titans
+
+ #if CLIENT
+
+ AddVDULineForSarah( "CoopTD_TurretAvailable", "diag_gm_coop_playerTurretEarned_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretAvailable", "diag_gm_coop_playerTurretEarned_mcor_Sarah" )
+
+ // player turret available nag
+ AddVDULineForSarah( "CoopTD_TurretAvailableNag", "diag_gm_coop_playerTurretNag_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretAvailableNag", "diag_gm_coop_playerTurretNag_mcor_Sarah" )
+
+ // player turret destroyed
+ AddVDULineForSarah( "CoopTD_TurretDestroyed", "diag_gm_coop_playerTurretDestro_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretDestroyed", "diag_gm_coop_playerTurretDestro_mcor_Sarah" )
+
+ // player turret destroyed and another one ready
+ AddVDULineForSarah( "CoopTD_TurretDeadAndReady", "diag_gm_coop_playerTurretDeadAndReady_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretDeadAndReady", "diag_gm_coop_playerTurretDeadAndReady_mcor_Sarah" )
+
+ // player turret has lots of kills
+ AddVDULineForSarah( "CoopTD_TurretKillstreak", "diag_gm_coop_playerTurretHighKills_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretKillstreak", "diag_gm_coop_playerTurretHighKills_mcor_Sarah" )
+
+ // player turret killed a titan
+ AddVDULineForSarah( "CoopTD_TurretKilledTitan", "diag_gm_coop_playerTurretKilledTitan_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretKilledTitan", "diag_gm_coop_playerTurretKilledTitan_mcor_Sarah" )
+
+ // player turret killed multiple titans
+ AddVDULineForSarah( "CoopTD_TurretKilledTitan_Multi", "diag_gm_coop_playerTurretKilledTitanAgain_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretKilledTitan_Multi", "diag_gm_coop_playerTurretKilledTitanAgain_mcor_Sarah" )
+ #endif
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/weapons/_weapon_utility.nut b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_weapon_utility.nut
new file mode 100644
index 00000000..b3e5f5a3
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/weapons/_weapon_utility.nut
@@ -0,0 +1,3966 @@
+untyped
+
+//TODO: Should split this up into server, client and shared versions and just globalize_all_functions
+global function WeaponUtility_Init
+
+global function ApplyVectorSpread
+global function DebugDrawMissilePath
+global function DegreesToTarget
+global function DetonateAllPlantedExplosives
+global function EntityCanHaveStickyEnts
+global function EntityShouldStick
+global function FireExpandContractMissiles
+global function FireExpandContractMissiles_S2S
+global function GetVectorFromPositionToCrosshair
+global function GetVelocityForDestOverTime
+global function GetPlayerVelocityForDestOverTime
+global function GetWeaponBurnMods
+global function InitMissileForRandomDriftForVortexLow
+global function IsPilotShotgunWeapon
+global function PlantStickyEntity
+global function PlantStickyEntityThatBouncesOffWalls
+global function PlantStickyEntityOnWorldThatBouncesOffWalls
+global function PlantStickyGrenade
+global function PlantSuperStickyGrenade
+global function Player_DetonateSatchels
+global function PROTO_CanPlayerDeployWeapon
+global function ProximityCharge_PostFired_Init
+global function RegenerateOffhandAmmoOverTime
+global function ShotgunBlast
+global function FireGenericBoltWithDrop
+global function OnWeaponPrimaryAttack_GenericBoltWithDrop_Player
+global function OnWeaponPrimaryAttack_GenericMissile_Player
+global function OnWeaponActivate_updateViewmodelAmmo
+global function TEMP_GetDamageFlagsFromProjectile
+global function WeaponCanCrit
+global function GiveEMPStunStatusEffects
+global function GetPrimaryWeapons
+global function GetSidearmWeapons
+global function GetATWeapons
+global function GetPlayerFromTitanWeapon
+global function ChargeBall_Precache
+global function ChargeBall_FireProjectile
+global function ChargeBall_ChargeBegin
+global function ChargeBall_ChargeEnd
+global function ChargeBall_StopChargeEffects
+global function ChargeBall_GetChargeTime
+
+global function PlayerUsedOffhand
+#if SERVER
+global function SetPlayerCooldowns
+global function ResetPlayerCooldowns
+global function StoreOffhandData
+#endif
+
+global function GetRadiusDamageDataFromProjectile
+
+#if DEV
+global function DevPrintAllStatusEffectsOnEnt
+#endif // #if DEV
+
+#if SERVER
+ global function ClusterRocket_Detonate
+ global function PassThroughDamage
+ global function PROTO_CleanupTrackedProjectiles
+ global function PROTO_InitTrackedProjectile
+ global function PROTO_PlayTrapLightEffect
+ global function Satchel_PostFired_Init
+ global function StartClusterExplosions
+ global function TrapDestroyOnRoundEnd
+ global function TrapExplodeOnDamage
+ global function PROTO_DelayCooldown
+ global function PROTO_FlakCannonMissiles
+ global function GetBulletPassThroughTargets
+ global function IsValidPassThroughTarget
+ global function GivePlayerAmpedWeapon
+ global function GivePlayerAmpedWeaponAndSetAsActive
+ global function ReplacePlayerOffhand
+ global function ReplacePlayerOrdnance
+ global function DisableWeapons
+ global function EnableWeapons
+ global function WeaponAttackWave
+ global function AddActiveThermiteBurn
+ global function GetActiveThermiteBurnsWithinRadius
+ global function OnWeaponPrimaryAttack_GenericBoltWithDrop_NPC
+ global function OnWeaponPrimaryAttack_GenericMissile_NPC
+ global function EMP_DamagedPlayerOrNPC
+ global function EMP_FX
+ global function GetWeaponDPS
+ global function GetTTK
+ global function GetWeaponModsFromDamageInfo
+ global function Thermite_DamagePlayerOrNPCSounds
+ global function AddThreatScopeColorStatusEffect
+ global function RemoveThreatScopeColorStatusEffect
+#endif //SERVER
+#if CLIENT
+ global function GlobalClientEventHandler
+ global function UpdateViewmodelAmmo
+ global function ServerCallback_AirburstIconUpdate
+ global function ServerCallback_GuidedMissileDestroyed
+ global function IsOwnerViewPlayerFullyADSed
+#endif //CLIENT
+
+global const PROJECTILE_PREDICTED = true
+global const PROJECTILE_NOT_PREDICTED = false
+
+global const PROJECTILE_LAG_COMPENSATED = true
+global const PROJECTILE_NOT_LAG_COMPENSATED = false
+
+const float EMP_SEVERITY_SLOWTURN = 0.35
+const float EMP_SEVERITY_SLOWMOVE = 0.50
+const float LASER_STUN_SEVERITY_SLOWTURN = 0.20
+const float LASER_STUN_SEVERITY_SLOWMOVE = 0.30
+
+const asset FX_EMP_BODY_HUMAN = $"P_emp_body_human"
+const asset FX_EMP_BODY_TITAN = $"P_emp_body_titan"
+const asset FX_VANGUARD_ENERGY_BODY_HUMAN = $"P_monarchBeam_body_human"
+const asset FX_VANGUARD_ENERGY_BODY_TITAN = $"P_monarchBeam_body_titan"
+const SOUND_EMP_REBOOT_SPARKS = "marvin_weld"
+const FX_EMP_REBOOT_SPARKS = $"weld_spark_01_sparksfly"
+const EMP_GRENADE_BEAM_EFFECT = $"wpn_arc_cannon_beam"
+const DRONE_REBOOT_TIME = 5.0
+const GUNSHIP_REBOOT_TIME = 5.0
+
+global struct RadiusDamageData
+{
+ int explosionDamage
+ int explosionDamageHeavyArmor
+ float explosionRadius
+ float explosionInnerRadius
+}
+
+#if SERVER
+
+global struct PopcornInfo
+{
+ string weaponName
+ array weaponMods // could be array< string >
+ int damageSourceId
+ int count
+ float delay
+ float offset
+ float range
+ vector normal
+ float duration
+ int groupSize
+ bool hasBase
+}
+
+struct ColorSwapStruct
+{
+ int statusEffectId
+ entity weaponOwner
+}
+
+struct
+{
+ float titanRocketLauncherTitanDamageRadius
+ float titanRocketLauncherOtherDamageRadius
+
+ int activeThermiteBurnsManagedEnts
+ array<ColorSwapStruct> colorSwapStatusEffects
+} file
+
+global int HOLO_PILOT_TRAIL_FX
+
+global struct HoverSounds
+{
+ string liftoff_1p
+ string liftoff_3p
+ string hover_1p
+ string hover_3p
+ string descent_1p
+ string descent_3p
+ string landing_1p
+ string landing_3p
+}
+
+#endif
+
+function WeaponUtility_Init()
+{
+ level.weaponsPrecached <- {}
+
+ // what classes can sticky thrown entities stick to?
+ level.stickyClasses <- {}
+ level.stickyClasses[ "worldspawn" ] <- true
+ level.stickyClasses[ "player" ] <- true
+ level.stickyClasses[ "prop_dynamic" ] <- true
+ level.stickyClasses[ "prop_script" ] <- true
+ level.stickyClasses[ "func_brush" ] <- true
+ level.stickyClasses[ "func_brush_lightweight" ] <- true
+ level.stickyClasses[ "phys_bone_follower" ] <- true
+
+ level.trapChainReactClasses <- {}
+ level.trapChainReactClasses[ "mp_weapon_frag_grenade" ] <- true
+ level.trapChainReactClasses[ "mp_weapon_satchel" ] <- true
+ level.trapChainReactClasses[ "mp_weapon_proximity_mine" ] <- true
+ level.trapChainReactClasses[ "mp_weapon_laser_mine" ] <- true
+
+ RegisterSignal( "Planted" )
+ RegisterSignal( "EMP_FX" )
+ RegisterSignal( "ArcStunned" )
+
+ PrecacheParticleSystem( EMP_GRENADE_BEAM_EFFECT )
+ PrecacheParticleSystem( FX_EMP_BODY_TITAN )
+ PrecacheParticleSystem( FX_EMP_BODY_HUMAN )
+ PrecacheParticleSystem( FX_VANGUARD_ENERGY_BODY_HUMAN )
+ PrecacheParticleSystem( FX_VANGUARD_ENERGY_BODY_TITAN )
+ PrecacheParticleSystem( FX_EMP_REBOOT_SPARKS )
+
+ PrecacheImpactEffectTable( CLUSTER_ROCKET_FX_TABLE )
+
+ #if SERVER
+ AddDamageCallbackSourceID( eDamageSourceId.mp_titanweapon_triple_threat, TripleThreatGrenade_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_defender, Defender_DamagedPlayerOrNPC )
+ //AddDamageCallbackSourceID( eDamageSourceId.mp_titanweapon_rocketeer_rocketstream, TitanRocketLauncher_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_smr, SMR_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_flak_rifle, PROTO_Flak_Rifle_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_titanweapon_stun_laser, VanguardEnergySiphon_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_grenade_emp, EMP_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_proximity_mine, EMP_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId[ CHARGE_TOOL ], EMP_DamagedPlayerOrNPC )
+ if ( IsMultiplayer() )
+ AddCallback_OnPlayerRespawned( PROTO_TrackedProjectile_OnPlayerRespawned )
+ AddCallback_OnPlayerKilled( PAS_CooldownReduction_OnKill )
+ AddCallback_OnPlayerGetsNewPilotLoadout( OnPlayerGetsNewPilotLoadout )
+ AddCallback_OnPlayerKilled( OnPlayerKilled )
+
+ file.activeThermiteBurnsManagedEnts = CreateScriptManagedEntArray()
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+
+ HOLO_PILOT_TRAIL_FX = PrecacheParticleSystem( $"P_ar_holopilot_trail" )
+
+ PrecacheParticleSystem( $"wpn_laser_blink" )
+ PrecacheParticleSystem( $"wpn_laser_blink_fast" )
+ PrecacheParticleSystem( $"P_ordinance_icon_owner" )
+ #endif
+}
+
+#if SERVER
+void function EntitiesDidLoad()
+{
+#if SP
+ // if we are going to do this, it should happen in the weapon, not globally
+ //float titanRocketLauncherInnerRadius = expect float( GetWeaponInfoFileKeyField_Global( "mp_titanweapon_rocketeer_rocketstream", "explosion_inner_radius" ) )
+ //float titanRocketLauncherOuterRadius = expect float( GetWeaponInfoFileKeyField_Global( "mp_titanweapon_rocketeer_rocketstream", "explosionradius" ) )
+ //file.titanRocketLauncherTitanDamageRadius = titanRocketLauncherInnerRadius + ( ( titanRocketLauncherOuterRadius - titanRocketLauncherInnerRadius ) * 0.4 )
+ //file.titanRocketLauncherOtherDamageRadius = titanRocketLauncherInnerRadius + ( ( titanRocketLauncherOuterRadius - titanRocketLauncherInnerRadius ) * 0.1 )
+#endif
+}
+#endif
+
+////////////////////////////////////////////////////////////////////
+
+#if CLIENT
+void function GlobalClientEventHandler( entity weapon, string name )
+{
+ if ( name == "ammo_update" )
+ UpdateViewmodelAmmo( false, weapon )
+
+ if ( name == "ammo_full" )
+ UpdateViewmodelAmmo( true, weapon )
+}
+
+function UpdateViewmodelAmmo( bool forceFull, entity weapon )
+{
+ Assert( weapon != null ) // used to be: if ( weapon == null ) weapon = this.self
+
+ if ( !IsValid( weapon ) )
+ return
+ if ( !IsLocalViewPlayer( weapon.GetWeaponOwner() ) )
+ return
+
+ int bodyGroupCount = weapon.GetWeaponSettingInt( eWeaponVar.bodygroup_ammo_index_count )
+ if ( bodyGroupCount <= 0 )
+ return
+
+ int rounds = weapon.GetWeaponPrimaryClipCount()
+ int maxRoundsForClipSize = weapon.GetWeaponPrimaryClipCountMax()
+ int maxRoundsForBodyGroup = (bodyGroupCount - 1)
+ int maxRounds = minint( maxRoundsForClipSize, maxRoundsForBodyGroup )
+
+ if ( forceFull || (rounds > maxRounds) )
+ rounds = maxRounds
+
+ //printt( "ROUNDS:", rounds, "/", maxRounds )
+ weapon.SetViewmodelAmmoModelIndex( rounds )
+}
+#endif // #if CLIENT
+
+void function OnWeaponActivate_updateViewmodelAmmo( entity weapon )
+{
+#if CLIENT
+ UpdateViewmodelAmmo( false, weapon )
+#endif // #if CLIENT
+}
+
+#if SERVER
+//////////////////WEAPON DAMAGE CALLBACKS/////////////////////////////////////////
+void function TripleThreatGrenade_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ if ( !IsValid( ent ) )
+ return
+
+ if ( ent.GetClassName() == "grenade_frag" )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ vector damagePosition = DamageInfo_GetDamagePosition( damageInfo )
+
+ vector entOrigin = ent.GetOrigin()
+ vector entCenter = ent.GetWorldSpaceCenter()
+ float distanceToOrigin = Distance( entOrigin, damagePosition )
+ float distanceToCenter = Distance( entCenter, damagePosition )
+
+ vector normal = Vector( 0, 0, 1 )
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ if ( IsValid( inflictor.s ) )
+ {
+ if ( "collisionNormal" in inflictor.s )
+ normal = expect vector( inflictor.s.collisionNormal )
+ }
+
+ local zDifferenceOrigin = deg_cos( DegreesToTarget( entOrigin, normal, damagePosition ) ) * distanceToOrigin
+ local zDifferenceTop = deg_cos( DegreesToTarget( entCenter, normal, damagePosition ) ) * distanceToCenter - (entCenter.z - entOrigin.z)
+
+ float zDamageDiff
+ //Full damage if explosion is between Origin or Center.
+ if ( zDifferenceOrigin > 0 && zDifferenceTop < 0 )
+ zDamageDiff = 1.0
+ else if ( zDifferenceTop > 0 )
+ zDamageDiff = GraphCapped( zDifferenceTop, 0.0, 32.0, 1.0, 0.0 )
+ else
+ zDamageDiff = GraphCapped( zDifferenceOrigin, 0.0, -32.0, 1.0, 0.0 )
+
+ DamageInfo_ScaleDamage( damageInfo, zDamageDiff )
+}
+
+void function Defender_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ if ( !IsValid( ent ) )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ local damage = Vortex_HandleElectricDamage( ent, DamageInfo_GetAttacker( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetWeapon( damageInfo ) )
+ DamageInfo_SetDamage( damageInfo, damage )
+}
+
+/*
+void function TitanRocketLauncher_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ Assert( IsSingleplayer() )
+
+ if ( !IsValid( ent ) )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ vector damagePosition = DamageInfo_GetDamagePosition( damageInfo )
+
+ if ( ent == DamageInfo_GetAttacker( damageInfo ) )
+ return
+
+ if ( ent.IsTitan() )
+ {
+ vector entOrigin = ent.GetOrigin()
+ if ( Distance( damagePosition, entOrigin ) > file.titanRocketLauncherTitanDamageRadius )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ }
+ else if ( IsHumanSized( ent ) )
+ {
+ if ( Distance( damagePosition, ent.GetOrigin() ) > file.titanRocketLauncherOtherDamageRadius )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ }
+}
+*/
+
+void function SMR_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ //Hack - JFS ( The explosion radius is too small on the SMR to deal splash damage to pilots on a Titan. )
+ if ( !IsValid( ent ) )
+ return
+
+ if ( !ent.IsTitan() )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( IsValid( attacker ) && attacker.IsPlayer() && attacker.GetTitanSoulBeingRodeoed() == ent.GetTitanSoul() )
+ attacker.TakeDamage( 30, attacker, attacker, { scriptType = DF_GIB | DF_EXPLOSION, damageSourceId = eDamageSourceId.mp_weapon_smr, weapon = DamageInfo_GetWeapon( damageInfo ) } )
+}
+
+
+void function PROTO_Flak_Rifle_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !IsValid( ent ) || !IsValid( attacker ) )
+ return
+
+ if ( attacker == ent )
+ DamageInfo_ScaleDamage( damageInfo, 0.5 )
+}
+
+function EngineerRocket_DamagedPlayerOrNPC( ent, damageInfo )
+{
+ expect entity( ent )
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !IsValid( ent ) || !IsValid( attacker ) )
+ return
+
+ if ( attacker == ent )
+ DamageInfo_SetDamage( damageInfo, 10 )
+}
+///////////////////////////////////////////////////////////////////////
+
+#endif // SERVER
+
+vector function ApplyVectorSpread( vector vecShotDirection, float spreadDegrees, float bias = 1.0 )
+{
+ vector angles = VectorToAngles( vecShotDirection )
+ vector vecUp = AnglesToUp( angles )
+ vector vecRight = AnglesToRight( angles )
+
+ float sinDeg = deg_sin( spreadDegrees / 2.0 )
+
+ // get circular gaussian spread
+ float x
+ float y
+ float z
+
+ if ( bias > 1.0 )
+ bias = 1.0
+ else if ( bias < 0.0 )
+ bias = 0.0
+
+ // code gets these values from cvars ai_shot_bias_min & ai_shot_bias_max
+ float shotBiasMin = -1.0
+ float shotBiasMax = 1.0
+
+ // 1.0 gaussian, 0.0 is flat, -1.0 is inverse gaussian
+ float shotBias = ( ( shotBiasMax - shotBiasMin ) * bias ) + shotBiasMin
+ float flatness = ( fabs(shotBias) * 0.5 )
+
+ while ( true )
+ {
+ x = RandomFloatRange( -1.0, 1.0 ) * flatness + RandomFloatRange( -1.0, 1.0 ) * (1 - flatness)
+ y = RandomFloatRange( -1.0, 1.0 ) * flatness + RandomFloatRange( -1.0, 1.0 ) * (1 - flatness)
+ if ( shotBias < 0 )
+ {
+ x = ( x >= 0 ) ? 1.0 - x : -1.0 - x
+ y = ( y >= 0 ) ? 1.0 - y : -1.0 - y
+ }
+ z = x * x + y * y
+
+ if ( z <= 1 )
+ break
+ }
+
+ vector addX = vecRight * ( x * sinDeg )
+ vector addY = vecUp * ( y * sinDeg )
+ vector m_vecResult = vecShotDirection + addX + addY
+
+ return m_vecResult
+}
+
+
+float function DegreesToTarget( vector origin, vector forward, vector targetPos )
+{
+ vector dirToTarget = targetPos - origin
+ dirToTarget = Normalize( dirToTarget )
+ float dot = DotProduct( forward, dirToTarget )
+ float degToTarget = (acos( dot ) * 180 / PI)
+
+ return degToTarget
+}
+
+function ShotgunBlast( entity weapon, vector pos, vector dir, int numBlasts, int damageType, float damageScaler = 1.0, float ornull maxAngle = null, float ornull maxDistance = null )
+{
+ Assert( numBlasts > 0 )
+ int numBlastsOriginal = numBlasts
+ entity owner = weapon.GetWeaponOwner()
+
+ /*
+ Debug ConVars:
+ visible_ent_cone_debug_duration_client - Set to non-zero to see debug output
+ visible_ent_cone_debug_duration_server - Set to non-zero to see debug output
+ visible_ent_cone_debug_draw_radius - Size of trace endpoint debug draw
+ */
+
+ if ( maxDistance == null )
+ maxDistance = weapon.GetMaxDamageFarDist()
+ expect float( maxDistance )
+
+ if ( maxAngle == null )
+ maxAngle = owner.GetAttackSpreadAngle() * 0.5
+ expect float( maxAngle )
+
+ array<entity> ignoredEntities = [ owner ]
+ int traceMask = TRACE_MASK_SHOT
+ int visConeFlags = VIS_CONE_ENTS_TEST_HITBOXES | VIS_CONE_ENTS_CHECK_SOLID_BODY_HIT | VIS_CONE_ENTS_APPOX_CLOSEST_HITBOX | VIS_CONE_RETURN_HIT_VORTEX
+
+ entity antilagPlayer
+ if ( owner.IsPlayer() )
+ {
+ if ( owner.IsPhaseShifted() )
+ return;
+
+ antilagPlayer = owner
+ }
+
+ //JFS - Bug 198500
+ Assert( maxAngle > 0.0, "JFS returning out at this instance. We need to investigate when a valid mp_titanweapon_laser_lite weapon returns 0 spread")
+ if ( maxAngle == 0.0 )
+ return
+
+ array<VisibleEntityInCone> results = FindVisibleEntitiesInCone( pos, dir, maxDistance, (maxAngle * 1.1), ignoredEntities, traceMask, visConeFlags, antilagPlayer, weapon )
+ foreach ( result in results )
+ {
+ float angleToHitbox = 0.0
+ if ( !result.solidBodyHit )
+ angleToHitbox = DegreesToTarget( pos, dir, result.approxClosestHitboxPos )
+
+ numBlasts -= ShotgunBlastDamageEntity( weapon, pos, dir, result, angleToHitbox, maxAngle, numBlasts, damageType, damageScaler )
+ if ( numBlasts <= 0 )
+ break
+ }
+
+ //Something in the TakeDamage above is triggering the weapon owner to become invalid.
+ owner = weapon.GetWeaponOwner()
+ if ( !IsValid( owner ) )
+ return
+
+ // maxTracer limit set in /r1dev/src/game/client/c_player.h
+ const int MAX_TRACERS = 16
+ bool didHitAnything = ((numBlastsOriginal - numBlasts) != 0)
+ bool doTraceBrushOnly = (!didHitAnything)
+ if ( numBlasts > 0 )
+ weapon.FireWeaponBullet_Special( pos, dir, minint( numBlasts, MAX_TRACERS ), damageType, false, false, true, false, false, false, doTraceBrushOnly )
+}
+
+
+const SHOTGUN_ANGLE_MIN_FRACTION = 0.1;
+const SHOTGUN_ANGLE_MAX_FRACTION = 1.0;
+const SHOTGUN_DAMAGE_SCALE_AT_MIN_ANGLE = 0.8;
+const SHOTGUN_DAMAGE_SCALE_AT_MAX_ANGLE = 0.1;
+
+int function ShotgunBlastDamageEntity( entity weapon, vector barrelPos, vector barrelVec, VisibleEntityInCone result, float angle, float maxAngle, int numPellets, int damageType, float damageScaler )
+{
+ entity target = result.ent
+
+ //The damage scaler is currently only > 1 for the Titan Shotgun alt fire.
+ if ( !target.IsTitan() && damageScaler > 1 )
+ damageScaler = max( damageScaler * 0.4, 1.5 )
+
+ entity owner = weapon.GetWeaponOwner()
+ // Ent in cone not valid
+ if ( !IsValid( target ) || !IsValid( owner ) )
+ return 0
+
+ // Fire fake bullet towards entity for visual purposes only
+ vector hitLocation = result.visiblePosition
+ vector vecToEnt = ( hitLocation - barrelPos )
+ vecToEnt.Norm()
+ if ( Length( vecToEnt ) == 0 )
+ vecToEnt = barrelVec
+
+ // This fires a fake bullet that doesn't do any damage. Currently it triggeres a damage callback with 0 damage which is bad.
+ weapon.FireWeaponBullet_Special( barrelPos, vecToEnt, 1, damageType, true, true, true, false, false, false, false ) // fires perfect bullet with no antilag and no spread
+
+#if SERVER
+ // Determine how much damage to do based on distance
+ float distanceToTarget = Distance( barrelPos, hitLocation )
+
+ if ( !result.solidBodyHit ) // non solid hits take 1 blast more
+ distanceToTarget += 130
+
+ int extraMods = result.extraMods
+ float damageAmount = CalcWeaponDamage( owner, target, weapon, distanceToTarget, extraMods )
+
+ // vortex needs to scale damage based on number of rounds absorbed
+ string className = weapon.GetWeaponClassName()
+ if ( (className == "mp_titanweapon_vortex_shield") || (className == "mp_titanweapon_vortex_shield_ion") || (className == "mp_titanweapon_heat_shield") )
+ {
+ damageAmount *= numPellets
+ //printt( "scaling vortex hitscan output damage by", numPellets, "pellets for", weaponNearDamageTitan, "damage vs titans" )
+ }
+
+ float coneScaler = 1.0
+ //if ( angle > 0 )
+ // coneScaler = GraphCapped( angle, (maxAngle * SHOTGUN_ANGLE_MIN_FRACTION), (maxAngle * SHOTGUN_ANGLE_MAX_FRACTION), SHOTGUN_DAMAGE_SCALE_AT_MIN_ANGLE, SHOTGUN_DAMAGE_SCALE_AT_MAX_ANGLE )
+
+ // Calculate the final damage abount to inflict on the target. Also scale it by damageScaler which may have been passed in by script ( used by alt fire mode on titan shotgun to fire multiple shells )
+ float finalDamageAmount = damageAmount * coneScaler * damageScaler
+ //printt( "angle:", angle, "- coneScaler:", coneScaler, "- damageAmount:", damageAmount, "- damageScaler:", damageScaler, " = finalDamageAmount:", finalDamageAmount )
+
+ // Calculate impulse force to apply based on damage
+ int maxImpulseForce = expect int( weapon.GetWeaponInfoFileKeyField( "impulse_force" ) )
+ float impulseForce = float( maxImpulseForce ) * coneScaler * damageScaler
+ vector impulseVec = barrelVec * impulseForce
+
+ int damageSourceID = weapon.GetDamageSourceID()
+
+ //
+ float critScale = weapon.GetWeaponSettingFloat( eWeaponVar.critical_hit_damage_scale )
+ target.TakeDamage( finalDamageAmount, owner, weapon, { origin = hitLocation, force = impulseVec, scriptType = damageType, damageSourceId = damageSourceID, weapon = weapon, hitbox = result.visibleHitbox, criticalHitScale = critScale } )
+
+ //printt( "-----------" )
+ //printt( " distanceToTarget:", distanceToTarget )
+ //printt( " damageAmount:", damageAmount )
+ //printt( " coneScaler:", coneScaler )
+ //printt( " impulseForce:", impulseForce )
+ //printt( " impulseVec:", impulseVec.x + ", " + impulseVec.y + ", " + impulseVec.z )
+ //printt( " finalDamageAmount:", finalDamageAmount )
+ //PrintTable( result )
+#endif // #if SERVER
+
+ return 1
+}
+
+int function FireGenericBoltWithDrop( entity weapon, WeaponPrimaryAttackParams attackParams, bool isPlayerFired )
+{
+#if CLIENT
+ if ( !weapon.ShouldPredictProjectiles() )
+ return 1
+#endif // #if CLIENT
+
+ weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 )
+
+ const float PROJ_SPEED_SCALE = 1
+ const float PROJ_GRAVITY = 1
+ int damageFlags = weapon.GetWeaponDamageFlags()
+ entity bolt = weapon.FireWeaponBolt( attackParams.pos, attackParams.dir, PROJ_SPEED_SCALE, damageFlags, damageFlags, isPlayerFired, 0 )
+ if ( bolt != null )
+ {
+ bolt.kv.gravity = PROJ_GRAVITY
+ bolt.kv.rendercolor = "0 0 0"
+ bolt.kv.renderamt = 0
+ bolt.kv.fadedist = 1
+ }
+
+ return 1
+}
+var function OnWeaponPrimaryAttack_GenericBoltWithDrop_Player( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ return FireGenericBoltWithDrop( weapon, attackParams, true )
+}
+
+var function OnWeaponPrimaryAttack_EPG( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ entity missile = weapon.FireWeaponMissile( attackParams.pos, attackParams.dir, 1, damageTypes.largeCaliberExp, damageTypes.largeCaliberExp, false, PROJECTILE_NOT_PREDICTED )
+ if ( missile )
+ {
+ EmitSoundOnEntity( missile, "Weapon_Sidwinder_Projectile" )
+ missile.InitMissileForRandomDriftFromWeaponSettings( attackParams.pos, attackParams.dir )
+ }
+
+ return missile
+}
+
+#if SERVER
+var function OnWeaponPrimaryAttack_GenericBoltWithDrop_NPC( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ return FireGenericBoltWithDrop( weapon, attackParams, false )
+}
+#endif // #if SERVER
+
+
+var function OnWeaponPrimaryAttack_GenericMissile_Player( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 )
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+ vector bulletVec = ApplyVectorSpread( attackParams.dir, weaponOwner.GetAttackSpreadAngle() - 1.0 )
+ attackParams.dir = bulletVec
+
+ if ( IsServer() || weapon.ShouldPredictProjectiles() )
+ {
+ entity missile = weapon.FireWeaponMissile( attackParams.pos, attackParams.dir, 1.0, weapon.GetWeaponDamageFlags(), weapon.GetWeaponDamageFlags(), false, PROJECTILE_PREDICTED )
+ if ( missile )
+ {
+ missile.InitMissileForRandomDriftFromWeaponSettings( attackParams.pos, attackParams.dir )
+ }
+ }
+}
+
+#if SERVER
+var function OnWeaponPrimaryAttack_GenericMissile_NPC( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 )
+
+ entity missile = weapon.FireWeaponMissile( attackParams.pos, attackParams.dir, 1.0, weapon.GetWeaponDamageFlags(), weapon.GetWeaponDamageFlags(), true, PROJECTILE_NOT_PREDICTED )
+ if ( missile )
+ {
+ missile.InitMissileForRandomDriftFromWeaponSettings( attackParams.pos, attackParams.dir )
+ }
+}
+#endif // #if SERVER
+
+bool function PlantStickyEntityOnWorldThatBouncesOffWalls( entity ent, table collisionParams, float bounceDot )
+{
+ entity hitEnt = expect entity( collisionParams.hitEnt )
+ if ( hitEnt && ( hitEnt.IsWorld() || hitEnt.HasPusherRootParent() ) )
+ {
+ float dot = expect vector( collisionParams.normal ).Dot( Vector( 0, 0, 1 ) )
+
+ if ( dot < bounceDot )
+ return false
+
+ return PlantStickyEntity( ent, collisionParams )
+ }
+
+ return false
+}
+
+bool function PlantStickyEntityThatBouncesOffWalls( entity ent, table collisionParams, float bounceDot )
+{
+ if ( expect entity( collisionParams.hitEnt ) == GetEntByIndex( 0 ) )
+ {
+ // Satchel hit the world
+ float dot = expect vector( collisionParams.normal ).Dot( Vector( 0, 0, 1 ) )
+
+ if ( dot < bounceDot )
+ return false
+ }
+
+ return PlantStickyEntity( ent, collisionParams )
+}
+
+
+bool function PlantStickyEntity( entity ent, table collisionParams, vector angleOffset = <0.0, 0.0, 0.0> )
+{
+ if ( !EntityShouldStick( ent, expect entity( collisionParams.hitEnt ) ) )
+ return false
+
+ // Don't allow parenting to another "sticky" entity to prevent them parenting onto each other
+ if ( collisionParams.hitEnt.IsProjectile() )
+ return false
+
+ // Update normal from last bouce so when it explodes it can orient the effect properly
+
+ vector plantAngles = AnglesCompose( VectorToAngles( collisionParams.normal ), angleOffset )
+ vector plantPosition = expect vector( collisionParams.pos )
+
+ if ( !LegalOrigin( plantPosition ) )
+ return false
+
+ #if SERVER
+ ent.SetAbsOrigin( plantPosition )
+ ent.SetAbsAngles( plantAngles )
+ ent.proj.isPlanted = true
+ #else
+ ent.SetOrigin( plantPosition )
+ ent.SetAngles( plantAngles )
+ #endif
+ ent.SetVelocity( Vector( 0, 0, 0 ) )
+
+ //printt( " - Hitbox is:", collisionParams.hitbox, " IsWorld:", collisionParams.hitEnt )
+ if ( !collisionParams.hitEnt.IsWorld() )
+ {
+ if ( !ent.IsMarkedForDeletion() && !collisionParams.hitEnt.IsMarkedForDeletion() )
+ {
+ if ( collisionParams.hitbox > 0 )
+ ent.SetParentWithHitbox( collisionParams.hitEnt, collisionParams.hitbox, true )
+
+ // Hit a func_brush
+ else
+ ent.SetParent( collisionParams.hitEnt )
+
+ if ( collisionParams.hitEnt.IsPlayer() )
+ {
+ thread HandleDisappearingParent( ent, expect entity( collisionParams.hitEnt ) )
+ }
+ }
+ }
+ else
+ {
+ ent.SetVelocity( Vector( 0, 0, 0 ) )
+ ent.StopPhysics()
+ }
+ #if CLIENT
+ if ( ent instanceof C_BaseGrenade )
+ #else
+ if ( ent instanceof CBaseGrenade )
+ #endif
+ ent.MarkAsAttached()
+
+ ent.Signal( "Planted" )
+
+ return true
+}
+
+bool function PlantStickyGrenade( entity ent, vector pos, vector normal, entity hitEnt, int hitbox, float depth = 0.0, bool allowBounce = true, bool allowEntityStick = true )
+{
+ if ( ent.GetTeam() == hitEnt.GetTeam() )
+ return false
+
+ if ( ent.IsMarkedForDeletion() || hitEnt.IsMarkedForDeletion() )
+ return false
+
+ vector plantAngles = VectorToAngles( normal )
+ vector plantPosition = pos + normal * -depth
+
+ if ( !allowBounce )
+ ent.SetVelocity( Vector( 0, 0, 0 ) )
+
+ if ( !LegalOrigin( plantPosition ) )
+ return false
+
+ #if SERVER
+ ent.SetAbsOrigin( plantPosition )
+ ent.SetAbsAngles( plantAngles )
+ ent.proj.isPlanted = true
+ #else
+ ent.SetOrigin( plantPosition )
+ ent.SetAngles( plantAngles )
+ #endif
+
+ if ( !hitEnt.IsWorld() && (!hitEnt.IsTitan() || !allowEntityStick) )
+ return false
+
+ // SetOrigin might be causing the ent to get markedForDeletion.
+ if ( ent.IsMarkedForDeletion() )
+ return false
+
+ ent.SetVelocity( Vector( 0, 0, 0 ) )
+
+ if ( hitEnt.IsWorld() )
+ {
+ ent.SetParent( hitEnt, "", true )
+ ent.StopPhysics()
+ }
+ else
+ {
+ if ( hitbox > 0 )
+ ent.SetParentWithHitbox( hitEnt, hitbox, true )
+ else // Hit a func_brush
+ ent.SetParent( hitEnt )
+
+ if ( hitEnt.IsPlayer() )
+ {
+ thread HandleDisappearingParent( ent, hitEnt )
+ }
+ }
+
+ #if CLIENT
+ if ( ent instanceof C_BaseGrenade )
+ ent.MarkAsAttached()
+ #else
+ if ( ent instanceof CBaseGrenade )
+ ent.MarkAsAttached()
+ #endif
+
+ return true
+}
+
+
+bool function PlantSuperStickyGrenade( entity ent, vector pos, vector normal, entity hitEnt, int hitbox )
+{
+ if ( ent.GetTeam() == hitEnt.GetTeam() )
+ return false
+
+ vector plantAngles = VectorToAngles( normal )
+ vector plantPosition = pos
+
+ if ( !LegalOrigin( plantPosition ) )
+ return false
+
+ #if SERVER
+ ent.SetAbsOrigin( plantPosition )
+ ent.SetAbsAngles( plantAngles )
+ ent.proj.isPlanted = true
+ #else
+ ent.SetOrigin( plantPosition )
+ ent.SetAngles( plantAngles )
+ #endif
+
+ if ( !hitEnt.IsWorld() && !hitEnt.IsPlayer() && !hitEnt.IsNPC() )
+ return false
+
+ ent.SetVelocity( Vector( 0, 0, 0 ) )
+
+ if ( hitEnt.IsWorld() )
+ {
+ ent.StopPhysics()
+ }
+ else
+ {
+ if ( !ent.IsMarkedForDeletion() && !hitEnt.IsMarkedForDeletion() )
+ {
+ if ( hitbox > 0 )
+ ent.SetParentWithHitbox( hitEnt, hitbox, true )
+ else // Hit a func_brush
+ ent.SetParent( hitEnt )
+
+ if ( hitEnt.IsPlayer() )
+ {
+ thread HandleDisappearingParent( ent, hitEnt )
+ }
+ }
+ }
+
+ #if CLIENT
+ if ( ent instanceof C_BaseGrenade )
+ ent.MarkAsAttached()
+ #else
+ if ( ent instanceof CBaseGrenade )
+ ent.MarkAsAttached()
+ #endif
+
+ return true
+}
+
+#if SERVER
+void function HandleDisappearingParent( entity ent, entity parentEnt )
+{
+ parentEnt.EndSignal( "OnDeath" )
+ ent.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ ent.ClearParent()
+ }
+ )
+
+ parentEnt.WaitSignal( "StartPhaseShift" )
+}
+#else
+void function HandleDisappearingParent( entity ent, entity parentEnt )
+{
+ parentEnt.EndSignal( "OnDeath" )
+ ent.EndSignal( "OnDestroy" )
+
+ parentEnt.WaitSignal( "StartPhaseShift" )
+
+ ent.ClearParent()
+}
+#endif
+
+bool function EntityShouldStick( entity stickyEnt, entity hitent )
+{
+ if ( !EntityCanHaveStickyEnts( stickyEnt, hitent ) )
+ return false
+
+ if ( hitent == stickyEnt )
+ return false
+
+ return true
+}
+
+bool function EntityCanHaveStickyEnts( entity stickyEnt, entity ent )
+{
+ if ( !IsValid( ent ) )
+ return false
+
+ if ( ent.GetModelName() == $"" ) // valid case, other projectiles bullets, etc.. sometimes have no model
+ return false;
+
+ local entClassname
+ if ( IsServer() )
+ entClassname = ent.GetClassName()
+ else
+ entClassname = ent.GetSignifierName() // Can return null
+
+ if ( !( entClassname in level.stickyClasses ) && !ent.IsNPC() )
+ return false
+
+ #if CLIENT
+ if ( stickyEnt instanceof C_Projectile )
+ #else
+ if ( stickyEnt instanceof CProjectile )
+ #endif
+ {
+ string weaponClassName = stickyEnt.ProjectileGetWeaponClassName()
+ local stickPlayer = GetWeaponInfoFileKeyField_Global( weaponClassName, "stick_pilot" )
+ local stickTitan = GetWeaponInfoFileKeyField_Global( weaponClassName, "stick_titan" )
+ local stickNPC = GetWeaponInfoFileKeyField_Global( weaponClassName, "stick_npc" )
+
+ if ( ent.IsTitan() && stickTitan )
+ return true
+ else if ( ent.IsPlayer() && stickPlayer )
+ return true
+ else if ( ent.IsNPC() && stickNPC )
+ return true
+
+ // not pilots
+ if ( ent.IsPlayer() && !ent.IsTitan() )
+ return false
+ }
+
+ return true
+}
+
+#if SERVER
+// shared with the vortex script which also needs to create satchels
+function Satchel_PostFired_Init( entity satchel, entity player )
+{
+ satchel.proj.onlyAllowSmartPistolDamage = false
+ thread SatchelThink( satchel, player )
+}
+
+function SatchelThink( entity satchel, entity player )
+{
+ player.EndSignal("OnDestroy")
+ satchel.EndSignal("OnDestroy")
+
+ int satchelHealth = 15
+ thread TrapExplodeOnDamage( satchel, satchelHealth )
+
+ #if DEV
+ // temp HACK for FX to use to figure out the size of the particle to play
+ if ( Flag( "ShowExplosionRadius" ) )
+ thread ShowExplosionRadiusOnExplode( satchel )
+ #endif
+
+ player.EndSignal( "OnDeath" )
+
+ OnThreadEnd(
+ function() : ( satchel )
+ {
+ if ( IsValid( satchel ) )
+ {
+ satchel.Destroy()
+ }
+ }
+ )
+
+ WaitForever()
+}
+
+#endif // SERVER
+
+function ProximityCharge_PostFired_Init( entity proximityMine, entity player )
+{
+ #if SERVER
+ proximityMine.proj.onlyAllowSmartPistolDamage = false
+ #endif
+}
+
+function DetonateAllPlantedExplosives( entity player )
+{
+ // ToDo: Could use Player_DetonateSatchels but it only tracks satchels, not laser mines.
+
+ // Detonate all explosives - satchels and laser mines are also frag grenades in disguise
+ array<entity> grenades = GetProjectileArrayEx( "grenade_frag", TEAM_ANY, TEAM_ANY, Vector( 0, 0, 0 ), -1 )
+ foreach( grenade in grenades )
+ {
+ if ( grenade.GetOwner() != player )
+ continue
+
+ if ( grenade.ProjectileGetDamageSourceID() != eDamageSourceId.mp_weapon_satchel && grenade.ProjectileGetDamageSourceID() != eDamageSourceId.mp_weapon_proximity_mine )
+ continue
+
+ thread ExplodePlantedGrenadeAfterDelay( grenade, RandomFloatRange( 0.75, 0.95 ) )
+ }
+}
+
+function ExplodePlantedGrenadeAfterDelay( entity grenade, float delay )
+{
+ grenade.EndSignal( "OnDeath" )
+ grenade.EndSignal( "OnDestroy" )
+
+ float endTime = Time() + delay
+
+ while ( Time() < endTime )
+ {
+ EmitSoundOnEntity( grenade, DEFAULT_WARNING_SFX )
+ wait 0.1
+ }
+
+ grenade.GrenadeExplode( grenade.GetForwardVector() )
+}
+
+function Player_DetonateSatchels( entity player )
+{
+ #if SERVER
+ Assert( IsServer() )
+
+ array<entity> traps = GetScriptManagedEntArray( player.s.activeTrapArrayId )
+ traps.sort( CompareCreationReverse )
+ foreach ( index, satchel in traps )
+ {
+ if ( IsValidSatchel( satchel ) )
+ {
+
+ thread PROTO_ExplodeAfterDelay( satchel, index * 0.25 )
+ }
+ }
+ #endif
+}
+
+function IsValidSatchel( entity satchel )
+{
+ #if SERVER
+ if ( satchel.ProjectileGetWeaponClassName() != "mp_weapon_satchel" )
+ return false
+
+ if ( satchel.e.isDisabled == true )
+ return false
+
+ return true
+ #endif
+}
+
+#if SERVER
+function PROTO_ExplodeAfterDelay( entity satchel, float delay )
+{
+ satchel.EndSignal( "OnDestroy" )
+
+ #if MP
+ while ( !satchel.proj.isPlanted )
+ {
+ WaitFrame()
+ }
+ #endif
+
+ wait delay
+
+ satchel.GrenadeExplode( satchel.GetForwardVector() )
+}
+#endif
+
+
+#if DEV
+function ShowExplosionRadiusOnExplode( entity ent )
+{
+ ent.WaitSignal( "OnDestroy" )
+
+ float innerRadius = expect float( ent.GetWeaponInfoFileKeyField( "explosion_inner_radius" ) )
+ float outerRadius = expect float( ent.GetWeaponInfoFileKeyField( "explosionradius" ) )
+
+ vector org = ent.GetOrigin()
+ vector angles = Vector( 0, 0, 0 )
+ thread DebugDrawCircle( org, angles, innerRadius, 255, 255, 51, true, 3.0 )
+ thread DebugDrawCircle( org, angles, outerRadius, 255, 255, 255, true, 3.0 )
+}
+#endif // DEV
+
+#if SERVER
+// shared between nades, satchels and laser mines
+void function TrapExplodeOnDamage( entity trapEnt, int trapEntHealth = 50, float waitMin = 0.0, float waitMax = 0.0 )
+{
+ Assert( IsValid( trapEnt ), "Given trapEnt entity is not valid, fired from: " + trapEnt.ProjectileGetWeaponClassName() )
+ EndSignal( trapEnt, "OnDestroy" )
+
+ trapEnt.SetDamageNotifications( true )
+ var results //Really should be a struct
+ entity attacker
+ entity inflictor
+
+ while ( true )
+ {
+ if ( !IsValid( trapEnt ) )
+ return
+
+ results = WaitSignal( trapEnt, "OnDamaged" )
+ attacker = expect entity( results.activator )
+ inflictor = expect entity( results.inflictor )
+
+ if ( IsValid( inflictor ) && inflictor == trapEnt )
+ continue
+
+ bool shouldDamageTrap = false
+ if ( IsValid( attacker ) )
+ {
+ if ( trapEnt.proj.onlyAllowSmartPistolDamage )
+ {
+ if ( attacker.IsNPC() || attacker.IsPlayer() )
+ {
+ entity attackerWeapon = attacker.GetActiveWeapon()
+ if ( IsValid( attackerWeapon ) && WeaponIsSmartPistolVariant( attackerWeapon ) )
+ shouldDamageTrap = true
+ }
+ }
+ else
+ {
+ if ( trapEnt.GetTeam() == attacker.GetTeam() )
+ {
+ if ( trapEnt.GetOwner() != attacker )
+ shouldDamageTrap = false
+ else
+ shouldDamageTrap = !ProjectileIgnoresOwnerDamage( trapEnt )
+ }
+ else
+ {
+ shouldDamageTrap = true
+ }
+ }
+ }
+
+ if ( shouldDamageTrap )
+ trapEntHealth -= int ( results.value ) //TODO: This returns float even though it feels like it should return int
+
+ if ( trapEntHealth <= 0 )
+ break
+ }
+
+ if ( !IsValid( trapEnt ) )
+ return
+
+ inflictor = expect entity( results.inflictor ) // waiting on code feature to pass inflictor with OnDamaged signal results table
+
+ if ( waitMin >= 0 && waitMax > 0 )
+ {
+ float waitTime = RandomFloatRange( waitMin, waitMax )
+
+ if ( waitTime > 0 )
+ wait waitTime
+ }
+ else if ( IsValid( inflictor ) && (inflictor.IsProjectile() || (inflictor instanceof CWeaponX)) )
+ {
+ int dmgSourceID
+ if ( inflictor.IsProjectile() )
+ dmgSourceID = inflictor.ProjectileGetDamageSourceID()
+ else
+ dmgSourceID = inflictor.GetDamageSourceID()
+
+ string inflictorClass = GetObitFromDamageSourceID( dmgSourceID )
+
+ if ( inflictorClass in level.trapChainReactClasses )
+ {
+ // chain reaction delay
+ Wait( RandomFloatRange( 0.2, 0.275 ) )
+ }
+ }
+
+ if ( !IsValid( trapEnt ) )
+ return
+
+ if ( IsValid( attacker ) )
+ {
+ if ( attacker.IsPlayer() )
+ {
+ AddPlayerScoreForTrapDestruction( attacker, trapEnt )
+ trapEnt.SetOwner( attacker )
+ }
+ else
+ {
+ entity lastAttacker = GetLastAttacker( attacker )
+ if ( IsValid( lastAttacker ) )
+ {
+ // for chain explosions, figure out the attacking player that started the chain
+ trapEnt.SetOwner( lastAttacker )
+ }
+ }
+ }
+
+ trapEnt.GrenadeExplode( trapEnt.GetForwardVector() )
+}
+
+bool function ProjectileIgnoresOwnerDamage( entity projectile )
+{
+ var ignoreOwnerDamage = projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_ignore_owner_damage" )
+
+ if ( ignoreOwnerDamage == null )
+ return false
+
+ return ignoreOwnerDamage == 1
+}
+
+bool function WeaponIsSmartPistolVariant( entity weapon )
+{
+ var isSP = weapon.GetWeaponInfoFileKeyField( "is_smart_pistol" )
+
+ //printt( isSP )
+
+ if ( isSP == null )
+ return false
+
+ return ( isSP == 1 )
+}
+
+// NOTE: we should stop using this
+function TrapDestroyOnRoundEnd( entity player, entity trapEnt )
+{
+ trapEnt.EndSignal( "OnDestroy" )
+
+ svGlobal.levelEnt.WaitSignal( "ClearedPlayers" )
+
+ if ( IsValid( trapEnt ) )
+ trapEnt.Destroy()
+}
+
+function AddPlayerScoreForTrapDestruction( entity player, entity trapEnt )
+{
+ // don't get score for killing your own trap
+ if ( "originalOwner" in trapEnt.s && trapEnt.s.originalOwner == player )
+ return
+
+ string trapClass = trapEnt.ProjectileGetWeaponClassName()
+ if ( trapClass == "" )
+ return
+
+ string scoreEvent
+ if ( trapClass == "mp_weapon_satchel" )
+ scoreEvent = "Destroyed_Satchel"
+ else if ( trapClass == "mp_weapon_proximity_mine" )
+ scoreEvent = "Destored_Proximity_Mine"
+
+ if ( scoreEvent == "" )
+ return
+
+ AddPlayerScore( player, scoreEvent, trapEnt )
+}
+
+table function GetBulletPassThroughTargets( entity attacker, WeaponBulletHitParams hitParams )
+{
+ //HACK requires code later
+ table passThroughInfo = {
+ endPos = null
+ targetArray = []
+ }
+
+ TraceResults result
+ array<entity> ignoreEnts = [ attacker, hitParams.hitEnt ]
+
+ while ( true )
+ {
+ vector vec = ( hitParams.hitPos - hitParams.startPos ) * 1000
+ ArrayRemoveInvalid( ignoreEnts )
+ result = TraceLine( hitParams.startPos, vec, ignoreEnts, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE )
+
+ if ( result.hitEnt == svGlobal.worldspawn )
+ break
+
+ ignoreEnts.append( result.hitEnt )
+
+ if ( IsValidPassThroughTarget( result.hitEnt, attacker ) )
+ passThroughInfo.targetArray.append( result.hitEnt )
+ }
+ passThroughInfo.endPos = result.endPos
+
+ return passThroughInfo
+}
+#endif // SERVER
+
+bool function WeaponCanCrit( entity weapon )
+{
+ // player sometimes has no weapon during titan exit, mantle, etc...
+ if ( !weapon )
+ return false
+
+ return weapon.GetWeaponSettingBool( eWeaponVar.critical_hit )
+}
+
+
+#if SERVER
+bool function IsValidPassThroughTarget( entity target, entity attacker )
+{
+ //Tied to PassThroughHack function remove when supported by code.
+ if ( target == svGlobal.worldspawn )
+ return false
+
+ if ( !IsValid( target ) )
+ return false
+
+ if ( target.GetTeam() == attacker.GetTeam() )
+ return false
+
+ if ( target.GetTeam() != TEAM_IMC && target.GetTeam() != TEAM_MILITIA )
+ return false
+
+ return true
+}
+
+function PassThroughDamage( entity weapon, targetArray )
+{
+ //Tied to PassThroughHack function remove when supported by code.
+
+ int damageSourceID = weapon.GetDamageSourceID()
+ entity owner = weapon.GetWeaponOwner()
+
+ foreach ( ent in targetArray )
+ {
+ expect entity( ent )
+
+ float distanceToTarget = Distance( weapon.GetOrigin(), ent.GetOrigin() )
+ float damageToDeal = CalcWeaponDamage( owner, ent, weapon, distanceToTarget, 0 )
+
+ ent.TakeDamage( damageToDeal, owner, weapon.GetWeaponOwner(), { damageSourceId = damageSourceID } )
+ }
+}
+#endif // SERVER
+
+vector function GetVectorFromPositionToCrosshair( entity player, vector startPos )
+{
+ Assert( IsValid( player ) )
+
+ // See where we're looking
+ vector traceStart = player.EyePosition()
+ vector traceEnd = traceStart + ( player.GetViewVector() * 20000 )
+ local ignoreEnts = [ player ]
+ TraceResults traceResult = TraceLine( traceStart, traceEnd, ignoreEnts, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE )
+
+ // Return vec from startPos to where we are looking
+ vector vec = traceResult.endPos - startPos
+ vec = Normalize( vec )
+ return vec
+}
+
+/*
+function InitMissileForRandomDriftBasic( missile, startPos, startDir )
+{
+ missile.s.RandomFloatRange <- RandomFloat( 1.0 )
+ missile.s.startPos <- startPos
+ missile.s.startDir <- startDir
+}
+*/
+
+function InitMissileForRandomDriftForVortexHigh( entity missile, vector startPos, vector startDir )
+{
+ missile.InitMissileForRandomDrift( startPos, startDir, 8, 2.5, 0, 0, 100, 100 )
+}
+
+function InitMissileForRandomDriftForVortexLow( entity missile, vector startPos, vector startDir )
+{
+ missile.InitMissileForRandomDrift( startPos, startDir, 0.3, 0.085, 0, 0, 0.5, 0.5 )
+}
+
+/*
+function InitMissileForRandomDrift( missile, startPos, startDir )
+{
+ InitMissileForRandomDriftBasic( missile, startPos, startDir )
+
+ missile.s.drift_windiness <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_drift_windiness" )
+ missile.s.drift_intensity <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_drift_intensity" )
+
+ missile.s.straight_time_min <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_straight_time_min" )
+ missile.s.straight_time_max <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_straight_time_max" )
+
+ missile.s.straight_radius_min <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_straight_radius_min" )
+ if ( missile.s.straight_radius_min < 1 )
+ missile.s.straight_radius_min = 1
+ missile.s.straight_radius_max <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_straight_radius_max" )
+ if ( missile.s.straight_radius_max < 1 )
+ missile.s.straight_radius_max = 1
+}
+
+function SmoothRandom( x )
+{
+ return 0.25 * (sin(x) + sin(x * 0.762) + sin(x * 0.363) + sin(x * 0.084))
+}
+
+function MissileRandomDrift( timeElapsed, timeStep, windiness, intensity )
+{
+ // This function makes the missile go in a random direction.
+ // Windiness is how frequently the missile changes direction.
+ // Intensity is how strongly the missile steers in the direction it has chosen.
+
+ local sampleTime = timeElapsed - timeStep * 0.5
+
+ intensity *= timeStep
+
+ local offset = self.s.RandomFloatRange * 1000
+
+ local offsetx = intensity * SmoothRandom( offset + sampleTime * windiness )
+ local offsety = intensity * SmoothRandom( offset * 2 + 100 + sampleTime * windiness )
+
+ local right = self.GetRightVector()
+ local up = self.GetUpVector()
+
+ //DebugDrawLine( self.GetOrigin(), self.GetOrigin() + right * 100, 255,255,255, true, 0 )
+ //DebugDrawLine( self.GetOrigin(), self.GetOrigin() + up * 100, 255,128,255, true, 0 )
+
+ local dir = self.GetVelocity()
+ local speed = Length( dir )
+ dir = Normalize( dir )
+ dir += right * offsetx
+ dir += up * offsety
+ dir = Normalize( dir )
+ dir *= speed
+
+ return dir
+}
+
+// designed to be called every frame (GetProjectileVelocity callback) on projectiles that are flying through the air
+function ApplyMissileControlledDrift( missile, timeElapsed, timeStep )
+{
+ // If we have a target, don't do anything fancy; just let code do the homing behavior
+ if ( missile.GetMissileTarget() )
+ return missile.GetVelocity()
+
+ local s = missile.s
+ return MissileControlledDrift( timeElapsed, timeStep, s.drift_windiness, s.drift_intensity, s.straight_time_min, s.straight_time_max, s.straight_radius_min, s.straight_radius_max )
+}
+
+function MissileControlledDrift( timeElapsed, timeStep, windiness, intensity, pathTimeMin, pathTimeMax, pathRadiusMin, pathRadiusMax )
+{
+ // Start with random drift.
+ local vel = MissileRandomDrift( timeElapsed, timeStep, windiness, intensity )
+
+ // Straighten our velocity back along our original path if we're below pathTimeMax.
+ // Path time is how long it tries to stay on a straight path.
+ // Path radius is how far it can get from its straight path.
+ if ( timeElapsed < pathTimeMax )
+ {
+ local org = self.GetOrigin()
+ local alongPathLen = self.s.startDir.Dot( org - self.s.startPos )
+ local alongPathPos = self.s.startPos + self.s.startDir * alongPathLen
+ local offPathOffset = org - alongPathPos
+ local pathDist = Length( offPathOffset )
+
+ local speed = Length( vel )
+
+ local lerp = 1
+ if ( timeElapsed > pathTimeMin )
+ lerp = 1.0 - (timeElapsed - pathTimeMin) / (pathTimeMax - pathTimeMin)
+
+ local pathRadius = pathRadiusMax + (pathRadiusMin - pathRadiusMax) * lerp
+
+ // This circle shows the radius the missile is allowed to be in.
+ //if ( IsServer() )
+ // DebugDrawCircle( alongPathPos, VectorToAngles( AnglesToUp( VectorToAngles( self.s.startDir ) ) ), pathRadius, 255,255,255, true, 0.0 )
+
+ local backToPathVel = offPathOffset * -1
+ // Cap backToPathVel at speed
+ if ( pathDist > pathRadius )
+ backToPathVel *= speed / pathDist
+ else
+ backToPathVel *= speed / pathRadius
+
+ if ( pathDist < pathRadius )
+ {
+ backToPathVel += self.s.startDir * (speed * (1.0 - pathDist / pathRadius))
+ }
+
+ //DebugDrawLine( org, org + vel * 0.1, 255,255,255, true, 0 )
+ //DebugDrawLine( org, org + backToPathVel * intensity * lerp * 0.1, 128,255,128, true, 0 )
+
+ vel += backToPathVel * (intensity * timeStep)
+ vel = Normalize( vel )
+ vel *= speed
+ }
+
+ return vel
+}
+*/
+
+#if SERVER
+function ClusterRocket_Detonate( entity rocket, vector normal )
+{
+ entity owner = rocket.GetOwner()
+ if ( !IsValid( owner ) )
+ return
+
+ int count
+ float duration
+ float range
+
+ array mods = rocket.ProjectileGetMods()
+ if ( mods.contains( "pas_northstar_cluster" ) )
+ {
+ count = CLUSTER_ROCKET_BURST_COUNT_BURN
+ duration = PAS_NORTHSTAR_CLUSTER_ROCKET_DURATION
+ range = CLUSTER_ROCKET_BURST_RANGE * 1.5
+ }
+ else
+ {
+ count = CLUSTER_ROCKET_BURST_COUNT
+ duration = CLUSTER_ROCKET_DURATION
+ range = CLUSTER_ROCKET_BURST_RANGE
+ }
+
+ if ( mods.contains( "fd_twin_cluster" ) )
+ {
+ count = int( count * 0.7 )
+ duration *= 0.7
+ }
+ PopcornInfo popcornInfo
+
+ popcornInfo.weaponName = "mp_titanweapon_dumbfire_rockets"
+ popcornInfo.weaponMods = mods
+ popcornInfo.damageSourceId = eDamageSourceId.mp_titanweapon_dumbfire_rockets
+ popcornInfo.count = count
+ popcornInfo.delay = CLUSTER_ROCKET_BURST_DELAY
+ popcornInfo.offset = CLUSTER_ROCKET_BURST_OFFSET
+ popcornInfo.range = range
+ popcornInfo.normal = normal
+ popcornInfo.duration = duration
+ popcornInfo.groupSize = CLUSTER_ROCKET_BURST_GROUP_SIZE
+ popcornInfo.hasBase = true
+
+ thread StartClusterExplosions( rocket, owner, popcornInfo, CLUSTER_ROCKET_FX_TABLE )
+}
+
+
+function StartClusterExplosions( entity projectile, entity owner, PopcornInfo popcornInfo, customFxTable = null )
+{
+ Assert( IsValid( owner ) )
+ owner.EndSignal( "OnDestroy" )
+
+ string weaponName = popcornInfo.weaponName
+ float innerRadius
+ float outerRadius
+ int explosionDamage
+ int explosionDamageHeavyArmor
+
+ innerRadius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosion_inner_radius )
+ outerRadius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosionradius )
+ if ( owner.IsPlayer() )
+ {
+ explosionDamage = projectile.GetProjectileWeaponSettingInt( eWeaponVar.explosion_damage )
+ explosionDamageHeavyArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.explosion_damage_heavy_armor )
+ }
+ else
+ {
+ explosionDamage = projectile.GetProjectileWeaponSettingInt( eWeaponVar.npc_explosion_damage )
+ explosionDamageHeavyArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.npc_explosion_damage_heavy_armor )
+ }
+
+ local explosionDelay = projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_explosion_delay" )
+
+ if ( owner.IsPlayer() )
+ owner.EndSignal( "OnDestroy" )
+
+ vector origin = projectile.GetOrigin()
+
+ vector rotateFX = Vector( 90,0,0 )
+ entity placementHelper = CreateScriptMover()
+ placementHelper.SetOrigin( origin )
+ placementHelper.SetAngles( VectorToAngles( popcornInfo.normal ) )
+ SetTeam( placementHelper, owner.GetTeam() )
+
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ Remote_CallFunction_NonReplay( player, "SCB_AddGrenadeIndicatorForEntity", owner.GetTeam(), owner.GetEncodedEHandle(), placementHelper.GetEncodedEHandle(), outerRadius )
+ }
+
+ int particleSystemIndex = GetParticleSystemIndex( CLUSTER_BASE_FX )
+ int attachId = placementHelper.LookupAttachment( "REF" )
+ entity fx
+
+ if ( popcornInfo.hasBase )
+ {
+ fx = StartParticleEffectOnEntity_ReturnEntity( placementHelper, particleSystemIndex, FX_PATTACH_POINT_FOLLOW, attachId )
+ EmitSoundOnEntity( placementHelper, "Explo_ThermiteGrenade_Impact_3P" ) // TODO: wants a custom sound
+ }
+
+ OnThreadEnd(
+ function() : ( fx, placementHelper )
+ {
+ if ( IsValid( fx ) )
+ EffectStop( fx )
+ placementHelper.Destroy()
+ }
+ )
+
+ if ( explosionDelay )
+ wait explosionDelay
+
+ waitthread ClusterRocketBursts( origin, explosionDamage, explosionDamageHeavyArmor, innerRadius, outerRadius, owner, popcornInfo, customFxTable )
+
+ if ( IsValid( projectile ) )
+ projectile.Destroy()
+}
+
+
+//------------------------------------------------------------
+// ClusterRocketBurst() - does a "popcorn airburst" explosion effect over time around the origin. Total distance is based on popRangeBase
+// - returns the entity in case you want to parent it
+//------------------------------------------------------------
+function ClusterRocketBursts( vector origin, int damage, int damageHeavyArmor, float innerRadius, float outerRadius, entity owner, PopcornInfo popcornInfo, customFxTable = null )
+{
+ owner.EndSignal( "OnDestroy" )
+
+ // this ent remembers the weapon mods
+ entity clusterExplosionEnt = CreateEntity( "info_target" )
+ DispatchSpawn( clusterExplosionEnt )
+
+ if ( popcornInfo.weaponMods.len() > 0 )
+ clusterExplosionEnt.s.weaponMods <- popcornInfo.weaponMods
+
+ clusterExplosionEnt.SetOwner( owner )
+ clusterExplosionEnt.SetOrigin( origin )
+
+ AI_CreateDangerousArea_Static( clusterExplosionEnt, null, outerRadius, TEAM_INVALID, true, true, origin )
+
+ OnThreadEnd(
+ function() : ( clusterExplosionEnt )
+ {
+ clusterExplosionEnt.Destroy()
+ }
+ )
+
+ // No Damage - Only Force
+ // Push players
+ // Test LOS before pushing
+ int flags = 11
+ // create a blast that knocks pilots out of the way
+ CreatePhysExplosion( origin, outerRadius, PHYS_EXPLOSION_LARGE, flags )
+
+ int count = popcornInfo.groupSize
+ for ( int index = 0; index < count; index++ )
+ {
+ thread ClusterRocketBurst( clusterExplosionEnt, origin, damage, damageHeavyArmor, innerRadius, outerRadius, owner, popcornInfo, customFxTable )
+ WaitFrame()
+ }
+
+ wait CLUSTER_ROCKET_DURATION
+}
+
+function ClusterRocketBurst( entity clusterExplosionEnt, vector origin, damage, damageHeavyArmor, innerRadius, outerRadius, entity owner, PopcornInfo popcornInfo, customFxTable = null )
+{
+ clusterExplosionEnt.EndSignal( "OnDestroy" )
+ Assert( IsValid( owner ), "ClusterRocketBurst had invalid owner" )
+
+ // first explosion always happens where you fired
+ //int eDamageSource = popcornInfo.damageSourceId
+ int numBursts = popcornInfo.count
+ float popRangeBase = popcornInfo.range
+ float popDelayBase = popcornInfo.delay
+ float popDelayRandRange = popcornInfo.offset
+ float duration = popcornInfo.duration
+ int groupSize = popcornInfo.groupSize
+
+ int counter = 0
+ vector randVec
+ float randRangeMod
+ float popRange
+ vector popVec
+ vector popOri = origin
+ float popDelay
+ float colTrace
+
+ float burstDelay = duration / ( numBursts / groupSize )
+
+ vector clusterBurstOrigin = origin + (popcornInfo.normal * 8.0)
+ entity clusterBurstEnt = CreateClusterBurst( clusterBurstOrigin )
+
+ OnThreadEnd(
+ function() : ( clusterBurstEnt )
+ {
+ if ( IsValid( clusterBurstEnt ) )
+ {
+ foreach ( fx in clusterBurstEnt.e.fxArray )
+ {
+ if ( IsValid( fx ) )
+ fx.Destroy()
+ }
+ clusterBurstEnt.Destroy()
+ }
+ }
+ )
+
+ while ( IsValid( clusterBurstEnt ) && counter <= numBursts / popcornInfo.groupSize )
+ {
+ randVec = RandomVecInDome( popcornInfo.normal )
+ randRangeMod = RandomFloat( 1.0 )
+ popRange = popRangeBase * randRangeMod
+ popVec = randVec * popRange
+ popOri = origin + popVec
+ popDelay = popDelayBase + RandomFloatRange( -popDelayRandRange, popDelayRandRange )
+
+ colTrace = TraceLineSimple( origin, popOri, null )
+ if ( colTrace < 1 )
+ {
+ popVec = popVec * colTrace
+ popOri = origin + popVec
+ }
+
+ clusterBurstEnt.SetOrigin( clusterBurstOrigin )
+
+ vector velocity = GetVelocityForDestOverTime( clusterBurstEnt.GetOrigin(), popOri, burstDelay - popDelay )
+ clusterBurstEnt.SetVelocity( velocity )
+
+ clusterBurstOrigin = popOri
+
+ counter++
+
+ wait burstDelay - popDelay
+
+ Explosion(
+ clusterBurstOrigin,
+ owner,
+ clusterExplosionEnt,
+ damage,
+ damageHeavyArmor,
+ innerRadius,
+ outerRadius,
+ SF_ENVEXPLOSION_NOSOUND_FOR_ALLIES,
+ clusterBurstOrigin,
+ damage,
+ damageTypes.explosive,
+ popcornInfo.damageSourceId,
+ customFxTable )
+ }
+}
+
+
+entity function CreateClusterBurst( vector origin )
+{
+ entity prop_physics = CreateEntity( "prop_physics" )
+ prop_physics.SetValueForModelKey( $"models/weapons/bullets/projectile_rocket.mdl" )
+ prop_physics.kv.spawnflags = 4 // 4 = SF_PHYSPROP_DEBRIS
+ prop_physics.kv.fadedist = 2000
+ prop_physics.kv.renderamt = 255
+ prop_physics.kv.rendercolor = "255 255 255"
+ prop_physics.kv.CollisionGroup = TRACE_COLLISION_GROUP_DEBRIS
+
+ prop_physics.kv.minhealthdmg = 9999
+ prop_physics.kv.nodamageforces = 1
+ prop_physics.kv.inertiaScale = 1.0
+
+ prop_physics.SetOrigin( origin )
+ DispatchSpawn( prop_physics )
+ prop_physics.SetModel( $"models/weapons/grenades/m20_f_grenade.mdl" )
+
+ entity fx = PlayFXOnEntity( $"P_wpn_dumbfire_burst_trail", prop_physics )
+ prop_physics.e.fxArray.append( fx )
+
+ return prop_physics
+}
+#endif // SERVER
+
+vector function GetVelocityForDestOverTime( vector startPoint, vector endPoint, float duration )
+{
+ const GRAVITY = 750
+
+ float Vox = (endPoint.x - startPoint.x) / duration
+ float Voy = (endPoint.y - startPoint.y) / duration
+ float Voz = (endPoint.z + 0.5 * GRAVITY * duration * duration - startPoint.z) / duration
+
+ return Vector( Vox, Voy, Voz )
+}
+
+vector function GetPlayerVelocityForDestOverTime( vector startPoint, vector endPoint, float duration )
+{
+ // Same as above but accounts for player gravity setting not being 1.0
+
+ float gravityScale = expect float( GetPlayerSettingsFieldForClassName( DEFAULT_PILOT_SETTINGS, "gravityscale" ) )
+ float GRAVITY = 750 * gravityScale // adjusted for new gravity scale
+
+ float Vox = (endPoint.x - startPoint.x) / duration
+ float Voy = (endPoint.y - startPoint.y) / duration
+ float Voz = (endPoint.z + 0.5 * GRAVITY * duration * duration - startPoint.z) / duration
+
+ return Vector( Vox, Voy, Voz )
+}
+
+bool function HasLockedTarget( weapon )
+{
+ if ( weapon.SmartAmmo_IsEnabled() )
+ {
+ local targets = weapon.SmartAmmo_GetTargets()
+ if ( targets.len() > 0 )
+ {
+ foreach ( target in targets )
+ {
+ if ( target.fraction == 1 )
+ return true
+ }
+ }
+ }
+ return false
+}
+
+function CanWeaponShootWhileRunning( entity weapon )
+{
+ if ( "primary_fire_does_not_block_sprint" in weapon.s )
+ return weapon.s.primary_fire_does_not_block_sprint
+
+ if ( weapon.GetWeaponInfoFileKeyField( "primary_fire_does_not_block_sprint" ) == 1 )
+ {
+ weapon.s.primary_fire_does_not_block_sprint <- true
+ return true
+ }
+
+ weapon.s.primary_fire_does_not_block_sprint <- false
+ return false
+}
+
+#if CLIENT
+function ServerCallback_GuidedMissileDestroyed()
+{
+ entity player = GetLocalViewPlayer()
+
+ // guided missiles has not been updated to work with replays. added this if statement defensively just in case. - Roger
+ if ( !( "missileInFlight" in player.s ) )
+ return
+
+ player.s.missileInFlight = false
+}
+
+function ServerCallback_AirburstIconUpdate( toggle )
+{
+ entity player = GetLocalViewPlayer()
+ entity cockpit = player.GetCockpit()
+ if ( cockpit )
+ {
+ entity mainVGUI = cockpit.e.mainVGUI
+ if ( mainVGUI )
+ {
+ if ( toggle )
+ cockpit.s.offhandHud[OFFHAND_RIGHT].icon.SetImage( $"vgui/HUD/dpad_airburst_activate" )
+ else
+ cockpit.s.offhandHud[OFFHAND_RIGHT].icon.SetImage( $"vgui/HUD/dpad_airburst" )
+ }
+ }
+}
+
+bool function IsOwnerViewPlayerFullyADSed( entity weapon )
+{
+ entity owner = weapon.GetOwner()
+ if ( !IsValid( owner ) )
+ return false
+
+ if( !owner.IsPlayer() )
+ return false
+
+ if ( owner != GetLocalViewPlayer() )
+ return false
+
+ float zoomFrac = owner.GetZoomFrac()
+ if ( zoomFrac < 1.0 )
+ return false
+
+ return true
+
+}
+#endif // CLIENT
+
+array<entity> function FireExpandContractMissiles( entity weapon, WeaponPrimaryAttackParams attackParams, vector attackPos, vector attackDir, int damageType, int explosionDamageType, shouldPredict, int rocketsPerShot, missileSpeed, launchOutAng, launchOutTime, launchInAng, launchInTime, launchInLerpTime, launchStraightLerpTime, applyRandSpread, int burstFireCountOverride = -1, debugDrawPath = false )
+{
+ local missileVecs = GetExpandContractRocketTrajectories( weapon, attackParams.burstIndex, attackPos, attackDir, rocketsPerShot, launchOutAng, launchInAng, burstFireCountOverride )
+ entity owner = weapon.GetWeaponOwner()
+ array<entity> firedMissiles
+
+ vector missileEndPos = owner.EyePosition() + ( attackDir * 5000 )
+
+ for ( int i = 0; i < rocketsPerShot; i++ )
+ {
+ entity missile = weapon.FireWeaponMissile( attackPos, attackDir, missileSpeed, damageType, explosionDamageType, false, shouldPredict )
+
+ if ( missile )
+ {
+ /*
+ missile.s.flightData <- {
+ launchOutVec = missileVecs[i].outward,
+ launchOutTime = launchOutTime,
+ launchInLerpTime = launchInLerpTime,
+ launchInVec = missileVecs[i].inward,
+ launchInTime = launchInTime,
+ launchStraightLerpTime = launchStraightLerpTime,
+ endPos = missileEndPos,
+ applyRandSpread = applyRandSpread
+ }
+ */
+
+ missile.InitMissileExpandContract( missileVecs[i].outward, missileVecs[i].inward, launchOutTime, launchInLerpTime, launchInTime, launchStraightLerpTime, missileEndPos, applyRandSpread )
+
+ if ( IsServer() && debugDrawPath )
+ thread DebugDrawMissilePath( missile )
+
+ //InitMissileForRandomDrift( missile, attackPos, attackDir )
+ missile.InitMissileForRandomDriftFromWeaponSettings( attackPos, attackDir )
+
+ firedMissiles.append( missile )
+ }
+ }
+
+ return firedMissiles
+}
+
+array<entity> function FireExpandContractMissiles_S2S( entity weapon, WeaponPrimaryAttackParams attackParams, vector attackPos, vector attackDir, shouldPredict, int rocketsPerShot, missileSpeed, launchOutAng, launchOutTime, launchInAng, launchInTime, launchInLerpTime, launchStraightLerpTime, applyRandSpread, int burstFireCountOverride = -1, debugDrawPath = false )
+{
+ local missileVecs = GetExpandContractRocketTrajectories( weapon, attackParams.burstIndex, attackPos, attackDir, rocketsPerShot, launchOutAng, launchInAng, burstFireCountOverride )
+ entity owner = weapon.GetWeaponOwner()
+ array<entity> firedMissiles
+
+ vector missileEndPos = attackPos + ( attackDir * 5000 )
+
+ for ( int i = 0; i < rocketsPerShot; i++ )
+ {
+ entity missile = weapon.FireWeaponMissile( attackPos, attackDir, missileSpeed, DF_GIB | DF_IMPACT, damageTypes.explosive, false, shouldPredict )
+ missile.SetOrigin( attackPos )//HACK why do I have to do this?
+ if ( missile )
+ {
+ /*
+ missile.s.flightData <- {
+ launchOutVec = missileVecs[i].outward,
+ launchOutTime = launchOutTime,
+ launchInLerpTime = launchInLerpTime,
+ launchInVec = missileVecs[i].inward,
+ launchInTime = launchInTime,
+ launchStraightLerpTime = launchStraightLerpTime,
+ endPos = missileEndPos,
+ applyRandSpread = applyRandSpread
+ }
+ */
+
+ missile.InitMissileExpandContract( missileVecs[i].outward, missileVecs[i].inward, launchOutTime, launchInLerpTime, launchInTime, launchStraightLerpTime, missileEndPos, applyRandSpread )
+
+ if ( IsServer() && debugDrawPath )
+ thread DebugDrawMissilePath( missile )
+
+ //InitMissileForRandomDrift( missile, attackPos, attackDir )
+ missile.InitMissileForRandomDriftFromWeaponSettings( attackPos, attackDir )
+
+ firedMissiles.append( missile )
+ }
+ }
+
+ return firedMissiles
+}
+
+function GetExpandContractRocketTrajectories( entity weapon, int burstIndex, vector attackPos, vector attackDir, int rocketsPerShot, launchOutAng, launchInAng, int burstFireCount = -1 )
+{
+ bool DEBUG_DRAW_MATH = false
+
+ if ( burstFireCount == -1 )
+ burstFireCount = weapon.GetWeaponBurstFireCount()
+
+ local additionalRotation = ( ( 360.0 / rocketsPerShot ) / burstFireCount ) * burstIndex
+ //printt( "burstIndex:", burstIndex )
+ //printt( "rocketsPerShot:", rocketsPerShot )
+ //printt( "burstFireCount:", burstFireCount )
+
+ vector ang = VectorToAngles( attackDir )
+ vector forward = AnglesToForward( ang )
+ vector right = AnglesToRight( ang )
+ vector up = AnglesToUp( ang )
+
+ if ( DEBUG_DRAW_MATH )
+ DebugDrawLine( attackPos, attackPos + ( forward * 1000 ), 255, 0, 0, true, 30.0 )
+
+ // Create points on circle
+ float offsetAng = 360.0 / rocketsPerShot
+ for ( int i = 0; i < rocketsPerShot; i++ )
+ {
+ local a = offsetAng * i + additionalRotation
+ vector vec = Vector( 0, 0, 0 )
+ vec += up * deg_sin( a )
+ vec += right * deg_cos( a )
+
+ if ( DEBUG_DRAW_MATH )
+ DebugDrawLine( attackPos, attackPos + ( vec * 50 ), 10, 10, 10, true, 30.0 )
+ }
+
+ // Create missile points
+ vector x = right * deg_sin( launchOutAng )
+ vector y = up * deg_sin( launchOutAng )
+ vector z = forward * deg_cos( launchOutAng )
+ vector rx = right * deg_sin( launchInAng )
+ vector ry = up * deg_sin( launchInAng )
+ vector rz = forward * deg_cos( launchInAng )
+ local missilePoints = []
+ for ( int i = 0; i < rocketsPerShot; i++ )
+ {
+ local points = {}
+
+ // Outward vec
+ local a = offsetAng * i + additionalRotation
+ float s = deg_sin( a )
+ float c = deg_cos( a )
+ vector vecOut = z + x * c + y * s
+ vecOut = Normalize( vecOut )
+ points.outward <- vecOut
+
+ // Inward vec
+ vector vecIn = rz + rx * c + ry * s
+ points.inward <- vecIn
+
+ // Add to array
+ missilePoints.append( points )
+
+ if ( DEBUG_DRAW_MATH )
+ {
+ DebugDrawLine( attackPos, attackPos + ( vecOut * 50 ), 255, 255, 0, true, 30.0 )
+ DebugDrawLine( attackPos + vecOut * 50, attackPos + vecOut * 50 + ( vecIn * 50 ), 255, 0, 255, true, 30.0 )
+ }
+ }
+
+ return missilePoints
+}
+
+function DebugDrawMissilePath( entity missile )
+{
+ EndSignal( missile, "OnDestroy" )
+ vector lastPos = missile.GetOrigin()
+ while ( true )
+ {
+ WaitFrame()
+ if ( !IsValid( missile ) )
+ return
+ DebugDrawLine( lastPos, missile.GetOrigin(), 0, 255, 0, true, 20.0 )
+ lastPos = missile.GetOrigin()
+ }
+}
+
+
+function RegenerateOffhandAmmoOverTime( entity weapon, float rechargeTime, int maxAmmo, int offhandIndex )
+{
+ weapon.Signal( "RegenAmmo" )
+ weapon.EndSignal( "RegenAmmo" )
+ weapon.EndSignal( "OnDestroy" )
+
+ #if CLIENT
+ entity weaponOwner = weapon.GetWeaponOwner()
+ if ( IsValid( weaponOwner ) && weaponOwner.IsPlayer() )
+ {
+ entity cockpit = weaponOwner.GetCockpit()
+ if ( IsValid( cockpit ) )
+ {
+ cockpit.s.offhandHud[offhandIndex].bar.SetBarProgressSource( ProgressSource.PROGRESS_SOURCE_SCRIPTED )
+ cockpit.s.offhandHud[offhandIndex].bar.SetBarProgressRemap( 0.0, 1.0, 0.0, 1.0 )
+ cockpit.s.offhandHud[offhandIndex].bar.SetBarProgressAndRate( 1.0 / maxAmmo , 1 / ( rechargeTime * maxAmmo ) )
+ }
+ }
+ #endif
+
+ if ( !( "totalChargeTime" in weapon.s ) )
+ weapon.s.totalChargeTime <- rechargeTime
+
+ if ( !( "nextChargeTime" in weapon.s ) )
+ weapon.s.nextChargeTime <- null
+
+ for ( ;; )
+ {
+ weapon.s.nextChargeTime = rechargeTime + Time()
+
+ wait rechargeTime
+
+ if ( IsServer() )
+ {
+ int max = maxAmmo
+ int weaponMax = weapon.GetWeaponPrimaryClipCountMax()
+ if ( weaponMax < max )
+ max = weaponMax
+
+ int ammo = weapon.GetWeaponPrimaryClipCount()
+ if ( ammo < max )
+ weapon.SetWeaponPrimaryClipCount( ammo + 1 )
+ }
+ }
+}
+
+bool function IsPilotShotgunWeapon( string weaponName )
+{
+ return GetWeaponInfoFileKeyField_Global( weaponName, "weaponSubClass" ) == "shotgun"
+}
+
+array<string> function GetWeaponBurnMods( string weaponClassName )
+{
+ array<string> burnMods = []
+ array<string> mods = GetWeaponMods_Global( weaponClassName )
+ string prefix = "burn_mod"
+ foreach ( mod in mods )
+ {
+ if ( mod.find( prefix ) == 0 )
+ burnMods.append( mod )
+ }
+
+ return burnMods
+}
+
+int function TEMP_GetDamageFlagsFromProjectile( entity projectile )
+{
+ var damageFlagsString = projectile.ProjectileGetWeaponInfoFileKeyField( "damage_flags" )
+ if ( damageFlagsString == null )
+ return 0
+ expect string( damageFlagsString )
+
+ return TEMP_GetDamageFlagsFromString( damageFlagsString )
+}
+
+int function TEMP_GetDamageFlagsFromString( string damageFlagsString )
+{
+ int damageFlags = 0
+
+ array<string> damageFlagTokens = split( damageFlagsString, "|" )
+ foreach ( token in damageFlagTokens )
+ {
+ damageFlags = damageFlags | getconsttable()[strip(token)]
+ }
+
+ return damageFlags
+}
+
+#if SERVER
+function PROTO_InitTrackedProjectile( entity projectile )
+{
+ // HACK: accessing ProjectileGetWeaponInfoFileKeyField or ProjectileGetWeaponClassName during CodeCallback_OnSpawned causes a code assert
+ projectile.EndSignal( "OnDestroy" )
+ WaitFrame()
+
+ entity owner = projectile.GetOwner()
+
+ if ( !IsValid( owner ) || !owner.IsPlayer() )
+ return
+
+ int maxDeployed = projectile.GetProjectileWeaponSettingInt( eWeaponVar.projectile_max_deployed )
+ if ( maxDeployed != 0 )
+ {
+ AddToScriptManagedEntArray( owner.s.activeTrapArrayId, projectile )
+
+ array<entity> traps = GetScriptManagedEntArray( owner.s.activeTrapArrayId )
+ array<entity> sameTypeTrapEnts
+ foreach ( ent in traps )
+ {
+ if ( ent.ProjectileGetWeaponClassName() != projectile.ProjectileGetWeaponClassName() )
+ continue
+
+ sameTypeTrapEnts.append( ent )
+ }
+
+ int numToDestroy = sameTypeTrapEnts.len() - maxDeployed
+ if ( numToDestroy > 0 )
+ {
+ sameTypeTrapEnts.sort( CompareCreation )
+ foreach ( ent in sameTypeTrapEnts )
+ {
+ ent.Destroy()
+ numToDestroy--
+
+ if ( !numToDestroy )
+ break
+ }
+ }
+ }
+}
+
+
+function PROTO_CleanupTrackedProjectiles( entity player )
+{
+ array<entity> traps = GetScriptManagedEntArray( player.s.activeTrapArrayId )
+ foreach ( ent in traps )
+ {
+ ent.Destroy()
+ }
+}
+
+int function CompareCreation( entity a, entity b )
+{
+ if ( a.GetProjectileCreationTime() > b.GetProjectileCreationTime() )
+ return 1
+
+ return -1
+}
+
+int function CompareCreationReverse( entity a, entity b )
+{
+ if ( a.GetProjectileCreationTime() > b.GetProjectileCreationTime() )
+ return 1
+
+ return -1
+}
+
+void function PROTO_TrackedProjectile_OnPlayerRespawned( entity player )
+{
+ thread PROTO_TrackedProjectile_OnPlayerRespawned_Internal( player )
+}
+
+void function PROTO_TrackedProjectile_OnPlayerRespawned_Internal( entity player )
+{
+ player.EndSignal( "OnDeath" )
+
+ if ( player.s.inGracePeriod )
+ player.WaitSignal( "GracePeriodDone" )
+
+ entity ordnance = player.GetOffhandWeapon( OFFHAND_ORDNANCE )
+
+ array<entity> traps = GetScriptManagedEntArray( player.s.activeTrapArrayId )
+ foreach ( ent in traps )
+ {
+ if ( ordnance && ent.ProjectileGetWeaponClassName() == ordnance.GetWeaponClassName() )
+ continue
+
+ ent.Destroy()
+ }
+}
+
+function PROTO_PlayTrapLightEffect( entity ent, string tag, int team )
+{
+ asset ownerFx = ent.ProjectileGetWeaponInfoFileKeyFieldAsset( "trap_warning_owner_fx" )
+ if ( ownerFx != $"" )
+ {
+ entity ownerFxEnt = CreateServerEffect_Owner( ownerFx, ent.GetOwner() )
+ SetServerEffectControlPoint( ownerFxEnt, 0, FRIENDLY_COLOR )
+ StartServerEffectOnEntity( ownerFxEnt, ent, tag )
+ }
+
+ asset friendlyFx = ent.ProjectileGetWeaponInfoFileKeyFieldAsset( "trap_warning_friendly_fx" )
+ if ( friendlyFx != $"" )
+ {
+ entity friendlyFxEnt = CreateServerEffect_Friendly( friendlyFx, team )
+ SetServerEffectControlPoint( friendlyFxEnt, 0, FRIENDLY_COLOR_FX )
+ StartServerEffectOnEntity( friendlyFxEnt, ent, tag )
+ }
+
+ asset enemyFx = ent.ProjectileGetWeaponInfoFileKeyFieldAsset( "trap_warning_enemy_fx" )
+ if ( enemyFx != $"" )
+ {
+ entity enemyFxEnt = CreateServerEffect_Enemy( enemyFx, team )
+ SetServerEffectControlPoint( enemyFxEnt, 0, ENEMY_COLOR_FX )
+ StartServerEffectOnEntity( enemyFxEnt, ent, tag )
+ }
+}
+
+string ornull function GetCooldownBeepPrefix( weapon )
+{
+ var reloadBeepPrefix = weapon.GetWeaponInfoFileKeyField( "cooldown_sound_prefix" )
+ if ( reloadBeepPrefix == null )
+ return null
+
+ expect string( reloadBeepPrefix )
+
+ return reloadBeepPrefix
+}
+
+void function PROTO_DelayCooldown( entity weapon )
+{
+ weapon.s.nextCooldownTime = Time() + weapon.s.cooldownDelay
+}
+
+string function GetBeepSuffixForAmmo( int currentAmmo, int maxAmmo )
+{
+ float frac = float( currentAmmo ) / float( maxAmmo )
+
+ if ( frac >= 1.0 )
+ return "_full"
+
+ if ( frac >= 0.25 )
+ return ""
+
+ return "_low"
+}
+
+#endif //SERVER
+
+bool function PROTO_CanPlayerDeployWeapon( entity player )
+{
+ if ( player.IsPhaseShifted() )
+ return false
+
+ if ( player.ContextAction_IsActive() == true )
+ {
+ if ( player.IsZiplining() )
+ return true
+ else
+ return false
+ }
+
+ return true
+}
+
+#if SERVER
+void function PROTO_FlakCannonMissiles( entity projectile, float speed )
+{
+ projectile.EndSignal( "OnDestroy" )
+
+ float radius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosionradius )
+ vector velocity = projectile.GetVelocity()
+ vector currentPos = projectile.GetOrigin()
+ int team = projectile.GetTeam()
+
+ float waitTime = 0.1
+ float distanceInterval = speed * waitTime
+ int forwardDistanceChecks = int( ceil( distanceInterval / radius ) )
+ bool forceExplosion = false
+ while ( forceExplosion == false )
+ {
+ currentPos = projectile.GetOrigin()
+ for ( int i = 0; i < forwardDistanceChecks; i++ )
+ {
+ float frac = float( i ) / float (forwardDistanceChecks )
+ if ( PROTO_FlakCannon_HasNearbyEnemies( currentPos + velocity * waitTime * frac , team, radius ) )
+ {
+ if ( i == 0 )
+ {
+ forceExplosion = true
+ break
+ }
+ else
+ {
+ projectile.SetVelocity( velocity * ( frac - 0.05 ) )
+ break
+ }
+ }
+ }
+
+ if ( forceExplosion == false )
+ wait waitTime
+ }
+
+ projectile.MissileExplode()
+}
+
+bool function PROTO_FlakCannon_HasNearbyEnemies( vector origin, int team, float radius )
+{
+ float worldSpaceCenterBuffer = 200
+
+ array<entity> guys = GetPlayerArrayEx( "any", TEAM_ANY, team, origin, radius + worldSpaceCenterBuffer )
+ foreach ( guy in guys )
+ {
+ if ( IsAlive( guy ) && Distance( origin, guy.GetWorldSpaceCenter() ) < radius )
+ return true
+ }
+
+ array<entity> ai = GetNPCArrayEx( "any", TEAM_ANY, team, origin, radius + worldSpaceCenterBuffer )
+ foreach ( guy in ai )
+ {
+ if ( IsAlive( guy ) && Distance( origin, guy.GetWorldSpaceCenter() ) < radius )
+ return true
+ }
+
+ return false
+}
+#endif // #if SERVER
+
+void function GiveEMPStunStatusEffects( entity ent, float duration, float fadeoutDuration = 0.5, float slowTurn = EMP_SEVERITY_SLOWTURN, float slowMove = EMP_SEVERITY_SLOWMOVE)
+{
+ entity target = ent.IsTitan() ? ent.GetTitanSoul() : ent
+ int slowEffect = StatusEffect_AddTimed( target, eStatusEffect.turn_slow, slowTurn, duration, fadeoutDuration )
+ int turnEffect = StatusEffect_AddTimed( target, eStatusEffect.move_slow, slowMove, duration, fadeoutDuration )
+
+ #if SERVER
+ if ( ent.IsPlayer() )
+ {
+ ent.p.empStatusEffectsToClearForPhaseShift.append( slowEffect )
+ ent.p.empStatusEffectsToClearForPhaseShift.append( turnEffect )
+ }
+ #endif
+}
+
+#if DEV
+string ornull function FindEnumNameForValue( table searchTable, int searchVal )
+{
+ foreach( string keyname, int value in searchTable )
+ {
+ if ( value == searchVal )
+ return keyname;
+ }
+ return null
+}
+
+void function DevPrintAllStatusEffectsOnEnt( entity ent )
+{
+ printt( "Effects:", ent )
+ array<float> effects = StatusEffect_GetAll( ent )
+ int length = effects.len()
+ int found = 0;
+ for ( int idx = 0; idx < length; idx++ )
+ {
+ float severity = effects[idx];
+ if ( severity <= 0.0 )
+ continue
+ string ornull name = FindEnumNameForValue( eStatusEffect, idx )
+ Assert( name )
+ expect string( name )
+ printt( " eStatusEffect." + name + ": " + severity )
+ found++;
+ }
+ printt( found + " effects active.\n" );
+}
+#endif // #if DEV
+
+array<entity> function GetPrimaryWeapons( entity player )
+{
+ array<entity> primaryWeapons
+ array<entity> weapons = player.GetMainWeapons()
+ foreach ( weaponEnt in weapons )
+ {
+ int weaponType = weaponEnt.GetWeaponType()
+ if ( weaponType == WT_SIDEARM || weaponType == WT_ANTITITAN )
+ continue;
+
+ primaryWeapons.append( weaponEnt )
+ }
+ return primaryWeapons
+}
+
+array<entity> function GetSidearmWeapons( entity player )
+{
+ array<entity> sidearmWeapons
+ array<entity> weapons = player.GetMainWeapons()
+ foreach ( weaponEnt in weapons )
+ {
+ if ( weaponEnt.GetWeaponType() != WT_SIDEARM )
+ continue
+
+ sidearmWeapons.append( weaponEnt )
+ }
+ return sidearmWeapons
+}
+
+array<entity> function GetATWeapons( entity player )
+{
+ array<entity> atWeapons
+ array<entity> weapons = player.GetMainWeapons()
+ foreach ( weaponEnt in weapons )
+ {
+ if ( weaponEnt.GetWeaponType() != WT_ANTITITAN )
+ continue
+
+ atWeapons.append( weaponEnt )
+ }
+ return atWeapons
+}
+
+entity function GetPlayerFromTitanWeapon( entity weapon )
+{
+ entity titan = weapon.GetWeaponOwner()
+ entity player
+
+ if ( titan == null )
+ return null
+
+ if ( !titan.IsPlayer() )
+ player = titan.GetBossPlayer()
+ else
+ player = titan
+
+ return player
+}
+
+
+const asset CHARGE_SHOT_PROJECTILE = $"models/weapons/bullets/temp_triple_threat_projectile_large.mdl"
+
+const asset CHARGE_EFFECT_1P = $"P_ordnance_charge_st_FP" // $"P_wpn_defender_charge_FP"
+const asset CHARGE_EFFECT_3P = $"P_ordnance_charge_st" // $"P_wpn_defender_charge"
+const asset CHARGE_EFFECT_DLIGHT = $"defender_charge_CH_dlight"
+
+const string CHARGE_SOUND_WINDUP_1P = "Weapon_ChargeRifle_WindUp_1P"
+const string CHARGE_SOUND_WINDUP_3P = "Weapon_ChargeRifle_WindUp_3P"
+const string CHARGE_SOUND_WINDDOWN_1P = "Weapon_ChargeRifle_WindDown_1P"
+const string CHARGE_SOUND_WINDDOWN_3P = "Weapon_ChargeRifle_WindDown_3P"
+
+void function ChargeBall_Precache()
+{
+#if SERVER
+ PrecacheModel( CHARGE_SHOT_PROJECTILE )
+ PrecacheEffect( CHARGE_EFFECT_1P )
+ PrecacheEffect( CHARGE_EFFECT_3P )
+#endif // #if SERVER
+}
+
+void function ChargeBall_FireProjectile( entity weapon, vector position, vector direction, bool shouldPredict )
+{
+ weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 )
+
+ entity owner = weapon.GetWeaponOwner()
+ const float MISSILE_SPEED = 1200.0
+ const int CONTACT_DAMAGE_TYPES = (damageTypes.projectileImpact | DF_DOOM_FATALITY)
+ const int EXPLOSION_DAMAGE_TYPES = damageTypes.explosive
+ const bool DO_POPUP = false
+
+ if ( shouldPredict )
+ {
+ entity missile = weapon.FireWeaponMissile( position, direction, MISSILE_SPEED, CONTACT_DAMAGE_TYPES, EXPLOSION_DAMAGE_TYPES, DO_POPUP, shouldPredict )
+ if ( missile )
+ {
+ EmitSoundOnEntity( owner, "ShoulderRocket_Cluster_Fire_3P" )
+ missile.SetModel( CHARGE_SHOT_PROJECTILE )
+#if CLIENT
+ const ROCKETEER_MISSILE_EXPLOSION = $"xo_exp_death"
+ const ROCKETEER_MISSILE_SHOULDER_FX = $"wpn_mflash_xo_rocket_shoulder_FP"
+ entity owner = weapon.GetWeaponOwner()
+ vector origin = owner.OffsetPositionFromView( Vector(0, 0, 0), Vector(25, -25, 15) )
+ vector angles = owner.CameraAngles()
+ StartParticleEffectOnEntityWithPos( owner, GetParticleSystemIndex( ROCKETEER_MISSILE_SHOULDER_FX ), FX_PATTACH_EYES_FOLLOW, -1, origin, angles )
+#else // #if CLIENT
+ missile.SetProjectileImpactDamageOverride( 1440 )
+ missile.kv.damageSourceId = eDamageSourceId.charge_ball
+#endif // #else // #if CLIENT
+ }
+ }
+}
+
+bool function ChargeBall_ChargeBegin( entity weapon, string tagName )
+{
+#if CLIENT
+ if ( InPrediction() && !IsFirstTimePredicted() )
+ return true
+#endif // #if CLIENT
+
+ weapon.w.statusEffects.append( StatusEffect_AddEndless( weapon.GetWeaponOwner(), eStatusEffect.move_slow, 0.6 ) )
+ weapon.w.statusEffects.append( StatusEffect_AddEndless( weapon.GetWeaponOwner(), eStatusEffect.turn_slow, 0.35 ) )
+
+ weapon.PlayWeaponEffect( CHARGE_EFFECT_1P, CHARGE_EFFECT_3P, tagName )
+ weapon.PlayWeaponEffect( $"", CHARGE_EFFECT_DLIGHT, tagName )
+
+#if SERVER
+ StopSoundOnEntity( weapon, CHARGE_SOUND_WINDDOWN_3P )
+ entity weaponOwner = weapon.GetWeaponOwner()
+ if ( IsValid( weaponOwner ) )
+ {
+ if ( weaponOwner.IsPlayer() )
+ EmitSoundOnEntityExceptToPlayer( weapon, weaponOwner, CHARGE_SOUND_WINDUP_3P )
+ else
+ EmitSoundOnEntity( weapon, CHARGE_SOUND_WINDUP_3P )
+ }
+#else
+ StopSoundOnEntity( weapon, CHARGE_SOUND_WINDDOWN_1P )
+ EmitSoundOnEntity( weapon, CHARGE_SOUND_WINDUP_1P )
+#endif
+
+ return true
+}
+
+void function ChargeBall_ChargeEnd( entity weapon )
+{
+#if CLIENT
+ if ( InPrediction() && !IsFirstTimePredicted() )
+ return
+#endif
+
+ if ( IsValid( weapon.GetWeaponOwner() ) )
+ {
+ #if CLIENT
+ if ( InPrediction() && IsFirstTimePredicted() )
+ {
+ #endif
+
+ foreach ( effect in weapon.w.statusEffects )
+ {
+ StatusEffect_Stop( weapon.GetWeaponOwner(), effect )
+ }
+
+ #if CLIENT
+ }
+ #endif
+ }
+
+#if SERVER
+ StopSoundOnEntity( weapon, CHARGE_SOUND_WINDUP_3P )
+ entity weaponOwner = weapon.GetWeaponOwner()
+ if ( IsValid( weaponOwner ) )
+ {
+ if ( weaponOwner.IsPlayer() )
+ EmitSoundOnEntityExceptToPlayer( weapon, weaponOwner, CHARGE_SOUND_WINDDOWN_3P )
+ else
+ EmitSoundOnEntity( weapon, CHARGE_SOUND_WINDDOWN_3P )
+ }
+#else
+ StopSoundOnEntity( weapon, CHARGE_SOUND_WINDUP_1P )
+ EmitSoundOnEntity( weapon, CHARGE_SOUND_WINDDOWN_1P )
+#endif
+
+ ChargeBall_StopChargeEffects( weapon )
+}
+
+void function ChargeBall_StopChargeEffects( entity weapon )
+{
+ Assert( IsValid( weapon ) )
+ // weapon.StopWeaponEffect( CHARGE_EFFECT_1P, CHARGE_EFFECT_3P )
+ // weapon.StopWeaponEffect( CHARGE_EFFECT_3P, CHARGE_EFFECT_1P )
+ // weapon.StopWeaponEffect( CHARGE_EFFECT_DLIGHT, CHARGE_EFFECT_DLIGHT )
+ thread HACK_Deplayed_ChargeBall_StopChargeEffects( weapon )
+}
+
+void function HACK_Deplayed_ChargeBall_StopChargeEffects( entity weapon )
+{
+ weapon.EndSignal( "OnDestroy" )
+ wait 0.2
+ weapon.StopWeaponEffect( CHARGE_EFFECT_1P, CHARGE_EFFECT_3P )
+ weapon.StopWeaponEffect( CHARGE_EFFECT_3P, CHARGE_EFFECT_1P )
+ weapon.StopWeaponEffect( CHARGE_EFFECT_DLIGHT, CHARGE_EFFECT_DLIGHT )
+}
+
+float function ChargeBall_GetChargeTime()
+{
+ return 1.05
+}
+
+#if SERVER
+void function GivePlayerAmpedWeapon( entity player, string weaponName )
+{
+ array<entity> weapons = player.GetMainWeapons()
+ int numWeapons = weapons.len()
+ if ( numWeapons == 0 )
+ return
+
+ //Figure out what weapon to take away.
+ //This is more complicated than it should be because of rules of what weapons can be in what slots, e.g. your anti-titan weapon can't be replaced by non anti-titan weapons
+ if ( HasWeapon( player, weaponName ) )
+ {
+ //Simplest case:
+ //Take away the currently existing version of the weapon you already have.
+ player.TakeWeaponNow( weaponName )
+ }
+ else
+ {
+ bool ampedWeaponIsAntiTitan = GetWeaponInfoFileKeyField_Global( weaponName, "weaponType" ) == "anti_titan"
+ if ( ampedWeaponIsAntiTitan )
+ {
+ foreach( weapon in weapons )
+ {
+ string currentWeaponClassName = weapon.GetWeaponClassName()
+ if ( GetWeaponInfoFileKeyField_Global( currentWeaponClassName, "weaponType" ) == "anti_titan" )
+ {
+ player.TakeWeaponNow( currentWeaponClassName )
+ break
+ }
+ }
+
+ unreachable //We had no anti-titan weapon? Shouldn't ever be possible
+
+ }
+ else
+ {
+ string currentActiveWeaponClassName = player.GetActiveWeapon().GetWeaponClassName()
+ if ( ShouldReplaceWeaponInFirstSlot( player, currentActiveWeaponClassName ) )
+ {
+ //Current weapon is anti_titan, but amped weapon we are trying to give is not. Just replace the weapon that is in the first slot.
+ //Assumes that weapon in first slot is not an anti-titan weapon
+ //We could get even fancier and look to see if the amped weapon is a primary weapon or a sidearm and replace the slot accordingly, but
+ //that makes it more complicated, plus there are cases where you can have no primary weapons/no side arms etc
+ string firstWeaponClassName = weapons[ 0 ].GetWeaponClassName()
+ Assert( GetWeaponInfoFileKeyField_Global( firstWeaponClassName, "weaponType" ) != "anti_titan" )
+ player.TakeWeaponNow( firstWeaponClassName )
+ }
+ else
+ {
+ player.TakeWeaponNow( currentActiveWeaponClassName )
+ }
+ }
+ }
+
+ array<string> burnMods = GetWeaponBurnMods( weaponName )
+ entity ampedWeapon = player.GiveWeapon( weaponName, burnMods )
+ ampedWeapon.SetWeaponPrimaryClipCount( ampedWeapon.GetWeaponPrimaryClipCountMax() ) //Needed for weapons that give a mod with extra clip size
+}
+
+bool function ShouldReplaceWeaponInFirstSlot( entity player, string currentActiveWeaponClassName )
+{
+ if ( GetWeaponInfoFileKeyField_Global( currentActiveWeaponClassName, "weaponType" ) == "anti_titan" ) //Active weapon is anti-titan weapon. Can't replace anti-titan weapon slot with non-anti-titan weapon
+ return true
+
+ if ( currentActiveWeaponClassName == player.GetOffhandWeapon( OFFHAND_ORDNANCE ).GetWeaponClassName() )
+ return true
+
+ return false
+
+}
+
+void function GivePlayerAmpedWeaponAndSetAsActive( entity player, string weaponName )
+{
+ GivePlayerAmpedWeapon( player, weaponName )
+ player.SetActiveWeaponByName( weaponName )
+}
+
+void function ReplacePlayerOffhand( entity player, string offhandName, array<string> mods = [] )
+{
+ player.TakeOffhandWeapon( OFFHAND_SPECIAL )
+ player.GiveOffhandWeapon( offhandName, OFFHAND_SPECIAL, mods )
+}
+
+void function ReplacePlayerOrdnance( entity player, string ordnanceName, array<string> mods = [] )
+{
+ player.TakeOffhandWeapon( OFFHAND_ORDNANCE )
+ player.GiveOffhandWeapon( ordnanceName, OFFHAND_ORDNANCE, mods )
+}
+
+void function PAS_CooldownReduction_OnKill( entity victim, entity attacker, var damageInfo )
+{
+ if ( !IsAlive( attacker ) || !IsPilot( attacker ) )
+ return
+
+ array<string> weaponMods = GetWeaponModsFromDamageInfo( damageInfo )
+
+ if ( GetCurrentPlaylistVarInt( "featured_mode_tactikill", 0 ) > 0 )
+ {
+ entity weapon = attacker.GetOffhandWeapon( OFFHAND_LEFT )
+
+ switch ( GetWeaponInfoFileKeyField_Global( weapon.GetWeaponClassName(), "cooldown_type" ) )
+ {
+ case "grapple":
+ attacker.SetSuitGrapplePower( attacker.GetSuitGrapplePower() + 100 )
+ break
+
+ case "ammo":
+ case "ammo_instant":
+ case "ammo_deployed":
+ case "ammo_timed":
+ int maxAmmo = weapon.GetWeaponPrimaryClipCountMax()
+ weapon.SetWeaponPrimaryClipCountNoRegenReset( maxAmmo )
+ break
+
+ case "chargeFrac":
+ weapon.SetWeaponChargeFraction( 0 )
+ break
+
+ // case "mp_ability_ground_slam":
+ // break
+
+ default:
+ Assert( false, weapon.GetWeaponClassName() + " needs to be updated to support cooldown_type setting" )
+ break
+ }
+ }
+ else
+ {
+ if ( !PlayerHasPassive( attacker, ePassives.PAS_CDR_ON_KILL ) && !weaponMods.contains( "tactical_cdr_on_kill" ) )
+ return
+
+ entity weapon = attacker.GetOffhandWeapon( OFFHAND_LEFT )
+
+ switch ( GetWeaponInfoFileKeyField_Global( weapon.GetWeaponClassName(), "cooldown_type" ) )
+ {
+ case "grapple":
+ attacker.SetSuitGrapplePower( attacker.GetSuitGrapplePower() + 25 )
+ break
+
+ case "ammo":
+ case "ammo_instant":
+ case "ammo_deployed":
+ case "ammo_timed":
+ int maxAmmo = weapon.GetWeaponPrimaryClipCountMax()
+ weapon.SetWeaponPrimaryClipCountNoRegenReset( min( maxAmmo, weapon.GetWeaponPrimaryClipCount() + ( maxAmmo / 4 ) ) )
+ break
+
+ case "chargeFrac":
+ weapon.SetWeaponChargeFraction( max( 0, weapon.GetWeaponChargeFraction() - 0.25 ) )
+ break
+
+ // case "mp_ability_ground_slam":
+ // break
+
+ default:
+ Assert( false, weapon.GetWeaponClassName() + " needs to be updated to support cooldown_type setting" )
+ break
+ }
+ }
+}
+
+void function DisableWeapons( entity player, array<string> excludeNames )
+{
+ array<entity> weapons = GetPlayerWeapons( player, excludeNames )
+ foreach ( weapon in weapons )
+ weapon.AllowUse( false )
+}
+
+void function EnableWeapons( entity player, array<string> excludeNames )
+{
+ array<entity> weapons = GetPlayerWeapons( player, excludeNames )
+ foreach ( weapon in weapons )
+ weapon.AllowUse( true )
+}
+
+array<entity> function GetPlayerWeapons( entity player, array<string> excludeNames )
+{
+ array<entity> weapons = player.GetMainWeapons()
+ weapons.extend( player.GetOffhandWeapons() )
+
+ for ( int idx = weapons.len() - 1; idx > 0; idx-- )
+ {
+ foreach ( excludeName in excludeNames )
+ {
+ if ( weapons[idx].GetWeaponClassName() == excludeName )
+ weapons.remove( idx )
+ }
+ }
+
+ return weapons
+}
+
+void function WeaponAttackWave( entity ent, int projectileCount, entity inflictor, vector pos, vector dir, bool functionref( entity, int, entity, entity, vector, vector, int ) waveFunc )
+{
+ ent.EndSignal( "OnDestroy" )
+
+ entity weapon
+ entity projectile
+ int maxCount
+ float step
+ entity owner
+ int damageNearValueTitanArmor
+ int count = 0
+ array<vector> positions = []
+ vector lastDownPos
+ bool firstTrace = true
+
+ dir = <dir.x, dir.y, 0.0>
+ dir = Normalize( dir )
+ vector angles = VectorToAngles( dir )
+
+ if ( ent.IsProjectile() )
+ {
+ projectile = ent
+ string chargedPrefix = ""
+ if ( ent.proj.isChargedShot )
+ chargedPrefix = "charge_"
+
+ maxCount = expect int( ent.ProjectileGetWeaponInfoFileKeyField( chargedPrefix + "wave_max_count" ) )
+ step = expect float( ent.ProjectileGetWeaponInfoFileKeyField( chargedPrefix + "wave_step_dist" ) )
+ owner = ent.GetOwner()
+ damageNearValueTitanArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.damage_near_value_titanarmor )
+ }
+ else
+ {
+ weapon = ent
+ maxCount = expect int( ent.GetWeaponInfoFileKeyField( "wave_max_count" ) )
+ step = expect float( ent.GetWeaponInfoFileKeyField( "wave_step_dist" ) )
+ owner = ent.GetWeaponOwner()
+ damageNearValueTitanArmor = weapon.GetWeaponSettingInt( eWeaponVar.damage_near_value_titanarmor )
+ }
+
+ owner.EndSignal( "OnDestroy" )
+
+ for ( int i = 0; i < maxCount; i++ )
+ {
+ vector newPos = pos + dir * step
+
+ vector traceStart = pos
+ vector traceEndUnder = newPos
+ vector traceEndOver = newPos
+
+ if ( !firstTrace )
+ {
+ traceStart = lastDownPos + <0.0, 0.0, 80.0 >
+ traceEndUnder = <newPos.x, newPos.y, traceStart.z - 40.0 >
+ traceEndOver = <newPos.x, newPos.y, traceStart.z + step * 0.57735056839> // The over height is to cover the case of a sheer surface that then continues gradually upwards (like mp_box)
+ }
+ firstTrace = false
+
+ VortexBulletHit ornull vortexHit = VortexBulletHitCheck( owner, traceStart, traceEndOver )
+ if ( vortexHit )
+ {
+ expect VortexBulletHit( vortexHit )
+ entity vortexWeapon = vortexHit.vortex.GetOwnerWeapon()
+
+ if ( vortexWeapon && vortexWeapon.GetWeaponClassName() == "mp_titanweapon_vortex_shield" )
+ VortexDrainedByImpact( vortexWeapon, weapon, projectile, null ) // drain the vortex shield
+ else if ( IsVortexSphere( vortexHit.vortex ) )
+ VortexSphereDrainHealthForDamage( vortexHit.vortex, damageNearValueTitanArmor )
+
+ WaitFrame()
+ continue
+ }
+
+ //DebugDrawLine( traceStart, traceEndUnder, 0, 255, 0, true, 25.0 )
+ array ignoreArray = []
+ if ( IsValid( inflictor ) && inflictor.GetOwner() != null )
+ ignoreArray.append( inflictor.GetOwner() )
+
+ TraceResults forwardTrace = TraceLine( traceStart, traceEndUnder, ignoreArray, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS )
+ if ( forwardTrace.fraction == 1.0 )
+ {
+ //DebugDrawLine( forwardTrace.endPos, forwardTrace.endPos + <0.0, 0.0, -1000.0>, 255, 0, 0, true, 25.0 )
+ TraceResults downTrace = TraceLine( forwardTrace.endPos, forwardTrace.endPos + <0.0, 0.0, -1000.0>, ignoreArray, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS )
+ if ( downTrace.fraction == 1.0 )
+ break
+
+ entity movingGeo = null
+ if ( downTrace.hitEnt && downTrace.hitEnt.HasPusherRootParent() && !downTrace.hitEnt.IsMarkedForDeletion() )
+ movingGeo = downTrace.hitEnt
+
+ if ( !waveFunc( ent, projectileCount, inflictor, movingGeo, downTrace.endPos, angles, i ) )
+ return
+
+ lastDownPos = downTrace.endPos
+ pos = forwardTrace.endPos
+
+ WaitFrame()
+ continue
+ }
+ else
+ {
+ if ( IsValid( forwardTrace.hitEnt ) && (StatusEffect_Get( forwardTrace.hitEnt, eStatusEffect.pass_through_amps_weapon ) > 0) && !CheckPassThroughDir( forwardTrace.hitEnt, forwardTrace.surfaceNormal, forwardTrace.endPos ) )
+ break;
+ }
+
+ TraceResults upwardTrace = TraceLine( traceStart, traceEndOver, ignoreArray, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS )
+ //DebugDrawLine( traceStart, traceEndOver, 0, 0, 255, true, 25.0 )
+ if ( upwardTrace.fraction < 1.0 )
+ {
+ if ( IsValid( upwardTrace.hitEnt ) )
+ {
+ if ( upwardTrace.hitEnt.IsWorld() || upwardTrace.hitEnt.IsPlayer() || upwardTrace.hitEnt.IsNPC() )
+ break
+ }
+ }
+ else
+ {
+ TraceResults downTrace = TraceLine( upwardTrace.endPos, upwardTrace.endPos + <0.0, 0.0, -1000.0>, ignoreArray, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS )
+ if ( downTrace.fraction == 1.0 )
+ break
+
+ entity movingGeo = null
+ if ( downTrace.hitEnt && downTrace.hitEnt.HasPusherRootParent() && !downTrace.hitEnt.IsMarkedForDeletion() )
+ movingGeo = downTrace.hitEnt
+
+ if ( !waveFunc( ent, projectileCount, inflictor, movingGeo, downTrace.endPos, angles, i ) )
+ return
+
+ lastDownPos = downTrace.endPos
+ pos = forwardTrace.endPos
+ }
+
+ WaitFrame()
+ }
+}
+
+void function AddActiveThermiteBurn( entity ent )
+{
+ AddToScriptManagedEntArray( file.activeThermiteBurnsManagedEnts, ent )
+}
+
+array<entity> function GetActiveThermiteBurnsWithinRadius( vector origin, float dist, team = TEAM_ANY )
+{
+ return GetScriptManagedEntArrayWithinCenter( file.activeThermiteBurnsManagedEnts, team, origin, dist )
+}
+
+void function EMP_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ Elecriticy_DamagedPlayerOrNPC( ent, damageInfo, FX_EMP_BODY_HUMAN, FX_EMP_BODY_TITAN, EMP_SEVERITY_SLOWTURN, EMP_SEVERITY_SLOWMOVE )
+}
+
+void function VanguardEnergySiphon_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( IsValid( attacker ) && attacker.GetTeam() == ent.GetTeam() )
+ return
+
+ Elecriticy_DamagedPlayerOrNPC( ent, damageInfo, FX_VANGUARD_ENERGY_BODY_HUMAN, FX_VANGUARD_ENERGY_BODY_TITAN, LASER_STUN_SEVERITY_SLOWTURN, LASER_STUN_SEVERITY_SLOWMOVE )
+}
+
+void function Elecriticy_DamagedPlayerOrNPC( entity ent, var damageInfo, asset humanFx, asset titanFx, float slowTurn, float slowMove )
+{
+ if ( !IsValid( ent ) )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ local inflictor = DamageInfo_GetInflictor( damageInfo )
+ if( !IsValid( inflictor ) )
+ return
+
+ // Do electrical effect on this ent that everyone can see if they are a titan
+ string tag = ""
+ asset effect
+
+ if ( ent.IsTitan() )
+ {
+ tag = "exp_torso_front"
+ effect = titanFx
+ }
+ else if ( IsStalker( ent ) || IsSpectre( ent ) )
+ {
+ tag = "CHESTFOCUS"
+ effect = humanFx
+ if ( !ent.ContextAction_IsActive() && IsAlive( ent ) && ent.IsInterruptable() )
+ {
+ ent.Anim_ScriptedPlayActivityByName( "ACT_STUNNED", true, 0.1 )
+ }
+ }
+ else if ( IsSuperSpectre( ent ) )
+ {
+ tag = "CHESTFOCUS"
+ effect = humanFx
+
+ if ( ent.GetParent() == null && !ent.ContextAction_IsActive() && IsAlive( ent ) && ent.IsInterruptable() )
+ {
+ ent.Anim_ScriptedPlayActivityByName( "ACT_STUNNED", true, 0.1 )
+ }
+ }
+ else if ( IsGrunt( ent ) )
+ {
+ tag = "CHESTFOCUS"
+ effect = humanFx
+ if ( !ent.ContextAction_IsActive() && IsAlive( ent ) && ent.IsInterruptable() )
+ {
+ ent.Anim_ScriptedPlayActivityByName( "ACT_STUNNED", true, 0.1 )
+ ent.EnableNPCFlag( NPC_PAIN_IN_SCRIPTED_ANIM )
+ }
+ }
+ else if ( IsPilot( ent ) )
+ {
+ tag = "CHESTFOCUS"
+ effect = humanFx
+ }
+ else if ( IsAirDrone( ent ) )
+ {
+ if ( GetDroneType( ent ) == "drone_type_marvin" )
+ return
+ tag = "HEADSHOT"
+ effect = humanFx
+ thread NpcEmpRebootPrototype( ent, damageInfo, humanFx, titanFx )
+ }
+ else if ( IsGunship( ent ) )
+ {
+ tag = "ORIGIN"
+ effect = titanFx
+ thread NpcEmpRebootPrototype( ent, damageInfo, humanFx, titanFx )
+ }
+
+ ent.Signal( "ArcStunned" )
+
+ if ( tag != "" )
+ {
+ local inflictor = DamageInfo_GetInflictor( damageInfo )
+ Assert( !(inflictor instanceof CEnvExplosion) )
+ if ( IsValid( inflictor ) )
+ {
+ float duration = EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MAX
+ if ( inflictor instanceof CBaseGrenade )
+ {
+ local entCenter = ent.GetWorldSpaceCenter()
+ local dist = Distance( DamageInfo_GetDamagePosition( damageInfo ), entCenter )
+ local damageRadius = inflictor.GetDamageRadius()
+ duration = GraphCapped( dist, damageRadius * 0.5, damageRadius, EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MIN, EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MAX )
+ }
+ thread EMP_FX( effect, ent, tag, duration )
+ }
+ }
+
+ if ( StatusEffect_Get( ent, eStatusEffect.destroyed_by_emp ) )
+ DamageInfo_SetDamage( damageInfo, ent.GetHealth() )
+
+ // Don't do arc beams to entities that are on the same team... except the owner
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( IsValid( attacker ) && attacker.GetTeam() == ent.GetTeam() && attacker != ent )
+ return
+
+ if ( ent.IsPlayer() )
+ {
+ thread EMPGrenade_EffectsPlayer( ent, damageInfo )
+ }
+ else if ( ent.IsTitan() )
+ {
+ EMPGrenade_AffectsShield( ent, damageInfo )
+ #if MP
+ GiveEMPStunStatusEffects( ent, 2.5, 1.0, slowTurn, slowMove )
+ #endif
+ thread EMPGrenade_AffectsAccuracy( ent )
+ }
+ else if ( ent.IsMechanical() )
+ {
+ #if MP
+ GiveEMPStunStatusEffects( ent, 2.5, 1.0, slowTurn, slowMove )
+ DamageInfo_ScaleDamage( damageInfo, 2.05 )
+ #endif
+ }
+ else if ( ent.IsHuman() )
+ {
+ #if MP
+ DamageInfo_ScaleDamage( damageInfo, 0.99 )
+ #endif
+ }
+
+ if ( inflictor instanceof CBaseGrenade )
+ {
+ if ( !ent.IsPlayer() || ent.IsTitan() ) //Beam should hit cloaked targets, when cloak is updated make IsCloaked() function.
+ EMPGrenade_ArcBeam( DamageInfo_GetDamagePosition( damageInfo ), ent )
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// HACK: might make sense to move this to code
+void function NpcEmpRebootPrototype( entity npc, var damageInfo, asset humanFx, asset titanFx )
+{
+ if ( !IsValid( npc ) )
+ return
+
+ npc.EndSignal( "OnDeath" )
+ npc.EndSignal( "OnDestroy" )
+
+ if ( !( "rebooting" in npc.s ) )
+ npc.s.rebooting <- null
+
+ if ( npc.s.rebooting ) // npc already knocked down and in rebooting process
+ return
+
+ float rebootTime
+ vector groundPos
+ local nearestNode
+ local neighborNodes
+ local groundNodePos
+ local origin = npc.GetOrigin()
+ local startOrigin = origin
+ local classname = npc.GetClassName()
+ local soundPowerDown
+ local soundPowerUp
+
+ //------------------------------------------------------
+ // Custom stuff depending on AI type
+ //------------------------------------------------------
+ switch ( classname )
+ {
+ case "npc_drone":
+ soundPowerDown = "Drone_Power_Down"
+ soundPowerUp = "Drone_Power_On"
+ rebootTime = DRONE_REBOOT_TIME
+ break
+ case "npc_gunship":
+ soundPowerDown = "Gunship_Power_Down"
+ soundPowerUp = "Gunship_Power_On"
+ rebootTime = GUNSHIP_REBOOT_TIME
+ break
+ default:
+ Assert( 0, "Unhandled npc type: " + classname )
+
+ }
+
+ //------------------------------------------------------
+ // NPC stunned and is rebooting
+ //------------------------------------------------------
+ npc.Signal( "OnStunned" )
+ npc.s.rebooting = true
+
+
+ //TODO: make drone/gunship slowly drift to the ground while rebooting
+ /*
+ groundPos = OriginToGround( origin )
+ groundPos += Vector( 0, 0, 32 )
+
+
+ //DebugDrawLine(origin, groundPos, 255, 0, 0, true, 15 )
+
+ //thread AssaultOrigin( drone, groundPos, 16 )
+ //thread PlayAnim( drone, "idle" )
+ */
+
+
+ thread EmpRebootFxPrototype( npc, humanFx, titanFx )
+ npc.EnableNPCFlag( NPC_IGNORE_ALL )
+ npc.SetNoTarget( true )
+ npc.EnableNPCFlag( NPC_DISABLE_SENSING ) // don't do traces to look for enemies or players
+
+ if ( IsAttackDrone( npc ) )
+ npc.SetAttackMode( false )
+
+ EmitSoundOnEntity( npc, soundPowerDown )
+
+ wait rebootTime
+
+ EmitSoundOnEntity( npc, soundPowerUp )
+ npc.DisableNPCFlag( NPC_IGNORE_ALL )
+ npc.SetNoTarget( false )
+ npc.DisableNPCFlag( NPC_DISABLE_SENSING ) // don't do traces to look for enemies or players
+
+ if ( IsAttackDrone( npc ) )
+ npc.SetAttackMode( true )
+
+ npc.s.rebooting = false
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// HACK: might make sense to move this to code
+function EmpRebootFxPrototype( npc, asset humanFx, asset titanFx )
+{
+ expect entity( npc )
+
+ if ( !IsValid( npc ) )
+ return
+
+ npc.EndSignal( "OnDeath" )
+ npc.EndSignal( "OnDestroy" )
+
+ string classname = npc.GetClassName()
+ vector origin
+ float delayDuration
+ entity fxHandle
+ asset fxEMPdamage
+ string fxTag
+ float rebootTime
+ string soundEMPdamage
+
+ //------------------------------------------------------
+ // Custom stuff depending on AI type
+ //------------------------------------------------------
+ switch ( classname )
+ {
+ case "npc_drone":
+ if ( GetDroneType( npc ) == "drone_type_marvin" )
+ return
+ fxEMPdamage = humanFx
+ fxTag = "HEADSHOT"
+ rebootTime = DRONE_REBOOT_TIME
+ soundEMPdamage = "Titan_Blue_Electricity_Cloud"
+ break
+ case "npc_gunship":
+ fxEMPdamage = titanFx
+ fxTag = "ORIGIN"
+ rebootTime = GUNSHIP_REBOOT_TIME
+ soundEMPdamage = "Titan_Blue_Electricity_Cloud"
+ break
+ default:
+ Assert( 0, "Unhandled npc type: " + classname )
+
+ }
+
+ //------------------------------------------------------
+ // Play Fx/Sound till reboot finishes
+ //------------------------------------------------------
+ fxHandle = ClientStylePlayFXOnEntity( fxEMPdamage, npc, fxTag, rebootTime )
+ EmitSoundOnEntity( npc, soundEMPdamage )
+
+ while ( npc.s.rebooting == true )
+ {
+ delayDuration = RandomFloatRange( 0.4, 1.2 )
+ origin = npc.GetOrigin()
+
+
+ EmitSoundAtPosition( npc.GetTeam(), origin, SOUND_EMP_REBOOT_SPARKS )
+ PlayFX( FX_EMP_REBOOT_SPARKS, origin )
+ PlayFX( FX_EMP_REBOOT_SPARKS, origin )
+
+ OnThreadEnd(
+ function() : ( fxHandle, npc, soundEMPdamage )
+ {
+ if ( IsValid( fxHandle ) )
+ fxHandle.Fire( "StopPlayEndCap" )
+ if ( IsValid( npc ) )
+ StopSoundOnEntity( npc, soundEMPdamage )
+ }
+ )
+
+ wait ( delayDuration )
+ }
+}
+
+function EMP_FX( asset effect, entity ent, string tag, float duration )
+{
+ if ( !IsAlive( ent ) )
+ return
+
+ ent.Signal( "EMP_FX" )
+ ent.EndSignal( "OnDestroy" )
+ ent.EndSignal( "OnDeath" )
+ ent.EndSignal( "StartPhaseShift" )
+ ent.EndSignal( "EMP_FX" )
+
+ bool isPlayer = ent.IsPlayer()
+
+ int fxId = GetParticleSystemIndex( effect )
+ int attachId = ent.LookupAttachment( tag )
+
+ entity fxHandle = StartParticleEffectOnEntity_ReturnEntity( ent, fxId, FX_PATTACH_POINT_FOLLOW, attachId )
+ fxHandle.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY
+ fxHandle.SetOwner( ent )
+
+ OnThreadEnd(
+ function() : ( fxHandle, ent )
+ {
+ if ( IsValid( fxHandle ) )
+ {
+ EffectStop( fxHandle )
+ }
+
+ if ( IsValid( ent ) )
+ StopSoundOnEntity( ent, "Titan_Blue_Electricity_Cloud" )
+ }
+ )
+
+ if ( !isPlayer )
+ {
+ EmitSoundOnEntity( ent, "Titan_Blue_Electricity_Cloud" )
+ wait duration
+ }
+ else
+ {
+ EmitSoundOnEntityExceptToPlayer( ent, ent, "Titan_Blue_Electricity_Cloud" )
+
+ var endTime = Time() + duration
+ bool effectsActive = true
+ while( endTime > Time() )
+ {
+ if ( ent.IsPhaseShifted() )
+ {
+ if ( effectsActive )
+ {
+ effectsActive = false
+ if ( IsValid( fxHandle ) )
+ EffectSleep( fxHandle )
+
+ if ( IsValid( ent ) )
+ StopSoundOnEntity( ent, "Titan_Blue_Electricity_Cloud" )
+ }
+ }
+ else if ( effectsActive == false )
+ {
+ EffectWake( fxHandle )
+ EmitSoundOnEntityExceptToPlayer( ent, ent, "Titan_Blue_Electricity_Cloud" )
+ effectsActive = true
+ }
+
+ WaitFrame()
+ }
+ }
+}
+
+function EMPGrenade_AffectsShield( entity titan, damageInfo )
+{
+ int shieldHealth = titan.GetTitanSoul().GetShieldHealth()
+ int shieldDamage = int( titan.GetTitanSoul().GetShieldHealthMax() * 0.5 )
+
+ titan.GetTitanSoul().SetShieldHealth( maxint( 0, shieldHealth - shieldDamage ) )
+
+ // attacker took down titan shields
+ if ( shieldHealth && !titan.GetTitanSoul().GetShieldHealth() )
+ {
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( attacker && attacker.IsPlayer() )
+ EmitSoundOnEntityOnlyToPlayer( attacker, attacker, "titan_energyshield_down" )
+ }
+}
+
+function EMPGrenade_AffectsAccuracy( npcTitan )
+{
+ npcTitan.EndSignal( "OnDestroy" )
+
+ npcTitan.kv.AccuracyMultiplier = 0.5
+ wait EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MAX
+ npcTitan.kv.AccuracyMultiplier = 1.0
+}
+
+
+function EMPGrenade_EffectsPlayer( entity player, damageInfo )
+{
+ player.Signal( "OnEMPPilotHit" )
+ player.EndSignal( "OnEMPPilotHit" )
+
+ if ( player.IsPhaseShifted() )
+ return
+
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ local dist = Distance( DamageInfo_GetDamagePosition( damageInfo ), player.GetWorldSpaceCenter() )
+ local damageRadius = 128
+ if ( inflictor instanceof CBaseGrenade )
+ damageRadius = inflictor.GetDamageRadius()
+ float frac = GraphCapped( dist, damageRadius * 0.5, damageRadius, 1.0, 0.0 )
+ local strength = EMP_GRENADE_PILOT_SCREEN_EFFECTS_MIN + ( ( EMP_GRENADE_PILOT_SCREEN_EFFECTS_MAX - EMP_GRENADE_PILOT_SCREEN_EFFECTS_MIN ) * frac )
+ float fadeoutDuration = EMP_GRENADE_PILOT_SCREEN_EFFECTS_FADE * frac
+ float duration = EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MIN + ( ( EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MAX - EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MIN ) * frac ) - fadeoutDuration
+ local origin = inflictor.GetOrigin()
+
+ int dmgSource = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( dmgSource == eDamageSourceId.mp_weapon_proximity_mine || dmgSource == eDamageSourceId.mp_titanweapon_stun_laser )
+ {
+ strength *= 0.1
+ }
+
+ if ( player.IsTitan() )
+ {
+ // Hit player should do EMP screen effects locally
+ Remote_CallFunction_Replay( player, "ServerCallback_TitanCockpitEMP", duration )
+
+ EMPGrenade_AffectsShield( player, damageInfo )
+
+ Remote_CallFunction_Replay( player, "ServerCallback_TitanEMP", strength, duration, fadeoutDuration )
+ }
+ else
+ {
+ if ( IsCloaked( player ) )
+ player.SetCloakFlicker( 0.5, duration )
+
+ // duration = 0
+ // fadeoutDuration = 0
+
+ StatusEffect_AddTimed( player, eStatusEffect.emp, strength, duration, fadeoutDuration )
+ //DamageInfo_SetDamage( damageInfo, 0 )
+ }
+
+ GiveEMPStunStatusEffects( player, (duration + fadeoutDuration), fadeoutDuration)
+}
+
+function EMPGrenade_ArcBeam( grenadePos, ent )
+{
+ if ( !ent.IsPlayer() && !ent.IsNPC() )
+ return
+
+ Assert( IsValid( ent ) )
+ local lifeDuration = 0.5
+
+ // Control point sets the end position of the effect
+ entity cpEnd = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpEnd, UniqueString( "emp_grenade_beam_cpEnd" ) )
+ cpEnd.SetOrigin( grenadePos )
+ DispatchSpawn( cpEnd )
+
+ entity zapBeam = CreateEntity( "info_particle_system" )
+ zapBeam.kv.cpoint1 = cpEnd.GetTargetName()
+ zapBeam.SetValueForEffectNameKey( EMP_GRENADE_BEAM_EFFECT )
+ zapBeam.kv.start_active = 0
+ zapBeam.SetOrigin( ent.GetWorldSpaceCenter() )
+ if ( !ent.IsMarkedForDeletion() ) // TODO: This is a hack for shipping. Should not be parenting to deleted entities
+ {
+ zapBeam.SetParent( ent, "", true, 0.0 )
+ }
+
+ DispatchSpawn( zapBeam )
+
+ zapBeam.Fire( "Start" )
+ zapBeam.Fire( "StopPlayEndCap", "", lifeDuration )
+ zapBeam.Kill_Deprecated_UseDestroyInstead( lifeDuration )
+ cpEnd.Kill_Deprecated_UseDestroyInstead( lifeDuration )
+}
+
+void function GetWeaponDPS( bool vsTitan = false )
+{
+ entity player = GetPlayerArray()[0]
+ entity weapon = player.GetActiveWeapon()
+
+ local fire_rate = weapon.GetWeaponInfoFileKeyField( "fire_rate" )
+ local burst_fire_count = weapon.GetWeaponInfoFileKeyField( "burst_fire_count" )
+ local burst_fire_delay = weapon.GetWeaponInfoFileKeyField( "burst_fire_delay" )
+
+ local damage_near_value = weapon.GetWeaponInfoFileKeyField( "damage_near_value" )
+ local damage_far_value = weapon.GetWeaponInfoFileKeyField( "damage_far_value" )
+
+ if ( vsTitan )
+ {
+ damage_near_value = weapon.GetWeaponInfoFileKeyField( "damage_near_value_titanarmor" )
+ damage_far_value = weapon.GetWeaponInfoFileKeyField( "damage_far_value_titanarmor" )
+ }
+
+ if ( burst_fire_count )
+ {
+ local timePerShot = 1 / fire_rate
+ local timePerBurst = (timePerShot * burst_fire_count) + burst_fire_delay
+ local burstPerSecond = 1 / timePerBurst
+
+ printt( timePerBurst )
+
+ printt( "DPS Near", (burstPerSecond * burst_fire_count) * damage_near_value )
+ printt( "DPS Far ", (burstPerSecond * burst_fire_count) * damage_far_value )
+ }
+ else
+ {
+ printt( "DPS Near", fire_rate * damage_near_value )
+ printt( "DPS Far ", fire_rate * damage_far_value )
+ }
+}
+
+
+void function GetTTK( string weaponRef, float health = 100.0 )
+{
+ local fire_rate = GetWeaponInfoFileKeyField_Global( weaponRef, "fire_rate" ).tofloat()
+ local burst_fire_count = GetWeaponInfoFileKeyField_Global( weaponRef, "burst_fire_count" )
+ if ( burst_fire_count != null )
+ burst_fire_count = burst_fire_count.tofloat()
+
+ local burst_fire_delay = GetWeaponInfoFileKeyField_Global( weaponRef, "burst_fire_delay" )
+ if ( burst_fire_delay != null )
+ burst_fire_delay = burst_fire_delay.tofloat()
+
+ local damage_near_value = GetWeaponInfoFileKeyField_Global( weaponRef, "damage_near_value" ).tointeger()
+ local damage_far_value = GetWeaponInfoFileKeyField_Global( weaponRef, "damage_far_value" ).tointeger()
+
+ local nearBodyShots = ceil( health / damage_near_value ) - 1
+ local farBodyShots = ceil( health / damage_far_value ) - 1
+
+ local delayAdd = 0
+ if ( burst_fire_count && burst_fire_count < nearBodyShots )
+ delayAdd += burst_fire_delay
+
+ printt( "TTK Near", (nearBodyShots * (1 / fire_rate)) + delayAdd, " (" + (nearBodyShots + 1) + ")" )
+
+
+ delayAdd = 0
+ if ( burst_fire_count && burst_fire_count < farBodyShots )
+ delayAdd += burst_fire_delay
+
+ printt( "TTK Far ", (farBodyShots * (1 / fire_rate)) + delayAdd, " (" + (farBodyShots + 1) + ")" )
+}
+
+array<string> function GetWeaponModsFromDamageInfo( var damageInfo )
+{
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+
+ if ( IsValid( weapon ) )
+ {
+ return weapon.GetMods()
+ }
+ else if ( IsValid( inflictor ) )
+ {
+ if ( "weaponMods" in inflictor.s && inflictor.s.weaponMods )
+ {
+ array<string> temp
+ foreach ( string mod in inflictor.s.weaponMods )
+ {
+ temp.append( mod )
+ }
+
+ return temp
+ }
+ else if( inflictor.IsProjectile() )
+ return inflictor.ProjectileGetMods()
+ else if ( damageType & DF_EXPLOSION && inflictor.IsPlayer() && IsValid( inflictor.GetActiveWeapon() ) )
+ return inflictor.GetActiveWeapon().GetMods()
+ //Hack - Splash damage doesn't pass mod weapon through. This only works under the assumption that offhand weapons don't have mods.
+ }
+ return []
+}
+
+void function OnPlayerGetsNewPilotLoadout( entity player, PilotLoadoutDef loadout )
+{
+ if ( GetCurrentPlaylistVarInt( "featured_mode_amped_tacticals", 0 ) >= 1 )
+ {
+ player.GiveExtraWeaponMod( "amped_tacticals" )
+ }
+
+ if ( GetCurrentPlaylistVarInt( "featured_mode_all_grapple", 0 ) >= 1 )
+ {
+ player.GiveExtraWeaponMod( "all_grapple" )
+ }
+
+ if ( GetCurrentPlaylistVarInt( "featured_mode_all_phase", 0 ) >= 1 )
+ {
+ player.GiveExtraWeaponMod( "all_phase" )
+ }
+
+ SetPlayerCooldowns( player )
+}
+
+void function SetPlayerCooldowns( entity player )
+{
+ if ( player.IsTitan() )
+ return
+
+ array<int> offhandIndices = [ OFFHAND_LEFT, OFFHAND_RIGHT ]
+
+ foreach ( index in offhandIndices )
+ {
+ float lastUseTime = player.p.lastPilotOffhandUseTime[ index ]
+ float lastChargeFrac = player.p.lastPilotOffhandChargeFrac[ index ]
+ float lastClipFrac = player.p.lastPilotClipFrac[ index ]
+
+ if ( lastUseTime >= 0.0 )
+ {
+ entity weapon = player.GetOffhandWeapon( index )
+ if ( !IsValid( weapon ) )
+ continue
+
+ string weaponClassName = weapon.GetWeaponClassName()
+
+ switch ( GetWeaponInfoFileKeyField_Global( weaponClassName, "cooldown_type" ) )
+ {
+ case "grapple":
+ // GetPlayerSettingsField isn't working for moddable fields? - Bug 129567
+ float powerRequired = 100.0 // GetPlayerSettingsField( "grapple_power_required" )
+ float regenRefillDelay = 3.0 // GetPlayerSettingsField( "grapple_power_regen_delay" )
+ float regenRefillRate = 5.0 // GetPlayerSettingsField( "grapple_power_regen_rate" )
+ float suitPowerToRestore = powerRequired - player.p.lastSuitPower
+ float regenRefillTime = suitPowerToRestore / regenRefillRate
+
+ float regenStartTime = lastUseTime + regenRefillDelay
+
+ float newSuitPower = GraphCapped( Time() - regenStartTime, 0.0, regenRefillTime, player.p.lastSuitPower, powerRequired )
+
+ player.SetSuitGrapplePower( newSuitPower )
+ break
+
+ case "ammo":
+ case "ammo_instant":
+ case "ammo_deployed":
+ case "ammo_timed":
+ int maxAmmo = weapon.GetWeaponPrimaryClipCountMax()
+ float fireDuration = weapon.GetWeaponSettingFloat( eWeaponVar.fire_duration )
+ float regenRefillDelay = weapon.GetWeaponSettingFloat( eWeaponVar.regen_ammo_refill_start_delay )
+ float regenRefillRate = weapon.GetWeaponSettingFloat( eWeaponVar.regen_ammo_refill_rate )
+ int startingClipCount = int( lastClipFrac * maxAmmo )
+ int ammoToRestore = maxAmmo - startingClipCount
+ float regenRefillTime = ammoToRestore / regenRefillRate
+
+ float regenStartTime = lastUseTime + fireDuration + regenRefillDelay
+
+ int newAmmo = int( GraphCapped( Time() - regenStartTime, 0.0, regenRefillTime, startingClipCount, maxAmmo ) )
+
+ weapon.SetWeaponPrimaryClipCountAbsolute( newAmmo )
+ break
+
+ case "chargeFrac":
+ float chargeCooldownDelay = weapon.GetWeaponSettingFloat( eWeaponVar.charge_cooldown_delay )
+ float chargeCooldownTime = weapon.GetWeaponSettingFloat( eWeaponVar.charge_cooldown_time )
+ float regenRefillTime = lastChargeFrac * chargeCooldownTime
+ float regenStartTime = lastUseTime + chargeCooldownDelay
+
+ float newCharge = GraphCapped( Time() - regenStartTime, 0.0, regenRefillTime, lastChargeFrac, 0.0 )
+
+ weapon.SetWeaponChargeFraction( newCharge )
+ break
+
+ default:
+ printt( weaponClassName + " needs to be updated to support cooldown_type setting" )
+ break
+ }
+ }
+ }
+}
+
+void function ResetPlayerCooldowns( entity player )
+{
+ if ( player.IsTitan() )
+ return
+
+ array<int> offhandIndices = [ OFFHAND_LEFT, OFFHAND_RIGHT ]
+
+ foreach ( index in offhandIndices )
+ {
+ float lastUseTime = -99.0//player.p.lastPilotOffhandUseTime[ index ]
+ float lastChargeFrac = -1.0//player.p.lastPilotOffhandChargeFrac[ index ]
+ float lastClipFrac = 1.0//player.p.lastPilotClipFrac[ index ]
+
+ entity weapon = player.GetOffhandWeapon( index )
+ if ( !IsValid( weapon ) )
+ continue
+
+ string weaponClassName = weapon.GetWeaponClassName()
+
+ switch ( GetWeaponInfoFileKeyField_Global( weaponClassName, "cooldown_type" ) )
+ {
+ case "grapple":
+ // GetPlayerSettingsField isn't working for moddable fields? - Bug 129567
+ float powerRequired = 100.0 // GetPlayerSettingsField( "grapple_power_required" )
+ player.SetSuitGrapplePower( powerRequired )
+ break
+
+ case "ammo":
+ case "ammo_instant":
+ case "ammo_deployed":
+ case "ammo_timed":
+ int maxAmmo = weapon.GetWeaponPrimaryClipCountMax()
+ weapon.SetWeaponPrimaryClipCountAbsolute( maxAmmo )
+ break
+
+ case "chargeFrac":
+ weapon.SetWeaponChargeFraction( 1.0 )
+ break
+
+ default:
+ printt( weaponClassName + " needs to be updated to support cooldown_type setting" )
+ break
+ }
+ }
+}
+
+void function OnPlayerKilled( entity player, entity attacker, var damageInfo )
+{
+ StoreOffhandData( player )
+}
+
+void function StoreOffhandData( entity player, bool waitEndFrame = true )
+{
+ thread StoreOffhandDataThread( player, waitEndFrame )
+}
+
+void function StoreOffhandDataThread( entity player, bool waitEndFrame )
+{
+ if ( !IsValid( player ) )
+ return
+
+ player.EndSignal( "OnDestroy" )
+
+ if ( waitEndFrame )
+ WaitEndFrame() // Need to WaitEndFrame so clip counts can be updated if player is dying the same frame
+
+ array<int> offhandIndices = [ OFFHAND_LEFT, OFFHAND_RIGHT ]
+
+ // Reset all values for full cooldown
+ player.p.lastSuitPower = 0.0
+
+ foreach ( index in offhandIndices )
+ {
+ player.p.lastPilotOffhandChargeFrac[ index ] = 1.0
+ player.p.lastPilotClipFrac[ index ] = 1.0
+
+ player.p.lastTitanOffhandChargeFrac[ index ] = 1.0
+ player.p.lastTitanClipFrac[ index ] = 1.0
+ }
+
+ if ( player.IsTitan() )
+ return
+
+ foreach ( index in offhandIndices )
+ {
+ entity weapon = player.GetOffhandWeapon( index )
+ if ( !IsValid( weapon ) )
+ continue
+
+ string weaponClassName = weapon.GetWeaponClassName()
+
+ switch ( GetWeaponInfoFileKeyField_Global( weaponClassName, "cooldown_type" ) )
+ {
+ case "grapple":
+ player.p.lastSuitPower = player.GetSuitGrapplePower()
+ break
+
+ case "ammo":
+ case "ammo_instant":
+ case "ammo_deployed":
+ case "ammo_timed":
+
+ if ( player.IsTitan() )
+ {
+ if ( !weapon.IsWeaponRegenDraining() )
+ player.p.lastTitanClipFrac[ index ] = min( 1.0, weapon.GetWeaponPrimaryClipCount() / float( weapon.GetWeaponPrimaryClipCountMax() ) ) //Was returning greater than one with extraweaponmod timing.
+ else
+ player.p.lastTitanClipFrac[ index ] = 0.0
+ }
+ else
+ {
+ if ( !weapon.IsWeaponRegenDraining() )
+ player.p.lastPilotClipFrac[ index ] = min( 1.0, weapon.GetWeaponPrimaryClipCount() / float( weapon.GetWeaponPrimaryClipCountMax() ) ) //Was returning greater than one with extraweaponmod timing.
+ else
+ player.p.lastPilotClipFrac[ index ] = 0.0
+ }
+ break
+
+ case "chargeFrac":
+ if ( player.IsTitan() )
+ player.p.lastTitanOffhandChargeFrac[ index ] = weapon.GetWeaponChargeFraction()
+ else
+ player.p.lastPilotOffhandChargeFrac[ index ] = weapon.GetWeaponChargeFraction()
+ break
+
+ default:
+ printt( weaponClassName + " needs to be updated to support cooldown_type setting" )
+ break
+ }
+ }
+}
+#endif // #if SERVER
+
+void function PlayerUsedOffhand( entity player, entity offhandWeapon )
+{
+ array<int> offhandIndices = [ OFFHAND_LEFT, OFFHAND_RIGHT, OFFHAND_ANTIRODEO, OFFHAND_EQUIPMENT ]
+
+ foreach ( index in offhandIndices )
+ {
+ entity weapon = player.GetOffhandWeapon( index )
+ if ( !IsValid( weapon ) )
+ continue
+
+ if ( weapon != offhandWeapon )
+ continue
+
+ #if SERVER
+ if ( player.IsTitan() )
+ player.p.lastTitanOffhandUseTime[ index ] = Time()
+ else
+ player.p.lastPilotOffhandUseTime[ index ] = Time()
+
+ #if MP
+ string weaponName = offhandWeapon.GetWeaponClassName()
+ if ( weaponName != "mp_ability_grapple" ) // handled in CodeCallback_OnGrapple // nope, it's not (?)
+ {
+ string category
+ float duration
+ if ( index == OFFHAND_EQUIPMENT && player.IsTitan() )
+ {
+ category = "core"
+ duration = -1
+ }
+ else
+ {
+ category = ""
+ duration = Time() - offhandWeapon.GetNextAttackAllowedTimeRaw()
+ }
+ PIN_PlayerAbility( player, category, weaponName, {}, duration )
+ }
+ #endif
+ #endif // SERVER
+
+ #if HAS_TITAN_TELEMETRY && CLIENT
+ ClTitanHints_ClearOffhandHint( index )
+ #endif
+
+ #if HAS_TITAN_TELEMETRY && SERVER
+ TitanHints_NotifyUsedOffhand( index )
+ #endif
+
+ return
+ }
+}
+
+RadiusDamageData function GetRadiusDamageDataFromProjectile( entity projectile, entity owner )
+{
+ RadiusDamageData radiusDamageData
+
+ radiusDamageData.explosionDamage = -1
+ radiusDamageData.explosionDamageHeavyArmor = -1
+
+ if ( owner.IsNPC() )
+ {
+ radiusDamageData.explosionDamage = projectile.GetProjectileWeaponSettingInt( eWeaponVar.npc_explosion_damage )
+ radiusDamageData.explosionDamageHeavyArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.npc_explosion_damage_heavy_armor )
+ }
+
+ if ( radiusDamageData.explosionDamage == -1 )
+ radiusDamageData.explosionDamage = projectile.GetProjectileWeaponSettingInt( eWeaponVar.explosion_damage )
+
+ if ( radiusDamageData.explosionDamageHeavyArmor == -1 )
+ radiusDamageData.explosionDamageHeavyArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.explosion_damage_heavy_armor )
+
+ radiusDamageData.explosionRadius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosionradius )
+ radiusDamageData.explosionInnerRadius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosion_inner_radius )
+
+ Assert( radiusDamageData.explosionRadius > 0, "Created RadiusDamageData with 0 radius" )
+ Assert( radiusDamageData.explosionDamage > 0 || radiusDamageData.explosionDamageHeavyArmor > 0, "Created RadiusDamageData with 0 damage" )
+ return radiusDamageData
+}
+
+#if SERVER
+void function Thermite_DamagePlayerOrNPCSounds( entity ent )
+{
+ if ( ent.IsTitan() )
+ {
+ if ( ent.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( ent, ent, "titan_thermiteburn_3p_vs_1p" )
+ EmitSoundOnEntityExceptToPlayer( ent, ent, "titan_thermiteburn_1p_vs_3p" )
+ }
+ else
+ {
+ EmitSoundOnEntity( ent, "titan_thermiteburn_1p_vs_3p" )
+ }
+ }
+ else
+ {
+ if ( ent.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( ent, ent, "flesh_thermiteburn_3p_vs_1p" )
+ EmitSoundOnEntityExceptToPlayer( ent, ent, "flesh_thermiteburn_1p_vs_3p" )
+ }
+ else
+ {
+ EmitSoundOnEntity( ent, "flesh_thermiteburn_1p_vs_3p" )
+ }
+ }
+}
+#endif
+
+#if SERVER
+void function RemoveThreatScopeColorStatusEffect( entity player )
+{
+ for ( int i = file.colorSwapStatusEffects.len() - 1; i >= 0; i-- )
+ {
+ entity owner = file.colorSwapStatusEffects[i].weaponOwner
+ if ( !IsValid( owner ) )
+ {
+ file.colorSwapStatusEffects.remove( i )
+ continue
+ }
+ if ( owner == player )
+ {
+ StatusEffect_Stop( player, file.colorSwapStatusEffects[i].statusEffectId )
+ file.colorSwapStatusEffects.remove( i )
+ }
+ }
+}
+
+void function AddThreatScopeColorStatusEffect( entity player )
+{
+ ColorSwapStruct info
+ info.weaponOwner = player
+ info.statusEffectId = StatusEffect_AddTimed( player, eStatusEffect.cockpitColor, COCKPIT_COLOR_THREAT, 100000, 0 )
+ file.colorSwapStatusEffects.append( info )
+}
+#endif